박종훈 기술블로그

프레젠터와 험블 객체

클린 아키텍처 - 로버트 C. 마틴 23장 프레젠터와 험블 객체


프레젠터(Presenter)는 험블 객체(Humble Object) 패턴을 따른 형태로, 아키텍처 경계를 식별하고 보호하는 데 도움이 된다.

험블 객체 패턴

험블 객체 패턴은 테스트하기 어려운 행위와 테스트하기 쉬운 행위를 단위 테스트 작성자가 분리하기 쉽게 하는 방법으로 고안된 디자인 패턴이다.

아이디어는 매우 단순하다. 행위들을 두 개의 모듈 또는 클래스로 나눈다. 이들 모듈 중 하나가 험블(humble)이다.

가장 기본적인 본질은 남기고, 테스트하기 어려운 행위를 모두 험블 객체로 옮긴다. 나머지 모듈에는 험블 객체에 속하지 않은, 테스트하기 쉬운 행위를 모두 옮긴다.

예를 들어 GUI의 경우 단위 테스트가 어려운데, 화면을 보면서 각 요소가 필요한 위치에 적절히 표시되었는지 검사하는 테스트는 작성하기 매우 어렵기 때문이다.

하지만 GUI에서 수행하는 행위의 대다수는 쉽게 테스트할 수 있다.

험블 객체 패턴을 사용하면 두 부류의 행위를 분리하여 프레젠터와 뷰 라는 서로 다른 클래스로 만들 수 있다.

프레젠터와 뷰

뷰는 험블 객체이고 테스트하기 어렵다. 이 객체에 포함된 코드는 가능한 한 간단하게 유지한다. 뷰는 데이터를 GUI로 이동시키지만, 데이터를 직접 처리하지는 않는다.

프레젠터는 테스트하기 쉬운 객체다. 프레젠터의 역할은 애플리케이션으로부터 데이터를 받아 화면에 표현할 수 있는 포맷으로 만드는 것이다. 이를 통해 뷰는 데이터를 화면으로 전달하는 간단한 일만 처리하도록 만든다.

예를들어 애플리케이션에서 어떤 필드에 날짜를 표시하고자 한다면, 애플리케이션은 프레젠터에 Date 객체를 전달한다. 그러면 프레젠터는 해당 데이터를 적절한 포맷의 문자열로 만들고, 이 문자열을 뷰 모델(view model)이라고 부르는 간단한 데이터 구조에 담는다. 그러면 뷰는 뷰 모델에서 이 데이터를 찾는다.

만약 애플리케이션에서 화면에 금액을 표시하고자 한다면, 애플리케ㅣ션은 프레젠터에 Currency 객체를 전달한다. 프레젠터는 해당 객체를 소수점과 통화 표시가 된 포맷으로 적절하게 변환하여 문자열을 생성한 후 뷰 모델에 저장한다. 만약 금액이 음수일 때 빨간색으로 변해야 한다면, 간단히 불 타입 플래그를 뷰 모델에 두고 적절한 값으로 설정한다.

화면에 보이는 버튼은 모두 이름이 있을 것이다. 그 이름은 뷰 모델 내부에서 문자열로 존재하며, 프레젠터에 의해 뷰 모델에 위치하게 된다. 특정 버튼을 비활성화해야 한다면, 브레젠터는 뷰 모델에 적절한 불 타입 플래그를 설정한다. 메뉴 아이템 이름은 모두 뷰 모델에서 문자열로 존재하며, 그 값은 프레젠터가 로드한다. 모든 라디오 버튼, 체크 박스, 텍스트 필드의 이름 또한 프레젠터가 적절한 문자열과 불 타입 플래그로 뷰 모델에 설정한다. 수치를 담은 테이블을 화면에 표시해야 한다면, 프레젠터는 적절한 형식의 문자열이 테이블 형태를 가지도록 뷰 모델에 로드한다.

화면에 표시되고 애플리케이션에서 어느 정도 제어할 수 있는 요소라면 무조건 뷰 모델 내부에 문자열, 불, 또는 열거형 형태로 표현한다. 뷰는 뷰 모델의 데이터를 화면으로 로드할 뿐이며, 이 외에 뷰가 맡은 역할은 전혀 없다. 따라서 뷰는 보잘것 없다.(humble)

테스트와 아키텍처

테스트 용이성은 좋은 아키텍처가 지녀야 할 속성으로 오랫동안 알려져 왔다. 험블 객체 패턴이 좋은 예인데, 행위를 테스트하기 쉬운 부분과 테스트하기 어려운 부분으로 분리하면 아키텍처 경계가 정의되기 때문이다. 프레젠터와 뷰 사이의 경계는 이러한 경계 중 하나이며, 이 밖에도 수많은 경계가 존재 한다.

