자바 언어·플랫폼 용어 사전
언어 기초immutability · 방어적 복사

불변 객체

생성 후 상태가 바뀌지 않는 객체. 그 자체로 스레드 안전하고 해시 키·캐시로 안전하다.

생성된 뒤 상태가 절대 바뀌지 않는 객체. 자바에서 가장 값싼 안전장치다.

왜 강력한가

  • ① 그 자체로 스레드 안전하다
    • 아무도 못 바꾸니 동기화가 필요 없다 — 락도 volatile 도 불필요
  • ② 해시 컬렉션의 키로 안전하다
    • 해시가 바뀌지 않으니 잃어버릴 일이 없다
  • ③ 캐시·공유가 안전하다
    • 여러 곳에서 같은 인스턴스를 써도 된다 (Integer 캐시가 이 원리)
  • ④ 사이드 이펙트가 없어 추론이 쉽다

만드는 규칙

public final class Money {                       // ① 상속 금지 (하위가 가변으로 만들 수 있다)
    private final long amount;                   // ② 모든 필드 final + private
    private final List<String> tags;

    public Money(long amount, List<String> tags) {
        this.amount = amount;
        this.tags = List.copyOf(tags);           // ③ 방어적 복사 (들어올 때)
    }
    public List<String> getTags() {
        return tags;                             // ④ 이미 불변 컬렉션이라 그대로 반환해도 안전
    }
    public Money plus(long v) {                  // ⑤ 변경 대신 새 객체를 반환
        return new Money(amount + v, tags);
    }
}

방어적 복사를 빠뜨리면

List<String> input = new ArrayList<>(List.of("a"));
Money m = new Money(100, input);   // 복사하지 않았다면
input.add("b");                    // 바깥에서 m 의 내부를 바꿔 버린다

final은 참조를 고정할 뿐 가리키는 객체의 내용까지 막지 않는다 — 불변의 가장 흔한 오해다.

record는 얕은 불변이다

record Order(Long id, List<String> items) { }

var list = new ArrayList<String>();
var o = new Order(1L, list);
list.add("해킹");                  // o.items() 가 바뀐다

// 컴팩트 생성자에서 복사한다
record Order(Long id, List<String> items) {
    Order { items = List.copyOf(items); }
}

성능이 걱정되면

  • "매번 새 객체를 만들면 느리지 않나?"

  • 대부분의 객체는 짧게 살고 죽는다 → 젊은 세대 GC 는 매우 싸다

  • TLAB 덕분에 할당 자체가 포인터를 밀어내는 수준으로 빠르다

  • 동기화를 없앤 이득이 대개 더 크다

  • 정말 문제가 되는 구간(대량 반복 · 큰 객체)만 측정해서 예외를 둔다

자바 표준에서의 예

String · Integer 등 래퍼 · LocalDate/LocalDateTime · BigDecimal List.of() · Map.of() · List.copyOf() 로 만든 컬렉션

면접 함정

  • "final을 붙이면 불변" → 참조만 고정된다. 내부 컬렉션은 여전히 바뀔 수 있다.
  • "record면 불변" → 얕은 불변이다. 가변 필드를 담으면 깨진다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 기초·타입·객체지향