인프런 커뮤니티 질문&답변

아리마님의 프로필 이미지
아리마

작성한 질문수

실전! 스프링 부트와 JPA 활용2 - API 개발과 성능 최적화

버그 문의드려봅니다.

작성

·

5.3K

6

영한님 안녕하세요.

강좌들으면서 실무에서 직접 해보고 있는데 풀리지 않는 오류가 하나 등장했습니다.

ManyToOne 단방향 맵핑한 영역이 있습니다. (MemberWebUser / MemberCompany)

(다만,  PK 가 아닌 Unique 한 다른 필드로 Join 을 설정했고요..)

저장까지는 문제없이 잘 됩니다. (FK 도 원하는 필드로 저장됐구요)

그런데 이후 QueryDsl 로 두 개를 조인해서 Many 쪽의 객체만 가져오는 Select Repository 를 아래와 같이 작성했는데

....

해당 리파지토리 실행 시점에 ClassCastException : MemberCompany cannot be cast to java.io.Serializle

at org.hibernate.type.ManyToOneType.hydate(ManyToOneType.java) 에러가 발생합니다.

원인을 모르겠어요 ㅜ

답변 6

18

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요 아리마님^^

추가 설명을 해주신 내용으로 이해가 되었습니다.

결론부터 말씀드리면 관련된 엔티티들에 implements Serializable을 해주시면 될꺼에요.

왜냐하면 영속성 컨텍스트는 엔티티의 PK를 사용해서 엔티티를 관리하는데, JPA를 구현한 하이버네이트 입장에서 지금 PK가 아닌 다른 기준(UNIQUE 필드)으로 데이터를 한번 조회하고, 그 결과에 있는 PK를 다시 찾은 다음에 영속성 컨텍스트에 관리해야 합니다. 이 속에서는 엔티티를 생성하고 관리하는 복잡한 라이프사이클도 있구요. 이 복잡한 과정을 풀어내기 위해 하이버네이트 구현체는 객체를 임시로 직렬화(Serializable)해서 메모리에 올려두는 작업을 하는 것 같습니다. 결국 자바의 직렬화 기능을 사용하려면 해당 클래스에 Serializable 마커 인터페이스를 구현해야 합니다. (이것은 제가 하이버네이트 코드를 다 까본 것은 아니고, 제 추측입니다.)

JPA 표준 스펙에 모든 엔티티는 Serializable을 구현해야 한다. 라고 되어 있기 때문에 하이버네이트 입장에서는 이렇게 구현해도 문제가 없습니다.

저는 사실 이 경우는 제외하고는 Serializable가 꼭 필요한 경우를 거의 보지 못해서, 실용적인 관점에서 엔티티에 Serializable를 사용하지 않는 편입니다.

그리고 추가로 설계 관점에서 몇가지 조언을 드리겠습니다.

설계 관점에서 모든 연관관계는 PK를 보도록 설계하는 것이 좋은 설계입니다. 저는 모든 연관관계를 PK만 보도록 설계합니다.

만약 PK가 아닌 다른 컬럼을 봐야 한다면, 올바른 연관관계가 아니라 판단하고, 연관관계를 끊어버립니다.

(연관관계가 없어도 조인은 할 수 있습니다^^!)

만약 성능 관점에서 해당 컬럼이 필요하면 해당 컬럼을 역정규화합니다.

감사합니다^^ (혹시 또 잘 안되면 질문 주세요 ㅎㅎ)

모든 연관관계는 PK를 보도록 설계하는게 좋다는 말씀 이해를 못하고 있었는데... 이제 이해가 가서 무릎을 탁 치고 박수를 치며 떠납니다...! 늘 감사합니다 영한님!! 

7

김영한님의 프로필 이미지
김영한
지식공유자

ㅎㅎ 도움이 되셨다니 다행입니다^^

우선 하나씩 추가 답을 드리자면

1. 추상클래스 하나에 @ID @GeneratedValue 설정된 Long id ,그리고 등록일, 등록자, 수정일, 수정자 속성을포함시켜놓 모든 Entity 에서 이를 상속해서 사용

-> 이 부분은 BaseEntity 같은 것으로 정의해서 실무에서 보통 많이 사용을 하는데요. 저 같은 경우 등록일, 수정일은 공통으로 사용하고, 등록자, 수정자도 별도의 상위 클래스로 만드는 편입니다. 아래와 같이요.

BaseTimeEntity(등록일, 수정일), BaseEntity(등록자, 수정자) extends BaseTimeEntity

