안녕하세요!
오늘은 Bridge-Api 라이브러리의 인터셉터 구현에 대해 이야기해보려고 합니다.
인터셉터는 HTTP 요청/응답을 가로채서 공통적인 처리를 수행할 수 있게 해주는 중요한 기능인데요, 이를 구현하기 위해 데코레이터 패턴을 활용했습니다.
1. 데코레이터 패턴을 선택한 이유
인터셉터를 구현하는 방법은 여러 가지가 있지만, 데코레이터 패턴을 선택한 이유는 다음과 같습니다:
- 유연성: 새로운 기능을 동적으로 추가/제거할 수 있습니다.
- 단일 책임 원칙: 각 인터셉터는 하나의 책임만을 가집니다.
- OCP 원칙: 기존 코드를 수정하지 않고도 새로운 기능을 추가할 수 있습니다.
- 재사용성: 인터셉터를 독립적으로 재사용할 수 있습니다.
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"
}
})
테스트 결과를 보면:
- GET 요청은 TestInterceptor1에서 가로챕니다.
- POST 요청은 TestInterceptor2에서 가로챕니다.
- DELETE 요청은 모든 인터셉터를 통과하여 컨트롤러까지 도달합니다.
6. 실제 사용 사례
인터셉터는 다음과 같은 실제 상황에서 유용하게 사용될 수 있습니다:
인증/인가 처리
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) } }
로깅
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. 마치며
데코레이터 패턴을 활용한 인터셉터 구현을 통해 다음과 같은 이점을 얻을 수 있었습니다:
- 모듈화: 각 인터셉터는 독립적인 모듈로 작동합니다.
- 유연성: 필요에 따라 인터셉터를 쉽게 추가/제거할 수 있습니다.
- 가독성: 각 인터셉터의 책임이 명확하여 코드를 이해하기 쉽습니다.
- 유지보수성: 기능 추가나 수정이 용이합니다.
다음 글에서는 Bridge-Api의 마지막 주요 기능인 파라미터 바인딩 구현에 대해 다루어보도록 하겠습니다.
- 라이브러리 레포지토리: https://github.com/shiniseong/bridge-api
728x90