💡 많은 고수 개발자 분들이 불변 객체 사용의 중요성에 대해 이야기하고있습니다. 소프트웨어 개발에서 매우 중요하지만 때로는 간과될 수 있는 개념인 불변 객체(Immutable Object)와 가변 객체(Mutable Object)에 대해 조금 더 깊이 알아보고 왜 가변 객체의 사용보다 불변 객체 사용을 지향해야 하는지 그 이유를 알아보겠습니다.
우선 두 개념을 명확히 정의해보곘습니다.
// Java 예시
List<String> mutableList = new ArrayList<>();
mutableList.add("Apple"); // 객체의 상태 변경
mutableList.add("Banana");
// Java 예시
String originalString = "Hello";
String newString = originalString.concat(" World"); // 새로운 String 객체 생성
// originalString은 여전히 "Hello" 입니다.
이제 핵심 질문입니다. 왜 많은 전문가들이 불변 객체 사용을 권장할까요? 특히 현대의 복잡한 소프트웨어 환경에서는 불변 객체의 장점이 더욱 두드러집니다.
가변 객체는 언제든 상태가 변할 수 있으므로, 해당 객체를 사용하는 코드 전체에서 그 상태 변화를 예측하고 추적하기 어렵습니다. 특히 함수나 메서드를 호출할 때 원본 객체가 변경될 수 있다면, 개발자는 항상 부수 효과(Side Effect)를 염두에 두어야 합니다.
반면 불변 객체는 한 번 생성되면 상태가 변하지 않으므로, 객체의 동작을 예측하기 매우 쉽습니다. 특정 시점에 객체가 어떤 상태를 가지고 있는지 명확하게 알 수 있어, 코드를 이해하고 디버깅하는 데 드는 많은 공수를 크게 줄여줍니다. 이는 클린 아키텍처에서 강조하는 계층 간의 명확한 역할 분리와도 맞닿아 있습니다.
멀티스레드 환경은 현대 소프트웨어 개발의 필수 요소입니다. Spring Applicaion의 경우 톰캣 기반으로 구동하기 때문에 기본적으로는 멀티 스레드 환경에서 구동되는 소프트웨어라고 할 수 있습니다. 여러 활성 상태의 스레드가 동일한 가변 객체에 동시에 접근하여 상태를 변경하는 경우를 생각해봅시다. 이런 상황에서 너무나 간단하게 동시성 문제가 발생할 수 있습니다. 이는 데이터 불일치, 값이 오염되는 문제, 심지어 애플리케이션 충돌로 이어질 수 있으며, 복잡한 동기화 메커니즘(락, 세마포어 등)을 필요로 합니다. 이러한 동기화 로직은 데드락이나 라이브락과 같은 더 큰 문제를 야기하기도 합니다.
하지만 불변 객체는 상태가 변경되지 않으므로, 여러 스레드가 동시에 접근하더라도 데이터를 안전하게 공유할 수 있습니다. 별도의 동기화 메커니즘이 필요 없어 멀티스레드 프로그래밍의 복잡성을 크게 줄여주며, 도메인 주도 설계(DDD)에서 도메인 모델의 일관성을 유지하는 데 결정적인 이점을 제공합니다.
가변 객체를 사용하는 메서드는 종종 객체의 내부 상태를 변경하는 '부수 효과'를 가질 수 있습니다. 이러한 부수 효과는 예기치 않은 동작을 유발하고 코드의 흐름을 파악하기 어렵게 만들어 버그의 온상이 됩니다.
불변 객체는 상태를 변경하는 메서드를 가질 수 없습니다. 대신, 상태가 변경된 새로운 객체를 반환하는 방식으로 동작합니다. 이는 함수형 프로그래밍 패러다임과도 잘 맞으며, 부수 효과를 최소화하여 코드의 순수성을 높이고 버그 발생 가능성을 현저히 줄여줍니다.
가변 객체는 상태가 변할 수 있으므로 캐싱된 데이터의 유효성을 관리하기 어렵습니다. 캐시된 객체가 변경되면 캐시된 데이터와 실제 데이터 간의 불일치가 발생할 수 있습니다.
반면 불변 객체는 상태가 변하지 않으므로 한 번 계산된 값을 안전하게 캐시할 수 있습니다. 이는 성능 최적화에 유리하며, HashMap이나 HashSet 같은 컬렉션의 키로 사용될 때도 안정적인 동작을 보장합니다. DDD에서 복잡한 계산 결과나 값 객체(Value Object)를 효율적으로 재사용할 수 있게 합니다.
가변 객체를 테스트할 때는 객체의 초기 상태를 설정하고, 테스트 도중 발생할 수 있는 모든 상태 변화를 고려해야 하므로 테스트 코드가 복잡해질 수 있습니다.
불변 객체는 상태가 고정되어 있으므로 특정 상태를 재현하기 쉽고, 테스트 간의 독립성이 보장됩니다. 이는 클린 아키텍처에서 각 계층의 단일 책임 원칙(SRP)을 지키고, 유닛 테스트를 용이하게 하는 데 크게 기여합니다.
DDD는 복잡한 비즈니스 도메인을 모델링하는 데 중점을 둡니다. 불변 객체는 DDD의 여러 핵심 개념과 시너지를 발휘합니다.
값 객체(Value Object): DDD의 가장 중요한 개념 중 하나인 값 객체는 "측정하거나 서술하는 속성들을 모아놓은 객체"로 정의됩니다. 예를 들어, Money
, Address
, DateRange
등이 있습니다. 값 객체는 그 본질상 불변이어야 합니다. $5는 항상 $5이지, $10으로 변하지 않습니다. 만약 금액이 변하면 새로운 Money 객체가 생성되는 것이 자연스럽습니다. 불변 값 객체는 공유하기 쉽고 부수 효과가 없으므로 도메인 모델의 견고성을 높입니다.
도메인 이벤트(Domain Event): 도메인 이벤트는 과거에 발생한 사실을 나타내므로 불변해야 합니다. 이벤트가 발생한 후에는 그 내용이 변경될 수 없습니다.
스냅샷(Snapshot): 특정 시점의 도메인 모델 상태를 저장하는 스냅샷도 불변 객체로 생성되어야 합니다.
불변 객체의 장점이 많지만, 무분별한 사용이 항상 최선은 아닙니다.
메모리 및 GC(Garbage Collection) 오버헤드: 불변 객체는 상태 변경 시 항상 새로운 객체를 생성하므로, 매우 빈번하게 객체를 변경해야 하는 상황에서는 많은 객체가 생성되고 소멸됩니다. 이는 가비지 컬렉션의 부담을 늘려 애플리케이션의 응답 시간에 미세한 영향을 줄 수 있습니다.
성능 최적화는 나중에: 하지만 현대의 JVM과 GC 기술은 매우 발전하여 대부분의 비즈니스 애플리케이션에서는 이러한 오버헤드가 큰 문제가 되지 않습니다. 불변 객체 사용으로 인한 성능 이슈는 대부분의 경우 코드의 안정성, 가독성, 유지보수성보다 후순위에 있습니다. 프로파일링을 통해 명확한 성능 병목 현상이 발견될 때만 가변 객체로의 전환이나 객체 풀링 같은 최적화 기법을 고려해야 합니다. 섣부른 최적화는 오히려 복잡성과 버그를 유발할 수 있습니다.
불변 객체는 현대 소프트웨어 개발에서 강력한 이점을 제공하며, 여러분의 코드를 더욱 예측 가능하고, 안전하며, 유지보수하기 쉽게 만들어 줄 것입니다. 특히 멀티스레드 환경과 복잡한 도메인 로직을 다룰 때 그 가치는 더욱 빛을 발합니다.
이제 여러분의 코드베이스에서 불변 객체를 적극적으로 활용해보는 것은 어떨까요? 작은 변화가 큰 차이를 만들 것입니다!