FireDrago
[bobzip] JPA 엔티티의 equals 구현 (동등성) 본문
문제상황
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()) 가 잘 작동했다.
레시피 작성자와 현재 로그인한 유저의 일치 확인 로직이 완성되었다!
'프로젝트' 카테고리의 다른 글
| [bobzip] AJAX 비동기 요청을 통한 댓글 조회 (0) | 2024.07.02 |
|---|---|
| [bobzip] 다중 MultipartFile 이미지를 포함한 엔티티 수정하기 (0) | 2024.06.24 |
| [bobzip] 로컬에 저장된 이미지 표시하기 (ResourceHandler) (0) | 2024.06.14 |
| [bobzip] 사용자정의 Validation으로 중복검사 하기 (0) | 2024.06.11 |
| [bobzip] 테스트하기 편한 엔티티 생성 (Builder) (0) | 2024.06.10 |
