이제 책임을 분리함으로써 소프트웨어의 구조를 잘 만들 수 있게 되었으니 책임을 중심으로 패키지를 만드는 방식을 이야기 할 차례다.
<단일 패키지 내 클래스들 : 지금까지 만든 클래스들을 각각의 소스 파일로 분리하여 하나의 패키지에 넣으면 이와 같은 모양이 될 것이다.>
패키지(package)는 소프트웨어를 배포하는 물리적 단위다. 만약 잘 만들어진 소프트웨어를 재사용해야 하는 상황이라면 패키지 단위로 재사용하는 것이 가장 현명한 선택이다. 그리고 패키지 단위로 배포를 하기 위해서는 패키지 구조를 잘 만들어 두어야 한다. 다행히 지금까지 만들어 왔던 책임 중심의 구조는 패키지로 구조화 시키기에도 매우 적합하다. 잘 패키징 된 소프트웨어는 소스를 분석하기에도 편리하다. Java의 패키지 구조는 디렉토리 구조와 매치된다. 따라서 패키징을 잘 하면 디렉토리 별로 잘 분류된 문서들처럼 손쉽게 필요한 소스들을 찾아 볼 수가 있다. 여기서는 지금까지 만든 클래스들을 어떤 형태로 패키징 하는 것이 좋으며, 특히 구현 책임을 외부로 노출시키지 않기 위해서 어떤 형태로 소스 파일들을 분리하고 가시성을 설정하는 것이 좋은지 이야기 해보도록 하겠다.
가시성 조정
가장 간단한 가시성 부분을 먼저 해결해보도록 하자. 우리는 지금까지 여러 책임을 분리하면서 그 기저에 단 한가지 목표를 두었다. 그것은 Client와 구현 객체의 분리이다. 이를 위해서 계약 책임을 도입하고, 생성 책임을 분리하고, 조립 책임을 분리했다. 결국 Client 클래스 눈에 구현 객체들이 보이면 안되는 것이다. 이것을 아예 설계 단계에서 방지할 수 있는 방법은 구현 객체들의 가시성을 낮추는 것이다. 만약 구현 객체들을 패키지 가시성 수준으로만 낮춘다고 해도 다른 패키지에서는 구현 객체들이 존재하는지조차 알 수 없게 된다. Client 클래스와 구현 클래스들을 서로 다른 패키지에 넣고, 구현 클래스들의 가시성을 패키지 수준(즉 class 선언부에 아무런 가시성 키워드가 없는 형태)으로 떨어뜨려 놓기만 해도 실수로라도 구현 객체에 접근해서 의존성이 발생하는 것을 미리 막을 수 있다.
그 다음엔 Client와 Client와 구현 객체를 별도의 패키지로 분리해 둔다. 이 시기에는 다른 기준이 없기 때문에 구현객체와 Client만 떨어뜨려 놓으면 된다. 일단 Client는 사용자의 입장이고, 다른 모든 클래스들은 사용자가 사용할 대상에 해당된다. 따라서 Client 클래스만 별도의 패키지로 설정하도록 한다. 이제 남은 부분은 모두 MasterContract 클래스와 그 조립을 위한 클래스들이다. Java에서는 기본적으로 서브 패키지 개념이 없다. 사실 모든 패키지는 독립적이다. 하지만 아무래도 이것은 불편하다. 왜냐면 상위 패키지와 그 하위 패키지로 구분해 두면 어떤 패키지가 다른 패키지에 종속적인지, 반대로 이야기 하자면 어떤 패키지가 다른 어떤 패키지에 의존적인지를 알기가 수월할 수 있기 때문이다. 우리의 예제에서는 일단 오직 Client만 MasterContract 클래스를 사용하도록 되어 있기 때문에 패키지의 구조를 계층적으로 구성하도록 한다. 만일 패키지 간에 1:1의 의존 관계가 존재하지 않는다면 하위 패키지 형태로 분류하지 않는 편이 좋다. 아래는 client 패키지와 그 하위 패키지, 즉 master 패키지를 구분한 예이다.
<client 패키지와 master 패키지의 분리. master 패키지는 client 패키지에 의해서만 사용되고 있으므로 하위 패키지 형태로 구성했다.>
패키지 계층 구성
master 패키지 내부에 있는 클래스들은 아직도 정리가 안 된 느낌이다. Assembler나 MasterContract 클래스와 같이 좀 더 종합적인 구조를 가지고 있는 것과 Contract1, 2와 같이 하위 의존성을 위한 클래스나 interface들이 뒤섞여 있다. 이런 형태로 패키지가 구성되어 있으면 일단 소프트웨어의 구조를 분석하는데 불편하다. 어떤 클래스가 다른 어떤 클래스와 어떻게 의존하고 있는지를 알기가 어렵기 때문이다. 또 이런 형태는 배포의 기준점을 잡기가 힘들다. 예를 들어 Contract1에 해당하는 클래스들만 외부에 배포하고 싶다고 할 때 일일이 그와 연관된 클래스 파일들을 골라서 배포해야만 한다. 따라서 master 패키지 내의 소스 파일들도 하위 패키지들을 만들어 분리해 주는 편이 좋다.
master 패키지를 구분한다고 할 때 그 단위가 될 수 있는 기준을 먼저 살펴보도록 하자.
1. MasterContract를 구성하는데 필요한 클래스들.
2. 단일 계약 책임(Contract1, 2)과 그 구현 클래스들.
1번의 MasterContract를 구성하는데 필요한 클래스들은 MasterContract와 그 조립 책임을 담당하는 Assembler 클래스이다. 이들은 여전히 master 패키지에 존재할 필요가 있다. 2번에 해당하는 각각의 계약 책임 클래스들은 서로 분리할 필요가 있다. 첫째 이유는 master 패키지에 있을 클래스들과는 다른 패키지에 놓여져야 하기 때문이고, 두번째 이유는 Contract1과 Contract2는 서로간에 의존성이 없고, 따라서 서로를 모르는 편이 좋기 때문이다. 그리고 세번째로는 소스를 분석하는 입장에서 Contract1과 Contract2가 다른 패키지로 구분되어 있을 때 자연스럽게 체계를 파악할 수 있기 때문이다. 그렇다면 결국 패키지의 구분은 master 패키지의 하위 패키지로 contract1과 contract2를 위한 패키지를 도입하는 형태가 될 것이다. 아래는 이와 같은 형태로 분류된 패키지 구성이다.
<master 하위에 contract1과 contract2를 분류한 패키지 구성>
우선 contract1과 contract2에 대한 패키지를 master 패키지의 하위 패키지 형태로 구성한 이유는 master를 client 패키지의 하위로 분류한 이유와 같다. 만약 contract1이나 contract2가 외부의 다른 패키지와의 의존관계가 있다면 이렇게 종속적인 형태로 구성해서는 안될 것이다. contract1과 2를 구분하는 것은 이슈가 없을 정도로 합리적인 선택이다. 약간의 이슈가 될 수 있는 부분은 Contract1Factory나 Contract2Factory를 함께 패키지에 넣은 것이다. 이들을 master 패키지에 두는 것도 고려해 볼 수 있다. 하지만 패키지 구성을 조정해야 하는 상황이라면 생성 책임은 역시 계약 책임과 함께 존재하는 것이 나은 선택일 수 있다.
위의 패키지에서 주목할 부분은 Task 클래스와 Job 클래스이다. 이들은 각각 Contract1과 Contract2에 대한 구현 클래스이다. 앞서서 이야기 한 것처럼 Client 클래스가 이들 구현 객체는 몰라야 하는 상황이다. 따라서 Task 클래스와 Job 클래스의 가시성을 패키지 가시성 수준으로 떨어뜨려 두었다.
배포과정에서의 이슈
위의 구성에서 보면 client 패키지가 가진 의존성은 모두 하위 패키지 형태로 구성되어 있기 때문에 client 패키지를 배포하는데 전혀 문제가 없다. 또한 master 패키지를 배포한다고 해도 조립책임 및 하위 계약 책임, 그리고 그 구현 책임들을 포함한 패키지 계층이 배포될 수 있는 구조이므로 문제가 될 것이 없다. 한가지 생각해 볼 부분은 각 계약 책임의 구체 구현 객체들에 대한 처리이다.
첫번째 이슈는 배포 시 Task와 같은 구현 객체가 필요하지 않고, 따라서 Contract1Factory와 같은 생성 책임도 필요가 없는 경우이다. 즉 단지 계약 책임만을 가지고 가고 구현 객체는 필요 없을 경우에는 어떻게 하는 것이 좋을까? 잘 생각해 보면 Contract1Factory는 Task에 의존적이다. Contract1은 계약 책임만 가지고 있으므로 Task 클래스와 같은 구현 책임이나 Contract1Factory와 같은 생성 책임에 의존하지 않는다. 따라서 Contract1은 contract1 패키지 내에서 독립적이다. 만약 구현 객체가 필요 없는 경우를 대비한다면 생성 책임을 가진 Contract1Factory와 구현 책임을 가진 Task 클래스를 묶어 contract1의 하위 패키지로 만들어 구성할 수 있다.
<구현 책임과 생성책임을 별도의 하위 패키지로 구성한 경우>
위의 구성에서 만일 Contract1의 계약 책임만을 배포하겠다고 하면 하위 패키지를 제외하고 배포시키면 된다.
배포 과정에서의 또 한가지 이슈는 위의 구성에서 impl 패키지 하위의 구현 책임들을 모두 함께 배포하되, 배포를 받은 입장에서 새로운 구현 객체를 등록, 사용하려고 할 수 있다는 점이다. 이 경우는 Contract1Factory의 생성 책임이 추가되어야 한다는 의미이다. 이 때 만일 Contract1Factory가 컴파일 된 형태로 배포된다면 간단하게 Contract1Factory를 상속 받아서 추가된 구현 객체에 대한 생성 메소드를 추가하는 형태로 구현하면 된다. 대신 새롭게 구현된 구현 클래스나 생성 책임을 가진 클래스는 위의 패키지를 배포 받은 쪽 소스 상에 추가되어야 할 것이다.
단위 테스트(Unit Test) 이슈
위의 패키지 구성에서 또 한가지 고려해야 하는 부분은 단위 테스트이다. 우선 책임을 종류별로 분리하고 이를 각각 클래스화 시켰을 때 단위 테스트는 그렇지 않은 형태보다 훨씬 수월하다. 우선 조립 책임에 의해 의존 관계를 파악하기가 매우 쉽고, 필요하다면 계약 책임을 구현한 mock 객체나 spy 객체 등을 생성하여 재조립(reassemble)하기도 매우 편리하다. 또 각각의 책임이 명확하기 때문에 테스트 작성 계획을 세우거나 테스트 클래스들을 합리적으로 계층화 시키기에도 좋다.
단 한가지 이슈가 될 만한 부분은 구체적인 구현 객체의 가시성으로 인한 테스트 가능성이다. 구현 클래스들의 가시성을 패키지 가시성 수준으로 떨어뜨려 두었기 때문에, 테스트 클래스가 구현 클래스들과 같은 패키지에 있지 않으면 클래스가 존재하는지조자 알 수 없다. 이에 대한 해법은 이미 Spring 프레임워크 등에서 사용하고 있는 폴더 분리 방식을 통해 해결 할 수 있다. 이에 대해서는 아래 글을 참고하기 바란다.
package private class에 대한 unit test
'3.객체지향(OOP) 개념' 카테고리의 다른 글
객체지향의 올바른 이해 : 10. 조립 책임 (3) | 2016.12.03 |
---|---|
객체지향의 올바른 이해 : 9. 생성 책임 (2) | 2016.12.03 |
객체지향의 올바른 이해 : 8. 구현 책임과 계약 책임 (3) | 2016.11.29 |
객체지향의 올바른 이해 : 7. 의존(Dependency)과 책임(Responsibility) (0) | 2016.11.26 |
객체지향의 올바른 이해 : 6. 객체지향 개념 관계도 (0) | 2016.11.06 |