DDD 도메인주도설계

도메인주도개발 시작하기 4장

MIN우 2024. 1. 18. 17:38
728x90

JPA 를 이용한 리포지터리 구현

도메인 모델과 리포지터리를 구현할 때 선호하는 기술은 JPA 를 들 수 있다.

데이터 보관소로 RDMS 를 사용할 때 객체 기반의 도메인 모델과 관계형
데이터 모델 간의 매핑을 처리하는 기술로 ORM 만한 것이 없다.

모듈 위치

리포지터리 인터페이스는 애그리거트와 같이 도메인 영역에 속하고
리포지터리를 구현한 클래스는 인프라스트럭처 영역에 속한다.

이는 리포지터리 구현 클래스를 인프라스트럭처 영역에 위치시켜서 인프라스트럭처에 대한 의존을 낮춰야 한다.

 

DIP에 따라 리포지터리 구현 클래스는 인프라스트럭처 영역에 위치한다.

즉 , mongodb,mysql,postgre등등 구현기술은 인프라스트럭처계층에서 구현하고 언제든지 바꿔낄 수 있어야한다.

 

리포지터리가 제공하는 기능은 두가지.

- ID로 애그리거트 찾기

- 애그리거틑 저장하기

 

인터페이스는 애그리거트 루트를 기준으로 작성한다.

 

ID외의 다른 조건으로 애그리거트를 조회할 때 Criteria,JPQL을 사용할 수 있다.

 

삭제기능의 경우 실제 삭제 요구사항이 있더라도 삭제 데이터조회, 복구등을 위해
일정 기간 보관해야 할수도 있어 삭제 플래그를 사용해서 화면에 보여줄지 여부를 결정하는 방식으로 구현하기도 한다.

 

즉 , 개인정보처리방침에 따라 유저를 약 6개월동안 보관해야하기 때문에 state값 바꿔서 유저의 상태를 관리해야한다.

 

스프링 데이터 JPA 를 이용한 리포지터리 구현

스프링과 JPA 를 적용할때 스프링 데이터 JPA 를 사용한다.
지정한 규칙에 맞게 인터페이스를 정의하면 구현체를 만들어 스프링 Bean 으로 등록해준다.

스프링 데이터 JPA 인터페이스 규칙 및 메서드 설명이므로 생략

 

 

매핑 구현

엔티티와 밸류 기본 매핑 구현

애그리거트와 JPA 매핑을 위한 기본 규칙은 다음과 같다.

  • 애그리거트 루트는 엔티티이므로 @Entity 로 매핑 설정한다.

한 테이블에 엔티티와 밸류 데이터가 같이 있다면

  • 밸류는 @Embeddable 로 매핑 설정한다.
  • 밸류 타입 프로퍼티는 @Embedded 로 매핑 설정한다.

@Embeddable , @Embedded 예시

@Entity
@Tagble(name = "purchase_order")
public class Order {
  // ...

  @Embedded
  private Orderer orderer;

  // ...
}

@Embeddable
public class Orderer {

  // MemberId에 정의된 칼럼 이름을 변경하기 위해
  // @AttributeOverride 애노테이션 사용
  @Embedded
  @AttributeOverrides(
      @AttributeOverride(name = "id", column = @Column(name = "orderer_id"))
  )
  private MemberId memberId;

  @Column(name = "orderer_name")
  private String name;

  // ...
}

@Embaddable
public class MemberId implements Serializable {

  @Column(name = "member_id")
  private String id;
  // ...
}

 

기본 생성자

밸류 타입의 경우 불변이므로 생성 시점에 필요한 값을 모두 전달받으므로 값을 변경하는 set 메서드는 제공하지 않는다.

하지만 JPA 의 @Entity 와 @Embeddable 로 클래스를 매핑하려면 기본 생성자를 제공해야 한다.

DB 에서 데이터를 읽어와 매핑된 객체를 생성할 때 기본 생성자를 사용해서 객체를 생성한다.
이런 이유로 다른 코드에서 기본 생성자를 사용하지 못하도록 protected 로 선언한다.

@Embeddable
public class Receiver {

  @Column(name = "receiver_name")
  private String name;

  // ...

