LINE Corporation이 2023년 10월 1일부로 LY Corporation이 되었습니다. LY Corporation의 새로운 기술 블로그를 소개합니다. LY Corporation Tech Blog

Blog


LINE 개발자들이 Spring 대신 Armeria를 사용하는 이유

LINE DEV Meetup #11 'LINE 서버 개발자들이 말한다! Armeria 아직도 안 써요?'에서 김기환, 임경수 님이 발표하신 'Hello Armeria, Bye Spring' 세션 내용을 옮긴 글입니다.

안녕하세요. 이번 글에서는 레거시 Spring 프로젝트 애플리케이션을 Armeria로 마이그레이션하면서 얻었던 인사이트를 공유하려고 합니다. 먼저 Armeria로 마이그레이션하게 된 동기를 말씀드리고 이어서 어떻게 마이그레이션 작업을 진행했고 그 과정에서 무엇을 얻었는지 말씀드리겠습니다. 앞서 이희승 님이 Armeria를 소개합니다라는 글에서 Armeria는 'at your pace'를 지향한다고 말씀해 주셨습니다. Armeria로 마이그레이션하는 것 또한 조금씩 나눠서 점진적으로, 자신의 페이스대로 진행하는 것이 가능합니다. 저희는 세 단계로 나눠서 진행했는데요. 각 단계별로 어떤 작업을 진행했고 그 작업을 통해 무엇을 얻었는지 말씀드리겠습니다. 그리고 마지막으로 Armeria로 마이그레이션한 뒤 얻었던 여러 이점 중에서 꼭 언급하고 싶은 성능 관점에서의 이점을 말씀드리겠습니다.

 

Armeria로 마이그레이션한 이유

Armeria로 마이그레이션한 동기를 말씀드리기 전에 마이그레이션 대상 컴포넌트를 먼저 소개하겠습니다. 저희는 현재 LINE에서 API 게이트웨이 역할을 맡고 있는 Channel Gateway라는 컴포넌트를 개발하고 있습니다. LINE의 여러 패밀리 서비스와 서드파티 앱들이 LINE에서 리소스를 가져가고 싶을 때 반드시 거쳐야 하는 컴포넌트입니다. LINE에 존재하는 여러 리소스를 다양한 형태로 가공해서 클라이언트로 보내야 하는 역할을 맡고 있으며, 그에 따라 굉장히 많은 업스트림 서비스가 Channel Gateway 서버에 연결돼 있습니다. 규모 측면에서 하루에 약 80억 건 정도의 요청이 들어오는, 대규모 트래픽을 소화하고 있는 서버입니다. 

Channel Gateway는 Armeria를 도입하기 전에는 전형적인 Spring MVC 애플리케이션으로 구성돼 있었습니다. 서버에서의 네트워크는 모두 Tomcat으로 처리하고, 업스트림과의 통신은 Apache HTTPClient를 사용해서 동기 방식으로 통신하는 구성이었는데요. 이와 같은 구성에 기인하는 전형적인 문제를 겪고 있었습니다. 앞서 업스트림 서비스가 굉장히 많이 붙어 있다고 말씀드렸는데 그중 어떤 하나의 업스트림에 장애가 발생해서 응답 지연 시간이 길어지는 상황이 발생했을 때, 그로부터 파생된 장애가 전체 서버 시스템을 마비시키기까지 그리 오랜 시간이 걸리지 않는다는 문제였습니다. 아래 슬라이드 하단의 지표를 보시겠습니다. 장애가 발생한 업스트림의 응답을 Tomcat 스레드가 계속 기다리면서 대기하다가, 종국에는 Tomcat 스레드 풀의 모든 스레드가 해당 업스트림의 응답을 기다리게 되면서 더 이상 요청을 받을 수 없는 상태에 빠지는 모습입니다. 이와 같은 일이 굉장히 빈번히 발생했습니다.

이에 Armeria 적용 전에는 이런 전면 장애 상황에 빠지는 것을 막기 위해 우회 정책을 적용하기도 했습니다. 대표적으로 서버를 여러 그룹으로 나눠 일부 서버는 특정 API만 서빙하는 방식으로 문제를 일부 지역으로 격리하려고 시도했습니다. 하지만 이런 정책은 리소스 관리 측면에서 그렇게 좋은 방법이 아니라고 판단했고, Armeria를 통해서 한 번 해결해 보려고 마이그레이션을 준비하게 됐습니다.

