LAYERED ARCHITECTURE(계층형 아키텍처)

등록일: 2014. 10. 02

도메인 주도 설계: 소프트웨어의 복잡성을 다루는 지혜

  • 에릭 에반스 지음
  • 이대엽 옮김
  • 600쪽
  • 38,000원
  • 2011년 07월 21일

도시 목록에서 화물의 목적지를 선택하는 것과 같이 간단한 사용자 행위를 지원하는 해운 애플리케이션에도 (1) 위젯을 화면에 그리고 (2) 선택 가능한 모든 도시 목록을 데이터베이스에서 조회하며 (3) 사용자가 입력한 내용을 해석하고 유효성을 검증하고 (4) 선택된 도시를 화물과 연결하며 (5) 변경내역을 데이터베이스에 반영하는 프로그램 코드가 들어 있어야 한다. 이러한 모든 코드가 동일한 프로그램의 일부를 차지하더라도 그중 일부만이 해운 업무와 관련돼 있다.

소프트웨어 프로그램에는 갖가지 작업을 수행하는 설계와 코드가 포함된다. 소프트웨어 프로그램은 사용자 입력을 받아들이고 업무 로직을 수행하며, 데이터베이스에 접근하고, 네트워크상으로 통신하며, 사용자에게 정보를 보여주는 등의 일을 수행한다. 따라서 각 프로그램의 기능과 관련된 코드의 양은 상당히 많을 수 있다.

객체지향 프로그램에서는 종종 사용자 인터페이스(UI, User Interface)와 데이터베이스, 기타 보조적인 성격의 코드를 비즈니스 객체 안에 직접 작성하기도 한다. 부가적인 업무 로직은 UI 위젯과 데이터베이스 스크립트에 들어간다. 이런 일이 발생하는 까닭은 단기적으로는 이렇게 하는 것이 뭔가를 동작하게 하는 가장 쉬운 방법이기 때문이다.

도메인에 관련된 코드가 상당한 양의 도메인과 관련이 없는 다른 코드를 통해 널리 확산될 경우 도메인에 관련된 코드를 확인하고 추론하기가 굉장히 힘들어진다. UI를 표면적으로 변경하는 것이 실질적으로 업무 로직을 변경하는 것으로 이어질 수 있다. 업무 규칙을 변경하고자 UI 코드나 데이터베이스 코드, 또는 다른 프로그램 요소를 세심하게 추적해야 할지도 모른다. 응집력 있고, 모델 주도적인 객체를 구현하는 것이 비현실적인 이야기가 돼버리고 자동화 테스트가 어려워진다. 기술과 로직이 모두 각 활동에 포함돼 있다면 프로그램을 매우 단순하게 유지해야 하며, 그렇지 않으면 프로그램을 이해하기가 불가능해진다.

매우 복잡한 작업을 처리하는 소프트웨어를 만들 경우 관심사의 분리(separation of concern)가 필요하며, 이로써 격리된 상태에 있는 각 설계 요소에 집중할 수 있다. 동시에 시스템 내의 정교한 상호작용은 그러한 분리와는 상관없이 유지돼야 한다.

소프트웨어 시스템을 분리하는 방법은 다양하지만 경험과 관례에 근거해 산업계에서는 LAYERED ARCHITECTURE, 좀더 구체적으로 몇 개의 일반화된 계층이 널리 받아들여지고 있다. 계층화라는 은유는 널리 활용되므로 대다수의 개발자는 이를 직관적으로 받아들인다. 계층화에 관한 각종 논의는 논문에서 찾아볼 수 있으며 간혹 패턴의 형식을 갖추기도 한다(Buschmann et al. 1996의 31 ~ 51페이지에 나온 것처럼). 계층화의 핵심 원칙은 한 계층의 모든 요소는 오직 같은 계층에 존재하는 다른 요소나 계층상 “아래”에 위치한 요소에만 의존한다는 것이다. 위로 거슬러 올라가는 의사소통은 반드시 간접적인 메커니즘을 거쳐야 하며, 이러한 간접적인 메커니즘에 관해서는 나중에 논의하겠다.

계층화의 가치는 각 계층에서 컴퓨터 프로그램의 특정 측면만을 전문적으로 다룬다는 데 있다. 이러한 전문화를 토대로 각 측면에서는 더욱 응집력 있는 설계가 가능해지며, 이로써 설계를 훨씬 더 쉽게 이해할 수 있다. 물론 가장 중요한 응집력 있는 설계 측면을 격리하는 계층을 선택하는 것도 매우 중요하다. 거듭 말하지만 경험과 관례를 바탕으로 널리 받아들여지는 계층화가 어느 정도 정해졌다. 이러한 계층화는 다양한 모습으로 나타나지만 대다수의 성공적인 아키텍처에서는 아래의 네 가지 개념적 계층으로 나뉜다.

