WebFlux + Kotlin의 대기열 시스템 개발 중 만난 3가지 핵심 오류 해결기

궁금하면 500원·2025년 4월 9일
0

미생의 개발 이야기

목록 보기
38/58

Spring WebFlux 프로젝트에서 발생한 오류 해결 과정

문제 배경

사이드 프로젝트로 Spring WebFlux와 Kotlin을 사용해 대기열 시스템을 구현하던 중, 여러 컴파일 오류와 빈 충돌 문제가 발생했습니다.

이 프로젝트는 Redis를 활용해 사용자 대기열을 관리하고, WebFlux의 반응형 라우팅을 통해 API와 대기실 페이지를 제공하는 시스템입니다.
하지만 다음과 같은 주요 오류가 발생하며 개발이 중단되었습니다.

  • RedisQueueService: ReactiveRedisTemplate 빈이 두 개 이상 존재해 자동 주입(autowire)이 실패했습니다.
  • QueueRouterConfig: cookies() 처리와 Mono 타입 불일치로 컴파일 오류가 발생했습니다.
  • QueueError: RuntimeException의 message 프로퍼티를 재정의하며 override 키워드 누락으로 오류가 발생했습니다.

이 포스팅에서는 각 문제의 원인, 변경 전 코드, 해결 방법, 그리고 이 과정에서 배운 점을 정리합니다.

변경 전 코드

1. RedisQueueService (빈 충돌 문제)

RedisQueueService에서 ReactiveRedisTemplate을 주입받으려 했으나, Spring Boot의 자동 설정과 사용자 정의 설정에서 각각 ReactiveRedisTemplate 빈이 생성되어 충돌이 발생했습니다.

@Service
class RedisQueueService(
    private val redisTemplate: ReactiveRedisTemplate<String, String>,
    @Value("\${scheduler.enabled:false}") private val scheduling: Boolean
) : QueueService {
    // ... (나머지 코드 생략)
}

오류 메시지

  • Could not autowire. There is more than one bean of 'ReactiveRedisTemplate' type.

2. QueueRouterConfig (쿠키 처리 및 타입 불일치)

QueueRouterConfig에서 cookies()를 잘못 처리하고, Mono를 coRouter에서 직접 반환하며 타입 불일치 오류가 발생했습니다.

GET("/waiting-room") { request ->
    val queue = request.queryParamOrNull("queue") ?: "default"
    val userId = request.queryParamOrNull("user-id")?.toLong()
        ?: return@GET ServerResponse.badRequest().buildAndAwait()
    val redirectUrl = request.queryParamOrNull("redirect-url")
        ?: return@GET ServerResponse.badRequest().buildAndAwait()

    val cookieToken = request.cookies()
        .firstOrNull { it.name == "user-queue-$queue-token" }
        ?.value ?: ""

    if (queueService.validateToken(queue, userId, cookieToken)) {
        return@GET ServerResponse.temporaryRedirect(java.net.URI(redirectUrl)).buildAndAwait()
    }

    val status = queueService.registerOrGetStatus(queue, userId)
    val progress = QueueProgress.from(status)

    ServerResponse.ok()
        .render("waiting-room", mapOf(
            "queue" to queue,
            "userId" to userId,
            "queueFront" to progress.queueFront,
            "queueBack" to progress.queueBack,
            "progress" to progress.progress
        ))
}

오류 메시지

  • Unresolved reference: firstOrNull
  • Type mismatch: inferred type is Mono<ServerResponse!> but ServerResponse was expected

3. QueueError (message 오버라이드 문제)

QueueError 클래스에서 message 파라미터를 정의했으나, 상위 클래스 RuntimeException의 message 프로퍼티를 오버라이드한다고 명시하지 않아 오류가 발생했습니다.

sealed class QueueError(val message: String) : RuntimeException(message) {
    object UserAlreadyRegistered : QueueError("이미 등록된 사용자입니다")
    object InvalidToken : QueueError("유효하지 않은 토큰입니다")
}

해결 방법

1. RedisQueueService 빈 충돌 해결

ReactiveRedisTemplate 빈 충돌은 Spring Boot의 RedisReactiveAutoConfiguration에서 생성된 기본 빈(reactiveStringRedisTemplate)과 사용자 정의 빈(reactiveRedisTemplate) 간의 충돌로 인해 발생했습니다.

이를 해결하기 위해 RedisQueueService의 생성자에 @Qualifier를 추가해 사용자 정의 빈을 명시적으로 주입했습니다.

@Service–
class RedisQueueService(
    @Qualifier("reactiveRedisTemplate") private val redisTemplate: ReactiveRedisTemplate<String, String>,
    @Value("\${scheduler.enabled:false}") private val scheduling: Boolean
) : QueueService {
    // ... (나머지 코드 생략)
}

변경 이유: @Qualifier("reactiveRedisTemplate")를 사용해 Spring이 RedisConfig에서 정의한 특정 빈을 주입하도록 지정했습니다.

2. QueueRouterConfig 쿠키 처리 및 타입 불일치 해결

