변수의 기록

(JAVA) 불변 객체 본문

자바/자바

(JAVA) 불변 객체

불광동 물주먹 2025. 4. 25. 11:23

불변 객체란 뭘까?

프로그래밍 하다 보면 "이 객체 상태가 바뀌면 안 되는데..." 싶은 순간이 있죠.
그럴 때 딱 맞는 개념이 바로 불변 객체(Immutable Object)입니다.

불변 객체는 한 번 만들면 내부 상태가 절대 바뀌지 않는 객체를 말해요.

객체의 내부 상태를 제공하는 메소드를 제공하지 않거나 방어적 복사(defensive-copy)를 통해 제공한다.

쉽게 말해, 생성 이후에는 그냥 읽기 전용! (read-only)

 


왜 불변 객체가 좋을까?.

1. 코드가 깔끔하고 안전함

상태가 안 바뀌니까 예상 못 한 사이드 이펙트도 없고, 디버깅도 쉬워져요.
특히 협업할 때 다른 사람이 뭘 건드릴까 걱정 안 해도 되는 게 제일 큼.

2. 자료구조와 찰떡궁합

Map, Set, Cache 같은 데서 많이 쓰이는데,
값이 안 바뀌니까 equals, hashCode 같은 거도 안정적으로 동작해요.

3. 멀티스레드 환경에서도 안전

불변이니까 여러 스레드가 동시에 접근해도 충돌이 거의 없어요.
물론 내부에 mutable 객체를 참조하고 있다면 얘는 예외니까 주의!

4. 방어적 복사 안 해도 됨

보통 mutable 객체를 필드로 들고 있으면 외부에 노출할 때 복사해서 줘야 하는데,
불변 객체는 복사 없이 그대로 줘도 안전하니까 코드가 훨씬 간결해져요.


이런 상황에 잘 어울려요

  • 조회만 많은 데이터 (예: 설정값, 좌표, DB에서 자주 가져오는 값 등)
  • 캐시에 자주 넣는 데이터
  • 상태를 고정시켜서 넘겨야 하는 DTO 같은 경우

자바에서 흔히 보는 불변 객체

  • String
  • Integer, Long 등 기본형 래퍼 클래스
  • LocalDate, BigDecimal, UUID 등도 대표적인 불변 객체

 

 

Java의 String은 불변 클래스이기 때문에 아래와 같이 String 내부의 char형 배열을 얻어 수정하여도 반영이 되지 않는다. Java에서는 배열이나 객체 등의 참조(Reference)를 전달한다. 그렇기 때문에 참조를 통해 값을 수정하면 내부의 상태가 변하기 때문에 내부를 복사하여 전달하고 있는데, 이를 방어적 복사(defensive-copy) 라고 한다.

String의 toCharArray를 다음과 같이 복사하여 전달하고 있다.

 

public char[] toCharArray() {
    // Cannot use Arrays.copyOf because of class initialization order issues
    char result[] = new char[value.length];
    System.arraycopy(value, 0, result, 0, value.length);
    return result;
}

어떻게 만들 수 있을까?

불변 객체 만드는 법은 생각보다 간단하지만 규칙을 꼭 지켜야 해요.

  1. 모든 필드는 private final로 선언
  2. setter 같은 상태 변경 메서드는 만들지 않기
  3. 클래스에 final 붙여서 상속 못 하게 하기
  4. mutable 객체를 들고 있다면 방어적 복사하기

예시 하나 볼까요?

public final class User {
    private final String name;
    private final LocalDate birthDate;

    public User(String name, LocalDate birthDate) {
        this.name = name;
        this.birthDate = birthDate;
    }

    public String getName() {
        return name;
    }

    public LocalDate getBirthDate() {
        return birthDate;
    }
}

 

이렇게 하면 진짜로 "건드릴 수 없는 객체"가 만들어져요.


이렇게 깨지기도 함 (주의!)

DTO 상속 같은 거 쓸 때 실수하기 쉬워요.

class ImmutableParent {
    private final String value;
    public ImmutableParent(String value) {
        this.value = value;
    }
    public String getValue() {
        return value;
    }
}

class MutableChild extends ImmutableParent {
    private String somethingMutable;
    public void setSomethingMutable(String s) {
        this.somethingMutable = s;
    }
}

 

이런 식이면 ImmutableParent는 겉보기에 불변이지만, 자식 클래스가 상태를 바꿔버릴 수 있어서 완전한 불변 객체라고 할 수 없어요. 그래서 클래스도 final로 막아야 해요.


얕은 복사 vs 깊은 복사

불변 객체를 만들 때 내부 필드에 다른 객체가 있다면? 이게 불변인지 가변인지에 따라 전략이 달라져요.

  • 불변 객체라면 → 얕은 복사(Shallow Copy) 괜찮아요.
  • 가변 객체라면 → 깊은 복사(Deep Copy)로 새로 복제해서 들고 있어야 안전해요.

예를 들어 RGB 색상을 들고 있는 객체가 있다고 하면:

  • RGB가 불변이면 그냥 갖다 써도 됨
  • RGB가 가변이면 복사해서 써야 다른 데서 바꿔도 영향 안 받아요

마무리 정리

불변 객체를 만들 땐 아래 네 가지를 꼭 지켜야 해요:

 

체크리스트 설명
상태 변경 불가 setter 제거, 메서드로 값 못 바꾸게
private final 필드 생성 시 고정, 이후 변경 불가
상속 금지 클래스에 final 붙이기
참조 객체 방어 mutable 객체는 깊은 복사해서 들고 있기