Programming/Java

[Java] 불변 객체, 값 객체

soeun2537 2025. 4. 13. 18:06
우아한테크코스 레벨 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의 여부는 객체의 불변성과는 별개의 문제였다.

 

 

📍 참고 자료