안녕하세요.
업무가 많다는 핑계로 항상 기술 블로그 쓰기를 미루는 게으름을 이기고 오랜만에, 라이브러리 제작기를 쓰려고 합니다.
라이브러리를 제작한지 벌써 6개월이 지났는데, 이제서야 마지막 포스팅을 하게 되네요.
그 동안 정말 많은 일이 있었습니다.
최근 멀티플랫폼 POS를 개발하는 프로젝트에서 이제 갓 1년차가 된 저에게 벌써 과분하게도, 그리고 책임이 막중하게도, 아키텍쳐를 설계해야 하는 아키텍트의 역할이 주어졌습니다.
POS, KIOSK 등을 위시한 오프라인 결제 솔루션은 그, 특성상 다양한 소프트웨어적, 하드웨어적 외부 세계(ex. 다양한 van사 결제 모듈과 프린터 등)에 의존하게 됩니다.
헌데, 기존의 레거시 아키텍쳐(컨트롤, 서비스, 영속성 레이어로 구성된 일반적인 삼 계층 아키텍쳐)로는 다양한 외부세계를 표현하기에 해상도가 떨어져서, 레이어의 책임이 명확해 지지 않고, 서비스 레이어가 비대해 지는 문제점이 있었습니다.
이를 해결하기 위해 새로운 아키텍쳐를 설계하고 적용하는 과정이 저에게는 상당히 벅찬 과정이었기 때문에, 그 간 블로그에 상당히 소홀했네요 ㅠㅠ.
아키텍쳐 설계에 관한 자세한 글은 차후 별도의 포스팅 예정입니다.
자, 쓸데없는 근황 토크와 핑계는 여기까지!
각설하고, 오늘은 Bridge-Api 라이브러리의 제작의 마지막 과정이었던, 인터셉터 구현에 대해 이야기해보려고 합니다.
이번 구현에서는 데코레이터 패턴을 활용하여 유연하고 확장 가능한 인터셉터 시스템을 구축했습니다.
1. 데코레이터 패턴을 선택한 이유
인터셉터를 구현하기 위해 데코레이터 패턴을 선택한 주요 이유는 다음과 같습니다:
첫째, 데코레이터 패턴은 기존 코드를 수정하지 않고도 새로운 기능을 동적으로 추가할 수 있게 해줍니다.
이는 SOLID 설계 원칙의 하나로 잘 알려진 개방-폐쇄 원칙(OCP)을 잘 준수하는 방법입니다.
둘째, 인터셉터들을 체인 형태로 구성할 수 있어, 여러 인터셉터를 유연하게 조합할 수 있습니다.
이는 복잡한 요청 처리 파이프라인을 구축하는 데 매우 유용합니다.
셋째, 각 인터셉터는 독립적으로 작동하면서도 전체 요청-응답 사이클에 참여할 수 있습니다.
이는 관심사의 분리(SoC)를 촉진합니다.
넷째, 감싸진 데코레이터를 언랩하고 serve하는 과정 전후에 인터셉터 코드를 기술함으로써, 요정 전후에 대한 처리를 명확히 할 수 있습니다.
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는 다른 BridgeService를 감싸고, 요청을 처리하기 전후에 추가 작업을 수행할 수 있습니다.
3. 인터셉터 구현
실제 인터셉터 구현의 예시를 살펴보겠습니다:
class MeasureDecorator : ServiceDecorator() {
private val logger: Logger = LoggerFactory.getLogger(MeasureDecorator::class.java)
override suspend fun serve(ctx: RequestContext): BridgeResponse {
val start = System.currentTimeMillis()
val response = unwrap().serve(ctx)
val end = System.currentTimeMillis()
logger.debug("Execution time: ${end - start}ms")
return response
}
}
이 인터셉터는 요청 처리에 걸린 시간을 측정하고 로깅하는 간단한 예시입니다.
아래와 같이 더 복잡한 특정 조건에서 작동할 인터셉터도 구현할 수 있습니다:
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
}
}
예시의 인터셉터는, 경로에 세그먼트에 test와 interceptor가 포함되면서, 요청 메소드가 GET인 경우 인터셉트하여 서비스 로직이 실행되기전에 바로 값을 반환하도록 하는 인터셉터입니다.
4. 인터셉터 체인 구성
BridgeRouter에서 인터셉터 체인을 구성하는 방법입니다:
private fun buildDecoratedService(): BridgeService =
serviceDecorators
.foldRight(BaseService(objectMapper) as BridgeService) { service, acc ->
service.wrap(acc)
}
이와 같이 접기 연산을 통해 등록된 모든 데코레이터를 기본 서비스에 순차적으로 적용합니다.
5. 인터셉터 등록 및 사용
인터셉터는 BridgeRouter를 구성할 때 등록할 수 있습니다:
val router = BridgeRouter.builder()
.apply {
setSerializer(objectMapper)
registerController("api/v1/users", userController)
registerDecorator(TestInterceptor())
registerDecorator(TestInterceptor2())
registerDecorator(MeasureDecorator())
}.build()
6. 테스트 및 검증
인터셉터의 동작을 검증하기 위한 테스트 코드입니다:
class InterceptorTest : StringSpec({
"GET: /api/v1/test/interceptor" {
router.routingRequest("api/v1/test/interceptor", MethodType.GET) shouldBe """
"Intercepted by TestInterceptor1 and not reach controller"
""".trimIndent()
}
"POST: /api/v1/test/interceptor" {
router.routingRequest("api/v1/test/interceptor", MethodType.POST) shouldBe """
"Intercepted by TestInterceptor2 and not reach controller"
""".trimIndent()
}
})
7. 실제 사용 사례
이러한 인터셉터 구조는 다음과 같은 실제 사용 사례에 매우 유용합니다:
- 성능 모니터링: 요청 처리 시간 측정 및 로깅
- 인증/인가: 요청의 인증 상태 확인
- 로깅: 요청/응답 정보의 상세 로깅
- 캐싱: 응답 캐싱 및 캐시 히트율 모니터링
- 요청/응답 변환: 특정 형식으로의 데이터 변환
8. 마치며
데코레이터 패턴을 활용한 인터셉터 구현은 다음과 같은 이점을 제공했습니다:
- 유연성: 새로운 인터셉터를 쉽게 추가하고 조합할 수 있습니다.
- 재사용성: 인터셉터를 독립적인 모듈로 관리할 수 있습니다.
- 테스트 용이성: 각 인터셉터를 독립적으로 테스트할 수 있습니다.
- 관심사의 분리: 각 인터셉터가 특정 책임만을 담당합니다.
다음 글에서는 아마 제가 입사후 세 번째로 제작한 라이브러리 제작기를 다룰 것 같습니다.
DDL(데이터 정의어)을 RDBMS 방언에 상관없이 타입 안전하게 생성하도록 도와주는, DSL(도메인 특화 언어) 라이브러리 입니다.
길었던 글 읽어주셔서 감사합니다!
+ 데코레이터 패턴을 이해하고 적용하는데 GOAT 인파님의 데코레이터 패턴 관련 포스팅이 정말 큰 도움이 되었습니다. 정말 감사합니다!
link1. 데코레이터 패턴을 정말 최고로 잘 정리해주신 인파님의 포스팅:
💠 데코레이터(Decorator) 패턴 - 완벽 마스터하기
Decorator Pattern 데코레이터 패턴(Decorator Pattern)은 대상 객체에 대한 기능 확장이나 변경이 필요할때 객체의 결합을 통해 서브클래싱 대신 쓸수 있는 유연한 대안 구조 패턴이다. Decorator을 해석하
inpa.tistory.com
link2. BridgeApi 라이브러리 레포지토리: https://github.com/shiniseong/bridge-api
GitHub - shiniseong/bridge-api: bridge-api
bridge-api. Contribute to shiniseong/bridge-api development by creating an account on GitHub.
github.com