어떤 프로젝트에서는 사용자 인터페이스와 애플리케이션 계층을 명확히 구분하지 않기도 하며, 여러 개의 인프라스트럭처 계층이 존재하는 프로젝트도 있다. 하지만 MODEL-DRIVEN DESIGN을 가능케 하는 것은 결정적으로 도메인 계층을 분리하는 데 있다.

그러므로

복잡한 프로그램을 여러 개의 계층으로 나눠라. 응집력 있고 오직 아래에 위치한 계층에만 의존하는 각 계층에서 설계를 발전시켜라. 표준 아키텍처 패턴에 따라 상위 계층과의 결합을 느슨하게 유지하라. 도메인 모델과 관련된 코드는 모두 한 계층에 모으고 사용자 인터페이스 코드나 애플리케이션 코드, 인프라스트럭처 코드와 격리하라. 도메인 객체(표현이나 저장, 애플리케이션 작업을 관리하는 등의 책임에서 자유로운)는 도메인 모델을 표현하는 것에만 집중할 수 있다. 이로써 모델은 진화를 거듭해 본질적인 업무 지식을 포착해서 해당 업무 지식이 효과를 발휘할 수 있을 만큼 풍부하고 명확해질 것이다.

인프라스트럭처 계층과 사용자 인터페이스 계층에서 도메인 계층을 분리하면 각 계층을 훨씬 더 명료하게 설계할 수 있다. 격리된 계층을 유지하는 데 드는 비용은 훨씬 더 적은데, 이는 격리된 계층이 각자 다른 속도로 발전해서 각기 다른 요구에 대처할 것이기 때문이다. 그뿐만 아니라 분산된 시스템에 배포할 때도 이러한 분리가 도움되는데, 통신상의 부하를 최소화하고 성능을 개선하고자 각기 다른 서버나 클라이언트에 각 계층을 유연하게 둘 수 있기 때문이다(Fowler 1996).

예제

온라인 뱅킹 기능을 여러 계층으로 나누기

어떤 애플리케이션에서 은행 계좌를 유지하는 데 필요한 다양한 기능을 제공하고 있다. 그중 하나가 바로 사용자가 두 계좌번호와 일정 금액을 입력하거나 선택하면 이체가 시작되는 자금 이체 기능이다.

예제를 쉽게 다루고자 보안과 같은 주요 기술적인 특징은 생략했다. 그뿐만 아니라 도메인 설계도 상당히 단순화했다(현실적인 복잡성은 계층형 아키텍처에 대한 필요성만을 높일 것이다). 나아가 여기에 포함된 인프라스트럭처는 예제를 명확하게 설명하고자 단순하고 분명하게 만든 것일 뿐 제안하고자 하는 설계는 아니다. 나머지 기능에 대한 책임은 그림 4.1과 같이 계층화할 수 있다.

그림 4-1객체는 자신이 속한 계층에 맞는 책임을 수행하며, 같은 계층에 존재하는 객체와 더 결합된다.

여기서 눈여겨봐야 할 것은 응용 계층이 아닌 도메인 계층에서 주요 업무 규칙을 책임지고 있다는 것이며, 이 경우 “모든 대변(debit)에는 그것과 일치하는 차변(credit)이 있다”가 업무 규칙에 해당한다.

이 애플리케이션에서는 누가 송금을 요청했는가에 관해서는 아무런 가정도 하지 않는다. 아마 프로그램에는 계좌번호와 금액을 입력하는 필드와 명령 버튼이 포함된 사용자 인터페이스가 있을 것이다. 하지만 그와 같은 사용자 인터페이스는 응용 계층이나 그 아래의 계층에는 영향을 주지 않는 XML 형식의 송금 요청으로 대체될 수도 있다. 이 같은 분리가 중요한 까닭은 프로젝트에서 사용자 인터페이스를 자주 대체해야 하기 때문이 아니라 깔끔한 관심사의 분리를 토대로 각 계층의 설계를 이해하고 유지하기가 쉬워지기 때문이다.

사실 그림 4.1은 도메인을 격리하지 않았을 때의 문제점을 완화해서 보여준다. 왜냐하면 여기엔 요청에서 트랜잭션 제어에 이르기까지의 모든 것들을 포함해야 했기 때문에 전체 상호작용을 이해하기에 충분할 정도로 단순하게 유지하고자 도메인 계층을 어렵지 않게 표현해야 했다. 우리가 격리된 도메인 계층의 설계에만 집중했다면 우리의 머릿속과 페이지상에는 도메인 규칙을 더욱 잘 표현하는 모델이 들어 있을 것이며, 이러한 모델에는 원장 1 객체를 비롯해 차변과 대변 객체, 또는 금전 거래와 관련된 객체가 들어 있을 것이다.


