레이블이 디자인 패턴인 게시물을 표시합니다. 모든 게시물 표시
레이블이 디자인 패턴인 게시물을 표시합니다. 모든 게시물 표시

2014년 9월 2일 화요일

기업 통합 패턴 옮긴이 서문


기업 통합 패턴은 2014년 9월 30일 출간됐습니다.

이탈리아 반도에서 출발한 고대 로마는 "모든 길은 로마로 통한다"란 말이 생겨날 정도로 수많은 도로를 건설했다. 이렇게 건설된 도로는 로마를 군사, 경제, 문화적으로 통합시켰고, 이 인프라 덕분에 로마는 거대한 제국으로 성장할 수 있었다. 로마의 도로 포장 기술은 당시 건설했던 도로를 현재까지 사용할 정도로 시대를 초월한 기술이었다. 독일은 1차 대전 패전 직후임에도 속도 제한 없는 아우토반 고속도로 건설을 시작해 현재 세계 최강을 다투는 자동차 생산 선진국이 되었다. 경제적으로 풍족하지 않던 1970년 대 건설한 대한민국의 고속도로도 산업 발전의 촉매가 되었다. 이들 모두 부강할 때 도로를 건설한 것이 아니라, 도로를 건설함으로 부강해졌던 것이다.

기업 내 애플리케이션들도 서비스와 데이터를 이용하기 위해 도로가 필요하다. 그럼 애플리케이션들의 도로는 어떻게 건설해야 할까? 다시 말해 애플리케이션들은 어떻게 통합해야 할까? 어떻게 통합해야 로마의 도로처럼 시대를 초월할 수 있을까? 기업 통합 패턴은 이 질문에 해결책을 제시하는 책이다.

기업 통합 패턴은 2003년 마틴 파울러(Martin Fowler) 시리즈로 출간됐다. 당시 애플리케이션 통합 분야는 수많은 시행착오를 경험했음에도 여전히 시행착오를 반복하고 있었고 찾아낸 통합 해결책은 널리 알려지지 않고 있었다. 이런 혼란스러운 시대에 기업 통합 패턴은 애플리케이션 통합의 여러 방법들 중 비동기 메시징이 최상의 해결책이고 이에 기반한 65개 패턴과 공통 어휘를 제시함으로 애플리케이션 통합을 비로소 패턴과 패턴 언어로써 소통할 수 있게 만들었다. 기업 통합 패턴은 UML의 창시자 중 한 명인 그래디 부치(Grady Booch) 교수가 OOPSLA 2005 컨퍼런스에서 가장 영향력 있는 패턴 책으로 언급할 만큼 애플리케이션 통합에 있어서 독보적인 책이다. 출간된 지 10년이 지났음에도 SOA 분야의 베스트 셀러로서 여전히 많은 독자들이 찾고 있으며 책에 대한 독자들의 평가가 출간 당시보다 더 좋아지고 있는 독특한 현상을 보이는 책이기도 하다.

기업 통합 패턴이 다른 패턴 책들과 다른 점은 패턴 구현체가 통합 프레임워크나 기업 서비스 버스로 존재한다는 점이다. 통합 프레임워크인 Apache Camel, Spring Integration, ESB 미들웨어인 Apache ServiceMix, Mule ESB, Talend ESB 등 점점 많은 오픈 소스 프로젝트들이 기업 통합 패턴을 이용하거나 기반하고 있다. 이들 오픈 소스를 잘 활용하기 위해서는 기업 통합 패턴의 이해가 필수적이다. 상용 통합 제품들도 점점 기업 통합 패턴의 어휘를 사용해 가는 추세다.

고대 로마가 도로 건설을 소홀히 하면서 성 건축을 중심으로만 발전했다면, 지역적으로는 부유한 지역들이 생겨났겠지만, 부실한 도로 인프라로 인해 군사, 경제, 문화는 제대로 유통되지 못해 결국 거대한 로마 제국이 될 수는 없었을 것이다. 마찬가지로 기업도 애플리케이션 통합을 소홀히 한다면, 애플리케이션들 사이 서비스와 데이터 이용 한계로 인해, 기업 성장은 한계에 부딪칠 수 있게 된다. 그러므로 애플리케이션 통합은 기업의 모든 단계에 필수적이다.

기업 통합 패턴은 최상의 애플리케이션 통합 인프라를 위한 시대를 초월한 해결책을 제시한다. 그러므로 기업 서비스를 위해 애플리케이션들을 통합해야 하는 아키텍트, 개발자, 운영자라면 누구나 기업 통합 패턴을 읽어야 할 것이다.


참고 자료




2014년 7월 2일 수요일

분산 비즈니스 트랜잭션과 오류 해결 방법


애플리케이션에서 우리가 일반적으로 트랜잭션이라 부르는 용어는 실제로 두 가지로 구분된다. 첫째, 시스템 트랜잭션(system transaction)[EAA]이다. 시스템 트랜잭션은 일반적으로 단일 데이터베이스 수준에서 처리되는 단일 트랜잭션을 말한다. 둘째, 비즈니스 트랜잭션(business transaction)[EAA]이 있다. 비즈니스 트랜잭션이란 하나 이상의 요청을 처리하기 위해 여러 시스템 트랜잭션을 포함한 트랜잭션을 말한다. 예를 들어 여행 예약을 진행하는 경우 항공 예약, 호텔 예약, 렌터카 예약, 기차 예약 등 여행하면서 타거나 머물거나 묵게 될 각 장소가 모두 예약돼야 완성되는 트랜잭션이 비즈니스 트랜잭션이다. 비즈니스 트랜잭션은 하나 이상의 요청과 일련의 시스템 트랜잭션을 포함하므로 완성되기까지 긴 시간이 필요하다.

일반적으로 기업은 위 두 트랜잭션과 더불어 분산 시스템 트랜잭션(distributed system transaction)과 분산 비즈니스 트랜잭션(distributed business transaction)이 필요한 경우가 있다. 우선, 분산 시스템 트랜잭션은 분산된 자원들이 트랜잭션 프로토콜에 따라 마치 단일 시스템 트랜잭션처럼 보이도록 하는 기술로 2단계 커밋(two-phase commit) 기술로 구현된다. 비즈니스 로직은 분산 트랜잭션을 시작한 애플리케이션에 집중된다. 반면, 분산 비즈니스 트랜잭션은 하나 이상의 요청에 분산된 비즈니스 애플리케이션들이 여럿 참여하고 참여한 애플리케이션이 일련의 시스템 트랜잭션들을 포함할 수 있는 트랜잭션을 말한다. 분산 비즈니스 트랜잭션은 시스템 트랜잭션이 하나 이상 여러 시스템에서 발생한다. 그러므로 분산 비즈니스 트랜잭션은 나머지 다른 모든 트랜잭션을 부분 집합으로 가질 수 있는 가장 일반적인 형태의 트랜잭션이다. 분산 비즈니스 트랜잭션은 비즈니스가 복잡한 만큼 다양한 형태로 등장할 수 있으므로 정형화된 아키텍처 패턴을 갖지 않는다. 분산 비즈니스 트랜잭션은 애플리케이션 수준에서 ACID를 보장하는 방안을 제공해야 한다.

트랜잭션들의 특성을 보면 다음과 같다.

구분
시스템 트랜잭션
비즈니스 트랜잭션
분산 시스템 트랜잭션
분산 비즈니스 트랜잭션
처리 시간
단시간(수초 이내) 장시간 단시간(수초 이내) 장시간
참여자
단일 로컬 시스템 단일 로컬 시스템 복수 분산 시스템 분산 복수 시스템
원자성
알고리즘 보장 애플리케이션 보장 프로토콜 보장 애플리케이션 보장
영속성
알고리즘 보장 애플리케이션 보장 프로토콜 보장 애플리케이션 보장
고립성
알고리즘 보장 애플리케이션 보장 프로토콜 보장 애플리케이션 보장
무결성
알고리즘 보장 애플리케이션 보장 프로토콜 보장 애플리케이션 보장
장애 조치
리두 로그,
롤백 세그먼트 등
오류 해결 전략 수작업 복구 오류 해결 전략
장애 지점
적음
(CPU, 메모리, 디스크)
적음
(CPU, 메모리, 디스크)
많음
(자원, 네트워크, 관리)
많음
(자원, 네트워크, 관리)

일반적으로 분산 환경은 로컬 환경에 비해 여러 가지로 불리하다. (시스템 커밋이 발생하는 위치를 기준으로 분산 시스템 트랜잭션과 분산 비즈니스 트랜잭션을 분산 트랜잭션이라 하고, 시스템 트랜잭션과 비즈니스 트랜잭션을 로컬 트랜잭션이라 하자.) 분산 트랜잭션은 참여 시스템들이 분산됨으로, 경우에 따라 처리 시간이 길어짐으로, 네트워크가 개입함으로, 시스템 관리 주체가 달라짐으로 약점들을 갖게 된다. 만약 분산 트랜잭션에 참여하는 시스템들이 고성능이고, 네트워크도 시스템 버스만큼 성능과 안정성이 보장되고, 업무 흐름이 처리 중에는 절대로 다운이나 정비로 참여 시스템들을 멈추게 하지 않는다면 분산 트랜잭션은 로컬 트랜잭션과 동일한 트랜잭션이 될 것이다. 그러나 현실의 분산 환경은 절대로 로컬 환경과 같을 수 없다. 그러므로 분산 트랜잭션과 로컬 트랜잭션을 동일하게 가정을 하는 것은 현실에 맞지 않는다.

로컬 트랜잭션과 분산 트랜잭션은 로컬 프로시저 호출(Local Procedure Call)과 원격 프로시저 호출(Remote Procedure Call)[EIP]과 많이 닮아 있다. 로컬 프로시저 호출과 원격 프로시저 호출도 단일 프로그램, 단일 시스템, 단일 메모리 공간에서 클라이언트와 서버 프로그램, 네트워크로 연결된 두 시스템으로 실질적으로 다른 인프라 구조를 갖는다. 마찬가지로 로컬 트랜잭션과 분산 트랜잭션도 실질적으로 다른 인프라 구조를 갖는다. 그러므로 로컬 프로시저 호출에서 원격 프로시저 호출로 넘어 갈 때 고려해야 할 수많은 문제들이 로컬 트랜잭션에서 분산 트랜잭션으로 넘어 갈 때도 그대로 등장한다.

