Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- HashMap
- switch
- 백트래킹
- 백준
- dynamic programming
- 해시
- BFS
- Algorithm
- 깊이우선탐색
- 구현
- 동적계획법
- 부분수열의합
- 프로그래머스
- Spring
- Backtracking
- DynamicProgramming
- 그리디
- 알고리즘
- 브루트포스
- programmers
- 해시맵
- boj
- 스프링
- 이분탐색
- DFS
- DP
- 너비우선탐색
- 네트워크
- greedy
- Network
Archives
- Today
- Total
옌의 로그
DDD와 Hexagonal Architecture, 상품관리 시스템에 적용해보기 본문
DDD(Domain-Driven Design) ?
DDD는 도메인(업무 규칙과 의미) 에 집중해서 시스템을 설계하자는 접근 방식이다.
단순히 “이런 기능이 필요하다”는 시각이 아니라, 비즈니스의 언어와 규칙을 코드에 녹이는 것이 핵심이다.
주요 개념 정리
개념설명예시 (상품 관리)
도메인(Domain) | 해결하고자 하는 비즈니스 영역 | 상품 등록, 메타데이터 수집, 발행 |
엔티티(Entity) | 고유 ID가 있는 객체 | Product, Copyright |
밸류 오브젝트(Value Object) | 고유 ID 없이 값으로 의미 | ProductName, Price, Period |
애그리거트(Aggregate) | 엔티티들의 일관성을 묶는 루트 | Product가 루트, 내부에 ProductMetadata 포함 |
도메인 서비스 | 엔티티에 넣기 애매한 복잡한 비즈니스 규칙 | 상품 발행 정책, 정산 방식 |
리포지토리(Repository) | 애그리거트를 저장/조회하는 인터페이스 | ProductRepository |
애플리케이션 서비스 | 유스케이스 조합 및 트랜잭션 관리 | 상품 등록, 상품 발행 등 |
더보기
애그리거트(Aggregate) ?
“하나의 트랜잭션 경계를 기준으로 뭉쳐서 관리되어야 하는 도메인 객체들의 집합”
예시 1: 상품(Product)과 상품 메타데이터(ProductMetadata)
- 상품은 이름, 가격, 메타데이터를 가진다.
- 메타데이터는 외부 API에서 가져온다.
- 상품 저장 시, 메타데이터까지 같이 저장되어야 한다.
이 경우, Product 가 루트(aggregate root)고, ProductMetadata는 내부 구성요소다.
여기서 “애그리거트”는 다음과 같다:
- Product + ProductMetadata는 하나의 논리적 단위로 관리되어야 한다.
- 외부에서는 ProductMetadata를 직접 수정하면 안 되고, 반드시 Product를 통해서만 접근해야 한다.
- 트랜잭션 경계도 Product 단위로 묶인다.
즉, 애그리거트 루트가 도메인 안의 일관성을 책임지는 중심 객체다.
규칙 3가지
1. 외부에서는 루트만 접근한다 | 외부에서 내부 구성요소에 직접 접근 금지 |
2. 루트가 일관성을 보장한다 | 내부 객체의 상태 변화는 루트를 통해서만 일어난다 |
3. 트랜잭션 경계는 애그리거트 단위로 | 하나의 애그리거트는 하나의 트랜잭션 안에서 처리 |
잘못된 애그리거트 설계
예를 들어, 아래처럼 Product와 Category 를 같은 애그리거트로 묶는다면?
- 상품 등록 시, 카테고리도 같이 저장하려고 함
- 카테고리는 공용으로 사용되며 여러 상품에서 공유됨
이건 잘못된 설계다.
왜?
- Category는 여러 Product에서 참조하는 별도 애그리거트로 관리되어야 한다.
- Product 저장할 때 Category까지 트랜잭션으로 묶으면 DB 충돌/락 문제 생김.
- 한 애그리거트는 되도록 작고 일관성 있는 단위로 유지하는 게 원칙이다.
예시: 상품 애그리거트 다시 정리
// Aggregate Root
public class Product {
private Long id;
private ProductName name;
private Money price;
private ProductMetadata metadata;
public void attachMetadata(ProductMetadata metadata) {
this.metadata = metadata;
}
public boolean isMetadataAttached() {
return metadata != null;
}
}
- 이때 ProductMetadata는 외부에서 직접 수정 못함
- ProductRepository는 오직 Product만 저장/조회
→ 이게 바로 “애그리거트 루트를 통해서만 상태 변화가 일어나도록 하는 것”이다.
📌 핵심 정리
질문답변애그리거트는 뭐야? | 관련 도메인 객체들을 하나의 트랜잭션으로 묶는 일관성 경계다 |
루트는 뭐야? | 외부에서 접근 가능한 유일한 진입점이다 |
왜 중요해? | 복잡한 도메인 구조를 안정적으로 유지하기 위해 |
실무에서 언제 쓰지? | 예를 들어 상품-메타데이터, 주문-주문내역, 사용자-주소 등 관계가 있는 객체들을 묶고 싶을 때 |
Hexagonal Architecture ?
Hexagonal Architecture (또는 포트-어댑터 아키텍처)는 애플리케이션 내부와 외부를 명확히 구분하는 설계다.
핵심은 도메인과 애플리케이션 로직은 외부 기술 (DB, API 등)에 전혀 의존하지 않게 설계하는 것이다.
✅ 왜 중요한가?
- 도메인 로직이 외부 기술에 종속되지 않아야 테스트가 쉽고 변경에 유연하다.
- 외부 시스템 교체(Firebase → SNS, RDB → NoSQL 등)에 내부 로직이 영향을 받지 않는다.
구성 요소 요약
도메인 모델 | 핵심 로직 | 상품, 발행 정책 |
애플리케이션 서비스 | 유스케이스 조합, 트랜잭션 | 상품 등록, 메타데이터 요청 |
포트 (Port) | 내부가 사용하는 인터페이스 | MetadataClient, ProductRepository |
어댑터 (Adapter) | 외부 구현체 | 외부 API 호출, JPA 리포지토리, REST 컨트롤러 |
예시: 상품 생성 + 외부 메타데이터 요청
상품을 생성하고, 외부 API를 통해 해당 상품의 메타데이터를 불러와 저장하는 흐름이다.
1) Domain Layer
Product.java - 애그리거트 루트
public class Product {
private final Long id;
private final ProductName name;
private final Money price;
private ProductMetadata metadata;
public Product(Long id, ProductName name, Money price) {
this.id = id;
this.name = name;
this.price = price;
}
public void attachMetadata(ProductMetadata metadata) {
this.metadata = metadata;
}
public boolean isMetadataAttached() {
return metadata != null;
}
}
Value Object 예시 - ProductName, Money
public class ProductName {
private final String value;
public ProductName(String value) {
if (value == null || value.isBlank()) throw new IllegalArgumentException("상품명 필수");
this.value = value;
}
public String getValue() {
return value;
}
}
2) Port 정의 (Interface)
ProductRepository.java
public interface ProductRepository {
Product save(Product product);
Optional<Product> findById(Long id);
}
MetadataClient.java
public interface MetadataClient {
ProductMetadata fetchMetadata(Product product);
}
3) Application Layer
RegisterProductUseCase.java
public interface RegisterProductUseCase {
Long register(String name, BigDecimal price);
}
RegisterProductService.java
@Transactional
public class RegisterProductService implements RegisterProductUseCase {
private final ProductRepository productRepository;
private final MetadataClient metadataClient;
public RegisterProductService(ProductRepository productRepository, MetadataClient metadataClient) {
this.productRepository = productRepository;
this.metadataClient = metadataClient;
}
@Override
public Long register(String name, BigDecimal price) {
Product product = new Product(null, new ProductName(name), new Money(price));
Product saved = productRepository.save(product);
ProductMetadata metadata = metadataClient.fetchMetadata(saved);
saved.attachMetadata(metadata);
return saved.getId();
}
}
4) Adapter Layer
JpaProductRepository.java
@Repository
public class JpaProductRepository implements ProductRepository {
private final SpringDataJpaRepository jpaRepository;
public JpaProductRepository(SpringDataJpaRepository jpaRepository) {
this.jpaRepository = jpaRepository;
}
@Override
public Product save(Product product) {
return jpaRepository.save(ProductEntity.from(product)).toDomain();
}
@Override
public Optional<Product> findById(Long id) {
return jpaRepository.findById(id).map(ProductEntity::toDomain);
}
}
ExternalMetadataClient.java
@Component
public class ExternalMetadataClient implements MetadataClient {
private final WebClient webClient;
public ExternalMetadataClient(WebClient.Builder builder) {
this.webClient = builder.baseUrl("https://api.metadata.com").build();
}
@Override
public ProductMetadata fetchMetadata(Product product) {
return webClient.get()
.uri("/products/{id}/metadata", product.getId())
.retrieve()
.bodyToMono(ProductMetadata.class)
.block();
}
}
ProductController.java
@RestController
@RequiredArgsConstructor
@RequestMapping("/products")
public class ProductController {
private final RegisterProductUseCase registerProductUseCase;
@PostMapping
public ResponseEntity<Void> register(@RequestBody ProductRequest request) {
registerProductUseCase.register(request.name(), request.price());
return ResponseEntity.ok().build();
}
}
이런 식으로 나누면 뭐가 좋은데?
- 도메인(상품)의 책임과 상태 변화가 깔끔하게 관리된다.
- 외부 API 호출 방식이 바뀌어도 MetadataClient만 바꾸면 된다.
- 테스트 시에도 실제 API 호출 없이 도메인 테스트가 가능하다.
- 컨트롤러와 DB는 단순한 어댑터일 뿐, 핵심 로직은 모두 애플리케이션 & 도메인 계층에 집중된다.
마무리하며
MSA 전환을 위해 DDD, hexagonal architecture에 대해 공부해보았다.
이전 프로젝트였던 상품관리 시스템이 도메인 중심 설계를 어느정도 적용해놨던 터라 크게 어색하게 느껴지지 않았는데,
헥사고날 아키텍처는 많이 낯설다.. ㅋㅋㅋ
'스터디 > 기타' 카테고리의 다른 글
[Reactive Programming] 리액티브 프로그래밍 & 리액티브 스트림즈 (1) | 2025.08.29 |
---|---|
ENUM과 DB 매핑 전략 (feat. Enum Converter,, 코드 테이블,,) (1) | 2025.08.28 |
(충격) 지금까지 배치 서버를 써본 줄 알았는데… 아니었음 (3) | 2025.08.26 |
[Linux] linux환경에서 javac로 java 컴파일하기 (0) | 2024.04.20 |
[Linux] linux환경에서 gcc로 c++ 컴파일하기 (0) | 2023.06.22 |
Comments