회원, 주문, 상품의 관계: 회원은 여러 상품을 주문할 수 있다. 그리고 한 번 주문할 때 여러 상품을 선택할 수 있으므로 주문과 상품은 다대다 관계다.
하지만 이런 다대다 관계는 관계형 데이터베이스는 물론이고 엔티티에서도 거의 사용하지 않는다. 따라서 그림처럼 주문상품이라는 엔티티를 추가해서 다대다
관계를 일대다
, 다대일
관계로 풀어냈다.
회원(Member): 이름과 임베디드 타입인 주소(Address
), 그리고 주문(orders
) 리스트를 가진다.
주문(Order): 한 번 주문시 여러 상품을 주문할 수 있으므로 Order
와 OrderItem
은 일대다 관계이다. 주문은 상품을 주문한 회원과, 주문 날짜, 주문 상태를 가지고 있다.
현재 그림에서 Member가 Order를 일대다로 가지고 있고,
Order도 Member를 다대일로 가지고 있는데, 이런 양방향은 운영에서 쓰면 안된다. (예제이므로 그냥 양방향으로 썼지만, 안쓰는게 좋다.)
개발자 입장에서 회원을 통해 상품을 주문한다는 생각을 가질 수 있지만, 시스템은 다르다.
시스템 입장에서 회원과 상품은 동등한 입장으로 생각하기 때문에 회원을 통해서 주문을 한다는 개념이 아니라, 주문을 새로 생성할때 회원이 필요하다의 개념으로 접근해야 한다. 즉, Order는 생성할때 Member가 필요한 반면, Member가 Order가 필요없다.
또, 이건 개인적인 생각이지만, Order와 OrderItem도 양방향으로 묶어져 있는데 Order는 OrderItem이 필요없다고 생각된다. 이것 역시 OrderItem은 생성할때 Order를 필요로 하지만, Order는 필요없기 때문이다.
주문상품(OrderItem): 주문한 상품 정보(Item)과, 주문 금액(orderPrice), 주문 수량(count) 정보를 가지고 있다. (보통 OrderLine , LineItem 으로 많이 표현한다.)
상품(Item): 가격(price), 재고(stockQuantity), 카테고리 정보를 가지고 있고 상속으로 Album
, Book
, Movie
를 가지고 있다.
MEMBER: 회원 엔티티의 Address
임베디드 타입 정보가 회원 테이블에 그대로 들어갔다.
ITEM: 싱글 테이블 전략을 통해 앨범, 도서, 영화 타입을 통합해서 하나의 테이블로 만들었다. DTYPE
컬럼으로 타입을 구분한다.
데이터베이스는 order by
때문에 예약어로 잡고 있는 경우가 많아서 보통 ORDER라고 안하고 ORDERS로 많이 사용한다.
회사마다 관례가 다른데 대문자+_(언더스코어) 또는 소문자+_(언더스코어)를 보통 많이 사용한다.
회원과 주문 : 양방향 관계이다. 따라서 연관관계의 주인을 정해야 하는데, 외래 키가 있는 주문(ORDER)을 연관관계의 주인으로 하는 것이 좋다. 그러므로 Order.member
를 ORDERS.MEMBER_ID
외래 키와 매핑한다. 주인을 정하는 것은 중요한 부문이다.
주문과 배송: 일대일 단방향 관계이다. Order.delivery
를 ORDERS.DELIVERY_ID
외래 키와 매핑한다.
카테고리와 상품: @ManyToMany
를 사용해서 매핑한다.(실무에서 @ManyToMany
는 사용하지 말자. 여기서는 다대다 관계를 예제로 보여주기 위해 추가했을 뿐이다)
외래 키가 있는 곳을 연관관계의 주인으로 정해라.
예제에서는 쉽게 설명하기 위해 @Getter
, @Setter
를 전부 열고 최대한 단순하계 설계할 거지만,
실무에서는 가급적으로 @Getter
를 열어두고, @Setter
는 꼭 필요한 경우에만 사용하는 것을 추천한다.
멤버의 경우 이름
과 임베디드 타입 Address
를 가지고 있다. 그리고 양방향으로 쓰기 때문에 Order
를 읽기용 리스트로 갖고 있다.
양방향성을 가지고 있는 다대일의 경우 주인을 정해야한다. 주인은 FK를 가지고있는 Many가 주인이 된다. 예제의 경우 Member
와 Order
는 일대다의 관계이다. 그렇다면 Order
가 주인이 되고 Member
는 Order
에 있는 데이터를 참고하는 읽기용 데이터만 사용할 수 있다.
하지만 주인이 누구인지 따로 알려주지 않으면 시스템은 어디서 데이터를 수정해야할지 혼란스러워 한다. 그러므로 주인을 설정해줘야 하는데, mappedBy
를 사용해서 주인을 정해줄 수 있다.
먼저 Order
의 경우 FK 키를 참고하기 때문에 컬럼을 참고하여 Member
를 생성한다.
주인이 아닌 클래스인 Member
에서 mappedBy
를 사용하여 Order
가 주인이라는 것을 알릴 수 있다.
이 뜻은 Order
에 있는 member
를 연결해서 사용하겠다 라는 뜻이다. 그러므로 읽기용으로만 사용해야하고 값을 수정해도 적용되지 않는다.
먼저 임베디드 타입을 설정할때 임베디드 타입이라는 것을 알려주는 방법은 두 가지가 있는데 하나는
이렇게 @Embedded
를 Member
에서 설정해주는 방법이 있고,
@Embeddable
을 address
에 설정해주는 방법이 있다. 그런데 그냥 두 개다 설정해서 시각적으로 알 수 있도록 설정하는 것이 좋다.
Order의 경우 데이터베이스에서는 orders
로 만들 예정이기 때문에 테이블 이름을 따로 설정해준다.
그리고 Member
와 다대일 관계이므로 설정을 해주고, OrderItem
과 일대다 양방향 관계이므로 OrderItem
이 주인이라는 것을 알려주기 위해 mappedBy
를 통해 OrderItem
안에 있는 order
객체를 읽기용으로 가져온다.
그리고 주문시간을 위해 자바8에서 사용하는 LocalDateTime
타입을 넣는다. 이전 Date
와 같은 타입은 날짜 설정을 따로 해줘야 했으나 자바8부터는 알아서 처리를 해주기 때문에 위 그림처럼 설정해도 된다.
현재 Orders
와 Delivery
는 일대일 관계이다. 그런데 설계에서 FK키를 Orders
가 가지고 있도록 설계를 했기 때문에 Orders
가 주인이 된다.
그러므로 Order
에서는 FK키인 delivery_id
로 delivery
객체를 생성하고
Delivery
에서는 mappedBy
를 통해 Order
가 주인인 것을 설정함과 동시에 Order
에 있는 delivery
객체를 가져와 읽기용으로 사용한다.
일대일 관계에서 주인을 누구로 설정할지는 엑세스가 자주 일어나는 즉, 더 자주 사용하는 쪽으로 정하면 된다.
Order
에서 마지막에 DeliveryStatus
를 Enum
으로 넣기 위해 @Enumerated
를 사용한다.
이때, EnumType이 2개 있는데
EnumType.ORDINAL는 숫자로 접근하는 방식이다. 즉, READY는 1, COMP는 2 로 지정해서 사용하는 방식이다.
이 방법은 치명적인 단점이 있는데 READY와 COMP 사이에 XXX가 들어오게 되면 2번이 XXX로 되버려 데이터가 뒤죽박죽 섞인다는 정말 큰 단점이 존재한다.
그렇기 때문에 EnumType.STRING을 사용하자.
OrderItem
의 경우 Orders
와 Item
을 둘다 다대일로 가지고 있으므로 위 그림처럼 세팅을 해줍니다.
Item
에서는 OrderItem
와 양방향이 아닌 단방향이므로 OrderItem
에 대한 설정이 따로 없습니다.
Item
의 경우 Album, Movie, Book
을 모두 한 곳에 모으는 싱글 테이블 전략을 사용한다. 이를 위해 Item
은 먼저 추상 클래스로 만들어줬다.(abstract
)
그리고 싱글 테이블 세팅을 위해 @Inheritance
를 쓰고 strategy
를 싱글 테이블
로 설정해줬다.(strategy
종류 중에 조인, 테이블 퍼 클래스 도 있다.)
그리고 이는 dtype
을 통해 접근을 할 것이기 때문에 @DiscriminatorColumn
을 설정해준 것을 확인할 수 있다.
Book, Movie, Album
의 경우 먼저 Item
을 상속을 한 다음, dtype
에서 어떻게 접근할지@DiscriminatorValue
를 통해 정하게 되는데 기본적으로 클래스의 첫 글자로 설정이 되어있다. 예제에서는 학습을 위해 기본 세팅을 적은 것이다.
Delivery
의 경우 Order
와 일대일 관계이며, 주인이 Order
이므로 mappedBy
를 사용한 것을 확인할 수 있다. 또, Address
를 임베디드로 받으니 @Embedded
어노테이션을 설정하였고, 마지막으로 Enum
타입을 받는 것을 확인할 수 있다.
Category
는 엔티티 설계에서는 다대다 관계로 설정했고, 데이터베이스 설계에서는 다대다 관계를 설정할 수 없으니 사이에 테이블을 하나 추가한 것을 확인할 수 있다.
다대다의 경우 @JoinTable
을 사용하여 다대다 사이에 테이블 category_item
을 연결해준다. joinColumns
는 category_item
테이블 안에 있는 cateogry_id
를 통해 inverseJoinColumns
로 item_id
를 이어준다.
item
에서는 mappedBy
를 통해 값을 얻어온다.
다대다를 사용하는 것이 편리해 보일지는 몰라도, 사용했을때 사이에 있는 category_item
은 더 이상 컬럼을 추가할 수가 없게 된다. 또, 세밀하게 쿼리를 실행하기 어려운 점이 있는 등, 여러 이유 때문에 실무에서는 사용하지 않는다.
카테고리에 이런 코드가 있는데 이것은 말 그대로 parent
와 child
를 카테고리 테이블 안에서 다대일 설정을 한 것이다. 이렇게 되면 부모 카테고리와 자식 카테고리를 설정할 수 있게 된다.
먼저 Address
는 @Setter
를 설정할 필요가 없다. 그러므로 생성자에서 첫 세팅에만 설정한 후에 건들지 않는 방식으로 코딩을 한 것이다. 추가로 임베디드
나 엔티티
타입은 JPA 스펙상 자바 기본 생성자를 public
또는 protected
로 설정해야 한다. 하지만, 쉽게 사용하게 할 순 없으므로 protected
로 설정한다.
JPA가 이런 제약을 두는 이유는 JPA 구현 라이브러리가 객체를 생성할 때 리플랙션 같은 기술을 사용할 수 있도록 지원해야 하기 때문이다.
Setter가 너무 많으면 유지보수가 어려워진다.
즉시로딩(EAGLE
)은 연관되어있는 데이터를 모두 가져오기 때문에 예측이 어렵고, 어떤 SQL이 실행될지 추측하기 어렵다. 그러므로 N+1 문제가 자주 발생한다.
N+1은 하나의 데이터를 조회할때 무조건 그와 연관된 데이터를 추가로 가져오는 문제이다.
Order
에서 Member
가 Eagle
로 설정되어 있으면 Order
를 100번 조회했을때 Order
안에 있는 Member
리스트도 100번 이상 데이터를 가져온다는 뜻이다.
실무에서 모든 연관과계는 지연로딩(LAZY
)으로 설정해야 한다.
XToOne
은 기본값이 EAGLE
이므로 꼭 LAZY
로 설정해줘야 한다.
컬렉션은 필드에서 바로 초기화 하는 것이 안전하다.
null
문제에서 안전하다.
하이버네이트는 엔티티를 영속화 할때, 컬렉션을 감싸서 하이버네이트가 제공하는 내장 컬렉션으로 변경한다.
그러므로 이 컬렉션의 값은 변경해서는 안되고 꺼내서 조회하는 용도로만 사용해야 한다.
https://docs.spring.io/spring-boot/docs/2.1.3.RELEASE/reference/htmlsingle/#howto-configure-hibernate-naming-strategy http://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#naming
엔티티의 필드명을 그대로 테이블 명으로 사용
(SpringPhysicalNamingStrategy
)
명시적으로 컬럼, 테이블명을 직접 적지 않으면 ImplicitNamingStrategy 사용
spring.jpa.hibernate.naming.physical-strategy
: 모든 논리명에 적용됨, 실제 테이블에 적용(username -> xx_username 등으로 회사 룰로 바꿀 수 있음)
Order
에서 orderItems
를 받는 곳에 cascade=CascadeType.ALL
설정을 했다면,
원래 로직
persist(orderItemA);
persist(orderItemB);
persist(orderItemC);
persist(order);
변경 후 로직
persist(order);
즉, 이전에는 orderItem
들을 일일이 저장한 후 order
를 저장하는 과정을 거쳐야 했는데, cascade
설정을 하면, order
만 저장해도 cascade
로 전파되어 orderItem
들이 저장되게끔 설정이 된다.
뿐만 아니라 delete
할때도 같이 지워준다.
양방향 관계일때 Member
든 Order
든 서로 객체를 갖고 있어야 한다. 이를 위해 위 코드처럼 Member
에 있는 Orders
에 order
를 넣어줘야 하고 이 멤버를 order
에 넣어줘야 한다.
하지만, 이렇게 Order
안에서 메서드를 세팅해주면,
이렇게 줄일 수 있다.
이런 식으로 양방향일 때는 연관관계 메서드를 설정해주는 것이 편리하다.