본문 바로가기
작디 작은 나만의 라이브러리/BridgeApi

[신입 개발자의 '작디 작고 작디 작고 자그만' 첫 라이브러리 제작기] 4. Bridge-Api 데코레이터 패턴을 활용한 인터셉터 구현

by 시니성 2025. 1. 5.

안녕하세요!
오늘은 Bridge-Api 라이브러리의 인터셉터 구현에 대해 이야기해보려고 합니다.
인터셉터는 HTTP 요청/응답을 가로채서 공통적인 처리를 수행할 수 있게 해주는 중요한 기능인데요, 이를 구현하기 위해 데코레이터 패턴을 활용했습니다.

1. 데코레이터 패턴을 선택한 이유

인터셉터를 구현하는 방법은 여러 가지가 있지만, 데코레이터 패턴을 선택한 이유는 다음과 같습니다:

  1. 유연성: 새로운 기능을 동적으로 추가/제거할 수 있습니다.
  2. 단일 책임 원칙: 각 인터셉터는 하나의 책임만을 가집니다.
  3. OCP 원칙: 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있습니다.
  4. 재사용성: 인터셉터를 독립적으로 재사용할 수 있습니다.

2. 기본 구조 구현

먼저, 서비스 인터페이스와 기본 데코레이터 클래스를 정의했습니다.

// 기본 서비스 인터페이스
interface BridgeService {
    suspend fun serve(ctx: RequestContext): BridgeResponse
}

// 추상 데코레이터 클래스
abstract class ServiceDecorator : BridgeService {
    private lateinit var nestedService: BridgeService

    fun wrap(service: BridgeService): BridgeService {
        this.nestedService = service
        return this
    }

    fun unwrap(): BridgeService = nestedService
}

이 구조에서 ServiceDecorator는 기본 서비스를 감싸고, 필요한 기능을 추가할 수 있는 기반을 제공합니다.

3. 인터셉터 구현

다음은 실제 인터셉터의 구현 예시입니다:

class TestInterceptor : ServiceDecorator() {
    override suspend fun serve(ctx: RequestContext): BridgeResponse {
        println("Before TestInterceptor1")

        // 특정 조건에서 요청을 가로채서 처리
        if ("test" in ctx.segments && "interceptor" in ctx.segments && ctx.method.isGet()) {
            return BridgeResponse("Intercepted by TestInterceptor1 and not reach controller")
        }

        // 다음 체인으로 요청 전달
        val response = unwrap().serve(ctx)

        println("After TestInterceptor1")
        return response
    }
}

이 인터셉터는:

  • 요청 처리 전/후에 로그를 출력합니다.
  • 특정 조건을 만족하는 경우 요청을 가로채서 직접 응답을 반환합니다.
  • 그 외의 경우 체인의 다음 단계로 요청을 전달합니다.

4. 인터셉터 등록 및 체인 구성

인터셉터는 BridgeRouter의 Builder를 통해 등록됩니다:

val router = BridgeRouter.builder()
    .apply {
        setSerializer(objectMapper)
        registerController("api/v1/users", userController)
        registerDecorator(TestInterceptor())
        registerDecorator(TestInterceptor2())
        registerDecorator(TestInterceptor3())
    }.build()

내부적으로는 다음과 같이 체인을 구성합니다:

private fun buildDecoratedService(): BridgeService =
    serviceDecorators
        .foldRight(BaseService(objectMapper) as BridgeService) { service, acc -> 
            service.wrap(acc) 
        }

이 구현은 데코레이터들을 역순으로 연결하여 체인을 구성합니다.

5. 테스트로 보는 인터셉터 동작

인터셉터의 동작을 확인하기 위한 테스트 코드입니다:

class InterceptorTest : StringSpec({
    // ... router 설정 ...

    "GET: /api/v1/test/interceptor" {
        router.routingRequest("api/v1/test/interceptor", MethodType.GET) shouldBe 
            "Intercepted by TestInterceptor1 and not reach controller"
    }

    "POST: /api/v1/test/interceptor" {
        router.routingRequest("api/v1/test/interceptor", MethodType.POST) shouldBe 
            "Intercepted by TestInterceptor2 and not reach controller"
    }

    "DELETE: /api/v1/test/interceptor" {
        router.routingRequest("api/v1/test/interceptor", MethodType.DELETE) shouldBe 
            "reach test interceptor delete controller"
    }
})

테스트 결과를 보면:

  1. GET 요청은 TestInterceptor1에서 가로챕니다.
  2. POST 요청은 TestInterceptor2에서 가로챕니다.
  3. DELETE 요청은 모든 인터셉터를 통과하여 컨트롤러까지 도달합니다.

6. 실제 사용 사례

인터셉터는 다음과 같은 실제 상황에서 유용하게 사용될 수 있습니다:

  1. 인증/인가 처리

    class AuthInterceptor : ServiceDecorator() {
     override suspend fun serve(ctx: RequestContext): BridgeResponse {
         if (ctx.headers["Authorization"] != "Valid Token") {
             return BridgeResponse(
                 ApiCommonResponse(
                     status = -1,
                     message = "Unauthorized",
                     data = null
                 )
             )
         }
         return unwrap().serve(ctx)
     }
    }
  2. 로깅

    class LoggingInterceptor : ServiceDecorator() {
     override suspend fun serve(ctx: RequestContext): BridgeResponse {
         val startTime = System.currentTimeMillis()
         val response = unwrap().serve(ctx)
         val endTime = System.currentTimeMillis()
    
         logger.info("Request processed in ${endTime - startTime}ms")
         return response
     }
    }

7. 마치며

데코레이터 패턴을 활용한 인터셉터 구현을 통해 다음과 같은 이점을 얻을 수 있었습니다:

  1. 모듈화: 각 인터셉터는 독립적인 모듈로 작동합니다.
  2. 유연성: 필요에 따라 인터셉터를 쉽게 추가/제거할 수 있습니다.
  3. 가독성: 각 인터셉터의 책임이 명확하여 코드를 이해하기 쉽습니다.
  4. 유지보수성: 기능 추가나 수정이 용이합니다.

다음 글에서는 Bridge-Api의 마지막 주요 기능인 파라미터 바인딩 구현에 대해 다루어보도록 하겠습니다.

728x90