2017년 7월 7일 금요일

Apache Kafka 에서 정확히 한 번(Exactly-once)이 가능한가?


  이 문서는 “Exactly-once Support in Apache Kafka” 블로그를 번역한 것입니다. 저자의 블로그에서는 정확히 한번(Exactly-once)의 개념을 설명하고 Kafka로 어떻게 이 개념을 이용할 수 있는지 설명합니다. 그러나 이 개념이 왜 어려운지 또 정말로 가능한지에 대해서는 좀더 성찰이 필요할 것 같습니다.

  우리는 목요일에 의미론적 보증을 극적으로 강화하는 Apache Kafka의 새 버전을 출시 했습니다.

  우리는 빠르고 실용적이며 정확한 방식으로 안정적인 스트림 처리를 수행하는 방법을 수년 간의 고민한 끝에 이 릴리스를 발표했습니다. 구현 노력 자체는 약 1 년 정도 걸렸습니다. 이 과정에서 Kafka 커뮤니티는 약  100 페이지의 세부 설계 문서가 논의했고, 비판도 받고, 광범위한 성능 테스트를 수행했고, 특히 정확이 한번(Exactly-once) 기능을 타깃으로 하는 살인적인 수천 라인의 분산 테스트를 수행했습니다.
 
  이 릴리스에 대한 반응은 대부분 "와우, 정말 멋지네"라는 것이 었습니다. 그러나 나는 불행히도 고전적인 실수를 했습니다. 나는 주석을 읽었습니다. 이것은 우리가 거짓말쟁이라고 주장하는 매우 혼란스러운 사람들의 흥분 섞인 주장들이었습니다.

다음은 반응들 중 일부입니다 .

  "정확히 한 번 전달(exactly-once delivery)는 될 수는 없습니다... 아주 간단한 수학적 정리로 인해 불가능합니다. 또한 저자가 혼란스러워서 독자가 모든 것을 불신하게 만드는 것은 의심의 여지가 있습니다. "

  "수학적으로 입증된 사실에 어떻게 대처할 지 재미있는 기사. 이 기사에서 주의 깊게 지정하지 않은 가정을 변경하지 않는다면 모든 경우에 작동할 수는 없으며 이는 시스템에 관한 것입니다. "

  나는 이 반응들이 틀렸다는 것을 믿습니다. 당신이 이것을 생각하는 사람들 중 하나라면, 저는 우리가 실제로 불가능하고 불가능한 것이 무엇인지, 그리고 Kafka에 무엇이 세워 졌는지를 실제로 들여다 보라고 요청합니다. 그리고 더 많은 정보에 입각한 의견을 얻으시길 바랍니다.

  이 부분을 두 부분으로 나누어 봅시다. 첫째, 정확히 한 번(exactly-once)은 이론적으로 불가능한가? 둘째, 어떻게 Kafka는 이 기능을 지원하는가?

정확히 한 번은 불가능한가요?


  정확히 한 번 전달/의미론이 수학적으로 불가능하다는 것의 정확한 이유를 모른 체 사람들의 확신해 찬 주장이 주변을 떠돌아 다니고 있습니다. 그러나 이것이 분명히 일반적인 지식임에도 불구하고 사람들은 이것이 어떤 증거인지 또는 정확히 한 번 의미가 무엇인지에 대한 정확한 정의에 연결되는 경우는 드뭅니다. 이것들은 FLP 결과 나 Two Generals Problem 와 같은 것들을 증거로 연결하지만 정확히 한 번(exactly-once)에 관한 것은 아닙니다. 분산 시스템에서 가능한 것(비동기, 반동기 등)을 제어하는 설정과 또 그것이 무엇인지를 정확하게 기술하지 않는다면 가능하거나 불가능한 것을 이야기 할 수 없습니다. 그리고 나쁜 일이 일어날 수 있는 것을 설명하는 결함 모델도 이야기 할 수 없습니다.

  그러면 우리가 성취하고자 하는 것과 같은 속성을 공식적으로 정의할 수 있는 방법은 있는 걸까요?

  네 있습니다. 그런 속성이 있다는 것이 밝혀졌습니다. 그것은 "원자적 브로드캐스트(Atomic Broadcast)" 또는 "총 주문 브로드캐스트(Total Order Broadcast)" 라고 합니다. 다음은 널리 사용되는 분산 시스템 교과서들 중 하나의 정의입니다.


  읽어보십시오. 내 생각에 이것은 pub/sub 메시징의 컨텍스트에서 사람들이 정확히 한 번 전달한다는 의미입니다. 즉, 메시지를 게시할 수 있으며 하나 이상의 수신 애플리케이션에 정확히 한 번 전달됩니다.

  그렇다면 원자적 브로드캐스트를 해결할 수 있을까요?

  해결할 수 있습니다. 제가 사진을 찍은 책 외에도 수십 개의 알고리즘을 비교 분류한 논문을 읽을 수 있습니다 .

  그러나 분산 시스템 책을 읽지 못하면 어떻게 이것이 사실이라는 것을 스스로에게 확신시킬 수 있습니까?

  원자적 브로드캐스트는 합의(consensus)와 동등한 것으로 드러났으므로 우리는 합의가 이루어 질 수 있는지 여부를 이해하는 것으로 문제를 좁힐 수 있습니다. 이는 아마도 합의가 분산 시스템에서 가장 많이 연구된 문제이기 때문입니다.

  합의가 가능한 것일까요? 아마 그렇다고 느낄 수 있습니다. 이는 Paxos 및 Raft 와 같은 잘 알려진 알고리즘에 의해 공격받는 문제이고 현대 분산 시스템 실행에 널리 의존하기 때문에 발생합니다. 그러나 이론적인 결과를 원한다면 설정 및 실패 모드에 대해 구체적으로 설명해야합니다. 예를 들어 의견에 있는 몇몇 사람들은 "하나의 잘못된 프로세스를 가진 합의의 불가능성"이라는 제목의 "FLP"논문을 인용했습니다. 그것은 좋아 보이지 않습니다! 첫 번째 문장에서 실패 감지기가 "충돌 오류가 있는 비동기 시스템에서 합의를 해결하는 데 사용될 수 있다"고 주장하는 논문을 쉽게 찾을 수 있습니다. 이것을 어떻게 이용할까요? 이것은 이론적으로 분산된 시스템 주장에서 세부 사항이 중요한 부분입니다.  우리는 설정과 결함 모델에 대해 구체적이어야 합니다. FLP 결과는 매우 제한적인 환경에서 합의가 가능하지 않다는 것을 증명합니다. 로컬 타이머 또는 무작위화와 같은 간단한 작업을 허용하면 가능해집니다. 합의 알고리즘은 이런 것들에 의존하여 일종의 시끄럽지만 결국에는 "올바른 시간 동안 하트 비트가 없는 프로세스가 죽었다"와 같은 올바른 오류 감지를 구현합니다. 이것들이 사람들이 "합의를 이끌어 낸다" 알고리즘을 말할 때 사람들이 참조하는 설정입니다.

  (FLP 외에도 많은 사람들이 Two Generals 문제를 "수학적 정리"와 연결시켰습니다. 그 이유는 실제로 전통적인 메시징 시스템의 유추를 볼 수는 있지만 실제로 Kafka는 그 문제와 별로 비슷하지 않습니다.)

  이것은 깊은 주제입니다. 관심이 있으시면 첫 걸음으로 Martin Kleppmann의 멋진 책을 추천 할 수 있습니다 . 진정으로 사로 잡힌 사람들은 참고 문헌들로 한 달 동안은 바쁘게 지낼 수 있을 것입니다.

  그렇다면 어떻게 이것을 실제로 실천할 수 있을까요? 실용적인 측면에서, 합의는 현대 분산 시스템 개발의 주류입니다. AWS에서 거의 모든 서비스를 사용하거나 AWS를 기반으로하는 서비스 위에 구축된 서비스를 사용했다면 합의로 구축된 시스템에 의존하게 됩니다. 이것은 현재 구축되는 시스템이 많지는 않지만 많은 경우에 해당됩니다. Kafka 는 이 중 하나이며, 그 중심적 추상은 분산된 일관된 로그이며, 사실상 가장 순수한 아날로그에서 다중 라운드로의 합의입니다. 따라서 합의가 가능하다고 믿지 않는다면 Kafka도 가능하다고 믿지 않습니다. 이 경우 Kafka에서 정확히 한 번 지원될 가능성에 대해 너무 걱정할 필요가 없습니다!

Kafka로 정확히 한 번 전달되는 애플리케이션을 어떻게 만들 수 있습니까?


  Kafka는 다음과 같은 로그를 갖습니다.