QueueRouterConfig의 오류는 두 가지로 나뉩니다.

  • 쿠키 처리: cookies()는 Map<String, List>를 반환하므로 firstOrNull을 직접 사용할 수 없었습니다.
    대신 cookies().get(key)로 특정 쿠키 리스트를 가져오고, 첫 번째 HttpCookie의 값을 추출했습니다.
  • 타입 불일치: coRouter는 ServerResponse를 기대하지만, temporaryRedirect와 render는 Mono를 반환했습니다.
    이를 해결하기 위해 buildAndAwait()와 renderAndAwait()를 사용해 Mono를 풀었습니다.

수정된 코드

GET("/waiting-room") { request ->
    val queue = request.queryParamOrNull("queue") ?: "default"
    val userId = request.queryParamOrNull("user-id")?.toLong()
        ?: return@GET ServerResponse.badRequest().buildAndAwait()
    val redirectUrl = request.queryParamOrNull("redirect-url")
        ?: return@GET ServerResponse.badRequest().buildAndAwait()

    // 쿠키에서 토큰 가져오기
    val cookieToken = request.cookies().get("user-queue-$queue-token")
        ?.firstOrNull()?.value ?: ""

    if (queueService.validateToken(queue, userId, cookieToken)) {
        ServerResponse.temporaryRedirect(java.net.URI(redirectUrl)).buildAndAwait()
    } else {
        val status = queueService.registerOrGetStatus(queue, userId)
        val progress = QueueProgress.from(status)

        ServerResponse.ok()
            .renderAndAwait("waiting-room", mapOf(
                "queue" to queue,
                "userId" to userId,
                "queueFront" to progress.queueFront,
                "queueBack" to progress.queueBack,
                "progress" to progress.progress
            ))
    }
}

변경 이유

  • cookies().get(...)?.firstOrNull()?.value로 쿠키를 안전하게 처리.
  • buildAndAwait()와 renderAndAwait()로 비동기 결과를 동기적으로 처리해 coRouter의 기대 타입과 일치시켰습니다.

3. QueueError message 오버라이드

QueueError에서 message 파라미터가 RuntimeException의 message 프로퍼티를 가리기 때문에 override 키워드를 추가하고, 상위 클래스 생성자에 message를 전달했습니다.

수정된 코드

sealed class QueueError(override val message: String) : RuntimeException(message) {
    object UserAlreadyRegistered : QueueError("이미 등록된 사용자입니다")
    object InvalidToken : QueueError("유효하지 않은 토큰입니다")
}

변경 이유: override val message를 추가해 RuntimeException의 message를 명시적으로 재정의하고, RuntimeException(message)로 상위 클래스에 값을 전달했습니다.

배우게 된 점

1. Spring 빈 관리의 중요성
Spring의 의존성 주입에서 빈 충돌은 흔한 문제입니다.
@Qualifier와 @Primary 같은 어노테이션을 사용하거나, 불필요한 자동 설정을 비활성화(@SpringBootApplication(exclude = [...]))해 빈 정의를 명확히 관리하는 법을 배웠습니다.

2. WebFlux와 코루틴의 비동기 처리
Spring WebFlux의 Mono와 Kotlin 코루틴을 함께 사용할 때, await() 또는 buildAndAwait() 같은 메서드로 비동기 결과를 동기적으로 처리해야 한다는 점을 알게 되었습니다.
특히 coRouter는 코루틴 기반이므로 타입 일치를 신경 써야 합니다.

3. Kotlin의 상속과 오버라이드 규칙
Kotlin은 상위 클래스의 멤버를 재정의할 때 override 키워드를 강제합니다.
이를 통해 실수로 상위 클래스의 프로퍼티나 메서드를 덮어쓰는 문제를 방지할 수 있습니다.

4. 쿠키 처리의 세심함
WebFlux에서 cookies()는 Map<String, List>를 반환하므로, 단순히 컬렉션 메서드를 호출하기보다는 타입을 명확히 이해하고 처리해야 한다는 점을 배웠습니다.

5. 디버깅과 로그 활용
오류를 해결하며 로그를 추가하거나 Spring의 디버그 로깅
(logging.level.org.springframework.beans.factory=DEBUG)을 활성화해 빈 생성 과정을 추적하는 방법이 문제 해결에 유용했습니다.

결론

이번 사이드 프로젝트에서 발생한 오류들은 Spring WebFlux와 Kotlin의 특성을 이해하는 데 큰 도움이 되었습니다.

특히, 빈 충돌, 비동기 처리, 상속 규칙 같은 복잡한 문제를 해결하며 프레임워크와 언어의 동작 원리를 깊이 파악할 수 있었습니다.

이러한 경험은 향후 반응형 애플리케이션 개발 시 더 나은 설계와 디버깅 능력을 발휘할 수 있는 밑거름이 될 것입니다.

사이드 프로젝트를 진행하며 비슷한 문제를 마주친다면, 다음 단계를 추천합니다.

  • 오류 메시지를 꼼꼼히 읽고, 어떤 타입이나 빈이 충돌하는지 확인.
  • Spring 문서나 Kotlin 문서를 참고해 예상치 못한 동작을 이해.
  • 로그와 디버깅 도구를 적극 활용해 문제의 근원을 파악.

이 과정을 통해 더 견고하고 효율적인 코드를 작성할 수 있을 것입니다!

profile
꾸준히, 의미있는 사이드 프로젝트 경험과 문제해결 과정을 기록하기 위한 공간입니다.

0개의 댓글