신입으로 입사한 뒤, 프로젝트 구조를 처음 봤을 때 가장 당황스러웠던 건 디렉토리였다.
내가 개인 프로젝트나 학원에서 쓸 때는 대부분 아래처럼 단순한 구조였다.
- controller
- service
- repository
- dto
- entity
그런데 회사 프로젝트는 이렇게 구성돼 있었다.
- controller
- application
- domain
- infra
- support
처음엔 그냥 “이름만 다른건가?”라고 생각했는데, 하나하나 코드를 열어보면서 알게 됐다.
디렉토리 구조가 다른 게 아니라, 애초에 설계의 출발점이 달랐다는 것.
| 기준 | Spring MVC 구조 | DDD 구조 |
|---|---|---|
| 요청 흐름 | Controller → Service → Repository | Presentation → Application → Domain → Infra |
| 설계 중심 | 데이터(DB) 중심 | 도메인(업무 의미) 중심 |
| 트랜잭션 위치 | Service 내부 | Application 계층 |
| 역할 분리 | 불분명 (Service에 몰림) | 명확 (계층별 책임 분리) |
| 테스트 용이성 | 단위 테스트 어려움 | 도메인 중심 테스트 쉬움 |
@PostMapping("/orders")
public void placeOrder(@RequestBody OrderRequest req) {
orderService.placeOrder(req);
}
@Service
public class OrderService {
public void placeOrder(OrderRequest req) {
if (req.totalPrice() < 10000) throw new IllegalArgumentException();
orderRepository.save(req.toEntity());
}
}
작을 땐 괜찮지만, 프로젝트가 커지면 어느 순간
Service가 모든 책임을 지고 있는 구조가 된다.
// controller
@PostMapping("/orders")
public void placeOrder(@RequestBody OrderRequestDto dto) {
orderUseCase.placeOrder(dto);
}
// application
@Transactional
public void placeOrder(OrderRequestDto dto) {
Order order = dto.toDomain();
orderService.validateMinimumPrice(order);
orderRepository.save(order);
}
// domain
public class Order {
public void complete() {
if (!status.isPending()) throw new IllegalStateException();
this.status = OrderStatus.COMPLETED;
}
}
DDD 구조를 처음 마주했을 때 낯설었던 건,
단순히 디렉토리 이름 때문이 아니라
아예 다른 아키텍처 설계 원칙이 적용돼 있었기 때문이었다.
소프트웨어 구조를 어떻게 나눌지, 각 계층에 어떤 책임을 줄지를 결정하는 설계 방식
MVC, DDD, Hexagonal 같은 이름들은
모두 이런 “구조적 분리 기준”을 다르게 설정한 아키텍처 패턴이다.
DDD 구조에서는
→ 즉, 기술보다 앞서는 건 구조 설계의 기준이다.
이 구조는 특정 기술에 종속되지 않는다.
JPA를 쓰든, MyBatis를 쓰든, 심지어 Mongo를 쓰든
도메인 계층은 오직 “비즈니스 의미”만 다루고,
“데이터 저장 방식”은 infra로 분리돼야 한다.
// domain
public interface OrderRepository {
void save(Order order);
}
// infra
@RequiredArgsConstructor
public class OrderRepositoryImpl implements OrderRepository {
private final JpaOrderRepository jpaRepo;
public void save(Order order) {
jpaRepo.save(order);
}
}
이렇게 구성하면 도메인은 저장 방식, 프레임워크, 외부 API와 전혀 무관해진다.
결국 이게 구조 설계의 목적이다.
처음엔 “이 구조 왜 이렇게 복잡하지?”라고만 느꼈는데,
지금은 “책임과 의존을 어떻게 나눴는가”를 먼저 보게 된다.
회사 프로젝트 구조는 다음과 같았다.
├── application → 유즈케이스 단위 처리
├── controller → HTTP 요청 수신 및 응답 DTO 처리
├── domain → Entity, VO, DomainService, Repository 인터페이스
├── infra → JPA, MyBatis, 외부 API 등 기술 구현
├── support → 공통 예외, 유틸리티, 설정 등
| 디렉토리 | 주요 책임 |
|---|---|
controller | 외부 요청 처리, DTO ↔ 도메인 변환 |
application | 유즈케이스 실행, 트랜잭션 관리 |
domain | 비즈니스 핵심 로직 (상태, 규칙, 행위) |
infra | 기술 의존 구현 (JPA, MyBatis, 외부 API 등) |
support | 공통 유틸, 예외, 로깅, 설정 |
Support는 도메인이나 인프라와 무관하게
전역적으로 사용하는 공통 기능을 넣는 곳이다.
예시:
✔️ 디렉토리
support
├── exception
│ ├── BusinessException.java
│ ├── GlobalExceptionHandler.java
├── util
│ └── EncryptionUtil.java
✔️ 예외 처리
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BusinessException.class)
public ResponseEntity<?> handleBusinessException(BusinessException e) {
return ResponseEntity.badRequest().body(new ErrorResponse(e.getMessage()));
}
}
domain에 넣지 않는다support에 모아서 관리
DDD 구조를 처음 접했을 땐 낯설기만 했다.
하지만 하나씩 코드를 읽고 구조를 따라가면서 알게 됐다.
핵심은 어떤 기술을 쓰느냐가 아니라, 어떤 방향으로 구조를 설계하느냐였다.
도메인을 기술로부터 분리해놓는 그 설계 방식 자체가
내가 이해한 DDD의 시작점이었다.