Netty ByteBuf is not Immutable.

주싱·2021년 11월 18일
0

Netty

목록 보기
2/9
post-custom-banner

Netty ByteBuf 는 immutable 하지 않습니다. 대표적으로 ByteBuf 에서 값을 읽거나 쓰면 객체가 가진 wrtierIndex 와 readerIndex 값이 바뀝니다. 그래서 다음에 ByteBuf 에 접근하는 연산의 결과는 이전 동작의 결과에 영향을 받게 됩니다. 너무 당연한 내용이지만, 주의 깊게 코드를 작성하지 못해서 아래와 같은 문제를 만났고 간단한 리팩토링을 수행한 과정을 정리해 보았습니다.

문제 현상

아래와 같이 특정 장비와 TCP 통신에 사용되는 상수들을 정의한 정적 클래스를 만들었습니다. RX_LINE_DELIMITER 는 장비에서 스트림 기반 TCP를 사용하기 때문에 메시지의 끝을 알려주기 위해 붙여주는 구분자입니다. (문서에는 '\r' 문자라고 명시되어 있는데 실제로는 0 값이 하나 더 붙어와서 저런 특이한 값을 가지게 되었습니다)

public class BheChannelSpec {
    public static final int SERVER_PORT = 23;
    public static final ByteBuf RX_LINE_DELIMITER = Unpooled.wrappedBuffer(new byte[]{'\r', 0});
    ... 
}

다시 코드를 보면 RX_LINE_DELIMITER 타입이 ByteBuf 입니다. 이유는 단순히 Netty 에서 제공하는 DelimiterBasedFrameDecoder 의 생성자가 Delimiter 를 ByteBuf 타입으로 입력받기 때문이었습니다. (DelimiterBasedFrameDecoder 에 구분자를 지정해주면 구분자를 기준으로 메시지를 잘라서 Pipeline 의 다음 Handler 로 전달해 줍니다.)

여기까지 당연히 빌드도 잘되고 문제가 없습니다. 그래서 아래와 같이 Netty Pipeline 을 테스트하기 위해 EmbeddedChannel 을 사용해 테스트 코드를 작성하였습니다. @BeforeEach 어노테이션을 통해 단위 테스트 하나가 수행될 때 마다 채널을 새롭게 생성하도록 하여 단위 테스트 각각이 독립적인 상태를 가지도록 하였습니다.

@BeforeEach
void beforeEach() {
    channel = new EmbeddedChannel();
    channel.pipeline()
            // Inbound
            .addLast(new DelimiterBasedFrameDecoder(BheChannelSpec.MAX_LENGTH_OF_MESSAGE, true, true, BheChannelSpec.RX_LINE_DELIMITER))
            .addLast(new StringDecoder())
            .addLast(new StringSeparator(BheChannelSpec.FIELD_SEPARATOR_REG_EXP))
    ... 
}

@Test
void rxActualStatusTest() {
     // Some Test 1
}

@Test
void rxSetStepResponseTest() {
    // Some Test 2
}

그런데 코드를 실행시켜보니 첫번째 단위 테스트만 성공하고, 나머지 모든 테스트가 아래와 같은 오류로 실패합니다.

원인 분석

조금 살펴보니 앞에서 보았던 RX_LINE_DELIMITER 값이 empty delimiter 라고 합니다. '나는 분명 값을 할당했는데?' 브레이크 포인트를 찍어서 코드를 찾아가 보았더니 첫번째 테스트 케이스가 실행될 때는 아래와 같이 ridx (readerIndex) = 0, widx (writerIndex) = 2 로 실제 읽을 수 있는 문자가 2글자로 (writerIndex - readerIndex) 위에서 정의한 값이었습니다.

그러나 다음번 테스트 케이스가 실행될 때는 아래와 같이 ridx (readerIndex) = 2, widx (writerIndex) = 2 로 실제 읽을 수 있는 문자가 0글자로 (writerIndex - readerIndex) 위에서 empty delimiter 가 맞았습니다. 이전 테스트 케이스에서 Delimiter 를 읽음으로 readerIndex 가 증가한 것이었습니다.

아차! ByteBuf 를 Immutable 한 String 객체 처럼 착각했습니다. 단위 테스트가 실행될 때 마다 beforeEach() 함수가 새롭게 실행되지만 정적 인스턴스는 한번 만 올라가 있다는 걸 함께 생각하지 못했습니다. 그래서 RX_LINE_DELIMITER 타입을 String 으로 바꾸고, DelimiterBasedFrameDecoder 생성자 인수로 전달할 때 ByteBuf 로 타입을 바꾸어 전달하는 방향으로 수정하기로 했습니다.

리팩토링

그런데 뭔가 거슬리기 시작했습니다. 아래 코드를 다시 보면 RX_LINE_DELIMITER 값은 public static 으로 정의해 두었습니다. 바쁘기도 하고, 절대 바뀔 일도 없고, 별다른 연산을 수행할 일이 없을거라 생각해서 직접 필드에 접근하게 코드를 작성했습니다.

public class BheChannelSpec {
    public static final int SERVER_PORT = 23;
    public static final ByteBuf RX_LINE_DELIMITER = Unpooled.wrappedBuffer(new byte[]{'\r', 0});
    ... 
}

그래서 RX_LINE_DELIMITER 타입을 String 으로 바꾸어주면 RX_LINE_DELIMITER 를 사용하고 있는 모든 코드들도 아래와 같이 바꾸어 주어야 했습니다. 변경이 프로그램에 전반에서 발생합니다.

@BeforeEach
void beforeEach() {
		ByteBuf rxLineDelimiter = Unpooled.copiedBuffer(BheChannelSpec.RX_LINE_DELIMITER, StandardCharsets.UTF_8);

    channel = new EmbeddedChannel();
    channel.pipeline()
            // Inbound
            .addLast(new DelimiterBasedFrameDecoder(BheChannelSpec.MAX_LENGTH_OF_MESSAGE, true, true, rxLineDelimiter ))
    ... 
}

만약 처음부터 Getter 를 통해 필드에 접근했다면, 사용자 코드는 전혀 바꾸지 않고 아래와 같이 구현 코드만 바꾸어 주어 변경이 국지적으로 코드 한 군데에서만 발생하게 할 수 있었습니다. 코드 사용 부에서는 변경에 영향을 받지 않고 수정이 이루어 질 수 있죠!

    private static final String rxLineDelimiter = "\r\0";

    public static ByteBuf getRxLineDelimiter() {
        return Unpooled.copiedBuffer(rxLineDelimiter, StandardCharsets.UTF_8);
    }

마치며

이렇게 바꾸면 깔끔하게 모든 테스트 케이스가 성공합니다.

대학생 때 처음 객체지향 프로그래밍 언어를 배울 때 아무런 연산을 하지 않는 필드를 굳이 Getter를 귀찮게 정의해서 접근하는지 이해가 되지 않았는데, 누군가 이후에 변경 사항이 발생하면 사용부의 변경 없이 구현을 고칠 수 있다고 말해주어 감동했던 기억이 납니다. 이런 사소한 배려들이 모여서 좋은 코드들이 만들어져 가는 것 같습니다.

profile
소프트웨어 엔지니어, 일상
post-custom-banner

0개의 댓글