  protected Receiver() {
  } // JPA를 적용하기 위해 기본 생성자 추가

  public Receiver(String name, String phone) {
    this.name = name;
    this.phone = phone;
  }

}

불변 객체

  • 객체 타입을 수정할 수 없게 만들면 부작용을 원천 차단
  • 값 타입은 불변 객체(immutable object)로 설계해야함
  • 불변 객체: 생성 시점 이후 절대 값을 변경할 수 없는 객체
  • 생성자로만 값을 설정하고 수정자(Setter)를 만들지 않으면 됨 (핵심!)

필드 접근 방식 사용

JPA 는 필드와 메서드의 두 가지 방식으로 매핑 처리를 할 수 있다.

메서드 방식을 사용하려면 get/set 메서드가 추가되어야한다.

set 메서드는 내부 데이터를 외부에서 변경할 수 있는 수단이 되기 때문에 캡슐화를 깨는 원인이 될 수 있다.
엔티티가 객체로서 제 역활을 하려면 외부에 set 메서드 대신 의도가 잘 드러나는 기능을 제공해야 한다.

엔티티를 객체가 제공할 기능 중심으로 구현하도록 유도하려면 JPA 매핑 처리를 프로퍼티 방식이
아닌 필드 방식으로 선택해서 불필요한 get/set 메서드를 구현하지 말아야 한다.

 

AttributeConverter 를 이용한 밸류 맵핑 처리

구현방식에 따라 밸류 타입의 프로퍼티를 한 개 칼럼에 매핑해야 할 때도 있다.

예를 들어 Length 가 길이 값과 단위의 두 프로퍼티를 갖고 있는데 DB 테이블에는
한 개 칼럼에 '1000mm' 와 같은 형식으로 저장할 수 있다.

이때 사용할 수 있는 것이 AttributeConverter 이다.
이는 밸류타입과 칼럼 데이터간의 변환을 처리하는 기능을 지원한다.

 

AttributeConverter

- int, long, String, LocalDate 같은 타입은 DB 테이블에 한 개의 컬럼에 매핑됨.

- 이와 비슷하게 벨로타입의 프로퍼티를 한 개의 컬럼에 매핑해야할 때도 있음.

- AttributeConverter을 통해 벨류 타입과 칼럼 데이터 간의 변환 처리 가능

 

- 벨류 컬렉션을 별도 테이블로 매핑시에는 @ElementCollection과 @CollectionTable을 함께 사용

- List에 자체 인덱스를 갖고 있기 때문에 OrderLine 객체에는 인덱스를 위한 프로퍼티가 존재하지 않음.

- @OrderColumn 에너테이션을 통해 지정한 칼럼에 리스트 인덱스값을 저장

- @CollectionTable은 벨류를 저장할 테이블을 지정. name은 테이블명, joinColumns는 외부키로 사용할 컬럼 지정

 

벨류를 이용한 ID 매핑

- 식별자라는 의미를 부각하기 위해 식별자 자체를 벨류 타입으로 만들 수 있음. @Id 대신 @EmbeddedId

JPA에서 식별자 타입은 Serialzable 타입이어야 함.

 

벨류 컬렉션을 @Entity로 매핑

- 기술적 한계로 인해 @Entity를 사용할 수도 있음

- Image가 값객체이지만 @Embeddable 타입 클래스는 상속 매핑을 지원하지 않기 때문에 @Entity를 사용해야

 

- @Inheritance 에너테이션을 사용하고, @DiscriminatorColumn을 사용하여 타입 구분 칼럼 지정

- 리스트에서 객체 삭제 시 DB에서도 함꼐 삭제되도록 orphanRemoval도 true로 설정

- @OneToMany 매핑에서 clear 메서드를 호출하면 각 개별 엔티티에 대해 Delete 쿼리 실행함

- @Embeddedable 타입에 대한 컬랙션은 한 번의 delete쿼리로 삭제 처리 수행

 

애그리거트 로딩 전략

- 애그리거트 속한 객체가 모두 모여야 하나가 됨. 즉, 애그리거트 루트를 로딩하면 루트에 속한 모든 객체가 완전한 상태여야

