Spring Boot with Clean Architecture Example

TMT

클린 아키텍처(Clean Architecture)는 도메인 중심성의존성 역전을 핵심으로 하는 설계 철학으로,
로버트 C. 마틴이 제시한 계층 구조를 통해 시스템을 유지보수성, 테스트 용이성, 프레임워크 독립성을 갖추도록 구성합니다.
DDD와 결합하여 구현할 때 가장 강력한 효과를 발휘하며, 다음 구조를 추천합니다.


🏗 클린 아키텍처 계층 구조 (Layers)

src/main/java/com/example/
├── **domain**              // 엔터프라이즈 비즈니스 규칙 (순수한 도메인)
│   ├── entity             // 핵심 비즈니스 객체 (JPA 애노테이션 ❌)
│   ├── valueobject        // 불변 값 객체
│   ├── service            // 도메인 서비스 (무상태 로직)
│   ├── repository         // 인터페이스 (구현체 X)
│   └── event              // 도메인 이벤트

├── **application**         // 애플리케이션 비즈니스 규칙 (Use Case)
│   ├── usecase            // 사용자 시나리오 구현 (트랜잭션 경계)
│   ├── dto                // Use Case 입출력 모델
│   └── port               // 인터페이스 (Input/Output Port)

├── **adapter**             // 외부 시스템과의 어댑터
│   ├── input              // Delivery Mechanism (Web, CLI, Event)
│   │   └── web           // REST Controller, GraphQL Resolver
│   ├── output             // 영속성, 외부 API 연동 구현체
│   │   ├── persistence   // JPA/Hibernate Entity, RepositoryImpl
│   │   └── external      // Feign Client, SDK Wrapper
│   └── event              // 메시지 브로커 발행/구독

└── **infrastructure**      // 프레임워크/라이브러리 설정
    ├── config             // Spring Boot, Database 설정
    └── exception         // 전역 예외 핸들러

1️⃣ Domain Layer (도메인 계층)

  • 핵심 규칙: 비즈니스 정책을 표현하는 순수한 자바/Kotlin 객체
  • 특징:
    // Entity (ID로 식별되는 객체)
    public class Order {
        private OrderId id;
        private List<OrderItem> items;
        
        public void addItem(Product product, int quantity) {
            items.add(new OrderItem(product, quantity));
        }
    }
    
    // Value Object (불변성)
    public record Address(String street, String city) {}
    
    // Domain Service (도메인 규칙 집약)
    public class PaymentValidator {
        public void validate(Order order, Payment payment) {
            if (order.totalPrice() != payment.amount()) 
                throw new PaymentMismatchException();
        }
    }
  • 원칙:
    • 프레임워크/DB 의존성 금지
    • @Entity, @Table 같은 JPA 애노테이션 사용 ❌ → Infrastructure 계층의 Entity와 분리

2️⃣ Application Layer (애플리케이션 계층)

  • Use Case 구현: 사용자 시나리오를 기술적 구현 없이 표현
  • 포트(인터페이스) 정의: 외부 시스템과의 통신 규약
    // Use Case (트랜잭션 관리)
    @Service
    @RequiredArgsConstructor
    public class PlaceOrderUseCase {
        private final OrderRepository orderRepo;  // 인터페이스 의존
        private final PaymentGateway paymentGateway;
    
        @Transactional
        public void execute(PlaceOrderCommand command) {
            Order order = createOrder(command);
            paymentGateway.process(order.totalAmount());
            orderRepo.save(order);
        }
    }
    
    // Port (외부 시스템 연동 인터페이스)
    public interface PaymentGateway {
        void process(Money amount);
    }

3️⃣ Adapter Layer (어댑터 계층)

  • 입력 어댑터: 외부 요청을 Use Case 형식으로 변환

    @RestController
    @RequiredArgsConstructor
    public class OrderController {
        private final PlaceOrderUseCase useCase;
    
        @PostMapping("/orders")
        ResponseEntity<Void> placeOrder(@RequestBody OrderRequest request) {
            useCase.execute(request.toCommand());
            return ResponseEntity.ok().build();
        }
    }
  • 출력 어댑터: Port 인터페이스의 구현체 제공

    @Component
    @RequiredArgsConstructor
    public class JpaOrderRepository implements OrderRepository {
        private final OrderJpaRepository jpaRepository;
    
        @Override
        public Order findById(OrderId id) {
            return jpaRepository.findById(id.value())
                .map(this::toDomain)
                .orElseThrow(OrderNotFoundException::new);
        }
    }

4️⃣ Infrastructure Layer (인프라 계층)

  • 프레임워크 설정: Spring Boot, DB 연결, 보안
    @Configuration
    @EnableJpaRepositories(basePackages = "com.example.adapter.output.persistence")
    public class JpaConfig { ... }

🔄 의존성 방향 규칙

Request → Controller → UseCase → Domain  
           ↑                ↓  
           └── Presenter ←──┘  
               (Output 변환)
  • 원칙:
    • 내부 → 외부 의존: Domain ← Application ← Adapter ← Infrastructure
    • DIP: 저수준 모듈(Adapter)이 고수준 모듈(Application/Domain)의 인터페이스 구현

🧪 테스트 전략

계층테스트 유형Mock 대상
Domain단위 테스트없음 (순수 로직)
Use Case통합 테스트Adapter 구현체
Adapter슬라이스 테스트실제 DB/API 호출

클린 아키텍처의 이점

  1. 프레임워크 독립성: Spring → Quarkus 전환 시 Domain/Application 계층 변경 없음
  2. 비즈니스 규칙 격리: 도메인 로직이 UI/DB 변경에 영향받지 않음
  3. 테스트 용이성: Domain 계층은 Mock 없이 테스트 가능

💡 적용 팁

  • Entity 분리:

    • Domain Entity: 비즈니스 규칙 포함
    • Persistence Entity (JPA): DB 매핑 전용
    // Domain Entity
    public class User { 
        private UserId id;
        private Email email;
    }
    
    // JPA Entity (Adapter 계층)
    @Entity
    class UserJpaEntity { 
        @Id private String id;
        private String email;
    }
  • 계층 간 변환:

    // Domain → JPA 변환
    public class UserJpaMapper {
        public static UserJpaEntity toEntity(User domain) {
            return new UserJpaEntity(domain.getId().value(), domain.getEmail().value());
        }
    }
Edit this page