데이터베이스 게이트웨이

유스케이스 인터랙터와 데이터베이스 사이에는 데이터베이스 게이트웨이(Database Gateway)가 위치한다. 이 게이트웨이는 다형적 인터페이스로, 애플리케이션이 데이터베이스에 수행하는 생성, 조회, 갱신, 삭제 작업과 관련도니 모든 메서드를 포함한다.

예를 들어 애플리케이션에서 어제 로그인한 모든 사용자의 성(last name)을 알아야 한다면, UserGateway 인터페이스는 getLastNamesOfUsersWhoLoggedInAfter라는 메서드를 제공할 것이고, 이 메서드는 날짜를 인자로 받아서 사용자 성들을 담은 목록을 반환할 것이다.

다시 한번 말하지만 유스케이스 계층은 SQL을 허용하지 않는다. 따라서 유스케이스 계층은 필요한 메서드를 제공하는 게이트웨이 인터페이스를 호출한다. 그리고 인터페이스의 구현체는 데이터베이스 계층에 위치한다. 이 구현체는 험블 객체다. 구현체에서 직접 SQL을 사용하거나 데이터베이스에 대한 임의의 인터페이스를 통해 게이트웨이의 메서드에서 필요한 데이터에 접귾나다.

이와 달리 인터랙터는 애플리케이션에 특화된 업무 규칙을 캡슐화하기 때문에 험블 객체가 아니다. 따라서 테스트하기 쉬운데, 게이트웨이는 스텁(stub)이나 테스트 더블(test-double)로 적당히 교체할 수 있기 때문이다.

데이터 매퍼

다시 주제를 데이터베잉스로 돌려보자. 하이버네이트 같은 ORM은 어느 계층에 속한다고 보는가?

먼저 분명히 해야할 점이 있다. 객체 관계 매퍼(Object Relational Mapper, ORM) 같은 건 사실 존재하지 않는다. 이유는 간단하다. 객체는 데이터 구조가 아니기 때문이다. 최소한 객체를 사용하는 사람 관점에서 객체는 데이터 구조가 아니다. 데이터는 모두 private으로 선언되므로 객체의 사용자는 데이터를 볼 수 없다. 사용자는 객체에서 public 메서드만 볼 수 있다. 따라서 사용자 관점에서 볼 때 객체는 단순히 오퍼레이션의 집합니다.

객체와 달리 데이터 구조는 함축된 행위를 가지지 않는 public 데이터 변수의 집합이다. ORM보다는 차라리 ‘데이터 매퍼(Data Mapper)’ 라고 부르는 편이 나아 보이는데, 관계형 데이터베이스 테이블로부터 가져온 데이터를 데이터 구조에 맞게 담아주기 때문이다.

이러한 ORM 시스템은 어디에 위치해야 하는가? 물론 데이터베이스 계층이다. 실제로 ORM은 게이트웨이 인터페이스와 데이터베이스 사이에서 일종의 또 다른 험블 객체 경계를 형성한다.

서비스 리스너

서비스는 어떨까? 애플리케이션이 다른 서비스와 반드시 통신해야 한다면, 또는 애플리케이션에서 일련의 서비스를 제공해야 한다면, 우리는 여기에서 서비스 경계를 생성하는 험블 객체 패턴을 발견할 수 있다.

애플리케이션은 데이터를 간단한 데이터 구조 형태로 로드한 후, 이 데이터 구조를 경계를 가로질러서 특정 모듈로 전달한다. 그러면 해당 모듈은 데이터를 적절한 포맷으로 만들어서 외부 서비스로 전송한다. 반대로 외부로부터 데이터를 수신하는 서비스의 경우, 서비스 리스너가 서비스 인터페이스로부터 데이터를 수신하고, 데이터를 애플리케이션에서 사용할 수 있게 간단한 데이터 구조로 포맷을 변경한다. 그런 후 이 데이터 구조는 서비스 경계를 가로질러서 내부로 전달된다.

결론

각 아키텍처 경계마다 결계 가까이 숨어 있는 험블 객체 패턴을 발견할 수 있을 것이다. 경계를 넘나드는 통신은 거의 모두 간단한 데이터 구조를 수반할 때가 많고, 대개 그 경계는 테스트하기 어려운 무언가와 테스트하기 쉬운 무언가로 분리될 것이다. 그리고 이러한 아키텍처 경계에서 험블 객체 패턴을 사용하면 전체 시스템의 테스트 용이성을 크게 높일 수 있다.