채팅 서버 '패킷 변환기' 디자인 패턴 적용 해보기

Daeyoung Nam·2021년 12월 9일
4

프로젝트

목록 보기
13/16
post-thumbnail

현재 개발중인 채팅서버에서는 다양한 타입의 패킷을 받고있습니다.
TCP/IP 통신을 하면서 바이트 배열로 들어오는 패킷을 '패킷 변환기'가 적절히 변형을 시켜주고 있는데요.
기존 변환기 코드의 경우 코드가 너무 길어지고 깔끔하지가 않아서 'Converter' 패턴을 도입하여 재구성해보았습니다.

기존 코드

interface ProtocolMessageConverter<T> {
	fun toValueLE(buf: ByteBuf): T 
}

ProtocolMessageConvertor를 구현하여 inbound packet을 각 타입에 맞게 변환시켜줘야 합니다.

class BytesToIntConverter : ProtocolMessageConverter<Int> {

	override fun toValueLE(buf: ByteBuf): Int {
		return buf.readIntLE()
	}

}

// 위와같이 여러개의 데이터 converter 구현

위와 같이 구현하여 리틀 엔디언 방식으로 바이트를 읽어오도록 하였습니다.

이후에 short, float, string 데이터를 읽어오는 converter를 추가로 구현하였지만 건너뛰겠습니다.

class ProtocolMessageConverterFactory {

    inline fun <reified R : Any> getConvertor(): ProtocolMessageConverter<R> = when(R::class) {
        Int::class -> intPayloadConverter
        Short::class -> shortPayloadConverter
        Float::class -> floatPayloadConverter
        String::class -> stringPayloadConverter
        else -> throw ClassNotFoundException("Message converter is not found!")
    } as ProtocolMessageConverter<R>

    companion object {
        fun getFactory(): ProtocolMessageConverterFactory {
            return protocolMessageConverterFactory
        }
    }

}

Factory 패턴을 구현하여 적절한 변환기를 반환해주도록 하였습니다.

@Test
@DisplayName("바이트 to int가 제대로 변환되는지")
fun bytesToIntTest() {
    val messageConverter = ProtocolMessageConverterFactery.getFactory().getConverter<Int>()

    val byteArray = ByteArray(4)
    val payload = 1024
    byteArray[0] = (payload and 0xff).toByte()
    byteArray[1] = ((payload shr 8) and 0xff).toByte()
    byteArray[2] = ((payload shr 16) and 0xff).toByte()
    byteArray[3] = ((payload shr 24) and 0xff).toByte()

    val buf = Unpooled.wrappedBuffer(byteArray)

    assertEquals(messageConverter.toValueLE(buf), payload)
}


테스트는 통과합니다.

다만 저 방식대로 계속 구현해 나간다면 계속 인터페이스를 상속받고 메소드를 구현해야합니다.
단순히 데이터를 변환해주는 converter라면 좀더 간단한 방식으로 바꿔보고 싶었습니다.

현재 코드

open class MessageConverter<P, R>(
    private val decode: (P) -> R,
    private val encode: (P, R) -> Unit
) {

    fun convertTo(source: P): R {
        return decode(source)
    }

    fun write(source: P, data: R) {
        encode(source, data)
    }

}

ProtocolMessageConverter는 과감하게 삭제했습니다. 그리고 MessageConverter클래스를 새로 구현하였습니다.

생성자에서 고차함수를 인자로 받습니다.
또한 넘어올 파라미터와 리턴 타입을 제네릭으로 받아서 어느 타입이든지 유연하게 대응이 가능합니다.

💡 람다를 인자로 받거나 반환하는 함수를 고차 함수(high order function) 라고 합니다.
class IntConvertor : MessageConverter<ByteBuf, Int>(
    { byteBuf -> byteBuf.readIntLE() },
    { byteBuf: ByteBuf, i: Int -> byteBuf.writeIntLE(i) }
)

위와같이 부모 생성자에 해야할 행위들을 람다로 넘겨줍니다.

@Test
@DisplayName("int 바이트가 제대로 변환되는지")
fun bytesToIntTest() {
    val messageConverter = ProtocolMessageConverterFactory.getFactory().getConvertor<Int>()

    val byteArray = ByteArray(4)
    val payload = 1024
    byteArray[0] = (payload and 0xff).toByte()
    byteArray[1] = ((payload shr 8) and 0xff).toByte()
    byteArray[2] = ((payload shr 16) and 0xff).toByte()
    byteArray[3] = ((payload shr 24) and 0xff).toByte()

    val buf = Unpooled.wrappedBuffer(byteArray)

    assertEquals(messageConverter.convertTo(buf), payload)
}

부모 클래스에는 convertTo 라는 메소드가 있는데 역할은 생성자로 넘어온 encode 고차함수를 호출해줍니다.

ProtocolMessageConverterFactory는 MessageConverter를 상속한 서브 클래스들을 반환합니다.
서브 클래스들은 각 고차함수들을 목적에 맞게 구현하고 부모 생성자로 넘겨줍니다.
이 구조의 장점은 프로그래머가 하위 구현체를 몰라도 사용이 가능하다는 점입니다.
왜냐하면 Factory에서 적절한 변환기를 만들어서 주기 때문입니다.

참고자료: https://github.com/iluwatar/java-design-patterns/tree/master/converter

profile
내가 짠 코드가 제일 깔끔해야하고 내가 만든 서버는 제일 탄탄해야한다 .. 😎

0개의 댓글