기업에서 네트워크를 통해 분산된 애플리케이션이나 시스템(자원)을 결합해 하나의 비즈니스 흐름을 완성하는 애플리케이션 통합은 비일비재하다. 그런데 이 경우 비즈니스 흐름에 참여하는 각 요소들이 원격지에 존재한다는 사실이 쉽게 간과된다. 그리고 로컬 프로그래밍에 익숙한 개발자들은 원격지의 서비스나 자원도 가능하면 로컬 메소드나 자원을 활용하는 것처럼 사용하고 싶어하는 경향이 강하다. 이런 요구로 등장하는 것들이 동기 방식의 원격 프로시저 호출이나 2단계 커밋 같은 기술들이다. 이들은 개발자에게 원격지 서비스나 자원을 마치 같은 시스템의 자원처럼 활용하게 해준다. 이 기술은 언뜻 보기에는 사용하기 편리한 기술처럼 보이지만 비즈니스의 안정성을 위해 상당한 기술적 복잡성과 성능을 희생하게 된다. 즉 참여 시스템(자원)들은 서로 단단히 결합(tight coupling)된다. 그러나 경우에 따라 제품의 도입을 주도하는 이해관계로 인해 이런 사실은 잘 알려지지도 알리려고도 하지 않는다.

분산 시스템의 주요 취약점들에 대해 좀더 살펴보자. 분산 시스템들의 통신은 로컬 시스템 버스 통신이 아닌 이더넷과 같은 저수준 프로토콜을 이용한 원격 통신이다. 그런데 네트워크는 본질적으로 불안정하다. 물론 최근 네트워크 장비들은 일년에 몇 시간도 안 되는 다운 타임을 보장하기도 한다. 그러나 장애 확률은 얼마나 많은 시스템들이 업무 흐름에 참여했느냐에 따라 개별 시스템들의 장애 확률의 합으로 나타난다. 즉 참여 시스템이나 장비들이 많을수록 확률은 높아진다. 예를 들어 업무 흐름에 포함된 10개의 시스템이나 장비가 2시간/년의 장애 시간을 보장하더라도 전체적으로 보면 20시간/년의 장애 시간을 갖는 것이다. 시스템은 게다가 각 시스템의 관리 담당자도 별도로 운영될 가능성이 높다. 이 경우 각 시스템의 담당자는 필요에 따라 시스템을 중지시키거나 다른 작업을 실행시킴으로 비즈니스를 보장하기 위한 가용성이나 성능을 보장받지 못하게 될 수도 있다. 더 중요한 점은 장애 발생 시 장애를 찾거나 복구하는데 점검하고 대처해야 할 지점의 증가로 장애 회복 시간이 가늠 없이 길어 질 수 있다는 점이다. 이런 문제에 대해 시스템을 오래 운영해 본 운영자들은 대부분 동의할 수 있을 것이다. 그러므로 분산 시스템은 분명이 단일 시스템과 다르게 접근해야 한다.

분산 환경이 갖는 취약성으로 인해 분산 시스템 트랜잭션은 해결하기 곤란한 문제를 야기할 수 있다. 즉 처리 시간 동안 참여 시스템이나 자원 중 하나라도 문제가 발생하면 거래는 실패한다. 그러므로 분산 시스템 트랜잭션 기술인 2단계 커밋도 완전한 기술이 아니다.[DTT] 2단계 커밋 기술은 단일 자원만 사용하는 단일 시스템 트랜잭션과 본질적으로 다르다. 2단계 커밋이 안전하다고 생각하는 것은 잘못된 생각이다.

일반적으로 2단계 커밋은 다음과 같은 문제들을 갖는다.

    1) 투표와 커밋 단계 사이 장애로 2단계 커밋에 참여한 전체 시스템의 무결성 훼손이 발생할 수 있으며, 이 경우 무결성을 복원하기 위한 복잡한 수작업이 필요하다.

    2) 2단계 커밋을 지원하지 않는 기존 시스템들과 협업하기 어렵다.

    3) 하나 이상의 요청과 장시간이 걸리는 애플리케이션 통합에 적합하지 않다.

첫째, 통신 장애는 분산 기술에 내재된 속성이다. 네트워크는 신호는 본질적으로 불안정하다. 2단계 커밋의 조정자가 아무리 잘 트랜잭션을 관리하더라도 투표와 커밋 단계 사이에서 네트워크나 참여 자원의 장애가 발생하는 경우 무결성은 파괴된다. 2단계 커밋에서 장애가 발생하면 심각한 문제가 뒤따른다. 일단 2단계 커밋의 오류가 발생하면 애플리케이션은 참여 단계를 프로그램적으로 특정하기 어렵다. 그러므로 2단계 커밋에 참여한 모든 시스템의 상태를 확인해야 하고 일부 시스템의 완료된 커밋과 일부 시스템의 미완료된 커밋을 어떻게 할 것인가에 대해 비즈니스 담당자와 데이터베이스 관리자들과 애플리케이션 담당자들이 모두 합의하는 결론을 도출해야 한다. 참여 자원의 완료된 커밋 조합에 따라 대응 결론이 달라질 수도 있다. 그러므로 2단계 커밋을 마치 단일 시스템 트랜잭션처럼 간단히 생각하고 애플리케이션에 포함시키는 것은 해결할 수 없는 문제를 안고 있는 애플리케이션을 운영하는 것이다. 2단계 커밋을 사용하더라도, 분산 시스템들의 무결성은 정교한 비즈니스 시나리오를 통해 애플리케이션이 해결해야 한다. 둘째, 기업 내 2단계 커밋을 지원하는 시스템이 실제로 많지 않을 수 있다. 그러므로 2단계 커밋을 지원하지 않는 애플리케이션과 협업할 때는 2단계 커밋을 결국 사용하지 못하게 된다. 그러므로 이 경우도 역시 분산 무결성을 정교한 비즈니스 시나리오 분석을 통해 애플리케이션이 해결해야 한다. 셋째, 일반적으로 비즈니스 프로세스는 일련의 시스템 트랜잭션을 포함하는 장시간 업무 흐름을 통해 완성된다. 장시간 처리 비즈니스 프로세스도 2단계 커밋을 적용하면 자원의 무결성과 고립성 문제는 어느 정도 해결할 수 있으나 참여 애플리케이션들 간 성능이나 부하 차이로 인해 전체 성능에 지나친 병목을 발생시킬 수 있다. 이런 경우 2단계 커밋이 제공하는 장점보다 성능 병목으로 인한 단점이 더 커지게 된다. 그러므로 애플리케이션들이 협업하는 애플리케이션 통합에는 2단계 커밋을 적합하지 않을 수 있다.

그러므로 2단계 커밋을 사용해도 좋은 분야가 있고 그렇지 않은 분야가 있다는 것을 반드시 명심해야 한다. 사용의 판단 기준은 애플리케이션의 특성을 먼저 고려한 후 사용 여부를 판단해야 한다. 예를 들어 참여 시스템의 수, 참여 시스템의 성능, 참여 시스템의 2단계 커밋 지원 여부, 비즈니스 프로세스 완성에 필요한 시간, 참여 시스템의 안정성 등등을 고려해야 한다. 일반적으로 2단계 커밋은 비즈니스 애플리케이션 로직이 최소화된 성능과 안정성이 보장된 데이터베이스 시스템들을 하나의 트랜잭션으로 엮는 경우 잘 동작한다.

결국 2단계 커밋을 사용하더라도 비즈니스 트랜잭션의 단계 별 상태를 정교하게 분석해야 하고 일부 성공, 일부 실패에 대한 대응책도 준비해야 한다. 그런데 우리가 2단계 커밋을 사용하는 이유는 이런 무결성 문제를 회피하기 위해서인데 결국 일련의 시스템 트랜잭션들에 대한 성공, 실패 대응책을 고려해야 한다면 2단계 커밋을 사용할 이유가 있겠는가? 혹은 필자가 자주 발생하지도 않는 2단계 커밋의 오류를 너무 확대 해석한 것일 수도 있고, 오류가 발생한다면 무시해 버리면 될 것이 아닌가 반문할 수도 있을 것이다. 그러나 만약에 2단계 커밋이 단순히 극히 짧은 순간의 무결성 보장을 위해 사용된다면 굳이 2단계 커밋을 사용할 이유는 또 무엇인가? 그리고 무결성이 훼손되더라도 무시해 버린다면 더더군다나 2단계 커밋을 사용할 이유는 무엇인가?

2단계 커밋을 사용하지 않더라도 비즈니스 트랜잭션의 무결성과 고립성은 낙관적 오프라인 잠금 패턴(Optimistic Offline Lock pattern)[EAA]이나 비관적 오프라인 잠금 패턴(Pessimistic Offline Lock pattern)[EAA], 성긴 잠금 패턴(Coarse-Grained Lock pattern)[EAA], 암시적 잠금 패턴(Implicit Lock pattern)[EAA] 등을 통해 별도로 보장할 수 있다.

