당니의 개발자 스토리
도메인 모델과 테이블 설계 본문
도메인 모델과 테이블 설계
이번 시간에는 도메인 모델과 테이블을 설계해 보겠습니다.


먼저 회원은 여러 상품을 주문할 수 있단 말이에요. 그래서 회원과 주문의 관계는 이제 일대다의 관계구요.
그리고 회원이 한번 주문할 때 여러 개의 상품을 주문할 수 있거든요. 한번 주문할 때 여러 개를 주문할 수 있고, 상품도 여러 주문에 담길 수 있기 때문에 회원과 상품은 다대다 관계가 됩니다.
하지만 이런 다대다 관계는 관계형 데이터베이스는 물론이고 엔티티에서도 거의 사용하지 않습니다. 따라서 그림처럼 주문상품이라는 엔티티(Entity는 JPA에서 테이블에 대응하는 하나의 클래스라고 생각하면 됩니다. 즉, 테이블이죠.)를 추가해서 다대다 관계를 일대다, 다대일 관계로 풀어낸 겁니다.

사실 여기 주문상품 안에 주문 수량 같은 것도 들어가게 되죠.

그리고 이제 한번 주문을 할 때 배송 정보를 입력할 수 있게 해놓았습니다. 그리고 상품은 아까 말씀드린 대로 도서, 음반, 영화 이렇게 타입이 나눠질 수가 있고요. 상품이라는 공통 속성을 사용하므로 상속 구조로 표현했습니다.

그 다음에 상품은 여기 카테고리에 다대다로 맵핑이 됩니다. 왜냐하면 하나의 카테고리에 여러 상품이 들어갈 수도 있고, 어떤 상품이 또 다른 여러 개의 카테고리에 복수로 들어갈 수도 있기 때문에 이렇게 다대다 관계로 일단 세팅을 해놨습니다.
자 이거는 큰 그림에서의 어떤 모델이라고 보시면 되구요. 도메인 모델이라고 하는 것이죠.
이제 이걸 가지고 실제 설계상 엔티티를 만들어내면 어떻게 되냐면,

이렇게 됩니다.

여러분 디테일한데 한번 쭉 들어보세요.
변수명 : 타입 으로 확인하시면 됩니다.
먼저, 기본적으로 공통속성에 id 가 다 있습니다. 이제 pk generating 해주는 id 값, 데이터베이스가 pk 값은 다 Long 타입으로 잡았구요.

먼저, 회원 같은 경우에는

이름이 있고, 그 다음에 address 라는 게 있습니다.

이게 뭐냐면 embedded type 이라고 하는 건데 내장 타입이죠.

내장 값 타입이라고 해서 address 타입이 있고 city, street, zipcode 이렇게 있습니다.
여러분 기본편에 보시면 값 타입이라고 해가지고 설명되어 있는 부분을 보시면 되게 바로 직관적으로 이해가 되실 거예요.

그리고 회원(Member)은 orders라 해서 주문을 List로 가지게 됩니다. 왜냐면 한 회원이 여러 개의 주문을 넣을 수가 있잖아요.
그 다음에 주문,


주문도 한번 주문할 때 여러 상품을 담을 수 있거든요. 상품도 여러 개의 주문에 담길 수 있기 때문에 여기 중간에 OrderItem 이라는 테이블을 만들어서 다대다를 일대다, 다대일 관계로 풀어내는 걸 만들었습니다.
자 근데 이제 추가로 다대다를 풀어내는 걸 떠나서, OrderItem을 꼭 해야 되는 이유가 한번 주문할 때 여러 개 상품을 담을 수 있는데,

그 상품을 몇 개 담았는지 카운트 정보가 필요하거든요.

그리고 상품의 금액이 바뀔 수 있기 때문에 주문 시점의 금액을 알아야 하므로, 이런 OrderItem 안에 orderPrice, count 같은 걸 넣어놨습니다.

일단 주문(Order)이 들어오면 주문은 회원을 알고 있어야 되니까, 즉 어떤 회원이 주문했는지 알아야 되니까 Member가 있고, orderItems을 list로 가지고 있고요. 그 다음에 배송 정보, delivery 정보를 가지고 있습니다. 그리고 orderDate, 주문한 날짜랑 주문 상태(OrderStatus), 그니까 주문인지 취소인지 등등이 있습니다.

delivery로 가볼게요.

