당니의 개발자 스토리
실전 예제 2 - 연관관계 매핑 시작 본문
실전 예제 2 - 연관관계 매핑 시작
이번 시간은 실전 예제 두 번째 시간입니다.

지난 시간에는 연관관계를 몰랐으니까 연관관계 맵핑을 안하고 단순하게 DB 테이블에 맞춰서 설계를 했는데 그것을 객체스럽게 실전 예제 1번에다가 연관관계 맵핑을 씌워보겠습니다.

보시면 기존과 테이블 구조는 완전히 동일하고요. 하나의 Member가 여러 개의 주문을 할 수 있는 일대다 구조고, 주문과 아이템이 있는데 하나의 주문이 여러 개의 아이템을 주문할 수 있기 때문에, 다대다를 일대다, 다대일로 풀어낸 ORDER_ITEM 이라는 테이블이 있습니다. 그래서 실전 예제 1번이랑 테이블 구조는 완전히 동일합니다.
그리고 이제 참조를 사용하도록 바꿔 볼게요.

결론적으로는 보시면 Member가 Order와 일대다고, Order가 Item과의 관계를 일대다, 다대일로 풀어내는데 보시면 여기 Member는 orders 라는 리스트를 가지고 있어요. Order는 주문의 입장에서 '나를 주문한 회원은 누구야' 이렇게 member를 가지고 있죠. 그리고 또 orderItems가 있어서 OrderItem으로 갈 수 있고, OrderItem은 Item으로 이렇게 갈 수 있죠. 그래서 거의 대부분의 연관관계를 다 세팅을 해놨습니다.
Item 입장에서 보면 물론 OrderItem 컬렉션을 가질 수 있는데 이런 것들은 뺐구요. 이거를 가지고 이제 한번 바꾸면서 보겠습니다.
제가 이전에 연관관계 맵핑에서 설명을 드렸듯이 가장 먼저 중요한 것은 단방향 연관관계를 맵핑하는 게 중요해요.
단방향 연관관계를 맵핑할 때 또 중요한 게 뭐였냐면 외래키, 연관관계 주인을 누구로 정할 거냐라고 말씀드렸죠.

그래서 이 외래키가 있는 MEMBER_ID를 그냥 연관관계 주인으로 생각하시고, 이걸 가지고 단방향 맵핑을 하면 된다고 이전 시간에 설명을 드렸고요. 이제 그 구조로 설계를 해보겠습니다.
그러면 이제 먼저 고민해야 될 게 뭐냐면 Member랑 Order 테이블 관계를 보면 Member가 1이고 Order가 N이잖아요. 그러면 Member랑 Order의 관계에서는 여기 ORDERS에 있는 MEMBER_ID만 잘 맵핑해주면 된다는 거예요. 그럼 이제 단방향으로 맵핑해서 설계를 해주면 돼요.
그리고 이제 양방향이 필요하면 그때 Member 객체에다가 orders를 넣으면 되구요.
자 코드를 한번 보여드리겠습니다.

일단 필요없는 코드는 먼저 지울게요.

그러면 이 Order 입장에서는 나를 주문한 Member가 필요하겠죠.

그래서 이제 더 이상 이걸 쓰는 게 아니라,

member 필드를 만들어주는데, 연관관계 맵핑이 안되어 있으니까 빨간불이 뜨겠죠. 인텔리제이가 이걸 해주는데 아무튼 Order 입장에서는 Member와 @ManyToOne이 되겠죠. Member 입장에서는 하나의 회원이 여러 개의 주문을 날리니까, 즉 주문을 여러 번 할 수 있으니까 일대다인데 반대로 주문의 입장에서는 나를 주문한 회원은 하나겠죠?

그래서 @ManyToOne 이라고 보시면 되고, 이제 @JoinColumn 으로 맵핑을 해줘야죠.

이렇게 해주면 되겠죠. 그러면 연관관계 맵핑이 다 끝납니다. 그리고 Getter Setter를 바꿔주고,

이제 여기 보시면 이전에는 memberId 외래키 값을 맵핑해서 그대로 가지고 있었는데, 엔티티에서 이제는 더이상 필요가 없어지는 거죠.

그래서 member, 이 레퍼런스가 테이블 구조에 있는 MEMBER_ID랑 맵핑이 되는거죠. 이렇게 하시면 되고 만약에 이제 양방향 맵핑을 하고 싶다고 하면,

