과제를 진행하다 레이어드 아키텍처를 적용하던 중, "Repository 인터페이스는 Domain에, 구현체는 Infrastructure에" 라는 요구사항을 보고, 보통 상위 계층이 하위 계층을 호출하는 거 아닌가?
라는 생각이 들었다.
이 의문을 해결하는 핵심이 의존성 역전 원칙이었다. SOLID 원칙 중 마지막 "D"에 해당하는 이 원칙은, 제대로 이해하면 아키텍처 설계의 핵심을 꿰뚫을 수 있다.
@Service
class OrderService(
private val mySqlOrderRepository: MySqlOrderRepository // 구체 클래스에 의존 💥
) {
fun createOrder(order: Order): Order {
return mySqlOrderRepository.save(order)
}
}
@Repository
class MySqlOrderRepository {
fun save(order: Order): Order {
// MySQL에 저장하는 구체적인 로직
val sql = "INSERT INTO orders ..."
// ...
return order
}
}
문제점
1. MySQL에서 MongoDB로 변경하려면? -> OrderService 코드 수정이 필요 💥
2. 테스트하려면 -> 실제 MySQL이 필요 💥
3. Service가 저수준 모듈(DB)에 의존 -> 비즈니스 로직이 기술에 종속 💥
Robert C. Martin이 정의한 원칙!
A. 고수준 모듈은 저수준 모듈에 의존해서는 안된다. 둘 다 추상화에 의존
B. 주상화는 세부사항에 의존해서는 안되고, 세부사항이 추상화에 의존해야 한다.
| 용어 | 의미 | 예시 |
|---|---|---|
| 고수준 모듈 | 비즈니스 규칙, 정책 | OrderService, Domain |
| 저수준 모듈 | 기술적 세부사항, 구현 | MySqlRepository, EmailService |
| 추상화 | 인터페이스, 추상 클래스 | OrderRepository 인터페이스 |
| 세부사항 | 구체적인 구현 | MySqlOrderRepository 구현체 |
┌─────────────────┐
│ OrderService │ (고수준)
└────────┬────────┘
↓ 의존 (직접 참조)
┌─────────────────┐
│ MySqlRepository │ (저수준)
└─────────────────┘
문제: 고수준 모듈이 저수준 모듈에 의존 → 변경에 취약
┌─────────────────┐
│ OrderService │ (고수준)
└────────┬────────┘
↓ 의존 (인터페이스)
┌─────────────────────┐
│ OrderRepository │ (추상화 - Interface)
└─────────┬───────────┘
↑ 구현 (implements)
┌─────────────────────┐
│ MySqlRepository │ (저수준)
└─────────────────────┘
해결: 고수준과 저수준 모두 추상화에 의존 -> 유연함!
// Service가 구체 클래스에 직접 의존
@Service
class OrderService(
private val mySqlOrderRepository: MySqlOrderRepository // 💥 구체 클래스
) {
fun createOrder(order: Order): Order {
return mySqlOrderRepository.save(order)
}
}
@Repository
class MySqlOrderRepository {
fun save(order: Order): Order {
// MySQL 저장 로직
}
}
문제점:
// MongoDB로 변경하려면 Service 코드 수정 필요!
@Service
class OrderService(
// private val mySqlOrderRepository: MySqlOrderRepository // 삭제
private val mongoOrderRepository: MongoOrderRepository // 추가
) {
fun createOrder(order: Order): Order {
return mongoOrderRepository.save(order) // 메서드 호출도 변경
}
}
1단계: 인터페이스 정의 (Domain Layer)
// domain/order/OrderRepository.kt
package com.hhplus.ecommerce.domain.order
// ✅ 고수준 모듈(Domain)이 정의하는 인터페이스
interface OrderRepository {
fun save(order: Order): Order
fun findById(id: Long): Order?
fun findByUserId(userId: Long): List<Order>
}
2단계: Service는 인터페이스에 의존 (Application Layer)
// application/order/OrderService.kt
package com.hhplus.ecommerce.application.order
import com.hhplus.ecommerce.domain.order.OrderRepository // 인터페이스
import com.hhplus.ecommerce.domain.order.Order
@Service
class OrderService(
private val orderRepository: OrderRepository // ✅ 추상화에 의존!
) {
fun createOrder(order: Order): Order {
return orderRepository.save(order)
}
}
3단계: 구현체들 (Infrastructure Layer)
// infrastructure/order/MySqlOrderRepository.kt
package com.hhplus.ecommerce.infrastructure.order
import com.hhplus.ecommerce.domain.order.OrderRepository // Domain 인터페이스
import com.hhplus.ecommerce.domain.order.Order
@Repository
@Primary // 기본 구현체
class MySqlOrderRepository : OrderRepository {
override fun save(order: Order): Order {
// MySQL 저장 로직
println("MySQL에 저장")
return order
}
override fun findById(id: Long): Order? {
// MySQL 조회 로직
}
}
// infrastructure/order/MongoOrderRepository.kt
package com.hhplus.ecommerce.infrastructure.order
import com.hhplus.ecommerce.domain.order.OrderRepository
import com.hhplus.ecommerce.domain.order.Order
@Repository
class MongoOrderRepository : OrderRepository {
override fun save(order: Order): Order {
// MongoDB 저장 로직
println("MongoDB에 저장")
return order
}
override fun findById(id: Long): Order? {
// MongoDB 조회 로직
}
}
장점:
application.yml 또는 @Primary만 변경하면 끝.// OrderService.kt (상위 모듈)
class OrderService(
private val repository: MySqlOrderRepository // 하위 모듈 직접 참조
)
// MySqlOrderRepository.kt (하위 모듈)
class MySqlOrderRepository {
// 구현
}
의존성 방향: OrderService -> MySqlOrderRepository
// Domain Layer
interface OrderRepository { // 고수준 모듈이 정의
fun save(order: Order): Order
}
// Application Layer
class OrderService(
private val repository: OrderRepository // 인터페이스 의존
)
// Infrastructure Layer
class MySqlOrderRepository : OrderRepository { // 인터페이스 구현
override fun save(order: Order): Order { ... }
}
의존성 방향:
OrderService -> OrderRepository (인터페이스)MySqlOrderRepository -> OrderRepository (인터페이스 구현)핵심: 구현체가 인터페이스에 의존하므로, 의존성 방향이 역전됨.
전통정: 상위 -> 하위
역전됨: 상위 -> 인터페이스 <- 하위
// ❌ DIP 위반
@Service
class OrderService(
private val smtpEmailService: SmtpEmailService // SMTP에 강하게 결합
) {
fun createOrder(order: Order) {
// ...
smtpEmailService.sendEmail(
to = "user@example.com",
subject = "주문 완료",
body = "주문이 완료되었습니다"
)
}
}
class SmtpEmailService {
fun sendEmail(to: String, subject: String, body: String) {
// SMTP 프로토콜로 이메일 전송
}
}
이 코드의 문제는 이메일을 slack으로 알림을 변경하려면 Service 전체 수정이 필요하다.
// ✅ DIP 적용
// domain/port/NotificationPort.kt (Domain이 정의)
interface NotificationPort {
fun sendOrderConfirmation(order: Order)
}
// application/order/OrderService.kt
@Service
class OrderService(
private val notificationPort: NotificationPort // 추상화에 의존
) {
fun createOrder(order: Order) {
// ...
notificationPort.sendOrderConfirmation(order) // 구현 방법은 모름!
}
}
// infrastructure/notification/EmailNotificationAdapter.kt
@Component
class EmailNotificationAdapter : NotificationPort {
override fun sendOrderConfirmation(order: Order) {
// SMTP로 이메일 전송
}
}
// infrastructure/notification/SlackNotificationAdapter.kt
@Component
class SlackNotificationAdapter : NotificationPort {
override fun sendOrderConfirmation(order: Order) {
// Slack API로 메시지 전송
}
}
이 코드의 장점은 이메일에서 slacke으로 변경을 할 때 Service 코드 수정이 불필요하다.
class OrderServiceTest {
@Test
fun `주문 생성 테스트`() {
val service = OrderService(
MySqlOrderRepository()
)
// MySQL이 없으면 테스트 불가
}
}
// 테스트용 Fake Repository
class FakeOrderRepository : OrderRepository {
private val storage = mutableMapOf<Long, Order>()
override fun save(order: Order): Order {
storage[order.id!!] = order
return order
}
override fun findById(id: Long): Order? = storage[id]
}
class OrderServiceTest {
@Test
fun `주문 생성 테스트`() {
// ✅ DB 없이도 테스트 가능!
val fakeRepository = FakeOrderRepository()
val service = OrderService(fakeRepository)
val order = service.createOrder(
Order.create(userId = 1L, items = listOf(...))
)
assertNotNull(order.id)
}
@Test
fun `주문 생성 시 알림 전송 테스트`() {
// ✅ Mock으로 쉽게 검증
val mockNotification = mock<NotificationPort>()
val service = OrderService(
orderRepository = fakeRepository,
notificationPort = mockNotification
)
service.createOrder(order)
verify(mockNotification).sendOrderConfirmation(any())
}
}
┌─────────────────────────────────────┐
│ Application Layer │
│ ┌────────────────┐ │
│ │ OrderService │ │
│ └────────┬───────┘ │
└───────────┼─────────────────────────┘
↓ 의존 (인터페이스)
┌───────────────────────────────────────┐
│ Domain Layer │
│ ┌────────────────────────────────┐ │
│ │ OrderRepository (interface) │ │ ← 고수준이 정의!
│ └────────────────────────────────┘ │
└───────────────────────────────────────┘
↑ 구현 (implements)
┌───────────────────────────────────────┐
│ Infrastructure Layer │
│ ┌────────────────────────────────┐ │
│ │ MySqlOrderRepository │ │ ← 저수준이 구현!
│ │ (implements OrderRepository) │ │
│ └────────────────────────────────┘ │
└───────────────────────────────────────┘
핵심은 Domain이 인터페이스를 정의하고, infrastructure가 구현을 한다.
의존성 역전 원칙은 단순히 "인터페이스를 만들자"가 아니라 "누가 인터페이스를 정의하고 소유하는가" 의 문제다. 고수준 모듈이 필요로 하는 것을 인터페이스로 정의하고, 저수준 모듈이 그것을 구현하게 만드는 것. 이것이 의존성을 역전시키는 핵심이다!