delivery는 누가 주문했는데 어떤 주문에 의한 배송인지 알아야 되니까 Order가 있고, address, 배송지 주소죠. 이거는 고객 주소라기 보다는 배송지에 대한 주소인데 여기서

이 값 타입을 재활용한 거죠. 그 다음에 이제 Status, DeliveryStatus를 이렇게 가지고 있습니다.

그 다음에 이제 상품(Item) 같은 경우에는

상품의 이름, 가격, 그 다음에 이거 stockQuantity는 재고, 그 다음에 이 상품이 어느 카테고리에 맵핑이 되어 있는지에 대한 어떤 카테고리 정보들을 포함하고 있습니다.

그리고 상속관계로 앨범, 북, 뮤비가 있고 앨범은 뭐 아티스트나 기타, 북은 뭐 저자나 isbn, 그리고 뮤비는 디렉터나 액터 같은 각각의 개별 속성들이 상속관계로 이렇게 표현이 되어 있습니다.

자 그 다음에 카테고리는 보시면 재밌는게 parent랑 child가 있어요. 그러니까 이거는 계층구조로 되어 있는거죠.

부모가 누군데 자식은 여러 개고, 부모는 하나만 할 수 있는 구조의 카테고리라고 보시면 됩니다. 그래서 이 속성들을 가지고 있고 카테고리가 가지고 있는 items도 List로 표현이 됩니다.
자 지금 잘 보시면 여러분 제가 JPA에서 표현할 수 있는 모든 관계를 사실 다 넣어둔 거에요.

여기 상속 관계가 표현되어 있죠. 그리고 다대일, 일대일, 다대다 등 관계도 다 표현이 되어 있습니다.

참고로 여기에 보면 이제 실생활에서, 그러니까 실무에서 사용하기에는 좀 애매한 게 몇 개가 있어요.
제가 기본편에서도 몇 개 강조한 것들이 있고, 예를 들면 여기 다대다 이런 거는 쓰면 안 돼요. 그 JPA 골뱅이 다대다 관계를 쓸 수 있는데 그거는 운영에서 쓰면 안 된다라고 말씀드렸고, 그에 대한 자세한 이야기는 기본편에서 제가 계속 말씀드렸던 거여서, 결론은 그런 것도 일대다 다대일로 풀어내야 된다고 말씀드렸죠.

여기 보면 Member가 Order를 일대다로 가지고 있거든요. Order도 Member를 가지고 있는 양방향 연관관계란 말이에요. 가급적이면 양방향을 쓰지 말고 단방향 연관관계를 쓰라고 제가 설계 단계에서는 그게 좋다고 말씀드렸는데 여기서는 예제니까 다양한 관계를 표현하기 위해서 여기에 양방향 연관관계를 썼습니다.
사실 저희가 생각할 때는 개발자들이 처음 객체지향을 공부하면서 실수하는 게 뭐냐면,

이거예요. Member, Order가 있으면 회원이 주문을 하니까 '회원의 orders List를 두고 하면 되겠구나' 하는데 이건 사람 생각인 거고 시스템은 사실 Member랑 Order랑 동급으로 놓고 봅니다.

그러니까 회원을 통해서 항상 주문이 일어나는 게 아니라 주문을 생성할 때 회원이 필요하다고 보시는 게 더 맞아요.
그리고 생각해보면 쿼리로 들어갈 때도 어떤 사람의 주문 내역이 필요하면 멤버를 찾아서 거기 있는 리스트로 오더를 찾아오는 게 아니라, 그냥 오더에서 필터링 조건에 멤버가 들어가게 하는 거죠.

그래서 사실상 Member에서의 Order로의 일대다가 표현되는 이 컬렉션은 사실 필요가 없어요. 그런데 제가 풍성한 예제를 위해서 넣은 거라고 이해해 주시면 될 것 같습니다.
자 이제 넘어가서 회원 테이블에 대해서 분석을 해보겠습니다.
지금 조금 빠르다고 느끼시는 분도 있을 것 같은데 제가 기본편에서 이거를 이렇게 설계하는 걸 쭉 설명을 드린 거고, 지금은 기본편에 설명드린 거를 활용하는 거로 이해해 주시면 될 것 같아요. 그래서 너무 빠르거나 이해가 안 되시는 분들은 기본편에서 이런 설계 부분들을 돌아가서 다시 이해를 해주시면 될 것 같습니다. 다시 이거를 다 설명하면 이 강의의 끝이 안나겠죠.

