목차
@Sharable
- 하나의
ChannelHandler인스턴스를 여러 채널의 파이프라인에 추가해도 되는 경우를 표시한다.
@Sharable을 붙인다고 핸들러가 자동으로 스레드 안전해지는 것은 아니다. 여러 채널의 이벤트가 같은 인스턴스에 도착할 수 있으므로, 채널마다 달라지는 상태를 인스턴스 필드에 저장하면 값이 섞일 수 있다.
@ChannelHandler.Sharable
final class LoggingHandler extends ChannelInboundHandlerAdapter {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
System.out.println("channel=" + ctx.channel().id());
ctx.fireChannelRead(msg); // 다음 핸들러로 전달
}
}
위처럼 공유 필드가 없는 핸들러는 재사용하기 쉽다. 요청별 누적값이나 인증 상태가 필요하다면 채널별 핸들러 인스턴스를 만들거나 채널 속성 등 채널에 귀속된 저장소를 사용한다. 공유 여부를 정할 때는 필드뿐 아니라 호출하는 객체의 가변 상태도 함께 확인한다.
예를 들어 아래 핸들러를 여러 채널이 공유하면 messageCount가 모든 채널의 메시지를 합쳐 센다. 채널별 메시지 수를 기대한 코드라면 버그다.
@ChannelHandler.Sharable
final class CountingHandler extends ChannelInboundHandlerAdapter {
private int messageCount;
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
messageCount++; // 여러 채널에서 갱신하면 값이 섞이고 경쟁 상태도 생긴다.
ctx.fireChannelRead(msg);
}
}
이런 경우 채널마다 new CountingHandler()를 만들거나 채널에 귀속된 상태 저장 방식을 사용한다. 여러 채널을 합친 전체 통계가 목적이라면 공유 카운터를 명시적으로 스레드 안전하게 구현한다. @Sharable은 설계자가 안전성을 보증한다는 표시이지 Netty가 필드 접근을 직렬화한다는 뜻이 아니다. 반대로 인스턴스별 상태가 없어도 공유하지 않을 핸들러에는 굳이 붙일 이유가 없다.
참고: Netty ChannelHandler.Sharable API
채널별 카운터가 필요할 때
위 CountingHandler의 messageCount는 핸들러 인스턴스에 하나뿐이다. 채널 A에서 세 번, 채널 B에서 두 번 받으면 두 채널 모두 같은 값을 공유한다. 채널별 카운트를 원한다면 다음처럼 각 채널의 속성에 값을 둘 수 있다.
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.util.AttributeKey;
@ChannelHandler.Sharable
final class PerChannelCountHandler extends ChannelInboundHandlerAdapter {
private static final AttributeKey<Integer> COUNT =
AttributeKey.valueOf("messageCount");
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) {
Integer previous = ctx.channel().attr(COUNT).get();
int next = (previous == null ? 0 : previous) + 1;
ctx.channel().attr(COUNT).set(next);
ctx.fireChannelRead(msg);
}
}
같은 채널의 이벤트를 한 EventLoop에서 처리한다는 전제의 간단한 예다. 다른 스레드에서도 카운터를 갱신한다면 원자성·가시성을 별도로 다뤄야 한다. 카운터가 업무적으로 중요하고 재연결 뒤에도 남아야 한다면 채널 속성이 아니라 외부 저장소에 상태를 둔다.
flowchart LR A["채널 A의 메시지"] --> H["공유 핸들러"] B["채널 B의 메시지"] --> H H --> SA["채널 A 속성 COUNT"] H --> SB["채널 B 속성 COUNT"]
핸들러 인스턴스는 하나여도 ctx.channel()을 통해 접근하는 저장 공간은 채널마다 다르다. 이 차이를 이해하면 @Sharable을 붙여야 하는 기준이 분명해진다. 일반 객체 필드에 연결별 인증 정보나 부분 수신 버퍼를 저장하는 구현은 피한다.
검증할 때는 두 EmbeddedChannel에 같은 핸들러 인스턴스를 넣고, A에 세 메시지, B에 두 메시지를 보낸 뒤 각 채널 속성이 3과 2인지 확인한다. 한 채널만 시험하면 공유 상태 버그가 드러나지 않는다. ctx.fireChannelRead(msg)로 메시지를 넘긴 경우 마지막 소비자가 참조 카운트 버퍼를 해제해야 한다. 공유 핸들러의 상태 안전성과 버퍼 소유권은 서로 다른 문제다.