우아한테크코스 레벨 1에서 학습한 내용을 정리한 글입니다.
💭 들어가며
블랙잭 미션을 진행하던 중, 배팅 금액처럼 의미가 있는 값을 원시값으로 다루지 말고 Money와 같은 값 객체로 리팩터링해 보라는 리뷰를 받았다. 솔직히 "값 객체"라는 용어는 많이 들어봤지만, "왜 굳이 불변이어야 하지?"라는 의문이 들었고, 해소가 되지 않은 상태에서는 억지로 사용하지 말자고 생각했다. 그러다 이번 미션에서 직접 값 객체를 적용하면서 불변 객체의 장점을 알게 되었고, 이에 대해 좀 더 자세하게 정리해보고자 한다.
✅ 불변 객체
불변 객체란 한 번 생성되면 내부 상태가 절대 바뀌지 않는 객체를 말한다.
▶ 불변 객체의 조건
- 객체의 상태를 변경하는 메서드를 제공하지 않는다.
- 클래스를 확장할 수 없도록 한다.
- 모든 필드는 final로 선언한다.
- 모든 필드를 private로 선언한다.
- 자신 외에는 내부의 가변 컴포넌트(List, Map, 배열 등)에 접근할 수 없도록 한다.
- 노출을 해야할 경우, 복사를 통해 외부 참조를 막아야 한다.
✅ 불변 객체의 장점
▶ Side Effect가 없음
불변 객체는 상태가 바뀌지 않기 때문에 Side Effect를 방지할 수 있다.
🔽 예시 코드
Money money1 = Money.of(1000);
Money money2 = money1.add(500); // 새 객체 반환
System.out.println(money1.getAmount()); // 1000
System.out.println(money2.getAmount()); // 1500
money1은 절대 변하지 않고, add()는 새로운 값을 가진 객체를 반환한다.
▶ 멀티스레드 환경에서도 안전
불변 객체는 공유 자원으로 사용하더라도 동기화를 고려할 필요가 없다. 값이 절대 변하지 않기 때문에 여러 스레드에서 동시에 접근해도 문제가 없다.
🔽 예시 코드
class SharedResource {
private final Money money = Money.of(1000); // 불변 객체
public Money getMoney() {
return money; // 안전하게 반환 가능
}
}
멀티스레드 환경에서도 money 객체는 안전하게 공유할 수 있다.
▶ 캡슐화 유지
불변 객체는 외부에서 값을 직접 변경할 수 없으므로 캡슐화 원칙을 자연스럽게 지킬 수 있다. 모든 변경은 의도된 메서드를 통해서만 가능하며, 항상 예측 가능한 결과를 반환한다.
🔽 예시 코드
Money money1 = Money.of(1000);
Money money2 = money1.add(500);
내부 값을 직접 변경할 수 없으며, 연산은 add()처럼 의도된 메서드를 통해서만 가능하다.
✅ 불변 객체의 주의할 점
- 연산할 때마다 새로운 객체를 생성하기 때문에 메모리 사용량이 증가할 수 있다.
- 값 변경이 잦은 경우, GC 비용 증가와 같은 성능 이슈가 발생할 수 있다.
- JPA 같은 프레임워크에서는 상태 변경을 추적해야 하므로, 불변 객체를 사용하기엔 제약이 따른다.
✅ 동일성 VS 동등성
▶ 동일성(Identity Equality)
동일성은 동일하다는 뜻으로, 두 객체가 완전히 같은 경우를 의미한다. 즉, 두 변수가 동일한 메모리 주소를 가리키고 있어, 사실상 하나의 객체를 가리키고 있는 상태다.
- 두 객체의 참조 값이 같은지
- == 비교
🔽 예시 코드
Money money1 = Money.of(1000);
Money money2 = money1;
System.out.println(money1 == money2); // true
▶ 동등성(Structural Equality)
동등성은 동등하다는 뜻으로, 두 객체가 동일한 값을 가지고 있는 경우를 의미한다. 즉, 서로 다른 객체라 하더라도 내용이 같다면 동등하다고 판단할 수 있다. 동일한 객체는 항상 동등하지만, 동등한 객체가 반드시 동일한 것은 아니다. (동일성 ⊃ 동등성)
- 두 객체의 내용이 같은지
- 재정의한 equals() 메서드 비교
🔽 예시 코드
Money money1 = Money.of(1000);
Money money2 = Money.of(1000);
System.out.println(money1.equals(money2)); // true
System.out.println(money1 == money2); // false
equals()는 Object 클래스에 기본으로 정의되어 있지만, 원하는 기준으로 비교하려면 오버라이딩이 필요하다.
✅ 값 객체(Value Object, VO)
값 객체는 값 자체에 의미가 있는 객체이다. 예를 들어, 이름, 주소, 돈, 위치 등이 있다.
▶ 특징
- ==이 아닌 equals()로 비교한다. (동등성)
- 대부분 불변 객체로 설계된다.
- 엔티티처럼 고유한 식별자가 없으며, 값 자체로 객체를 구분한다.
🔽 예시 코드: 블랙잭 미션에서의 Money
public class Money {
private final int amount;
private Money(int amount) {
this.amount = amount;
}
public static Money of(int amount) {
return new Money(amount);
}
public Money add(int amount) {
return new Money(this.amount + amount);
}
public Money minus(int amount) {
return new Money(this.amount - amount);
}
public Money multiply(double rate) {
return new Money((int) (this.amount * rate));
}
public int getAmount() {
return amount;
}
// equals, hashCode 오버라이드
}
▶ 내가 헷갈렸던 부분: 불변 객체를 사용하는 곳에서의 final
public class Wallet {
private final Money bettingMoney;
private Money earnedMoney;
private Profit profit;
...
}
처음엔 Money가 불변 객체니까 Wallet에서 사용하는 earnedMoney도 반드시 final이어야만 한다고 생각했다. 하지만,
- final은 "참조를 바꾸지 않겠다"는 약속이다.
- 객체가 불변으로 설계되어 있다면, 그 자체는 바뀌지 않으므로 final이 아니어도 공유해도 안전하다.
결론적으로, "불변 객체는 final을 써야 한다!"는 생각에 꽂혀 불변 객체의 목적을 제대로 이해하지 못하고 사용한 것 같다. final의 여부는 객체의 불변성과는 별개의 문제였다.
📍 참고 자료
'Programming > Java' 카테고리의 다른 글
[Java] Optional (0) | 2025.04.07 |
---|---|
[Java] 상속, 조합 (0) | 2025.04.01 |
[Java] 인터페이스, 추상 클래스, 일반 상속 (0) | 2025.03.22 |
[Java] 제네릭(Generic) (0) | 2025.03.17 |
[Java] 컬렉션 프레임워크(Collection Framework) (0) | 2025.03.11 |