Apache Kafka의 로그

  Kafka의 로그는 강하게 정렬된 레코드 순서며 각 레코드에는 로그의 레코드 위치를 식별하는 순차적 숫자 오프셋이 지정됩니다.

  "생산자"는 이 로그에 레코드를 추가하고, 0 이상의 소비자 애플리케이션은 자신이 제어하는 ​​지정된 오프셋에서 메시지를 읽습니다.

  다음과 같은 애플리케이션을 상상해 봅시다.

  게시자가 메시지를 게시하려고 하고 소비자가 메시지를 읽고 이를 데이터베이스에서 저장하려고 합니다. 우리는 어떻게 이것을 할 수 있고 올바른 해결책을 얻을 수 있을까요?

  발생할 수 있는 두 가지 범주의 문제를 볼 수 있습니다.
  1. 첫 번째 문제는 게시자 애플리케이션이 로그에 메시지를 기록하지만 네트워크를 통해 확인 응답을 받지 못하면 발생합니다. 이것은 이 게시자를 묶어 놓을 것입니다. 이 때 메시지는 실제로 쓰기가 성공했거나 Kafka에 전혀 도착하지 않았을 수 있습니다. 우리는 모릅니다! 우리가 재시도하고 메시지 쓰기가 성공했다면 우리는 복제본을 가질 수 있습니다. 우리가 재시도하지 않고 쓰기가 성공하지 못했다면 우리는 메시지를 잃을 것입니다. 이는 기본 키가 없거나 자동 증가 기본 키가 없는 데이터베이스 테이블에 삽입했을 때와 실질적으로 동일한 딜레마입니다.
  2. 두 번째 문제는 소비자 측면에 있습니다. 소비자 애플리케이션은 로그에서 일부 메시지를 읽고 데이터베이스에 결과를 쓸 수 있지만 위치를 표시하는 오프셋을 업데이트하기 전에 실패할 수 있습니다. 해당 소비자가 다시 시작될 때 (잠재적으로 Kafka 그룹 관리 메커니즘을 사용하는 다른 컴퓨터에서 자동으로) 중복될 수 있습니다. 애플리케이션이 저장된 오프셋을 먼저 업데이트 한 다음 데이터베이스를 업데이트하는 경우, 실패로 인해 재시작시 업데이트가 누락 될 수 있습니다.
  두 문제에 대해 이야기 해 봅시다. 첫 번째 문제는 우리가 게시물에서 발표한 멱등성 지원에 의해 해결됩니다. 따라서 게시자는 중복 가능성에 대한 걱정 없이 성공할 때까지 항상 다시 시도할 수 있습니다 (Kafka는 투명하게 탐지하여 무시합니다).

 믿거나 말거나, 우리는 두 번째 문제에 대해서는 깊이 생각하지 않았습니다. 그러나 우리가 그것을 생각하지 않았기 때문이 아닙니다! Kafka를 알고 있는 사람들을 위해 이미 긴 블로그 게시물이 있기 때문에 우리는 더 깊이 뛰어 들지 않았습니다. 그래서 짧은 요약 설명을 했습니다.

  다음은 좀 더 깊이 있는 토론입니다.

  소비자가 정확히 한 번 처리하도록 하려면 생성된 파생 상태와 업스트림을 가리키는 오프셋을 동기화 상태로 유지해야 합니다. 여기서 중요한 사실은 소비자가 로그에서 오프셋을 제어할 수 있고 원하는 위치에 저장할 수 있다는 것입니다. Kafka 위에 정확하게 한 번 의미를 얻기 위해 이 방법을 사용하는 일반적인 두 가지 방법이 있습니다.
  1. 파생된 상태와 오프셋을 동일한 DB에 저장하고 트랜잭션에서 둘 다 업데이트하십시오. 다시 시작할 때 DB에서 현재 오프셋을 읽고 거기에서 읽기 시작하십시오.
  2. 모든 상태 업데이트와 오프셋을 멱등성(idempotent) 방식으로 작성하십시오. 예를 들어 파생 상태가 발생 횟수를 추적하는 키와 카운터인 경우 오프셋과 함께 카운터를 저장하고 오프셋 <= 현재 저장된 값으로 모든 업데이트를 무시합니다.
  좋습니다. 하지만 "어렵습니다!"라고 반대할 수도 있습니다. 실제로 그렇게 힘들지는 않다고 생각합니다. 그럼에도 저도 트랜잭션은 간단하지 않다고 생각합니다. 여러 테이블을 업데이트하는 경우 트랜잭션 문제가 발생합니다. 오프셋 테이블을 추가하고 이를 업데이트에 포함시키는 것은 로켓 과학이 아닙니다.

  이에 대해 제가 들었던 또 다른 반대는 실제로 "정확히 한 번"이 아니라 실제로는 "효과적 한 번(effectively once)"이라는 것입니다. 나는 (일반적으로는 덜 이해되지만) 이런 측면이 더 좋다고 동의합니다만, 우리는 여전히 정의되지 않은 용어의 정의에 대해 논쟁 중입니다. 우리가 전달과 관련해 잘 정의된 속성을 원한다면 나는 실제로 원자적 브로드캐스트(Atomic Broadcast)가 꽤 좋은 정의라고 생각합니다. 우리가 비공식적으로 말하면, 사람들은 의미에 대해 직관적인 생각을 갖기 때문에 "정확히 한 번"이라고 말하는 것이 좋습니다. (우리가 원자성 에 대한 지원을 발표했다더라도 혼란은 결코 더 적지 않았을 것입니다. ). 더 큰 비판은 사람들이 원하는 진정한 보증이 "정확히"도 "효과적"도 아니며 "한번" 또는 "전달"과 관련된 어떤 것이라는 것입니다. 사람들이 원하는 진정한 보증은 애플리케이션과의 통합에 대해 열심히 생각할 필요없이 오류가 발생했을 때 메시지를 철저히 정확하게 처리하는 것입니다.

  결국, 제가 설명한 해결책은 그다지 복잡하지는 않지만 여전히 애플리케이션의 의미에 대해 생각해야 합니다. 우리는 "애플리케이션에 마법의 요정 가루를 내 뿌릴 수 있습니까" 라는 제목의 블로그에서 이 문제를 다루려고 했습니다 (대답은 "아니오"였습니다).

  우리는 이것을 쉽게 할 수 있을까요? 우리는 할 수 있다고 생각합니다. 여기에서 기능 집합의 두 번째 부분인 트랜잭션이 등장합니다.

  실제로 위에서 제시한 예는 데이터 처리와 결과를 저장 시스템에 통합하는 다른 두 문제를 혼합한 것입니다. 이들은 서로 얽혀 있기 때문에, 개발자는 두 가지 방법을 함께 풀어 내기가 어렵습니다.

  이를 개선하기 위한 아이디어는 애플리케이션을 다른 두 부분으로 분류하는 것입니다. 이 두 부분은 하나 이상의 입력 스트림을 변환하는 (레코드 간에 결합되거나 측면 데이터와 결합될 수 있는) "스트림 처리" 부분과 이 데이터를 데이터 저장소로 전송하는 (동일한 애플리케이션 또는 프로세스에서 실행될 수 있지만 논리적으로는 구분된) 커넥터입니다.

  커넥터는 Kafka로부터 특정 데이터 시스템에 대한 트랜잭션 또는 멱등적 데이터 전달에 대한 추론을 필요로 합니다. 커넥터는 깊게 생각하고 오프셋을 관리해야 하지만 완전히 재사용할 수 있습니다. JDBC를 지원하는 모든 데이터베이스에서 JDBC 커넥터를 정확히 한 번은 제대로 작동하므로,  애플리케이션 개발자는 이를 고려할 필요가 없습니다. 우리는 이미 이런 것들을 가지고 있습니다, 그러므로 이 기능은 개발하기 보다 JDBC 커넥터를 다운로드합니다.

  어려운 부분은 데이터 스트림에서 범용 변환을 올바로 수행하는 것입니다. Kafka의 스트림 API와 함께 트랜잭션 지원도 필요합니다.

  Kafka 스트림 API 는 입력 스트림과 출력 스트림의 상단에 변환을 정의하는 매우 일반적인 API를 제공하는 생산자 및 소비자의 상위 계층입니다. 이 API를 사용하면 애플리케이션에서 수행 할 수 있는 거의 모든 작업을 수행 할 수 있습니다. 고전적인  메시징 시스템 API에 비해 더 강력하지는 않습니다.

  Kafka 스트림 애플리케이션:

  이 애플리케이션은 분산된 "단어 수"를 계산하는 하는 고전적인 빅데이터 예제입니다. 단어 수는 완전히 실시간이며 연속적입니다 (새 문서가 작성 될 때마다 카운트가 증가합니다).

 이 프로그램은 메인 메소드를 가진 보통의 자바 애플리케이션입니다. 이 애플리케이션은 보통의  애플리케이션들처럼 시작되고 배포됩니다. Kafka 소비자는 모든 인스턴스가 들어오는 데이터 스트림을 처리하도록 작동합니다.

  이 애플리케이션은 어떻게 정확성을 보장할 수 있습을까요? 결국엔 입력, 출력, 수신 메시지 전반의 집계 및 분산 처리 등 모든 복잡한 것들을 상상할 수 있습니다.

  그것에 대해 생각해보면 Kafka의 모든 스트림 처리는 다음 세 가지를 수행하고 있습니다.
  1. 입력 메시지 읽기
  2. 상태에 대한 업데이트를 생성(애플리케이션 인스턴스가 실패하고 다른 곳에서 복구되는 경우 내결함성이 필요하기 때문에)
  3. 출력 메시지 생성
  핵심 요구 사항은 이 세 가지가 항상 함께 발생하거나 전혀 발생하지 않도록 보장하는 것입니다. 상태가 업데이트되었지만 출력이 생성되지 않거나 그 반대로 오류가 발생하는 경우를 허용할 수 없습니다.

  우리는 어떻게 이것을 할 수 있을까요?

  우리는 수년에 걸쳐 이것에 관해 정말로 열심히 생각했고, 지금도 이것을 건설하고 있습니다. 기초 작업은 지난 몇 년 동안 한 번도 변경하지 않은 사항이었습니다.
  1. 0.8.1 릴리스에서 Kafka는 상태 변경을 위한 저널 및 스냅샷으로 사용할 수 있는 로그 압축을 추가했습니다. 즉, 임의의 로컬(디스크 또는 메모리 내) 데이터 구조에 대한 일련의 업데이트를 Kafka에 대한 일련의 기록으로 모델링할 수 있습니다. 이를 통해 로컬 연산의 내결함성을 만들 수 있었습니다.
  2. Kafka에서 데이터를 읽는 것은 오프셋을 증가시킵니다. 0.8.2 에서  오프셋 저장에 Kafka 자체를 사용하도록 오프셋 저장 메커니즘을 이동했습니다. 내부에서 오프셋 커밋(Commit)은 Kafka에 쓰여집니다. (소비자 클라이언트가 이 작업을 수행하므로 우리는 모를 수도 있습니다).
  3. Kafka로 데이터를 쓰는 것은 항상 Kafka에 쓰기였습니다.
  위 프로그램은 방금 추가한 (이 세 가지 작업을 투명하게 단일 트랜잭션으로 감싸는) 기능을 설정합니다.   이렇게 하면 읽기, 처리, 상태 업데이트 및 출력 모두가 함께 발생하거나 전혀 발생하지 않습니다.

  이 과정은 느리지 않을까요? 많은 사람들은 분산 트랜잭션이 본질적으로 매우 느리다고 가정합니다. 모든 단일 입력에 대해 트랜잭션을 수행할 필요는 없습니다. 이 경우 입력들을 함께 배치로 처리할 수 있습니다. 배치가 클수록 트랜잭션의 실제 오버헤드는 낮아집니다 (트랜잭션은 트랜잭션의 메시지 수와 관계 없이 일정한 비용을 가집니다). 블로그 포스트는 이것에 대한 성과 결과를 보여 주었고 이는 매우 유망한 성과였습니다.

  결과적으로 스트림 API를 사용하는 애플리케이션에 내 애플리케이션을 인수로 지정하고 출력 시스템과의 통합을 위해 정확히 한 번 커넥터를 사용하는 경우, 이제는 구성 변경만으로 종단 간 정확성을 얻을 수 있습니다.

  정말 멋진 점은 이 기능이 Java API에 전혀 묶여 있지 않다는 것입니다. Java API는 데이터 스트림의 연속적이고 상태를 유지하고 올바른 처리를 모델링하기 위한 범용 네트워크 프로토콜을 둘러싼 단순한 래퍼입니다. 모든 언어에서 이 프로토콜을 사용할 수 있습니다. 우리는 이 기능에  일종의 매우 강력한 클로저 속성을 추가함으로 변환을 수행하고 프로토콜을 구현하는 임의의 프로세스를 통해 입력 및 출력 토픽을 올바르게 연결한다고 생각합니다

