백엔드 면접 용어 사전
Java·객체지향immutable

불변 객체

생성 후 상태가 바뀌지 않는 객체. 공유해도 안전해 동시성 문제를 원천 차단한다.

생성된 뒤 내부 상태가 절대 바뀌지 않는 객체(immutable object). String, Integer, LocalDate가 대표적이다.

만드는 방법

public final class Money {                      // ① 상속 금지
    private final BigDecimal amount;            // ② 모든 필드 final + private
    private final List<String> tags;

    public Money(BigDecimal amount, List<String> tags) {
        this.amount = amount;
        this.tags = List.copyOf(tags);          // ③ 방어적 복사 (입력)
    }

    public List<String> getTags() {
        return Collections.unmodifiableList(tags);  // ④ 방어적 복사 (출력)
    }
    // ⑤ setter 없음. 변경이 필요하면 새 객체를 반환
    public Money plus(BigDecimal v) { return new Money(amount.add(v), tags); }
}

③④를 빠뜨리면 불변이 아니다 — 가장 흔한 실수

List<String> list = new ArrayList<>(List.of("a"));
Money m = new Money(BigDecimal.TEN, list);

list.add("b");         // ③이 없으면 → m의 내부가 바뀐다!
m.getTags().add("c");  // ④가 없으면 → 밖에서 내부를 수정할 수 있다

필드에 final을 붙여도 그것은 "참조를 바꿀 수 없다"는 뜻일 뿐, 가리키는 객체의 내용은 바뀔 수 있다. 이 구분이 핵심이다.

왜 좋은가

① 스레드 안전 — 아무 장치 없이

  • 상태가 바뀌지 않는다 → 경쟁 조건이 성립할 수 없다
  • 락도, volatile도, 동기화도 필요 없다
  • 여러 스레드가 마음껏 공유해도 안전하다

동시성 문제의 근원은 공유 + 가변이다. 가변을 없애면 공유가 안전해진다.

② 안전한 공유와 캐싱

Integer.valueOf(100);   // -128~127 은 미리 만들어 둔 것을 재사용
"hello"                 // 문자열 상수 풀에서 공유

불변이므로 같은 인스턴스를 여러 곳이 써도 문제가 없다.

③ 해시 키로 안전 — 상태가 안 바뀌니 해시도 안 바뀐다. 가변 객체를 키로 썼을 때의 "못 찾는 원소" 문제가 없다.

④ 실패 원자성 — 중간에 예외가 나도 원본이 훼손되지 않는다. 새 객체를 만들다 실패하면 그냥 버리면 된다.

⑤ 추론이 쉽다 — 이 객체를 넘긴 뒤 누가 바꿨을까 걱정할 필요가 없다.

대가

값을 바꿀 때마다 새 객체 → 객체 생성 비용, GC 부담

실제로는 대부분 문제가 되지 않는다 — Young 영역의 짧게 사는 객체는 회수 비용이 거의 없기 때문이다(약한 세대 가설).

정말 문제가 되는 경우가 있다.

String s = ""; for (…) s += x;      // 반복마다 새 String → O(n²)
✅ StringBuilder                        // 가변 버퍼를 재사용

"불변이 기본, 성능이 문제일 때만 가변" 이 원칙이다.

자바의 도구

public record Point(int x, int y) { }   // 자바 16+ — 필드 final, setter 없음
List.of(...) / Map.of(...)              // 불변 컬렉션
LocalDate, Duration, BigDecimal          // 표준 불변 타입

record도 완전 불변은 아니다 — 필드가 가변 객체(List)면 그 내용은 바뀔 수 있다. 컴팩트 생성자에서 방어적 복사를 해야 한다.

public record Team(String name, List<String> members) {
    public Team {
        members = List.copyOf(members);   // 컴팩트 생성자에서 복사
    }
}

실무에서

  • DTO·값 객체(금액, 좌표, 기간) — → 불변으로
  • 설정 객체 — → 불변으로 (기동 후 바뀌면 안 됨)
  • JPA 엔티티 — → 불변이 어렵다(프록시·더티 체킹 때문)

"엔티티는 가변, 그 외는 불변" 정도가 현실적인 지침이다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — Java·객체지향 — OOP·JVM·GC·컬렉션