일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | ||||
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 |
- C++
- springboot
- Java
- spring
- API
- 해싱
- index.html
- SpringFramework
- TDD
- DB
- JPA autiding
- SpringSecurity
- baekjoon
- Comparable
- 프로그래머스
- SpringDataJPA
- tdo
- gitignore
- Gradle
- JPA
- 알고리즘
- SpringBean
- DynamicProgramming
- rombok
- mustache
- kotlin
- 코딩테스트
- 알고리즘 #코딩테스트 #프로그래머스 #C++ #vector #string
- testcode
- test case
- Today
- Total
천천히, 한결같이
[Spring] {entity class} cannot be cast to java.lang.comparable 해결하기 본문
[Spring] {entity class} cannot be cast to java.lang.comparable 해결하기
Donghwan Lee 2022. 7. 20. 18:50본 코드는 필자의 프로젝트 백엔드 파트의 Domain 계층에 해당하는 부분입니다. SpringBoot 라이브러리를 사용해 개발하고 있으며, Kotlin 언어로 개발중입니다. 학부생이 스스로 공부하는 내용이라 틀린 부분이 있을 수도 있고, 잘못된 부분이 많을 수도 있습니다. 언제나 많은 지적바랍니다.
Entity class, Many To Many
- 현재 필자의 코드에서 오류가 생긴 부분입니다. Users 엔티티와 Planners 엔티티는 다대다 양방향 매핑이 되어 있는 상태입니다.
- 따라서 JPA를 사용하기 때문에, Users와 Planners는 각각의 객체 안에 Planners, Users를 가지고 있는 상태입니다.
- Spring Data Jpa를 사용하면 객체지향적으로 조인 관계를 활용할 수 있습니다. 아래 엔티티 클래스 코드를 보면서 같이 확인해 보겠습니다.
package com.entrip.domain.entity
import com.entrip.domain.BaseTimeEntity
import org.springframework.data.jpa.repository.config.EnableJpaAuditing
import java.util.*
import javax.persistence.*
@Entity
class Users(
@Id @Column(name = "USER_ID")
val user_id: String? = null,
@Column
@ManyToMany (fetch = FetchType.EAGER)
@JoinColumn(name = "PLANNERS_USERS")
var planners : MutableSet<Planners> = TreeSet(),
@Column
@OneToMany (mappedBy = "users", fetch = FetchType.EAGER, orphanRemoval = true)
//Check "orphanRemoval = true" is possible
var comments : MutableSet<Comments> = TreeSet(),
@Column
val nickname: String,
var gender : Int? = null,
var photoUrl : String? = null,
var token : String? = null
): BaseTimeEntity(){
public fun addPlanners(planners : Planners) : Long? {
this.planners.add(planners)
return planners.planner_id
}
public fun updateToken(token: String) : String {
this.token = token
return token
}
}
package com.entrip.domain.entity
import com.entrip.domain.BaseTimeEntity
import org.springframework.data.jpa.repository.config.EnableJpaAuditing
import java.time.LocalDateTime
import java.util.TreeSet
import javax.persistence.*
@Entity
class Planners (
@Id
@GeneratedValue(strategy = GenerationType.*IDENTITY*)
@Column(name = "PLANNER_ID")
var planner_id : Long? = null,
@Column
var title : String = “제목 없음”,
var start_date : String,
var end_date : String,
var comment_timeStamp : LocalDateTime = LocalDateTime.now(),
@Column
@ManyToMany(fetch = FetchType.*EAGER*)
@JoinColumn(name = “USERS_PLANNERS”)
var users : MutableSet<Users> = TreeSet(),
@Column
@OneToMany (mappedBy = “planners”, fetch = FetchType.*EAGER*)
var plans : MutableSet<Plans>? = TreeSet()
): BaseTimeEntity() {
fun update (title: String, start_date: String, end_date: String) : Unit{
this.title = title
this.start_date = start_date
this.end_date = end_date
}
fun addUsers (users : Users) : String? {
this.users?.add(users)
return users.user_id
}
fun setComment_timeStamp () : Unit {
this.comment_timeStamp = LocalDateTime.now()
return Unit
}
}
이렇게 두 엔티티를 작성하고, 통합 테스트를 위해서 Postman으로 테스트를 진행하였습니다. Users 엔티티의 경우 저장이 잘 되었습니다. 그런데 Planners 엔티티를 저장하였더니
{
“httpStatus”: 520,
“message”: “handleEntityNotFoundException : unknown exception\ncom.entrip.domain.entity.Users cannot be cast to java.lang.Comparable”,
“data”: null
}
위에서 보시는 바와 같이 {Entity class} cannot be cast to java.lang.Comparable 오류가 발생했습니다. 도대체 왜 이런 예외가 생긴 걸까요?
양방향 매핑과 TreeSet
문제는 바로 양방향 매핑 을 구현한 코드 부분에서 있었습니다. 위의 planners 클래스에 entity 클래스 일부를 다시 가져와서 보면
@Entity
class Planners (
@Id
@GeneratedValue(strategy = GenerationType.*IDENTITY*)
@Column(name = "PLANNER_ID")
var planner_id : Long? = null,
,,,중략
@Column
@ManyToMany(fetch = FetchType.*EAGER*)
@JoinColumn(name = “USERS_PLANNERS”)
var users : MutableSet<Users> = TreeSet(),
,,,생략
){
,,,생략
}
문제가 되는 부분이 보이시나요? JPA를 사용해서 다대다 매핑을 구현하였는데, Planners라는 클래스 안에 있는 Users를 MutableSet = TreeSet()으로 구현하였습니다.
TreeSet의 경우 자동 정렬이 되서 출력이 되고, 중복되는 값은 허용하지 않습니다. 여기서 저희가 주목할 점은 자동 정렬이 되서 출력된다는 점입니다. 잠깐, 자동 정렬이 되어 출력된다면 분명 TreeSet 내부에서 각각의 객체들을 비교해서 정렬을 해야 합니다. 다시 저희에게 문제가 되었던 예외 메세지를 살펴보면 다음과 같습니다.
com.entrip.domain.entity.Users cannot be cast to java.lang.Comparable
이제 실마리가 조금 풀리기 시작합니다. Users 를 구성 요소로 하는 TreeSet에서 Users를 정렬할 수 없기 때문입니다! 그럼 어떻게 이 문제를 해결해야 할까요? 바로 Users 클래스가 Comparable를 상속받도록 하고, 두 Users를 비교하기 위한 메소드인 compareTo 메소드를 override하면 됩니다!
@Entity
@EnableJpaAuditing
class Users(
,,,중략
): BaseTimeEntity(), Comparable<Users> {
,,,중략
public override fun compareTo(other: Users): Int {
if (this.user_id!! >= other.user_id!!) return 1;
return -1;
}
}
다음과 같이 코드를 수정 후 실행하면 이제 정상적으로 작동하는 것을 알 수 있습니다! 저희 코드에서는 user_id라는 @Id인 String의 사전식 정렬을 할 수 있도록 위와 같이 코드를 구현하였는데, 만약 다른 기준으로 정렬을 하고 싶다면 compareTo 메소드 내부에서 if문의 조건을 잘 조절한다면 되겠죠?
자, 그런데 여기서 하나의 의문이 생길 수 있습니다. 그럼 왜 Planners 클래스는 아무런 문제 없이 잘 작동할까요? 다대다 양방향 매핑이기 때문에 반드시 Users 클래스 내부에도 Planners 클래스가 TreeSet으로 존재할텐데 말이죠. 바로 @Id값을 기준으로 비교 연산이 일어났기 때문입니다. Users 클래스와 Planners 클래스의 가장 큰 차이점 중 하나는 바로 @Id의 값의 타입이 다르다는 겁니다. 일반적으로 대부분의 엔티티 클래스가 @Id값을 Long으로 쓰는 반면, 저의 프로젝트에서는 각각의 Users의 이메일 주소를 @Id값으로 사용하고 싶어서 그렇게 선언을 했고, 그 때문에 문제가 생긴 것 같습니다.
ORM을 완벽하게 숙지하고 사용하자
ORM이라는 것은 정말 편리한 것 같습니다. SpringDataJpa를 사용하면서 가장 크게 느낀 편리함은 첫 번째, 쿼리를 직접 작성하지 않아도 된다는 점과 두 번째, 조인 관계를 마치 객체들처럼 참조하는 것이 가능해서 보다 코드 다운 코드를 작성할 수 있습니다.
하지만 이러한 세부적으로 동작하는 원리들을 무시한 체 그냥 편한 도구 정도로 사용한다면 예기치 못한 문제가 많이 생기는 것 같습니다. (Cascade나 조인 되어있는 두 객체를 delete하는 등) 얼마 전 백기선님의 유튜브에서 “이해하지 못하면 JPA 다시 공부하세요” 라는 내용의 유튜브를 본 적이 있는데, 그 장면이 새록새록 기억납니다.
개발자들이 보다 편리하게 개발할 수 있도록 만들어준 다양한 라이브러리들이 존재하지만, 역시 항상 가장 기초를 튼튼히 하고 배경 지식을 잘 쌓은 상태에서 해당 라이브러리들을 충분히 이해하고 숙지해야 겠다는 생각이 드는 하루였습니다.
'Spring' 카테고리의 다른 글
[Spring] 코드 재사용으로 클린 코드 만들기 (0) | 2022.07.18 |
---|---|
[Spring] 프로젝트 생성 시 spring security 사용 후 localhost:8080/login (0) | 2022.02.27 |
[Spring] IntelliJ exit with non-zero value 1 오류 (0) | 2022.01.18 |
[Spring] 스프링 시큐리티, OAuth2를 사용해서 Google 서비스 등록하기 (0) | 2022.01.08 |
[Spring] 글 등록 화면 만들기 (0) | 2022.01.02 |