정확히 한 번에 대해 생각해 보기로 돌아가기


  나는 회의적인 사람들에게 우리가 한 일을 이해하고 그것을 이해하도록 격려하기 위해 이 글을 썼습니다. 벤더로부터 오는 많은 헛소리들에 나는 회의적입니다. 그러나 나는 이 기능 세트가 정말 흥미롭고 LIAR을 모든 대문자로 외칠뿐 아니라 실제로 무엇을하는지,하지 않는지, 실제로 한계가 무엇인지 이해함으로써 이해가 훨씬 더 높아질 것이라고 생각합니다.

  업계에서는 올바른 결과를 얻을 수 없다는 것, 근본적으로 비효율적이고, 일괄처리 없이는 불완전하다는 것 등,  롤백되는 과정에 있는 스트림 처리와 관련하여 많은 가정이 있었습니다. 나는 정확히 한 번 처리하는 것이 불가능하다는 주변의 광범위하고 모호한 주장이 결국 양동이에 빠지게 된다고 생각합니다. 그것들은 나에게 일종의 분산 시스템인 broscience("나는 형제의 형제에게서 구글에서 일하는 형제가 정확히 한번이 CAP 이론에 위배된다는 말하는 것을 들었습니다”)를 생각나게 합니다. 나에게 진보는 일반적으로 실제로 가능하지 않은 것을 더 깊이 이해하고 문제를 재정의하여 우리를 앞으로 나아갈 실제적인 추상화를 구축하려는 시도로 이루어집니다.

  이런 것이 일어나는 좋은 예는 Spanner 와 CockroachDB 와 같은 시스템에서 수행되는 작업입니다. 이 작업은  가능한 범위 내에서 애플리케이션 개발자에게 유용한 기능을 제공하기 위해 많은 것을 수행합니다. 나는 NoSQL 분야에서 많은 경험을 갖고 있습니다. 이 시스템이 무엇을 하고 있는지는 대부분의 사람들이 불가능하고 비현실적인 조합이라고 생각하는 것으로 잘못 인식되었습니다. 나는 이것이 우리에게 교훈이되어야 한다고 생각한다. 애플리케이션을 구현하는 가난한 사람에게 모든 어려운 문제를 포기하고 구멍을 내기보다는 문제 공간을 재정의하여 올바른, 빠르며 가능한 대부분의 사용 가능한 시스템 기본 요소를 구축하는 방법을 이해해야 합니다.

원문 : Exactly-once Support in Apache Kafka



2014년 12월 5일 금요일

마이크로 서비스(Microservice) 소개


필자는 "Apache Camel을 이용한 마이크로 웹 서비스 개발" 글 등에서 아파치 카멜이 마이크로 서비스를 구축하는 데 좋은 도구로 사용될 수 있다는 말을 남긴 적이 있다. 그런데 그곳에서는 마이크로 서비스의 개념에 대해서는 별로 설명하지 않고 간단히 언급 정도만 했었다. 그리고 나서는 마이크로 서비스를 좀 더 소개할 필요가 있다고 느꼈고 이 글을 통해 간단하기는 하지만 마이크로 서비스가 갖는 핵심적인 특징을 간단히 소개하고자 한다.

"마이크로 서비스 아키텍처 스타일(Microservice architecture style)"은 독립적으로 배포 가능한 서비스들의 묶음으로 소프트웨어 애플리케이션을 설계하는 방법 을 말하지만, 아직까지는 이 아키텍처 스타일에 대한 정확한 정의는 없다. 그럼에도 마이크로 서비스 아키텍처 스타일은 비즈니스 개발, 배포 자동화, 지능적인 엔드포인트, 중립적인 언어나 데이터, 비 집중화된 제어 등의 공통적인 특징들을 갖는다.

마이크로 서비스란 용어를 처음 만든 사람은 마틴 파울러(Martin Fowler)다. 그는 그의 블로그에서 마이크로 서비스를 다음과 같이 언급했다.

"마이크로 서비스(Microservice)"는 소프트웨어 아키텍처에 있어서 아직은 새로운 용어다. 일반적으로 이런 것들은 얕잡아 보게 되고 무시되는 경향이 있기는 하지만, 이 짧은 용어는 점점 더 설득력 있게 보이는 소프트웨어 시스템들의 스타일을 설명한다. 우리는 지난 몇 년 동안 이런 스타일이 적용된 수많은 프로젝트들을 보아왔다. 결과는 지금까지 긍정적이었다. 이런 긍정적인 결과들로 인해 이 스타일은 점점 우리 동료들의 기본적인 기업 애플리케이션 개발 스타일이 되어가고 있다. 그러나 불행히도 아직까지 마이크로 서비스 스타일이 무엇인지, 어떻게 적용하는지에 대해 대강이라도 설명하는 정보들은 부족하다.

마이크로 서비스 아키텍처 스타일(Microservice architecture style)은 작은 서비스(service)들의 집합으로써 애플리케이션(monolithic application)을 개발하는 방법이다. 서비스들은 각각이 프로세스고, 서로 HTTP, JMX, JMS, AMQP, STOMP, REST API 같은 가벼운 통신 메커니즘을 사용한다. 서비스들은 비즈니스를 구현하고, 각각은 완전히 자동화된 방법으로 독립적으로 배포될 수 있다. 서비스들을 위해 최소한의 중앙 관리를 사용한다. 서비스들은 다른 프로그래밍 언어로 개발될 수 있고, 다른 데이터 저장 기술을 사용할 수도 있다.

마이크로 서비스 스타일(Microservice style)은 덩어리 스타일(monolithic style)과 비교된다. 덩어리 애플리케이션은 덩어리 단위로 개발된다. 일반적으로 기업 애플리케이션들은 세 주요 부분으로 구성된다. (사용자의 브라우저에서 동작하는 HTML 페이지와 자바스크립트를 구성하는) 클라이언트 측 사용자 인터페이스 로직과 (일반적으로 관계형 데이터베이스를 사용하는) 도메인 로직과 서버 측 애플리케이션 로직이다. 애플리케이션은 HTTP 요청을 수락하고, 도메인 로직으로 데이터베이스로부터 데이터를 갱신하거나 추출해, 브라우저에게 전송할 HTML 뷰에 덧붙여 클라이언트에 응답을 반환한다. 이런 기업 애플리케이션들은 덩어리 구조를 갖는다. 즉 한 덩어리 안에서 로직을 실행한다. 변경이 필요한 경우, 이런 애플리케이션은 이를 반영한 전체 애플리케이션을 새롭게 빌드해 배포해야 한다.

이런 덩어리 아키텍처는 LINUX 운영 체제 같은 견고한 아키텍처를 접근하기 위해서는 자연스러운 방법이다. 덩어리 애플리케이션의 모든 로직은 단일 프로세스에서 실행된다. 이런 애플리케이션은 언어의 특징을 이용해 패키지(네임스페이스), 클래스, 메소드(함수)들로 나눠 소프트웨어를 모듈화/구조화 시킨다. 이런 애플리케이션이더라도 (오랫동안 실행 및 테스트 방법이 발전해 왔으므로) 개발자 컴퓨터에서 테스트되거나 실행될 수 있다. 테스트와 운영 배포는 정해진 절차에 따라 순차적으로 진행한다. 덩어리 아키텍처 스타일에서는 부하 분산기(load-balancer) 뒤에 인스턴스들을 실행함으로 서버 성능을 수평적으로 확장시킨다.

덩어리 애플리케이션 아키텍처 스타일은 성공적일 수 있으나, (특히 클라우드에 배포되는 애플리케이션들인 경우) 사람들은 점점 더 좌절을 맛보게 되었다. 변경 주기는 이들을 더욱 옥죄었다. 덩어리 애플리케이션은 작은 부분을 변경하더라도, 전체 애플리케이션을 다시 빌드하고 배포해야 한다. 결과적으로 시간이 지나면서 모듈 구조는 훼손되고, 여러 모듈 중에 한 모듈만 변경하는 것도 쉽지 않게 된다. 일부만 확장하려고 해도, 전체 애플리케이션을 확장해야 하므로, 많은 자원을 사용해야 한다.

다음은 덩어리 아키텍처 스타일과 마이크로 서비스 아키텍처 스타일의 배포 방식 비교다.

위 그림은 마틴 파울러의 블로그에서 인용했다.

이런 좌절들이 기업 애플리케이션 아키텍처 스타일을 마이크로 서비스 아키텍처 스타일로 향하게 했다. 즉 애플리케이션을 서비스들의 묶음으로 만드는 것이다. 마이크로 서비스 아키텍처 스타일의 서비스들은 독립적으로 배포될 뿐만 아니라 확장된다. 서비스들은 견고한 모듈 경계를 가지고 있어, 설령 다른 언어로 개발된 서비스라도 함께 동작할 수 있다. 뿐만 아니라 서비스마다 관리하는 팀이 다를 수도 있다. 이런 특징들을 갖는 것이 마이크로 서비스다.

필자는 마이크로 서비스 아키텍처 개념을 몰랐던 지난 몇 년 전부터 몇몇 글에서 100줄 이내로 프로그램하는 기술을 소개한 적이 있다. 기상청 서버에서 기상 정보를 얻어 오는 애플리케이션을 개발한다거나, 자동 메일 발신 애플리케이션을 개발한다거나, 확장 가능한 웹 서비스를 제공하는 플랫폼을 개발한다거나 등 모두 아주 적은 개발량으로 비즈니스 기능을 수행하는 애플리케이션을 개발하는 방법을 소개했었다. 그리고 이런 프로그램들의 핵심 도구로 아파치 카멜(Apache Camel)을 이용했다. 그런데 이런 개발 방법이 마이크로 서비스 아키텍처와 닮았음을 알게 되었다. 이 지점에서 필자는 IT의 진지한 고민은 타국에서 마이크로 서비스 아키텍처를 고민하던 사람들과 결국 같은 지향점을 찾아 가는 과정이 아닐까 생각하게 되어 신기하기도 했다. 그리고 더 나아가 마이크로 서비스 아키텍처의 기저에는 느슨한 결합(loose coupling)과 메시징 기반의 기업 통합 패턴(Enterprise Integration Patters)이 자리 잡고 있다고 필자는 생각한다.

과거 가장 거대한 몸집을 자랑하던 공룡들은 진화에서 도태됐고, 몸집은 아주 작지만 기능적으로 잘 분업화된 사회 구조를 가진 개미들은 현재 지구상에서 가장 번성하고 있다. (지구에 사는 개미들의 전체 무게가 지구에 사는 사람들의 전체 무게보다 무겁다고 한다.) 필자는 이런 진화의 필연과 덩어리 아키텍처 스타일이 마이크로 서비스 아키텍처 스타일로 변하는 과정이 많이 닮았다고 생각한다. 그러므로 마이크로 서비스 아키텍처 스타일은 기업 아키텍처 스타일에서 중요한 아키텍처 스타일이 될 것이라 미래를 예측해 본다.


참고 사이트

2014년 11월 20일 목요일

Apache Camel을 이용한 대출 모집인 구현


이 글은 팁코 액티브엔터프라이즈(TIBCO ActiveEnterpise) 기반 통합 솔루션을 Apache Camel 기반 통합 솔루션으로 대체하는 방법을 설명합니다.

1. 들어가며

기업 통합 패턴(Enterprise Integration Patterns)의 "9장 사잇장: 복합 메시징"에서는 대출 모집인을 세가지 통합 도구를 각각 이용해 실질적인 통합 솔루션을 구현하는 예들을 설명한다. 첫째 예는 SOAP 프레임워크인 Apache Axis를 이용하고, 둘째 예는 마이크로소프트 MSMQ를 이용하고, 셋째 예는 상용 EAI 제품인 팁코 액티브엔터프라이즈(TIBCO ActiveEnterprise)를 이용한다. 이 책 9장에 등장하는 예들의 특징을 표로 보이면 다음과 같다.

구현
실행 방식
주소 지정
수집 전략
채널 형식
기술
1
동기
수신자 목록
채널
웹 서비스/SOAP
자바/아파치 액시스
2
비동기
수신자 목록
상관관계 식별자
메시지 큐
C#/마이크로소프트 MSMQ
3
동기,비동기
게시 구독 채널
상관관계 식별자
게시 구독
팁코 액티브엔터프라이즈

위 구현 예들은 중 "팁코 액티브엔터프라이즈를 이용한 구현"은 앞의 다른 예들과 달리 동기 요청과 비동기 메시징이 결합됐다는 점에서 차이가 있다. 즉 첫째 예는 SOAP을 이용한 동기 호출 기반의 솔루션이고, 둘째 예는 마이크로소프트 MSMQ를 이용한 비동기 메시징 솔루션인데, 셋째 예는 팁코 액티브엔터프라이즈를 이용해 요청은 동기로 수신하고 후속 처리는 비동기 메시징을 이용한다. 셋째 예의 메커니즘을 좀더 구체적으로 설명하면, "팁코 액티브엔터프라이즈를 이용한 구현" 예의 대출 모집인은 대출 클라이언트로부터 대출 견적을 동기로 요청 받아, 동기 호출로 신용 평가 기관에서 대출 클라이언트의 신용 점수를 얻고, 은행들에게 비동기 게시 구독 채널로 견적 요청을 브로드캐스트하고, 각 은행들로부터 대출 견적 응답을 비동기 메시지 큐(서브젝트)로 수집해, 최상 대출 견적을 대출 클라이언트에게 동기 응답으로 반환한다. 여기서 중요한 점은 상용 EAI 제품을 이용했다는 점이다.

그런데 기업 통합 패턴에서는 이런 메커니즘에 대한 해결책이 의도적이든 의도적이지 않든 부실하다. ("팁코 액티브엔터프라이즈를 이용한 구현"에서만 동기, 비동기의 결합에 대한 전체적인 예를 살펴볼 수 있다.) 기업 통합 패턴은, 비동기 메시징을 전제로 해결책들을 제시하는 책이므로, 비동기 인프라에서 동기 인프라에 접근하기 위한 패턴으로 서비스 액티베이터 패턴[EIP]을 설명하고, 애플리케이션이 비동기 인프라에 접근하기 위한 패턴으로는 메시징 게이트웨이 패턴[EIP]을 설명한다. 이와 같이 "팁코 액티브엔터프라이즈를 이용한 구현" 예를 벗어난 곳에서는 동기 인프라와 비동기 인프라가 결합된 종합적인 해결책을 비중 있게 다루지는 않는다. 상관관계 식별자 패턴[EIP]을 활용할 수도 있지만, 상관관계 식별자 패턴은 주로 비동기 요청의 응답을 상관시키기 위한 패턴이다.

이에 반해 팁코 액티브엔터프라이즈 같은 상용 EAI 제품들은 동기 호출과 비동기 응답을 상관시키는 종합적인 메커니즘을 내장한다. 이 기술은 간단한 것처럼 보이지만 실상은 그렇지 않다. 동기 호출은 응답을 반환할 때까지 동기 프로세스(스레드)가 (중간) 처리 결과와 처리 상태를 유지하는 반면, 비동기 메시징에서는 (중간) 처리 결과는 메시지와 필터 프로세스(스레드)들에 유지되고 처리 상태는 메시지가 위치한 채널로 관리한다. 즉 동기 호출과 비동기 메시징의 처리 결과와 상태 관리는 완전히 다른 방식이다.

그렇다면 동기 호출과 비동기 메시징을 결합하기 위해서는 상용 EAI 제품만 사용해야 하는 것일까? 그렇지 않다. 통합 프레임워크를 이용한다면 얼마든지 상용 EAI 제품처럼 동기 호출과 비동기 메시징을 결합할 수 있다. 그러므로 이 기능 때문에 상용 EAI 제품을 선택해야 한다는 기준은 전제하지 않아도 될 것이다.


2. 대출 모집인 솔루션 아키텍처 비교

기업 통합 패턴 9장의 "팁코 액티브엔터프라이즈를 이용한 구현" 예의 팁코 대출 모집인 솔루션 아키텍처는 다음과 같다.

위 아키텍처 그림에서 테스트 대출 클라이언트와 대출 모집인 사이와 대출 모집인과 신용 평가 기관 사이는 팁코의 동기 랑데부 전송을 사용하고, 대출 모집인과 은행들 사이는 비동기 랑데부 전송을 사용한다. 중간의 프로세스 관리자는 팁코의 시각적 도구와 스크립트와 메타 저장소 등을 이용하여 메시지 송수신 흐름과 처리 상태를 관리한다. 보다시피 동기 메커니즘과 비동기 메커니즘이 결합되어 있다. (이 솔루션 아키텍처는 기업 통합 패턴의 복합 메시징의 예를 보여주기 위해 모든 통신 인프라를 팁코 랑데부 통신으로 구성한 것이다. 실무적을 보면 대출 클라이언트는 원격지에서 인터넷으로 대출 모집인에 접속할 것이고, 신용 평가 기관이나 은행들도 원격지에 위치해 전용선이나 인터넷으로 대출 모집인을 연결할 것이다.)

위 솔루션 아키텍처에 대응되는 "Apache Camel을 이용한 대출 모집인 솔루션 아키텍처"는 다음과 같다.

"팁코 액티브엔터프라이즈를 이용한 구현"에서 달라진 점은 Apache Camel을 이용한 대출 모집인 솔루션은 팁코 동기 랑데부 전송을 좀더 범용적인 SOAP/HTTP 전송으로 대체하고, 팁코 비동기 랑데부 전송을 내부 메시징과 Apache ActiveMQ의 JMS Topic으로 대체하고, 프로세스 관리자를 프로세스 관리자(웹 서비스 빈) 라우팅과 응답 관리자 라우팅으로 대체한다는 점이다.


3. 대출 클라이언트 인터페이스


3.1. 대출 모집인

앞서도 간단히 언급했듯이 "Apache Camel를 이용한 구현"은 "팁코 액티브엔터프라이즈를 이용한 구현"의 팁코 동기 랑데부 전송을 SOAP/HTTP 동기 웹 서비스로 대체한다. 이를 위해 대출 모집인은 대출 견적 요청 서비스를 스프링 설정 파일에 SOAP 웹 서비스 인터페이스 빈으로 다음과 같이 노출한다.

brokerQouteServiceEndpoint 빈은 웹 서비스 인터페이스 객체고, brokerQouteServiceImp 빈은 웹 서비스 객체다. BrokerQouteServiceImp 빈 클래스가 웹 서비스를 실제 수행한다. 그러므로 BrokerQouteServiceImp 클래스가 실제 프로세스 관리자[EIP]다. 여기서는 서비스 우선 개발(service first development) 방법에 따라 웹 서비스 인터페이스를 먼저 정의했다. 이 설정으로 대출 클라이언트에 대한 대출 모집인의 SOAP 웹 서비스 노출을 완성된다. 서비스 우선 개발 방법의 자세한 설명은 "Apache Camel을 이용한 마이크로 웹 서비스 개발" 글을 참고하기 바란다.

BrokerQouteService 인터페이스는 다음과 같다.

BrokerQouteService 는 평범한 자바 인터페이스다. 이 인터페이스를 상속한 BrokerQouteServiceImp 빈 클래스는 프로세스 관리자 절에서 설명한다.


3.2. 대출 클라이언트

대출 클라이언트는 대출 모집인이 노출한 웹 서비스를 호출한다. 그런데 이 예는 대출 모집인과 대출 클라이언트가 하나의 스프링 설정 파일을 공유하므로, 대출 클라이언트는 위한 (호출 측) 웹 서비스 빈은 따로 설정하지 않는다. (대출 클라이언트는 (호출 측) 웹 서비스 빈으로 대출 모집인의 (서버 측) 웹 서비스 빈을 사용한다.)

웹 서비스를 호출하는 대출 클라이언트의 Camel 라우팅 설정은 다음과 같다.

대출 클라이언트 라우팅은 5초마다 bean:client 엔드포인트를 실행한다. 그러면 대출 클라이언트 빈은 대출 모집인의 웹 서비스를 호출한다.

LoanQouteClient 빈 클래스는 다음과 같다.

LoanQouteClient는 client 생산자 템플릿 객체를 이용해, 대출 모집인 웹 서비스로 동기 호출을 수행하고 반환된 결과를 화면에 출력한다.


4. 신용 평가 기관 인터페이스


4.1. 신용 평가 기관

신용 평가 기관도 동기 웹 서비스를 노출하므로, 신용 평가 기관은 신용 평가 서비스를 스프링 설정 파일에 SOAP 웹 서비스 인터페이스 빈으로 다음과 같이 노출한다.

creditScoreServiceEndpoint 빈은 웹 서비스 인터페이스 객체고, creditScoreServiceImp 빈은 웹 서비스 객체다. CreditScoreServiceImp 빈 클래스가 웹 서비스를 실제 수행한다. 여기서도 웹 서비스 개발에 서비스 우선 개발(service first development) 방법을 사용한다.

CreditScoreService 인터페이스는 다음과 같다.

CreditScoreService 는 평범한 자바 인터페이스다.

CreditScoreServiceImp 빈 클래스는 다음과 같다.

신용 평가 기관의 신용 점수 계산을 시뮬레이션 하기 위해 랜덤 메소드를 사용한다.


4.2. 대출 모집인

대출 모집인은 위에서 설명한 creditScoreServiceEndpoint 빈 설정을 그대로 사용해 신용 평가 기관에게 웹 서비스를 요청한다. 대출 모집인은 BrokerQouteServiceImp 빈에서 다음과 같이 creditBurea 생산자 템플릿을 이용해 신용 평가 기관에게 (동기) 웹 서비스를 호출한다. (BrokerQouteServiceImp의 전체 소스는 프로세스 관리자 절에 있다.)


5. 은행 인터페이스


5.1. 은행

이 예에서 각 은행들은 게시 구독 채널인 ActiveMQ의 JMS Topic에 가입한다. 그러므로 각 은행들은 스프링 설정 파일에 다음과 같은 JMS 수신자(가입자) Camel 라우팅 설정을 갖는다.

각 은행은 bank.loan.request 게시 구독 채널로부터 견적 요청을 수신해, bank? 빈 엔드포인트를 수행하고, 수행 결과 메시지를 bank.loan.relpy 채널로 응답한다.

BankQouteService 인터페이스는 다음과 같다.

BankQouteService 는 평범한 자바 인터페이스다.

BankQouteServiceImp 빈 클래스는 다음과 같다.

BankQouteServiceImp는 기업 통합 패턴 9장의 "MSMQ를 이용한 비동기 구현"을 참조했다. 요청마다 다른 견적을 응답하기 위해 랜덤 메소드를 사용한다.


5.2. 대출 모집인

대출 모집인은 BrokerQouteServiceImp 빈에서 다음과 같이 auction 생산자 템플릿을 이용해 bank.loan.request 게시 구독 채널로 견적 요청 메시지를 게시한다. (BrokerQouteServiceImp의 전체 소스는 프로세스 관리자 절에 있다.)


6. 대출 모집인 프로세스 개요

팁코 예의 대출 모집인 프로세스 정의는 다음과 같다.

Apache Camel 예는 위 프로세스 워크플로우를 프로세스 관리자 라우팅 설정인 borkerProcessManager와 응답 관리자 라우팅 설정인 brokerReplyManager로 대체한다. borkerProcessManager는 대출 클라이언트의 동기 요청을 처리하고, brokerReplyManager는 비동기로 은행들로부터 응답을 수신해 최상의 결과를 선택한다.

프로세스 관리자 borkerProcessManager의 라우팅 설정은 다음과 같다.

borkerProcessManager는 팁코 프로세스 워크플로우의 첫 번째부터 마지막까지 직선상에 위치한 액티비티들을 대체한다. borkerProcessManager는 은행들에게 대출 견적을 게시한 후 견적 결과가 완성될 때까지 실행 스레드를 중지한다. (자세한 설명은 뒤에 나온다.)

응답 관리자 brokerReplyManager의 라우팅 설정은 다음과 같다.

brokerReplyManager는 팁코 프로세스 워크플로우에서 오른쪽에 위치한 "Process Received Quote From Bank" 액티비티를 대체한다. JMS 큐로부터 은행들의 견적 응답을 수집해 최상 견적의 선택하고 borkerProcessManager를 재시작시킨다.


7. 대출 모집인 프로세스 상세


7.1. 프로세스 관리자

프로세스 관리자는 웹 서비스를 통해 대출 클라이언트로부터 대출 견적 요청을 수신하고, 신용 평가 기관 웹 서비스를 호출하고, 은행들에게 견적 요청을 게시하고, 최상의 대출 견적을 대출 클라이언트에게 반환한다.

다음은 프로세스 관리자를 위한 빈들과 borkerProcessManager 라우팅 설정이다.

BrokerQouteServiceImp 빈 클래스의 전체 소스는 다음과 같다.

BrokerQouteServiceImp은 프로세스 관리자로서 신용 평가 기관 인터페이스와 은행 견적 요청을 위한 게시 구독 채널을 멤버 변수로 갖고, 팁코 액티브엔터프라이즈의 대출 모집인 프로세스의 대부분의 액티비티들을 구현한다. BrokerQouteServiceImp은 게시 구독 채널로 은행 견적 요청을 브로드캐스트한 후 잡 저장소와 자바의 동기 메커니즘을 이용해 스레드를 중지하고 brokerReplyManager 라우팅이 job 객체를 깨울 때까지 대기한다.


7.2. 응답 관리자

응답 관리자는 은행들로부터 견적 응답을 수신하고, 견적 응답을 수집하고, 최상의 견적을 선택하고, 프로세스 관리자를 재시작시킨다.

다음은 응답 관리자를 위한 응답 수신 빈들과 brokerReplyManager 라우팅 설정이다.

수집기는 요청마다 10초동안 은행들로부터 견적들을 수집한다. bankReplyAggregator 빈은 은행들의 견적 응답을 bids 객체에 추가한다.

BankReplyAggregator 클래스는 다음과 같다.

이 수집기는 은행으로부터 수신한 메시지로부터 상관관계 식별자를 추출하여 상관 관계 식별자로 잡 저장소에서 잡을 꺼내와 bid 객체에 은행 응답을 추가하고, 다시 잡 저장소에 잡을 저장한다. AggregationStrategy을 상속해 Apache Camel의 수집 로직 전략 메커니즘을 이용했다.

ProcessReceivedQoute 클래스는 다음과 같다.

ProcessReceivedQoute 는 잡 저장소로부터 잡을 꺼내고, 은행 응답 객체를 생성하고, 완성된 bid 목록으로부터 최상의 견적을 찾아, 응답을 채운 후, 잡 객체의 자바 동기화 메커니즘을 이용해 BrokerQouteServiceImp 빈에서 중지된 스레드를 깨운다.


7.3. 잡 저장소

동기 프로세스와 비동기 메시징을 결합하려면 세션 객체 저장소가 필요하다. 이 저장소는 스프링 설정 파일에 빈과 Camel 라우팅을 이용해 설정한다.

잡 저장소의 스프링 빈과 Camel 라우팅 설정은 다음과 같다.

JobRepository 인터페이스는 다음과 같다.

JobRepository 인터페이스를 상속한 EhCacheRepository 클래스는 다음과 같다.

JobRepository 객체는 BrokerQouteServiceImp 클래스 등에서 멤버 변수로 참조된다.


8. 실행

대출 클라이언트는 5초마다 대출 모집인 웹 서비스에게 대출 견적을 요청하고, 대출 모집인은 대출 견적 요청을 은행들에게 게시하면, 5개 은행들은 각각 자신의 대출 견적을 응답하고, 대출 모집인은 5개의 대출 견적 응답 중 최상의 대출 견적을 선택해, 대출 클라이언트에게 반환한다. 아래는 이 솔루션을 실행한 결과다.

이 시뮬레이션 솔루션은 Camel 라우팅 설정으로 실행되며, 대출 클라이언트, 대출 모집인, 신용 평가 기관, 은행들을 모두 포함한다. Apache CXF 프레임워크가 포함되어 있으며, 테스트를 위해 Apache ActiveMQ가 함께 실행된다. 실행 결과의 아래를 보면 대출 클라이언트는 각 대출 견적 요청마다 다른 은행의 견적을 최상의 견적으로 반환 받을 것을 볼 수 있다.


9. 맺음말

필자는 이 글에서 Apache Camel로도 팁코 액티브엔터프라이즈로 구축한 통합 솔루션을 충분히 대체할 수 있음을 보였다. 게다가 팁코 액티브엔터프라이즈로 구축한 솔루션 예보다 Apache Camel로 구축한 솔루션 예가 설정이나 코딩을 더 적게 사용했다. 이렇게 작업량을 줄일 수 있었던 이유는 Apache Camel의 DSL(Domain Specific Language, 도메인 특화 언어)와 이미 완성된 컴포넌트들을 이용했기 때문이다. 웹 서비스를 위해 단지 몇 십 줄의 설정과 코딩이 필요했으며, JMS 서비스를 위해서도 몇 십 줄의 설정과 코딩을 추가했다. 이점이 Apache Camel이 갖는 강력한 생산성이다.

이와 같이 오픈 소스 통합 프레임워크인 Apache Camel을 이용하면 팁코와 같은 상용 EAI 제품으로 구축한 통합 솔루션을 대체하거나 통합 솔루션을 새롭게 구축할 수도 있으나, 여기에는 전제가 따른다. 기업 통합 패턴으로 아키텍처를 분석할 수 있어야 하며, 기존 솔루션의 아키텍처에 대한 이해도 충실해야 하고, Apache Camel과 Spring 프레임워크와 통합에 포함되는 다양한 기술에 대해서도 깊은 이해가 있어야 한다. 이런 준비가 없다면 기업 애플리케이션들을 제대로 통합할 수 없을 것이다. 물론 이런 전제는 상용 EAI 제품을 도입하더라도 동일하게 적용된다. 상용 EAI 제품도 어떤 기능을 제공한다고 해서 단순히 몇 줄의 설정이나 코딩으로 기능이 동작하는 것은 아니다. 상용 EAI 제품을 제대로 활용하려고 해도 통합을 위한 각종 어댑터들을 이해해야 한다. (실제로 어떤 EAI 상용 제품은 기업 통합 패턴의 번호표 패턴[EIP]를 제공하기 위해 수십 페이지 정도의 설정 절차를 숙지해야 하는 경우도 있다.) 그럼에도 상용 EAI 제품만이 해결할 수 있는 통합은 없으며, 오픈 소스를 잘 이용한다면 그에 못지 않는 통합 품질뿐만 아니라 솔루션 구조도 단순화시킬 수 있다는 점은 고무적인 것이다.

필자는 기업 통합 패턴과 통합 프레임워크에서 많은 가능성을 전망하고 있다. 기업 통합 패턴과 Apache Camel 통합 프레임워크는 이 글의 예처럼 상용 EAI인프라를 대체해 기업을 통합시킬 뿐만 아니라, ETL, 배치, 동기화 등 통합의 각론 분야에서도 충분히 가치를 발휘한다. 또한 새롭게 등장하는 개념의 마이크로 서비스(Microservice)를 개발하는 데 있어서도 Apache Camel은 가장 적합한 도구가 되어 가고 있다. 그러므로 아키텍트와 개발자들도 기업 통합 패턴과 Apache Camel에 관심을 갖기 바란다.


참고 사이트

2014년 11월 11일 화요일

Apache Camel을 이용한 마이크로 웹 서비스 개발


1. 들어가며

일반적으로 기업은 사업 상 멀리 떨어진 외부 협력사들이나 자사의 지점들과 정보나 업무를 주고 받아야 한다. 예를 들어 쇼핑몰은 배송지 주소와 물품 배송 정보 등을 인터넷을 통해 배송 업체와 주고 받아야 한다. 즉 쇼핑몰 전자 상거래 시스템은 배송 업체의 물류 시스템과 인터넷에서 원격으로 접속해서 정보를 주고 받아야 한다. 그런데 문제가 되는 점은 이들 개별 회사들이 인터넷의 표준 프로토콜인 TCP/IP 접속을 합의했다 손치더라도 애플리케이션 프로토콜이나 데이터 형식에 대해서는 그다지 합의하지 못했다는 점이다. 즉 각 사의 애플리케이션 프로토콜이나 데이터 형식은 각 사의 환경에 맞춰 각기 달리 발전함으로써 양 사가 통합하려면 한쪽이든 양쪽이든 프로토콜과 데이터 형식을 새로 합의해야 한다.

과거에는 (여전히 현재도) 인터넷 또는 TCP/IP 망에서 떨어진 두 시스템은 데이터 전송에 이미 합의한 TCP/IP 계층의 전송 프로토콜을 적극 이용하는 방향으로 발전해 왔다. 즉 전송을 위해 TCP/IP 소켓을 활용하고, 소켓 접속 수립에 양 애플리케이션이 합의한 절차를 추가하고, 데이터 형식은 전문이라 불리는 (일반적으로 고정 길이 필드를 가진) 메시지를 이용했다. 그런데 이런 방식으로 원격 시스템들을 통합하는 경우 개발자가 부담해야 할 개발 범위와 시간은 상당하게 된다. 통합을 위해 각 사에서 사용하는 다른 프로토콜과 데이터 형식을 서로 맞추거나, 한 쪽에 맞춰야 하고, 통합을 위한 테스트도 양 사가 동시에 수행해야 하기 때문이다. 기업들은 이런 문제를 해결하기 위해 통신과 전문을 편리하게 처리해 주는 통신/전문 라이브러리나 미들웨어 패키지들을 구매하기도 한다. 그러나 벤더 독자적 솔루션을 사용하는 기업은 상이한 솔루션을 사용하는 다른 원격지와 통합하려는 경우 다시 유사한 문제를 해결해야 한다.

이와 같은 문제로 인터넷의 원격지 시스템들 사이 통합에 모두가 동의할 수 있는 수많은 표준들이 제안되었고, 그 중 SOAP(Simple Object Access Protocol)을 많이 사용되고 있다. 그러나 SOAP이 처음 세상에 나왔을 때, SOAP은 이름처럼 간단함을 추구하려 했으나 실제 사용하려면 지나치게 자유도는 높고 사용 절차도 상당히 복잡하다는 문제를 가지고 있었다. 그 결과 관심을 가졌던 전문가들조차 SOAP 사용에 어려움을 호소하거나 심지어 SOAP을 외면하는 사태까지 이르렀었다. 그러나 시간이 지나면서 SOAP을 편리하게 사용할 수 있는 도구들과 프레임워크들이 시행착오를 통해 발전하면서 SOAP의 사용이 과거보다 상당히 편리해 졌고, 인터넷 상의 원격지 시스템들을 통합하는 데 이만큼 잘 정비된 표준도 없기에, 현재는 점차 SOAP을 원격지 통합에 사용해 가는 추세다.

SOAP 이외에 인터넷 상의 원격지 시스템들을 통합하는 표준이나 기술로 XML-RPC, JSON-RPC, Restful, Spring Remote, Camel Remote 등등 HTTP 프로토콜을 기반으로 애플리케이션 계층을 정의하는 여러 프로토콜들이 있다. HTTP 프로토콜은 요청 응답 패턴에 헤더와 본문을 가진 메시지를 사용하고 사용하는 TCP 포트도 기업 인터넷 방화벽이 접속을 허용하므로, 대부분의 애플리케이션 프로토콜들이 HTTP 프로토콜을 전송 프로토콜로 사용한다. 우리가 일반적으로 알고 있는 RPC(Remote Procedure Call) 기술인 CORBA, DCOM, 자바 RMI 등은 사실 인터넷과 같은 WAN 망을 위한 프로토콜이 아니다. 이런 프로토콜은 간단한 요청 응답 패턴 이상의 송수신이 발생하고, 경우에 따라서는 브로커가 개입해야 하고, 사용하는 TCP 포트도 일반적으로 인터넷 방화벽이 개방하지 않는다. 즉 이런 기술들은 LAN에서 사용되는 기술들이다. 그러므로 이런 기술을 WAN 망에 적용하면 도리어 성능이 떨어지거나 플랫폼에 종속되거나 상호 상호 접속이 불가능할 수도 있게 된다. 즉 RPC에 사용되는 프로토콜들도 접속 상황에 따라 적절하게 구분해서 사용해야 제대로 된 효용을 발휘할 수 있는 것이다.

최근에는 마이크로 서비스(Microservice)라는 개념이 확산되고 있다. 마이크로 서비스는 아주 작은 코딩으로 업무를 처리하는 기능 중심의 컴포넌트 애플리케이션을 말한다. 즉 약 100 줄 내외의 짧은 프로그램이나 간단한 설정으로 업무를 수행하게 하는 기술이다. 모든 기능을 집적한 강력한 단일형 애플리케이션에서 기능 별로 전문화된 컴포넌트 애플리케이션으로 분업화하는 기술이라고도 볼 수 있다. 이 기술의 장점은 서비스의 추가/삭제/수정에 다른 서비스들의 영향을 최소화 할 수 있다는 점이다. 모든 서비스를 함께 운영하는 단일형 애플리케이션의 경우 운영되는 서비스들 중 일부를 수정하려면 애플리케이션 내 모든 서비스들도 영향을 받거나 중지해야 하는 문제가 생길 수 있다. 이점은 기업 통합 패턴의 단단한 결합(tight coupling)과 느슨한 결합(Loose Coupling)의 원리 비교와 일맥 상통한다. 마이크로 서비스는 이제 막 등장하는 개념으로 앞으로 어떻게 발전/수정/쇠퇴할 지 좀더 지켜봐야 할 것이다. 그럼에도 필자는 이 글에서 SOAP 웹 서비스를 마이크로 서비스로 구현해 보려고 한다.

SOAP 프로토콜을 이용하려면 웹 서비스를 기술하는 WSDL도 알아야 하고 SOAP 구현체에 맞추어 프로그램도 개발해야 한다. 그러므로 SOAP을 이용한 개발 방법은 SOAP 구현체에 따라 상당히 달라진다. 그리고 SOAP은 웹 서비스에 주로 사용된다. 여기서 말하는 웹 서비스란 인터넷에 HTTP 프로토콜을 통해 노출된 서비스를 말한다. 즉 SOAP 웹 서비스는 인터넷에서 호출 가능한 서비스로 데이터 전송 프로토콜로 HTTP를 사용하고 데이터의 전송 형식과 원격 기능 호출 절차에 SOAP 프로토콜을 사용하는 서비스를 말한다. 그러므로 SOAP 웹 서비스를 구축하기 위해서는 웹 서버가 필요하고 SOAP 프로토콜을 지원하는 구현체(프레임워크)도 필요하다.

SOAP 웹 서비스는 두 가지 접근 방법으로 개발한다. 계약 우선 개발(contract-first development) 방법과 서비스 우선 개발(service-first development) (또는 코드 우선 개발(code-first development)또는 계약 나중 개발(contract-last development)) 방법이다. 계약 우선 개발(contract-first development) 방법이란 WSDL(Web Service Description Language)를 우선 사용해 인터넷 상의 웹 서비스를 정의한 후 웹 서비스 서버나 클라이언트들이 정의된 WSDL에 따라 웹 서비스를 개발 방법이다. 서비스 우선 개발(service-first development) 방법이란 인터넷 상에 웹 서비스를 노출하는 서버가 자바 같은 개발 언어로 서비스를 우선 개발하고 개발된 서비스를 웹 서비스로 노출하는 개발 방법이다. 이때 웹 서비스 서버는 노출된 웹 서비스의 WSDL도 자동으로 노출한다. 이후 웹 서비스 클라이언트들은 웹 서비스 서버가 노출한 WSDL을 사용해 계약 우선 개발 방법으로 웹 서비스 호출을 개발한다. 계약 우선 개발 방법의 장점은 WSDL을 이용해 서비스 구현 없이 서비스의 서명과 입출력을 정의할 수 있다는 점이다. 즉 웹 서비스 서버와 클라이언트가 웹 서비스 구현 없이도 웹 서비스를 문서만으로 우선 정의할 수 있다는 점이다. 반면 서비스 우선 개발은 웹 서비스 서버가 웹 서비스를 우선 구현한 후 웹 서비스 클라이언트에게 WSDL을 알려줌으로써 웹 서비스 클라이언트가 웹 서비스 호출을 구현할 수 있게 한다. 이 방법의 장점은 이미 만들어진 내부 서비스를 웹 서비스로 노출할 때 편리하고, 자동화된 웹 서비스 노출 관례를 따르므로 서비스 개발에 집중할 수 있다는 점이다. 반면 이 방법의 단점은 웹 서비스 노출 시, WSDL이 기계적으로 생성됨으로, WSDL을 직접 기술하는 것보다 웹 서비스 정의 융통성이 떨어질 수 있다는 점이다. 그러나 이런 문제도 웹 서비스 어노테이션이나 설정 등을 통해서 웹 서비스 노출 서명을 수정할 수 있으므로 사실 과거에 서비스 우선 개발이 가졌던 단점은 상당히 완화된다. 필자는 SOAP 웹 서비스 서버 개발에는 서비스 우선 개발 방법을 사용할 것이고, SOAP 웹 서비스 클라이언트 개발에는 계약 우선 개발 방법을 사용할 것이다.

첨언하자면 이 글은 Apache Camel Developer's Cookbook 12장 Web Services가 설명하지 않는 SOAP 웹 서비스의 서비스 우선 개발 방법에 대해 필자가 도전하면서 얻은 노하우를 설명하는 글이다.


2. 웹 서비스 서버 프로젝트 생성

웹 서비스를 구현하기 위해서는 SOAP 기반 웹 서비스 구현을 돕는 아파치 CXF(Apache CXF) 프레임워크와 마이크로 서비스를 가능하게 하는 아파치 카멜(Apache Camel) 통합 프레임워크와 애플리케이션 프레임워크인 스프링(Spring) 프레임워크가 필요하다. 그리고 이들 프레임워크들을 편리하게 추가하고 서비스도 간단히 실행하기 위해 maven(3.1 이상) 프로젝트로 웹 서비스 서버를 개발할 것이다. Maven을 이용해 쉘(명령창)에서 웹 서비스 서버 프로젝트를 다음 같이 생성한다.

생성된 프로젝트 디렉터리 아래 pom.xml 파일에 다음의 의존들을 추가한다.

포함되는 의존들은 CXF를 사용하기 위한 Camel CXF 컴포넌트와 자동으로 의존에 포함되는 CXF 프레임워크와 CXF 웹 서비스를 웹 상에 구동해 주는 CXF jetty 라이브러리들이다.


3. 서비스 입출력 정의

일반적으로 서비스를 구현하려면 클라이언트 요청과 서비스 응답을 정의해야 한다. (이곳의 입출력 클래스는 앞서 언급한 Apache Camel Developer's Cookbook의 웹 서비스 예를 참조했다. 그런데 Cookbook의 예는 계약 우선 개발 방법을 사용하므로 WSDL로부터 Cookbook의 웹 서비스 입출력 클래스 생성되나, 필자는 서비스 우선 개발 방법을 사용하므로 필자의 웹 서비스는 입출력 클래스를 우선 정의한다.)

그러므로 서비스 입력 출력 클래스를 다음과 같이 정의한다.

입출력 클래스는 com.brm.ws.payment.types 패키지 아래 놓았다. 이 패키지 이름은 나중에 SOAP WSDL 입출력 정의에서 네임스페이스로 사용된다. CXF 프레임워크는 자바의 리플렉션 기능을 사용해 위에 정의한 입출력 클래스를 WSDL 입출력 정의로 생성해낸다. 그리고 생성된 WSDL 입출력 정의는 SOAP 마샬링의 메타데이터로 활용된다. 그러므로 SOAP 원격 통신으로 전송되는 입출력 클래스는 마샬링 정보가 WSDL로 정의되므로 별도의 마샬링 메타데이터는 필요하지 않다. 그러나 예를 들어 자바 RMI나 JMS같은 SOAP 이외의 프로토콜을 이용하는 경우, 입출력 객체를 전달하고자 한다면 해당 기술에 맞도록 마샬링 메타데이터를 정의하거나, 입출력 클래스가 Serializable 인터페이스를 상속하도록 해야 한다. Serializable 인터페이스를 상속하는 방법은 기업 통합 패턴의 "9장 팁코 액티브엔터프라이즈를 이용한 비동기 구현" 예를 팁코 액티브엔터프라이즈 대신 아파치 카멜로 대체하는 "아파치 카멜을 이용한 비동기 구현"이란 글에서 JMS 통신을 위해 사용될 예정이니, 그 글을 참고하기 바란다.

SOAP은, 입출력 마샬링 메커니즘뿐만 아니라, 웹 서비스의 오류 정보를 전달하는 fault 메커니즘도 제공한다. 서비스 우선 개발 방법에서는 서비스 메소드가 하나의 반환 형식이 고정되므로, 이 기능은 예외를 이용해 전달한다. (이 점은 서비스 우선 개발 방법이 계약 우선 개발 방법과 비교하여 융통성이 떨어지는 부분이기도 하다. 그러나 웹 서비스 서버와 클라이언트는 fault 메커니즘 대신 서비스 응답에 '반환 코드'나 '오류 상세 메시지'를 포함하는 방식으로 입출력을 합의할 수 있다. 이 경우 SOAP 서비스 클라이언트는, fault 처리를 위한 별도 예외 처리 로직 대신, 서비스의 응답으로부터 '반환 코드'나 '오류 상세 메시지'를 추출해 예외를 처리한다. 게다가 이 방법은 충분히 편리하다. 그러므로 이 점이 꼭 서비스 우선 개발 방법의 약점이라고만은 할 수 없다.)

이 글에서는 기술적인 측면에서 SOAP fault를 웹 서비스 어떻게 클라이언트에게 전달할 수 있는지를 보여줄 것이므로, 다음과 같이 Fault 클래스를 정의한다.

위 Fault 클래스는 reason 멤버 변수를 하나 정의해 간단하게 오류를 설명한다. 서비스 오류를 설명하는 추가적인 정보가 필요한 경우 이곳에 필드를 추가할 수 있을 것이다.


4. 서비스 인터페이스 정의

서비스 입출력을 정의한 후 서비스 기능을 노출하는 서비스 인터페이스를 정의한다. 이 서비스는 위에서 정의한 서비스 입출력 정의의 입력 클래스와 출력 클래스를 사용한다. 여기서 주목할 점은 아직까지 웹이나 SOAP에 대한 고려가 없다는 점이다. 즉 서비스 인터페이스는 순전히 일반적인 자바 인터페이스와 메소드로 정의한다. 정의된 서비스 인터페이스는 다음과 같다.

위 서비스 인터페이스에서 TransferException 예외에 주목한다. 서비스 우선 개발에서는 예외를 이용해 웹 서비스 서버가 fault를 웹 서비스 클라이언트에게 전달한다. Fault 정보를 포함한 TransferException 클래스 정의는 다음과 같다.

CXF 프레임워크의 동작 메커니즘으로 보면 TransferException가 SOAP fault로 전달되지만 TransferException은 예외를 위한 클래스이므로 의미의 중첩을 피하기 위해 실제 SOAP fault의 상세 정보는 여기처럼 Fault 객체를 사용하는 것이 바람직하다. 이것으로 서비스 우선 개발 방법의 서비스 인터페이스 정의는 완료된다.


5. 서비스 빈 구현

서비스 인터페이스가 정의되면, 일반적인 POJO 스타일의 서비스를 구현한다. 여전히 웹 서비스라든지 SOAP이라든지에 대한 구현 상의 고려는 별달리 추가되지 않는다. 즉 이 일반적인 POJO 스타일의 서비스 빈은 손쉽게 웹 서비스로 전환될 것이다. 이 예는 웹 서비스의 뼈대 구현에 초점을 맞추므로, 서비스 빈은 이체를 성공했다고 가정하고 간단하게 성공을 응답한다. 그러므로 구현된 DefaultPayment 서비스 빈 은 다음과 같다.

위 서비스 빈은 백만 원을 넘는 이체 요청은 예외를 발생시킨다. 정상 거래와 비정상 거래를 모두 테스트 하기 위해 이체 한도를 서비스 기능에 포함시켰기 때문이다. 보다시피 위 서비스 빈은 전형적인 POJO 스타일의 자바 클래스다.


6. 웹 서비스 서버 구현

서비스 우선 개발 방법에 따라, 위와 같이 일반적인 POJO 스타일의 서비스 빈을 완성했으면 이제 이 평범한 서비스 빈을 웹 서비스로 노출시킬 차례다. 이를 위해서는 아파치 카멜이 제공하는 camel 네임스페이스뿐만 아니라 cxf 네임스페이스도 스프링 빈 정의 XML에 포함해야 한다. 이들 네임스페이스를 포함한 스프링 XML의 beans 엘리먼트는 다음과 같은 속성들을 갖는다.

위와 같이 카멜과 웹 서비스를 위한 네임스페이스를 추가했다면, 서비스 인터페이스를 웹 서비스로 노출시키는 웹 서비스 빈을 지정한다. 이 빈에 노출될 웹 서비스의 웹 URL과 서비스 인터페이스를 지정한다. 실제 서비스를 구현한 빈도 지정한다.

웹 서비스 빈과 구현 빈을 지정했다면, 이제 카멜 라우팅 정의에 이 두 빈을 포함시켜 웹 서비스 서버로 동작하게 한다. 웹 서비스 서버를 노출하는 카멜 라우팅 정의는 다음과 같다.

이 카멜 라우팅 정의는 카멜 CXF 컴포넌트를 이용해 PaymentServiceEndpoint 를 웹 서비스 소비자(서버)로 노출하고, 웹 서비스 클라이언트로부터 수신한 요청을 다시 paymentService 빈에게 전달하게 한다. 즉 from 엔드포인트는 웹 서비스 서버를 노출하고 to 엔드포인트는 웹 서비스 구현을 호출한다. 그리고 route에 handleFault 속성을 참으로 지정해 서비스 빈에서 발생한 예외를 SOAP fault 메시지로 전달한다. 카멜 라우팅 정의를 이용한 웹 서비스 노출은 이렇게 간단하다. 아래는 모든 설정을 포함한 완전한 스프링 빈 정의 XML이다.

이것으로써 POJO 스타일의 서비스가 SOAP 웹 서비스로의 확장이 완성됐다. 실질적으로 웹 서비스로 노출하기 위해 추가된 코드와 설정은 다 합해도 100줄도 채 되지 않았다. 카멜의 장점 중 하나는 이렇게 카멜 정의 파일만으로도 즉시 카멜 컨테이너를 실행할 수 있다는 점이다.


7. 웹 서비스 서버 실행

이제 웹 서비스 서버를 실행해 보자. 프로젝트 디렉터리에서 maven camel:run 골(goal)을 실행한다. camel:run 골을 실행할 수 있은 이유는 프로젝트 생성 과정에서 pom.xml에 camel-maven-plugin이 포함됐기 때문이다.

다음은 웹 서비스 서버 실행 결과다.

그런데 눈치 빠른 독자라면 무엇인가 이상한 점을 발견했을 것이다. 이 예의 SOAP 웹 서비스 서버는 Tomcat이나 그 밖의 WAS를 이용해 기동되지 않았을뿐더러, 웹 서비스 서버를 기동하기 위해 Jetty와 같은 내장형 서블릿 컨테이너를 스프링 빈으로 복잡하게 설정한 후 다시 여러 빈을 엮는 작업을 하지도 않았다. 웹과 관련된 유일한 설정은 “웹 서비스 구현”에서 cxf:cxfEndpoint를 정의할 때 주소로 지정한 http://localhost:9090/paymentService가 전부다. 위 로그를 보면 CXF 프레임워크가 ReflectionServiceFactoryBean 등을 이용해 cxf:cxfEndpoint에서 지정한 http://localhost:9090/paymentService 주소로 웹 서버를 자동으로 기동한 것을 볼 수 있다. 그리고 이 모든 과정이 관례(CoC, Convention Over Configuration)에 따라 자동으로 완성된다. 즉 개발자는 일반적인 POJO 서비스 빈을 30줄도 안 되는 아주 적은 설정을 사용해 SOAP 웹 서비스를 완성한 것이다. 이런 점에서 이 웹 서비스는 마이크로 웹 서비스다.


8. 웹 서비스 WSDL

CXF 프레임워크는, SOAP 웹 서비스 서버의 실행과 더불어, 웹 서비스의 SOAP 계약인 WSDL을 자동으로 노출한다. 그러므로 웹 서비스 서버를 실행한 후, 웹 브라우저는 웹 서비스가 노출한 웹 URL 주소 뒤에 “?wsdl”을 추가해 조회함으로 웹 서비스의 WSDL을 읽을 수 있다.


9. 웹 서비스 클라이언트 프로젝트 생성

웹 서비스 클라이언트 프로젝트는 웹 서비스 서버 프로젝트 생성에서 사용한 동일한 maven archetype을 사용한다. 서버와 달라지는 점은 'artifactId'가 wsc-example로 바뀌는 정도다.

생성된 프로젝트 디렉터리 아래 pom.xml에 다음과 같이 의존들을 추가한다.

jaxws-api 의존은 CXF 프레임워크의 wsdl2java가 생성한 클래스가 참조하는 의존이고, junit 의존은 웹 서비스 클라이언트를 JUnit 클래스로 만들기 위해 필요한 의존이다.


10. 웹 서비스 클라이언트 입출력 정의

앞서 웹 서비스 서버를 완성했으므로, 웹 서비스 클라이언트의 입출력 개발은 웹 서비스 서버가 노출한 WSDL을 이용해 보자. WSDL이 이미 완성된 경우, 계약 우선 개발 방법을 이용하는 것이 훨씬 수월하기 때문이다. 이 예에서는 WSDL로부터 자바 클래스들을 생성하도록 돕는 CXF 프레임워크의 wsdl2java 유틸리티를 이용해 웹 서비스 클라이언트가 필요한 자바 클래스들을 생성한다. wsdl2java를 다음과 같이 실행하면, 이 예의 웹 서비스 서버 WSDL로부터 웹 서비스 클라이언트 실행에 필요한 자바 패키지와 클래스들이 생성된다.

위 명령을 통해 생성된 패키지와 클래스들을 웹 서비스 클라이언트 프로젝트 디렉터리의 src/java/main 아래 복사한다. 복사하면 다음과 같은 패키지와 클래스들이 보일 것이다.

위와 같이 복사하면 웹 서비스 클라이언트의 입출력 정의는 완성된다. com.brm.ws.payment 패키지는 웹 서비스 서버 개발에서 Payment 인터페이스를 정의한 패키지 위치와 같다. (패키지 이름은 웹 서비스 서버 개발 시 웹 서비스 어노테이션을 사용해 명시적으로 지정할 수도 있다. 사실 요즘은 웹 서비스 어노테이션을 사용해 서비스 우선 개발이더라도 계약 우선 개발의 WSDL을 개발하는 것처럼 섬세하게 웹 서비스 구조를 정의할 수 있다. 즉 어노테이션을 이용해 네임스페이스에 대응되는 패키지 이름이나 서버의 웹 서비스가 정의된 패키지에 대응되는 웹 서비스 인터페이스 이름 등을 명시적으로 지정할 수도 있다. 문제는 어느 방법이 더 수월한 가다.) 패키지 아래 입출력 클래스인 TransferRequest.java, TransferResponse.java와 fault 클래스인 Fault.java를 볼 수 있다. 이렇게 언어별 소스 생성에 자동화 도구를 사용할 수 있다는 점이 계약 우선 개발 방법의 장점 중 하나다.


11. 웹 서비스 클라이언트 인터페이스 정의

웹 서비스의 클라이언트 인터페이스도 wsdl2java 유틸리티가 WSDL로부터 자동으로 생성한다. com.brm.ws.payment 패키지 아래 PaymentPortType.java가 클라이언트의 웹 서비스 인터페이스다.


12. 웹 서비스 호출 구현

wsdl2java 유틸리티를 사용해 웹 서비스 WSDL로부터 자동으로 웹 서비스 클라이언트의 입출력 클래스와 웹 서비스 인터페이스를 모두 생성했으므로 이제 웹 서비스를 호출하기 위한 웹 서비스 클라이언트를 구현해 보자. 웹 서비스 클라이언트를 구현하려면 스프링 빈 정의 XML 파일에 웹 서비스 클라이언트의 CXF 빈과 카멜 라우팅 정의를 설정해야 하고, 이 XML 파일을 이용해 웹 서비스를 호출하는 웹 클라이언트 단위 테스트 클래스를 개발해야 한다. (웹 서비스 클라이언트의 스프링 빈 정의 XML파일도 "웹 서비스 서버 구현"에서 사용한 네임스페이스와 같은 네임스페이스를 사용하므로 이에 대한 지정은 "웹 서비스 서버 구현"을 참조하거나 아래 구현된 camel-context.xml을 참조한다.) 우선 스프링 빈 XML 파일을 설정해 보자. Maven 프로젝트로 생성한 웹 서비스 클라이언트의 src/main/resources/META-INF/spring/camel-context.xml을 다음과 같이 설정한다.

위 스프링 빈 XML 파일에서 cxf:cxfEndpoint 설정은 웹 서비스 서버의 cxf:cxfEndpoint 설정과 구조가 동일하다. 차이점은 웹 서비스 서버의 address 속성 값은 이 주소로 웹 서비스를 노출하고, 웹 서비스 클라이언트는 address 속성 값은 이 주소로 웹 서비스를 요청한다는 점이다. serviceClass 속성에는 웹 서비스 서버의 WSDL로부터 생성된 웹 서비스의 클라이언트 인터페이스를 지정한다.

카멜 라우팅 정의에서 웹 서비스는 생산자 엔드포인트다. 웹 서비스 클라이언트는 카멜의 direct 소비자 엔드포인트를 통해 웹 서비스(웹 서비스 생산자 엔드포인트)를 호출한다. 카멜에서는 생산자 엔드포인트를 동기적으로 호출하는 일관된 방법으로 direct 소비자 엔드포인트를 이용한다. 테스트 클라이언트가 오류를 처리하므로 카멜이 오류를 중간에서 굳이 가로채지 않게 라우팅 정의에 errorHandlerRef 속성을 사용해 카멜이 오류를 무시하도록 했다. errorHandlerRef 속성 값은 오류를 무시하는 noErrorHandler 빈의 참조다.

웹 서비스 클라이언트는 정상 처리와 비정상 처리를 모두 확인하기 위해 다음과 같이 두 테스트 메소드를 포함한 JUnit 테스트 클래스로 작성했다.

이 단위 테스트 클래스는 이체 한도 이내의 이체 웹 서비스 요청과 이체 한도 초과의 웹 서비스 요청을 실행한다. 웹 서비스 단위 테스트 클라이언트 클래스가 카멜의 동기 요청 호출 메소드인 request…를 이용해 direct 소비자를 호출하면 카멜은 이 요청을 웹 서비스 생산자 엔드포인트로 라우팅한다.

이체 한도 이내의 이체 웹 서비스 요청 메소드인 testSmallAmount는 정상적은 응답 객체를 수신하고, 이체 한도 초과의 이체 웹 서비스 요청 메소드인 testLargeAmount는 fault를 수신한다. testLargeAmount 메소드 예외 처리과정을 좀더 자세히 살펴보자. 앞서 웹 서비스 서버의 Payment 서비스 인터페이스는 다음과 같이 정의했었다.

위처럼 웹 서비스 서버의 인터페이스는 TransferException를 transferFunds 메소드의 예외로 지정했었다. 즉 transferFunds 웹 서비스 호출 시 TransferException를 fault로 반환하도록 지정했다. 그리고 TransferException은 다시 Fault 클래스를 포함하고, 이 클래스가 실제 fault 메시지를 포함하도록 개발했었다. 여기서 카멜과 CXF 프레임워크의 오류 처리 관례를 제대로 이해하는 것은 아주 중요하다. 카멜과 CXF를 이용한 웹 서비스 개발의 관례를 제대로 이해하지 못한다면 웹 서비스 개발에 상당한 혼란을 초래할 수 있기 때문이다. 카멜과 CXF 프레임워크의 호출 처리 흐름은 다음과 같다. 정상적인 호출인 경우 입출력 객체만 활용되므로 특별한 어려움은 없다. 그러나 fault를 응답하는 경우 상당한 주위가 필요하다. 일반적으로 카멜은 라우팅 중 발생한 예외를 붙잡아 원인으로 포함시킨 후 CamelExecutionException을 발생시킨다. 이 예의 경우 카멜 예외의 원인은 웹 서비스 호출 중 발생한 TransferException_Exception 예외가 되며, 이 예외는 다시 실제 웹 서비스 서버가 발생시킨 fault를 (여기서는 TransferException을) 감싼다. 그러므로 카멜 메소드를 이용해 웹 서비스를 호출한 클라이언트는 예외가 발생했을 때 CamelExecutionException을 잡고 CamelExecutionException에서 TransferException_Exception를 추출한 후, TransferException_Exception에서 웹 서비스 서버가 발생시킨 fault를 (여기서는 TransferException을) 추출하고, fault에서 필드를(여기서는TransferException에서 Fault 객체를) 최종적으로 추출한다.

여기서 주목할 점은 CXF 프레임워크가 서비스 우선 개발로 작성한 서비스 메소드의 자바 예외 객체를 언어 중립적인 SOAP 입출력 구조가 되도록 WSDL 표현을 자동 생성한다는 점이다. 이 과정에서 CXF 프레임워크는 웹 서비스 서버의 자바 예외를 fault 필드로 표현이 가능하도록 멤버 객체들만 WSDL의 TransferException로 옮긴다. 즉 이 과정에서 자바 예외 클래스의 구조가 사라지고 예외 객체의 멤버들만 WSLD의 fault 필드에 포함된다. 이런 이유로 해서, 이 예의 웹 서비스 서버의 WSDL로부터 생성한 웹 서비스 클라이언트의 TransferException 객체는 자바 예외가 아닌 자바 빈(bean) 클래스가 된 것이다. (이런 혼란이 발생하지 않도록 좀더 섬세하게 웹 서비스를 구축하려면 웹 서비스 어노테이션과 카멜의 예외 핸들러를 이용해야 한다. 그러나 이런 기술을 포함하는 경우 처음 웹 서비스를 맛보거나 실용적인 웹 서비스 구축을 원하는 독자들에게 지나치게 복잡한 설명을 포함해야 할 것 같아, 이에 대한 설명은 포함하지 않았다.)


13. 웹 서비스 클라이언트 실행

이제 웹 서비스를 클라이언트를 실행해 보자. 웹 서비스 클라이언트를 실행하기 전에 웹 서비스 서버를 먼저 실행해야 한다. 글의 "웹 서비스 서버 실행"을 참조한다. 웹 서비스 서버를 실행했으면, maven을 이용해 웹 서비스 클라이언트 테스트를 실행한다. 우리는 maven의 관례에 따라 JUnit 테스트 클래스를 작성했으므로, 테스트 클라이언트의 실행은 다음과 같이 간단하다.

다음은 테스트 클라이언트의 실행 결과이다.

위 결과를 보면 웹 서비스 클라이언트 JUnit 클래스는 적은 금액의 이체 요청(testSmallAmount)의 결과로는 Response 객체에 reply로 OK 값을 반환 받았고, 큰 금액의 이체 요청(testLargeAmount)의 결과로는 예외를 반환 받았다. 그리고 예외를 반환 받은 경우 웹 서비스 클라이언트는 예외 객체 안에서 실제 Fault 정보를 획득할 수 있었다. 기대했던 결과를 얻은 것이다.


14. 맺음말

우리는 이 글에서 서비스 우선 개발(service-first development) 방법으로 SOAP 웹 서비스 서버를 개발했고, 계약 우선 개발(contract-first development) 방법으로 SOAP 웹 서비스 클라이언트를 개발해 보았다. SOAP 웹 서비스 서버는 일반적인 POJO 서비스를 개발하는 절차에 카멜과 CXF 프레임워크 설정을 포함하는 30줄 이내의 스프링 빈 정의 XML을 작성하고, maven으로 camel:run 골(goal)을 실행함으로 완성했다. SOAP 웹 서비스 클라이언트는 웹 서비스 서버의 WSDL로부터 패키지와 클래스들을 생성하고, 카멜과 CXF 프레임워크 설정을 포함하는 30줄 이내의 스프링 빈 정의 XML을 추가하고, 웹 서비스 클라이언트 JUnit 테스트 클래스를 개발하여 maven의 test 단계를 실행함으로 웹 서비스 클라이언트를 테스트 할 수 있었다.

실질적으로 웹 서비스 서버를 구축하는 데 추가된 스프링 빈 정의 XML의 설정은 30줄 이내였고, 웹 서비스 클라이언트가 웹 서비스를 호출하도록 하는 데 추가된 스프링 빈 정의 XML의 설정도 30줄 이내였다. 이런 정도의 개발량으로 웹 서비스를 구축하는 방법으로는 카멜을 활용하는 것 이외는 달리 없을 것이다. 이점이 바로 카멜이 갖는 강력함이다.

이와 같이 짧은 소스나 간단한 설정으로 구축되는 기능 중심의 서비스를 마이크로 서비스(Microservice)라 부른다. 물론 마이크로 서비스가 갖는 여러 가지 특징들이 있긴 있으나, 현재 이 용어는 완전하게 정립되지는 않았다. 그럼에도 아파치 카멜은 마이크로 서비스를 구현하는 데 가장 적합한 도구라는 것을 필자를 포함해 통합 전문가들이 동의하고 있다. 앞으로는 중앙 집중식 단일 시스템에 의한 서비스들의 제공이 아닌 기능 별 분산 자치식 시스템에 의한 마이크로 서비스로의 패러다임이 다가 올 것이다. 그리고 마이크로 서비스 인프라 솔루션을 구축함에 있어, 기업 통합 패턴의 패턴 언어는 중심 방법론이 될 것이고, 아파치 카멜은 중요한 구축 도구로서 사용될 것이다.

이 글은 기업 통합 패턴 "9장 팁코 액티브엔터프라이즈를 이용한 비동기 구현"을 아파치 카멜로 대체하는 방법을 모색하던 중, 동기 호출 부분을 웹 서비스로 대체하는 과정에서 획득한 기술적인 노하우를 별도의 내용으로 정리한 글이다. 기업 통합 패턴 "9장 팁코 액티브엔터프라이즈를 이용한 비동기 구현"을 아파치 카멜로 대체한 글에 이 기술을 활용할 것이므로 이 글 이후 이후 조만간 게시할 글도 관심 있게 지켜봐 주길 바란다.


참고 사이트

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월 4일 금요일

실시간 트위터 이미지 사이트 개발기 3


웹소켓(WebSocket)

웹 브라우저에서 오랫동안 사용한 통신 프로토콜은 HTTP다. HTTP는 동기 방식의 요청-응답 프로토콜로 반드시 클라이언트인 웹 브라우저가 요청해야만 서버인 웹 서버가 응답하는 프로토콜이다. 그러므로 HTTP 접속은 요청으로 시작해서 요청-응답을 반복하다가 반드시 응답으로 끝난다. 이 방식은 최초에 웹 혁명을 이끄는 데 있어서 중요한 단순성을 제공했다. 단순한 요청-응답 방식은 극단적인 수준까지 요청-응답을 처리하도록 서버를 개선하도록 했다. 범용 웹 사이트를 가능하게 하기 위해 TCP/IP 스택은 비약적으로 개선됐고, 아파치 웹 서버 같은 견고하고 성능도 좋은 서버들도 개발됐다. 사실 웹 기술이 보급되기 전까지 전통적인 클라이언트 서버 방식의 서버는 수십 클라이언트들의 접속을 처리할 수 있다면 할 일을 충분히 한다고 인정받던 시절도 있었다. 그러나 웹의 보급과 발전 덕분에 서버는 동시에 수천 접속을 유지하고 수만 요청도 처리할 수 있도록 비약적으로 진화했다. C10k 문제(일만 동시 접속 문제)라는 용어가 등장할 정도로 다수 동시 접속 처리 문제는 개발자들을 골치 아프게 했지만, 현재는 다양한 아키텍처 패턴을 이용해 이 문제를 극복하고 있다. 그러나 시대가 발전하면서 요청-응답으로는 처리하기 어려운 시나리오들도 웹 브라우저를 이용해 처리하고자 하는 요구가 끊임없이 확대됐다. 예를 들어 웹에서 실시간으로 변하는 증권 시세를 표시하려면 요청-응답 방식만으로는 해결할 수 없었다. 해결했다손 치더라도 속도나 성능 효용성이 충분하지 못했다. 그래서 자바 애플릿이나 액티브X 컨트롤을 이용해 소켓 통신으로 문제를 우회하려고도 했다. 그러나 이런 플러그인 기술들도 만족스럽지 못했고 보안 취약성이 또 다른 문제가 되기도 했다. 또 다른 예로 웹 채팅 애플리케이션이 있다. 웹 채팅을 구현하려면 Ajax 같은 복잡한 기술을 사용해야 그런대로 가능했다. 이런 방법들은 온전한 비동기 통신 기술이 아니다. 채팅은 상대방이 입력한 글이 웹 브라우저의 요청 없이도 즉시 본인의 웹 브라우저에 도착해야 하는 전형적인 비동기 통신 모델이다. 그런데 Ajax도 반드시 요청해야만 응답을 (비동기로) 수신할 수 있으므로 온전한 비동기 통신 방식은 아니다. 결국 요청-응답 방식의 HTTP 프로토콜 덕에 가질 수 있었던 단순성이 웹의 성공을 이끌었던 반면, 이 단순성은 웹 기반 비즈니스 시나리오가 확대됨에 따라 점점 부족함으로 작용하게 됐다. 완전한 비동기를 웹 브라우저에서 사용하려고 할 때, 단순함이 도리어 복잡한 Ajax 방식을 사용하게 강요한 면도 있었다. 이와 같이 확장되는 웹 기반 비즈니스 시나리오를 충족시키기 위해서는 좀더 편리한 통신 방법이 필요했다. 그리고 그 산물로 완전한 양방향 통신을 지원하는 웹소켓 프로토콜이 등장했다.

웹소켓 프로토콜은 상당히 최근에 등장했다. 2011년에서야 IETF는 웹소켓 프로토콜을 정의한 RFC 6455 문서를 표준으로 제정했다. 이에 따라 웹 브라우저들은 2011년부터 상호 호환성이 보장되는 RFC 6455 웹소켓 표준으로 웹소켓 지원을 통일했다 크롬 16, 사파리 6, 파이어폭스 11, 인터넷 익스플로러 10 이후 웹 브라우저들은 RFC 6455 웹소켓 표준을 준수한다. 웹소켓은 글자 그대로 소켓과 거의 같은 기능을 제공한다. 즉 일반적인 소켓이 제공하는 양방향 통신을 제공한다. (그러나 TCP/IP 계층보다 상위 계층 프로토콜로 독자적인 패킷 구조를 갖는다.) 그러므로 HTTP 프로토콜과 달리 접속이 완성된 후 TCP 소켓 접속처럼 서버의 우선 전송도 가능해 진다. 즉 요청-응답 순서가 클라이언트에서 서버뿐만 아니라 서버에서 클라이언트로도 가능하다. 그러므로 웹소켓은 클라이언트 풀(pull) 뿐만 아니라 서버의 푸시(push)도 어렵지 않게 제공한다. 그 결과 웹소켓은 동기 방식뿐만 아니라 비동기 방식 통신 프로토콜로 완벽하게 구현하게 하는 기반을 제공한다.

웹소켓은 HTTP 프로토콜에서 프로토콜 갱신 핸드쉐이크를 통해 웹소켓 프로토콜로 전환이 되는 구조를 가지고 있어, HTTP 표준 포트인 TCP 80 포트를 그대로 활용할 수 있다는 장점을 가진다고 한다. 그러나 이 부분에 있어서 필자는 다른 견해를 갖는데, 그 이유는 TCP 80 포트를 바인드하는 웹 서버가 HTTP 프로토콜과 웹소켓 프로토콜을 동시에 처리하려면 내부 아키텍처가 상당히 다른 두 프로토콜 스택을 서버 안에 함께 포함해야 한다. 웹소켓은 Stomp 같은 상위 프로토콜의 하부 프로토콜로 사용되기도 하는데 이 경우 또 다른 Stomp 프로토콜 스택도 웹 서버에 포함시켜야 하는 문제가 발생한다. 프로토콜 스택 개발은 웹 서버 개발과는 다른 문제일 수 있다. 그리고 아직 웹 서버들이 충분이 웹소켓을 다루는 기술이 성숙되지 않은 문제도 있다. 그러므로 웹소켓을 사용해야 한다면 웹 서버와 별도로 전용 웹소켓 서버를 운영하면서 TCP 포트를 별도로 개방하거나 방화벽 등의 정책으로 인해 접속 문제가 있을 수 있다면 별도 서버에 또 다른 TCP 80 포트를 개방해 웹소켓 서버를 운영하는 방법이 합리적이지 않을까 생각된다. 아직까지는 웹소켓의 사용 방식에 대해 뚜렷한 일관적인 방식이 등장하고 있지는 않은 것 같다.

웹소켓의 또 다른 문제는 웹소켓 위의 애플리케이션 프로토콜이 충분히 성숙돼지 않았다는 점이다. 웹소켓은 TCP 계층에 아주 얇은 층을 추가한다. 그러므로 웹소켓을 직접 사용하는 것은 마치 TCP 소켓을 직접 사용하는 것과 같다. 일반적으로 개발자들은 TCP 소켓을 직접 사용하기보다 상위 애플리케이션 프로토콜을 이용한다. 이런 예로 Telnet, Ftp, Ssh, HTTP, AMQP, OpenWire, Stomp, MQTT 등등이 애플리케이션 프로토콜이다. 그러므로 웹소켓을 충분히 잘 활용하려면 웹소켓 위의 애플리케이션 프로토콜이 많아지고 또 쓰임새도 정비돼야 한다.


실시간 트위터 이미지 사이트의 웹소켓 사용

웹소켓을 먼저 설명한 이유는 실시간 트위터 이미지 사이트도 웹소켓을 사용하기 때문이다. 실시간 트위터 이미지 사이트는 웹소켓을 아주 쓸모 있게 사용한다. 실시간 트위터 이미지 사이트의 전체적인 정보 흐름과 통신 프로토콜들은 다음과 같다.

1) 미국에 위치한 실시간 트위터 이미지 사이트의 백그라운드 Camel 서비스는 트위터에서 고양이로 트윗을 검색해 결과들을 수신한다. 2) 그 중 이미지를 포함한 트윗을 추출해 한국에 위치한 ActiveMQ 서버에 OpenWire 프로토콜을 사용해 메시지로 전달한다. 3) 메시지를 수신한 ActiveMQ 서버는 메시지를 게시 구독 방식의 토픽에 게시한다. 실시간 트위터 이미지 사이트에서 페이지를 조회한 웹 브라우저는 이미 웹소켓 위의 Stomp 프로토콜로 ActiveMQ 서버에 접속한 토픽 구독자다. 4) 토픽에 게시된 고양이 이미지 트윗 메시지 사본은 웹소켓 위의 Stomp 프로토콜을 통해 ActiveMQ 서버로부터 웹 브라우저로 비동기적으로 전송된다. 웹 브라우저는 비동기적으로 수신된 메시지를 웹 브라우저 화면에 고양이 쎔네일 이미지로 출력한다.

