도메인 분석 설계

slee2·2022년 2월 2일
0

기능 목록

  • 회원 기능
    • 회원 등록
    • 회원 조회
  • 상품 기능
    • 상품 등록
    • 상품 수정
    • 상품 조회
  • 주문 기능
    • 상품 주문
    • 주문 내역 조회
    • 주문 취소
  • 기타 요구사항
    • 상품은 제고 관리가 필요하다.
    • 상품의 종류는 도서, 음반, 영화가 있다.
    • 상품을 카테고리로 구분할 수 있다.
    • 상품 주문시 배송 정보를 입력할 수 있다.

도메인 모델과 테이블 설계

회원, 주문, 상품의 관계: 회원은 여러 상품을 주문할 수 있다. 그리고 한 번 주문할 때 여러 상품을 선택할 수 있으므로 주문과 상품은 다대다 관계다.

하지만 이런 다대다 관계는 관계형 데이터베이스는 물론이고 엔티티에서도 거의 사용하지 않는다. 따라서 그림처럼 주문상품이라는 엔티티를 추가해서 다대다 관계를 일대다, 다대일 관계로 풀어냈다.

회원 엔티티 분석

회원(Member): 이름과 임베디드 타입인 주소(Address), 그리고 주문(orders) 리스트를 가진다.

주문(Order): 한 번 주문시 여러 상품을 주문할 수 있으므로 OrderOrderItem은 일대다 관계이다. 주문은 상품을 주문한 회원과, 주문 날짜, 주문 상태를 가지고 있다.

현재 그림에서 MemberOrder를 일대다로 가지고 있고,
OrderMember를 다대일로 가지고 있는데, 이런 양방향은 운영에서 쓰면 안된다. (예제이므로 그냥 양방향으로 썼지만, 안쓰는게 좋다.)

개발자 입장에서 회원을 통해 상품을 주문한다는 생각을 가질 수 있지만, 시스템은 다르다.
시스템 입장에서 회원과 상품은 동등한 입장으로 생각하기 때문에 회원을 통해서 주문을 한다는 개념이 아니라, 주문을 새로 생성할때 회원이 필요하다의 개념으로 접근해야 한다. 즉, Order는 생성할때 Member가 필요한 반면, Member가 Order가 필요없다.

또, 이건 개인적인 생각이지만, OrderOrderItem도 양방향으로 묶어져 있는데 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.memberORDERS.MEMBER_ID 외래 키와 매핑한다. 주인을 정하는 것은 중요한 부문이다.

주문과 배송: 일대일 단방향 관계이다. Order.deliveryORDERS.DELIVERY_ID 외래 키와 매핑한다.

카테고리와 상품: @ManyToMany를 사용해서 매핑한다.(실무에서 @ManyToMany는 사용하지 말자. 여기서는 다대다 관계를 예제로 보여주기 위해 추가했을 뿐이다)

외래 키가 있는 곳을 연관관계의 주인으로 정해라.

엔티티 클래스 개발

예제에서는 쉽게 설명하기 위해 @Getter, @Setter를 전부 열고 최대한 단순하계 설계할 거지만,
실무에서는 가급적으로 @Getter를 열어두고, @Setter는 꼭 필요한 경우에만 사용하는 것을 추천한다.

Member

멤버의 경우 이름과 임베디드 타입 Address를 가지고 있다. 그리고 양방향으로 쓰기 때문에 Order를 읽기용 리스트로 갖고 있다.

다대일 양방향

양방향성을 가지고 있는 다대일의 경우 주인을 정해야한다. 주인은 FK를 가지고있는 Many가 주인이 된다. 예제의 경우 MemberOrder일대다의 관계이다. 그렇다면 Order가 주인이 되고 MemberOrder에 있는 데이터를 참고하는 읽기용 데이터만 사용할 수 있다.

하지만 주인이 누구인지 따로 알려주지 않으면 시스템은 어디서 데이터를 수정해야할지 혼란스러워 한다. 그러므로 주인을 설정해줘야 하는데, mappedBy를 사용해서 주인을 정해줄 수 있다.

먼저 Order의 경우 FK 키를 참고하기 때문에 컬럼을 참고하여 Member를 생성한다.

