우리는 적은 리소스로도 높은 쓰로풋(Throughtput)을 달성하기 위해 비동기 프로그래밍을 활용한다. 스프링 개발자들은 주로 리엑터(Reactor)와 코틀린의 코루틴을 통해 비동기를 구현한다. 그러나 리엑터는 그 복잡성으로 인해, 많은 개발자들이 코루틴을 선호하는 경향이 있다. 코루틴은 주로 논블로킹 환경을 지원하는 Spring WebFlux와 함께 사용된다. 반면 Spring MVC와는 잘 사용되지 않는다. 그 이유는 코루틴을 사용하더라도 Spring MVC의 블로킹 구조로 인해 쓰로풋이 향상되기 어렵기 때문이다. 이제 간단한 테스트를 통해 이 차이점을 좀 더 자세히 설명을 해보겠다.

 
 

MVC와 WebFlux 환경에서의 쓰로풋 비교

테스트 개요

  1. 해당 테스트는 BFF 환경에서 User 서비스의 사용자 정보와 Order 서비스의 주문 정보를 조회하고, 이를 조합하여 mobile 클라이언트에 전달하는 과정을 재현하였다.
  2. Spring MVC를 사용하느냐 Spring WebFlux를 사용하느냐에 차이가 있을 뿐, 비즈니스 로직은 동일하다.
  3. Spring MVC는 Tomcat을 사용하고, 스레드 풀 크기는 기본 값인 200으로 설정하였다. 이는 스레드 수가 증가함에 따라 발생할 수 있는 컨텍스트 스위칭 비용이 요청 처리 성능에 부정적인 영향을 미치지 않도록 하기 위함이다. 물론, 필자가 작성한 테스트 코드는 복잡한 비즈니스 로직이 없기 때문에 스레드 풀 크기를 200 이상으로 설정하면 동시성은 높아질 수 있다. 하지만, 비즈니스 로직이 복잡한 서버에서는 오히려 성능 저하를 초래할 수 있으며, 일반적인 서버는 필자가 작성한 코드보다 더 복잡한 로직을 처리하는 경우가 많다. 이번 테스트는 그러한 일반적인 환경을 가정하고 동시성 및 성능을 측정하려는 목적이 있다.
  4. 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편에서 계속 다루겠습니다.**

+ Recent posts