회원 - 주문 = 1 : N
- 회원은 여러 상품을 주문할 수 있다.
주문 - 상품 = N : M
- 주문은 여러 상품을 선택할 수 있고, 상품은 여러 주문에 들어갈 수 있으므로 다대다 관계
- 하지만 RDB는 물론 엔티티에서도 다대다 관계는 거의 사용하지 않는다.
- 주문 상품이라는 중간 테이블을 만들어 1대다 + 다대1 관계를 통해 다대다 관계를 구현
상품
- 상품은 도서, 음반, 영화로 구분
- 상품이라는 공통 속성을 사용하므로 상속 구조
엔티티
회원(Member)
- 이름, 임베디드 타입인 주소(Address), 주문 리스트(orders)
주문(Order)
- 주문한 회원, 배송 정보, 주문 날짜, 주문 상태
- 주문 상태는 열거형(Enum)을 사용
주문 상품(OrderItem)
- 주문한 상품 정보, 주문 금액, 주문 수량
상품(Item)
- 이름, 가격, 재고수량
- 상품이 주문되면 재고수량이 줄어듦
배송(Delivery)
- 주문시 하나의 배송 정보를 생성 ( 1 : 1 관계 )
카테고리(Category)
- 상품과 다대다 관계, parent, child로 부모, 자식 카테고리간의 계층적 관계 표현
테이블
ITEM
- 앨범, 도서, 영화 타입을 통합해서 하나의 테이블로 만듦, DTYPE 칼럼으로 타입을 구분
- @Inheritance(strategy = InheritanceType.SINGLE_TABLE) 을 사용하여 하나로 통합할 것임.
연관관계 매핑
회원 - 주문
- 일대다의 양방향 관계
- 연관관계의 주인을 정해야 하는데, 외래키가 있는 주문을 연관관계의 주인으로 설정
- Order.member를 ORDERS.MEMBER_ID 외래키와 매핑
연관관계의 주인에 대한 개념을 간단히 설명해보면, 엔티티에서는 Member가 orders라는 주문 리스트를 가지고 있지만, 실제 MEMBER 테이블에서는 외래키를 관리하지 않는다. 다만 ORDERS에 있는 MEMBER_ID라는 외래키를 통해 조인하여 조회할 뿐이다.
엄밀히 이야기하면 객체에는 양방향 연관관계라는 것이 없다. 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 할 뿐이다. 반면에 DB 테이블은 외래 키 하나로 양쪽이 서로 조인할 수 있다. 따라서 테이블은 외래 키 하나만으로 양방향 연관관계를 맺는다.
엔티티를 양방향 연관관계로 설정하면 객체의 참조는 둘인데 외래 키는 하나이다. 따라서 둘 사이에 차이가 발생한다. 그렇다면 둘 중 어떤 관계를 사용해서 외래 키를 관리해야 할까?
이런 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데
이것을 연관관계의 주인이라 한다.
결론적으로, 외래키가 있는 곳을 연관관계의 주인으로 정하면 된다.
DB에서 일대다 관계가 존재한다면 외래키는 항상 다쪽에 존재한다. 따라서 외래키가 있는 다쪽을
연관관계의 주인으로 정하는 것이다. 그렇지 않다면, MEMBER에서 관리하지 않는 ORDER 테이블의 외래 키 값이 업데이트 되므로 관리와 유지보수가 어렵고, 추가적으로 별도의 업데이트 쿼리가 발생하는 성능 문제도 발생함.
주문 상품과 상품
- 다대일 단방향 관계
- OrderItem.item을 ORDER_ITEM.ITEM_ID 외래키와 매핑
주문과 배송
- 일대일 양방향 관계
- Order.delivery를 ORDERS.DELIVERY_ID 외래키와 매핑
카테고리와 상품
- @ManyToMany를 사용해서 매핑
- 실제로는 사용해선 안된다. 다대다 관계의 공부를 위해 써보는 것 뿐
엔티티 클래스
회원 엔티티
주문 엔티티
- 참고로 Enum을 사용할 때, EnumType을 String으로 해주는 것이 좋다.
- Default 값이 ORDINAL인데 이것은 배열의 index처럼 값을 넣어주게된다.
- ex) [ORDER, CANCEL] : ORDER라면 0
- 만약 열거형에 새로운 것이 추가가 된다면?
- 원래는 ORDER가 0이 었는데 앞에 HI가 새로 추가된다면 HI가 0이되고 ORDER가 1이되므로 심각한 문제가 발생할 수 있음
주문 상태
주문상품 엔티티
상품 엔티티(추상 클래스)
- strategy 속성을 통해 하나의 테이블로 통합.
- InheritanceType의 값으로 JOINED, SINGLE_TABLE, TABLE_PER_CLASS 가 존재
- JOINED : 각각의 테이블로 변환하는 방법, 공통 속성은 추상클래스에 존재, 개별 속성은 각각의 테이블에 존재
- SINGLE_TABLE : 하나의 테이블로 통합
- TABLE_PER_CLASS : 서브타입 테이블로 변환하는 구현 클래스마다 테이블을 생성하는 전략
- JOINED 전략과 유사하지만, 공통 속성(컬럼)들이 중복되도록 허용하는 전략
- @DiscriminatorColumn을 통해 구분할 컬럼인 dtype을 지정
상품 - 도서 엔티티
- 도서 타입은 dtype이 "B"
상품 - 음반 엔티티
상품 - 영화 엔티티
배송 엔티티
배송 상태
카테고리 엔티티
- 실제 프로젝트를 만들 때는 @ManyToMany 사용 X
- @ManyToMany는 중간 테이블에 컬럼을 추가할 수 없고, 세밀하게 쿼리를 실행하기 어렵기 때문에 한계가 있다.
- 중간 엔티티를 만들고 @ManyToOne + @OneToMany로 매핑해서 사용하자.
- Order - OrderItem - Item과 같이
주소 값 타입
- 값 타입은 변경 불가능하게 설계해야함
- @Setter를 제공하지 않고, 생성자에서 값을 모두 초기화해서 변경 불가능한 클래스로 만드는 것이 좋다.
- JPA 스펙상 엔티티나 임베디드 타입은 Java 기본 생성자를 public 또는 protected로 설정해야한다.
- JPA가 이런 제약을 두는 이유는 JPA 구현 라이브러리가 객체를 생성할 때 리플랙션 같은 기술을 사용할 수 있도록 지원해야 하기 때문이다.
이론적으로 Getter, Setter 모두 제공하지 않고, 꼭 필요한 별도의 메서드를 제공하는게 가장 이상적이다.
하지만 실무에서 엔티티의 데이터는 조회할 일이 너무 많으므로, Getter의 경우 모두 열어두는 것이 편리하다. Getter는 아무리 호출해도 호출 하는 것 만으로 어떤 일이 발생하지는 않는다.
Setter는 문제가 다르다. Setter를 호출하면 데이터가 변한다. Setter를 막 열어두면 가까운 미래에 엔티티에가 도대 체 왜 변경되는지 추적하기 점점 힘들어진다. 그래서 엔티티를 변경할 때는 Setter 대신에 변경 지점이 명확 하도록 변경을 위한 비즈니스 메서드를 별도로 제공해야 한다.
실제 서버를 켜고 스프링 부트를 실행시켜 보자.
위와 같이 로그로 쿼리문이 찍히고 있는 것을 확인할 수 있고 H2 데이터베이스 서버에 접속해보면
설계대로 테이블이 생성된 것을 확인할 수 있다.
반응형
'Spring > SpringBoot_JPA' 카테고리의 다른 글
API 개발 기본 (0) | 2021.05.26 |
---|---|
변경 감지와 병합(merge) (0) | 2021.05.24 |
테스트 예외처리 (0) | 2021.05.20 |
엔티티 설계 주의점 (0) | 2021.05.18 |
JPA, DB 설정 + 간단한 테스트 (0) | 2021.05.16 |