주인이 아닌 클래스인 Member에서 mappedBy를 사용하여 Order가 주인이라는 것을 알릴 수 있다.

이 뜻은 Order에 있는 member를 연결해서 사용하겠다 라는 뜻이다. 그러므로 읽기용으로만 사용해야하고 값을 수정해도 적용되지 않는다.

임베디드 타입

먼저 임베디드 타입을 설정할때 임베디드 타입이라는 것을 알려주는 방법은 두 가지가 있는데 하나는

이렇게 @EmbeddedMember에서 설정해주는 방법이 있고,

@Embeddableaddress에 설정해주는 방법이 있다. 그런데 그냥 두 개다 설정해서 시각적으로 알 수 있도록 설정하는 것이 좋다.

Order

Order의 경우 데이터베이스에서는 orders로 만들 예정이기 때문에 테이블 이름을 따로 설정해준다.

그리고 Member와 다대일 관계이므로 설정을 해주고, OrderItem과 일대다 양방향 관계이므로 OrderItem이 주인이라는 것을 알려주기 위해 mappedBy를 통해 OrderItem안에 있는 order 객체를 읽기용으로 가져온다.

그리고 주문시간을 위해 자바8에서 사용하는 LocalDateTime 타입을 넣는다. 이전 Date와 같은 타입은 날짜 설정을 따로 해줘야 했으나 자바8부터는 알아서 처리를 해주기 때문에 위 그림처럼 설정해도 된다.

일대일 관계

현재 OrdersDelivery는 일대일 관계이다. 그런데 설계에서 FK키를 Orders가 가지고 있도록 설계를 했기 때문에 Orders가 주인이 된다.

그러므로 Order에서는 FK키인 delivery_iddelivery객체를 생성하고

Delivery에서는 mappedBy를 통해 Order가 주인인 것을 설정함과 동시에 Order에 있는 delivery 객체를 가져와 읽기용으로 사용한다.

일대일 관계에서 주인을 누구로 설정할지는 엑세스가 자주 일어나는 즉, 더 자주 사용하는 쪽으로 정하면 된다.

Enum 설정

Order에서 마지막에 DeliveryStatusEnum으로 넣기 위해 @Enumerated를 사용한다.

이때, EnumType이 2개 있는데
EnumType.ORDINAL는 숫자로 접근하는 방식이다. 즉, READY는 1, COMP는 2 로 지정해서 사용하는 방식이다.
이 방법은 치명적인 단점이 있는데 READY와 COMP 사이에 XXX가 들어오게 되면 2번이 XXX로 되버려 데이터가 뒤죽박죽 섞인다는 정말 큰 단점이 존재한다.

그렇기 때문에 EnumType.STRING을 사용하자.

OrderItem

OrderItem의 경우 OrdersItem을 둘다 다대일로 가지고 있으므로 위 그림처럼 세팅을 해줍니다.

Item

Item에서는 OrderItem와 양방향이 아닌 단방향이므로 OrderItem에 대한 설정이 따로 없습니다.

상속관계 전략(싱글 테이블)

Item의 경우 Album, Movie, Book을 모두 한 곳에 모으는 싱글 테이블 전략을 사용한다. 이를 위해 Item은 먼저 추상 클래스로 만들어줬다.(abstract)

그리고 싱글 테이블 세팅을 위해 @Inheritance를 쓰고 strategy싱글 테이블로 설정해줬다.(strategy 종류 중에 조인, 테이블 퍼 클래스 도 있다.)

그리고 이는 dtype을 통해 접근을 할 것이기 때문에 @DiscriminatorColumn을 설정해준 것을 확인할 수 있다.

Book, Movie, Album의 경우 먼저 Item을 상속을 한 다음, dtype에서 어떻게 접근할지@DiscriminatorValue를 통해 정하게 되는데 기본적으로 클래스의 첫 글자로 설정이 되어있다. 예제에서는 학습을 위해 기본 세팅을 적은 것이다.

Delivery

Delivery의 경우 Order와 일대일 관계이며, 주인이 Order이므로 mappedBy를 사용한 것을 확인할 수 있다. 또, Address를 임베디드로 받으니 @Embedded 어노테이션을 설정하였고, 마지막으로 Enum타입을 받는 것을 확인할 수 있다.