- FetchType.EAGER을 설정하여 즉시 로딩, 허나 모든 연관을 즉시 로딩으로 설정할 필요는 없음

- 즉시 로징 방식으로 설정하면 애그리거트 루트를 로딩하는 시점에 모든 객체를 함께 로딩할 수 있으나,
  카타시안 곱, n+1, 쿼리 중복 등 다양한 문제 발생

- 즉시 로딩 방식을 사용할 때 성능 검토 필수

 

애그리거트의 영속성 전파

- 애그리거트가 완전한 상태여야 한다는 것은 애그리거트 루트를 조회할 때뿐만 아니라 저장하고 삭제할 때도 하나로 처리해야 함.

 -저장/삭제 또한 애그리거트 모든 객체가 함께 저장/삭제 되어야

- @Embeddedable 매핑은 함꼐 저장/삭제 되므로 cascade 속성을 추가로 설정하지 않아도 됨.

- 반면, @Entity는 cascade 속성을 사용하여 저장/삭제 시 함께 처리되도록 설정

- cascade 속성의 기본값이 없음. PERSIST, REMOVE를 설정해야

 

 

 

도메인주도개발 시작하기 4장 요약 *** 

 

-  리포지터리 구현 클래스를 인프라스트럭처 영역에 위치시켜서 인프라스트럭처에 대한 의존을 낮춰야 한다.

-  개인정보처리방침에 따라 유저를 약 6개월동안 보관해야하기 때문에 state값 바꿔서 유저의 상태를 관리해야한다.

-  밸류는 @Embeddable 로 매핑 설정한다. , 밸류 타입 프로퍼티는 @Embedded 로 매핑 설정한다.

- 기본 생성자 - 밸류 타입의 경우 불변이므로 생성 시점에 필요한 값을 모두 전달받으므로 값을 변경하는 set 메서드는 제공하지 않는다.

- 필드 접근방식보다 생성자 접근방식을 사용하고 , 생성자 접근방식보다는 정적팩토리메소드를 이용하자.

- 한 개 칼럼에 '1000mm' 와 같은 형식으로 저장할 수 있다.이때 사용할 수 있는 것이 AttributeConverter 이다.
이는 밸류타입과 칼럼 데이터간의 변환을 처리하는 기능을 지원하기때문에 converter를 따로 만들어서 저장하자

- 객체지향적으로 설계하기 좋은 상속관계매핑전략은 조인전략이다. @Inheritance(strategy=InheritanceType.JOINED)전략

하이버네이트의 조인전략은 @DiscriminatorColumn을 선언하지 않으면 DTYPE 컬럼이 생성되지않는다.

조인하게되면 알게되겠지만, 그래도 DTYPE을 넣어주는 것이 명확하다. 

ex)

@Entity
@Table(name = "users")
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "dtype")
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@Getter
public abstract class User extends BaseTimeEntity {
    @Id
    @Column(name = "user_id")
    private String id;

    @Column(name = "account_id")
    private String accountId;
}
@Entity
@Table(name = "admin")
@DiscriminatorValue("admin")
@NoArgsConstructor
@AllArgsConstructor
@SuperBuilder
@Getter
public class AdminUser extends User{
    @Column(name = "phone_number")
    private String phoneNumber;

    @Column(name = "email")
    private String email;

    @Column(name = "name")
    private String name;
}

 

@Inheritance 상속관계는 조인전략으로 하겠다

@DiscriminatorColumn JPA에서 상속 관계 매핑을 할 때, 엔티티 클래스의 계층 구조에서 어떤 엔티티가 어떤 테이블에 매핑되는지를 지정해야 한다.

@NoArgsConstructor jpa는 db에서 데이터를 조회해 온 뒤 객체를 생성할 때 Reflection을 사용하여 필드값을 넣어준다.

때문에 기본 생성자로 객체를 생성해줘야한다.  public이나 protected로 해줘야하는데 private로하면 안되는가 ?