Armeria는 Tomcat과는 다르게, Spring MVC와는 다르게 비동기 서버와 클라이언트를 제공하고 있고, 클라이언트에도 마이크로서비스 패턴이 구현된 여러 데코레이터를 제공하고 있습니다. 이런 것들을 이용해 장애가 전파되는 것을 효율적으로 방지하고자 했습니다. 

 

단계별 마이그레이션 과정

마이그레이션이라고 하면 그 자체로 굉장히 어려운 작업이라고 생각하시는 분들이 많을 것 같습니다. 물론 특정 단계에서의 마이그레이션은 어려운 게 맞습니다. 하지만, 앞서 마이그레이션을 세 단계로 나눠볼 수 있다고 말씀드렸는데요. 그중에서 앞 두 단계는 아주 쉽게, Spring 애플리케이션의 레거시 코드를 별로 변경하지 않거나 혹은 단 한 줄도 변경하지 않고 Armeria의 클라이언트로 대체할 수 있고, 네트워크 레이어 역시 Armeria로 대체할 수 있습니다.

이와 같이 단계적으로 쉽고 편하게 시작할 수 있도록 Armeria에서 많은 것을 제공하고 있는데요. 그럼 각 단계별로 어떤 작업을 했는지와 어떤 이점을 얻을 수 있었는지 말씀드리겠습니다.

 

Quick-win 단계

먼저 Quick-win 단계라고 이름을 붙인 첫 번째 단계입니다. Spring 부트 애플리케이션에 Armeria의 클라이언트만 추가로 도입해서 사용하는 구조인데요. 마이그레이션에 들이는 노력에 비해 얻을 수 있는 이점이 굉장히 많기 때문에 지금 바로 여러분의 레거시 코드에 한 번 적용해 보셔도 좋을 것 같다는 의미에서 Quick-win이라고 이름을 지어봤습니다. 이 단계에서 작업해야 하는 내용은 아주 간단합니다. 우선 사용하는 빌드 툴에 Armeria의 코어 디펜던시를 추가하고, Spring 애플리케이션에서 하는 것처럼 Armeria의 WebClient를 Bean으로 정의한 다음, 필요한 서비스 레이어에 이 클라이언트를 주입해서 사용하면 됩니다.

코드를 보면 데코레이터가 많이 붙어 있습니다. 전부 마이크로서비스 패턴이 적용된 데코레이터입니다. 이렇게만 마이그레이션해도 마이크로서비스 패턴에서 서비스의 회복력을 조금 키울 수 있는 Failsafe 재시도나 서킷 브레이커, 헬스 체크가 동반된 클라이언트 측 로드 밸런싱 같은 것들이 포함된 클라이언트를 사용할 수 있습니다. 이에 이 단계부터 마이그레이션을 시작했고, 이 정도 마이그레이션만으로도 한 업스트림의 장애가 전체 시스템의 장애로 발전하는 것을 상당히 줄일 수 있었습니다.

 

Must-have 단계

다음 단계는 Must-have 단계입니다. Armeria의 클라이언트를 도입하는 것을 넘어서 기존에 Tomcat이 담당하고 있던 네트워크 레이어를 모두 Armeria로 대체하는 구성입니다. 서버의 네트워크 레이어까지 모두 Armeria가 담당하는 단계인데요. 여기까지도 마이그레이션에 필요한 노력이 굉장히 적기 때문에 만약 마이그레이션을 생각하고 계신다면 여기까지는 꼭 진행하셔야 한다는 의미로 Must-have라는 이름을 붙였습니다. 진행해야 하는 작업들은 앞선 단계와 같이 아주 간단합니다. 먼저 Armeria Tomcat이라는 디펜던시를 빌드 툴에 추가하고 아래와 같이 Bean을 정의하면 마이그레이션이 완료됩니다.

이 코드는 예시 코드로 따로 작성한 게 아니라 정말 프로덕션 코드에 존재하는 코드입니다. 이 10줄 남짓한 코드로 네트워크 레이어를 Armeria로 마이그레이션하는 작업이 완료됩니다.