Category

Category는 엔티티 설계에서는 다대다 관계로 설정했고, 데이터베이스 설계에서는 다대다 관계를 설정할 수 없으니 사이에 테이블을 하나 추가한 것을 확인할 수 있다.

다대다 관계

다대다의 경우 @JoinTable을 사용하여 다대다 사이에 테이블 category_item을 연결해준다. joinColumnscategory_item 테이블 안에 있는 cateogry_id를 통해 inverseJoinColumnsitem_id를 이어준다.

item에서는 mappedBy를 통해 값을 얻어온다.

다대다 관계를 쓰면 안되는 이유

다대다를 사용하는 것이 편리해 보일지는 몰라도, 사용했을때 사이에 있는 category_item은 더 이상 컬럼을 추가할 수가 없게 된다. 또, 세밀하게 쿼리를 실행하기 어려운 점이 있는 등, 여러 이유 때문에 실무에서는 사용하지 않는다.

셀프 다대일

카테고리에 이런 코드가 있는데 이것은 말 그대로 parentchild를 카테고리 테이블 안에서 다대일 설정을 한 것이다. 이렇게 되면 부모 카테고리와 자식 카테고리를 설정할 수 있게 된다.

Address

먼저 Address@Setter를 설정할 필요가 없다. 그러므로 생성자에서 첫 세팅에만 설정한 후에 건들지 않는 방식으로 코딩을 한 것이다. 추가로 임베디드엔티티 타입은 JPA 스펙상 자바 기본 생성자를 public 또는 protected로 설정해야 한다. 하지만, 쉽게 사용하게 할 순 없으므로 protected로 설정한다.
JPA가 이런 제약을 두는 이유는 JPA 구현 라이브러리가 객체를 생성할 때 리플랙션 같은 기술을 사용할 수 있도록 지원해야 하기 때문이다.

엔티티 설계시 주의점

엔티티에는 가급적 Setter를 사용하지 말자.

Setter가 너무 많으면 유지보수가 어려워진다.

모든 연관관계는 지연로딩(LAZY)으로 설정

즉시로딩(EAGLE)은 연관되어있는 데이터를 모두 가져오기 때문에 예측이 어렵고, 어떤 SQL이 실행될지 추측하기 어렵다. 그러므로 N+1 문제가 자주 발생한다.

N+1은 하나의 데이터를 조회할때 무조건 그와 연관된 데이터를 추가로 가져오는 문제이다.

Order에서 MemberEagle로 설정되어 있으면 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)

스프링 부트 신규 설정

  1. 케멀 케이스 -> 언더스코어 (memberPoint -> member_point)
  2. .점 -> _언더스코어
  3. 대문자 -> 소문자

논리명 생성

명시적으로 컬럼, 테이블명을 직접 적지 않으면 ImplicitNamingStrategy 사용

물리명 적용

spring.jpa.hibernate.naming.physical-strategy: 모든 논리명에 적용됨, 실제 테이블에 적용(username -> xx_username 등으로 회사 룰로 바꿀 수 있음)

cascade

Order에서 orderItems를 받는 곳에 cascade=CascadeType.ALL 설정을 했다면,
원래 로직

persist(orderItemA);
persist(orderItemB);
persist(orderItemC);
persist(order);

변경 후 로직

persist(order);

즉, 이전에는 orderItem들을 일일이 저장한 후 order를 저장하는 과정을 거쳐야 했는데, cascade 설정을 하면, order 만 저장해도 cascade로 전파되어 orderItem들이 저장되게끔 설정이 된다.

뿐만 아니라 delete 할때도 같이 지워준다.

연관관계 메서드

양방향 관계일때 MemberOrder든 서로 객체를 갖고 있어야 한다. 이를 위해 위 코드처럼 Member에 있는 Ordersorder를 넣어줘야 하고 이 멤버를 order에 넣어줘야 한다.

하지만, 이렇게 Order 안에서 메서드를 세팅해주면,

이렇게 줄일 수 있다.

이런 식으로 양방향일 때는 연관관계 메서드를 설정해주는 것이 편리하다.

0개의 댓글