1. 레이어드 아키텍처 개념
: 레이어드 아키텍처는 전통적인 애플리케이션 구조로, 관심사의 분리
(Separation of Concerns)에 기반해 계층을 나누는 패턴.
2. 계층(Layer) 구조
1) Presentation Layer (Controller, UI)
- 사용자의 요청을 받아서 응용 계층(비지니스 계층)에 전달
- DTO 변환, HTTP 응답 처리
2) Application Layer (Application Service) - (선택)
- 비즈니스 흐름(Use Case) 조합
- 도메인 로직을 직접 가지지 않고 도메인 객체를 조립 및 조율
3) Domain Layer (Entity, Domain Service, repositorty 추상체)
- 순수한 비즈니스 로직(domain service)
- 외부 프레임워크 의존
- 핵심 규칙과 불변식을 책임짐
4) Infrastructure Layer (Repository(구현체), 외부 API 등 연결)
- 기술적 세부사항 (DB, Kafka, Redis 등)
- 인터페이스 기반으로 응용/도메인 계층에서 사용
com.example
├─ user
│ ├─ domain
│ │ └─ User.kt
│ ├─ infrastructure
│ │ └─ UserJpaRepository.kt
│ ├─ application
│ │ └─ UserService.kt
│ └─ presentation
│ └─ UserController.kt
│
├─ coupon
│ ├─ domain
│ │ └─ Coupon.kt
│ └─ application
│ └─ CouponService.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) Application Service
package com.example.user.application
import com.example.user.domain.User
import com.example.user.infrastructure.UserJpaRepository
import com.example.coupon.domain.Coupon
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
//user service
@Service
class UserService(
private val userRepository: UserJpaRepository,
private val couponService: CouponService
) {
@Transactional
fun registerUser(email: String, name: String): User {
userRepository.findByEmail(email)?.let {
throw IllegalArgumentException("이미 존재하는 이메일입니다.")
}
val user = User(email = email, name = name).apply { validateEmail() }
val savedUser = userRepository.save(UserEntity.fromDomain(user)).toDomain()
couponService.issueWelcomeCoupon(savedUser.id!!)
return savedUser
}
}
//coupon service
@Service
class CouponService {
fun issueWelcomeCoupon(userId: Long): Coupon {
CouponRepository.save()
return Coupon(userId = userId, code = "WELCOME", discountRate = 10)
}
}
3) Domain
//user domain
package com.example.user.domain
data class User(
val id: Long? = null,
val email: String,
val name: String
) {
fun validateEmail() {
require(email.contains("@")) { "유효하지 않은 이메일 형식입니다." }
}
}
//coupon domain
package com.example.coupon.domain
data class Coupon(
val id: Long? = null,
val userId: Long,
val code: String,
val discountRate: Int
)
4) Infrastructure
package com.example.user.infrastructure
import com.example.user.domain.User
import org.springframework.data.jpa.repository.JpaRepository
interface UserJpaRepository : JpaRepository<UserEntity, Long> {
fun findByEmail(email: String): UserEntity?
}
@Entity
@Table(name = "users")
data class UserEntity(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null,
val email: String,
val name: String
) {
fun toDomain() = User(id, email, name)
companion object {
fun fromDomain(user: User) = UserEntity(user.id, user.email, user.name)
}
}
4. 주요 단점
"정통적인 레이어드 사용법이며, 실무에 바로 사용하기에는 여러 가지 문제점을 안고 있는것이 보입니다."
"도메인 순수성 저하"
- Application Layer에서 유스케이스 흐름 + 외부 의존(DB, API, 메시지 등)을 모두 다룸
- Domain Service에 외부 의존이 들어가거나 Application Service가 비대해질 수 있음
- 테스트할 때 단위 테스트와 통합 테스트 경계가 모호
"유스케이스 흐름이 Layered 구조에 제한"
- 여러 Layer를 거치면서 단일 유스케이스의 흐름이 분산됨
- Application Service에서 여러 도메인 호출 → 트랜잭션 관리, 예외 처리, 외부 호출이 섞임 → Fat Service 문제 발생
"외부 시스템 교체가 어려움"
- Infrastructure Layer(DB, Kafka, REST API 등)가 Application Layer/Domain Layer와 강하게 결합될 수 있음
- DB 교체, 메시징 시스템 변경 시 Domain/Service 코드 일부 수정 필요 → 유연성 낮음
"DI와 의존성 관리 문제"
- Layered 구조에서 Application Service → Application Service 호출 시 순환 참조 발생 가능
- Domain → Application 호출을 막지 않으면 의존성 뒤엉킴
- 순수 규칙과 유스케이스 흐름이 섞이면 DI 구조가 복잡해짐
"규모가 커지면 서비스가 항상 비대해짐"
- 서비스 단위로 역할이 혼합되면 Application Service가 Fat Service가 됨
- Domain Service는 역할이 제한적이지만, Application Service에 유스케이스 흐름이 모두 몰리면 유지보수 어려움
'Kotlin Spring > Kotlin Spring 강의 내용' 카테고리의 다른 글
6) 개발 architecture (3) 클린 아키텍처(Clean Architecture) (2) | 2025.08.31 |
---|---|
6) 개발 architecture (2) 헥사고날 아키텍처(Hexagonal Architecture) (0) | 2025.08.31 |
5) 요구사항 분석 및 ERD 설계 (0) | 2025.08.31 |
4) Spring @controller, @service, @repository (0) | 2025.08.29 |
3) Spring 컨테이너, Bean (0) | 2025.08.26 |