-
[Spring JPA] referencedColumnName 사용시 주의점 (Feat. 프록시)Back End/Spring Data JPA 2022. 1. 21. 15:49
본론부터 말씀드리자면
referencedColumnName을 사용하여 엔티티를 연결한 경우
정체모를 쿼리가 하나 더 나간다.
이 현상이 왜 발생하는지, 그리고 이를 어떻게 해결하는지에 대한 포스팅이다.
(해결방법은 찾지 못했다 😢)[들어가기 앞서]
referencedColumnName 은 비식별관계 FK 를 만들때 사용한다.
꼭 FK만 사용하는건 아니고, 영문 그대로 레퍼런스로 참조할 칼럼을 선택하는 것이다.
코드로 예를 들어보겠다.
# Child 엔티티
@Entity @Getter @Setter @NoArgsConstructor @ToString public class Child { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) Long id; String childItem; @Column(name="parent_id") Long parentId; @ManyToOne(fetch = FetchType.LAZY, targetEntity= Parent.class ) @JoinColumn(name="parent_id", insertable = false, updatable = false, // foreignKey = @ForeignKey(value=ConstraintMode.NO_CONSTRAINT), referencedColumnName = "item_id") @ToString.Exclude Parent parent; }
위는 Parent 엔티티를 다대일 관계로 맺고 있는 Child 엔티티인데,
Child 엔티티의 parent_id 칼럼을 조인컬럼으로 사용할 것이며, Parent 엔티티의 item_id 를 참조하겠다고 선언한 것이다.
# Parent 엔티티
@Entity @Getter @Setter @NoArgsConstructor @ToString public class Parent implements Serializable { @Id @GeneratedValue(strategy=GenerationType.IDENTITY) Long id; @Column(name="item_id") Long itemId; String parentItem; @OneToMany(fetch = FetchType.LAZY, mappedBy = "parent") @ToString.Exclude List<Child> children; }
둘다 FetchType은 Lazy이며, 연관관계의 주인은 Child 이다.
거두절미하고, referencedColumnName 을 사용할때 마주하게되는 문제점에 대해 알아보자.
# 테스트 코드
@SpringBootTest public class multiTest { @Autowired ChildRepo childRepo; @Autowired ParentRepo parentRepo; @Autowired EntityManager em; @Test @Transactional void tesT(){ Parent parent = new Parent(); parent.setParentItem("parentItem"); parent.setItemId(2L); Child child = new Child(); child.setChildItem("childItem"); child.setParentId(parent.getItemId()); Child child2 = new Child(); child2.setChildItem("childItem2"); child2.setParentId(parent.getItemId()); parentRepo.save(parent); childRepo.save(child); childRepo.save(child2); em.flush(); em.clear(); // 예상 쿼리 // 1. parentRepo.findById 로 인한 select 절 1개 // 2. getChildren() 호출 시, Lazy 하게 Fetch 되어 나가는 select 절 1개 System.out.println("1################"); Parent resultParent = parentRepo.findById(1L).get(); System.out.println(resultParent.getChildren()); // 실제 쿼리 // 1. parentRepo.findById 로 인한 select 절 1개 // 2. getChildren() 호출 시, Lazy 하게 Fetch 되어 나가는 select 절 2개 em.flush(); em.clear(); // 예상 쿼리 // 1. childRepo.findById 로 인한 select 절 1개 // 2. getParent() 호출 시, Lazy 하게 Fetch 되어 나가는 select 절 1개 System.out.println("2################"); Child resultChild = childRepo.findById(1L).get(); System.out.println("3################"); System.out.println(resultChild.getParent()); // 실제 쿼리 // 1. childRepo.findById 로 인한 select 절 2개 // 2. getParent()는 이미 영속화 되어있는 것을 불러오므로 sql 문을 호출하지 않는다. } }
# 테스트코드 실제 수행시 나가는 쿼리
정말 이상하지 않은가?
referencedColumnName 에 대해 구글링을 진행하면, 모두 어떻게 사용하느냐, 비식별관계를 어떻게 맺느냐에 대해 초점이 맞춰줘 있을뿐 왜 이런식의 쿼리가 나가는지에 대한 글은 찾아볼 수가 없다.
그냥 유명인의 강의나 공식문서를 퍼나를 뿐이다...
출력문 1번의 마지막 쿼리와 출력문 2번의 마지막쿼리를 보라.
정말 쓸데 없는 쿼리지 않는가? 왜 부모에서 자식을 가져왔는데 부모를 한번 더 검색해야하는지...
또 자식에서 getParent()를 호출하지 않았음에도, EAGER Fetch 마냥 왜 부모를 들고오는지 정말 이해가 안됬다.
필자는 왜 Lazy 인데 Eager 인가에 초점을 맞추어 삽질을 엄청했다.
" 하이버네이트가 referencedColumnName 을 사용하는 순간 Eager 로 매핑하나보네? "
" 그럼 이 설정은 어디서 만질 수 있지? "2월전에 개인프로젝트를 끝내야하는 나는 구현에 초점이 맞춰져 저런 생각만 하고 있었다.
그리고 하이버네이트의 EAGER는 JOIN으로 분명 들고 올텐데, 이 케이스는 뭐가 특별하기에 JOIN 없이 두번 Select하는지도 참 의문이었다. 무튼 하이버네이트의 구성에 의구심을 품은 난 하이버네이트 위주로만 코드를 뜯어보거나 검색을 하고 있었다.
원하는대로 안되는 건 둘째치고, 왜. 왜. 왜. 이따위의 쿼리가 나가는지 너무너무 이해하고 싶었다.
그러다 몸살에 하루 몸저 누워버렸다... 😥
[ 나타난 구세주님 ]
필자는 운이 좋게 JPA 스터디를 현업자분들과 하고 있는데, 스터디원 모두가 다들 너무너무 똑똑하시다...
그 중 한분은 수학과를 나오셔서, 개념을 탄탄히! 그 개념으로 응용문제를 풀면되지! 라는 느낌의 접근을 하셨는데
그로 인해 이 모든 궁금증이 풀려버렸다...
(나도 그 개념은 알고있었는데 왜 개념부터 차근히 생각할 생각을 안했을까... 구현에만 너무 집착하고 있었던 것인가...하며 반성하게 되는 시간이었다.)
[ 구세주님의 사고흐름 ]
childRepo.findById(1L) 에서 2개의 쿼리가 나가네?
Hibernate는 Eager Fetch 할 시, Join 문이 나가는데 Join 이 없는걸로 보아 저것은 Lazy Fetch 이다.
그럼 저 쿼리는 무엇인가를 초기화한다는 것인데, 그게 프록시 일 것이다.너무 너무 대단하지 않은가...? 너무나도 깔끔한 가감법에 할말을 잃었다.
그리고 그분은 프록시에 초점을 맞춰 프록시에는 PK가 필요하다는 정보를 얻고 난 후, 이렇게 정리하신다.
같은 맥락이니 더 설명하기 쉬운 childRepo.findById(1L) 에 대해 설명하겠다.
childRepo.findById(1L) 를 호출 했을 경우,
"Select child.parent_id from child" 쿼리가 나간다.
Hibernate는 child.parent_id 라는 필드를 가져오는 것이 아니라, parent라는 객체를 가져오도록 설계되어있다.
그리고 그 객체를 직접 접근하려면 쿼리가 나가게 되는데, 이를 방지하기 위해 프록시 객체를 넣어둔다.
그런데 프록시 객체의 필수요소는 PK이다.
프록시의 PK를 제외한 다른 필드에 대한 접근이 생길때
PK를 기준으로 영속성컨텍스트에서 존재유무를 파악한 뒤,
존재하지 않으면 쿼리를 날려 프록시의 다른필드들을 초기화 한다.
해당상황은 findById 로 인한 쿼리로 child.parent_id 라는 필드의 값을 Hibernate가 알아냈지만
child.parent_id 는 parent.item_id 이므로 PK가 아니다.
즉 Hibernate는 parent 프록시를 생성해야되는데 알고 있는 값이 PK가 아니라 PK가 필요하다.
또는 PK없이 프록시를 생성해서 Proxy.getItemId() 가 호출된 상황이더라도 PK가 비어 있으니 같은 상황이다.
두 경우 모두, PK가 없는 상황이기에 select 쿼리를 날려 parent 프록시에 필요한 PK값을 찾는 것이다.
그래서 저러한 쿼리가 하나 더 나가는 것이다.!!!!!!!!!!!!!! 대 박 사 건 !!!!!!!!!!!!!
와... 정말 대단하시지 않은가... 너무너무 논리적이여서 할말을 잃어버렸다...
심지어 저분은 프록시에 PK가 필요하다는 정보를 '프록시가 문제겠군' 이라는 사고 흐름에 따라 검색하셔서 알아내셨다.
필자는 이미 혼자 프로젝트에서 뻘짓을 하며, 체득화한 정보로 프록시에는 PK가 필요하다라는 것을 알고 있었던 상황인데...
저런 생각을 감히 하지 못했다....
정말 많은 것을 배운 하루였고, 사고방식과 접근법을 바꾸어야겠다는 생각이 드는 하루였다...
감사합니다. 종혁님... 당신은 정말 G.O.D....
[마치며]
그럼 저 쓸데없는 쿼리를 줄일 수 없을까?
원인을 알았으니 댕근 가능한줄 알았다.
PK가 존재하는 Parent 프록시를 넣어주고 싶은데...
getReference 로 될 줄 알았는데 프록시의 setter/getter 호출 시 Hibernate는 초기화시킨다는 사실을 다시 절감했다.
방법을 모르겠다 😥
[추가 수정 - 해결방안, 2022.02.17]
예전에 관련 검색어로 구글링을 신나게 하다가 영한님이 "설계 관점에서 모든 연관관계는 PK를 보도록 설계하는 것이 좋은 설계입니다." 라는 답변을 남기신 적이 있는데, 그때는 저 말을 잘못 이해했다... FK가 PK로 안엮여진 ERD를 많이 봐왔는데 그렇게 설계된 DB에 맞게 ORM을 장착해야 할 경우도 있을것인데... 안쓸수가없는 상황이 올텐데... 회사를 때려칠 수 도 없고, DB를 다 갈아엎을 수 도 없는거 아닌가.... 했었다.. 결국 referencedColumnName 을 쓰지 말라는 이야기인가... 했었다...
해당 오해는 내가 DB와 ORM을 분리해서 생각을 하지 않고 있어서 생긴 오해였다...
(그리고 저 한문장만 보고 뒤 문장을 읽지도 않았음.... 바보)
영한님의 "설계 관점에서 모든 연관관계는 PK를 보도록 설계하는 것이 좋은 설계입니다." 라는 말은 지극히 ORM의 입장에서 하신 말씀이다.
영한님이 언급하신 연관관계 란 정말 단순하게 ORM 의 연관관계(@OneToMany, @ManyToOne 등)이다.
그러니까 DB입장에서 PK를 바라보지않는 FK는 연관관계어노테이션으로 엮지말고, 단순하게 조인쿼리를 만들어 날리라는 말씀이었다.
아래는 영한님의 답변이다.
설계 관점에서 모든 연관관계는 PK를 보도록 설계하는 것이 좋은 설계입니다. 저는 모든 연관관계를 PK만 보도록 설계합니다.
만약 PK가 아닌 다른 컬럼을 봐야 한다면, 올바른 연관관계가 아니라 판단하고, 연관관계를 끊어버립니다.
(연관관계가 없어도 조인은 할 수 있습니다^^!)
만약 성능 관점에서 해당 컬럼이 필요하면 해당 컬럼을 역정규화합니다.답변 출처 : https://www.inflearn.com/questions/16570#
[GOD - 종혁님 블로그]
728x90'Back End > Spring Data JPA' 카테고리의 다른 글
[Spring JPA] @OneToOne 에서 @MapsId 를 이용해 컬럼갯수를 줄여보자! (0) 2022.01.26 [Spring JPA] @OneToMany 에서 @MapsId 사용하기 (2) 2022.01.24 [Spring JPA] 상속관계 매핑의 문제점 (@Inheritance) (0) 2022.01.19 [Spring JPA] 슈퍼-서브타입 관계 모델링 (상속관계 매핑) (0) 2022.01.14 [Spring JPA] 단방향, 양방향 결정기준과 연관관계의 주인 (0) 2022.01.14