자 위에서는 엔티티(객체)를 분석한 거고, 이제 회원 테이블을 분석해 보겠습니다.

먼저 MEMBER는 그냥 거의 일대일 맵핑이죠.

멤버 들어가면 name, city, street, zipocode가 있는데 이거 city, street, zipocode는 value type 값이 그대로 그냥 쭉 내려온거죠. embedded type 정보가 쭉 내려온거죠.

딜리버리도 마찬가지예요. 딜리버리에 대한 어떤 Status랑 city, street, zipcode는 그 value type, 아까 address 있었던 것들이 다 들어간 거죠.
그 다음에 아이템을 가볼게요.

아이템은 테이블 하나죠. 이거는 싱글 테이블 전략을 쓴 겁니다. 기본편에서 설명드린 상속관계 맵핑에 3가지 방법이 있었죠. 그 중에서 가장 단순한 싱글 테이블 전략을 썼습니다.

싱글 테이블 전략을 일단 한 테이블에 다 때려박고 이 구분 값, 즉 DTYPE으로 구분해서 쓰는 거죠. 그리고 보시면 저자, ISBN 이런 필드들이 책이든, 영화든 이런 필드가 다 섞여서 들어가게 됩니다. 이게 약간 단점이긴 한데 일반적으로 성능이 잘 나오죠.
그 다음 오더를 보면은

오더는 뭐 특별한게 없죠. 오더 아이디를 가지고 있고, 멤버 아이디, 즉 어떤 멤버가 주문했는지랑 그 다음에 딜리버리 아이디 라는 Foreign key를 가지고 있고 ORDER DATE, STATUS를 가지고 있습니다.

그런데 테이블 명이 좀 특이하죠. ORDER가 아니고 ORDERS죠. DB에 order by 라는 예약어 때문에 보통 ORDER가 잘 안 먹습니다. 그래서 관례상 ORDERS 라고 많이 씁니다.
그 다음에 오더 아이템도 보시면,

ORDER_ID로 어떤 주문의 주문 상품인지 알 수가 있고 ITEM_ID로 ITEM과 맵핑할 수 있겠죠. 그 다음에 오더 프라이스, 카운트 정보 그대로 가지고 있고요.
카테고리 같은 경우에는

이런데 우리가 '회원 엔티티 분석'에서 봤던 객체에서는

카테고리가 아이템 리스트를 가져도 되고 아이템이 카테고리를 리스트로 가져도 돼요. 이렇게 양쪽에 다 리스트가 있죠. 이렇게 객체에서는 양쪽 다 컬렉션을 가져가지고 다대다 관계를 만들 수 있어요.
그런데 관계형 데이터베이스는 일반적인 설계로는 다대다가 안돼요.

그래서 중간에 이렇게 맵핑 테이블을 둬야 됩니다.
중간에 CATEGORY_ITEM 이라는 맵핑 테이블을 두고 이걸로 카테고리랑 아이템의 다대다 관계를 일대다, 다대일 관계로 풀어내야 되는 거죠.
이건 이제 너무 데이터베이스 기본이어서 이 정도만 말씀드리고 넘어갈게요.

그리고 참고로 지금 데이터베이스 쪽은 다 대문자로 썼는데요. 실제 제가 할 때는 테이블명, 컬럼명 전부 소문자 + (언더스코어) 스타일을 쓸 거에요. 이제 데이터베이스의 테이블명이나 컬럼명에 대한 관례는 보통 회사마다 다 다르거든요. 보통은 대문자 + (언더스코어)를 쓰거나 소문자 + (언더스코어) 방식 중에 보통 하나를 선택하세요. 일관성 중요하니까요. 강의에서 설명할 때는 객체랑 구분하기 위해서 데이터베이스 테이블과 컬럼명에 대문자를 쓰긴 할 텐데, 실제 앞으로 진행하는 코드에서는 다 소문자 + (언더스코어) 코드 스타일로 그냥 쭉 하겠습니다. 저희 회사에서도 이게 관례고 저도 이 방식이 좋더라구요.
그 다음에 연관관계에 대해서 맵핑을 한번 분석을 해보겠습니다.

하나씩 봐보겠습니다.
먼저 회원과 주문,

회원과 주문은 보시면,