반대로 여기 Member에다가는 컬렉션을 넣으면 되죠. 근데 이거는 이제 나중에 할게요. 제가 이전 시간에도 한번 설명을 드렸는데 가급적이면 단방향 맵핑이 좋은 거예요. 객체 입장에서 양방향이라는 것은 뭔가 양쪽으로 신경을 써야 되기 때문에 복잡도가 되게 늘어나거든요.

그래서 가급적이면 설계할 때는 단방향으로 딱 해놓고 실제 개발하다가 필요하면 양방향으로 넣는 게 좋다고 그렇게 설명을 드렸었죠.

이제 Member는 다 됐고, OrderItem을 해결해야겠죠. OrderItem 입장에서 보면은 ORDER가 1이고 ORDER_ITEM은 N이란 말이예요. 당연히 테이블 설계상 다 쪽에 외래키가 존재하게 되죠. 그러면 OrderItem으로 가서

여기 보시면 아이디를 그대로 둘 다 들고 있는데 이렇게 하는게 아니라,

이렇게 잡으면 됩니다.

자 그 다음에 여기 보시면 테이블 구조에서 ORDER_ITEM 입장에서 ITEM과의 관계는 ORDER_ITEM이 다이고 ITEM이 1이죠.
그러니까 이렇게 가져가는 겁니다.

그러면 이제 이 두개가 필요없어지죠.

getter, setter도 다시 잡구요.

그럼 코드를 보시면 ORDER_ITEM도 foreign key, 외래키 값을 그대로 가지는 게 아니라 Order랑 Item이라는 객체를 가지고 있죠. 이렇게 되면 나중에 OrderItem 객체를 조회해서 필요하면 getOrder, getItem을 할 수가 있는 거죠.

자 그렇고 ITEM 같은 경우에는 따로 세팅한 게 없어요. 이게 이제 왜 그러냐면 ITEM 입장에서 필요하면 양방향으로 만들 수도 있어요. 하나의 아이템을 보고 이 아이템의 ORDER_ITEM을 연관관계로 추적하고 싶으면 양방향으로 만들 수는 있는데 잘 생각해보시면 그렇게 할 일이 거의 없어요.
주문 입장에선 상품이 뭐가 들어왔는지 되게 중요한데, 상품 입장에서 보면 어떤 주문에 의해서 됐는지가 연관관계를 찾아갈 만큼 중요하지가 않아요. 왜냐하면 대부분 주문에서 상품으로 참조가 넘어오지, 상품 자체를 보고 주문을 찾진 않는단 말이에요. 뭐 나중에 데이터베이스 통계를 내거나 이럴 때는 중요할 수 있겠지만 그게 아니면 어떤 주문들로 팔렸는지가 실시간 애플리케이션은 그렇게 중요하지 않아요. 그래서 아무튼 이렇게 설계를 했고,

이제 양방향에 대해서 고민을 해볼게요. 애플리케이션을 막 개발하고 있는데 하다보니까 Member 입장에서 orders, 주문 목록이 중요해진 거예요. 이거는 비즈니스마다 케이스 바이 케이스인데, 대부분의 경우에는 이 Member의 orders를 넣는게 그렇게 좋은 설계는 아니에요.

왜냐하면 뭔가 예를 들어서 테이블 입장에서 쿼리를 해도 여기 보면 MEMBER_ID가 이미 있단 말이에요. 그러니까 이걸 이용해서 쿼리를 날리잖아요. 무슨 얘기냐면, 실시간 애플리케이션에서 특정 회원의 주문 내역을 보고 싶어요. 그러면 이미 여기에 MEMBER_ID 라는 foreign key 값에 memberId가 들어가 있단 말이에요. 그러니까 그거를 가지고 query를 날려서 시작을 하지,

사실 여기 있는 Member를 찾아서 getOrders를 해가지고 주문내역을 뿌린다? 그건 이제 약간 설계를 잘못했다고 생각을 하거든요. 그게 이제 너무 복잡하게 설계를 한 거죠. 약간 관심사를 끊어내야 될 거를 제대로 못 끊어낸 거죠.
그러니까 막 이게 객체 지향이라고 해서 머릿속으로 생각할 때는 회원 입장에서 주문을 가야겠지? 뭘 해야지 하면 사실 이게 끝이 안 나요. 여기서 잘 끊어내는 게 설계에서 되게 중요하거든요. 특히 연관관계를 잘 끊어내는 게 되게 중요한데 회원은 그냥 거의 회원만 가지고 있으면 돼요. 주문이 필요하면 주문으로부터 시작을 하면 되는 거죠. 쿼리 입장에서 생각해봐도 JPQL 같이 나중에 그런 걸 짜셔도 할 수 있거든요. Order 파라미터에 Member를 그대로 넘길 수 있어요. 이것만 알면 되는 거지 Member가 굳이 Orders를 알 필요는 거의 없는 거예요. Order부터 시작하면 되는거죠.

