티스토리 뷰

728x90
반응형

 

 

 

지난 글에서 Spring Data JDBC를 적용하기 위해 도메인 엔티티와 데이터베이스 테이블을 설계했다.

 

이번 글에선 위 설계를 바탕으로 도메인 엔티티 클래스를 코드로 정의한다.

 

먼저 지난 글의 설계를 요약하고 가자.

 

엔티티 설계

 

  • Member 클래스와 Order 클래스는 1 대 N의 관계

    • 1에 해당되는 Member 클래스는 N에 해당되는 Order 클래스의 객체를 참조할 수 있도록 List<Order> 추가
  • Order 클래스와 Coffee 클래스는 N 대 N의 관계이므로, 1 대 N과 N 대 1의 관계로 변환

    • 1에 해당되는 Order 클래스는 N에 해당되는 OrderCoffee 클래스의 객체를 참조할 수 있도록 List<OrderCoffee> 추가
    • 1 해당되는 Coffee 클래스는 N 해당되는 OrderCoffee 클래스의 객체를 참조할 있도록 List<OrderCoffee> 추가

 

테이블 설계 확인

 

  • MEMBER 테이블과 ORDERS 테이블은 1 대 N의 관계

    • 1에 해당되는 MEMBER 테이블과 N에 해당되는 ORDERS 테이블은 member_id 외래 키(Foreign key)로 조인
  • ORDERS 테이블과 COFFEE 테이블은 N 대 N의 관계이므로, 1 대 N과 N 대 1의 관계로 변환

    • 1에 해당되는 ORDERS 테이블과 N에 해당되는 ORDER_COFFEE 테이블은 order_id 외래 키(Foreign key)로 조인
    • 1 해당되는 COFFEE 테이블과 N 해당되는 ORDER_COFFEE 테이블은 coffee_id 외래 키(Foreign key) 조인

 

테이블의 외래 키(Foreign key) vs. 클래스의 객체 참조 리스트(List<>)

 

클래스 간에는 외래 키가 없는 대신 객체 간에 참조가 가능하기 때문에 이 객체 참조를 사용해서 외래 키의 기능을 대신한다.

 

MEMBER 테이블은 member_id를 ORDERS 테이블의 외래 키로 지정해 조인함으로써 ORDERS의 데이터 조회가 가능.

 

하지만 Member 클래스는 외래 키 자체가 없기 때문에 객체 참조를 이용해서 조회해야 한다.

 

또한 Member 클래스가 Order 클래스의 객체를 여러 개 가질 수 있으므로 List<>를 사용한다.

 

 

도메인 엔티티 클래스 정의

위에 설계한 도메인 엔티티 클래스 간의 관계는 잘 만들어 졌지만,

 

Spring Data JDBC를 사용하기 위해서는 이 설계를 DDD의 Aggregate 매칭 규칙에 맞출 필요가 있다.

 

Aggregate와 Aggregate Root를 굳이 정의한 것도 이 규칙을 위한 것이라 할 수 있다.

 

먼저 규칙을 확인하자.

 

Aggregate 객체 매핑 규칙

 

  1. Aggregate 내부의 모든 엔티티 객체의 상태는 Aggregate Root를 통해서만 접근 및 변경
  2. 하나의 Aggregate 내에서는 엔티티 간에 객체 참조
  3. 서로 다른 Aggregate Root 간의 참조

    • Aggregate Root 간의 참조는 객체 참조 대신에 ID로 참조
    • 1 : 1과 1 : N 관계일 때는 테이블 간의 외래 키 방식과 동일
    • N : N 관계일 때는 외래 키 방식인 ID참조와 객체 참조 방식을 함께 사용

 

1번 규칙은 예를 들면 위 그림에서 결제를 완료한 회원에게 포인트를 지급할 때,

 

회원 포인트라는 엔티티에 바로 접근하지 말고 회원 정보라는 루트를 통해 접근하라는 뜻이다.

 

이는 Aggregate Root가 나머지 모든 엔티티에 대한 객체를 직간접적으로 참조할 수 있다는 의미가 된다.

 

이는 한 마디로 말하면 도메인 규칙의 일관성을 유지하도록 하는 규칙인데,

 

음식이 다 만들어진 이후에 주문 취소를 못 하게 막는다거나 배달 출발 후에 주소를 바꾸지 못하게 하는 등

 

일관성을 유지할 수 있게 도와준다.

 

3번 규칙은 서로 다른 Aggregate 간의 참조에 관한 내용을 담고 있다.

 