엔티티 객체의 생성 자체만 생각하면 옳은 얘기일지 모릅니다. 하지만 Proxy Lazy Loading이라는 개념이 들어가게 되면 다릅니다. JPA의 엔티티를 매핑하는 방식으로 조회 시 연관된 엔티티 정보를 바로 가져오는 즉시 로딩(Eager Loading) 방식과 연관된 엔티티에 프록시, 즉 가짜 객체를 넣어준 뒤 해당 객체의 정보가 실제로 필요한 타이밍에 연관된 엔티티를 조회해오는 지연 로딩(Lazy Loading) 이라는 방식이 있는데요, 여기서 지연 로딩과 프록시 객체를 사용하기 위해서는 생성자가 private이어서는 안됩니다.

자세한 것은 지연 로딩과 프록시 객체의 동작 방식에 대해 이해하고 넘어가야 하는 부분인데요, 결론부터 말씀드리자면 엔티티의 프록시는 원본 엔티티를 상속해서 만들기 때문입니다. 지연 로딩으로 인해 프록시 객체를 넣어줘야 하면 원본 엔티티를 상속한 프록시 엔티티를 만들고, 실제 필요한 타이밍에 엔티티를 조회해 온 뒤 프록시 엔티티가 원본 엔티티를 참조하도록 하여 사용하는 것이죠. 때문에 연관된 엔티티 자리에 지연 로딩으로 인해 프록시가 들어가든 즉시 로딩으로 실제 엔티티가 들어가든 상관 없이 항상 정상적으로 기능해야 하니 프록시는 당연히 원래 들어가야 할 엔티티의 하위 타입일 수 밖에 없다고 생각하면 이해가 빠를 것 같습니다.

@SuperBuilder 어노테이션은 @Builder 어노테이션의 기능을 보완하기 위해 도입되었습니다. @Builder 어노테이션으로는 상속받은 필드를 빌더에서 사용하지 못하는 등의 제한이 있었습니다. @SuperBuilder 어노테이션은 이러한 제한을 해결하고, 상속받은 필드도 빌더에서 사용할 수 있습니다.

@AllArgsConstructor

@Builder 사용 시, 생성한 생성자가 없다면 @AllArgsConstructor(access = AccessLevel.PACKAGE) 가 암묵적으로 적용된다고 한다. 반대로 생성한 생성자가 있다면 @AllArgsConstructor 적용이 반드시 필요한게 아닐까?.. (이건 추측)

builder()에서 BuilderExampleBuilder() 생성자를 반환하고,

BuilderExampleBuilder() 내부에서는 모든 매개변수에 대해 셋터용 메서드를 만들고 있다. (name, age)

이에 @Builder 내에서는 모든 파라미터를 갖는 생성자를 필요로 한다.

@Getter

캡슐화 및 외부에서 수정이 되지않도록 Setter는 열지않기

 

하위클래스의 AdminUser가 User클래스의 특성을 상속받게해야한다.

 

-  카타시안 곱, n+1, 쿼리 중복 등 다양한 문제 발생

-  즉시 로딩 방식을 사용할 때 성능 검토 필수, 대부분은 지연로딩으로 해결된다. 거의 다 지연로딩으로 하면 될듯함.

- @Embeddedable 매핑은 함꼐 저장/삭제 되므로 cascade 속성을 추가로 설정하지 않아도 됨.

- 반면, @Entity는 cascade 속성을 사용하여 저장/삭제 시 함께 처리되도록 설정

- cascade 속성의 기본값이 없음. PERSIST, REMOVE를 설정해야

 

 

 

생성자 정적 팩토리 메서드
생성자는 의미있는 이름을 가지지 않습니다. 그래서 생성자는 언제나 표준 네이밍 규칙에 따라 제한됩니다. (생성자의 이름은 클래스 이름과 동일해야함) 정적 팩토리 메서드는 의미 있는 이름을 가질 수 있으므로 이 메서드가 하는 것을 명시적으로 클라이언트에게 전달할 수 있습니다.
생성자는 리턴 타입이 존재하지 않습니다. 정적 팩토리 메서드는 리턴 타입이 존재합니다.
생성자의 정의에는 필드멤버의 초기화가 수행됩니다. 정적 팩토리 메서드 정의에는 인스턴스를 생성합니다.
생성자는 언제나 새로운 인스턴스를 힙 안에서 동적으로 생성합니다. 그래서 생성자로부터 클래스의 캐싱된 인스턴스를 리턴할 수 없습니다. 정적 팩토리 메서드는 캐싱 할 수 있기 때문에 새로운 인스턴스를 언제나 생성하는것 대신에 팩토리 메서드로부터 같은 불변 클래스의 인스턴스를 참조하여 반환할 수 있습니다. 
생성자의 첫번째 라인에는 super() 또는 this()가 호출됩니다. 정적 팩토리 메서드는 super()와 this()가 첫번째 라인에 있는 것이 필수적이지는 않습니다.

 