그래서 회원은 거의 할일이 없는데 이건 이제 예제니까

만약에 이거를 양방향 관계로 가지고 가고 싶다 라고 하면, Order에서 이게 지금 @ManyToOne니까

반대는 이렇게 하면 되겠죠. @OneToMany는 당연히 컬렉션이 되어야 겠죠.

그리고 jpa나 Hibernate 에서는 관례상 초기값을 보통 new ArrayList<>()를 사용합니다. 이렇게 하면 약간 메모리를 조금 쓸 수 있긴 한데, 데이터 없이 막 넣어서 NullPointerException이 생기는 것도 방지하고 여러가지 장점들이 있어요.
그럼 이제 연관관계 주인을 누가 가지고 가야되죠?

mappedBy 해서 당연히 연관관계 주인은 Order에 있는 member가 되겠죠.

넘어가서 얘가 연관관계 주인이죠. 왜냐면 member가 이미 외래키를 관리하겠다고 지정을 했기 때문에 이렇게 잡으면 되고, 필요하면 이제 필요한 메서드들을 추가해주면 됩니다.

그 다음에 이제 Order 입장에서는 비즈니스적으로 OrderItem이 의미가 있어요. 주문서를 뽑았는데 그 주문서와 연관된 아이템 목록을 가지고 찾거나 이럴 때가 되게 많잖아요. 그래서 이러한 케이스는 많이 쓰입니다. 주문서를 중심으로 어떤 아이템들이 필요해 이건 비즈니스적으로 의미가 있을 확률이 높아요. 그런데 물론 다른 비즈니스에서는 의미가 없을 수도 있어요. 비즈니스마다 다르기 때문에.
그래서 Order 입장에서 OrderItem과 양방향인 연관관계를 가지고 싶어요. 그러면 OrderItem으로 먼저 가서,

OrderItem 입장에서는 이것의 반대 방향을 만들어야 되는 거죠.

그러면 Order로 가서 이렇게 mappedBy를 해서 '이 연관관계 주인은 누구야' 라고 하면,

당연히 OrderItem에 있는 이 외래키로 지정한 order가 연관관계 주인이 됩니다.

그러니까 이 ORDER_ID랑 맵핑한 게

지금 OrderItem에 있는 여기 order 거든요. 이게 연관관계 주인이 돼야 돼요. ORDER_ID랑 딱 맵핑이 돼있기 때문에

이렇게 order로 잡으시면 됩니다. 그 다음에 필요한 연관관계의 편의 메서드 이런거는 쭉쭉쭉 여러분이 만드시면 되고 뭐 이제 비즈니스마다 좀 다르긴 한데 저라면 이렇게 만들 것 같네요.

예시 코드를 만들면, 이제 주문을 해야 된다고 하면,

수도 코드로 해서, Order를 만들 것 같구요. order.addOrderItem 이런식으로 만들 것 같아요. 아니면 그냥 addItem으로 만들어서 어떻게 지지고 볶던가.

그래서 여기서 newOrderItem() 뭐 이런식으로 들어가고 얘를 연관관계 편의 메서드로 만들겠죠.

이렇게. 그리고 지금 이게 양방향의 연관관계이기 때문에 연관관계 편의 메서드를 굳이 만든 거예요.

orderItems 에다가 add 해서 방금 한 orderItem 집어넣고 그 다음에 orderItem.setOrder를 해서 this를 넣어줍니다. 현재 나의 Order를 넣어서 이 양방향 연관관계가 딱 걸리도록 걸겠죠.

이렇게 하면 주문 객체를 만들어서 원하는 OrderItem들을 쭉쭉쭉 넣을 수 있겠죠.

물론 OrderItem에는 Item이 들어있어야 되고 또 orderPrice랑 count 이런건 다 들어가야 됩니다.
아무튼 이렇게 해서 여기까지 하면 저희가 이전 시간에 정말 단순하게 연관관계 없이 했던 거를 연관관계로 쭉 만들어낼 수가 있는 거죠.