계층 간 관계 설정

지금까지 계층의 분리와 그러한 분할 방법 가운데 어떤 것이 프로그램의 각 측면, 특히 도메인 계층의 설계를 향상시키는지 집중적으로 살펴봤다. 그럼에도 당연히 각 계층은 서로 연결돼야 한다. 분리의 이점을 잃지 않으면서 각 계층을 서로 연결하는 것이야말로 각종 패턴이 존재하는 이유다.

각 계층은 설계 의존성을 오직 한 방향으로만 둬서 느슨하게 결합된다. 상위 계층은 하위 계층의 공개 인터페이스를 호출하고 하위 계층에 대한 참조를 가지며(최소한 임시로라도), 그리고 일반적으로 관례적인 상호작용 수단을 이용해 하위 계층의 구성요소를 직접적으로 사용하거백과사전나 조작할 수 있다. 그러나 하위 수준의 객체가 상위 수준의 객체와 소통해야 할 경우에는(직접적인 질의에 응답하는 것 이상으로) 또 다른 메커니즘이 필요한데, 이 경우 콜백(callback)이나 OBSERVER(관찰자) 패턴(Gamma et al. 1995)처럼 계층 간에 관계를 맺어주는 아키텍처 패턴을 활용할 수 있다.

응용 계층과 도메인 계층에 UI를 연결하는 패턴은 MODEL-VIEW-CONTROLLER(MVC, 모델-뷰-컨트롤러)에서 유래한다. MVC는 과거 1970년대에 스몰토크(Smalltalk) 분야에서 발견되어 MVC를 따르는 여러 UI 아키텍처에 영감을 줬다. Fowler(2003)에서는 이 주제에 관해 MVC 패턴을 비롯한 몇 가지 유용한 변종에 관해 논한다. Larman(1998)에서는 이러한 MODEL-VIEW SEPARATION PATTERN(모델-뷰 분리 패턴)에서의 관심사에 관해 연구했는데, 그가 제안한 APPLICATION COORDINATOR(애플리케이션 조율자) 패턴은 애플리케이션 계층을 연결하는 접근법 가운데 하나다.

UI와 애플리케이션을 연결하는 것과 관련된 다른 유형의 접근법도 있다. 하지만 논의의 목적상 도메인 계층을 격리해서 해당 도메인 객체를 설계할 때 동시에 사용자 인터페이스도 생각할 필요가 없게 만들어준다면 어떤 접근법이라도 괜찮다.

보통 인프라스트럭처 계층에서는 도메인 계층에서 어떤 활동이 일어나게 하지 않는다. 인프라스트럭처 계층은 도메인 계층의 “아래”에 있으므로 해당 인프라스트럭처 계층이 보조하는 도메인의 구체적인 지식을 가져서는 안 된다. 사실 그와 같은 기술적인 기능은 대개 SERVICE(서비스)로 제공된다. 이를테면, 어떤 애플리케이션에서 이메일을 전송해야 한다면 메시지 전송 인터페이스가 인프라스트럭처 계층에 위치할 수 있으며, 애플리케이션 계층의 각 요소는 인프라스트럭처 계층에 메시지 전송을 요청할 수 있다. 이러한 분리는 어느 정도 융통성을 별도로 제공한다. 메시지 전송 인터페이스는 이메일 송신기나 팩스 송신기, 또는 다른 사용할 수 있는 어떤 것에도 연결될 수 있다. 그러나 분리의 주된 이점은 애플리케이션 계층이 단순해져서 애플리케이션 본연의 책임에만 집중하게 되는 것이며, 이로써 메시지를 “언제” 보내는지는 알아도 “어떻게” 보 내는지는 알 필요가 없어진다.

응용 계층과 도메인 계층에서는 인프라스트럭처 계층에서 제공하는 SERVICE를 요청한다. SERVICE의 범위와 인터페이스를 적절히 선정하고 설계한다면 호출하는 측은 SERVICE 인터페이스에서 캡슐화하는 정교한 행위를 바탕으로 느슨하게 결합되고 단순해질 수 있다.