회원도 오더 리스트로 오더 쪽으로 갈 수 있고 오더도 멤버를 가지고 있기 때문에 멤버로 갈 수 있게 설계를 했습니다. 그래서 양방향 연관관계 라는 거죠. 일대다, 다대일을 양방향 관계면 연관관계의 주인을 정해야 되는 거 아시죠. 기본편에서 엄청 강조한 겁니다.

지금 외래키가 주문쪽에 있단 말이에요. 일대다 관계에서는 테이블에서 다 쪽에 무조건 외래키가 존재하게 됩니다.
그래서 외래 키가 있는 주문을 연관관계의 주인으로 정하는 것이 좋습니다.
지금 MEMBER_ID가 FK죠. 여기 있을 수 밖에 없어요. 일대다에서 일 쪽에 있으면 말이 안되는 거에요. 그래서 관계형 DB에서 무조건 Foreign Key는 일대다 관계에서 항상 다가 됩니다.
자 그러면 ORDERS 테이블에 이 MEMBER_ID가 있단 말이에요. 그래서 ORDERS에 있는 애를 연관관계의 주인으로 잡아야 되는 거예요.

자 그래서 Order에 있는 이 member가 연관관계 주인이 되고, Member에 있는 orders는 주인이 아니고 mappedBy로 연관관계의 거울이 되는 거죠. 연관관계 주인 쪽에 값을 세팅을 해야 값이 변경된다는 거 아시죠. 거울 쪽은 그냥 단순하게 조회용으로만 쓰는 거다 라고 이해하시면 됩니다.
결론적으로 연관관계의 주인이 ORDERS 쪽이니까 Order.member를 ORDERS.MEMBER_ID 외래 키와 매핑하면 됩니다.
참고(https://jeong-pro.tistory.com/231)
자 그 다음에 이제 주문 상품과 주문 봅시다.

이거는 오더랑 오더 아이템. 뭐 ORDER_ITEM을 뭐 ORDERLINEITEM등으로 표현하기도 하는데 저는 그냥 예제에서는 ORDER_ITEM으로 적었습니다.

자 이게 한번 주문할 때 여러 개 상품을 주문할 수 있고 그 여러 개 상품당 어떤 수량이랑 금액이 다 다를 수 있기 때문에, 그렇다고 이 ITEM 테이블을 손 댈 수는 없잖아요. 그러니까 이런 중간 테이블 하나가 나오는 겁니다.
자 그래서 이제 관계는 어떻게 되냐면,

Order가 1이고 OrderItem이 N이 되겠죠. 한번 주문할 때 여러 개 상품을 선택해서 넣는 거기 때문입니다.
여기 Order에 보면 OrderItems 컬렉션을 가지고 있습니다. 자 그렇기 때문에 OrderItem이 N, 반대로 OrderItem은 하나의 Order에만 연관관계가 걸리겠죠. 그래서 1, 다른 사람의 주문서가 내 주문서에 들어오면 말이 안되는 거니까.

자 그래서 이거는 이제 다대일 양방향 관계인데 외래키가 주문 상품 쪽에 있기 때문에 주문 상품이 연관관계 주인입니다. 그래서 OrderItem.order를 ORDER_ITEM.ORDER_ID 외래 키와 매핑합니다.
그 다음에 주문 상품과 상품의 관계는 다대일인데,

지금 보시면

화살표를 적어놨죠? 이게 무슨 뜻일까요? 이쪽으로만 간다는 겁니다. 단방향 관계라는 거고요.
보시면 OrderItem은 Item으로 가는 참조값이 있는데, Item에는 OrderItem으로 가는 것에 대한 게 없습니다. 그래서 사실 이 별표를 지우는게 맞는데 카디널리티를 제가 그냥 쉽게 하나의 Item이 여러 OrderItem에 소속될 수 있다는 관계를 표현하기 위해서 그냥 놔둔거구요. 그래서 대신 이 화살표를 적은 겁니다.

자 그래서 Item은 보시면 OrderItem으로 갈 수 있는게 없죠. 근데 생각해보면 진짜 필요가 없어요. Item을 볼 때 나(상품)를 주문한 OrderItem을 다 찾아 갈 일이 없는거죠. 그냥 애초에 OrderItem을 통해서 보면 되는 거죠.
그래서 당연히 단방향 관계이기 때문에 이것만 잘 연관관계 맵핑하면 됩니다. 따라서 OrderItem.item 을 ORDER_ITEM.ITEM_ID 외래 키와 매핑하면 됩니다.
이제 주문과 배송,

이건 이제 일대일 양방향 관계인데요.

보시면 일대일 관계는 외래키를 OREDERS에도 둘 수 있고 DELIVERY에도 둘 수 있거든요. 지금 외래 키를 ORDERS의 DELIVERY_ID에 뒀죠. 그러면,

Order에 있는 delivery가 연관관계의 주인이 됩니다. 그래서
Order.delivery를 ORDERS.DELIVERY_ID 외래 키와 매핑했습니다.
뭐 다른 쪽으로도 고민할 수 있긴 한데, 일단 이렇게 하는 게 기본편에서 항상 강조되는 대원칙, 그러니까 외래키가 가까운 곳에 있는 것을 연관관계 주인으로 잡는다. 그래야 모든 것이 편하다. 그 원칙에 입각해서 설명을 드렸습니다.
자 그 다음에 카테고리와 상품은

제가 그냥 예제로 보여드리려고 다대다 맵핑이 뭔지 한번 넣었습니다.

그래서 객체는 이렇게 Categorh도 List로 Item도 List로 다대다 관계 돼서 @ManyToMany로 맵핑을 하면 되는데,

테이블 관계는 Many To Many를 표현할 수 있는 방법이 없습니다. 물론 이상하게 편법을 쓰면 어떻게 될 수 있긴 한데 정식적인 방법이 없기 때문에 CATEGORY는 CATEGORY_ITEM 이라는 중간 테이블을 가지고 풀어내야 합니다.
@ManyToMany를 보여드리려고 한 거고, 실무에서는 이 @ManyToMany를 쓰면 안됩니다.
자 이제 참고로

'외래키가 있는 곳을 연관관계의 주인으로 정해라' 연관관계 주인은 단순히 외래키를 누가 관리하냐의 문제지 비즈니스상 우위에 있다고 주인으로 정하면 안되는 거예요. 예를 들어서 자동차랑 바퀴가 있는데 사람은 본능적으로 자동차가 중요하지 라고 생각하기 때문에 자동차를 가지고 다 엮으려고 하는데 자동차가 1이고 바퀴가 N이거든요. DB상으로 보면 일대다 관계는 항상 다 쪽에 외래키가 있어요. 그렇기 때문에 외래키가 있는 바퀴를 연관관계 주인으로 정하시면 됩니다.
물론 자동차를 연관관계 주인으로 정하는 게 불가능한 건 아니에요. 방법이 있어요. 일대다 단방향 연관관계라든가, 이런 것들이 있어서 하면 되는데 자동차를 연관관계 주인으로 정하면 자동차가 관리하지 않는 테이블의 바퀴 쪽에 있는 컬럼들이 업데이트가 된단 말이에요. 그래서 관리랑 유지보수가 어려워져요. 추가적으로 별도의 업데이트 쿼리도 발생해서 성능상 불리한 게 많고요. 그래서 항상 외래키가 있는 것을 연관관계의 주인으로 정하는 것이 정석이라고 보시고 나머지는 JPA 공부를 많이 하시면 변형으로 쓰시는 것을 저는 권장 드립니다.
이 내용만 해도 사실 한 2-3시간 이상의 내용이기 때문에 자세한 내용은 JPA 기본편을 참고하셔야 되고 지금까지 설명드린 이 내용, 연관관계의 주인이라던가, 이것에 대해서 이해가 잘 안되시면 꼭 기본편을 들으셔서 이해를 하시고 여기로 돌아오시는 것을 추천드립니다.
그래서 이제 기본적인 설계는 끝났고 다음 시간에는 이거를 실제 코드로 제가 한번 쭉 쳐보겠습니다. 치고 맵핑 하는 거를 바로 라이브 코딩으로 다 보여드릴 거구요. 그리고 이 테이블이 이렇게 나오는 것도 한번 보여드리겠습니다.
자 그래서 이번 시간은 여기까지 하고 다음 시간에는 이제 실제 개발하는, 엔티티 클래스 개발에 대해서 보여드리겠습니다.
'스프링 > 실전! 스프링 부트와 JPA 활용1' 카테고리의 다른 글
| 엔티티 클래스 개발2 (0) | 2024.04.19 |
|---|---|
| 엔티티 클래스 개발1 (0) | 2024.04.17 |
| 요구사항 분석 (0) | 2024.04.13 |
| JPA와 DB 설정, 동작확인 (0) | 2024.04.13 |
| H2 데이터베이스 설치 (0) | 2024.04.02 |