우리는 적은 리소스로도 높은 쓰로풋(Throughtput)을 달성하기 위해 비동기 프로그래밍을 활용한다. 스프링 개발자들은 주로 리엑터(Reactor)와 코틀린의 코루틴을 통해 비동기를 구현한다. 그러나 리엑터는 그 복잡성으로 인해, 많은 개발자들이 코루틴을 선호하는 경향이 있다. 코루틴은 주로 논블로킹 환경을 지원하는 Spring WebFlux와 함께 사용된다. 반면 Spring MVC와는 잘 사용되지 않는다. 그 이유는 코루틴을 사용하더라도 Spring MVC의 블로킹 구조로 인해 쓰로풋이 향상되기 어렵기 때문이다. 이제 간단한 테스트를 통해 이 차이점을 좀 더 자세히 설명을 해보겠다.
MVC와 WebFlux 환경에서의 쓰로풋 비교
테스트 개요
- 해당 테스트는 BFF 환경에서 User 서비스의 사용자 정보와 Order 서비스의 주문 정보를 조회하고, 이를 조합하여 mobile 클라이언트에 전달하는 과정을 재현하였다.
- Spring MVC를 사용하느냐 Spring WebFlux를 사용하느냐에 차이가 있을 뿐, 비즈니스 로직은 동일하다.
- Spring MVC는 Tomcat을 사용하고, 스레드 풀 크기는 기본 값인 200으로 설정하였다. 이는 스레드 수가 증가함에 따라 발생할 수 있는 컨텍스트 스위칭 비용이 요청 처리 성능에 부정적인 영향을 미치지 않도록 하기 위함이다. 물론, 필자가 작성한 테스트 코드는 복잡한 비즈니스 로직이 없기 때문에 스레드 풀 크기를 200 이상으로 설정하면 동시성은 높아질 수 있다. 하지만, 비즈니스 로직이 복잡한 서버에서는 오히려 성능 저하를 초래할 수 있으며, 일반적인 서버는 필자가 작성한 코드보다 더 복잡한 로직을 처리하는 경우가 많다. 이번 테스트는 그러한 일반적인 환경을 가정하고 동시성 및 성능을 측정하려는 목적이 있다.
- Spring WebFlux는 Netty를 사용하였으며 기본 설정을 적용하였다.
테스트 환경
서버 환경(Amazon Lightsail) | |
CPU | 2.50GHz 2core |
Memory | 2GB |
OS | Amazon Linux 2023 |
테스트 도구(JMeter) | |
실행 위치 | Mac M1 Pro (Local) |
테스트 시 사용한 코드
(공통)
@RestController
class UserController(private val bffService: BffService) {
@GetMapping("/mobile/users/{id}")
suspend fun getUserDetails(@PathVariable id: Long): UserDetailsResponse {
return bffService.getUserDetails(id)
}
}
@Service
class BffService(private val userService: UserService, private val orderService: OrderService) {
suspend fun getUserDetails(id: Long): UserDetailsResponse = coroutineScope {
val userDeferred = async { userService.getUserById(id) }
val ordersDeferred = async { orderService.getOrdersByUserId(id) }
UserDetailsResponse(userDeferred.await(), ordersDeferred.await())
}
}
@Service
class UserService {
suspend fun getUserById(id: Long): User {
delay(1000) // NIO 호출을 시뮬레이션하기 위한 1초 지연
return User(id, "User$id")
}
}
@Service
class OrderService {
suspend fun getOrdersByUserId(userId: Long): List<Order> {
delay(1000) // NIO 호출을 시뮬레이션하기 위한 1초 지연
return listOf(
Order(1, "Item A", 100),
Order(2, "Item B", 200)
)
}
}
data class User(val id: Long, val name: String)
data class Order(val orderId: Long, val itemName: String, val price: Int)
data class UserDetailsResponse(val user: User, val orders: List<Order>)
(MVC)
dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
}
(WebFlux)
dependencies {
implementation("org.springframework.boot:spring-boot-starter-webflux")
}
테스트 결과
1. MVC 테스트
- 유저: 200명 (200명 이상 설정 시 에러가 발생하여, 동시에 처리 가능한 요청 수가 200개로 추정됨)
- 루프카운트: 250
- 요청 처리량: 5만건
- 실행 시간: 04:19
- 쓰로풋: 192.6/sec
2. WebFlux 테스트
- 유저: 5000명 (5000명 이상 설정 시 에러가 발생하여, 동시에 처리 가능한 요청 수가 5000개로 추정됨)
- 루프카운트: 10
- 요청 처리량: 5만건
- 실행 시간: 00:18
- 쓰로풋: 2709.7.sec
3. 결과 비교
코루틴을 MVC 환경에서 사용했을 때 쓰로풋은 192.6/sec, WebFlux 환경에서는 2709.7/sec로 나타났다. 단순히 쓰로풋만 비교했을 때, 약 14배의 차이가 발생했다.
**이러한 차이가 발생하는 이유에 대해서는 Spring MVC에서 코루틴 사용에 대한 생각 - 2편에서 계속 다루겠습니다.**
'스프링 프로젝트' 카테고리의 다른 글
Spring MVC에서 코루틴 사용에 대한 생각 - 2편 (0) | 2024.10.29 |
---|