@ID는 주로 각 엔티티에서 처리합니다. 다만 @Id는 @GeneratedValue라 해도 IDENTITY 전략이면 크게 문제될 것이 없는데, IDENTITY가 아니라 똑같은 시퀀스를 모든 엔티티에서 사용한다거나 하는 것은 좀 무리가 있다고 봅니다. 그래서 시퀀스 같은 것을 사용하면, 테이블 단위로 별도의 시퀀스를 사용하는 것을 권장합니다. (테이블 전략도 마찬가지로 테이블 별로 별도의 키를 채번하는 것이 좋습니다.)

2. 연관관계를 배제하고 진행

-> 이 부분은 저도 좀 아쉽네요. 연관관계가 없으면, 나중에 fetch join 같은 것으로 성능 최적화를 편리하게 하는 이점이 사라지고,  쿼리들도 다 DTO 중심으로 흘러가기도 하구요. 핵심 비즈니스 로직도 객체 보다는 테이블 위주로 자연스럽게 흘러가게 됩니다.

조금 더 스터디를 하고, 진행하면 좋을 건데 하는 아쉬움이 있네요. 처음 할 때 재대로 JPA를 사용하고 경험을 해봐야 하는데, 연관관계가 없으면 반쪽 자리 JPA를 사용하는 것이라는 생각이 듭니다. ㅠㅠ

2

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요 아리마님^^

제 생각에 .on절이 잘못된 것 같습니다.

join을 하면서 memberCompany를 이미 별칭으로 잡았는데, on절을 보면 조인한 별칭을 무시하고 잘못 사용하고 있습니다.

on(memberWebUser.memberCompany.memberCode ... )

이 코드를 다음과 같이 변경해보세요.

on(memberCompany.memberCode ...)

추가로 이렇게 변경해도 실행은 될 것 같지만, 조금은 모호합니다. 왜냐하면, 이미 Inner 조인을 했기 때문에, 

where에서 memberCompany.memberCode.eq 부분을 처리하시는게 더 이해하기 쉬울 듯 합니다.

(이 케이스는 on으로 처리하나, where에서 하나 결과는 같을꺼에요.)

한번 해보시고 만약에 그래도 잘 안되면 실패하는 예제를 만들어서, 돌려볼 수 있게 해주세요.

그러면 끝까지 도와드릴께요^^

감사합니다.

1

Serializable관련 설명이 큰 도움이 됐습니다. 감사합니다 😁

1

아리마님의 프로필 이미지
아리마
질문자

영한님

ON 절 이하 Where 절까지 모두 삭제하고 해보아도 동일하네요.

말씀드린 바와 같이 MemberCompany의 ID를 FK 로 하지 않고

@JoinColumn 의 referencedColumnName 설정을 활용하여 다른 unique 한 필드를 FK 로 설정해서 발생하는 문제인 거 같습니다. referencedColumnName  설정을 지우면 오류가 나지 않는 걸 확인했답니다. 혹시 referencedColumnName 설정 시, 유의해야할 사항이 있을까요?

많이 경험해 보셨을 듯 한데..

이글로도 감이 안오시면 간략하게 예제 만들어서 보내드릴께요.

0

아리마님의 프로필 이미지
아리마
질문자

영한님 정말 감사합니다.  혼자서는 해결하기 어려웠을텐데 이제 이해가 되었고, 해결도 하였네요.

사실 저는 JPA 를 영한님때문에 좀 깊게 개인스터디를 하고 있는 중인데

운이 좋게도 옆팀에서 JPA 로 프로젝을 하고 있더라고요..

참여는 할 수 없지만 궁금해서 소스를 좀 보고 있는데 잘못된 길로 가고 있는 부분들이 꽤 보이고 있습니다.

예를 들면 추상클래스 하나에 @ID @GeneratedValue 설정된 Long id ,

그리고 등록일, 등록자, 수정일, 수정자 속성을포함시켜놓고

모든 Entity 에서 이를 상속해서 사용하고 있었습니다.

일자 같은 부분은 공통 속성으로 문제가 없을 것 같지만

@GeneratedValue 설정된 id 를 공통으로 사용하다보니 위에서 질문드린 상황이 나타나게 되네요..

무엇보다 JPA를 경험해 보지 못한 분들이 플젝을 하다보니 연관관계 맵핑에 어려움이 많아

연관관계를 배제하고 진행하기로 결정을 했다고 합니다.

그래서 지켜보는 입장에서 참 답답하다는 생각이  많이 들고 있답니다.

위와 같이 플젝을 진행해도 문제없이 잘 완료할 수 있을까요? ㅎㅎ

아리마님의 프로필 이미지
아리마

작성한 질문수

질문하기