728x90
반응형
목차
JPA를 이용한 리포지터리 구현
- 도메인 모델과 리포지터리를 구현할 때 선호하는 기술은 JPA이다.
- 데이터 보관소로 RDBMS를 사용할 때, 객체 기반 도메인 모델과 관계 데이터 모델간의 매핑 처리 기술로 ORM만한 것이 없음
모듈위치
- 리포지터리 인터페이스는 애그리거트와 같이 도메인 영역에 속함
- 리포지터리 구현 클래스는 인프라스트럭처 영역
- 팀 표준에 따라 다르지만 리포지터리 구현 클래스틑 인프라스트럭처에 둬서 의존을 낮춰야 함
리포지터리 기본 기능 구현
리포지터리가 제공하는 기본 기능
- ID로 애그리거트 조회하기
- 애그리거트 저장하기
public interface OrderRepository {
Order findById(OrderNo no);
void save(Order order);
}
ex) 주문 애그리거트는 Order 루트 엔티티, OrderLine(주문목록), Orderer(주문자), ShippingInfo(배송지) 등 객체를 포함
위의 코드에서는 루트 엔티티인 Order 기준으로 인터페이스 작성
애그리거트 조회 기능의 이름은 'findBy프로퍼티이름' 형식이 널리 사용됨
- null리턴을 원하지 않을 경우 Optional 사용
Optional<Order> findById(OrderNo no);
- OrderRepository 구현 클래스
@Repository
public class JpaOrderRepository implements OrderRepository {
@PersistenceContext
private EntityManager entityManager;
@Override
public Order findById(OrderNo id) {
return entityManager.find(Order.class, id);
}
@Override
public void save(Order order) {
entityManager.persist(order);
}
}
- 애그리거트 트랜잭션
public class ChangeOrderService {
@Transactional
public void changeShippingInfo(OrderNo no, ShippingInfo newShppingInfo) {
Optional<Order> orderOpt = orderRepository.findById(no);
Order order = orderOpt.orElseThrow(() -> new OrderNotFoundException());
order.changeShippingInfo(newShippingInfo);
}
...
}
- changeShippingInfo() 메서드는 Spring Transaction 관리기능을 통해 트랜잭션 범위에서 실행됨
- JPA는 이때 DB에 변경된 데이터 반영을 위해 UPDATE실행
- changeShippingInfo()메서드 실행 결과로 애그리거트가 변경되면 JPA는 DB에 Update 쿼리 실행
- ID가 아닌 조건으로 애그리거트 조회 findBy프로퍼티
public interface OrderRepository {
List<Order> findByOrdererId(String ordererId, int startRow, int size);
}
- findByOrderId 메서드는 한 개 이상의 Order객체를 리턴할 수 있음 (List타입으로 리턴)
- JPQL을 이용해서도 구현할 수 있음
- 애그리거트 삭제 > EntityManager의 remove()로 삭제 가능
- 요구사항이 있더라도 실제 DB삭제하는 경우는 많지 않음(데이터를 화면에 보여줄지 여부 결정하는 방식)
스프링 데이터 JPA를 이용한 리포지터리 구현
- 스프링 데이터 JPA는 지정한 규칙에 맞게 리포지터리 인터페이스를 정의하면 구현객체를 만들어 스프링 빈으로 등록해줌
- 인터페이스를 직접 구현하지 않아도 되기 때문에 쉽게 정의할 수 있음
- OrderRepository
public interface OrderRepository extends Repository<Order, OrderNo> {
Optional<Order> findById(OrderNo id);
void save(Order order);
}
- Spring Data JPA 빈 등록
@Service
public class CancelOrderService{
private OrderRepository orderRepository;
public cancelOrderService(OrderRepository orderRepository, ...) {
this.orderRepository = orderRepository;
..
}
@Transactional
public void cancel(OrderNo orderNo, Canceller canceller) {
Order order = orderRepository.findById(orderNo)
.orElseThrow(() -> new NoOrderException());
if(!cancelPolicy.hasCancellationPermission(order, canceller)) {
throw new NoCancellablePermission();
}
order.cancel();
}
}
- OrderRepository가 필요하면 코드를 주입받아 사용하면 된다.
- OrderRepository 엔티티 저장하는 메서드
Order save(Order entity)
void save(Order entity)
- 식별자 조회
Order findById(OrderNo id)
Optional<Order> findById(OrderNo id)
- Order목록 조회
List<Order> findByOrderer(Orderer orderer)
// Order객체의 memberId 프로퍼티가 목록 조회(중첩파라미터)
List<Order> findByOrderMemberId(MemberId memberId)
- Entity 삭제
void delete(Order order)
void deleteById(OrderNo id)
매핑 구현
엔티티, 밸류 기본 매핑 구현
- 애거리루트는 @Entity로 매핑 설정
- 한 테이블에 엔티티와 밸류데이터가 같이 있을 경우
- 밸류는 @Embeddable
- 밸류 타입프로퍼티는 @Embedded
- 주문 애그리거트 루트 엔티티는 Order
- Orderer와 ShippingInfo는 밸류
- ShppingInfo에 포함된 Address, Receiver는 한 테이블에 매핑할 수 있음
- 루트 Entity Order는 JPA @Entity로 매핑
@Entity
@Table(name="purchase_order")
public class Order {
...
}
- Order에 속하는 Orderer는 밸류 @Embeddable로 매핑
@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;
}
- Orderer의 memberId와 매핑되는 컬럼은 orderer_id
- @Embeddable타입에 설정한 컬럼이름과 실제 컬럼이름이 다르므로 @AttributeOverrids 이용해서 Orderer의 memberId프로퍼티와 매핑 컬럼 이름 변경
- ShippingInfo 밸류도 Address와 Receiver를 포함
@Embeddable
public class ShippingInfo {
@Embedded
@AttributeOverrids({
@AttributeOverride(name = "zipCode", column= @Column(name="shipping_zipcode")),
@AttributeOverride(name = "address1", column= @Column(name="shipping_addr1")),
@AttributeOverride(name = "address2", column= @Column(name="shipping_addr2"))
})
private Address address;
@Column(name = "shipping_message")
private String message;
@Embedded
private Receiver receiver;
}
- @Embedded를 이용해서 밸류 타입 프로퍼티를 설정
기본 생성자
- 엔티티와 밸류 생성자는 객체 생성시 필요한 것을 전달받음
public class Receiver {
private String name;
private String phone;
public Receiver(String name, String phone) {
this.name = name;
this.phone = phone;
}
}
- Receiver가 불변타입이면 생성시 모두 값을 전달하므로 set메서드 제공하지 않음
- JPA에서 @Entity와 @Embeddable 클래스 매핑시 기본 생성자를 제공해야함
DB에서 데이터를 읽어와서 매핑된 객체 생성시 기본 생성자를 사용해서 객체를 생성하기 때문
@Embeddable
public class Receiver {
@Column(name = "receiver_name")
private String name;
@Column(name = "receiver_phone")
private String phone;
protected Receiver() {} // JPA 적용을 위한 기본 생성자
public Receiver(String name, String phone) {
this.name = name;
this.phone = phone;
}
}
- 기본생성자는 JPA 프로바이더가 객체 생성시에만 사용
- 다른 코드에서 사용할 경우 값이 온전하지 못한 객체가 되기 때문에 protected로 선언
필드 접근 방식 사용
- JPA는 필드와 메서드의 두 가지 방식으로 매핑 처리 가능
- 메서드 방식 이용시 get/set 메서드 구현
@Entity
@Access(AccessType.PROPERTY)
public class Order {
@Column(name = "state")
@Enumerated(EnumType.STRING)
public OrderState getState() {
return state;
}
public void setState(OrderState state) {
this.state = state;
}
}
- 프로퍼티를 위한 공개 get/set메서드 추가시 도메인 의도가 사라지고 데이터 기반 엔티티 구현 가능성이 높아짐
- set은 외부에서 변경할 수 있는 수단이므로 캡슐화가 깨지는 원인이 됨
- set메서드 대신 의도가 잘 드러나는 기능 제공
- 객체가 제공할 기능 중심 엔티티 구현 방법
@Entity
@Access(AccessType.FIELD)
public class Order {
@EmbeddedId
private OrderNo number;
@Column(name = "state")
@Enumerated(EnumType.STRING)
private OrderState state;
... // cancel(), changeShippingInfo() 등 도메인 기능 구현
... // 필요한 get메서드 제공
}
- JPA매핑 처리를 필드 방식으로 선택
- 불필요한 get/set 구현 X
AttributeConverter를 이용한 밸류 매핑처리
- int, long, String, LocalDate 같은 타입은 DB테이블 한 개 컬럼에 매핑
- 밸류타입의 프로퍼티를 한 개 컬럼에 매핑할 때가 있음
- @Embeddable 애너테이션으로 처리 X
- AttributeConverter
public interface AttributeConverter<X, Y>{
public Y convertToDatabaseColumn(X attribute);
public X convertToEntityAttribute(Y dbData);
}
@Converter(autoApply = true)
public class MoneyConverter implements AttributeConverter<Money, Integer> {
@Override
public Integer convertToDatabaseColumn(Money money) {
return money == null? null : money.getValue();
}
@Override
public Money convertToEntityAttribute(Integer value) {
return value == null? null : new Money(value);
}
}
- AttributeConverter인터페이스 구현 클래스는 @Converter 애너테이션 적용
- autoApply = true 일 때 모델에 출현하는 모든 Money타입 프로퍼티에 대해 MoneyConverter 자동 적용
@Entity
@Table(name = "purchase_order")
public class Order {
@Column(name = "total_amounts")
private Money totalAmounts; // MoneyConverter적용해서 값 변환
}
- autoApply속성 fal이면 프로퍼티 변환시 컨버터 직접 지정
public class Order {
@Column(name = "total_amounts")
@Convert(converter = MoneyConverter.class)
private Money totalAmounts;
}
밸류컬렉션 별도 테이블 매핑
- Order 엔티티는 한 개 이상의 OrderLine을 가질 수 있음
- ORDER_LINE테이블은 order_number외부키를 이용해 PURCHASE_ORDER테이블 참조
- 인덱스값을 저장하기 위한 컬럼도 존재(line_idx)
@Entity
@Table(name = "purchase_order")
public class Order {
@EmbeddedId
private OrderNo number;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "order_line", joinColumns = @JoinColumn(name = "order_number"))
@OrderColumn(name = "line_idx")
private List<OrderList> orderLines;
}
@Embeddable
public class OrderLine {
@Embedded
private ProductId productId;
@Column(name = "quantity")
private int quantity;
@Column(name = "amounts")
private Money amounts;
}
- JPA는 @OrderColumn을 통해 지정한 컬럼에 리스트의 인덱스 값을 저장
- @CollectionTable은 밸류를 저장할 테이블 지정 (name = 테이블이름, joinColumn = 외부키로 사용할 컬럼)
밸류컬렉션 : 한 개 컬럼 매핑
- 밸류컬렉션을 별도 테이블이 아닌 한 개 컬럼에 저장해야할 때
- 도메인 모델에는 이메일 주소 목록을 Set으로 보관, DB에는 한 개 컬럼에 콤마 구분할 경우
- AttributeConverter
public class EmailSet {
private Set<Email> emails = new HashSet<>();
public EmailSet(Set<Email> emails) {
this.emails.addAll(emails);
}
public Set<Email> getEmails() {
return Collections.unmodifiableSet(emails);
}
}
public class EmailSetConverter implements AttributeConverter<EmailSet, String> {
@Override
public String convertToDatabaseColumn(EmailSet attribute) {
if(attribute == null) return null;
return attribute.getEmails().stream().map(email -> email.getAddress())
.collect(Collectors.joining(","));
}
@Override
public EmailSet convertToEntityAttribute(String dbData) {
if(dbData == null) return null;
String[] emails = dbData.split(",");
Set<Email> emailSet = Arrays.stream(emails)
.map(value -> new Email(value)).collect(toSet());
return new EmailSet(emailSet);
}
}
@Column(name = "emails")
@Convert(converter = EmailSetConverter.class)
private EmailSet emailSet;
밸류를 이용한 ID매핑
- 식별자 자체를 밸류 타입으로 만들 수 있음
@Entity
@Table(name = "purchase_order")
public class Order {
@EmbeddedId
private OrderNo number;
}
@Embeddable
public class OrderNo implements Serializable {
@Column(name = "order_number")
private String number;
}
- JPA에서 식별자 타입은 Serializable타입이어야 함
- 밸류 타입으로 식별자 구현시 장점
- 식별자에 기능을 추가할 수 있음
- 1세대 주문번호, 2세대 주문 번호 구분시 첫 글자 이용할 경우 OrderNo클래스에서 구분 기능 구현 가능
@Embeddable
public class OrderNo implements Serializable {
@Column(name = "order_number")
private String number;
public boolean is2ndGeneration() {
return number.startsWith("N");
}
}
- JPA는 내부적으로 엔티티 비교시 equals(), hashCode값 사용
- 식별자로 사용할 밸류타입은 이 두 메서드를 알맞게 구현해야함
별도 테이블에 저장하는 밸류 매핑
- 애그리거트에서 루트 엔티티 뺀 부분은 대부분 밸류
- 또다른 엔티티가 있을 경우 애그리거트인지 체크 필요
- ex) Product와 Review는 서로 영향을 주지 않기 때문에 다른 애그리거트다.
- 애그리거트에 속한 객체가 밸류인지 엔티티인지 구별하는 방법
- 고유 식별자를 갖는지 확인 할 것
- 별도의 PK가 있다고 해서 고유식별자를 갖는 것은 아니다.
- ARTICLE_CONTENT 테이블의 ID컬럼은 식별자이기 때문에 엔티티로 생각할 수 있지만
Article의 내용을 담고 있으므로 밸류다. - ARTICLE_CONTENT의 ID는 ARTICLE테이블과 연결하기 위함, ARTICLE_CONTENT를 위한 별도 식별자가 필요한 것이 아님
- ArticleContent는 밸류이므로 @Embeddable로 매핑해야함
- ArticleContent와 매핑되는 테이블은 Article과 매핑되는 테이블과 다름
@Entity
@Table(name = "article")
@SecondaryTable(
name = "article_content",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "id")
)
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@AttributeOverride({
@AttributeOverride(
name="content",
column = @Column(table = "article_content", name = "content")),
@AttributeOverride(
name = "content",
column = @Column(table = "article_content", name = "content_type"))
})
@Embedded
private ArticleContent content;
}
- 밸류를 매핑한 테이블 지정을 위해 @SecondaryTable, @AttributeOverride사용
- @SecondaryTable의 name 속성은 밸류 테이블 , pkJoinColumns 속성 밸류 테이블에서 조인할 컬럼
- 위 코드를 통해 두 테이블 조인해서 데이터 조회
- 게시글 목록에서는 Article_content 데이터가 필요 없음
- 이 문제를 해소하기 위해 ArticleContent를 엔티티 매핑하고 지연 로딩 방식을 쓸 수 있음
- 좋은 방법은 아님(밸류를 엔티티로 만드는 것이기 때문에)
밸류컬렉션을 @Entity로 매핑
- 밸류이지만 구현기술 한계나 팀 표준때문에 @Entity를 사용해야할 때가 있음
- 이미지 업로드 방식에 따라 이미지 경로와 썸네일 이미지 제공여부가 달라지는 경우
- JPA는 @Embeddable타입의 클래스 상속 매핑 지원 X
- 상속 구조를 갖는 밸류 타입 사용하려면 @Entity사용이 필요
- @Entity매핑시 타입식별을 위해 식별 컬럼을 추가해야함
- 한 테이블에 Image와 하위 클래스를 매핑하므로 @Inheritance 애너테이션 적용
- strategy 값으로 SINGLE_TABLE사용
- @DiscriminatorColumn 을 이용해서 타입 구분용으로 사용할 컬럼 지정
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "image_type")
@Table(name = "image")
public abstract class Image {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "image_id")
private Long id;
@Column(name = "image_path")
private String path;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "upload_time")
private Date uploadTime;
protected Image() {}
public Image(String path) {
this.path = path;
this.uploadTime = new Date();
}
protected String getPath() {
return path;
}
public Date getUploadTime() {
return uploadTime;
}
public abstract String getURL();
public abstract boolean hasThumbnail();
public abstract String getThumbnailURL();
}
@Entity
@DiscriminatorValue("II")
public class InternalImage extends Image{
}
@Entity
@DiscriminatorValue("EI")
public class ExternalImage extends Image{
}
- Image를 상속받는 클래스는 @Entity와 @Discriminator를 사용해서 매핑 설정
@Entity
@Table(name = "product")
public class Product {
@EmbeddedId
private ProductId id;
private String name;
@Convert(converter = MoneyConverter.class)
private Money price;
private String detail;
@OneToMany(
cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, orphanRemoval = true)
@JoinColumn(name = "product_id")
@OrderColumn(name = "list_idx")
private List<Image> images = new ArrayList<>();
public void changeImages(List<Image> newImages) {
images.clear();
images.addAll(newImages);
}
}
- Image를 목록으로 담고 있는 Product는 @OneToMany를 통해 매핑
- Image는 완전히 Product에 의존하기 때문에 cascade속성으로 삭제시 함께 삭제되도록 설정
- clear()는 삭제 과정에 성능이 좋지 않음 (image_id를 하나씩 조회해서 삭제함)
- delete쿼리로 삭제가 가능한데 이걸 사용하려면 상속을 포기하고 @Embeddable로 매핑된 단일 클래스로 구현해야함
@Embeddable
public class Image {
@Column(name = "image_type")
private String imageType;
@Column(name = "image_path")
private String path;
@Temporal(TemporalType.TIMESTAMP)
@Column(name = "upload_time")
private Date uploadTime;
public boolean hasThumbnail() {
// 성능을 위해 다형 포기하고 if-else로 구현
if(imageType.equals("II")) {
return true;
} else {
return false;
}
}
}
- 코드 유지보수와 성능 두 가지 측면 고려해서 구현방식을 선택해야함
ID참조와 조인테이블 이용한 단방향M-N매핑
- 애그리거트 집합 연관은 성능상 이유로 피해야함
- 집합 연관이 유리할 경우 ID참조를 이용한 단방향 집합 연관 적용
@Entity
@Table(name = "product")
public class Product {
@EmbeddedId
private ProductId id;
@ElementCollection
@CollectionTable(name = "product_category",
joinColumns = @JoinColumn(name = "product_id"))
private Set<CategoryId> categoryIds;
- Product에서 Category로의 단방향 M-N 연관을 ID참조 방식으로 구현함
- 집합의 값에 밸류 대신 연관 맺는 식별자가 옴
- @ElementCollection을 이용하기 때문에 Product삭제시 매핑에 사용한 조인테이블 데이터도 삭제됨
애그리거트 로딩 전략
- JPA 매핑 설정시 애그리거트에 속한 객체가 모두 모여야 하나가 된다는 것이 중요
- 조회시점에 애그리거트가 완전한 상태가 되려면 조회방식을 즉시로딩(FetchType.EAGER)으로 설정
- 연관된 구성요소를 DB에서 함께 읽어옴
//@Entity컬렉션에 대한 즉시 로딩 설정
@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE},
orphanRemoval = true, fetch = FetchType.EAGER)
@JoinColumn(name = "product_id")
@OrderColumn(name = "list_idx")
private List<Image> images = new ArrayList<>();
//@Embeddable컬렉션에 대한 즉시 로딩 설정
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "order_line", joinColumns = @JoinColumn(name="order_number"))
@OrderColumn(name = "line_idx")
private List<OrderLine> orderLines;
- 로딩시점에 애그리거트에 속한 모든 객체를 함께 로딩
- 항상 좋은 것은 아님
@Entity
@Table(name = "product")
public class Product {
@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE},
orphanRemoval = true, fetch = FetchType.EAGER)
@JoinColumn(name = "product_id")
@OrderColumn(name = "list_idx")
private List<Image> images = new ArrayList<>();
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "product_option", joinColumns = @JoinColumn(name = "product_id"))
@OrderColumn(name = "line_idx")
private List<Option> options = new ArrayList<>();
- 위의 경우 Product 테이블 조회 시 Image, Option테이블을 조인한 쿼리를 실행함
- 카타시안 조인을 사용 (Product 이미지가 2개고 Option이 2개면 행 개수는 4개가 됨 -> 중복 )
- 애그리거트는 개념적으로 하나여야 하지만 로딩시점에 모두 로딩해야하는 것은 아님
- 실제로 상태 변경시점에 구성요소만 로딩해도 문제가 되지 않음
- FetchType.LAZY
- 일반적인 애플리케이션은 상태변경보다 조회기능 빈도가 훨씬 높음
애그리거트 영속성 전파
- 애그리거트 루트 조회 뿐 아니라 저장, 삭제시에도 하나로 처리해야함
- @Embeddable 매핑 타입은 함께 저장, 삭제 되므로 cascade속성 설정 필요X
- @Entity타입에 대한 매핑은 cascade 사용해서 처리하도록 설정해야함
- @OneToOne, @OneToMany는 cascade속성의 기본값이 없음
@OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE},
orphanRemoval = true)
@JoinColumn(name = "product_id")
@OrderColumn(name = "list_idx")
private List<Image> images = new ArrayList<>();
식별자 생성 기능
- 식별자 생성 방식
- 사용자가 직접 생성
- 도메인 로직으로 생성
- DB를 이용한 일련번호 사용
- 식별자 생성 규칙이 있으면 별도 서비스로 식별자 생성 기능 분리해야함
public class ProductIdService {
public ProductId nextId() {
// 정해진 규칙으로 식별자 생성
}
}
- 식별자 생성 기능은 도메인 영역에 위치해야함
public class CreateProductService {
@Autowired private ProductIdService idService;
@Autowired private ProductRepository productRepository;
@Transactional
public ProductId createProduct(ProductCreationCommand cmd) {
// 응용 서비스는 도메인 서비스를 이용해서 식별자 생성
ProductId id = productIdService.nextId();
Product product = new Product(id, cmd.getDetail(), cmd.getPrice(), ...);
productRepository.save(product);
return id;
}
}
- 응용서비스는 도메인 서비스를 이용해서 식별자 구하고 엔티티 생성
public class OrderIdService {
public OrderId createId(UserId userId) {
if(userId == null)
throw new IllegalArgumentException("invalid userId:" + userId);
return new OrderId(userId.toString() + "-" + timestamp());
}
private String timestamp() {
return Long.toString(System.currentTimeMillis());
}
}
- 특졍 값의 조합으로 식별자 생성하는 것 역시 도메인 서비스를 이용해서 생성할 수 있음
public interface ProductRepository {
...// save() ...
// 식별자 생성 메서드
ProductId nextId();
}
- 리포지터리에 구현할 수도 있다.
- @GeneratedValue 를 이용해서 DB자동 증가 컬럼을 식별자 매핑으로 사용할 수도 있음
- 자동증가 컬럼은 insert뒤에 식별자가 생성되므로 리포지터리에 저장할 때 식별자가 생성된다.
- 객체 생성시점에는 식별자를 알 수 없음(저장 후에 알 수 있음)
도메인 구현과 DIP
- DIP에 따르면 @Entity, @Table은 구현기술이기 때문에 Article과 같은 도메인 모델은 구현기술인 JPA에 의존하지 말아야하는데 의존하고 있음
- 리포지터리 인터페이스도 JPA Repository인터페이스를 상속하므로 도메인이 인프라에 의존함
- 구현 기술에 대한 의존 없이 도메인을 순수하게 유지하려면 Spring Date JPA Repository 인터페이스 상속받지 않고 ArticleRepository 인터페이스를 구현 클래스인프라에 위치시켜야함
- DIP 적용 이유는 저수준 구현이 변경되더라고 고수준 영향을 받지 않도록 하기 위함
- 리포지터리와 도메인 모델 구현기술은 거의 바뀌지 않음
- DIP를 완벽하게 지키면 좋지만 개발 편의성과 실용성을 가져가면서 구조적인 유연함은 어느정도 필요함
- 복잡도를 높이지 않으면서 기술에 따른 구현제약이 낮으면 합리적인 선택인듯
728x90
반응형
'책리뷰 > 도메인 주도 개발 시작하기(DDD핵심 개념 정리부터 구현까지)' 카테고리의 다른 글
도메인 주도 개발 (DDD) 시작하기 응용서비스와 표현영역 (0) | 2022.05.19 |
---|---|
도메인 주도 설계(DDD) 스프링데이터 JPA를 이용한 조회 (0) | 2022.05.17 |
도메인 주도 설계(DDD) 애그리거트 (0) | 2022.05.11 |
도메인 주도 설계 아키텍처 (0) | 2022.05.10 |
도메인(DDD)주도 개발 도메인이란 (0) | 2022.05.09 |