그러나 모든 인프라스트럭처가 상위 계층에서 호출할 수 있는 SERVICE의 형태로 만들어지는 것은 아니다. 어떤 기술적인 구성요소는 다른 계층의 기본적인 기능을 직접적으로 지원하도록 만들어져(이를 테면, 모든 도메인 객체에 대한 추상 기반 클래스를 제공하는 것과 같이) 그러한 계층과 관계를 맺는 메커니즘(MVC 및 그와 비슷한 패턴의 구현과 같은)을 제공하기도 한다. 그러한 “아키텍처 프레임워크”는 프로그램의 다른 요소를 설계하는 데 미치는 영향이 훨씬 더 크다.

아키텍처 프레임워크

인프라스트럭처가 인터페이스를 통해 호출되는 SERVICE의 형태로 제공된다면 계층화의 동작방식과 각 계층이 느슨하게 결합되는 방식은 상당히 직관적이다. 하지만 일부 기술적인 문제에는 더욱 침습적인 형태의 인프라스트럭처가 필요하다. 수많은 인프라스트럭처의 요구사항을 통합하는 프레임워크는 종종 다른 계층이 매우 특수한 방식으로 구현되기를 요구하는데, 이를테면 프레임워크 클래스의 하위 클래스가 돼야 한다거나 일정한 메서드 서명을 지정해야 한다는 것이 여기에 해당한다(하위 클래스가 그 클래스의 부모 클래스보다 상위 계층에 있는 것이 직관적이지 않아 보일지도 모르지만 염두에 둘 것은 어느 클래스가 다른 클래스에 대한 지식을 더 많이 반영하고 있느냐다). 가장 바람직한 아키텍처 프레임워크라면 도메인 개발자가 모델을 표현하는 것에만 집중하게 해서 복잡한 기술적 난제를 해결한다. 하지만 프레임워크가 방해가 될 수도 있는데, 프레임워크에서 도메인 설계와 관련된 의사결정을 제약하는 가정을 너무 많이 만들어 내거나 구현을 너무 과중하게 만들어 개발을 더디게 하는 경우가 있기 때문이다.

일반적으로 어떤 형태로든 아키텍처 프레임워크와 같은 것은 필요하다(간혹 팀에서 고른 프레임워크가 팀에 제대로 된 도움을 주지 못하더라도). 프레임워크를 적용할 때 팀은 프레임워크의 목적에 집중해야 하는데, 그러한 프레임워크의 목적은 도메인 모델을 표현하고 해당 도메인 모델을 이용해 중요한 문제를 해결하는 구현을 만들어내는 데 있다. 팀에서는 프레임워크를 이용해 그러한 결과를 만들어 내는 방법을 찾아야 하는데, 그렇다고 해서 프레임워크에서 제공하는 모든 기능을 사용해야 한다는 의미는 아니다. 예를 들면, 초창기의 J2EE 애플리케이션에서는 이따금 도메인 객체를 모두 “엔티티 빈”으로 구현하곤 했다. 이러한 접근법은 성능과 개발 속도 면에서 모두 좋지 않은 결과를 초래했다. 반면 오늘날 우수 실천법은 대부분의 업무 로직을 일반 자바 객체로 구현하면서 구성 단위가 큰(larger grain) 객체에 대해서는 J2EE 프레임워크를 사용하는 것이다. 한 프레임워크를 이용해 해결하기 힘든 갖가지 측면은 어려운 문제를 해결하고자 어디서든 통하는 일률적인 해법을 모색하는 것이 아니라 여러 프레임워크를 선택적으로 적용해서 극복할 수 있다. 프레임워크의 가장 유용한 기능만 분별력 있게 적용한다면 구현과 프레임워크 간의 결합이 줄어들어 차후 설계 의사결정을 더욱 유연하게 내릴 수 있을 것이다. 그리고 더 중요한 점은 현재 널리 사용되고 있는 여러 프레임워크가 사용하기에 얼마나 복잡한지 감안하면 이러한 최소주의적인 태도가 비즈니스 객체를 읽기 쉽고 표현력 있게 유지하는 데 이바지한다는 것이다.

아키텍처 프레임워크를 비롯한 여러 도구는 계속해서 발전을 거듭할 것이다. 새로 나오는 프레임워크는 애플리케이션의 기술적인 측면을 점점 더 자동화하거나 미리 만들어 줄 것이다. 제대로만 된다면 애플리케이션 개발자들은 핵심적인 업무 관련 문제만 모델링하는 데 점점 더 많은 시간을 보내게 되고 생산성과 품질이 훨씬 더 향상될 것이다. 그러나 이러한 방향으로 나아가더라도 우리는 기술적인 해결책에 대한 열정만큼은 반드시 사수해야 한다. 정교한 프레임워크는 애플리케이션 개발자들을 속박할 수도 있다.