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

6) 개발 architecture (1) 레이어드 아키텍처(Layered Architecture)

by Bill Lab 2025. 8. 31.
반응형

1. 레이어드 아키텍처 개념

      : 레이어드 아키텍처는 전통적인 애플리케이션 구조로, 관심사의 분리

       (Separation of Concerns)에 기반해 계층을 나누는 패턴.
     

2. 계층(Layer) 구조   

 

     1) Presentation Layer (Controller, UI)

         - 사용자의 요청을 받아서 응용 계층(비지니스 계층)에 전달

         - DTO 변환, HTTP 응답 처리

 

     2) Business Layer (Service) 

         - 비즈니스 로직 처리

         - 유스케이스 실행

         - 트랜젝션 경계설정

         - repository 호출

   

     3) Infrastructure Layer (Repository(구현체), 외부 API 등 연결)

         - DB CRUD 처리

         - 외부 API 호출

         - 메시지 브로커, 캐시 등 외부 시스템 처리

  

 

com.example
 ├─ presentation
 │    ├─ UserController.kt
 │    └─ CouponController.kt
 │
 ├─ service
 │    ├─ UserService.kt
 │    └─ CouponService.kt
 │
 ├─ repository
 │    ├─ UserRepository.kt
 │    └─ CouponRepository.kt
 │
 └─ common
      └─ exception

 

3. 예제 코드

     : 레이어드 아키텍처 기반으로 “회원 가입 & 쿠폰 발급”로직을 가볍게 구현단하면?

 

     [요구사항]

     - 사용자가 회원 가입을 하면, 환영 쿠폰을 1장 발급한다.

     - 회원 가입 시 이메일 중복 검증이 필요하다.

     - 가입 완료 후 이벤트 로그를 남긴다.

 

1) Controller

package com.example.user.presentation

import com.example.user.application.UserService
import org.springframework.web.bind.annotation.*

data class UserRegisterRequest(val email: String, val name: String)
data class UserResponse(val id: Long?, val email: String, val name: String)

@RestController
@RequestMapping("/users")
class UserController(
    private val userService: UserService
) {
    @PostMapping("/register")
    fun register(@RequestBody request: UserRegisterRequest): UserResponse {
        val user = userService.registerUser(request.email, request.name)
        return UserResponse(user.id, user.email, user.name)
    }
}

 

2) Service

@Service
class UserService(
    private val userRepository: UserJpaRepository,
    private val couponRepository: CouponRepository
) {

    @Transactional
    fun registerUser(email: String, name: String): User {
        // 1. 이메일 중복 체크
        if (userRepository.existsByEmail(email)) {
            throw IllegalArgumentException("이미 존재하는 이메일입니다.")
        }

        // 2. 도메인 객체 생성 + 검증
        val user = User(email = email, name = name).apply {
            validateEmail()
        }

        // 3. 유저 저장
        val savedUser = userRepository
            .save(UserEntity.fromDomain(user))
            .toDomain()

        // 4. 환영 쿠폰 발급 (같은 서비스 내에서 repo 직접 사용)
        val coupon = Coupon.issueWelcomeCoupon(savedUser.id!!)
        couponRepository.save(CouponEntity.fromDomain(coupon))

        return savedUser
    }
}

 

3) Repository

@Repository
class UserRepository(
    private val userJpaRepository: UserJpaRepository
) : UserRepository {

    override fun existsByEmail(email: String): Boolean =
        userJpaRepository.existsByEmail(email)

    override fun save(user: User): User =
        userJpaRepository
            .save(UserEntity.fromDomain(user))
 }

 

4. 주요 단점

"정통적인 레이어드 사용법이며, 실무에 바로 사용하기에는 여러 가지 문제점을 안고 있는것이 보입니다."
"테스트할 때 단위 테스트와 통합 테스트 경계가 모호"
 - Service가 외부 자원(DB, API 등)에 직접 의존하는 구조
 - 단위테스트의 경우 외부자원없이 순수 로직만 검증해야하만, service가 DB에 직접 의존하기때문에,
   통합 테스트의 성격이 됨
 - Mocking 이 어려움

"유스케이스 흐름이 Layered 구조에 제한"
 - 여러 Layer를 거치면서 단일 유스케이스의 흐름이 분산됨
 - Service에서 여러 도메인 호출 → 트랜잭션 관리, 예외 처리, 외부 호출이 섞임 
   → Fat Service 문제 발생

"외부 시스템 교체가 어려움"
 - Infrastructure Layer(DB, Kafka, REST API 등)가 Business Layer와 강하게 결합될 수 있음
 - DB 교체, 메시징 시스템 변경 시 Service 코드 일부 수정 필요 → 유연성 낮음

"규모가 커지면 서비스가 항상 비대해짐"
 - 서비스 단위로 역할이 혼합되면 Service가 Fat Service가 됨
반응형