JPA-DDD 1 프로젝트 시작하기 - 1

시작하기전에


DDD / JPA기반의 프로젝트를 처음 도입하며 고민한 내용을 모아보았습니다.

모든 예제를 자세하게 다루지는 않습니다.
구체적인 예시보다는 방향성을 중점으로 봐주시면 감사하겠습니다.
같이 작업한 전OO님과 많은 의논을하며 공부했습니다. 감사합니다

의존성

1
2
3
4
5
dependencies {
...
compile("org.springframework.boot:spring-boot-starter-data-jpa")
...
}

DOMAIN 모델링 - 방향설정하기 ★★★★

  • 방향을 잘못정하면 관계가 복잡해진다 -> DDD의 의도와 맞지않음
  • 책임 관계가 모호해진다
  • ….
  • 자세한 권한설정및 방향은 뒤에서 다룸.

예제

공장에서 만든 제품을 매장공급한다고 가정


방향설정 정책 정하기

1. 핵심이 되는 도메인이 무엇인지 설정하기 -> root 에그리거트로

  • root 에그리거트가 책임을 가지는 영역을 정한다
  • root -> 하위도메인 방향으로만 제어할것
  • 하위에서는 key만 가지고가기(1:n)
    • one to one 에는 논란이있음 -> 뒤에서 설명함

2. root 에그리거트끼리의 참조

  • root 에그리거트끼리는 서로 참조하지 않는것을 원칙으로한다. (알고있다는 의존관계를 최소화)
  • 참조가 반드시 필요하다고 생각된다면 방향성도 단방향으로 고려한다
  • 다른 에그리거트에서의 참조 권한은 readonly를 고려한다

3. root 에그리거트끼리의 관계(행위)와 이 관계에 의존된 도메인들

  • 관계,행위라고하면 아래 예시에서 Supplement에 해당한다.
  • 관계를 매핑하는 entity (행위) -> root쪽으로 방향을가지되, readonly 를 권장한다.
  • insert/update 가 영속성으로 관리되면 안된다. service transaction으로 제어해야한다

4. MSA를 고려하자

  • 추후 각 root 에그리거트들은 하나의module로서 하나의 appliation으로 동작할수있음을 유의하자
  • 서로의 연관관계는 가능한 결합으로 한다

도메인 나누기

만약에 관계를 나타내는 도메인에도, 하위 도메인이 있다면?

네이밍 변경을고려. 공장에서 상점에 공급한다는 의미로 변경. MarketFactory -> Supplement

완성된 에그리거트 영역

여담 - DDD에서 package 구조

대부분의 spring 기반의 프로젝트는 아래와 같은 구조를 하고있을것

1
2
3
4
5
6
|-- controller 
|-- service
|-- repository
|-- model
|-- config
|-- Application.java

하지만 페키지 구조를 만드는 방법은 매우 다양

DDD의 레이어별로 나누는 방법

1
2
3
4
5
6
|-- presentation 
|-- domain
|-- order
|-- member
|-- application
|-- infrastructure

그리고 도메인 내부에서 레이어를 나누는 방법도 있다

1
2
3
4
5
6
7
8
9
10
|-- order 
|-- presentation
|-- domain
|-- application
|-- infrastructure
|-- member
|-- presentation
|-- domain
|-- application
|-- infrastructure

무엇이 정답이다 라고 말할수는 없는것 같다
도메인과 서비스의 크기에따라, 구성원들의 합의에 다라 달라질것이다.

제가 담당하는 서비스의 페키지구조는 아래와 같습니다

1
2
3
4
5
6
7
8
9
|-- order
|-- controller
|-- presentation
|-- domain
|-- application
|-- repository
|-- infrastructure
|-- config
|-- exception