이 단계까지 마이그레이션을 진행하면 여러 이점을 얻을 수 있습니다. 먼저 기존에 Spring MVC에서 서빙하던 REST 서비스와 동일한 포트에 Tomcat의 오버헤드 없이 gRPC나 Thrift 같은 RPC 프로토콜을 지원하는 서비스를 완벽한 비동기 방식으로 추가하는 것이 가능합니다. 또한 서버의 프로토콜 측면에서도 강점이 생깁니다. 물론 요즘에는 Tomcat도 TLS HTTP/2를 지원하지만, Armeria를 적용하면 추가로 평문(clear text) TCP 연결에서도 HTTP/2로 통신할 수 있는 서버를 만들 수 있어서 튜닝해야 하는 지점이 다소 줄어드는 효과를 얻을 수 있습니다. 그리고 마지막이 무엇보다도 가장 중요한 장점인데요. 아까도 말씀드렸지만 레거시 코드를 단 한 줄도 변경할 필요 없이 이런 장점들을 모두 누릴 수 있습니다.

 

Final Goal 단계

마지막 단계는 Final Goal이라고 이름을 지어봤습니다. Armeria만 사용하는 구성인데요. Spring을 완전히 제거하는 것은 아니고 라우터 레벨, Spring으로 따지면 인터셉터 혹은 필터와 같은 것들을 모두 Armeria 데코레이터로 대체하고, 컨트롤러도 Armeria의 AnnotatedService로 대체하는 구성입니다. Spring은 DI(dependency injection) 컨테이너로써의 강점이 있기 때문에 이 역할을 담당하는 Spring 디펜던시는 남겨두고 Armeria를 사용하는 구성입니다. 아래 예제 코드를 보시겠습니다. 이번 단계에서는 마이그레이션이 다소 복잡하기 때문에 코드를 조금 간단하게 변경해서 가져왔습니다.

위와 같은 Spring 컨트롤러를 아래와 같은 Armeria의 AnnotatedService로 바꿀 수 있습니다.

위 코드와 같이 Armeria도 annotation 기반으로 라우터를 정의할 수 있는 강력한 기능을 제공하고 있습니다. 앞선 Spring 코드와 비교해 보면 형태가 많이 다르지 않기 때문에 Spring 코드를 작성하듯 리팩토링할 수 있습니다. 위와 같이 정의한 AnnotatedService를 Armeria 서버 Configurator에 등록하는 것으로 마이그레이션이 끝납니다.

이 단계까지 마이그레이션하면 하나의 요청을 처리하는 전체 라이프 사이클을 Armeria로 완벽하게 리액티브하고 비동기적인 방식으로 처리할 수 있습니다.

다음으로 Armeria의 또 하나의 장점인데요. JVM 환경에서 구현된 거의 모든 비동기 기술을 지원합니다. 따라서 Kotlin 코루틴이나 Reactive Streams의 구현체인 RxJava나 Reactor, CompletableFuture, Scala Future 등 본인이 원하는 비동기 기술을 이용해 비동기 애플리케이션을 구현할 수 있습니다.

또한 제목을 'Bye Spring'이라고 하긴 했지만 앞서 DI 컨테이너 역할로써의 강점을 말씀드렸듯 아직 Spring의 역할이 남아 있습니다. 따라서 Spring의 자동 설정을 기반으로 하는 생태계, '~starters'라는 이름의 디펜던시들을 별도의 변경 없이 그대로 사용할 수 있습니다.

이와 같이 마이그레이션을 통해 Spring의 강점인 자동 설정 기반의 생태계와 Armeria의 강점인 비동기 서버, 클라이언트, 마이크로 서비스 패턴의 구현체 등 각각의 장점만 쏙쏙 뽑아서 사용할 수 있으며, 현재 새로운 컴포넌트도 이런 구성을 염두에 두고 설계 및 개발하고 있습니다.

 

마이그레이션 과정에서의 챌린지

앞서 마지막 마이그레이션 단계는 그렇게 쉽지만은 않다고 말씀드렸습니다. 마지막 단계에서 여러 장애물에 부딪칠 수 있는데요. 그중에서 몇 가지 중요한 챌린지를 뽑아서 설명하겠습니다.