애플리케이션을 통합해야 경우 충분히 고려되지 않은 2단계 커밋을 바로 적용하기 보다 비즈니스 시나리오를 좀더 신중하게 분석해야 한다. 또한 각 참여 시스템의 트랜잭션 단계마다 오류 대응책도 마련해야 한다. 그럼 트랜잭션 오류는 어떻게 해결해야 할까? 애플리케이션 개발자들이 ACID를 보장하기 위해 동기 방식이면서 미들웨어에 의존하는 시스템 트랜잭션이나 2단계 커밋의 관점이 아닌, 비동기 메시징과 애플리케이션 상태 관점에서 비즈니스 트랜잭션 오류에 대한 대응책을 생각해 보자. 비동기 메시징에서는 본질적으로 전체 비즈니스 흐름에 트랜잭션을 적용하기가 사실상 불가능하므로 발생한 오류는 비즈니스적으로 해결한다. 비즈니스 트랜잭션에 발생한 오류를 해결하는 전략들은 다음과 같다.

    1) 취소 전략: 비즈니스 트랜잭션의 오류 해결책으로 가장 간단한 전략이다. 즉 지금까지 진행된 개별 트랜잭션들을 모두 취소시킨다. 어떻게 보면 이 전략은 2단계 커밋의 롤백 전략과 유사하다. 그리고 별로 좋은 전략 같아 보이지도 않는다. 그러나 현실 세계에서는 그런대로 쓸만하다. 취소함으로 발생하는 손실이 거래를 복구함으로 얻는 이익보다 큰 경우 특히 유효하다. 애플리케이션 개발 시 실행(삽입, 갱신) 기능과 취소(삭제, 갱신)기능을 함께 고려하고, 트랜잭션 조정자는 오류를 감지한 후 각 참여 자원의 취소 기능을 요청한다. 2단계 커밋의 롤백과 다른 점은 하나 이상의 요청이 트랜잭션 중간에 개입될 수 있고 긴 시간의 트랜잭션에서 발생한 오류를 해결하는 전략으로, 이런 문제는 2단계 커밋의 롤백으로는 해결되지 않는다.

    2) 재시도 전략: 재시도를 통해 비즈니스 트랜잭션이 성공할 수 있는 업무인 경우 다시 동일한 비즈니스 기능을 시작한다. 예를 들어 외부 자원이 일시적으로 중지된 경우 재시도를 통해 극복될 수 있다. 업무 규칙을 위반한 비즈니스 기능의 요청인 경우 이 전략을 사용하면 안된다. 참여 시스템이 멱등 수신자(Idempotent Receiver)[EIP]가 된다면, 이 전략은 좀더 효과를 발휘한다. 멱등 수신자란 동일한 요구에 대해 수신자의 상태가 변하지 않는 수신자를 말한다. 즉 재시도 전략을 비즈니스 트랜잭션 오류의 전략으로 선택한 경우 각 참여 자원을 멱등 수신자로 만들어야 한다. 멱등 수신자 패턴에 대한 자세한 설명은 [EIP]나 [SDP]를 참조한다. 재시도에는 재시도 횟수를 지정할 수 있다. 재시도 횟수를 넘어선 오류인 경우 다른 해결 전략으로 오류 해결 방법을 변경해야 한다.

    3) 보상 전략: 이미 처리된 비즈니스 오류를 별도의 비즈니스 기능으로 보상하는 전략이다. 예를 들어 은행이 고객의 이체 처리 중 출금은 성공했으나 입금에 실패한 경우 출금 액만큼 은행이 고객에게 입금해주는 전략이다. 이를 위해서는 트랜잭션 오류 상태를 해결 상태로 전이할 수 있는 기능(메소드)을 참여 애플리케이션에 포함해야 한다. 비즈니스 트랜잭션 오류가 발생하면 비즈니스 트랜잭션 조정자는 보상 비즈니스 기능을 참여 시스템에 요청해 비즈니스 트랜잭션의 오류 상태를 정상 상태로 전이시킨다.

비즈니스 트랜잭션을 설계할 때 위 전략들 고려해서 오류 대응책을 마련한다면, 트랜잭션 조정자 애플리케이션이나 트랜잭션 참여 서비스나 자원 애플리케이션들의 역할도 분명해 질 것이고, 2단계 커밋의 미해결 상태의 위험성도 자동적으로 배제할 수 있을 것이다.

분산 애플리케이션 통합의 경우 비즈니스 트랜잭션의 성격을 정확히 분석해 취소, 재시도, 보상 전략 중 해당 트랜잭션에 적합한 전략을 결정해야 한다. 그리고 나서 비즈니스 트랜잭션 조정자, 참여 시스템들에 필요 기능을 구현해야 한다. 이 과정은 생각보다 간단한 과정은 아니다. 비즈니스 트랜잭션의 오류 해결에 대한 명확한 요구를 현업이 정립해야 하고 이에 따라 아키텍트나 개발자들은 트랜잭션 상태 관리, 오류 분석, 전략 실행, 오류 보고, 재시도 제어 등 필요한 요소들을 설계 및 구현해야 한다. 이 때 "기업 애플리케이션 아키텍처 패턴"[EAA]이나 "기업 통합 패턴"[EIP]이나 서비스 디자인 패턴[SDP] 등 기업 애플리케이션을 위한 패턴들을 적절히 활용하고, 필요한 경우 메시징 시스템이나 웹 서비스 시스템을 이용하고, 적절한 프레임워크를 이용할 수도 있을 것이다. 그러므로 이 접근은 시간이나 자원을 좀더 소비한다. 반면 분산 시스템 트랜잭션(2단계 커밋)의 무책임한 사용은 설계 구현을 간단하게 해 시간이나 자원을 절약하게 할지는 몰라도 서비스 중 발생하는 분산 비즈니스 트랜잭션의 오류를 완전히 해결하지 못한다.

비즈니스 트랜잭션 중 무결성을 훼손하는 일부 트랜잭션 오류가 발생 했을 때, 위 세 전략을 언제 적용할지를 생각해 보자. 아마도 애플리케이션 개발자는 가능하면 즉시 해결되도록 전략을 가동시키려 할 것이다. 그러나 시스템이나 네트워크 장애가 길어지는 경우 해결 요청은 어떻게 유지될 수 있을 것인가? 장애가 한 시간가량 지속됐고 무결성이 훼손된 분산 비즈니스 트랜잭션이 10건이라면? 이 오류들을 해결하기 위해 트랜잭션 조정자가 동기 원격 프로시저 호출을 지속적으로 호출해도 참여 시스템이나 네트워크가 복구되지 않는 한 해결되지 못할 것이다. 그러므로 이 문제를 해결하는 가장 좋은 방법은 비동기 방식을 사용하는 것이다. 즉 오류 해결 요청 메시지를 메시징 시스템 큐에 추가하는 것이다. 그러면 참여 시스템은 메시지 큐에서 요청 메시지를 수신해 필요한 해결 기능을 수행할 수 있을 것이다. 그런데 비즈니스 트랜잭션의 오류 해결 기능이나 정상적인 비즈니스 트랜잭션의 기능 수행이나 애플리케이션에서 본다면 별로 다르지 않은 기능이다. 그러므로 정상 비즈니스 트랜잭션은 동기 원격 프로시저 호출 방식으로 오류 해결 비즈니스는 비동기 메시징으로 개발하는 것은 같은 기능을 두 번 전혀 다른 패러다임을 사용해 구현하는 모양새가 된다. 그러므로 둘 중 한 가지 방법을 선택해야 한다. 필자는 비동기 메시징 방식의 애플리케이션 통합을 권장한다.

"기업 통합 패턴(Enterprise Integration Patterns)"의 저자 그레거 호프는 "스타벅스는 2단계 커밋을 사용하지 않는다."[Hohpe]란 제목의 글을 블로그에 게시한 적이 있다. 필자가 언급한 분산 비즈니스 트랜잭션 오류 해결 전략도 그의 글에서 인용한 것이다. 그러므로 기업 애플리케이션 통합에 분산 비즈니스 트랜잭션이 포함될 때, 2단계 커밋을 사용하려는 경우 2단계 커밋이 꼭 필요한지, 2단계 커밋을 사용할 수 있는지, 2단계 커밋의 오류 시 해결할 대응 전략이 있는지, 애플리케이션 수준에서 트랜잭션을 해결할 수 있는지, 트랜잭션 오류에 대한 해결 전략은 완비됐는지 등을 충분히 고려해야 할 것이다.


참고 자료


2014년 4월 8일 화요일

메시지 디스패처

제14회 한국자바개발자 컨퍼런스에서 필자가 발표한 "100줄로 완성하는 웹 서비스 플랫폼"은 원래 기업 통합 패턴(Enterprise Integration Patterns)의 메시지 디스패처 패턴(Message Dispatcher Pattern)을 설명하기 위한 프로젝트로 블로그 게시를 준비 중이다가 컨퍼런스에서 먼저 발표한 주제였다. 발표 당시 사실 전하고 싶었지만 제한된 시간으로 제대로 전하지 못했던 메시지 디스패처 패턴에 대한 이야기를 이 글을 통해 전하고자 한다.

기업 통합 패턴(Enterprise Integration Patterns)은 메시지 디스패처 패턴(Message Dispatcher Pattern)을 설명한다. 이 패턴은 다음과 같은 상황을 해결한다.


하나의 채널 위에서 메시지를 처리하는 복수의 소비자들을 조종하려면 어떻게 해야 할까?



Enterprise Integration Patterns, Gregor Hohpe, Bobby Woolf; Addison-Wesley 2004, 509쪽 그림 인용

메시지 디스패처는 경쟁 소비자 패턴(Competing Consumer Pattern)과 비슷하지만, 각 경쟁 소비자가 갖는 대등한 소비 패턴보다 정교하게 소비자들을 조종할 수 있는 패턴이다. 일반적으로 경쟁 소비자는 독립된 프로세스로 동작하는 반면 메시지 디스패처의 소비자는 스레드로 동작한다.

일반적으로 디스패처는 익숙한 용어이다. 가장 익숙하게 사용되는 곳은 Web MVC 프레임워크이다. 그러나 디스패처는 Web MVC보다 하부 구조인 서블릿 컨테이너의 매핑 정의에서부터 사용된다. 즉 서블릿 컨테이너의 배치 서술자(deployment descriptor)인 web.xml 파일에 <servlet-mapping> 태그의 URL 패턴과 서블릿을 지정하는 부분이 바로 서블릿 컨테이너에서 요청 URL과 서블릿을 연결하는 디스패치 매핑의 정의다. Spring MVC는 이 하부 디스패처 매핑에 다시 MVC 패턴을 위한 Spring의 자체 DispatcherServlet 클래스를 추가하여 서블릿 컨테이너가 수신한 웹 요청을 Spring 서비스(컨트롤러)로 디스패치한다.

