코드 품질 개선 기법 22편: To equal, or not to equal (새 탭에서 열림)
Java와 Kotlin에서 객체의 등가성을 정의하는 equals 메서드는 반드시 객체의 동일성(Identity)이나 모든 속성이 일치하는 등가성(Equivalence) 중 하나를 명확히 표현해야 합니다. 식별자(ID)와 같은 일부 속성만 비교하도록 equals를 잘못 구현하면, 상태 변경을 감지하는 옵저버블 패턴에서 데이터 업데이트가 무시되는 심각한 버그를 초래할 수 있습니다. 따라서 특정 속성만 비교해야 하는 상황이라면 equals를 오버라이딩하는 대신 별도의 명시적인 함수를 정의하여 사용하는 것이 안전합니다.
부분 비교 equals 구현의 위험성
- 객체의 식별자(ID) 등 일부 필드만 사용하여
equals를 구현하면, 객체가 논리적으로는 변경되었음에도 기술적으로는 '같은 객체'로 판정되는 모순이 발생합니다. StateFlow,LiveData,Observable등의 프레임워크는 이전 데이터와 새 데이터를equals로 비교하여 변경 사항이 있을 때만 UI를 업데이트합니다.- 만약 사용자의 식별자는 같지만 닉네임이나 상태 메시지가 변경된 경우, 부분 비교
equals는true를 반환하므로 화면에 변경 사항이 반영되지 않는 버그가 발생합니다.
올바른 등가성 정의와 대안
- 동일성(Identity): 두 객체의 참조가 같은지를 의미하며, 특별한 구현이 필요 없다면 Java/Kotlin의 기본
equals를 그대로 사용합니다. - 등가성(Equivalence): 모든 속성과 필드가 같을 때
true를 반환하도록 설계해야 합니다. Kotlin에서는data class를 사용하면 생성자에 선언된 모든 필드를 비교하는equals가 자동으로 생성됩니다. - 명시적 비교 함수: 특정 식별자만 비교해야 하는 로직이 필요하다면
hasSameIdWith(other)와 같이 의도가 명확히 드러나는 별도의 함수를 정의하여 사용하는 것이 좋습니다.
구현 시 주의해야 할 예외와 맥락
- Kotlin data class의 제약:
data class는 생성자 파라미터에 정의된 속성만equals비교에 사용합니다. 클래스 본문에 선언된 변수(var)는 비교 대상에서 제외되므로 주의가 필요합니다. - 캐시 필드의 제외: 계산 결과의 캐시값처럼 객체의 논리적 상태에 영향을 주지 않고 성능 최적화를 위해 존재하는 필드는 등가성 비교에서 제외해도 무방합니다.
- 도메인 맥락에 따른 설계: 유리수(1/2과 2/4)의 예시처럼, 모델이 단순한 '표시용'인지 '수학적 계산용'인지에 따라 등가성의 기준이 달라질 수 있으므로 개발 목적에 맞는 신중한 정의가 필요합니다.
객체의 등가성을 설계할 때는 해당 객체가 시스템 내에서 어떻게 관찰되고 비교될지를 먼저 고려해야 합니다. 특히 데이터 바인딩이나 상태 관리를 사용하는 환경에서는 equals가 객체의 전체 상태를 대변하도록 엄격하게 구현하고, 식별자 비교는 명시적인 명칭의 메서드로 분리하는 것이 코드의 예측 가능성을 높이는 방법입니다.