먼저 애플리케이션을 Spring에서 Armeria로 마이그레이션했다면 대부분의 애플리케이션 레이어가 블러킹 코드로 구성돼 있으실 겁니다. 그런데 Armeria는 제한된 개수의 스레드 풀을 위해서 이벤트 루프를 기반으로 비동기 메커니즘을 구현한 비동기 서버이기 때문에 레거시 코드의 블러킹 코드가 실행되면 블로킹 코드가 I/O를 대기하는 시간만큼 막혀 있게 됩니다. 그렇게 되면 해당 이벤트 루프가 더 이상 요청를 받을 수 없게 되는 문제점이 있습니다. 그래서 레거시 코드를 Armeria에서 실행할 때는 항상 레거시 코드의 블로킹 코드가 어떤 스레드에서 실행되는지, 혹시 이벤트 루프 스레드에서 실행되지는 않는지 항상 염두에 두면서 개발해야 합니다.

Armeria에서는 이런 문제를 방지하기 위해서 @Blocking과 같이 블로킹 태스크를 애초에 블로킹 태스크 익스큐터에서 실행할 수 있는 방법을 제공하고 있습니다.

다음 역시 Armeria의 비동기 특성과 관련된 부분인데요. 기존의 블로킹 방식으로 작동하는 애플리케이션에서는 요청 컨텍스트를 관리할 때 대부분 ThreadlLocal에 의존합니다. 그런데 Armeria에서는 요청 하나가 여러 개의 스레드를 옮겨가면서 처리되다 보니 ThreadlLocal에 의존하는 방식의 요청 컨텍스트 관리가 불가능합니다. 물론 내부적으로는 ThreadlLocal을 사용해서 관리하긴 하는데요. 아래 예제 코드와 같이 요청을 처리하는 과정에서 스레드가 한 번 바뀌는 부분이 있다면 요청 컨텍스트를 수동으로 밀어 넣어주는 것과 같은 작업이 필요합니다.

이런 수동 작업이 너무 번거롭기 때문에 Armeria에서는 JVM 환경에서 구현된 비동기 기술에 대해서 RequestContextHooks라는 것을 제공합니다. 수동으로 요청 컨텍스트를 밀어 넣는 것보다는 RequestContextHooks를 사용해서 편하게 개발하는 게 좋을 것 같은데요. 핵심은 이런 요청 컨텍스트 관리에 대해서도 생각을 하면서 개발해야 보다 안정적으로 애플리케이션을 개발할 수 있다는 것입니다.

이와 같은 허들에도 불구하고 Armeria를 도입한 뒤 성능 관점에서 이점이 굉장히 컸습니다. 과연 어떤 이점이 있었는지 살펴보겠습니다.

 

마이그레이션 후 성능 변화

이제 Spring 서블릿에서 Armeria로 넘어가면서 얻을 수 있었던 성능 향상 사례를 소개하겠습니다. Channel Gateway에서 제공하는 API 중에 이미지나 비디오와 같은 콘텐츠를 다운로드할 수 있는 API가 있습니다. 그냥 다운로드만 하는 게 아니라 적절한 권한이 있는지 검사도 하고 다운로드하는 동안 다른 서버에 호출도 보내는 등 다른 비즈니스 로직이 추가된 API였습니다. 이에 따라 단순 파일 서버로는 제공하지 못하고 웹 애플리케이션 서버에서 제공하고 있었는데요. 애플리케이션 서버에서 파일을 제공하다 보면 종종 문제가 발생합니다(이 문제에 관해서는 추후 다시 살펴보겠습니다).

이 문제를 해결하기 위해서 NGINX와 OpenResty 도입을 시도해 봤습니다. OpenResty는 NGINX의 Lua 스크립트를 사용할 수 있게 해주는 모듈로 추가 비즈니스 로직을 구현하기 위해서 사용했습니다. 그런데 Lua 스크립트를 사용한다는 것 자체가 큰 단점으로 작용했습니다. 팀에서는 Java나 Kotlin 같은 JVM 기반 언어들을 사용하고 있었는데 이 API 하나를 위해 추가로 Lua 스크립트를 학습해야 했기 때문입니다. 물론 Lua는 간단하면서 상당히 직관적인 언어라서 학습 비용이 그렇게 크진 않을 수 있겠지만, 이미 기존 코드가 전부 Java 기반으로 작성돼 있어서 OpenResty에서 사용해야 하는 공통 기능을 자바에서 Lua로 다시 작성해야 했고, NGINX에서 잘 작동하는지 테스트도 해야 했습니다. 마음을 먹는다면 어떻게든 할 수 있었겠지만 앞으로도 꾸준히 이 모든 것들을 신경 쓰면서 유지 보수해야 한다는 사실 때문에 조심스러웠습니다.