웹에서 사용하는 디스패처는 웹의 요청과 응답을 중점으로 처리한다. 웹 디스패처는 수신한 요청으로부터 처리할 서비스를 탐색하고 탐색된 서비스로 요청을 전달하고 실행하여 실행 결과를 응답으로 반환하는 일련의 실행 작업을 처리한다. 반면 기업 통합 패턴의 메시지 디스패처는 메시지 중심이다. 기업 통합 패턴의 메시지 디스패처는 요청된 메시지를 실행자(performer)에게 디스패치한다. 메시지 디스패치 패턴에서 메시지 디스패처가 동기 방식으로 사용되는 경우 웹 디스패처처럼 메시지 디스패처의 요청 채널과 응답 채널은 동일하고, 비동기 방식으로 사용되는 경우 요청 채널과 응답 채널은 분리된다. 그러므로 메시지 디스패처를 웹 디스패처보다 더 포괄적인 패턴이라고 볼 수 있다. 즉 웹 디스패처도 메시지 디스패처의 일종으로 볼 수 있다.

웹 디스패처는 이벤트 기반 소비자로 일반적으로 POSA2의 반응자 패턴(Reactor Pattern)으로 구현된다. 반면 메시지 디스패처는 능동적인 폴링 소비자 패턴(Polling Consumer Pattern)이거나 수동적인 이벤트 기반 소비자 패턴(Event-Driven Consumer Pattern)도 될 수 있다. 반응자 패턴을 사용하는 웹 디스패처는 클라이언트의 요청에 대해 수동적으로 반응한다. 그러므로 클라이언트의 요청이 급증하는 경우 웹 디스패처는 병목이 발생할 수 있고, 웹 서비스가 장애인 경우 클라이언트는 웹 요청을 전달하지 못할 수가 있다. 이 문제를 해결하려면 별도의 부하 관리 로직을 웹 디스패처에 추가하여 부하가 급증하는 상황에 대처할 수 있다. 그러나 부하가 급증하는 상황을 우회하는 것은 그렇게 간단한 문제가 아니다. 웹 서비스에 장애가 발생하지 않게 하려면 웹 서비스의 안정성에 많은 노력을 기울여야 한다. 반면 메시지 디스패처는 능동적인 폴링 소비자가 될 수 있으므로, 자신의 상황에 맞추어 메시지의 처리량을 조정할 수도 있게 된다. 요청 메시지의 부하가 급증하거나 서비스를 중지하더라도 처리를 대기하는 메시지는 채널 즉 메시징 시스템의 큐에 일정시간 보관되기 때문이다. 즉 메시지 디스패처 패턴은 웹 디스패처 패턴보다 포괄적이기 때문에, 웹 디스패처 패턴의 부하 관리 방법에서는 고려되지 않는 비동기 시스템을 이용한 흐름 제어를 포함할 수 있다.

B2C를 위한 웹 서비스는 브라우저 화면 렌더링 위주의 컴퓨팅 시간이 길지 않은 단순 업무이고, 고성능 서버와 부하 분산기(Load Balancer)와 요청 부하의 예측을 통해 웹 서비스의 병목의 문제를 대비한다. 그리고 부하가 급증하는 경우나 애플리케이션에 장애가 발생한 경우, 네트워크 장비를 이용하여 웹 요청을 단순히 우회시키기도 한다. 그러나 B2B 웹 서비스에서는 위에 언급한 부하 급증이나 애플리케이션 장애 상황에서도 서비스를 중단할 수 없는 경우가 있다. 이런 장애 상황에서도 요청 기업은 장애 상태에 대한 구체적인 응답을 요구할 수도 있고, 요청의 수신도 요구할 수도 있다. 즉 기업 간 서비스는 일반 고객 서비스보다 거래의 중요도가 훨씬 더 높을 수 있기 때문이다. 이런 요구를 해결하기 위해서는 기업 통합 패턴의 보장 전송 패턴(Guaranteed Delivery Pattern)이 필요하다.

디스패처를 구현하는 일은 간단하지 않다. 디스패처는 독립적으로 동작하는 것이 아니라 요청 수신, 수행자 탐색, 할당, 전달, 처리, 응답 발신이 모두 포함돼야 제대로 동작할 수 있는 패턴이기 때문이다. 디스패처를 패턴으로 해석하는 방식도 제 각각이다. J2EE의 Dispatcher-View 패턴, POSA1의 Client-Dispatcher-Server 패턴, 기업 통합 패턴의 Message Dispatcher 패턴 등 Dispatcher를 포함하는 패턴은 다양하게 존재한다. 이 패턴들은 각각 강조하는 점과 쓰임새가 다르다. 그 중 "100줄로 완성하는 웹 서비스 플랫폼"에서는 메시지 디스패처의 관점의 디스패처를 구현했다.

JCO에서 발표한 “100줄로 완성하는 웹 서비스 플랫폼”은 발표 자료, 동영상, 프로그램 소스가 모두 공개되어 있으므로 통합 프레임워크가 웹 서비스를 구현하기 위해 어떻게 사용되었고 기업 통합 패턴의 메시지 디스패처가 어떻게 구현되었는지 해당 자료들을 참조하면 좀더 잘 이해할 수 있을 것이다.


참고 사이트


2013년 11월 6일 수요일

기업 통합 패턴 목록


필자가 올리는 기업 통합 패턴(Enterprise Integration Patterns) 관련 글을 읽는 독자들이 기업 통합 패턴의 목록이라도 알 수 있도록, 기업 통합 패턴의 전체 패턴 목록을 정리해 보았다. 기업 통합 패턴은 기업 통합을 위한 메시징 기반의 패턴으로 다음과 같은 상황(context)에 대한 방법론을 패턴으로 제공한다. 이 패턴들에 대한 상세한 방법론에 대해 좀더 관심이 있는 독자라면 원서를 읽어보거나, 출판될 필자의 번역서를 참조할 수 있을 것이다.



기업 통합 패턴 개요




메시징 엔드포인트(Messaging Endpoints)


메시지 엔드포인트(Message Endpoint)
메시징 시스템을 통해 메시지를 수신하고 발신하려면 애플리케이션은 어떻게 해야 할까?
메시징 게이트웨이(Messaging Gateway)
애플리케이션의 나머지 부분으로부터 메시징 시스템에 대한 액세스를 캡슐화하려면 어떻게 해야 할까?
메시징 매퍼(Messaging Mapper)
도메인 객체와 메시징 인프라의 독립성은 유지하면서, 이들 사이에 데이터를 이동시키려면 어떻게 해야 할까?
트랜잭션 클라이언트(Transactional Client)
클라이언트는 메시징 시스템과 함께 어떻게 트랜잭션을 제어할 수 있을까?
폴링 소비자(Polling Consumer)
준비된 애플리케이션만이 메시지를 소비하게 하려면 어떻게 해야 할까?
이벤트 기반 소비자(Event-Driven Consumer)
애플리케이션은 어떻게 사용 가능한 메시지를 자동으로 소비할 수 있을까?
경쟁 소비자(Competing Consumer)
메시징 클라이언트가 복 수개의 메시지들을 동시에 처리하려면 어떻게 해야 할까?
메시지 디스패처(Message Dispatcher)
하나의 채널에 대한 복수 소비자들은 자신들의 메시지 처리를 어떻게 조정할 수 있을까?
선택 소비자(Selective Consumer)
수신하려는 메시지만 선택하려면, 메시지 소비자는 어떻게 해야 할까?
영속 구독자(Durable Subscriber)
구독자의 수신 중지 동안, 발생 가능한 메시지 누락은 어떻게 방지할 수 있을까?
멱등 수신자(Idempotent Receiver)
메시지 수신자는 중복 메시지를 어떻게 처리할 수 있을까?
서비스 액티베이터(Service Activator)
애플리케이션은 메시징 기술과 비 메시징 기술 모두를 통해 호출되는 서비스를 어떻게 설계할 수 있을까?


메시지 구축(Message Construction)


메시지(Message)
메시지 채널로 연결된 두 애플리케이션은 어떻게 정보를 교환할까?
명령 메시지(Command Message)
애플리케이션들은 프로시저 호출에 어떻게 메시징을 사용할 수 있을까?
문서 메시지(Document Message)
애플리케이션들은 데이터 전송에 어떻게 메시징을 사용할 수 있을까?
이벤트 메시지(Event Message)
애플리케이션들은 이벤트 전송에 어떻게 메시징을 사용할 수 있을까?
요청 응답(Request-Reply)
애플리케이션은 어떻게 요청 메시지를 발신하고 응답 메시지를 수신할 수 있을까?
반환 주소(Return Address)
응답자는 응답 메시지를 전송할 채널을 어떻게 알까?
상관관계 식별자(Correlation Identifier)
요청자는 수신한 응답으로 어떤 요청에 대한 응답인지를 어떻게 알 수 있을까?
메시지 순서(Message Sequence)
많은 양의 데이터를 메시징을 사용하여 어떻게 전송할 수 있을까?
메시지 만료(Message Expiration)
메시지가 오래되어 사용 중단이 필요한 때를 발신자는 어떻게 지정할 수 있을까?
포맷 표시자(Format Indicator)
변경에 잘 대응하려면 메시지의 데이터 포맷은 어떻게 설계돼야 할까?


메시지 라우팅(Message Routing)


