자바 네트워크 소녀 Netty" 공부하면서 정리한 내용입니다.
Netty로 개발을 하다가 단위테스트를 어떻게 작성해야 할지 감이 잡히지 않았다. "자바 네트워크 소녀 Netty"를 보며 단위 테스트를 작성하는 방법을 알게 되었다.
먼저 Netty로 작성한 TelnetServer 코드를 보자.
Spring + netty로 작성한 Server 애플리케이션 코드이다.
NettyServerConfig
@ComponentScan("org.example")
@PropertySource("config/application.properties")
@Configuration
public class NettyServerConfig {
@Value("${tcp.port}")
private int tcpPort;
@Bean
public InetSocketAddress tcpPort() {
return new InetSocketAddress(tcpPort);
}
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
TelnetServer
@Component
public class TelnetServer {
@Autowired
private InetSocketAddress tcpPort;
@Value("${boss.thread.count}")
private int bossCount;
@Value("${worker.thread.count}")
private int workerCount;
public void start() throws InterruptedException {
ChannelGroup channels = new DefaultChannelGroup(GlobalEventExecutor.INSTANCE);
EventLoopGroup bossEventLoopGroup = new NioEventLoopGroup(bossCount);
EventLoopGroup workerEventLoopGroup = new NioEventLoopGroup(workerCount);
try {
ServerBootstrap bootstrap = new ServerBootstrap();
bootstrap
.group(bossEventLoopGroup, workerEventLoopGroup)
.channel(NioServerSocketChannel.class)
.childHandler(new TelnetServerInitializer());
ChannelFuture channelFuture = bootstrap.bind(tcpPort).sync();
channels.add(channelFuture.channel()); // 채널 그룹에 추가
channelFuture.channel().closeFuture().sync(); // 종료될 때 까지 대기
} finally {
channels.close().awaitUninterruptibly();
workerEventLoopGroup.shutdownGracefully().awaitUninterruptibly();
bossEventLoopGroup.shutdownGracefully().awaitUninterruptibly();
}
}
}
TelnetServerInitializer
public class TelnetServerInitializer extends ChannelInitializer<SocketChannel> {
private final Charset charset = Charset.defaultCharset();
@Override
protected void initChannel(SocketChannel socketChannel) {
socketChannel.pipeline()
.addLast(new StringDecoder(charset), new StringEncoder(charset))
.addLast(new TelnetServerHandler());
}
}
TelnetServerHandler
public class TelnetServerHandler extends SimpleChannelInboundHandler<String> {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("채널활성화");
String welcomeMsg = ResponseGenerator.makeHello();
System.out.println("전송 메시지: " + welcomeMsg);
ctx.writeAndFlush(welcomeMsg);
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, String s) throws Exception {
ResponseGenerator generator = new ResponseGenerator(s);
String response = generator.response();
System.out.println("응답메시지: " + response);
ChannelFuture future = ctx.writeAndFlush(response);
if(generator.isClose()) {
future.addListener(ChannelFutureListener.CLOSE);
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("예외 발생: " + cause.getLocalizedMessage());
ctx.close();
}
}
ResponseGenerator
public class ResponseGenerator {
private final String request;
public ResponseGenerator(String request) {
this.request = request;
}
public String response() {
String command = null;
if(this.request.isEmpty()) {
command = "명령어를 입력해주세요.\r\n";
} else if("bye".equals(this.request.toLowerCase())) {
command = "좋은 하루 보내세요.\r\n";
} else {
command = "입력하신 명령어가 '" + this.request + "'입니까?\r\n";
}
return command;
}
public static String makeHello() throws UnknownHostException {
StringBuilder sb = new StringBuilder();
sb.append("환영합니다. ")
.append(InetAddress.getLocalHost().getHostName())
.append("에 접속하셨습니다.\r\n")
.append("현재 시간은 ")
.append(new Date().toString())
.append(" 입니다.\r\n");
return sb.toString();
}
public boolean isClose() {
return "bye".equals(this.request);
}
}
NettyServerStarter
public class NettyServerStarter {
public static void main(String[] args) {
try {
ApplicationContext ac = new AnnotationConfigApplicationContext(NettyServerConfig.class);
TelnetServer server = ac.getBean(TelnetServer.class);
server.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}
2. 단위테스트 작성
크게 업무로직 처리부분 테스트, 이벤트 핸들러 테스트, 디코더 테스트를 작성할 수 있다.
테스트도구 프레임워크로는 assertj를 사용하였다.
a. 업무로직 처리 테스트(ResponseGeneratorTest)
이 부분은 Netty와 상관없이 일반적으로 테스트할 때 사용하는 방식이기 때문에 설명은 생략하겠다.
@DisplayName("Response 테스트")
public class ResponseGeneratorTest {
@DisplayName("cmd 테스트")
@Test
void givenStringHi_whenCreateResponseGenerator_thenReturnsResponse() {
String request = "hi";
ResponseGenerator generator = new ResponseGenerator(request);
assertThat(generator).isNotNull();
assertThat(generator.response()).isNotNull();
assertThat(generator.response()).isEqualTo("입력하신 명령어가 'hi'입니까?\r\n");
assertThat(generator.isClose()).isFalse();
}
@DisplayName("bye 테스트")
@Test
void givenStringBye_whenCreateResponseGenerator_thenReturnsResponse() {
String request = "bye";
ResponseGenerator generator = new ResponseGenerator(request);
assertThat(generator).isNotNull();
assertThat(generator.response()).isNotNull();
assertThat(generator.response()).isEqualTo("좋은 하루 보내세요.\r\n");
assertThat(generator.isClose()).isTrue();
}
}
b. 디코드 테스트(DecodeTest)
DelimiterBasedFrameDecoder가 아니더라도 직접 작성한 decode가 정상적으로 동작하는지 테스트가 필요하다. decode에 대해 검증하고 싶을 때 아래 코드를 참고하자.
예제코드는 인바운드 채널에 "안녕하세요\r\n반갑습니다\r\n"을 등록하고, DelimiterBasedFrameDecoder가 제대로 동작하는 검증하는 테스트이다.
@DisplayName("decode 테스트")
public class DecodeTest {
@DisplayName("DelimiterBasedFrameDecoder 테스트")
@Test
void givenWriteData_whenDelimiterBasedFrameDecoding_thenReturnDecodingResult () {
String writeData = "안녕하세요\r\n반갑습니다\r\n";
String firstResponse = "안녕하세요\r\n";
String secondResponse = "반갑습니다\r\n";
DelimiterBasedFrameDecoder decoder = new DelimiterBasedFrameDecoder(8192, false, Delimiters.lineDelimiter());
EmbeddedChannel embeddedChannel = new EmbeddedChannel(decoder);
ByteBuf request = Unpooled.wrappedBuffer(writeData.getBytes());
boolean result = embeddedChannel.writeInbound(request);
assertThat(result).isTrue();
ByteBuf response = null;
response = (ByteBuf) embeddedChannel.readInbound();
assertThat(firstResponse).isEqualTo(response.toString(Charset.defaultCharset()));
response = (ByteBuf) embeddedChannel.readInbound();
assertThat(secondResponse).isEqualTo(response.toString(Charset.defaultCharset()));
embeddedChannel.finish();
}
}
주요 코드 설명
- DelimiterBasedFrameDecoder: 구분자를 기준으로 잘라서 돌려주는 Netty에서 제공하는 디코더이다. 생성자로 lineDelimiter 전달했기 때문에 라인(\r\n)을 기준으로 데이터를 구분할 것이다.
- EmbeddedChannel: Netty에서는 채널파이프라인, 이벤트 루프 설정과 같은 부가작업 없이 순수하게 이벤트 핸들러를 테스트할 수 있도록 제공한다. EmbeddedChannel 디코더를 등록
- writebound(): writebound 메서드를 통해 인바운드 채널에 데이터를 등록할 수 있다. (true이면 성공적으로 등록된 것이다.)
- readInbound(): 인바운드 데이터를 읽는다.
DelimiterBasedFrameDecoder가 정상적으로 동작한다면 "안녕하세요\r\n반갑습니다\r\n"을 디코드한 결과는 "안녕하세요\r\n"를 돌려준 다음에 "반갑습니다\r\n"를 돌려줄 것이다.
테스트를 실행하면 정상적으로 동작하는 것을 확인할 수 있다.
c. 이벤트 핸들러 테스트
이벤트 핸들러 테스트이다. EmbededChannel의 생성자로 테스트 할 이벤트 핸들러만 전달하면 위의 디코드 테스트와 크게 차이가 없다.
@DisplayName("TelnetServerHandler 테스트")
class TelnetServerHandlerTest {
@Test
void test1() {
StringBuilder sb = new StringBuilder();
try {
sb.append("환영합니다. ")
.append(InetAddress.getLocalHost().getHostName())
.append("에 접속하셨습니다.\r\n")
.append("현재 시간은 ")
.append(new Date().toString())
.append(" 입니다.\r\n");
} catch (UnknownHostException e) {
fail();
e.printStackTrace();
}
EmbeddedChannel embeddedChannel = new EmbeddedChannel(new TelnetServerHandler());
String expected = (String) embeddedChannel.readOutbound();
assertThat(expected).isEqualTo(sb.toString());
embeddedChannel.finish();
}
@Test
void test2() {
String request = "hello";
String expected = "입력하신 명령어가 '" + request + "'입니까?\r\n";
EmbeddedChannel embeddedChannel = new EmbeddedChannel(new TelnetServerHandler());
embeddedChannel.writeInbound(request);
embeddedChannel.readOutbound(); // active 이벤트의 메시지를 버린다.
String channelReadMsg = (String) embeddedChannel.readOutbound();
assertThat(expected).isEqualTo(channelReadMsg);
embeddedChannel.finish();
}
@Test
void test3() {
String request = "bye";
String expected = "좋은 하루 보내세요.\r\n";
EmbeddedChannel embeddedChannel = new EmbeddedChannel(new TelnetServerHandler());
embeddedChannel.writeInbound(request);
embeddedChannel.readOutbound(); // active 이벤트의 메시지를 버린다.
String channelReadMsg = (String) embeddedChannel.readOutbound();
assertThat(expected).isEqualTo(channelReadMsg);
embeddedChannel.finish();
}
}
지금까지 Netty 단위 테스트를 작성해봤다. Netty 단위 테스트 작성하는 방법에 대해서 알게 되었으니 개발할 때 꼼꼼하게 테스트를 작성해서 안정적인 애플리케이션을 개발할 수 있을 것이다.
'Programming > etc' 카테고리의 다른 글
[Netty] 바이트버퍼(ByteBuffer) (0) | 2024.01.15 |
---|---|
[Netty] 이벤트 모델 (0) | 2024.01.15 |
[Netty] 채널 파이프라인과 코덱 (0) | 2024.01.12 |
[Netty] Netty의 Bootstrap(부트스트랩) (0) | 2024.01.12 |
[Netty] 네트워크 프레임워크 Netty란? (1) | 2024.01.11 |