생성자접근방식과 정적팩토리메소드를사용하는방식을 적절히 섞어사용하는 것이 중요할 것 같다.정적 팩토리메소드를 사용하게 될 시

이름을 가질 수 있다, 리턴타입 존재, 호출할 때마다 새로운 객체를 생성할 필요가 없습니다.객체 생성을 캡슐화할 수 있다라는 장점이 있다.

 

 

참고: https://ict-nroo.tistory.com/128

 

[JPA] 상속관계 매핑 전략(@Inheritance, @DiscriminatorColumn)

상속관계 매핑객체는 상속관계가 존재하지만, 관계형 데이터베이스는 상속 관계가 없다.(대부분)그나마 슈퍼타입 서브타입 관계라는 모델링 기법이 객체 상속과 유사하다.상속관계 매핑이라는

ict-nroo.tistory.com

참고:https://hyeonic.tistory.com/191

 

[JPA] 왜 JPA의 Entity는 기본 생성자를 가져야 하는가?

왜 JPA의 Entity는 기본 생성자를 가져야 하는가? 정확히 이야기하면 Entity는 반드시 파라미터가 없는 생성자가 있어야 하고, 이것은 public 또는 protected 이어야 한다. 이러한 궁금증을 가지게 된 이

hyeonic.tistory.com

참고:https://velog.io/@joonghyun/%EC%83%9D%EC%84%B1%EC%9E%90-%EC%98%A4%EB%B2%84%EB%A1%9C%EB%94%A9-%EC%9D%B4%EC%8A%88%EB%A5%BC-%EC%A0%95%EC%A0%81-%ED%8C%A9%ED%86%A0%EB%A6%AC-%EB%A9%94%EC%86%8C%EB%93%9C%EB%A1%9C-%ED%95%B4%EA%B2%B0%ED%95%98%EA%B8%B0#%EC%A0%95%EC%A0%81-%ED%8C%A9%ED%86%A0%EB%A6%AC-%EB%A9%94%EC%86%8C%EB%93%9C

 

생성자 오버로딩 이슈를 정적 팩토리 메소드로 해결하기

오버로딩 자바의 오버로딩은 여러 메소드를 하나의 이름으로 사용할 수 있다는 장점 때문에 메소드 이름을 절약할 수 있습니다. 자바에서 오버로딩으로 다형성을 지원하여 재사용성이 증가하

velog.io

참고: https://poododang.tistory.com/entry/%EC%9D%B4%ED%8C%A9%ED%8B%B0%EB%B8%8C%EC%9E%90%EB%B0%94-item1

 

Item1 - 생성자 대신 정적 팩토리 메소드를 고려하라

클라이언트가 클래스의 인스턴스를 얻는 수단은 2가지의 방법이 있다. public 생성자로 인스턴스를 제공하는 방법 생성자와 별도로 정적 팩토리 메서드로 제공하는 방법 - 클래스 인스턴스를 반

poododang.tistory.com

https://velog.io/@ohzzi/JPA%EC%9D%98-%EC%97%94%ED%8B%B0%ED%8B%B0%EC%97%90-protected-public-%EA%B8%B0%EB%B3%B8-%EC%83%9D%EC%84%B1%EC%9E%90%EA%B0%80-%ED%95%84%EC%9A%94%ED%95%9C-%EC%9D%B4%EC%9C%A0

 

JPA의 엔티티에 protected, public 기본 생성자가 필요한 이유

JPA는 엔티티에 기본 생성자, 즉 아무런 매개변수를 받지 않는 생성자를 만드는 것을 강제하고 있습니다. JPA 구현체마다 스펙이 조금 달라서 기본 생성자를 만들지 않아도 정상적으로 작동하는

velog.io

 

728x90