파이프 필터(Pipes and Filters)
독립성과 유연성을 유지하면서 메시지에 대한 복잡한 처리도 수행할 수 있으려면 어떻게 해야 할까?
메시지 라우터(Message Router)
개별 처리 단계들의 결합을 제거하여 메시지를 조건에 따라 서로 다른 필터로 전달할 수 있게 하려면 어떻게 해야 할까?
내용 기반 라우터(Content-Based Router)
단일 로직 기능이 여러 시스템에 물리적으로 분산되어 있는 경우 어떻게 처리해야 할까?
메시지 필터(Message Filter)
불필요한 메시지를 컴포넌트는 어떻게 수신하지 않을 수 있을까?
동적 라우터(Dynamic Router)
효율성을 유지하면서도 목적지에 대한 라우터의 종속성을 없애려면 어떻게 해야 할까?
수신자 목록(Recipient List)
수신자들이 가변적인 경우 어떻게 메시지를 라우팅할까?
분할기(Splitter)
메시지에 포함된 요소들을 각각 처리하려면 어떻게 해야 할까?
수집기(Aggregator)
서로 관련성이 있는 개별 메시지들은 어떻게 묶어 처리할 수 있을까?
리시퀀서(Resequencer)
순서가 뒤바뀐 메시지들 어떻게 올바른 순서로 되돌릴 것인가?
복합 메시지 처리기(Composed Message Processor)
서로 다른 처리를 요구하는 복수 개의 요소들을 포함한 메시지를 처리하면서도, 전체 메시지 흐름을 유지하려면 어떻게 해야 할까?
분산기 집합기(Scatter-Gather)
수신자들 각각에게 메시지를 발신하고 수신해야 하는 경우, 전체 메시지의 흐름은 어떻게 관리할까?
회람표(Routing Slip)
결정되지 않은 일련의 처리 단계들로 메시지를 라우팅하려면 어떻게 해야 할까?
프로세스 관리자(Process Manager)
설계 당시에는 필요한 단계가 알려지지 않았고 순차적이지 않을 수 있는 복합 처리 단계로 메시지를 라우팅하려면 어떻게 해야 할까?
메시지 브로커(Message Broker)
메시지 흐름의 중앙 제어를 유지하면서, 어떻게 메시지와 목적지의 결합을 제거할 수 있을까?


메시지 변환(Message Transformation)


메시지 변환기(Message Translator)
다른 데이터 포맷을 사용하는 시스템들이 메시징을 사용하여 서로 통신하려면 어떻게 해야 할까?
봉투 래퍼(Envelope Wrapper)
메시지 헤더 필드, 암호화 같은 특별한 포맷을 가진 메시지 교환에 기존 시스템을 참여시키려면 어떻게 해야 할까?
내용 보탬이(Content Enricher)
수신한 메시지에 필요한 데이터 항목이 완전하지 않은 경우 어떻게 다른 시스템과 통신할 수 있을까?
내용 필터(Content Filter)
큰 메시지에서 일부 데이터만 필요한 경우, 메시지 처리를 어떻게 단순화할까?
번호표(Claim Check)
시스템을 가로질러 전송되는 메시지의 데이터 크기를 정보 손실 없이 줄이려면 어떻게 해야 할까?
노멀라이저(Normalizer)
의미는 같지만 다른 포맷으로 수신된 메시지는 어떻게 처리할까?
정규 데이터 모델(Canonical Data Model)
다른 데이터 포맷을 사용하는 애플리케이션들을 통합할 때, 어떻게 하면 의존성을 최소화할 수 있을까?


시스템 관리(System Management)


제어 버스(Control Bus)
여러 플랫폼에 걸쳐 분산되어 있는 메시징 시스템을 효과적으로 관리하려면 어떻게 해야 할까?
우회기(Detour)
검증, 테스트, 디버깅 등을 수행하는 단계로 메시지를 통과시키려면 어떻게 라우팅해야 할까?
와이어 탭(Wire Tap)
포인트 투 포인트 채널을 지나가는 메시지는 어떻게 검사할 수 있을까?
메시지 이력(Message History)
느슨하게 결합된 시스템에서 메시지의 흐름을 어떻게 효과적으로 분석하고 디버깅할 수 있을까?
메시지 저장소(Message Store)
메시징 시스템의 느슨한 결합과 임시 보관적 특성을 방해하지 않으면서 어떻게 메시지 정보를 보고할 수 있을까?
스마트 프록시(Smart Proxy)
요청자가 지정한 반환 주소로 응답 메시지를 게시하는 서비스의 메시지는 어떻게 추적할 수 있을까?
테스트 메시지(Test Message)
컴포넌트가 메시지를 처리하면서 내부 오류로 인해 잘못된 메시지를 내보낸다면 어떤 일이 생길까?
채널 제거기(Channel Purger)
테스트 또는 운영 시스템이 교란되지 않게 채널 위에 남겨진 메시지들을 관리하려면 어떻게 해야 할까?


참고 사이트

2012년 8월 20일 월요일

동기 호출(Synchronous Call)에 대한 고찰


필자가 올린 “동기 호출(Synchronous Call), 비동기 호출(Asynchorous Call)” 블로그에서는 동기 호출, 비동기 호출의 정의에 대해 설명했었다. 여기에 그 정의를 다시 옮겨 보면 다음과 같다.
애플리케이션 프로그램에서 동기 호출(Synchronous Call)이란 애플리케이션 프로세스(스레드)에서 하부작업(프로시저, 함수, 메서드) 요청 시 요청된 하부작업이 진행되는 동안 호출 프로세스(스레드)의 실행 흐름이 멈추게 되는 호출을 말한다.
애플리케이션 프로그램에서 비동기 호출(Asynchronous Call)이란 애플리케이션 프로세스(스레드)에서 하부작업 요청 시 요청된 하부작업의 실행 또는 종료와 관계없이 호출 프로세스(스레드)의 실행 흐름은 계속되는 호출을 말한다. 이런 호출 방식에서는 하부작업의 실행완료 시점을 호출 프로세스(스레드)가 정확이 알지 못하므로 호출 프로세스(스레드)와 하부작업은 하부작업의 실행 결과를 둘 사이 약속된 결과 영역 조회나 하부작업이 호출 프로세스(스레드)를 호출하는 Callback 메커니즘을 통해 확인한다.

지금까지 컴퓨터는 빠른 계산 능력을 사용하여 사용자에게 최대한 빠른 응답을 제공하는 방향으로 발전해 왔다. 그 결과 사용자는 컴퓨터에 어떤 요구를 하던지 거의 실시간으로 컴퓨터로부터 응답을 받게 되었다. 만약 컴퓨터에 요구한 응답이 몇 초 이상 걸리게 되는 경우 사용자는 몇 초의 기다림에 불안감까지 느낄 정도로 컴퓨터는 사용자의 요구에 대한 신속한 처리를 보장해 주었다.

이런 빠른 응답을 제공하기 위하여 컴퓨터 하드웨어는 무어의 법칙에 따라 18개월마다 성능을 두 배씩 성능을 향상해 왔다. 그러나 그 하드웨어에서 동작하는 프로그램은 상당히 정체된 발전 단계를 거쳐 왔다. 한 언어가 주류 언어가 되면 적어도 10년 이상 그 언어로 프로그램은 작성되고 그 뒤로도 몇 십 년을 사용하는 방식이었다. 예를 들어 현재 주류 언어 중 하나인 C (대표적 구조적 프로그램 언어) 언어는 태어난 지 이미 40년이 지난 언어이고 Java (객체 지향 언어) 언어도 1995년에 때어나 벌써 17년이나 지나고 있다. 이렇게 하드웨어 발전 속도와 프로그램 언어 사이에 발전의 차이는 프로그램에서 발전된 하드웨어를 최대한 활용하는 점점 어려운 상황에 접어 들게 되었다.

예를 들어 하드웨어 CPU가 늘어났지만 프로그램은 한 CPU에서 밖에 동작하지 못한다던 지, 하드웨어 레지스터 크기가 확장되어 더 많은 정보를 표현할 수 있지만 프로그램 언어의 자료형이 지원을 하지 못한다던 지 등 하드웨어에 도입된 중요한 기술을 프로그램은 좀더 오랜 기간을 거쳐야 비로소 그 기술에 적응하고 활용할 수 있게 된다. 그 중에서도 작업 처리 측면에서 보면 현재 주류 프로그램 언어들이 가진 순차적 명령 처리를 패러다임은 이런 하드웨어 성능 활용에 최대 걸림돌이 되고 있다. 즉 모든 실행은 순서를 가지고 하나씩 실행해 나가는 방식으로 어떤 일이던지 시작에서 끝까지 실행 흐름이 줄을 서서 실행된다. 이 방식은 객체 지향 언어에서도 마찬가지인데 여기서는 액터와 오브젝트 사이의 역할을 나누지만 실행 흐름에 대해서는 순차적 명령 처리 패러다임의 범주에서 벗어나지 못하고 있다. 즉 하부작업 요청 시 함수(메서드) 호출과 결과 리턴이라는 기본 골격은 구조적 프로그래밍 언어와 같다.

일반적으로 프로그램은 실행 흐름작성, 하부작업 실행, 하부작업 실행 결과 활용 등으로 작성된다. 이 과정에서 하부작업 실행과 그 결과를 구성하는 방식은 일반적으로 동기 호출 방식을 사용하게 된다. 즉 하부작업 요청 시 호출 쪽 본작업은 실행을 멈추고 하부작업이 리턴 될 때까지 대기하는 방식이다.

동기 호출은 하부작업의 결과물 획득에 순차적 처리가 가능한 리턴이라는 직관적이고 간결한 메커니즘을 제공한다. 즉 동기 호출은 하부작업이 진행되는 동안 호출 프로세스(스레드)는 실행이 중지되고 하부작업의 리턴 시 자동으로 실행이 계속되어 실행의 순차적 논리흐름을 잘 표현할 수 있다. 이에 비하여 비동기 호출은 하부작업의 완료까지 프로세스(스레드)가 대기하지 못하므로 실행의 순차적 논리흐름을 표현하는데 어려움이 있다. 좀더 구체적으로 비동기 호출은 하부작업의 결과물을 획득하는 메커니즘으로 Call Back 방식을 통한 결과 획득 또는 약속된 장소에 하부작업 결과를 확인하는 Poll 방식이 있는데 Call Back 방식은 호출 프로세스(스레드)의 작업에 대하여 순차적 처리 흐름을 보장하지 못하고 Poll 방식은 호출 프로세스(스레드)의 순차적 작업 흐름 처리는 가능하지만 동기 방식에 비하여 특별한 장점도 없으면서 처리 과정만 복잡한 문제를 일으킨다. 결론적으로 동기 호출은 실행의 순차 처리가 쉽고 비동기 호출은 실행의 순차 처리가 어렵다. 이런 측면에서 애플리케이션 개발 시 동기 호출은 비동기 호출에 비해 훨씬 지배적인 호출 방식으로 활용되고 이다.

