문제 상황
JPA repository를 사용하는데, 처음으로 repository에 save() 하는 로직에서 아래와 같은 문제가 발생했다.
말 그대로 어떤 엔티티를 추가하려는 요청(request)을 받고, 그 request 객체를 entity 객체로 변환하여 처음으로 jpaRepository에 save(entity)를 하는 부분이다.
서비스 메서드 전체
@Override
@Transactional
public RecruitDto registerRecruit(Request request, Long memberId) {
MemberEntity memberEntity = memberRepository.findById(memberId)
.orElseThrow(() -> new MemberException(ErrorCode.MEMBER_NOT_FOUND));
SportsEntity sportsEntity = sportsRepository.findById(request.getSportsId())
.orElseThrow(() -> new SportsException(ErrorCode.SPORTS_NOT_FOUND));
RecruitEntity saved =
recruitRepository.save(Request.toEntity(request, memberEntity, sportsEntity));
System.out.println(saved);
return RecruitDto.fromEntity(saved);
}
테스트 코드
@Test
@DisplayName("성공")
void registerRecruit() {
SportsEntity sports = createSports(1000L);
MemberEntity member = createMember(1L);
RecruitEntity recruit = createRecruit(10L, member, sports);
//given
RecruitRegister.Request request = createRegisterRequest(recruit);
when(memberRepository.findById(1L))
.thenReturn(Optional.of(member));
when(sportsRepository.findById(1000L))
.thenReturn(Optional.of(sports));
when(recruitRepository.save(Request.toEntity(request, member, sports)))
.thenReturn(recruit);
//when
RecruitDto result = recruitService.registerRecruit(request, member.getMemberId());
//then
assertThat(result.getSportsId()).isEqualTo(sports.getSportsId());
assertThat(result.getMemberId()).isEqualTo(member.getMemberId());
assertThat(result.getRecruitStatus()).isEqualTo(RecruitStatus.RECRUITING);
}
에러 메시지
문제 인식
실제 코드
RecruitEntity saved =
recruitRepository.save(Request.toEntity(request, memberEntity, sportsEntity));
테스트 코드
when(recruitRepository.save(Request.toEntity(request, member, sports)))
.thenReturn(recruit);
에러 발생 이유
우선 Request.toEntity()로 Entity 객체를 생성하고, 해당 객체를 repository.save() 메서드로 저장 후, 저장된 entity를 반환하는 로직이다.
여기서 문제는, toEntity()로 생성한 엔티티 객체의 인스턴스와 save()가 반환하는 엔티티 객체 인스턴스의 equals() 결과가 일치하지 않아서 발생한다.
toEntity() 객체는 id값이 존재하지 않고, save() 메서드에서 저장 후 GenerateType.IDENTITY 전략으로 id를 설정한다.
또한, 저장 후 저장된 객체를 반환하기 때문에 실제로 두 객체의 주소도 다르다.
해결 방법 1 (비추천)
any()를 사용하여 어떤 인스턴스를 받는지에 상관 없이 내가 만들어 둔 recruit 엔티티를 반환하게 한다.
when(recruitRepository.save(any(RecruitEntity.class))
.thenReturn(recruit);
그러나 이 방법은 save() 할 엔티티의 값을 Request의 값과 맞출 수 없다는 단점이 있다.
request로부터 entity를 만들어서 해당 엔티티를 저장해야 하는데, 그렇지 못한다는 것이다.
해결 방법 2
Entity 클래스에 equals()와 hashcode()를 오버라이딩 하는 것이다.
@EqualsAndHashCode
@Entity(name = "recruit")
public class RecruitEntity {
해당 entity 클래스에 대한 equals(), hashcode()를 구현해주면, recruit entity의 필드값들에 의해 두 객체가 비교되게 된다.
when(recruitRepository.save(Request.toEntity(request, member, sports)))
.thenReturn(recruit);
따라서 Request.toEntity()와 recruit 를 동일하게 인식하게 된다.
아래는 lombok 어노테이션 @EqualsAndHashCode에 대한 설명 문서이다.
https://www.baeldung.com/java-lombok-equalsandhashcode
다만, EqualsAndHashCode는 조심해서 사용해야한다.
의도하지 않은 것처럼 동작할 수 있기 때문이다.
JPA 엔티티매니저가 엔티티를 비교하는 것도 고려해봐야 한다.
따라서 @EqualsAndHashCode를 사용하는 것보단 직접 overriding 하는 게 좋을 것 같다.
해결 방법 3
JPA는 일반적으로 PK(id) 값을 기준으로 엔티티 간의 비교를 진행한다.
따라서 아래와 같이 equals(), hashCode()를 overriding 해주면, 문제가 없을 것이다.
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object == null || getClass() != object.getClass()) {
return false;
}
RecruitEntity that = (RecruitEntity) object;
return Objects.equals(recruitId, that.recruitId);
}
@Override
public int hashCode() {
return Objects.hash(recruitId);
}