다행히 Armeria로의 전환이 성공하면서 Armeria의 힘을 사용할 수 있게 됐습니다. 앞서 말씀드린 것처럼 Armeria Tomcat을 사용해서 Spring 서블릿을 Armeria 환경에서 기동시키고 있기 때문에 방금 말씀드렸던 콘텐츠 다운로드 API와 같이 고성능이 필요한 API는 Armeria만 사용하도록 구현하고, 그 외 기존 레거시 API는 Tomcat을 사용하도록 설정할 수 있었습니다. 이와 같이 빅뱅 방식으로 모든 시스템을 바꾸지 않고 필요한 기능부터 하나씩 하나씩 바꿔 나갈 수 있다는 점이 Armeria의 큰 장점이라고 할 수 있겠습니다.

결론을 먼저 말씀드리자면, Spring MVC에서 Armeria로 변경했더니 지연 시간이 약 25% 감소했습니다. 아래 슬라이드 하단 그래프는 콘텐츠 다운로드 API의 마이그레이션 전후 지연 시간을 백분위수(percentile)로 취합한 그래프입니다.

백분위수가 50, 즉 중간 값은 Spring일 때는 0.8초에서 0.9초 사이에 위치했는데요. Armeria로 변경한 후엔 0.6초에서 0.7초 사이에 위치했습니다. 약 200ms 더 빨라졌습니다. 백분위수가 90일 때도 1.4초에서 1.5초 사이였던 응답 시간이 Armeria로 변경 후 약 1.2초에서 1.3초 사이로 줄어들었습니다. 역시 약 200ms 더 빨라졌습니다. 이번 마이그레이션 작업으로 NGINX를 사용했을 때보다도, 심지어 Spring MVC를 사용했을 때보다도 더 간단하고 짧은 코드로 성능을 향상할 수 있었는데요. 어떻게 이런 성능 향상을 얻을 수 있었는지 살펴보겠습니다.

 

Armeria를 이용한 성능 향상 방안

아래 코드는 블로킹 서버를 개발하시는 분들이라면 흔히 볼 수 있는 코드로 업스트림에서 무언가를 다운로드해서 메모리에 적재한 뒤에 다시 응답으로 만들어 클라이언트로 보내는 코드입니다. 크기가 큰 파일을 서빙하는 API도 이와 크게 다르지 않습니다. 이전에 제공하던 API도 마이그레이션 전에는 이런 방식으로 구현돼 있었는데요. 만약 이런 방식으로 구현된 서버에 많은 요청이 들어오면 어떻게 될까요? 혹은 요청이 한 번에 폭발적으로 들어오는 상황이 발생하면 어떻게 될까요? 혹은 요청을 처리하는 데 시간이 오래 걸리면 어떻게 될까요?

서블릿은 한 요청당 하나의 전용 스레드를 할당해야 하기 때문에 저는 제일 먼저 스레드 풀 고갈 문제가 걱정됩니다. 요청이 많아지거나 한 요청을 처리하는 데 많은 시간이 걸리면 특정 시점에 실행 중인 요청이 많아지면서 실행 중인 스레드도 덩달아 많아집니다. 이에 따라 가용 가능한 모든 스레드를 실행 중인 요청이 점령하면서 새로운 요청을 받을 수가 없어 해당 서버는 결국 응답 불능 상태에 빠집니다. 또한 스레드가 많아지더라도 서버의 코어 수는 그대로이기 때문에 증가한 스레드를 동시에 실행하기 위해서 컨텍스트 스위칭이 빈번히 발생하며 이 때문에 성능이 저하됩니다. 게다가 스레드는 공짜가 아닙니다. 스레드를 생성하고 유지하기 위해서도 또 메모리가 필요합니다. 또한 비단 스레드 때문이 아니더라도 OutOfMemoryException이 발생할 수 있는 경우가 있는데요. 응답을 보내기 전에 업스트림에서 받은 데이터를 메모리에 적재하고 있어야 하기 때문입니다. 물론 파일을 버퍼로 사용하는 등의 추가 테크닉을 적용할 수는 있겠지만, 다들 아시다시피 디스크는 느리기 때문에 트래픽을 안정적으로 처리하기 위해 성능 손실을 감수해야 하는 구조가 돼 버립니다.