그럼 이제부터 동기 호출 방식에 몇 가지 질문을 해보자 우선 처리 시간의 관점에서 동기 호출을 살펴보자. 일반적으로 동기 호출은 즉시 답을 요구하는 실시간 애플리케이션에 적합한 호출 방식이다. 즉 동기 호출의 처리는 아주 짧은 순간에 처리를 마쳐야 하는 경우에 주로 사용된다. 그런데 동기 호출을 미시적으로 살펴보면 (여기에서는 단일 애플리케이션에서 동기 호출을 살펴본다.) 호출 명령에서 호출이 일어난 실행 주소를 보관하고 하부작업 주소로 실행 주소를 옮겨 하부작업을 실행하고 하부작업 실행 결과를 저장하고 다시 호출 실행 주소로 작업을 옮겨 실행을 계속하게 된다. 이 과정에서 하부작업의 실행 시간이 반드시 일정 시간 필요하게 되는데 결과적으로 하부작업 요청시각(t0)와 하부작업 리턴 시각(t1) 사이 Δt = t1 – t0 이 진행된 후 본작업은 다음 작업을 할 수 있게 된다. 다시 말해 동기 호출도 엄밀히 말하면 동시성을 보장하지 못하고 반드시 비 동시성을 전제하는 호출인 것이다. 이렇게 동기 호출에 대하여 하부작업 처리를 위한 처리 시간이 필요하다는 것을 이해 한다면 결국 동기 호출도 실행 흐름은 순차적이지만 호출에 따른 실행 시간은 비동시적인 것이다.

동기 호출의 가장 큰 특징은 실행 흐름의 제어가 손쉽다는 것이다. 즉 앞에서 말한 하부작업 호출과 그 결과 획득의 프로그램의 간결성이다. 동기 호출을 사용하면 애플리케이션은 하부작업 처리 결과를 획득하기 위하여 별다른 노력 없이 단순히 함수(메서드)만 호출하면 된다.
int in, out;
in = 1;
out = subprocess(in);
System.out.println(in + " => " + out);
위 Java 소스 세 번째 줄을 보면 본처리 프로그램에서 하부처리 메서드 호출과 그 처리 결과 획득과정이 단 한 줄로 표현되는 것을 볼 수 있다. 이와 같은 동기 호출의 사용은 단순한 구조를 가지고 있고 단일 애플리케이션에서 분산 애플리케이션까지 일관되게 적용된다. 즉 분산 애플리케이션에서도 단일 애플리케이션의 호출 방식과 같은 방식으로 동기 호출을 사용한다. 그러나 이런 편리한 동기 호출을 지원하기 위하여 분산 애플리케이션은 통신 프로토콜, 하부 라이브러리, 프레임워크 부분에서는 상당히 복잡한 동기화 작업들을 구현하고 있다.

그럼 분산 애플리케이션에서 동기 호출 시 하부작업의 처리에 문제가 발생하면 어떻게 될까? 만약 하부작업에서 흐름제어를 관리할 수 있는 정도의 문제가 발생하는 경우에는 하부작업은 오류를 리턴하면 된다. 그러나 만약 하부작업의 문제가 흐름제어를 관리할 수 없을 정도의 심각한 문제가 발생하여 하부작업이 리턴을 할 수 없게 되면 전체 실행 흐름은 정지하고 애플리케이션은 정지(Hang-up)상태가 된다. 그리고 요구된 거래는 성공도 실패도 알 수 없는 상태에 이르게 될 것이다. 이렇게 하부작업에 흐름제어를 잃는 장애가 발생하면 애플리케이션은 중대한 문제 상황에 빠지게 된다. 그렇지만 일반적인 동기 호출에서는 하부작업 호출 시 타임아웃을 설정할 수 없다. 만약 하부작업 호출 시 타임아웃을 설정하고 실행 흐름 제어를 회복하기 위해서는 호출 프로세스(스레드)에 하부작업 완료를 확인하는 별도 스레드 등을 추가해서 하부작업을 모니터링 하는 기능을 추가 해야 한다. 그러나 일반적인 동기호출 사용 패턴에서 이런 문제까지는 고려는 되지 않고 있다. 예를 들어 J2EE 동기 호출 규격에도 흐름제어 회복을 위한 타임아웃에 대한 고려는 없다. 그러나 애플리케이션 운영측면에서 동기 호출 방식의 하부작업 호출에서 흐름제어 회복을 위한 타임아웃을 고려하지 않는다면 정지된 본작업 프로세스(스레드)는 아무런 일을 하지 않는 좀비가 되고 시스템 자원만 점유하게 되어 시스템 자원 가용성 및 성능에 영향을 줄 수 있다. 그리고 하부작업 흐름제어 회복 불능 장애 발생 시 더 심각한 애플리케이션 비즈니스 처리 상태의 불확실성을 만들어 낼 것이다.

동기 호출 패턴이 타임아웃 처리에 대한 관심이 적은 이유는 과거 단일 시스템에서 단일 프로세스로 동작하는 애플리케이션에서 사용하던 함수 호출에 대한 경험 때문일 것이다. 단일 프로세스 환경에서는 프로세스 내 실행 흐름이 하나 밖에 없기 때문에 본작업이던 하부작업이던 모두 같은 실행 흐름 내에 존재한다. 그러므로 하부작업이 흐름제어를 회복할 수 없는 문제가 발생하면 이미 전체 애플리케이션의 문제이기 때문에 애플리케이션은 동일한 정지(Hang-up) 현상을 보이게 된다. 그러나 분산 애플리케이션에서는 본작업의 실행흐름을 가진 프로세스(스레드)와 하부작업의 흐름을 가진 프로세스(스레드)가 각각 독립적인 실행 흐름을 가질 수 있다. 그 결과 동기 호출 시 본작업에서 요청한 하부작업의 실행흐름이 불능인 경우에도 본작업 프로세스(스레드)의 실행 흐름은 영향을 받지 않고 다음 처리를 할 수 있다. 단 본작업이 하부작업의 실행흐름을 잃는 장애에 대하여 자신의 실행 흐름을 계속하기 위해서는 본작업은 하부작업의 처리시간을 한정하는 타임아웃 기능을 추가해야 한다. 그러나 앞서 말했듯이 동기 호출에 대한 타임아웃 고려는 아직 개별적 고려 사항으로 일부 라이브러리나 규격에서 설정 또는 패러미터 입력으로 등장하고 있는 정도이다.

분산 애플리케이션 환경에서 동기 호출의 안정성에 대한 문제를 살펴보자. 일반적으로 단일 하드웨어에서 단일 애플리케이션의 동기 호출은 충분히 신뢰할 수 있다고 볼 수 있다. 그러나 분산 애플리케이션 환경에서 동기 호출은 시스템 하드웨어, 네트워크 등 관련된 구성 요소들이 모두 충분히 신뢰할 수 있다는 전제하에 안정성을 기대할 수 있다. 그러나 분산 애플리케이션에서는 아래와 같은 경우 동기 호출에 대한 신뢰성 문제를 가질 수 있다.

  1. 복수 시스템에 각각 동작하는 애플리케이션들 사이 동기 호출이 발생할 경우 시스템과 시스템의 네트워크 안정성은 프로세스 내 단일 애플리케이션의 하부작업 호출에 사용되는 시스템 내부 버스의 안정성보다 상당히 낮다. 그리고 네트워크 장애 발생 시 동기 호출로 연동되는 일련의 애플리케이션은 모두 중단될 수 밖에 없다. 
  2. 복수 시스템에서 각 시스템의 애플리케이션이 동기 호출을 사용하는 경우 상대 시스템의 안정성을 기대할 수 없는 경우 가 있다. 다시 말해 각 시스템에 소유 주체가 다름으로 각 시스템에 동작하는 애플리케이션의 동작 여부를 항상 보장 받지 못할 수 있다. 어느 쪽이던 시스템의 장애 또는 애플리케이션에 장애가 발생하면 동기 호출로 연동되는 일련의 애플리케이션은 모두 중단될 수 밖에 없다.

동기 호출 메커니즘은 프로그램 언어가 등장한 이래 가장 일반적인 작업 메커니즘이었다. 그러나 하드웨어가 발전하고 시스템들은 네트워크를 통해 연결되고 단일 애플리케이션에서 분산 애플리케이션으로 작업 규모가 커지면서 동기 호출의 한계가 점점 등장하고 있다. 동시성 또는 아주 짧은 처리 시간을 요구하는 업무들을 처리하는 방식에 동기 호출은 적합하지만 동시성을 지원하기 위하여 시스템 안정성, 네트워크 안정성, 애플리케이션 안정성을 모두 보장해야 하는 어려움이 따르게 되었다. 이런 어려움 속에서도 동기 호출 방식의 패러다임은 여전히 가장 선호하는 호출 방식으로 세상을 지배하고 있다. 그러나 분산 애플리케이션 환경에서 동기 호출을 사용할 때 하부작업 처리 시간, 실행 제어 회복, 분산 구간의 안정성 등을 고려해야 하므로 동기 호출 패턴이 예전 단일 애플리케이션과 비교하여 그렇게 단순하지 않게 되었으며 이와 관련하여 대안적인 패턴으로 비동기 호출의 필요성이 재 발견되고 있다.

지금까지 기술적 측면에서 동기 호출에 대한 몇 가지 사항을 생각해 보았다. 그런데 우리의 일상적인 비즈니스 패턴은 어떤 패턴일까도 고민해 볼 필요가 있다. 우리들이 살아가는 세상은 동기 호출 방식이 지배하는 세상일까 비동기 호출 방식이 지배하는 세상일까? 우리는 개인, 조직, 국가, 세계와 어떻게 의사 소통하며 살아가는가에 대하여 좀더 고민해 볼 필요가 있다. 그래야 동기, 비동기 문제를 좀더 객관화시킬 수 있고, 우리 생활과 잘 접목되는 애플리케이션 사용 방향을 찾을 수 있을 것이다.

2012년 8월 14일 화요일

동기 호출(Synchronous Call), 비동기 호출(Asynchorous Call)


