[TDD, 클린 코드 with Java] 불변 객체(Immutable Object)
by Hi.Claire🖥️ TDD, 클린 코드 with Java 19기 (박재성, 넥스트스텝)
불변 객체(Immutable Object)
<엘레강트 오브젝트> 불변 객체로 만드세요.
네 번째 라이브 강의는 불변 객체(Immutable Object)에 관한 내용으로 시작한다.
불변 객체(Immutable Object)란 생성자를 통해 인스턴스를 생성한 후에 상태를 변경할 수 없는 객체를 말한다.
반면 가변 객체는 인스턴스를 생성한 후에도 상태 변경이 가능한 객체를 의미한다.
다음의 CarNumber 객체는 불변 객체인가, 가변 객체인가?
CarNumber.java
public class CarNumber {
private int carNumber;
public CarNumber(int carNumber) {
throwIfNegative(carNumber);
this.carNumber = carNumber;
}
public void increase() {
carNumber++;
}
public void decrease() {
carNumber--;
}
private static void throwIfNegative(int carNumber) {
if (carNumber < 0) {
throw new IllegalArgumentException("carNumber 값은 0보다 작을 수 없습니다.");
}
}
}
인스턴스를 생성한 후에 increase()와 decrease() 메서드를 통해서 언제든지 상태값을 바꿀 수 있으므로 CarNumber 객체는 가변 객체이다.
그렇다면 프로그램의 안정성을 위해서는 객체를 불변 객체로 만드는 것이 좋을까, 가변 객체로 만드는 것이 좋을까?
이에 대한 답은 <엘레강트 오브젝트> 책의 "불변 객체로 만드세요" 부분에 나와있다.
이 책은 극단적으로 "가변 객체의 사용을 엄격하게 금지해야 합니다."라는 표현까지 쓰고 있다.
프로젝트가 복잡할수록, 여러명의 개발자가 함께 개발에 참여할수록 불변 객체의 중요성은 더욱 커진다.
가변 객체로 만들었을 때 버그 발생 가능성이 더 높아지며, 디버깅을 통해 문제의 원인을 찾아내는 것도 쉽지 않다.
다음은 책의 내용 중 일부이다.
모든 클래스를 상태 변경이 불가능한 불변 클래스(Immutable class)로 구현하면 유지보수성을 크게 향상시킬 수 있다.
불변 객체를 기반으로 사고하면 더 깔끔하고, 더 작고, 더 쉽게 이해할 수 있는 코드를 구현할 수 있다.
그렇다면 위의 CarNumber 객체를 불변 객체로 만들려면 어떻게 해야 할까?
CarNumber.java
public class CarNumber {
private final int carNumber;
public CarNumber(int carNumber) {
throwIfNegative(carNumber);
this.carNumber = carNumber;
}
public CarNumber increase() {
return new CarNumber(carNumber + 1);
}
public CarNumber decrease() {
return new CarNumber(carNumber - 1);
}
private static void throwIfNegative(int carNumber) {
if (carNumber < 0) {
throw new IllegalArgumentException("carNumber 값은 0보다 작을 수 없습니다.");
}
}
}
불변 객체로 만들기 위해 private 프로퍼티인 carNumber에 final 키워드를 추가한다.
final 키워드는 생성자 외부에서 프로퍼티의 값을 수정할 경우 컴파일 타임 에러가 발생해야 한다는 사실을 컴파일러에게 알려준다.
불변 객체는 필요한 모든 것을 내부에 캡슐화하고 변경할 수 없도록 통제한다.
불변 객체를 수정해야 한다면 프로퍼티를 수정하는 대신 원하는 상태를 가지는 새로운 객체를 생성해서 반환해야 한다.
CarNumberTest.java
public class CarNumberTest {
@Test
void increase() {
CarNumber position = new CarNumber(4);
CarNumber result = position.increase();
assertThat(result).isEqualTo(new CarNumber(5));
}
@Test
void decrease() {
CarNumber tryNo = new CarNumber(4);
CarNumber result = tryNo.decrease();
assertThat(result).isEqualTo(new CarNumber(3));
}
}
기존 코드에서 CarNumber의 increase()나 decrease() 메서드를 호출하는 부분이 있었다면 위와 같이 원하는 상태를 가지는 새로운 CarNumber 객체를 반환받아 사용하도록 수정한다.
앞으로 프로그래밍을 할 때 습관적으로 인스턴스 변수에 final 키워드를 사용해서 불변 객체로 만들도록 노력하자.
불가피한 경우에만 가변 객체로 바꾼다.
다음은 책에서 설명하는 불변 객체로 구현하면 좋은 점에 관한 내용이다.
- 식별자 가변성 문제가 없다.
- 실패 원자성이 있다.
- 완전하고 견고한 상태의 객체를 가지거나 아니면 실패하거나 둘 중 하나만 가능하다.
- 시간적 결합을 제거할 수 있다.
- 가변 객체들이 많을 경우 연산들의 순서를 일일이 기억해야 한다.
- 부수 효과를 제거할 수 있다.
- 멀티 스레드 환경에서 다수의 사용자가 동시에 상태값을 변경할 때 버그가 생기는 side effect가 발생할 수 있다. 이를 방지하기 위해 불변 객체를 사용하는 것이 좋다.
- null 참조를 없앨 수 있다.
- 스레드 안전한 코드를 구현할 수 있다.
- 더 작고 더 단순한 객체를 구현할 수 있다.
그런데 한 가지 이슈가 있다.
불변 객체(immutable object)가 좋은 것은 알겠는데, 인스턴스가 너무 많이 생성되어 성능이 떨어지는 문제가 있지 않을까?
그럴 경우 Position 객체에서 자주 쓰이는 범위 내의 값들은 인스턴스 캐싱을 통해 재사용하도록 하여 문제를 해결할 수 있다.
인스턴스 캐싱에 대한 내용은 다음 글을 참고하면 된다.
(참고) 인스턴스 캐싱
[TDD, 클린 코드 with Java] 인스턴스 캐싱(Caching)
🖥️ TDD, 클린 코드 with Java 19기 (박재성, 넥스트스텝) 인스턴스 캐싱(Caching)객체지향 생활 체조 원칙 중 "모든 원시값과 문자열을 포장한다."는 내용이 있다.이와 관련하여 지난 수업 때 객체 설
moominie.tistory.com
'☕️ Java > TDD, 클린 코드 with Java' 카테고리의 다른 글
[TDD, 클린 코드 with Java] 점진적 리팩터링 (1) | 2025.01.05 |
---|---|
[TDD, 클린 코드 with Java] 인스턴스 캐싱(Caching) (2) | 2025.01.05 |
[TDD, 클린 코드 with Java] 생성자 활용 (0) | 2024.11.03 |
[TDD, 클린 코드 with Java] 객체 설계(클래스 분리) : 인스턴스 변수, 함수 인수, private 메서드 테스트 (0) | 2024.11.02 |
[TDD, 클린 코드 with Java] 객체 설계(클래스 분리) : 일급 컬렉션, DI(Dependency Injection) (1) | 2024.10.30 |
블로그의 정보
Claire's Study Note
Hi.Claire