36장. Service와 의존성
Service는 흐름을 조율하는 객체라고 했습니다.
그리고 Service는 Repository에 의존합니다.
여기서 “의존“이라는 말이 나옵니다.
이 장에서는 의존성을 어떻게 다루는지,
그리고 그 유명한 “의존성 주입“이 무엇인지 배웁니다.
11부의 마무리이자,
스프링을 이해하는 데 가장 중요한 개념입니다.
36.1 Service의 역할
Service는 하나의 기능을
처음부터 끝까지 완성합니다.
예를 들어 주문 생성은 이런 흐름입니다.
- 회원이 존재하는지 확인
- 상품이 존재하는지 확인
- 주문 객체를 만든다
- 주문을 저장한다
- 응답을 돌려준다
이 흐름을 Service가 조율합니다.
class OrderService(
private val orderRepository: OrderRepository,
private val productRepository: ProductRepository
) {
fun createOrder(request: CreateOrderRequest): OrderResponse {
val product = productRepository.findById(request.productId)
?: throw IllegalArgumentException("상품 없음")
val order = Order(id = 0, items = listOf(OrderItem(product.id, request.quantity)))
val saved = orderRepository.save(order)
return saved.toResponse()
}
}
Service는 여러 객체를 엮어
“줄거리“를 완성하는 감독 같은 존재입니다.
36.2 생성자를 통한 의존성 전달
OrderService는
Repository들이 있어야 동작합니다.
이 필요한 객체들을
생성자로 받는 것이 핵심입니다.
class OrderService(
private val orderRepository: OrderRepository,
private val productRepository: ProductRepository
)
Service가 필요한 것을
스스로 만들지 않고 “밖에서 받는다“는 점이 중요합니다.
// 나쁜 예: Service가 직접 만든다
class OrderService {
private val orderRepository = MemoryOrderRepository() // 특정 구현에 묶임
}
직접 만들면
그 특정 구현에 딱 붙어 버립니다.
그러면 테스트에서 교체할 수 없습니다.
36.3 구현체가 아닌 Interface에 의존하기
생성자로 받되,
받는 타입은 구현체가 아니라 인터페이스여야 합니다. (8장)
class OrderService(
private val orderRepository: OrderRepository // 인터페이스!
)
OrderRepository는 인터페이스이므로
어떤 구현체든 넣을 수 있습니다.
- 운영에서는 데이터베이스 구현
- 테스트에서는 메모리/Fake 구현
이것이 31장에서 테스트가
그토록 쉬웠던 이유입니다.
구현이 아닌 인터페이스에 의존하라.
이 한 문장이 좋은 구조의 핵심입니다.
36.4 Dependency Injection의 기본 개념
지금까지 한 일에 이름이 있습니다.
바로 의존성 주입(Dependency Injection, DI)입니다.
말은 거창하지만 뜻은 간단합니다.
필요한 객체를 밖에서 만들어 넣어 주는 것.
여기서는 프레임워크 없이
직접 객체를 조립합니다.
OrderService
├─ OrderRepository
└─ ProductRepository
조립 코드는 이렇게 생깁니다.
fun main() {
// 구현체를 만들고
val orderRepository = MemoryOrderRepository()
val productRepository = MemoryProductRepository()
// Service에 넣어 준다 (주입)
val orderService = OrderService(orderRepository, productRepository)
// 이제 사용
orderService.createOrder(request)
}
이렇게 객체를 만들어 연결하는 코드를
조립(assembly)이라고 부릅니다.
스프링은 바로 이 조립을
자동으로 해 주는 도구입니다.
지금 우리는 그것을 손으로 해 본 것입니다.
36.5 Service 테스트하기
의존성을 생성자로 받으면
테스트가 아주 쉬워집니다. (30·31장)
@Test
fun `상품이 없으면 주문 생성에 실패한다`() {
// Given: 빈 Fake 저장소
val orderService = OrderService(
FakeOrderRepository(),
FakeProductRepository() // 상품이 하나도 없음
)
// When & Then
assertThrows<IllegalArgumentException> {
orderService.createOrder(CreateOrderRequest(productId = 999, quantity = 1))
}
}
진짜 데이터베이스 없이
원하는 상황을 자유롭게 만들 수 있습니다.
36.6 Fake Repository 활용하기
성공 시나리오도 테스트해 봅시다.
@Test
fun `주문을 생성하면 저장된다`() {
// Given
val productRepository = FakeProductRepository()
productRepository.save(Product(id = 1, name = "노트북", price = 1000))
val orderRepository = FakeOrderRepository()
val orderService = OrderService(orderRepository, productRepository)
// When
val response = orderService.createOrder(
CreateOrderRequest(productId = 1, quantity = 2)
)
// Then
assertNotNull(orderRepository.findById(response.id))
}
Fake를 미리 채워 두고
원하는 상태에서 검증합니다.
이렇게 빠르고 안정적인 테스트가
좋은 구조에서 자연스럽게 나옵니다.
36.7 의존성이 늘어날 때 발생하는 문제
서비스가 커지면
의존성도 함께 늘어납니다.
class OrderService(
private val orderRepository: OrderRepository,
private val productRepository: ProductRepository,
private val userRepository: UserRepository,
private val paymentService: PaymentService,
private val notificationService: NotificationService
// ... 계속 늘어남
)
손으로 조립하는 코드도 점점 길어집니다.
val orderService = OrderService(
orderRepository,
productRepository,
userRepository,
paymentService,
notificationService
)
객체가 수십, 수백 개가 되면
이 조립을 손으로 관리하기 벅차집니다.
바로 이 문제를 해결하려고
스프링 같은 DI 프레임워크가 등장합니다.
프레임워크는 이 “조립“을 대신해 줍니다.
우리가 손으로 한 일을 자동화한 것뿐입니다.
그래서 지금 이 과정을 직접 해 본 것이
나중에 스프링을 배울 때 큰 도움이 됩니다.
36장을 마치며
이 장에서 우리는 다음을 배웠습니다.
- Service가 흐름을 조율하는 역할이라는 점
- 의존성을 생성자로 받는 이유
- 구현체가 아닌 인터페이스에 의존하기
- 의존성 주입(DI)과 조립의 개념
- Fake로 Service를 쉽게 테스트하기
- 의존성이 늘면 프레임워크가 필요해지는 이유
이것으로 11부(백엔드 구조)를 마칩니다.
다음 12부에서는 지금까지 배운 모든 것을 모아
하나의 주문 시스템을 완성합니다.