프로그램에서 동기, 비동기 호출은 멀티 프로세스, 멀티 스레드 환경에서 동작하는 애플리케이션을 작성할 때 등장한다. 멀티 프로세스(스레드) 환경 즉 복수 프로세스(스레드)가 병렬적으로 동시에 실행되는 환경에서 각 프로세스(스레드)는 독립적인 실행 흐름을 가지고 동작하는데, 한 프로세스(스레드)가 다른 프로세스(스레드)에게 하부작업(프로시저, 함수, 메서드)을 요청할 경우 호출한 프로세스(스레드)의 실행 흐름의 중지 여부에 따라 따라 동기, 비동기를 구분 짓게 된다. 이 두 개념을 좀더 상세하게 설명하면 다음과 같다.

애플리케이션 프로그램에서 동기 호출(Synchronous Call)이란 애플리케이션 프로세스(스레드)에서 하부작업(프로시저, 함수, 메서드) 요청 시 요청된 하부작업이 진행되는 동안 호출 프로세스(스레드)의 실행 흐름이 멈추게 되는 호출을 말한다. 멈춘 호출 프로세스(스레드)의 실행 흐름은 하부작업이 리턴되면 다시 계속된다.



애플리케이션 프로그램에서 비동기 호출(Asynchronous Call)이란 애플리케이션 프로세스(스레드)에서 하부작업 요청 시 요청된 하부작업의 실행 또는 종료와 관계없이 호출 프로세스(스레드)의 실행 흐름은 계속되는 호출을 말한다. 이런 호출 방식에서는 하부작업의 실행완료 시점을 호출 프로세스(스레드)가 정확이 알지 못하므로 호출 프로세스(스레드)와 하부작업은 하부작업의 실행 결과를 둘 사이 약속된 결과 영역 조회나 하부작업이 호출 프로세스(스레드)를 호출하는 Callback 메커니즘을 통해 확인한다.



비동기 호출은 기업 통합 패턴(Enterprise Integration Patterns)을 이해하는 기본 개념이다. 기업 통합 패턴은 비동기 호출을 어떻게 기업 애플리케이션 아키텍처로 활용할 수 있는지 설명한 패턴으로 기업 시스템이 복잡해짐에 따라 애플리케이션은 애플리케이션 내 상호 작용, 애플리케이션 간 상호 작용을 위한 효과적인 아키텍처가 필요한대 이때 기업 통합 패턴이 유용하게 사용될 수 있다.

참고)
1) Hophe Gregor and Bobby Woolf. Enterprise Integration Patterns (Addison-Wesley, 2003)

2012년 8월 3일 금요일

설정자 패턴(Configurer pattern)


어떻게 오브젝트 생성 시 필요한 초기화 정보의 획득과 입력 작업을 오브젝트 생성 로직에서 분리할 수 있을까?

일반적으로 오브젝트 생성과 관련하여 개발자들의 관심사는 Constructor, Builder, Factory 패턴 등과 같은 것이다. 개발자들은 오브젝트 생성을 위해 상황에 맞는 적절한 생성 패턴을 사용하여 오브젝트 생성 알고리즘을 구현한다.

이와 같이 오브젝트 생성과 관련해서는 잘 정리된 생성 패턴들이 있지만, 오브젝트 생성 시 필요한 초기화 정보 획득과 등록 작업에 관련된 적절한 패턴에 대한 고민은 상대적으로 부족하다. 그러나 프로그램을 개발하다 보면 오브젝트 생성을 위한 생성 패턴뿐만 아니라 초기화 정보의 획득 및 입력 작업에 대해 일관되고 확장이 용이한 방법에 대해서도 자주 고민하게 된다.

프로그램에서 오브젝트를 생성하기 위해서는 오브젝트 초기화에 필요한 입력 정보를 오브젝트에 입력해야 한다. 생성되는 오브젝트가 수행하는 작업에 따라 초기화에 사용되는 입력 정보는 통신 관련 오브젝트라면 원격지 접속 주소, 데이터베이스 처리 관련 오브젝트라면 데이터베이스 접속 정보, 데이터 파일 관리 오브젝트라면 데이터 파일 디렉터리 위치와 파일명 등이 될 수 있다. 그리고 생성되는 오브젝트의 초기화 입력 정보 형식에 따라 문자형, 숫자형과 같은 단순 원시 자료형에서 복합 구조의 오브젝트 형식 등 입력되는 정보의 형식도 다양한 구조를 가질 수 있다.  또한 생성되는 오브젝트의 초기화 입력 정보의 추출 경로에 따라 초기화 입력 정보는 하드 코딩, 설정 파일, 설정 데이터베이스, 설정 리지스트리, 원격지 설정 서버 등 다양한 경로를 통해 획득할 수 있다.

이와 같이 오브젝트 생성을 위한 초기화 정보 획득 및 입력 작업은 오브젝트 작업 내용, 초기화 정보 입력 형식, 초기화 정보 추출 경로에 따라 다양한 조합이 만들어져 개발에 따른 복잡성이 증가하고 일관성 있는 구현을 방해하게 된다.  이렇게 오브젝트 생성과 관련된 초기화 정보 획득개발이 복잡하고 일관성 없는 개발로 빠져들기 쉬운 문제점을 해결하기 위해 오브젝트 생성 로직에서 설정자(Configurer) 패턴을 사용할 수 있다.

설정자(Configurer) 패턴은 전략 패턴을 응용한 것으로 오브젝트 생성 시 오브젝트 생성 로직과 초기화 정보 획득 및 등록 로직을 분리하여 초기화 정보 획득 및 등록 작업에 일관성을 부여하고 초기화 정보 획득 및 등록 작업 알고리즘을 필요에 따라 대체할 수 있는 구조를 제공한다.

아래는 Factory 패턴과 함께 사용된 설정자(Configurer) 패턴 클래스 다이어그램이다. Factory 패턴 구조는 Design Patterns 의 Factory 패턴 구조이고 Design Patterns에서 제시한 Factory 패턴에서 생성에 필요한 로직은 Factory에 구현하고 Factory는 초기화 정보 획득과 입력 작업을 Configurer에 위임한다. 즉 Factory에서 초기화 정보 획득 및 입력을 분리하여 Configurer가 처리하도록 Factory 패턴의 역할을 나누고 확장한 것이다.

설정자(Configurer) 패턴 구조


Product
  • Factory Method가 생성하는 오브젝트 인터페이스
ConcreteProduct
  • Product 인터페이스 구현 클래스
  • 생성 시 필요한 초기화 입력 Method 구현
Configurer
  • 오브젝트 생성 시 오브젝트 초기화 정보 입력 Method 인터페이스
CustomConfiguer
  • Configure 인터페이스 구현 클래스
  • 오브젝트 생성관련 초기화 정보 추출 및 입력 처리, Setter Method구현 등
  • configure Method에서 Product 구현 오브젝트에 초기화 정보 입력
Factory
  • 오브젝트 생성 Method인 getObject 정의를 가진 Factory 인터페이스
ConcreteFactory
  • Factory 구현 클래스
  • Product 오브젝트 생성 시 Configurer 오브젝트에 초기화 정보 추출 및 입력 처리 위임
  • 오브젝트 생성관련 준비 작업 및 생성 오브젝트 반환


설정자(Configurer) 패턴은 Factory 패턴뿐만 아니라 Builder 패턴에서도 적용할 수 있는데 Factory패턴에서와 마찬가지로 Builder에서 초기화 정보 입력을 Configurer 오브젝트에 위임하는 방식으로 구현하면 된다.

적용 사례

이제부터 설정자 패턴 사용을 실제 예를 통해 살펴보자. 설명을 위해 사용한 Factory 패턴은 Spring Framework의 FactoryBean이다. 즉 Spring Framework 내에서 FactoyBean 사용 시 설정자 패턴을 적용하는 예를 설명할 것이다.

먼저 Java 프로그램에서 오브젝트가 어떻게 생성되는지 ektorp 란 라이브러리에서 HttpClient 오브젝트를 생성하는 프로그램을 보자. 아래는 소스를 보면 ektorp 라이브러리는 HttpClient 오브젝트 생성을 위해 Builder 패턴을 사용하고 있다.

HttpClient 오브젝트 생성 예
HttpClient httpClient = new StdHttpClient.Builder()
    .host("localhost")
    .port(8080)
    .build();
ektorp 라이브러리는 Builder 패턴을 사용하여 오브젝트를 생성하므로 구조적으로는 잘 만들어진 라이브러리로 볼 수 있다. 그러나 Spring Framework에서는 Bean 형식을 고려하지 않고 개발된 Java 오브젝트를 Spring Framework에서 직접 생성하기가 쉽지 않다. 이런 경우 Spring Framework는 임의의 Java 오브젝트를 생성하는 Spring Bean Factory 메커니즘을 제공한다. 다시 말해서 Spring Framework가 제공하는 Factory 패턴 방식을 사용하면 Bean 형식의 생성 초기화 방식을 갖지 않는 Java 클래스를 Spring Framework에서 사용할 수 있는 오브젝트로 생성할 수 있다. 아래와 같이 Spring Framework가 제공하는 Factory 인터페이스인 FactoryBean 인터페이스를 상속받아 HttpClientFactoryBean을 작성하면 httpClient 오브젝트를 생성할 수 있다.

HttpClientFactoryNoConfigurerBean.java
package com.brm.pattern.configurer;

import org.ektorp.http.HttpClient;
import org.ektorp.http.StdHttpClient;
import org.springframework.beans.factory.FactoryBean;

public class HttpClientFactoryNoConfigurerBean implements FactoryBean<HttpClient> {

 private String host;
 private int port;

 public HttpClient getObject() throws Exception {
  return new StdHttpClient.Builder()
    .host(host)
    .port(port)
    .build();
 }

 public Class<? extends HttpClient> getObjectType() {
  return StdHttpClient.class;
 }

 public boolean isSingleton() {
  return true;
 }

 public void setHost(String host) {
  this.host = host;
 }

 public void setPort(int port) {
  this.port = port;
 }
}
Spring Framework에서 HttpClientFactoryNoConfigurerBean 클래스는 아래 Spring XML 설정처럼 사용된다. 아래 Bean myHttpClientOrg 정의에서 생성되는 오브젝트는 HttpClientFactoryNoConfigurerBean의 getObject 메서드를 통해 생성된 오브젝트다.

