[마이크로소프트웨어2월호]
바이트 기반 TCP/IP 코드에 객체지향 적용
Stream 기반 TCP/IP 통신의 물리적인 수신 데이터를 비즈니스 도메인 객체로 변환하는 decode 과정을 Netty를 통해 살펴보고 decoder 내부에서 보이는 바이트 기준의 레거시 해석 코드를 객체 지향적으로 바꿔서 비즈니스 로직에 의한 도메인 객체의 잦은 변화에 빠르게 대응할 수 있는 Frame API를 제작해 보자.
BAS(Building Automation System, 빌딩 자동제어 시스템) 통합 프로젝트를 진행 중에 빌딩 내에 설치된 엘리베이터(이하 E/V)를 실시간 감시하는 화면을 추가하기로 했다. E/V 시스템의 워크스테이션(workstation)에는 관리자용 관리 소프트웨어가 설치되어 있지만 이것은 데스크톱 애플리케이션이어서 웹(web) 기반인 통합 프로젝트를 위해 무언가 조치를 취해야만 한다.
엘리베이터 이야기
먼저, E/V 시스템 업체로부터 전달 받은 프로토콜 문서와 미리 구상한 E/V 감시 화면은 각각 <그림 1>, <그림 2>와 같다. E/V의 아이디와 상태 메시지는 프로토콜을 분석하면 해당 데이터를 추출할 수 있으리라 예상된다.
개략적인 설계는 웹 MVC가 E/V 클라이언트에 의존하지 않도록 데이터베이스를 두기로 했다. 이제 E/V 클라이언트는 서버 측으로부터 수신한 데이터를 분석해 이를 데이터베이스에 저장하는 기능을 구현하고자 한다.
Netty로 구현하기
Stream 기반 통신에서 물리적인 수신 데이터와 응용프로그램이 필요로 하는 논리적인 프레임 데이터가 반드시 일치하지 않는다. 프로토콜 문서에서 하나의 E/V 상태 정보를 담은 바이트 배열은 10 바이트 길이로 구성되므로 이때 논리적인 프레임은 ‘STX로 시작하며 ETX로 끝나고 그 길이가 10 바이트인 일련의 바이트 배열’이라고 볼 수 있다.
그런데, TCP/IP 통신에서 수신 데이터는 소켓 수신 버퍼에 저장되어 있는데 이는 운영체제가 관리하게 되며, 응용프로그램에서 필요한 논리적인 프레임으로 끊어서 수신할 수는 없다. 즉, 실제로 수신하는 물리적인 메시지는 <그림 3>과 같은 다양한 길이를 가질 수 있다.
따라서 물리적인 수신 데이터를 논리적인 프레임으로 끊어 주는 역할이 중요한데, 이벤트 기반 네트워크 애플리케이션 프레임워크인 Netty를 이용하면 이 문제를 보다 깔끔하게 해결할 수 있다. 이제 Netty를 이용해 ElevatorClient를 보자.
<그림 1> E/V 감시반 연동 프로토콜
<그림 2> E/V 감시화면
<그림 3> E/V client 의 물리적인 수신 데이터
먼저 org.jboss.netty.handler.codec.frame.FixedLength FrameDecoder를 이용해 물리적인 수신 데이터를 1개의 프레임(10 바이트)씩 끊어서 다음 handler로 이벤트를 전달한다(<리스트 1>의 [a]). 두 번째로 등록된 ElevatorFrameDecoder는 수신 받은 하나의 프레임을 도메인 객체로 바꿔서 다음 handler로 넘긴다(<리스트 1>의 [b]). 마지막으로 등록된 Elevator DaoHandler는 수신 받은 도메인 객체를 적합한 DAO를 이용해 데이터베이스에 저장하게 된다(<리스트 1>의 [c]).
Netty로 테스트하기
ElevatorFrameDecoder를 테스트해 볼 수 있을까? 테스트하기 위해 실제 I/O가 필요하다면 그것은 단위 테스트가 아니다. 단위 테스트는 문제를 지역화하고 빠르게 수행되어야 하기 때문이다. Netty는 테스트에 대한 지원도 놓치지 않는다.
org.jboss.netty.handler.codec.embedder.DecoderEmbedder는 실제 I/O 없이 handler를 테스트할 수 있는 길을 열어 준다. 이제 ElevatorFrameDecoderTest를 작성해 보자. 네 번째 바이트(index:3)가 E/V 아이디를 나타내며, 여덟 번째 바이트(index:9)가 E/V 상태를 나타내므로 이 테스트는 가뿐하게 성공한다. 모든 작업은 끝났다. 단위 테스트까지 마친 우리의 E/V 클라이언트는 에러 없이 그 역할을 충실히 수행한다.
두 번째 엘리베이터 이야기
SI 프로젝트가 언제나 그러하듯이(?) 얼마 지나지 않아 E/V 감시 화면이 <그림 4>와 같이 변경되기를 요청 받는다. 현재 층, 진행 방향, 문의 개폐 여부 등 E/V의 정보를 모두 표현하기로 한 것이다. 데이터베이스 테이블 구조를 변경하고 Elevator와 ElevatorFrameDecoder를 수정해야만 한다.
<그림 4> E/V 통합 감시 화면
무엇이 문제인가?
첫 번째 문제는 ElevatorFrameDecoder를 수정하기는 쉽지 않았다는 것이다. 현재 층 정보가 프레임에서 네 번째 바이트인지 다섯 번째 바이트인지 전혀 기억이 없기 때문이다. 만약 프로토콜 문서를 잃어 버렸다면 이 문제를 해결할 수 있었을까? 결코 해결될 수 없다. 아마도 상대방의 귀찮은 표정을 감수하며 프로토콜 문서를 재요청해야 할 것이다. ElevatorFrameDecoder는 이름 그대로 프레임 내에서 원하는 정보만 추출해 도메인 객체로 변환하는 것이 목적이지 프레임 자체를 표현하고자 작성된 것은 아니기 때문이다.
두 번째 문제는 코드의 길이다. 수정된 ElevatorFrame Decoder에서 특정 바이트를 도메인의 속성 값으로 변환하기 위해 getXXX() 같은 5개 정도의 method를 작성했는데, 만약 그 항목이 수십여 개 정도 되는 어떤 다른 XYZ 시스템의 프로토콜을 분석하기 위해 XYZFrameDecoder를 작성했다면 그 코드의 길이는 상상만으로도 힘이 빠지게 할 것이다. 이와 같은 XYZFrameDecoder는 누군가에게 레거시 코드로 여겨질 가능성이 매우 크다.
세 번째로, 프레임 자체는 변경이 거의 없지만, 프레임을 도메인 객체로 변환하는 ElevatorFrameDecoder는 비즈니스 로직이나 데이터베이스 테이블 구조 변화에 의해 잦은 수정이 발생되는 부분인데 이 부분에 대한 지원이 필요하다는 것이다. 따라서 프레임의 구조를 잘 기술하고 있으면서 도메인 객체로의 변환이 좀 더 수월한 다른 방법이 필요하다. 다음 ElevatorFrame처럼 말이다.
Frame API 구현하기
● 실패 테스트 작성하기
ElevatorFrameTest를 작성해 보자. 이곳에서 Elevator Frame의 기능에 대해 생각해 본다.
● 컴파일되게 하고 테스트 통과시키기
ElevatorFrame과 ElevatorFrameTest는 당연히 컴파일도 안 될 것이다. 일단 컴파일만 되게 하자. 그리고 테스트가 통과되는 것을 목표로 빠르게 수정한다.
● 중복 제거하기
중복(duplication) 제거하기는 비슷한 부분이 여러 곳에서 발견된다면 이를 한 곳으로 몰아 놓는 것을 의미한다. 여기서 한 가지 기억해 둬야 할 점은 방금 통과한 테스트 루틴은 중복을 제거한 후에도 동일하게 통과되어야 한다는 것이다. 이것은 매우 중요하고도 또한 유용하다. 중복을 제거하기 위한 리팩토링(refac toring)이란 코드의 외적 행위(즉, 통과된 테스트 루틴)는 그대로 유지하면서 내부 구조를 변경하는 작업이기 때문이다.
그렇다면 도대체 어느 부분이 중복일까? 중복을 찾는 대상을 좀 더 넓혀야 한다. 중복은 코드뿐만이 아니라 데이터나 혹은 Logic 자체까지 점검되어야 한다. 여기서 return 10;이 중복이다. 이게 왜 중복일까?
ElevatorFrame에서 추가했던 ElevatorFrameItem의 길이를 모두 합치면 ElevatorFrame의 decode된 프레임 길이를 알 수 있기 때문이다. 즉, ElevatorFrame의 decode돤 인덱스는 ElevatorFrameItem들에 의해 이미 정해져 있는데, return 10;이라고 또 다시 중복되게 기술한 것이다.
중복을 제거하기 위해서 ElevatorFrameItem에도 decode()를 추가하고 이를 순차적으로 호출해 인덱스를 더하는 루프를 만들어 본다. 테스트 루틴은 통과되어야 한다는 것을 기억하자.
● 실패 테스트 작성하기
다시 반복해 먼저 실패 테스트 케이스를 추가한다.
● 컴파일되게 하고 테스트 통과시키기
테스트가 통과되는 것을 목표로 빠르게 수정한다.
● 중복 제거하기
ElevatorFrameItem 내에서 데이터의 중복을 찾아 볼 수 있겠는가? 이미 ElevatorFrame에서 addMessage()를 이용해 이 값을 등록했다는 것을 기억하자.
● 실패 테스트 작성하기
ElevarorFrame은 decode()한 후 변환된 객체를 리턴할 수 있어야 한다.
● 컴파일되게 하고 테스트 통과시키기
● 중복 제거하기
reflection을 이용해 도메인 객체의 setter를 동적으로 호출하도록 수정한다. ElevarorFrameItem의 name과 도메인 객체의 setter 이름이 일치하는 경우 message 값을 도메인 객체에 설정하는 것이다.
Frame API 완성하기
ElevarorFrame과 ElevatorFrameItem이 완성되었다. 그러나 아직도 중복이 모두 제거되지 않았다. 먼저 ElevatorFrame을 살펴보자. 만약 XYZ 시스템을 위해 XYZFrame을 만들어 본다고 가정해 보면 아마 대부분의 method가 Elevator Frame과 중복된다. ElevatorFrame Item의 decode()에서는 바이트를 읽어서 Number 객체로 저장하고 있다. 그런데 XYZ 시스템에는 ASCII 같은 String 객체로 해석하는 일이 필요하다면 아마 ElevatorFrameItem과 대부분의 method가 중복될 것이다. 그렇다면 method 중복은 어떻게 제거할까? inter face나 abstract class를 이용해 추상화 레벨을 높여 나가는 방법이 최선이다.
이제 Stream 기반의 물리적인 수신 데이터를 비즈니스 도메인 객체로 변환하는 Netty Decoder 유틸리티 패키지인 Frame API는 <그림 6>과 같이 완성되었다.
<그림 5> ElevatorFrame과 ElevatorFrameltem
<그림 6> Frame API
세 번째 엘리베이터 이야기
FrameSet의 decode()는 reflection을 이용해 도메인의 setter가 동적으로 실행되므로 도메인 객체의 변경이 보다 쉬워졌고 ElevatorFrameDecoder도 <리스트 20>과 같이 간결해졌다.
화재 시스템 이야기
통합 프로젝트에서 화재 감시 통합 화면을 추가로 만들기로 했다. E/V 시스템과 유사한 케이스이다. 이제 더 이상 프레임을 해석하기 위해 바이트를 순서대로 읽지 않는다. Frame API를 이용해 프레임의 구조만 잘 적어 두면 되기 때문이다,
'나의 공간과 이야기 ' 카테고리의 다른 글
UNITAR, Green Cross Korea - 환경 강연회 참가확인 증명서 (0) | 2010.03.13 |
---|---|
야구 숫자 게임 하는 요령 (0) | 2010.03.13 |
君子之所爲 衆人固不識也 (0) | 2010.03.11 |
지방선거? (0) | 2010.03.10 |
지방선거 (0) | 2010.03.10 |