이때 Armeria라는 해결사가 나타났습니다. Armeria는 JVM 환경에서 작동하기 때문에 기존 Java 코드를 그대로 이용할 수 있습니다. 또한 완벽한 비동기 방식이고 리액티브하기 때문에 블로킹 서버에서 발생했던 문제들이 발생하지 않습니다. 비동기 프레임워크이기 때문에 더 이상 요청마다 전용 스레드가 하나씩 필요하지 않습니다. 만약 어떤 요청을 처리하다가 I/O가 발생하면, I/O가 완료될 때까지 해당 스레드가 대기하는 게 아니라 다른 요청을 처리할 수 있습니다. 이를 통해 적은 수의 스레드로도 효율을 극대화할 수 있고 컨텍스트 스위칭에 필요한 비용도 아낄 수 있습니다.

스레드 고갈 문제는 이와 같이 비동기 방식으로 해결했고, 남은 OutOfMemoryException 문제는 리액티브로 해결했습니다. 리액티브는 다소 생소할 수 있는 개념인데요. 업스트림에서 전송받은 데이터를 작은 청크 형태로 나눠서 하나씩 하나씩 그대로 클라이언트에게 전달할 수 있게 해주는 개념입니다. 블로킹 서버에서는 이 청크들을 한 곳에 모은 뒤 클라이언트로 보내지만 리액티브에서는 하나씩 하나씩 그대로 보낼 수 있습니다. HTTP 요청이 큰 페이로드(payload)를 갖고 있더라도 네트워크 레이어까지 내려가서 보면 작은 청크 형태로 나뉘어 전달되는데요. 블로킹에서는 이것들을 다 모아야 했지만 리액티브는 모을 필요가 없어지는 것이죠. 따라서 아무리 큰 페이로드라도 데이터를 전부 힙 메모리에 복사할 필요가 없기 때문에 메모리 걱정을 할 필요가 없습니다.

아래 그림은 reactive의 작동 방식을 도식화한 것입니다.

위쪽(Traditional)이 기존 블로킹 서버에서 작동하던 방식입니다. 데이터('DATA')를 모두 메모리에 적재했다가 한 번에 클라이언트로 전송하는 방식입니다. 반면 아래 리액티브에서는 'D' 한 번, 'A' 한 번, 'T' 한 번, 'A' 한 번, 이렇게 나눠서 클라이언트로 전송합니다. 전통적인 방식에서는 'DATA' 네 조각을 모두 메모리에 적재해야 했다면 리액티브에서는 한 조각만 메모리에 갖고 있으면 됩니다. 전체 페이로드와 비교하면 각 조각의 크기는 매우 작은데요. Armeria 기본 설정에서 HTTP/2일 때 16KB, HTTP/1일 때 8KB입니다. 최악의 경우를 가정하더라도 전통적인 방식에서는 모든 요청의 데이터를 모두 메모리에 적재하고 있어야 하는 반면, Armeria 서버에서는 현재 처리 중인 요청 수에 16KB를 곱한 양만큼만 메모리에 갖고 있어도 문제없이 처리할 수 있어서 더욱 안정적으로 서비스를 제공할 수 있습니다. 여기에 앞서 말씀드렸던 비동기까지 같이 적용돼 있으니 적은 서버로도 많은 요청을 처리할 수 있습니다.

비동기와 리액티브에 관한 설명을 듣고 나면 구현이 아주 복잡할 것 같지만, Armeria는 직관적인 인터페이스를 갖추고 있어서 걱정할 필요가 없습니다. 아래 코드가 앞서 설명한 전부가 녹아 있는 코드입니다.