application.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.springframework.org/schema/beans   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">
 <bean id="myHttpClientOrg" class="com.brm.pattern.configurer.HttpClientFactoryNoConfigurerBean">
  <property name="host" value="localhost" />
  <property name="port" value="8080" />
 </bean>
</beans>
위 Spring XML에서 Bean 정의에 오브젝트 생성을 위한 초기 입력값 host와 port 정보는 property로 정의에 속성값으로 주입하였다. 속성 주입은 Spring Framework의 가장 중요한 장점 중 하나로 속성 주입을 사용하면 속성값을 프로그램 변경 없이 외부에서 입력할 수 있게 된다. 이러한 속성 주입 기능을 사용하기 위해서 HttpClientFactoryBean 클래스에서 오브젝트 생성에 필요한 초기 입력 정보를 setter 속성으로 정의했다. 이렇게 setter 속성을 정의하면 Spring Framework는 Bean 정의 시 해당 setter 속성의 property에 값을 주입할 수 있게 된다. 여기에서 setter 속성이란 Bean 형식의 클래스에서 set 으로 시작하는 메서드를 말한다. 이 부분에 대한 자세한 내용은 이 글의 주제를 벗어나므로 이해가 되지 않는 독자는 Spring Framework의 Bean 정의 설명 문서를 참조하면 될 것이다.

Spring Framework의 속성값 주입 기능을 통해 오브젝트 생성에 필요한 초기화 입력 정보를 HttpClientFactoryNoConfigurerBean 클래스 소스의 수정 없이도 수정할 수 있는 장점도 함께 생기게 되었다.

그러나 만약 어떤 애플리케이션 개발 구조상 위 두 속성 정보인 host와 port 정보를 데이터베이스로부터 입력 받는다면 어떻게 될까? 이 경우 속성값을 데이터베이스에 값으로 바로 추출하는 기능은 Spring Framework가 제공하지 않으므로 처음 임의의 Java 오브젝트를 Spring 프레임워크에서 사용하기 위해 부딪쳤던 문제와 유사한 문제에 다시 부딪치게 된다. 즉 Spring Framework의 속성값 주입 기능만으로는 부족하고 다른 방안을 찾아 소스를 수정해야만 된다. Spring Framework의 속성 값 주입 기능을 버리고 HtttpClient 오브젝트를 생성하기 위해 새로운 Spring FactoryBean 클래스 작성하여 host와 port 정보를 데이터베이스에서 읽도록 재 작성해야 한다. 그러나 Factory 로직을 분석하고 해당 초기화 입력 추출 로직을 대체하는 작업을 하는 개발자는 Factory 로직 내의 오브젝트 생성 로직과 초기화 입력 정보 획득 로직이 결합된 로직을 모두 분석해야 할 것이다. 여기서 예를 든 소스는 이 과정이 그렇게 복잡하지 않지만 일반적인 경우 항상 이런 경우를 기대할 수 없을 것이고 생성 과정이 복잡한 경우도 대해서도 고려해야 할 것이다. 이 과정이 복잡한 소스를 개발자가 분석 수정하는 경우 소스에 대한 분석과 수정에 따른 오류 영향도가 커지게 되어 개발 생산성은 저하될 것이다. 그러므로 오브젝트 생성 로직과 초기화 입력 정보 획득 및 등록 로직을 분리하여 할 수 있다면 그리고 개발자는 오브젝트 생성을 위해 단지 초기화 입력 정보 추출과 등록 로직만 수정할 수 있다면 개발 생산성은 높아 지게 될 것이다. 그리고 구조적으로 보면 Factory 개발 측면에서는 오브젝트 생성과 관련된 로직의 은익성을 확보하고 사용 오브젝트 사용 측면에서는 초기화 입력의 유연성을 제공받는다.

그럼 지금까지 Factory 패턴만 적용된 소스와 설정에 설정자 패턴을 추가해 보자. 먼저 Configurer 인터페이스를 정의한다.

Configurer.java
package com.brm.pattern.configurer;

public interface Configurer<T> {

 public void configure(T client) throws Exception;
}
Configurer 인터페이스는 초기 입력 정보를 주입할 수 있는 오브젝트를 입력 파라미터로 가지는 configure Method를 정의한다. 이 메서드에서 추출된 초기 입력 정보를 오브젝트에 등록하는 기능을 한다

다음으로 Configurer 인터페이스 구현 클래스를 작성한다.

HttpClientConfiguer.java
package com.brm.pattern.configurer;

import org.ektorp.http.StdHttpClient.Builder;

public class HttpClientConfiguer implements Configurer<Builder> {

 private String host;
 private int port;

 public void configure(Builder builder) throws Exception {
  builder.host(host).port(port);
 }

 public void setHost(String host) {
  this.host = host;
 }

 public void setPort(int port) {
  this.port = port;
 }
}
HttpClientConfigurer는 설정자 패턴의 구조를 설명하기 위한 클래스이므로 복잡한 추출 로직을 제시하지 않고 HttpClientFactoryNoConfigurerBean Factory내에서 초기 입력 정보 획득 부분만을 옮겨 왔다. Host와 port를 속성(setter)으로 사용하고 configure Method에서 Builder 오브젝트에 host와 port값을 등록한다. 만약 추출 경로가 데이터베이스라면 HttpClientConfigurer 를 상속받아 새로운 클래스를 (예를 들어 DBConfigurer) 만들고 데이터베이스 추출 로직을 추가하여 데이터베이스에서 추출한 host와 port 속성 값을 등록하는 로직을 추가하면 된다. 여기에서는 HttpClientConfigurer 로만 설명을 진행한다.

이제 HttpClient Factory 에서 초기 입력 정보 추출 로직과 등록하는 로직을 제거하고 Configurer 오브젝트를 호출하는 부분을 추가하면 아래와 같이 좀더 간결한 Factory 클래스가 된다.

HttpClientFactoryBean.java
package com.brm.pattern.configurer;

import org.ektorp.http.HttpClient;
import org.ektorp.http.StdHttpClient;
import org.ektorp.http.StdHttpClient.Builder;
import org.springframework.beans.factory.FactoryBean;

public class HttpClientFactoryBean implements FactoryBean<HttpClient> {

 private Configurer<Builder> configurer;

 public HttpClient getObject() throws Exception {
  Builder builder = new StdHttpClient.Builder();
  configurer.configure(builder);
  return builder.build();
 }

 public Class<? extends HttpClient> getObjectType() {
  return StdHttpClient.class;
 }

 public boolean isSingleton() {
  return true;
 }

 public void setConfigurer(Configurer<Builder> configurer) {
  this.configurer = configurer;
 }
}
보는 바와 같이 host와 port 속성에 대한 로직이 제거되었고 대신 Configurer 주입 속성이 생겼으며 getObject Method에 host와 port 값 등록 로직 대신 configurer 오브젝트의 configure Method 호출 로직으로 대체 되었다. 변경된 Spring XML 파일은 다음과 같다.

application.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

 <bean id="myHttpClientOrg" class="com.brm.pattern.configurer.HttpClientFactoryNoConfigurerBean">
  <property name="host" value="localhost" />
  <property name="port" value="8080" />
 </bean>

 <bean id="myHttpClient" class="com.brm.pattern.configurer.HttpClientFactoryBean">
  <property name="configurer" ref="configuerer" />
 </bean>
 <bean name="configurer" class="com.brm.pattern.configurer.HttpClientConfiguer">
  <property name="host" value="localhost" />
  <property name="port" value="8080" />
 </bean>
</beans>

위에서 보면 새로운 Bean Configurer 정의가 추가 되었고 초기 입력 정보와 관련된 속성관리는 Configurer Bean 정의로 옮겨졌다.
이렇게 구조를 변경하여 오브젝트 생성 로직은 초기 입력 정보 추출 경로가 변경되더라도 Factory의 수정은 발생하지 않고 Configurer 구현 클래스만 상속 변경 또는 변경하여 추출 경로 변경에 대응할 수 있는 유연한 구조가 되었다. 이 예에서는 설명을 위해 간단한 구조만 언급하였지만 실제 오브젝트 생성에 필요한 초기 입력 정보는 생성 오브젝트가 복잡하고 다양한 환경에서 운영되기 위하여 적게는 하나의 입력에서 많게는 수 십개 이상의 초기 입력 정보를 가질 수 있다. 이런 경우 설정자 패턴이 더 빛나는 구조가 될 수 있을 것이다.

맺음말

필자가 설정자(Configurer) 패턴이라고 이름을 붙인 이 패턴은 Apache Camel Framework의 분석 과정에서 얻은 것이다. 한 예로 Camel의 Http Component 라이브러리에 설정자 패턴이 적용되어 있다. Camel Http Component는 설정자 패턴을 통해 Apache HttpClient 오브젝트의 수많은 초기 파라미터 입력을 생성 로직과 분리하여 입력할 수 있는 기능을 제공한다. 그리고 Spring Framework에서도 설정자 패턴과 유사한 로직을 볼 수 있는데, 필자의 분석으로는 설정자 패턴으로 명확한 개념으로 정리가 되지 않고 개발 소스 수준의 적용으로 보인다. 그리고 Camel Framework에서도 설정자 패턴을 단지 전략 패턴으로만 인식하고 있고 Camel Component에 일부는 적용되어 있지만 일관되게 적용되지 않고 있는 점으로 미루어 아직 패턴으로 인식하지 않는 것 같다. 그러나 Configurer를 하나의 패턴으로 인식하고 이런 측면에서 오브젝트 생성 프로그램을 개발 할 때 설정자 패턴을 적용하여 설정 사용에 일관성을 부여하면 개발자들에게 오브젝트 생성에 대한 초기 정보 등록의 생산성과 초기 정보 추출 확장성을 제공해 줄 수 있을 것이라 믿는다.

참고 사이트
1) ektorp : http://github.com/helun/Ektorp
2) Camel HTTP Component : http://camel.apache.org/http.html