( 참조 : https://stylishc.tistory.com/144 )

JPA 에서 단방향 관계 맺기 ★★★

단방향과 양방향

db에서의 연관관계란 (rdb 기준)

  • join을위해서 테이블과 테이블이 key를 통해 참조하는것
  • 양쪽에 공통적으로 가지고 있는 key가 존재, 2개 테이블의 데이터로만 본다면 방향성이 있을까?
  • 결과적으로 데이터가 나왔을때도 방향이라는것이 정해져있지 않고, join기준에 따라서 쿼리가 바뀌며, 이에따라 기준이 바뀐다고 볼 수 있음.

jpa에서 연관관계

  • DDD(객체지향)에서의 연관관계란 권한책임을 가진다.
  • 권한 / 책임에 따라서 로직과 디자인에 영향을 준다.
  • 방향성에따라 복잡도 및 side effect가 발생할 수 있다.

개발편의성

모든 로직을 root 에그리거트를 기준으로 작성하는 경우 불편해질수 있습니다
예를들면 Car에 대한 접근을 하려면

  1. MarketRepository를 통해 Market에 대한 정보를 가지고온 다음
  2. 하위 도메인인 Department에게 Car 조회를 해달라고 위임합니다.
  3. Department는 Car의 정보를 Market에 전달하고 그 데이터를 활용합니다.

하위도메인에 대한 접근이 매우 잦아진다면 그 플로우가 복잡해 질 수 있습니다.
그런경우 그 도메인을 root도메인으로 승격시키는것을 고려해볼수도 있습니다.

권한

  • 양방향 관계를 가진다는것은 하위도메인에서 상위도메인의 데이터를 조회/변경을 할 수 있다는 의미이다. 그리고 변경을 하고나서 save가 이루어지는 경우 상위 도메인의 내용이 변경 될 수 있음을 의미한다
  • 사실상 상위/하위 관계가 없어지는것과 같다고 생각한다.

이문서에서는

모든 관계는 단방향기준으로 공유드립니다
기본적으로 양방향 관계는 가능한 지양하겠습니다.

1:1 - @OneToOne

이번예제에서는

  1. 자식이 부모키와 동일한것을 사용하는경우,
  2. 자식이 부모키를 fk로 가지고있는 경우

2가지로 분류하어 서술합니다.

parent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Table(name = "market")
@Entity
...
public class Market {
@Id
private long marketId;

//1. 자식이 부모키와 동일한것을 사용하는경우,
@OneToOne(cascade = CascadeType.ALL)
@PrimaryKeyJoinColumn
private Address address;

// or

//2. 자식이 부모키를 fk로 가지고있는 경우
@OneToOne(cascade = CascadeType.ALL)
@JoinColumn(name = "market_id")
private Address address;

}

child

한쪽에서만 key를 소유하고있어서 단방향으로 관계를 맺음

1
2
3
4
5
6
7
@Table(name = "address")
@Entity
...
public class Address {
@Id
private long marcketId;
}

or

1
2
3
4
5
6
7
8
@Table(name = "address")
@Entity
...
public class Address {
@Id
private long addressId;
private long marcketId;
}

N:1 & 1:N - @ManyToOne & @OneToMany

1:N 관계에서는 N쪽에서(자식쪽) 부모의 key를 가집니다.

parent

1
2
3
4
5
6
7
8
9
10
11
@Table(name = "market")
@Entity
...
public class Market {
@Id
private long marketId;

@OneToMany(cascade = CascadeType.ALL)
@JoinColumn(name = "marketId")
private Set<Department> departments
}

child

1
2
3
4
5
6
7
8
9
10
11
@Table(name = "department")
@Entity
...
public class Department {

@Id
private long departmentId;

//자식은 부모의 key만 가지고있어서 접근불가!
private long marketId;
}

관계 annotation들(@OneToOne등)의 주요 options

크게 2가지 종류의 annotation으로 데이터의 연관관계를 표현하게된다.
도메인 객체로서의 관계를 먼저설명하고, / 이후 컬럼으로서의 관계에 대해 설명한다

도메인 객체로서의 관계를 정의하는 anotation으로는
@OneToOne, @ManyToOne … 것들이 있다.

cascade

1
@OneToMany(cascade = CascadeType.ALL)
  • 영속성을 의미한다
  • 부모를 저장할때 자식도 같이저장, / 제할때 같이삭제 등등 영속성을 관리할때 사용한다
  • 종류
    • ALL - 모두
    • PERSIST
    • MERGE
    • REMOVE
    • REFRESH
    • DETACH

fetch

1
@ManyToOne(fetch = FetchType.LAZY)

데이터를 가지고올때 fetch하는 방식을 정의한다.
크게 LAZY / EAGER 가 있다

LAZY :

  • 접근시도시 로딩
  • xxxToMany 의 디폴트

EAGER :

  • 부모데이터 로드시 함깨 로드
  • xxxToOne 의 디폴트값

orphanRemoval (고아객체 허용/삭제여부)

1
@OneToMany(orphanRemoval = true)
  • 관계에서 child 에서 참조하고있는 부모의 키 (fk)를 nulll로 셋팅하게되면 관계를 끊은것으로보고, 고아객체가된다
  • 일반적인 케이스에서는 고아객체를 접근 할 수 없다(root 를 통해서만 접근하므로 ^^; 그렇게 되어야한다)
  • orphanRemoval 이 true로 설정되어있다면 고아객체가 되는상황에서 child를 제거한다.

optional

1
@OneToMany(optional=false)

관계를 가지는 도메인이 필수값인경우 optional을 false로 줄 수 있다.
객체 생성시 해당 값이 null이면 exception이 난다

컬럼 annotation(@Column) 의 주요 options

두번째로 @Column의 옵션이다.
이항목은 사용하고있는 infrastructure 의 스팩과 닿아있다고 생각하면 편하다.
사용하고있는 infrastructure의 해당 컬럼의 속성에따라 실제 만들어지는 쿼리도 달라지게된다.

1
@Column(name = "registerDatetime", nullable = false, updatable = false, insertable=false,unique=true, length=100)
  • name: 테이블 컬럼이름
  • nullable : 해당 컬럼이 nullable인지 여부
  • unique : 해당 컬럼이 unique인지 여부
  • length : 해당 컬럼의 길이. string 인경우에만 사용하도록하자. default : 255

Comments