얼핏 보면 블로킹 방식일 때와 크게 다를 바가 없는 것 같지만 내부는 완전히 다릅니다. HttpResponse는 사실 아무 데이터도 가지고 있지 않은 껍데기입니다. 위 코드와 같이 Armeria 서비스의 리턴 값으로 HttpResponse를 연결해 놓으면 Armeria가 내부적으로 'https://example.com'의 응답을 클라이언트에게 리액티브하게 전달합니다. HTTP 데이터의 파이프라인이나 스트림이라고 생각하면 좀 더 이해하기 편하실 것 같습니다. 여기에 mapHeaders 같은 메서드를 추가로 이용하면 응답의 헤더를 변형하는 기능들도 사용할 수 있습니다. 위 코드는 저희가 마이그레이션 후에 사용하는 프로덕션 코드와 거의 같습니다. 정말 간단하지 않나요?

업로드도 다운로드처럼 매우 간단합니다. 받은 요청을 업로드 클라이언트에 연결하면 리액티브하게 전달합니다. 물론 응답처럼 요청의 헤더를 변경하는 것도 당연히 가능합니다.

 

마이그레이션 이후 지표 변화

지금까지 Armeria로 마이그레이션을 진행하면서 어떻게 성능이 향상됐는지 말씀드렸습니다. 아래 그래프는 마이그레이션한 날에 관찰한 지표 변화입니다.

마이그레이션을 시작하면서 응답 시간이 증가했는데요. 이는 크기가 큰 파일이 다운로드되면 자연스럽게 발생하는 현상입니다. 파일을 다운로드하니 그만큼 많은 시간이 걸리는 것이죠. 그럼에도 CPU 사용량은 별다른 변화가 없는데요. 대부분 I/O 처리가 주를 이루기 때문입니다. 8GB의 작은 힙 메모리이고 대용량 데이터를 서빙하는 서버임에도 굉장히 안정적으로 처리하고 있습니다. 마이그레이션은 올해 2월 말에 진행했는데요. 그 후로도 문제없이 잘 작동하고 있습니다. 

 

Q&A

Q: 마이그레이션을 위한 스텝 바이 스텝 문서가 따로 있을까요? 

A: 저희가 처음 진행할 때에는 따로 문서는 없었습니다. 그냥 직접 겪어보면서 진행했는데요. 이번에 경수 님이 발표하시면서 마이그레이션을 위한 스텝 바이 스텝 문서가 하나 생긴 것 같습니다. 혹시 다음에 진행하게 되신다면 이 발표 자료를 이용해 주시면 감사하겠습니다. 

추가로 답변드리자면, Armeria 리포지터리에 가보시면 샘플 예제 코드가 굉장히 많이 있습니다. Tomcat과 통합할 때 어떻게 해야 하는지, WebFlux와 통합할 때 어떻게 해야 하는지와 같은 것들이 예제 코드로 상세하게 설명돼 있습니다. 각자의 상황에 맞게 이런 코드를 참고하고 활용하면서 마이그레이션을 진행하시면 될 것 같습니다. 

 

Q: 기존의 ThreadLocal 기반의 트랜잭션이나 보안 등 Armeria 기반으로 전환할 때 고려하거나 참고할 만한 내용이 있을까요?

A: Spring Security나 ThreadLocal 기반으로 트랜잭션을 관리하는 JDBC 기반의 트랜잭션 기술 등은 Armeria를 사용하면 잘 작동하지 않습니다. 별도로 처리해야 합니다.

먼저 Spring Security에 돼 있는 인증 레이어 같은 경우에는, 만약 Armeria로 전부 마이그레이션해야 한다면 Armeria의 데코레이터로 다시 구현하는 게 좀 더 깔끔한 방향이라고 생각합니다. 그런데 앞서 언급했지만 Armeria는 RequestContextHooks와 같은 것을 제공하기 때문에 그런 훅을 이용하면 ThreadLocal에 있는 Spring RequestContext에 접근하듯 어디에서나 접근할 수 있습니다. 그곳의 인증 컨텍스트를 가져와서 사용하는 것과 같은 방식으로 인증과 권한 관리를 구현해야 할 것 같습니다. 'Armeria로 서버 간 인증 마이크로서비스 개발하기'에서 김도한 님이 보여주신 Doorkeeper 구현에도 그와 같은 부분들이 있어서 참고하시면 좋을 것 같습니다.

