본문 바로가기
Kotlin Spring/Kotlin Spring 강의 내용

6) 개발 architecture (5) 강의용 Architecture

by Bill Lab 2025. 9. 1.
반응형

1. 강의에서 사용하는 아키텍처는?

     : 레이어드 아키텍처와 DDD, EDA, 클린아키텍처(일부)를 종합하여 설계

2. 새로운 Architecture 를 적용한 배경

       - 로직의 중심인 Domain 을 보호하고, 외부 시스템도 쉽게 변경할 수 있는 구조로 설계

       - Facade 패턴을 이용, 프로젝트 초기 usecase 로 역할 수행하며, 도메인 레이어의 순환참조를 방지

       - EDA 를 적절히 활용하여 Fat service 화를 방지 (도메인 별 트랜젝션 관리필요)

프로젝트 초기에는 facade 로 usecase 의 흐름을 관리하다 커지면 EDA 기반으로 흐름을 관리하여 도메인의 느슨한 결합 추구

 

3. 패키지 아키텍처 구조 및 Layer 설명

presentation
 ├── api
 │    ├── controller           // REST/gRPC 등 외부 요청 진입점
 │    └── dto                  // Controller용 DTO (Request/Response)
 └── event
 │    └── consumer             // Kafka, RabbitMQ 등 이벤트 수신 처리
 └── scheduler     
      └── XXXscheduler         // scheduler 이벤트 발생
      
Application                    //복잡한 도메인의 흐름제어가 필요한 경우 사용
 ├── facade                    //service flow 제어(단순 CRUD에선 생략)
      

domain
 ├── service                  // 도메인 서비스 (비즈니스 규칙)
 ├── repository interface     // interface
 ├── entity                   // 도메인 모델 / Entity
 └── domain dto               // 도메인용 DTO

infrastructure
 ├── repository
 │    ├── jpaRepository                  // JPA Repository 인터페이스
 │    └── repository implementation      // Repository 구현체
 │    └── jpa entity                     // jpa entity
 ├── api client                          // 외부 API 호출 Adapter (feign 등)
 └── producer  
      └── event                          // 이벤트 발행 구현체

 

Layer  
Presentation - 외부 요청을 받음. Controller에서 DTO 변환 후 service 호출
- 이벤트 Consumer 처리
- 스케줄러 이벤트 발생
Application - 도메인 순환참조를 방지하기 위해 facade 레이어를 위치하는 곳
- 프로젝트 초기에 usecase의 흐름을 관리
Domain - 핵심 비즈니스 로직
- 도메인 서비스/Entity/도메인 DTO
- Repository Interface를 통해 외부 접근
Infrastructure - 외부 기술 구현체
- DB CRUD
- API, 메시징을 Adapter 패턴으로 구현
- 도메인 인터페이스를 구현.
1. Domain은 Infrastructure를 몰라야 함

2. Controller/Consumer/scheduler는 Application Layer 역할 없이 Presentation → Domain 호출 가능

3. 언제든 소스는 변경될 수 있음
   : 하지만 변경되더라도 최소 코드만 변경하며, 핵심 비지니스 로직이 들어있는 도메인 서비스 단을 최대한 보호

 

4. 예제소스

 

    1) Controller 

@RestController
@RequestMapping("/orders")
class OrderController(private val orderService: OrderService) {

    @PostMapping
    fun createOrder(@RequestBody request: OrderRequest): OrderResponse {
        val order = orderService.createOrder(
            userId = request.userId,
            items = request.items,
            totalAmount = request.totalAmount
        )
        return OrderResponse(orderId = order.id!!, userId = order.userId, totalAmount = order.totalAmount)
    }

    @GetMapping("/{id}")
    fun getOrder(@PathVariable id: Long): OrderResponse? {
        val order = orderService.getOrder(id) ?: return null
        return OrderResponse(orderId = order.id!!, userId = order.userId, totalAmount = order.totalAmount)
    }
}

 

    2) Domain Service 

@Service
class OrderService(private val orderRepository: OrderRepository) {

    fun createOrder(userId: Long, items: List<String>, totalAmount: Int): Order {
        require(totalAmount > 0) { "총 금액은 0 이상이어야 합니다." }
        val order = Order(userId = userId, items = items, totalAmount = totalAmount)
        return orderRepository.save(order)
    }

    fun getOrder(id: Long): Order? = orderRepository.findById(id)
}

 

    3) Domain Repository  

interface OrderRepository {
    fun save(order: Order): Order
    fun findById(id: Long): Order?
}

 

    4) Domain Entity

data class User(
    val id: Long? = null,
    val email: String,
    val name: String,
    val isActive: Boolean = true
) {
    fun deactivate() = this.copy(isActive = false)
    fun activate() = this.copy(isActive = true)
}

 

    5) Repository Implementation

@Repository
class OrderRepositoryImpl(private val jpa: SpringDataOrderJpa) : OrderRepository {

    override fun save(order: Order): Order {
        val entity = OrderEntity.fromDomain(order)
        return jpa.save(entity).toDomain()
    }

    override fun findById(id: Long): Order? =
        jpa.findById(id).orElse(null)?.toDomain()
}

 

    6) Consumer

@Component
class OrderEventConsumer {

    fun handleOrderCreatedEvent(event: String) {
        //서비스 레이어 호출
    }
}

 

    7) Producer

@Component
class OrderEventProducer {
    fun publishOrderCreated(order: Order) {       
    }
}

 

    8) Scheduler

@Component
class OrderScheduler() {
    @Scheduled(fixedRate = 60000) 
    fun generateOrderEvent() {
        //비지니스 로직이 담긴 서비스 메쏘드 호출
    }
}

 

4. 장점 요약

"도메인 중심 설계"
 - Domain Service가 핵심 로직만 담당, Infrastructure 몰라서 독립성 확보
 
 "Presentation에서 Domain 직접 호출도 가능하도 application 에서 조율도 가능"
 - 초기 빠른 개발에 유리

"서비스 확장 시 서비스 결합도 감소 가능"
 - 점진적 확장 가능(Facade → EDA로 자연스러운 진화)
 - Event 기반 통신으로 Publisher/Consumer 분리

"유연한 확장성"
 - 새로운 이벤트 리스너나 Adapter 추가 용이

"테스트 용이성"
 : Domain Service 단위 테스트 가능, 외부 의존은 Mock 처리
 
"읽기/유지보수 편리"
 - 각 레이어가 명확하게 역할 구분
반응형