Aggregate Root 간 객체 참조를 하지 않고, 참조하려는 Aggregate Root의 ID를 참조 값으로 멤버 변수에 추가하는 것이다.

 

1 : N일 경우와 N : N일 경우의 구현 방식이 조금 다른데, 이는 실제로 구현하면서 살펴보기로 하자.

 

위 그림은 지난 글에서 완성한 도메인 엔티티 모델의 설계이다.

 

이를 바탕으로 엔티티 클래스를 구현해 보자.

 

엔티티 클래스 구현

  • Member 클래스와 Order 클래스의 Aggregate Root 매핑
package com.gnidinger.member.entity;

import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.Id;

@Getter
@Setter
public class Member {

    @Id // @Id 애너테이션 추가
    private long memberId;

    private String email;

    private String name;

    private String phone;
}

Member 클래스는 Spring Data JDBC의 엔티티이므로 식별자에 @Id 애너테이션을 추가했다.

 

이제 Member 클래스는 회원 Aggregate의 루트이며, 데이터베이스의 MEMBER 테이블과 매핑된다.

...
import com.gnidinger.member.entity.Member;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.Id;
import org.springframework.data.jdbc.core.mapping.AggregateReference;
import org.springframework.data.relational.core.mapping.Table;
...

@Getter
@Setter
@Table("ORDERS")  // (1)
public class Order {

    @Id // (2)
    private long orderId;

    // (3)
    private AggregateReference<Member, Long> memberId;

    ...
}

다음은 Order 클래스이다. (2)에 의해 Order 클래스는 주문 Aggregate의 루트가 된다.

 

(1) - ‘ORDER’는 SQL 쿼리문에서 사용하는 예약어이기 때문에 @Table(”ORDERS”)와 같이 테이블 이름 변경

(2) - orderId에 @Id를 붙여 식별자로 지정. 이제 Order 클래스는 ORDERS 테이블과 매핑

(3) - Aggregate 매핑 규칙에 의해 서로 다른 Aggregate Root 간의 참조는 ID 참조로 이루어진다.

        AggregateReference는 다른 Aggregate Root 간의 ID 참조를 돕는 인터페이스이다.

        즉, AggregateReference를 사용하면 직접적인 객체 참조가 아닌 ID 참조가 이루어진다.

 

  • Order 클래스와 Coffee 클래스의 Aggregate Root 매핑

위에서 알아본 Member 클래스와 Order 클래스는 1 : N 매핑이었다.

 

이번에 진행할 Order 클래스와 Coffee 클래스의 매핑은 N : N이라 조금 다르게 진행된다.

package com.gnidinger.coffee.entity;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import org.springframework.data.annotation.Id;

@Getter
@Setter
@AllArgsConstructor
public class Coffee {

    @Id // @Id 애너테이션 추가
    private long coffeeId;
    private String korName;
    private String engName;
    private int price;
    private String coffeeCode; // 컬럼 추가
}

먼저 Coffee 클래스이다.

 

위와 마찬가지로 @Id 애너테이션을 추가해 커피 Aggregate의 루트로 만들고, 데이터베이스의 COFFEE 테이블과 매핑한다.

 

추가로 중복 등록을 확인하기 위한 coffeeCode 변수를 추가했다.

 

이어서 Order 클래스에 Coffee 클래스와의 매핑을 위한 코드를 추가한다.

...

@Getter
@Setter
@Table("ORDERS")
public class Order {
    ...

    @MappedCollection(idColumn = "ORDER_ID")
    private Set<CoffeeRef> orderCoffees = new LinkedHashSet<>();
    
    ...
}

Order 클래스와 Coffee 클래스는 둘 다 Aggregate Root이다.

 

따라서 ID 참조를 사용해 매핑해야 하지만, N : N 관계라 위의 경우와는 다르게 처리한다.

 

일반적으로 N : N 관계는 두 개의 1 : N 관계로 나누어 다룬다.

 

이때 필요한 엔티티 클래스가 바로 CoffeeRef이며, 이는 아래 그림에서 OrderCoffee 엔티티 역할을 한다고 보면 된다.

 

중요한 점은 이 엔티티가 Coffee가 아닌 Order와 같은 Aggregate에 속해있다는 것인데,

 

Aggregate 매핑 규칙 2번에 의해 Set<CoffeeRef>를 통해 Order 클래스와 CoffeeRef 클래스의 1 : N관계가 만들어진다.

 

@MappedCollection

 

@MappedCollection(idColumn = "ORDER_ID")은 엔티티 클래스 사이의 참조 관계에 대한 정보를 담고 있다.

 