다음으로 트랜잭션과 관련해서는 어떤 기술을 사용하느냐에 따라 다른데요. 만약 JDBC를 사용하신다면 JDBC 커넥션으로 작업하는 부분을 하나의 스레드에 격리해서 진행하는 것과 같은 우회 정책이 필요할 것 같습니다. 혹은 만약 R2DBC와 같은 좀 더 최신 기술을 사용하신다면 Spring에서 R2DBC와 통합할 수 있는 방법을 제공하고 있는데요. 트랜잭션 annotation만 붙여도 트랜잭션이 가능한 것들이 있습니다. 이와 같은 방식으로 해결해야 할 것 같습니다.

 

Q: Armeria의 이벤트 루프는 자체 구현인지 다른 라이브러리를 사용하는 것인지 궁금합니다.

A: Netty의 이벤트 루프를 사용하고 있습니다. 

 

Q: 전환 과정에서 고생하셨거나 신경 써야 할 부분은 없었을까요?

A: 우선 실제로 전환 자체는 그렇게 어렵지 않았습니다. 기존에 Tomcat 기반의 Spring 서블릿을 사용하고 있었기에 Armeria Tomcat만 사용하면 바로 Armeria 기반 서버로 움직일 수 있었습니다. 따라서 코드를 개발하고 구현하는 데 있어서는 사실 큰 문제가 없었습니다만, 아무래도 Armeria로 변경하면 서버 레이어 자체를 바꾸는 것이기 때문에 Tomcat에서 사용하고 있던 설정 같은 것들을 Armeria로 잘 옮겨오는 작업이 필요했습니다. 아무래도 다른 기종이기 때문에 설정도 다 다를 것이라서 이런 부분들을 잘 맞춰서 가져오는 부분에는 꼭 신경을 써야 할 것 같습니다.

 

Q: GraalVM에서 실행 가능할까요?

A: GraalVM은 아직 공식적으로 지원할 예정은 없습니다. 

 

Q: MariaDB와 같이 비동기를 지원하지 않는 저장소의 프로젝트에서는 블록으로 작동할 텐데요. 이런 경우에도 Armeria로 마이그레이션할 만한 장점이 있을까요?

A: 실제로 말씀해 주신 것과 같은 구성이 LINE에도 많이 존재합니다. MySQL이나 MariaDB 등 R2DBC 드라이버 지원이 잘 되지 않는 DB를 사용할 때 이런 문제와 맞닥뜨리게 되는데요. 그럼에도 Armeria를 써 볼 만한 장점이 충분히 있다고 생각합니다. 앞서 언급한 것처럼 클라이언트 단에서 강력한 데코레이터를 지원하고 있고 또한 네트워크 단 처리도 훨씬 효율적으로 진행하기 때문입니다. 커넥션 관리나 앞서 말씀드렸던 스트리밍 방식으로 대용량 페이로드를 보내야 하는 상황 등을 생각해 보면 도입할 메리트는 충분하다고 생각합니다. 물론 위와 같은 JDBC 드라이버를 사용할 때에는 좀 더 조심해서 사용해야 하는 건 맞습니다. 하지만, 스레드 풀을 격리해 DB와 분리하면 전체적으로 비동기 서버가 작동하는 데 있어서 특별한 문제는 없을 것이라고 생각합니다. 

또한 Armeria는 리액티브뿐 아니라 통합을 또 하나의 강점으로 내세우고 있습니다. 전체는 블로킹 서버로 작동시키더라도 그 외 추가할 수 있는 여러 가지 데코레이터들이 많이 있어서 이것들만 사용하셔도 충분히 장점을 얻으실 수 있을 것이라고 생각합니다.

 

마치며

이번 글에서는 레거시 Spring 프로젝트 애플리케이션을 Armeria로 마이그레이션하면서 얻었던 인사이트와 이를 통해 어떻게, 얼마나 성능이 향상됐는지 공유했습니다. Armeria로의 마이그레이션을 꼭 한 번 고려해 보시라고 말씀드리면서 글을 마치려고 합니다. 긴 글 읽어주셔서 감사합니다.