미국에 위치한 실시간 트위터 이미지 사이트 웹 서버의 Camel 서비스와 대한민국에 위치한 ActiveMQ 서버는 메시징 프로토콜로 OpenWire 프로토콜을 사용했다. OpenWire 프로토콜은 다양한 언어나 플랫폼에서 ActiveMQ에 접속할 수 있게 하는 메시징 프로토콜이다. ActiveMQ는 OpenWire, AMQP, XMPP, MQTT 등 다양한 메시징 프로토콜을 지원한다. 웹 브라우저와 ActiveMQ 서버는 웹소켓 위의 Stomp 프로토콜을 사용한다. Stomp 프로토콜을 사용한 이유는 WebSocket 위의 Stomp 프로토콜을 지원하는 STOMP.js라는 JavaScript 라이브러리가 있고, ActiveMQ 서버도 웹소켓 위의 Stomp 프로토콜을 잘 지원하기 때문이다. 참고로 TCP/IP 위의 Stomp 프로토콜과 웹소켓 위의 Stomp 프로토콜은 호환되지 않는다. TCP/IP 위의 Stomp 프로토콜은 Stomp 프로토콜이 TCP 계층 바로 위에 구현되는 반면, 웹소켓 위의 Stomp 프로토콜은 TCP 계층 위에 웹소켓 계층이 있고 그 위에 Stomp 프로토콜이 구현된다. 그러므로 Stomp 프로토콜을 지원한다고 해도 직접적으로 웹소켓 위의 Stomp 프로토콜로 통신할 수 없다. 문자 기반 프로토콜이어서 인지, 웹소켓 위에서 가장 구현하기 쉬워서 인지 상당히 이른 시기에 웹소켓 위의 Stomp 프로토콜은 등장했다. 한편 RabbitMQ의 기본 프로토콜인 AMQP 프로토콜이 웹소켓 위의 JavaScript 라이브러리로 개발이 된다면 웹 브라우저에서도 AMQP 프로토콜을 사용할 수 있을 것이다. 그러나 현재 RabbitMQ 쪽에서는 Node.js와 연결에 주력하고 있는 것 같다. RabbitMQ는 Node.js에서 동작하는 AMQP JavaScript 라이브러리까지만 지원하고 있다. 아마도 이 JavaScript 라이브러리는 TCP/IP 위에서 구현된 라이브러리일 것이다. 즉 AMQP 프로토콜의 하부 통신 계층이 TCP일 것이다. 그러므로 현재 웹소켓 위에서 사용할 수 있는 메시징 프로토콜 라이브러리는 STOMP.js이 유일하다. (글을 쓰면서 좀더 조사해 보니 최근 들어 웹소켓 위의 MQTT 프로토콜도 사용 가능한 것으로 보인다. 이에 대한 검토는 독자들에게 맡긴다!) 여기서 주목할 점은 메시징 프로토콜과 메시지 채널(큐, 토픽)은 서로 다른 종류의 개체라는 점이다. 즉 같은 메시지 채널이라도 여러 프로토콜로 발신, 수신, 게시, 구독이 가능하다. 실시간 트위터 이미지 사이트도 동일한 토픽에 대해 메시지 전송은 OpenWire 프로토콜을 메시지 수신은 웹소켓 위의 Stomp 프로토콜을 사용했다.

