FireDrago

[bobzip] JPA 엔티티의 equals 구현 (동등성) 본문

프로젝트

[bobzip] JPA 엔티티의 equals 구현 (동등성)

화이용 2024. 6. 15. 19:38

문제상황

1. 레시피의 상세정보를 호출할 때, 레시피 엔티티가 가진 Member 엔티티(레시피 작성자)와

    현재 로그인한 유저의 Member 정보를 비교하여, 글 작성자가 수정, 삭제를 호출할 수 있도록 하고 싶다.

@Entity
@Getter
public class Recipe {

    @Id
    @GeneratedValue
    @Column(name = "recipe_id")
    private Long id;
	
    // 지연로딩된 Member 객체
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "member_id")
    private Member member;

    @NotNull
    @Size(max = 50)
    private String title;

    @Column(length = 100)
    private String instruction;

    @Embedded
    private UploadFile thumbnail;

    @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<RecipeIngredient> recipeIngredients = new ArrayList<>();

    @OneToMany(mappedBy = "recipe", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<RecipeStep> recipeSteps = new ArrayList<>();
}
@GetMapping("/{id}")
public String viewRecipe(@PathVariable("id") Long id,
                         @SessionAttribute(value = LoginConst.LOGIN, required = false) Member member,
                         Model model) {
    Recipe recipe = recipeService.findRecipe(id);
    // 게시글 작성자와 현재 로그인 유저가 같으면 isWriter 전달, 수정, 삭제 버튼 표시
    if (member != null && member == recipe.getMember())) {
        model.addAttribute("isWriter", true);
    }
    model.addAttribute("recipe", recipe);
    return "/recipe/recipeView";
}

 

member == recipe.getMember() : 레시피 작성자로 로그인했음에도 불구하고 false가 출력되었다.

이 문제는 recipe.getMember()가 반환한 Member 객체가 지연 로딩(Lazy Loading)으로 인해 프록시 객체인 반면,

member 객체는 세션에 저장된 실제 Member 객체이기 때문이다.

따라서 두 객체의 동일성 비교에 '==' 연산자를 사용하면 false가 출력된다.

이 문제를 해결하기 위해서는 '==' 대신 객체의 내용을 비교하는 equals() 메서드를 사용해야 한다.

 

<JPA 엔티티의 equals 재정의>

 

우선, JPA 엔티티의 equals 정의는 일반적으로 id를 사용한다. 모든 필드를 사용하여 equals를 오버라이딩할 경우,

양방향 참조가 있는 필드에서 순환 참조 문제가 발생할 위험이 있다. 이로 인해 StackOverflowError가 발생할 수 있다.

 

id를 사용한 equals도 만능 해결책은 아니다.

비즈니스 로직에서 아직 영속성 컨텍스트에 관리되지 않는 엔티티를 사용하면,

id가 부여되지 않은 엔티티를 비교해야 할 수 있다. 이 경우 비즈니스 키를 사용하여 equals를 재정의해야 한다.

엔티티 생성부터 존재하는 필드를 이용하여 equals를 오버라이딩 하는 것이다.

JPA 엔티티의 equals 

1. id를 이용하자!

2. id 없다면, 비지니스 키 필드를 만들어 이용하자!

@Override
public boolean equals(Object obj) {
    if (this == obj) {
        return true;
    }
    if (obj == null || getClass() != obj.getClass()) {
        log.info("getClass(): {}", getClass());
        log.info("obj.getClass(): {}", obj.getClass());
        return false;
    }
    Member member = (Member) obj;
    return Objects.equals(id, member.id);
}

 

id를 이용하여 equals를 오버라이딩 했더니, 또 문제가 발생했다. 

getClass() 를 사용한 경우, 프록시 객체와 일반객체의 비교를 하게된다.

JPA 엔티티의 equals 메서드는 Hibernate.getClass() 를 사용한다는 것을 알게되었다.

JPA 엔티티 동등성 비교 중 만난 문제

1. 프록시 객체와 일반 객체의 비교 중 '==' 사용

2. equals 메서드에서 'getClass()' 사용

 

해결방법

1. JPA 엔티티의 동등성 비교는 'equals'를 사용하고, id 필드를 활용하자

2. JPA 엔티티의 equals 오버라이딩 할때는, Hibernate.getClass() 메서드를 사용하자

 

엔티티의 동일성과 동등성을 잘 구분하자! 동일성은 메모리상 같은 객체를 의미한다. '==' 사용한다.

동등성논리적으로 동일한 객체를 의미하고, equals 오버라이딩을 통해 개발자가 정의할 수 있다.

 

JPA 엔티티는 영속성 컨텍스트를 통해 관리되고, 트랜잭션 안에서 동작한다. 만약 같은 객체라도,

다른 트랜잭션에서 호출되었다면, 동일성이 보장되지 않을 가능성이 있다.

그래서  '==' 보다는 'equals'를 통해 엔티티의 동등성을 확인하는 것이 권장된다.

 

Member 클래스의 equals 메서드를 Hibernate.getClass() 를 사용하여 오버라이딩 했다.

Hibernate.getClass() 는 프록시 객체일 경우, 원래 객체를 가져오는 기능을 한다.

@Override
public boolean equals(Object obj) {
    if (this == obj) return true;
    if (obj == null || Hibernate.getClass(this) != Hibernate.getClass(obj)) {
        return false;
    }
    Member member = (Member) obj;
    return Objects.equals(id, member.getId());
}
@GetMapping("/{id}")
public String viewRecipe(@PathVariable("id") Long id,
                         @SessionAttribute(value = LoginConst.LOGIN, required = false) Member member, Model model) {
    Recipe recipe = recipeService.findRecipe(id);
    // 게시글 작성자와 현재 로그인 유저가 같으면 isWriter 전달, 수정, 삭제 버튼 표시
    if (member.equals(recipe.getMember())) {
        log.info("작성자 확인");
        model.addAttribute("isWriter", true);
    }
    model.addAttribute("recipe", recipe);
    return "/recipe/recipeView";
}

 

컨트롤러에서 member.equals(recipe.getMember()) 가 잘 작동했다.

레시피 작성자와 현재 로그인한 유저의 일치 확인 로직이 완성되었다!