바로 idColumn 속성 때문인데, 자식 테이블에 추가되는 외래 키에 해당되는 컬럼명을 지정해 준다.

 

마지막으로 CoffeeRef 클래스와 Coffee 클래스의 Aggregate Root 매핑을 보자.

package com.gnidinger.order.entity;

import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.data.relational.core.mapping.Table;

@Getter
@AllArgsConstructor
@Table("ORDER_COFFEE") // (1)
public class CoffeeRef {
    private long coffeeId; // (2)
    private int quantity;
}

(1) - Order 클래스와 마찬가지로 테이블 명 변경

 

(2) - CoffeeRef와 Coffee는 다른 Aggregate에 속하지만 N : N 관계에서는 AggregateReference가 필요 없음

 

N : N 관계에서의 참조를 요약하면

 

  1. N : N의 관계를 두 개의 1 : N 관계로 변경 
  2. 1 : N, N : 1의 관계를 CoffeeRef를 통해 다시 1 : N : 1의 관계로 변경

이 될 것이다.

 

  • Order 클래스의 멤버 변수 추가
...

@Getter
@Setter
@Table("ORDERS")
public class Order {
    ...

    private OrderStatus orderStatus = OrderStatus.ORDER_REQUEST; // (1)
    
    private LocalDateTime createdAt; // (2)

    public enum OrderStatus { // (3)
        ORDER_REQUEST(1, "주문 요청"),
        ORDER_CONFIRM(2, "주문 확정"),
        ORDER_COMPLETE(3, "주문 완료"),
        ORDER_CANCEL(4, "주문 취소");

        @Getter
        private int stepNumber;

        @Getter
        private String stepDescription;

        OrderStatus(int stepNumber, String stepDescription) {
            this.stepNumber = stepNumber;
            this.stepDescription = stepDescription;
        }
    }
}

(1) - 주문 상태 정보 설정을 위한 변수. OrderStatus enum타입이며 기본 값은 ORDER_REQUEST이다.

 

(2) - 주문 등록 시간 정보를 나타내는 변수. LocalDateTime 타입이다.

 

(3) - 주문의 상태를 나타내는 enum으로, 현재  개의 주문 상태를 가지고 있다.

 

  • schema.sql 파일에 테이블 생성 스크립트 추가

마지막으로 스키마 파일에 클래스와 매핑되는 테이블 생성 스크립트를 추가한다.

CREATE TABLE IF NOT EXISTS MEMBER (
    MEMBER_ID bigint NOT NULL AUTO_INCREMENT,
    EMAIL varchar(100) NOT NULL UNIQUE,
    NAME varchar(100) NOT NULL,
    PHONE varchar(100) NOT NULL,
    PRIMARY KEY (MEMBER_ID)
);

CREATE TABLE IF NOT EXISTS COFFEE (
    COFFEE_ID bigint NOT NULL AUTO_INCREMENT,
    KOR_NAME varchar(100) NOT NULL,
    ENG_NAME varchar(100) NOT NULL,
    PRICE number NOT NULL,
    COFFEE_CODE char(3) NOT NULL,
    PRIMARY KEY (COFFEE_ID)
);

CREATE TABLE IF NOT EXISTS ORDERS (
    ORDER_ID bigint NOT NULL AUTO_INCREMENT,
    MEMBER_ID bigint NOT NULL,
    ORDER_STATUS varchar(20) NOT NULL,
    CREATED_AT datetime NOT NULL,
    PRIMARY KEY (ORDER_ID),
    FOREIGN KEY (MEMBER_ID) REFERENCES MEMBER(MEMBER_ID)
);

CREATE TABLE IF NOT EXISTS ORDER_COFFEE (
    ORDER_ID bigint NOT NULL,
    COFFEE_ID bigint NOT NULL,
    QUANTITY int NOT NULL,
    FOREIGN KEY (ORDER_ID) REFERENCES ORDERS(ORDER_ID),
    FOREIGN KEY (COFFEE_ID) REFERENCES COFFEE(COFFEE_ID)
);

현재는 인메모리 DB를 사용하기 때문에 테이블 DROP 과정은 넣지 않았다.

 

이렇게 엔티티 클래스가 끝났다.

 

다음 글에선 서비스 클래스와 레포지토리 클래스를 구현해 보자.

반응형
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/06   »
1
2 3 4 5 6 7 8
9 10 11 12 13 14 15
16 17 18 19 20 21 22
23 24 25 26 27 28 29
30
글 보관함