현재 개발중인 채팅서버에서는 다양한 타입의 패킷을 받고있습니다.
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클래스를 새로 구현하였습니다.
생성자에서 고차함수를 인자로 받습니다.
또한 넘어올 파라미터와 리턴 타입을 제네릭으로 받아서 어느 타입이든지 유연하게 대응이 가능합니다.
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