3주차 로또 미션을 진행하며 일급 컬렉션, Value Object, 불변성에 대한 개념과 실제 적용 및 관계에 대해 혼동을 느꼈다.
그 기본인 불변성에 대해 final이 있으면 뭔가 아 ! 불변성을 얻었다 ! 싶은 생각이 충동적으로 들었다. 하지만 절대 절대 아니며 오히려 이런 잘못된 오해가 모든 혼란을 가져온다 ! 그로 인해 혼란 스러운 점과 실제 생성 방법 예시들을 정리해 보고자 한다
불변성 (Immutability)
객체가 생성된 이후 그 상태를 변경할 수 없는 것을 의미
// ❌ 가변 객체
public class MutableLotto {
private List<Integer> numbers;
public void addNumber(int number) {
numbers.add(number); // 상태 변경!
}
}
// ✅ 불변 객체
public class ImmutableLotto {
private final List<Integer> numbers;
public ImmutableLotto(List<Integer> numbers) {
this.numbers = List.copyOf(numbers); // 방어적 복사
}
public List<Integer> getNumbers() {
return Collections.unmodifiableList(numbers); // 불변 반환
}
}
장점 1 스레드 안전 (Thread-Safe)
// 불변 객체는 자동으로 스레드 안전!
public class BonusNumber {
private final int value;
// 여러 스레드에서 동시에 접근해도 안전
}
장점 2 예측 가능한 동작
// 불변 객체
BonusNumber bonus = new BonusNumber(7);
// bonus는 항상 7을 유지
// 예측 가능!
// 가변 객체
MutableBonus bonus = new MutableBonus(7);
someMethod(bonus); // 이 메서드가 bonus를 바꿀 수 있음
// bonus가 여전히 7인지 알 수 없음
불변성의 최대 사용 이유 !
사이드이펙트 가능성을 없애고 이를 통해 안전함을 보장받을 수 있다 !
장점3 방어적 복사 불필요
// 불변 객체는 그대로 공유 가능
public class WinningNumbers {
private final Lotto winningLotto; // 불변
public Lotto getWinningLotto() {
return winningLotto; // 복사 불필요!
}
}
// 가변 객체는 방어적 복사 필요
public class MutableWinningNumbers {
private MutableLotto winningLotto; // 가변
public MutableLotto getWinningLotto() {
return new MutableLotto(winningLotto); // 복사 필수!
}
}
불변성 vs 상수
final의 진실
final을 붙이면 불변 객체가 된다고 착각하기 쉽다. 하지만 이는 완전히 잘못된 생각이다 !
// ❌ 이것은 불변이 아니다 !
public class Lotto {
private final List<Integer> numbers = new ArrayList<>();
public Lotto(List<Integer> numbers) {
this.numbers.addAll(numbers);
}
public void addNumber(int number) {
this.numbers.add(number); // 가능! final은 재할당만 막음
}
}
// 사용
Lotto lotto = new Lotto(List.of(1, 2, 3, 4, 5, 6));
lotto.addNumber(7); // 리스트가 변경됨!
final이 막는 것 vs 막지 못하는 것
public class Example {
private final List<Integer> numbers = new ArrayList<>();
public void wrong() {
numbers = new ArrayList<>(); // ❌ 컴파일 에러!
// Cannot assign a value to final variable 'numbers'
}
}
final은 참조(reference)의 재할당만 막는다 !
public class Example {
private final List<Integer> numbers = new ArrayList<>();
private final StringBuilder sb = new StringBuilder();
private final Map<String, Integer> map = new HashMap<>();
public void canDoThis() {
// ✅ 모두 가능! (객체 내부 상태 변경)
numbers.add(1);
numbers.remove(0);
numbers.clear();
sb.append("Hello");
sb.delete(0, 5);
map.put("key", 1);
map.remove("key");
}
}
final은 객체 상태 변경은 막지 못한다 !
메모리 관점에서 이해
public class Example {
private final List<Integer> numbers;
public Example() {
numbers = new ArrayList<>(); // numbers는 0x1234 주소를 가리킴
}
public void wrong() {
numbers = new ArrayList<>(); // ❌ 다른 주소(0x5678)를 가리킬 수 없음!
}
public void canDo() {
numbers.add(1); // ✅ 0x1234 주소의 객체 내용 변경은 가능!
}
}
마치 집주소인 참조는 바꿀수 없지만(final) 집 안의 가구(객채 내용)은 바꿀 수 있는 것과 같다 !
원시 타입 vs 참조 타입
public class Counter {
private final int count = 0;
public void increment() {
count++; // ❌ 컴파일 에러!
// Cannot assign a value to final variable 'count'
}
}
원시 타입은 객체가 아니므로 final = 불변이 맞다 ! 하지만 아래와 같이 참조 타입의 경우에는 절대 불변 보장이 안된다 !
public class Container {
private final List<String> items = new ArrayList<>();
// final이지만 내용 변경 가능
public void addItem(String item) {
items.add(item); // ✅ 가능!
}
public void clearItems() {
items.clear(); // ✅ 가능!
}
}
;
진짜 불변 vs 가짜 불변
가짜 불변 (final만 사용)
public class FakeImmutable {
private final List<Integer> numbers;
public FakeImmutable(List<Integer> numbers) {
this.numbers = numbers; // 참조만 복사
}
public List<Integer> getNumbers() {
return numbers; // 원본 노출!
}
}
// 사용
List<Integer> original = new ArrayList<>(List.of(1, 2, 3));
FakeImmutable fake = new FakeImmutable(original);
// 😱 외부에서 변경 가능!
original.add(4);
fake.getNumbers().add(5);
System.out.println(fake.getNumbers()); // [1, 2, 3, 4, 5]
진짜 불변 (방어적 복사 + 불변 컬렉션)
public class RealImmutable {
private final List<Integer> numbers;
public RealImmutable(List<Integer> numbers) {
this.numbers = List.copyOf(numbers); // 불변 복사!
}
public List<Integer> getNumbers() {
return numbers; // 이미 불변이므로 안전
}
}
// 사용
List<Integer> original = new ArrayList<>(List.of(1, 2, 3));
RealImmutable real = new RealImmutable(original);
original.add(4); // ✅ 외부 변경 차단 real에 영향 없음
real.getNumbers().add(5); // ✅ 내부 변경 차단 UnsupportedOperationException!
진짜 불변 객체 만들기 예시
불변 객체의 3가지 조건
1️⃣ 모든 필드가 final
2️⃣ setter 메서드 없음
3️⃣ 가변 객체 참조 시 방어적 복사
1 원시 타입만 사용
public class Money {
private final long amount; // 원시 타입
public Money(long amount) {
this.amount = amount;
}
public Money plus(Money other) {
return new Money(this.amount + other.amount); // 새 객체 반환
}
public long getAmount() {
return amount; // 원시 타입은 복사됨
}
}
원시 타입은 자동으로 복사됨, getter도 안전, 가장 단순한 불변 객체
2 불변 객체만 참조
public class Person {
private final String name; // String은 불변
private final LocalDate birthDate; // LocalDate는 불변
public Person(String name, LocalDate birthDate) {
this.name = name;
this.birthDate = birthDate;
}
public String getName() {
return name; // String은 불변이므로 안전
}
public LocalDate getBirthDate() {
return birthDate; // LocalDate는 불변이므로 안전
}
}
불변 객체끼리 참조는 안전, 방어적 복사 불필요, Java의 불변 클래스: String, LocalDate, BigDecimal 등등 !
3 가변 객체 참조 (방어적 복사 필수)
public class Lotto {
private final List<Integer> numbers;
// ❌ 잘못된 구현
public Lotto(List<Integer> numbers) {
this.numbers = numbers; // 참조만 복사!
}
// ✅ 올바른 구현 방법 1: 방어적 복사
public Lotto(List<Integer> numbers) {
this.numbers = new ArrayList<>(numbers); // 새 리스트 생성
}
// ✅ 올바른 구현 방법 2: 불변 컬렉션
public Lotto(List<Integer> numbers) {
this.numbers = List.copyOf(numbers); // 불변 리스트
}
}
방어적 복사 패턴
생성자에서 방어적 복사
public class Lotto {
private final List<Integer> numbers;
public Lotto(List<Integer> numbers) {
// 방어적 복사
this.numbers = new ArrayList<>(numbers);
}
}
// 사용
List<Integer> original = new ArrayList<>(List.of(1, 2, 3, 4, 5, 6));
Lotto lotto = new Lotto(original);
original.add(7); // lotto에 영향 없음!
getter에서 방어적 복사
public class Lotto {
private final List<Integer> numbers;
public Lotto(List<Integer> numbers) {
this.numbers = new ArrayList<>(numbers);
}
// ❌ 잘못된 getter
public List<Integer> getNumbers() {
return numbers; // 내부 상태 노출!
}
// ✅ 올바른 getter 방법 1: 방어적 복사
public List<Integer> getNumbers() {
return new ArrayList<>(numbers); // 복사본 반환
}
// ✅ 올바른 getter 방법 2: 불변 뷰 반환
public List<Integer> getNumbers() {
return Collections.unmodifiableList(numbers); // 읽기 전용
}
}
불변 컬렉션 사용
public class Lotto {
private final List<Integer> numbers;
public Lotto(List<Integer> numbers) {
this.numbers = List.copyOf(numbers); // 불변 리스트!
}
public List<Integer> getNumbers() {
return numbers; // 이미 불변이므로 그대로 반환
}
}
// 시도
Lotto lotto = new Lotto(List.of(1, 2, 3, 4, 5, 6));
lotto.getNumbers().add(7); // UnsupportedOperationException!
불변 컬렉션 비교
| 방법 | 생성방법 | 변경시도 | 성능 | 최종 권장 |
| List.of() | List.of(1, 2, 3) | 예외 발생 | 빠름 | ⭐⭐⭐⭐⭐ |
| List.copyOf() | List.copyOf(list) | 예외 발생 | 빠름 | ⭐⭐⭐⭐⭐ |
| Collections.unmodifiableList() | Collections.unmodifiableList(list) | 예외 발생 | 보통 | ⭐⭐⭐⭐ |
| new ArrayList<>() | new ArrayList<>(list) | 변경 가능 | 느림 | ⭐ |
// 1. List.of() - 불변 리스트 직접 생성
List<Integer> list1 = List.of(1, 2, 3);
list1.add(4); // UnsupportedOperationException
// 2. List.copyOf() - 다른 컬렉션으로부터 불변 리스트 생성
List<Integer> list2 = List.copyOf(Arrays.asList(1, 2, 3));
list2.add(4); // UnsupportedOperationException
// 3. Collections.unmodifiableList() - 불변 뷰 생성
List<Integer> original = new ArrayList<>(Arrays.asList(1, 2, 3));
List<Integer> list3 = Collections.unmodifiableList(original);
list3.add(4); // UnsupportedOperationException
original.add(4); // 원본 변경 가능! (list3에도 영향)
// 4. new ArrayList<>() - 가변 리스트 (방어적 복사)
List<Integer> list4 = new ArrayList<>(Arrays.asList(1, 2, 3));
list4.add(4); // 가능! (진짜 불변 아님)
불변 객체 생성 패턴
정적 팩토리 메서드 패턴
생성자의 한계를 보완해, 의도 명확성·재사용성·유연성을 높이는 객체 생성 방식
public class PurchaseAmount {
private final int amount;
private PurchaseAmount(int amount) { // private 생성자
validate(amount);
this.amount = amount;
}
public static PurchaseAmount from(String input) { // 정적 팩토리
int amount = Integer.parseInt(input);
return new PurchaseAmount(amount);
}
}
생성자보다 메서드 이름으로 객체 생성의 목적을 드러낼 수 있다.
매번 new로 새 객체를 만들 필요 없이 캐싱/싱글톤 재사용 가능하다.
반환타입을 유연하게 지정할 수 있어 다형성 활용에 유리하다.
검증이나 변환과 같은 내부 로직을 감추고, 클라이언트에 단순한 인터페이스를 제공해서 로직을 캡슐화한다.
Builder 패턴
가독성·유연성·안정성을 높여 복잡한 객체를 직관적이고 안전하게 생성할 수 있게 해주는 패턴
public class WinningNumbers {
private final Lotto winningLotto;
private final BonusNumber bonusNumber;
private WinningNumbers(Builder builder) {
this.winningLotto = builder.winningLotto;
this.bonusNumber = builder.bonusNumber;
}
public static class Builder {
private Lotto winningLotto;
private BonusNumber bonusNumber;
public Builder winningLotto(Lotto lotto) {
this.winningLotto = lotto;
return this;
}
public Builder bonusNumber(BonusNumber bonus) {
this.bonusNumber = bonus;
return this;
}
public WinningNumbers build() {
return new WinningNumbers(this);
}
}
}
import lombok.Builder;
import lombok.Getter;
@Getter
@Builder
public class WinningNumbers {
private final Lotto winningLotto;
private final BonusNumber bonusNumber;
// Lombok이 Builder를 자동 생성!
}
// 사용
WinningNumbers winning = WinningNumbers.builder()
.winningLotto(lotto)
.bonusNumber(bonus)
.build();
생성자나 정적패곹리 메서드에 인지가 많을 때, 어떤 값이 어떤 필드인지 명확하게 표현가능.
모든 필드가 final인 불변 객체 쉽게 생성 가능.
일부 필드만 설정하고 나머지는 기본값으로 둘수 있다.
내부에서 값 검증로직을 수행해 잘못된 객체 생성을 방지하며 로직을 캡슐화한다.
메서드 체이닝으로 직관적인 코드를 작성할 수 있다.
불변 객체 만들기 체크리스트
진짜 불변 = final + 방어적 복사 + 불변 컬렉션
✅ 모든 필드 final 선언
✅ setter 메서드 제거
✅ 생성자에서 방어적 복사
✅ getter에서 방어적 복사 or 불변 컬렉션
✅ 가변 객체 대신 불변 객체 사용
✅ 상태 변경 시 새 객체 반환
생성 패턴 선택 가이드
정적 팩토리 메서드:
→ 파라미터 2-3개
→ 이름으로 의미 표현 필요
→ 인스턴스 캐싱 필요
→ 다형성 활용
Builder 패턴:
→ 파라미터 4개 이상
→ 선택적 매개변수 많음
→ 가독성 중요
→ 복잡한 검증 로직
생성자:
→ 파라미터 1-2개
→ 단순한 경우
항상 불변을 먼저 고려
// ✅ 기본: 불변
public class Money {
private final long amount;
}
// ❌ 예외적: 가변 (정말 필요한 경우만)
public class Counter {
private int count; // 성능상 이유로 가변
}
Lombok 활용
@Value // 모든 필드 final + getter + equals/hashCode + toString
public class Money {
long amount;
}
@Builder // Builder 패턴 자동 생성
@Getter // getter만
public class Order {
private final Long id;
private final Money amount;
}
불변 객체는 방어적 복사 불필요
public class Order {
private final Money totalAmount; // Money는 불변
public Money getTotalAmount() {
return totalAmount; // 복사 불필요! 안전하게 공유
}
}
컬렉션은 항상 방어적 복사
public class Lottos {
private final List<Lotto> lottos;
public Lottos(List<Lotto> lottos) {
this.lottos = List.copyOf(lottos); // 불변 복사!
}
public List<Lotto> getLottos() {
return lottos; // 이미 불변이므로 안전
}
}
'외부활동 > 우아한테크코스 [프리코스]' 카테고리의 다른 글
| 불변객체 == Record 가 아니다 🙅 (0) | 2025.10.31 |
|---|---|
| 도메인 모델과 VO, 일급 컬렉션 톺아보기 (0) | 2025.10.30 |
| ErrorMessage Enum 관리 및 간소화 시도 (0) | 2025.10.29 |
| [우아한테크코스 8기] 프리코스 2주차 회고 (0) | 2025.10.28 |
| 테스트 코드로 완성하는 자동차 경주: Callback과 Strategy의 시너지 (1) | 2025.10.27 |