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

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

by Bill Lab 2025. 9. 1.
728x90

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

     : 도메인 별로 분리를 고려하여, 레이어드 아키텍처와 EDA, 클린아키텍처(일부)를 종합하여 설계

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

       1) 도메인 구분없이 개발할 경우 거대한 강결합이 발생할 수 있는 구조로 개발된다.

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 구현체
 ├── api client                          // 외부 API 호출 Adapter
 └── producer  
      └── event                          // 이벤트 발행 구현체

 

Layer  
Presentation - 외부 요청을 받음. Controller에서 DTO 변환 후 service 호출
- 이벤트 Consumer 처리
- 스케줄러 이벤트 발생
Domain - 핵심 비즈니스 로직.
- 도메인 서비스/Entity/도메인 DTO
- Repository Interface를 통해 외부 접근
Infrastructure - 외부 기술 구현체
- DB CRUD
- API, 메시징을 Adapter 패턴으로 구현
- 도메인 인터페이스를 구현.
"Domain은 Infrastructure를 몰라야 함"
"Controller/Consumer/scheduler는 Application Layer 역할 없이 Presentation → Domain 호출 가능"

 

4. 예제소스

 

    1) Controller with dto

// presentation/api/dto/OrderRequest.kt
package com.example.presentation.api.dto

data class OrderRequest(
    val userId: Long,
    val items: List<String>,
    val totalAmount: Int
)

// presentation/api/dto/OrderResponse.kt
package com.example.presentation.api.dto

data class OrderResponse(
    val orderId: Long,
    val userId: Long,
    val totalAmount: Int
)

// presentation/api/controller/OrderController.kt
package com.example.presentation.api.controller

import com.example.domain.service.OrderService
import com.example.presentation.api.dto.OrderRequest
import com.example.presentation.api.dto.OrderResponse
import org.springframework.web.bind.annotation.*

@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 with dto, repository interface

package com.example.domain.dto

data class OrderDto(
    val orderId: Long?,
    val userId: Long,
    val totalAmount: Int
)


package com.example.domain.repository

import com.example.domain.entity.Order

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

// domain/service/OrderService.kt
package com.example.domain.service

import com.example.domain.entity.Order
import com.example.domain.repository.OrderRepository
import org.springframework.stereotype.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 Entity

// infrastructure/repository/entity/OrderEntity.kt
package com.example.infrastructure.repository.entity

import com.example.domain.entity.Order
import jakarta.persistence.*

@Entity
@Table(name = "orders")
data class OrderEntity(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    val userId: Long,
    @ElementCollection
    val items: List<String>,
    val totalAmount: Int
) {
    fun toDomain(): Order = Order(id = id, userId = userId, items = items, totalAmount = totalAmount)
    companion object {
        fun fromDomain(order: Order): OrderEntity = OrderEntity(id = order.id, userId = order.userId, items = order.items, totalAmount = order.totalAmount)
    }
}

 

    4) Repository Implementation

// infrastructure/repository/jpa/SpringDataOrderJpa.kt
package com.example.infrastructure.repository.jpa

import com.example.infrastructure.repository.entity.OrderEntity
import org.springframework.data.jpa.repository.JpaRepository

interface SpringDataOrderJpa : JpaRepository<OrderEntity, Long>

// infrastructure/repository/implementation/OrderRepositoryImpl.kt
package com.example.infrastructure.repository.implementation

import com.example.domain.entity.Order
import com.example.domain.repository.OrderRepository
import com.example.infrastructure.repository.jpa.SpringDataOrderJpa
import com.example.infrastructure.repository.entity.OrderEntity
import org.springframework.stereotype.Repository

@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()
}

 

    5) Consumer

// presentation/event/consumer/OrderEventConsumer.kt
package com.example.presentation.event.consumer

import org.springframework.stereotype.Component

@Component
class OrderEventConsumer {

    fun handleOrderCreatedEvent(event: String) {
        println("Received Event: $event")
        // 이벤트 처리 로직
    }
}

 

    6) Producer

// infrastructure/producer/event/OrderEventProducer.kt
package com.example.infrastructure.producer.event

import com.example.domain.entity.Order
import org.springframework.stereotype.Component

@Component
class OrderEventProducer {

    fun publishOrderCreated(order: Order) {
        println("Publishing Order Created Event: $order")
        // Kafka, RabbitMQ 등 실제 전송 로직 구현
    }
}

 

    7) Scheduler

// presentation/scheduler/OrderScheduler.kt
package com.example.presentation.scheduler

import com.example.infrastructure.producer.event.OrderEventProducer
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component

@Component
class OrderScheduler(private val orderEventProducer: OrderEventProducer) {

    @Scheduled(fixedRate = 60000) // 1분마다 실행
    fun generateOrderEvent() {
        //비지니스 로직이 담긴 서비스 메쏘드 호출
    }
}

 

4. 장단점 요약!

 

728x90