자 그래서 이게 여기서 사실 이제 이 List들 orders랑 orderItems는 사실 없어도 아무 문제가 없어요. 왜 문제가 없냐면 이전 시간에도 이제 말씀드렸지만 연관관계를 맵핑 하는 것 자체는 아무 문제가 없는 거에요.

그러니까 예를 들어서 이 케이스도, Order에 OrderItem을 과연 이렇게 꼭 넣어줘야 될까? 이렇게 안해도 돼요.
왜냐하면 코드를 이런식으로 짜도 되거든요.

이렇게 짜셔도 돼요. 무슨 얘기냐면 양방향 연관관계가 아니어도 애플리케이션 개발하는데 아무 문제가 없어요.

설명드렸듯이 중요한 건 이 단방향 연관관계만 돼도 애플리케이션은 그냥 커맨드성은 다 개발할 수가 있어요. 그런데 이제 orders나 orderItems 같은 List들을 써서 양방향 연관관계를 만드는 이유는 어떤 개발상의 편의 등의 이유로 나중에 조회할 때 만드는 거지, 말씀드렸듯이 이제 연관관계 주인이 아닌 거는 할 수 있는 게 조회밖에 없거든요.
근데 이제 뭐 예를 들어서 이럴 수 있잖아요. 되게 편리하게 'Order 엔티티를 딱 조회했을 때 바로 OrderItem을 알고 싶어' 라고 그럴 때 나중에 이 양방향 연관관계를 넣으시면 되고, 제가 단방향 관계만 세팅을 해도 설계가 다 되고 필요할 때 양방향을 넣으면 된다고 설명을 드렸는데 이게 또 실전에서 조금 애매한 게 있어요.

뭐가 애매하냐면 실전에서는 나중에는 결국 jpql이라는 걸 많이 작성을 하시게 되거든요. 그러면 약간 경험을 해봐야 되는데 실전에서 막 jpql을 짜요. 근데 Order를 조회할 때 jpql을 짤건데 OrderItem을 막 끌고 오고 싶단 말이에요. 그러면은 사실 이게 뭐라도 있어야 되는 거예요. 그리고 JPQL을 실무에서 복잡하게 짜는 이유들 때문에 양방향 연관관계를 주로 많이 걸게 됩니다. 그 정도 말씀드리고요.

아무튼 핵심은 할 수 있으면 최대한 단방향으로 해라. 그런데 실무에서 하다 보면 아무래도 조회를 좀 더 편하게 하고 JPQL을 작성할 때 좀 더 편하게 작성하려다 보니 양쪽 방향으로 조회해야 될 일이 많이 생기게 됩니다. 그리고 뭐 하시다 보면 양방향 연관관계가 아닌데, 필요하면 OrderItem을 가져오고 싶잖아요.

그러면 OrderItem을 다시 조회하면 돼요.
그런데 이제 아무래도 아까 말씀드렸듯이 뭔가 좀 더 객체지향적으로 짜고 싶은 이유들도 생기고 하다보면 비즈니스상 양쪽으로 다 걸리는게 조금 더 애플리케이션 개발하기에 더 순조로울 때가 있어요. 그럴 때는 양방향을 선택해서 넣으시면 됩니다.
핵심은 단방향을 잘 설계하는 게 가장 중요하다.

저는 이런 건 거의 잘못된 코드라고 생각해요. Member를 보고 Member의 orders를 꺼낼 일이 있을까? 이런 건 거의 없죠. 필요하면 Member를 조회하고 Order를 따로 조회하는 게 맞죠. 이건 비즈니스상 둘 간에 끊어주는 게 맞죠. 이렇게 해야 설계가 더 깔끔하게 나갈 확률이 높아요.

자 그러면 실전 예제2 연관관계 맵핑 하는 부분은 여기서 마치겠습니다.
'스프링 > 자바 ORM 표준 JPA 프로그래밍 - 기본편' 카테고리의 다른 글
| 일대다 [1:N] (0) | 2024.06.23 |
|---|---|
| 다대일 [N:1] (0) | 2024.06.21 |
| 양방향 연관관계와 연관관계의 주인 2 - 주의점, 정리 (0) | 2024.06.16 |
| 양방향 연관관계와 연관관계의 주인 1- 기본 (0) | 2024.06.10 |
| 단방향 연관관계 (0) | 2024.06.10 |