equals와 hashCode는 어떻게 쓰여야 할까? – ID 없이 동작하는 엔티티의 진짜 의미

Jayson·2025년 4월 8일
0
post-thumbnail

왜 equals와 hashCode를 고민하게 됐는가?

최근 도메인 모델을 설계하다가 흥미로운 구조를 마주하게 되었다. JPA 엔티티에는 당연히 존재하는 id 필드가, 도메인 객체에는 존재하지 않았다. 처음엔 어색했다. "ID 없이 어떻게 동일한 객체인지 판단하지?" 라는 의문이 들었고, 테스트 코드를 보며 이해가 되기 시작했다.

바로 이 지점에서 equals()hashCode()의 역할을 다시 짚고 넘어가야겠다는 생각이 들었다. 이 글에서는 ID 없이도 equals/hashCode로 도메인 객체를 다룰 수 있는 구조를 예시와 함께 설명한다.


도메인 모델에 id가 없다고?

다음은 내가 사용 중인 Member 도메인 객체다. 아래와 같이 정의되어 있다.

@EqualsAndHashCode
public class Member {

    private final MemberName name;
    private final MemberProfileImageKey profileImageKey;
    private final MemberAccountStatus accountStatus;

    private Member(MemberName name, MemberProfileImageKey profileImageKey, MemberAccountStatus accountStatus) {
        this.name = name;
        this.profileImageKey = profileImageKey;
        this.accountStatus = accountStatus;
    }

    public static Member of(MemberName name, MemberProfileImageKey profileImageKey, MemberAccountStatus accountStatus) {
        return new Member(name, profileImageKey, accountStatus);
    }
}

놀랍게도 id 필드가 없다. 하지만 이 도메인 객체는 테스트에서 아무 문제 없이 저장되고 조회된다. 그 비밀은 바로 @EqualsAndHashCode에 있다.


테스트에서 FakeRepository는 어떻게 활용되나?

public class FakeMemberRepository implements MemberRepository {

    private final Set<Member> storage = new HashSet<>();

    @Override
    public void save(Member member) {
        storage.remove(member);
        storage.add(member);
    }

    public Optional<Member> findBy(Member target) {
        return storage.stream()
                .filter(saved -> saved.equals(target))
                .findFirst();
    }
}

Set은 내부적으로 equals()hashCode()를 기준으로 중복 여부를 판단한다. 따라서 Member 객체의 동일성을 판단할 수 있는 기준이 필요하고, @EqualsAndHashCode 덕분에 이 기준이 VO 기반으로 정의된다.

이렇게 테스트에서는 id 없이도 객체의 동등성을 비교하며 저장/조회할 수 있다.


equals와 hashCode가 없다면 어떤 일이 벌어질까?

만약 우리가 @EqualsAndHashCode를 빼고 테스트를 했다면, 다음과 같은 문제가 생긴다.

repository.save(member);
Optional<Member> result = repository.findBy(member);
  • 저장한 객체와 findBy()로 찾으려는 객체가 같다고 판단되지 않음
  • Set 내부에서 equals()로 판단하지 못하므로 조회 실패

즉, 객체 간 비교 기준을 정의하지 않으면, 테스트나 컬렉션 관리가 불가능하다.


도메인에 id는 정말 필요 없는가?

사실, 도메인 모델에 꼭 id가 있어야 하는 것은 아니다. 아래와 같은 상황에서는 ID가 없어도 전혀 문제 되지 않는다.

  • 도메인 객체가 외부와 식별값을 주고받지 않는다
  • 순수하게 상태와 행위만으로 판단할 수 있다
  • 테스트나 컬렉션에서 VO 기준의 동일성 비교가 가능하다

반대로, 다음과 같은 경우라면 id를 포함하는 것이 낫다.

  • 외부 요청에서 id로 식별되는 객체 (ex. /members/1)
  • 연관 관계 매핑이 필요한 경우 (JPA 연관관계)
  • 도메인 객체가 DB 식별자 기반으로 로직을 갖는 경우

결론 – equals와 hashCode는 정체성을 정의하는 도구

  • equals()hashCode()는 객체가 같은지를 판단하는 기준이다.
  • ID가 없어도 VO 조합만으로 동일성을 정의할 수 있다면 충분하다.
  • 테스트에서도 DB 없이 Set과 Map을 활용해 도메인을 검증할 수 있다.
  • ID는 도메인 로직에 꼭 필요할 때만 포함하자. 그 외에는 VO와 행위 중심의 설계가 더 유연하다.

마무리

이번 경험을 통해 알게 된 것은, 도메인 모델은 꼭 JPA의 구조를 따라갈 필요가 없다는 점이다. 도메인은 “무엇을 하는가”에 집중하고, 저장소나 식별은 인프라 계층에 맡겨도 충분하다. 그리고 그 중심에는 equals()hashCode()가 있다.

profile
Small Big Cycle

0개의 댓글