BackEnd/Spring WebFlux

Kotlin + Spring WebFlux 테스트 구조 설계기

Raconer 2025. 5. 28. 16:12
728x90

WebTestClient 기반 공통 로깅과 JSON 출력까지 자동화


✅ 목표

WebFlux는 비동기 기반이기 때문에 기존 MockMvc 방식과는 다르게 WebTestClient를 사용합니다.

테스트가 복잡해지기 전에, 저는 다음을 목표로 구조를 설계했습니다:

  • 모든 테스트에서 요청/응답 로그 자동 수집
  • 응답 시간, 헤더, JSON 바디를 보기 좋게 출력
  • 중복 없이 재사용 가능한 공통 베이스 테스트 클래스 구성

🛠️ 프로젝트 환경 요약

// build.gradle.kts
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-webflux")      // WebFlux
    implementation("org.springframework.boot:spring-boot-starter-data-r2dbc")   // R2DBC
    implementation("org.springdoc:springdoc-openapi-starter-webflux-ui:2.5.0")  // Swagger
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.1")    // JSON 변환

    testImplementation("org.springframework.boot:spring-boot-starter-test")     // WebTestClient 포함
    testImplementation("org.jetbrains.kotlin:kotlin-test-junit5")
    testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin")    // Jackson + Kotlin
}

 


🧩 WebTestClient로 테스트할 때 불편했던 점

  • 요청 URL, 헤더 확인하려면 매번 println 써야 했음
  • 응답 시간도 수동으로 재야 했고
  • JSON 응답 바디는 한 줄로 출력돼서 보기 불편함
  • 테스트가 많아지면 로그 관리가 힘들어짐

그래서 아예 공통 베이스 테스트 클래스를 만들어서 자동화했습니다.


🧱 DebugWebFluxTestSupport.kt

@TestPropertySource(properties = ["jasypt.encryptor.password=temp"]) // 테스트용 암호화 우회 설정
abstract class DebugWebFluxTestSupport {
    protected open lateinit var webTestClient: WebTestClient          // 테스트 클라이언트
    protected val mapper = ObjectMapper().registerKotlinModule()      // JSON 변환용

    private lateinit var startTime: Instant                            // 요청 시작 시간
    protected lateinit var lastRequestInfo: String                     // 요청 로그
    protected lateinit var lastResponseInfo: String                    // 응답 로그
    protected var lastResponseBodyJson: String? = null                 // 응답 JSON

    @BeforeEach
    fun setupLoggingFilter() {
        webTestClient = webTestClient.mutate()
            .responseTimeout(Duration.ofSeconds(30))                   // 응답 타임아웃
            .filter { request, next ->

                lastRequestInfo = buildString {
                    appendLine("🔸[REQUEST] ${request.method()} ${request.url()}")
                    request.headers().forEach { (k, v) ->
                        appendLine("  ➜ $k: ${v.joinToString()}")      // 요청 헤더 출력
                    }
                }

                startTime = Instant.now()                               // 시간 기록 시작

                next.exchange(request).doOnNext { response ->
                    val duration = Duration.between(startTime, Instant.now()).toMillis()
                    lastResponseInfo = buildString {
                        appendLine("🔹[RESPONSE] Status: ${response.statusCode()}, Time: ${duration}ms")
                        response.headers().asHttpHeaders().forEach { (k, v) ->
                            appendLine("  ⇦ $k: ${v.joinToString()}")  // 응답 헤더 출력
                        }
                    }
                }
            }.build()
    }

    @AfterEach
    fun printDebugSummary() {
        println("\n✅ 테스트 종료 후 전체 요청/응답 정보 요약:")
        println(lastRequestInfo)
        println(lastResponseInfo)
        println("📦 응답 바디 (JSON Pretty):\n${lastResponseBodyJson ?: "(바디 없음)"}")
    }

    protected fun setResponseBody(body: Any?) {
        lastResponseBodyJson = mapper
            .writerWithDefaultPrettyPrinter()
            .writeValueAsString(body)                                   // JSON 예쁘게 저장
    }
}

 


✅ 실제 테스트 예제

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ContentControllerTest : DebugWebFluxTestSupport() {

    @Autowired
    override lateinit var webTestClient: WebTestClient                  // 부모 클래스 필드 초기화

    @Test
    fun getContentsTest() {
        val result = webTestClient
            .get().uri("/v1/content")                                   // API 호출
            .accept(MediaType.APPLICATION_JSON)                         // JSON 응답 기대
            .exchange()
            .expectStatus().isOk                                        // 200 OK 확인
            .returnResult(CommonRes::class.java)                        // 응답 본문 타입 지정

        val bodyList = result.responseBody.collectList().block()        // Flux → List로 변환
        setResponseBody(bodyList)                                       // JSON 로그 저장
    }
}

 


✅ 실행 결과 예시 (자동 출력)

✅ 테스트 종료 후 전체 요청/응답 정보 요약:
🔸[REQUEST] GET http://localhost:62581/v1/content
  ➜ Accept: application/json
🔹[RESPONSE] Status: 200 OK, Time: 45ms
  ⇦ Content-Type: application/json
📦 응답 바디 (JSON Pretty):
[
  {
    "status": "SUCCESS",
    "data": [...]
  }
]

 


✨ 효과 정리

  • 매 테스트마다 요청/응답 로그를 신경 쓸 필요 없음
  • JSON도 보기 좋게 자동 출력됨
  • 공통 베이스로 테스트 작성이 훨씬 깔끔해짐
  • 테스트 실패 시 디버깅이 쉬워짐

📌 다음 할 일

  • POST, PUT 요청도 위 방식으로 확장
  • .expectBody().jsonPath() 등으로 본문 필드 검증 추가
  • @WebFluxTest 단위 테스트 구성도 따로 분리 예정

이 구조는 WebFlux 프로젝트에서 안정적인 테스트 흐름을 만들고 싶은 분들께 추천드립니다.
위 코드 전체는 테스트 코드이기 때문에 실제 API 작성 전에 먼저 테스트부터 잡아보셔도 좋아요.

참고


https://github.com/Raconer/SpringWebFlux

 

GitHub - Raconer/SpringWebFlux: 스프링 웹 플럭스 공부용

스프링 웹 플럭스 공부용. Contribute to Raconer/SpringWebFlux development by creating an account on GitHub.

github.com

 

728x90