Armeria — Netty 기반 마이크로서비스 프레임워크
Netty로 HTTP/2 서버도 만들고 gRPC도 붙여봤다. 그런데 실무에서는 하나의 서버에 gRPC, Thrift, REST API를 모두 올려야 할 때가 있다. 매번 Netty 파이프라인을 직접 구성하는 건 너무 저수준이지 않을까? Netty의 강력함을 유지하면서도 더 편하게 마이크로서비스를 만들 수 있는 방법은 없을까?
Armeria란 — LINE이 만든 비동기 마이크로서비스 프레임워크
Armeria는 LINE 에서 만든 오픈소스 비동기 RPC/REST 프레임워크입니다. 내부적으로 Netty 위에 구축되어 있고, 비동기 논블로킹을 기본으로 합니다.
공부하다 보니 Armeria의 정체성을 한 문장으로 정리하면 이렇습니다.
Netty의 저수준 네트워킹 위에 **마이크로서비스에 필요한 것들 **(서비스 라우팅, 직렬화, 로깅, 메트릭, 서비스 디스커버리)을 얹은 프레임워크
일반적인 HTTP 프레임워크(Spring MVC, Express.js)와 다른 점이 있습니다.
- ** 프로토콜 독립적 **: gRPC, Thrift, REST를 하나의 서버에서 동시에 지원합니다
- **HTTP/2 기본 **: HTTP/1.1이 아니라 HTTP/2를 기본 프로토콜로 사용합니다
- ** 비동기 우선 **:
CompletableFuture기반의 비동기 API가 기본입니다 - **Netty 네이티브 **: 내부적으로 Netty의 EventLoop, Channel, ByteBuf를 그대로 활용합니다
import com.linecorp.armeria.server.Server;
import com.linecorp.armeria.server.ServerBuilder;
// Armeria 서버 기본 구성 — 놀라울 정도로 간결하다
Server server = Server.builder()
.http(8080)
// REST 서비스
.annotatedService("/api", new MyRestService())
// gRPC 서비스
.service(GrpcService.builder()
.addService(new MyGrpcServiceImpl())
.build())
// Thrift 서비스
.service("/thrift", THttpService.of(new MyThriftHandler()))
.build();
// 서버 시작
server.start().join();
이 코드 한 장으로 REST, gRPC, Thrift가 ** 같은 포트 **에서 동시에 동작합니다. Netty로 직접 이걸 구현하려면 파이프라인 분기, 코덱 설정, 프로토콜 감지 로직을 전부 직접 짜야 합니다.
왜 Armeria인가 — 프로토콜 독립적 통합 서버
마이크로서비스 환경에서 흔히 겪는 문제가 있습니다.
- 레거시 서비스는 Thrift 를 쓰고 있다
- 새 서비스는 gRPC 로 만들고 싶다
- 프론트엔드에는 REST API 를 제공해야 한다
- 이걸 각각 다른 서버로 띄우면 관리 포인트가 3배로 늘어난다
Armeria는 이 문제를 하나의 서버, 하나의 포트 로 해결합니다.
프로토콜 라우팅의 원리
Armeria가 여러 프로토콜을 동시에 지원할 수 있는 비밀은 HTTP/2 위에서의 라우팅 입니다.
클라이언트 요청
│
▼
[ Armeria Server (Netty 기반) ]
│
├── Content-Type: application/grpc ──→ gRPC 서비스
├── Content-Type: application/x-thrift ──→ Thrift 서비스
└── 경로 패턴 매칭 (/api/**) ──→ REST 서비스
gRPC도 HTTP/2 위에서 동작하고, Thrift도 THttpService를 통해 HTTP 위에서 동작하므로, HTTP/2라는 공통 기반 위에서 Content-Type과 경로만으로 분기할 수 있습니다.
REST 서비스 — 어노테이션 기반
import com.linecorp.armeria.server.annotation.*;
public class MyRestService {
// GET /api/users/{id}
@Get("/users/{id}")
public HttpResponse getUser(@Param("id") long id) {
User user = userRepository.findById(id);
return HttpResponse.ofJson(user);
}
// POST /api/users
@Post("/users")
public HttpResponse createUser(CreateUserRequest request) {
// 비동기 처리도 자연스럽다
return HttpResponse.from(
userService.create(request)
.thenApply(user -> HttpResponse.ofJson(HttpStatus.CREATED, user))
);
}
}
Spring MVC와 비슷한 어노테이션 스타일이지만, 반환 타입이 CompletableFuture나 HttpResponse로 비동기를 자연스럽게 지원합니다.
Netty를 어떻게 활용하는가 — ServerBuilder 내부
Armeria를 쓰면서 Netty를 직접 만질 일이 거의 없습니다. 하지만 내부적으로는 Netty가 모든 것을 처리하고 있습니다.
ServerBuilder → Netty ServerBootstrap
Armeria의 Server.builder()가 내부적으로 하는 일을 풀어보면 이렇습니다.
Armeria ServerBuilder
│
▼
Netty ServerBootstrap 구성
├── bossGroup: NioEventLoopGroup (acceptor)
├── workerGroup: NioEventLoopGroup (I/O 처리)
├── channel: NioServerSocketChannel (또는 EpollServerSocketChannel)
└── childHandler (ChannelPipeline):
├── SslHandler (TLS가 설정된 경우)
├── Http2FrameCodec (HTTP/2 프레이밍)
├── Http2MultiplexHandler (스트림 멀티플렉싱)
└── Armeria 내부 핸들러 (라우팅, 서비스 디스패치)
직접 Netty 파이프라인을 구성할 때와 비교하면, 개발자가 해야 할 일이 크게 줄어듭니다.
| 직접 Netty 구성 | Armeria 사용 |
|---|---|
ServerBootstrap 생성 | Server.builder() |
EventLoopGroup 직접 생성·관리 | 자동 생성 (CPU 코어 기반) |
ChannelInitializer 작성 | 서비스 등록만 하면 자동 구성 |
| HTTP/2 코덱 직접 파이프라인에 추가 | 기본 내장 |
| Graceful Shutdown 직접 구현 | server.stop() 호출 한 번 |
Netty 설정 커스터마이징
그래도 Netty 수준의 튜닝이 필요할 때가 있습니다. Armeria는 이를 위한 탈출구를 제공합니다.
Server server = Server.builder()
.http(8080)
// Netty ChannelOption 직접 설정
.channelOption(ChannelOption.SO_BACKLOG, 1024)
.childChannelOption(ChannelOption.SO_KEEPALIVE, true)
.childChannelOption(ChannelOption.TCP_NODELAY, true)
// worker EventLoop 스레드 수 지정
.workerGroup(16)
// 최대 연결 수 제한
.maxNumConnections(10000)
.annotatedService("/api", new MyRestService())
.build();
Netty의 ServerBootstrap.option()이나 childOption()과 같은 역할을 합니다. 추상화의 이점을 누리면서도 저수준 튜닝을 포기하지 않는 설계입니다.
데코레이터 패턴 — Netty Pipeline의 서비스 레벨 추상화
Netty의 핵심 패턴이 ChannelPipeline에 핸들러를 체이닝 하는 것이었다면, Armeria의 핵심 패턴은 데코레이터 입니다.
ChannelPipeline vs 데코레이터
[ Netty ChannelPipeline ]
바이트 → SslHandler → Http2FrameCodec → Http2MultiplexHandler → 비즈니스 핸들러
[ Armeria 데코레이터 체인 ]
HttpRequest → LoggingService → AuthService → MetricCollecting → 비즈니스 서비스
두 개념은 같은 원리(체이닝)를 다른 추상화 레벨에서 적용한 것입니다.
- Netty Pipeline: 바이트 레벨에서 인코딩/디코딩/프레이밍을 처리합니다
- **Armeria 데코레이터 **: HTTP 요청/응답 레벨에서 로깅, 인증, 메트릭을 처리합니다
데코레이터 사용법
Server server = Server.builder()
.http(8080)
.annotatedService("/api", new MyRestService())
// 데코레이터 체이닝 — 안쪽에서 바깥쪽 순서로 감싼다
.decorator(LoggingService.newDecorator()) // 요청/응답 로깅
.decorator(MetricCollectingService.newDecorator( // 메트릭 수집
MeterIdPrefixFunction.ofDefault("my.service")))
.decorator(AuthService.newDecorator( // 인증
new MyAuthHandler()))
.build();
요청이 들어오면 데코레이터가 순서대로 실행됩니다. decorator() 호출 순서에 따라 ** 바깥쪽부터 안쪽으로** 감싸지므로, 위 코드에서 실행 순서는 다음과 같습니다.
요청 → Auth → Metric → Logging → 비즈니스 로직
응답 ← Auth ← Metric ← Logging ← 비즈니스 로직
커스텀 데코레이터 만들기
데코레이터를 직접 만드는 것도 간단합니다. Netty에서 ChannelDuplexHandler를 상속하는 것보다 훨씬 직관적입니다.
// 요청 처리 시간을 측정하는 커스텀 데코레이터
public class TimingDecorator implements DecoratingHttpServiceFunction {
@Override
public HttpResponse serve(HttpService delegate, ServiceRequestContext ctx,
HttpRequest req) throws Exception {
long start = System.nanoTime();
// 원본 서비스에 위임
HttpResponse response = delegate.serve(ctx, req);
// 응답이 완료되면 시간 측정
response.whenComplete().thenRun(() -> {
long elapsed = System.nanoTime() - start;
logger.info("{} {} took {}ms",
req.method(), req.path(),
TimeUnit.NANOSECONDS.toMillis(elapsed));
});
return response;
}
}
// 사용
Server.builder()
.annotatedService("/api", new MyRestService())
.decorator(TimingDecorator::new)
.build();
Netty에서는 channelRead와 write를 오버라이드하고 바이트 버퍼를 다뤄야 했지만, Armeria에서는 HttpRequest와 HttpResponse라는 높은 추상화에서 작업합니다.
HTTP/2 기본 — 왜 HTTP/2를 기본 프로토콜로 선택했는가
대부분의 프레임워크(Spring MVC, Express.js)는 HTTP/1.1이 기본이고 HTTP/2는 선택 사항입니다. Armeria는 반대입니다. HTTP/2가 기본 이고 HTTP/1.1은 호환성을 위해 지원합니다.
마이크로서비스에서 HTTP/2의 이점
마이크로서비스 환경에서 HTTP/2가 특히 유리한 이유가 있습니다.
- **멀티플렉싱 **: 서비스 간 통신에서 하나의 연결로 여러 요청을 동시에 보낼 수 있습니다. 연결 수가 줄어들어 리소스 사용이 효율적입니다
- ** 헤더 압축 **: 마이크로서비스 간 호출은 비슷한 헤더가 반복되므로 HPACK 압축 효과가 큽니다
- **gRPC 호환 **: gRPC가 HTTP/2 위에서 동작하므로 자연스럽게 통합됩니다
Armeria의 HTTP/2 처리 내부
Armeria 내부에서 HTTP/2는 Netty의 Http2FrameCodec과 Http2MultiplexHandler를 통해 처리됩니다.
클라이언트 연결
│
▼
[ TLS + ALPN 협상 (h2 선택) ]
│
▼
[ Netty Http2FrameCodec ] ← 바이너리 프레임 파싱
│
▼
[ Netty Http2MultiplexHandler ] ← 스트림별 자식 Channel 생성
│
▼
[ Armeria HttpServerHandler ] ← 서비스 라우팅 및 디스패치
TLS가 없는 환경(개발·테스트)에서는 h2c(HTTP/2 over cleartext) 도 지원합니다. Armeria는 연결 첫 바이트를 보고 HTTP/2 프리페이스(PRI * HTTP/2.0)인지 HTTP/1.1인지 자동 감지합니다.
Server server = Server.builder()
.http(8080) // h2c + HTTP/1.1 자동 감지
.https(8443) // h2 (TLS + ALPN)
.tlsSelfSigned() // 개발용 자체 서명 인증서
.annotatedService("/api", new MyRestService())
.build();
서비스 디스커버리 — 클라이언트 사이드 로드밸런싱
마이크로서비스에서 서비스 디스커버리는 필수입니다. Armeria는 클라이언트 사이드 로드밸런싱 을 기본으로 제공합니다.
EndpointGroup
EndpointGroup은 여러 서버 엔드포인트를 하나의 그룹으로 묶는 개념입니다.
// 정적 엔드포인트 그룹
EndpointGroup staticGroup = EndpointGroup.of(
Endpoint.of("server1.example.com", 8080),
Endpoint.of("server2.example.com", 8080),
Endpoint.of("server3.example.com", 8080)
);
// DNS 기반 서비스 디스커버리
EndpointGroup dnsGroup = DnsEndpointGroup.builder("my-service.internal")
.port(8080)
.ttl(10, 60) // 최소 10초, 최대 60초 TTL
.build();
// ZooKeeper 기반 (별도 의존성)
EndpointGroup zkGroup = ZooKeeperEndpointGroup.builder(
curatorFramework, "/services/my-service")
.build();
로드밸런싱 전략
// 가중 라운드로빈 (기본)
WebClient client = WebClient.builder("http", staticGroup)
.endpointRemapper(endpoint ->
endpoint.withWeight(endpoint.host().equals("server1") ? 3 : 1))
.build();
// 엔드포인트 그룹으로 클라이언트 생성
WebClient client = WebClient.of(SessionProtocol.HTTP, staticGroup);
// 요청 — 자동으로 로드밸런싱된다
HttpResponse response = client.get("/api/users/1");
별도의 로드밸런서(L4/L7) 없이 클라이언트가 직접 여러 서버에 요청을 분배합니다. 서비스 메시(Istio, Linkerd) 없이도 기본적인 서비스 디스커버리와 로드밸런싱이 가능합니다.
헬스 체크 연동
// 엔드포인트 헬스 체크 — 죽은 서버는 자동으로 제외
EndpointGroup healthChecked = HealthCheckedEndpointGroup.builder(
staticGroup, "/internal/health")
.protocol(SessionProtocol.HTTP)
.retryInterval(Duration.ofSeconds(5)) // 5초마다 헬스 체크
.build();
// 헬스 체크 통과한 서버에만 요청이 간다
WebClient client = WebClient.of(SessionProtocol.HTTP, healthChecked);
Spring Boot 통합 — 기존 생태계와 공존
Armeria는 독립적으로도 사용할 수 있지만, 대부분의 자바 프로젝트가 Spring Boot를 쓰고 있는 현실을 무시하지 않습니다. armeria-spring-boot-starter를 통해 기존 Spring Boot 애플리케이션에 Armeria를 통합할 수 있습니다.
의존성 추가
<!-- build.gradle 또는 pom.xml -->
<dependency>
<groupId>com.linecorp.armeria</groupId>
<artifactId>armeria-spring-boot3-starter</artifactId>
<version>1.31.3</version>
</dependency>
ArmeriaServerConfigurator
Spring Bean으로 ArmeriaServerConfigurator를 등록하면 Armeria 서버 설정을 추가할 수 있습니다.
@Configuration
public class ArmeriaConfig {
@Bean
public ArmeriaServerConfigurator armeriaServerConfigurator() {
return builder -> {
// gRPC 서비스 등록
builder.service(GrpcService.builder()
.addService(new MyGrpcServiceImpl())
.build());
// 데코레이터 추가
builder.decorator(LoggingService.newDecorator());
// DocService — Armeria의 내장 API 문서 & 디버그 도구
builder.serviceUnder("/docs", DocService.builder().build());
};
}
}
Spring MVC와 공존
기존 Spring MVC 컨트롤러를 유지하면서 Armeria 서비스를 추가하는 것이 가능합니다.
[ Armeria Server (Netty 기반) ]
│
├── /grpc/** ──→ Armeria gRPC 서비스
├── /docs/** ──→ Armeria DocService
└── 나머지 ──→ 내장 Tomcat (Spring MVC)
Armeria가 프론트에서 모든 요청을 받고, 경로에 따라 Armeria 네이티브 서비스 또는 Tomcat으로 라우팅합니다. 기존 @RestController는 그대로 동작하면서, gRPC 서비스를 같은 서버에 추가할 수 있습니다.
// 기존 Spring MVC 컨트롤러 — 그대로 동작한다
@RestController
@RequestMapping("/api/v1")
public class UserController {
@GetMapping("/users/{id}")
public User getUser(@PathVariable long id) {
return userService.findById(id);
}
}
// 새로운 gRPC 서비스 — 같은 서버에서 함께 동작한다
public class UserGrpcService extends UserServiceGrpc.UserServiceImplBase {
@Override
public void getUser(GetUserRequest request,
StreamObserver<GetUserResponse> observer) {
// gRPC 구현
}
}
DocService — 내장 API 문서
Armeria의 숨은 보석은 DocService입니다. Swagger UI처럼 API 문서를 자동으로 생성하는데, gRPC, Thrift, REST 모든 프로토콜의 API를 한곳에서 보여줍니다.
builder.serviceUnder("/docs", DocService.builder()
// 예제 요청 추가
.exampleRequests(MyGrpcServiceGrpc.SERVICE_NAME,
"GetUser",
GetUserRequest.newBuilder().setId(1).build())
.build());
/docs 경로로 접속하면 웹 UI에서 모든 서비스의 메서드 목록, 요청/응답 스키마, 예제 요청 실행까지 가능합니다. 개발과 디버깅에서 큰 도움이 됩니다.
정리 — Armeria의 위치
Armeria를 한마디로 정리하면, Netty의 성능과 유연성 위에 마이크로서비스 프레임워크의 편의성을 올린 것 입니다.
| 계층 | 역할 | 예시 |
|---|---|---|
| 비즈니스 로직 | 서비스 구현 | @Get, @Post, gRPC impl |
| Armeria | 라우팅, 데코레이터, 직렬화 | ServerBuilder, LoggingService |
| Netty | 비동기 I/O, 프로토콜 코덱 | EventLoop, Http2FrameCodec |
| OS | 소켓, epoll/kqueue | 커널 |
개발자는 Armeria 레벨에서 작업하면서, 필요할 때만 Netty 레벨로 내려가면 됩니다.
순수 Netty와 비교했을 때의 트레이드오프도 있습니다.
- ** 장점 **: 프로토콜 통합, 데코레이터, 서비스 디스커버리, DocService 등이 기본 제공
- ** 장점 **: HTTP/2, gRPC, Thrift 설정을 직접 하지 않아도 됨
- ** 단점 **: Armeria 자체의 추상화 레이어가 추가되므로, 극한의 저수준 최적화가 필요한 경우 순수 Netty가 더 적합
- ** 단점 **: Armeria 특유의 API와 개념을 별도로 학습해야 함
면접에서 "Netty 기반 프레임워크를 써본 적 있나요?"라는 질문이 나오면, Armeria가 어떻게 Netty의 EventLoop, Pipeline, HTTP/2 코덱을 활용하는지 설명할 수 있으면 좋습니다. 단순히 "Armeria 써봤습니다"보다는 "내부적으로 Netty ServerBootstrap을 구성하고, 데코레이터 패턴으로 ChannelPipeline의 관심사를 서비스 레벨로 끌어올린 구조"라고 말할 수 있으면 훨씬 강한 인상을 줍니다.