웹소켓을 사용하면 웹 브라우저를 기업 통합 솔루션의 비동기 메시징 컴포넌트의 일원으로 포함시킬 수 있게 된다. 실시간 트위터 이미지 사이트는 트위터 이미지 메시지를 비동기로 전송하는 용도로 간단하게 웹소켓을 사용했지만, 앞서 언급했듯이 웹소켓은 웹 브라우저의 요청-응답 또는 폴링 방법으로는 해결하기 어려운 실시간 비동기 정보를 손쉽게 처리할 수 있게 해준다. 예를 들어 실시간 뉴스, 실시간 대시보드, 실시간 제어, 실시간 게임, 실시간 그래프 애플리케이션 같은 것들도 웹 브라우저로 구현할 수 있는 가능성을 열어준다. 이를 위해서는 SVG(SVG(Scalable Vector Graphics)나 Canvas 같은 HTML5의 다른 기능들도 활용해야 할 것이다. 또한 필자도 웹소켓이 요청-응답 형식의 기존 HTTP 프로토콜을 완전히 대체할 수 있다고는 생각지 않는다. 기존 HTTP 프로토콜은 화면 조회, 웹 서비스, Restful, Ajax RPC 등 다양한 요청-응답 패턴을 여전히 잘 수용한다. 그러므로 웹소켓의 역할은 요청-응답 방식의 HTTP 프로토콜로는 쉽게 해결할 수 없었던 실시간 비동기 통신을 웹 브라우저에 제공함으로 웹 브라우저의 사용 범위를 넓혀주는 것으로 봐야 한다. 즉 실시간 비즈니스 시나리오가 포함된 멋진 웹 애플리케이션을 만들려면, HTTP 프로토콜과 웹소켓 프로토콜 중 어느 하나만을 고집하는 것이 아니라 서로의 장점을 함께 활용해야 한다. 편식은 언제나 몸을 해롭게 한다.

웹소켓은 표준이 2011년에 완성됐고 표준을 지원하는 브라우저도 2011년 이후 버전들이고 특히 대한민국에서 많이 사용하는 인터넷 익스플로러는 버전 10부터 지원되므로 아직은 웹소켓을 사용한 사례가 많지 않은 것도 사실이다. 그러나 웹소켓을 범용 서비스가 아닌 기업 통합 등 기업의 특화된 서비스에 이용한다면 실시간 정보의 사용에 있어서 전용 클라이언트 애플리케이션 못지 않게 훌륭한 웹 브라우저 애플리케이션을 개발할 수 있을 것이라 기대한다. 이 글을 통해 독자들도 웹소켓의 가능성에 주목할 수 있는 계기가 됐으면 한다.



참고 사이트


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단계 커밋의 오류 시 해결할 대응 전략이 있는지, 애플리케이션 수준에서 트랜잭션을 해결할 수 있는지, 트랜잭션 오류에 대한 해결 전략은 완비됐는지 등을 충분히 고려해야 할 것이다.


참고 자료