이 글은 http://www.kamilgrzybek.com/design/grasp-explained/ 링크에 있는 글을 Swift 언어를 기준으로 재해석한 글입니다.
GRASP은 9가지 General Responsibility Assignment Software Patterns 집합다. 객체 책임을 할당하는 것은 OOD(객체 지향 설계) 핵심 설계 방법 중에 하나다. 개발자와 설계자라면 이런 패턴에 익숙해져야 한다.
1. 정보 담당자 Information Expert 2. 소유권한 Creator 3. 컨트롤러 Controller 4. 낮은 연결 Low Coupling 5. 높은 응집도 High Cohesion 6. 간접 참조 Indirection 7. 다형성 Polymorphism 8. 순수 조립 Pure Fabrication 9. 변화 보호 Protected Variations
1) 정보 담당자 (Information Expert)
문제정의: 객체에 책임을 할당하는 기본적인 원칙은 무엇인가? 해결방안: 해당 객체 필요한 정보를 채워넣는 것을 우선적으로 책임으로 할당한다.
다음 Customer 클래스는 모든 주문 Orders를 참조하고, 주문에 대한 총합을 계산하기 위한 책임을 갖는다.
이렇게 값과 관련된 역할이 가장 기본적인 규칙이다. 반대로 꼭 필요하지 않은 데이터라면 책임을 할당하지 않는게 좋다.
2) 소유권한 (Creator)
문제정의: 누가 객체 A를 생성하고 소유하는가? 해결방안: 다음과 사항을 고려해서 적어도 하나 이상이라면 A 객체를 생성하는 책임을 객체 B에 할당한다. * B가 A를 포함하거나 협력을 위해 조합하는 경우 * B가 A를 기록하는 경우 * B가 A를 긴밀하게(복잡하게) 사용하는 경우 * B가 A의 초기값을 갖고 있는 경우
Customer 클래스가 orders 속성을 협력을 위해 사용하고, order를 기록하고, 초기값을 넘겨주고 있다. 따라서 Order 생성하고 소유하기에 적합한 후보다.
3) 컨트롤러 (Controller)
문제정의: UI 계층 뒤에서 시스템 제어를 받아주고 관장하는 객체는 무엇인가? 해결방안: 다음과 사항을 고려해서 적어도 하나 이상이라면 컨트롤러 역할을 할당한다. * “시스템” 전체, “최상위 객체”, 디바이스에서 동작하는 소프트웨어나 가장 중심의 서브시스템을 표현하는 경우 * 시스템 동작을 구성하는 사용자 시나리오(use case)를 표현하는 경우
이 구현 규칙은 시스템에 대한 상위 수준 설계에 따라 달라진다. 늘 비즈니스 로직 처리를 위한 전반적인 조정을 담당하는 객체를 정의해야 한다. 다음 예제 코드에서는 전형적인 컨트롤러 역할을 담당하고 있다. 다른 객체에서 명령을 전달받아서 내부에 있는 manager 객체에 전달하는 역할을 책임진다.
4) 느슨한 연결 (Low Coupling)
문제의식: 어떻게 변화에 대한 충격을 줄일 수 있나? 어떻게 의존성을 줄이고 재사용성을 높일 수 있나? 해결방안: (불필요한) 연결을 줄이도록 책임을 할당한다. 이 방법과 다른 방법을 함께 고려하라.
연결(Coupling)은 하나의 요소가 다른 요소와 얼마나 관련이 있는지를 나타내는 지표다. 팽팽한 연결(Higher coupling)은 하나의 요소가 다른 요소가 의존성이 높아진다.
느슨한 연결을 객체들끼리 서로 독립적이면서 분리되어 있는 것을 의미한다. 분리되어 있는 객체들은 다른 객체가 변하더라도 걱정할 필요가 없고, 무언가 깨지지 않는 것을 의미한다. 객체 설계 SOLID 5원칙은 연결을 낮추는 데 도움이 되는 원칙이다. SOLID에서 강조하는 추상화한 인터페이스로 역할을 분리해서 느슨하게 연결한 예제 코드다.
5) 높은 응집도 (High Cohesion)
문제정의: 객체 자체에 집중해서 객체를 이해하기 쉽고, 관리하기 편하고, 느슨한 연결을 추구하려면 어떻게 해야할까? 해결방안: 응집도가 높아지도록 책임을 할당하자. 이 방법과 다른 방법을 함께 고려하라.
*응집도*는 객체가 모든 책임이 얼마나 관련이 높은지 나타내는 지표다. 다시 말해서 내부 요소에 있는 부분들이 서로 얼마나 관련이 높은지를 의미한다.
낮은 응집도를 가진 클래스는 관련이 없는 데이터나, 관련이 없는 행동을 포함하게 된다. 예를 들어서 Customer 클래스는 “Orders를 관리한다”는 한 가지에 집중하기 때문에 응집도가 높다. 만약 여기에 제품의 가격을 관리하는 책임을 추가한다면, 직접 관련이 없는 가격에 대한 것 때문에 응집도는 떨어진다.
6) 간접 참조 (Indirection)
> 문제정의: 두 객체 이상에서 직접적으로 연결을 피하도록 어디에 책임을 할당해야 할까? > 해결방안: 두 개의 서비스나 컴포넌트를 직접 연결하지 말고, 중간 매개체에게 책임을 할당하라.
중재자 패턴(Mediator Pattern)을 찾기 위해 직접적인 연결하는 예제 코드이다. 여기서는 Controller와 Service 가 직접 참조가 일어난다.
이제 중재자 역할을 하는 Manager를 활용하면 다음과 같다. Controller — Manager — Service 형태로 간접적으로 참조한다.
한 가지 고려할 사항은 간접 참조를 사용하면 느슨한 연결이 되지만, 시스템의 가독성과 분별력을 떨어트린다. 컨트롤러 코드 상에서는 command 처리를 어느 객체가 담당하는 지 알 수 없다. 트레이드 오프가 있다는 것을 늘 고려해야 한다.
7) 다형성 (Polymorphism)
문제정의: 타입을 (다른 다입으로) 대체할 방법은 어떻게 가능한가? 해결방안: 타입(또는 클래스)에 따라 다른 동작이나 대체 수단은, 각 타입에서 해당 동작이 다형성을 갖도록 책임을 할당하라.
다형성은 객체지향 설계에 기본 원칙이고, 전략 패턴(Strategy Pattern)과 매우 연관이 깊다.
앞서 살펴봤던 Customer 클래스는 초기화할 때 CustomerUniquenessCheckerDelegate 프로토콜을 전달받는다.
이렇게 프로토콜로 요구사항을 추상화해서 다른 구현체를 전략적으로 선택할 수 있다. 이런 구조를 가지면 동일한 입력과 출력에도 전혀 다른 알고리듬으로 동작하도록 만들 때 유용하다.
8) 순수 조립 (Pure Fabrication)
문제정의: 높은 응집도와 낮은 연결을 깨고 싶지 않은데, 다른 원칙들을 적용하기 애매한 상황에서는 어떤 객체가 책임을 가져야 할까? 해결방안: 문제에 대한 도메인을 표현하지 않는 편의상 또는 인공적인 객체가 높은 응집도를 갖도록 책임을 할당하라.
가끔씩은 어디에 어떤 책임을 할당해야만 할지 알아내기 어려운 경우도 있다. 도메인-주도 설계 (Domain-Driven Design)에 도메인 서비스(Domain Service) 개념이 있는 이유다. 도메인 서비스는 엔티티(Entity)와 관련이 없는 로직을 포함한다.
예를 들어 이커머스 시스템에서는 환율에 따라서 외환을 환전하는 기능이 필요하다. 이런 구현을 위해서 어디서 새 클래스나 프로토콜을 만들어야 하는지 혼란스러울 때가 있다.
이렇게 구현하면 환전 처리를 위한 높은 응집도와 ForiegnExchangeProtocol 프로토콜을 채택하도록 해서 느슨한 연결을 유지할 수 있다. 이런 클래스는 유지보수도 편리하고, 재사용성도 높고, 테스트하기에도 좋은 구조를 가진다.
9) 변화에 대한 보호 (Protected Variations)
문제의식: 불안정적이고 변화에 따라서 다른 요소에 영향을 덜 주도록, 객체나 시스템을 설계하는 방법은 무엇인가? 해결방안: 불안정적인 요소나 변화할 요소를 예측하고 분류해서, 안정적인 인터페이스를 갖도록 책임을 할당하라.
소프트웨어에서 가장 중요한 점은 쉽게 변화를 주는 것이다. 아키텍트나 개발자 모두 요구사항이 바뀔 것에 대해 대비가 되어 있어야 한다. 이것은 선택사항이 아니라 항상 유지해야 하는 품위처럼 꼭 갖고 있어야 하는 의무라고 생각한다.
수많은 디자인 가이드라인, 원칙, 패턴과 변화를 위해서 다양한 수준의 추상화를 연습해야 한다. 아래 소개하는 내용은 그 중에 일부일 뿐이다.
- SOLID 원칙 (그 중에서도 Open-Close 원칙) - GoF (Gang of Four) 디자인 패턴 - 캡슐화 Encapsulation - 디미터 법칙 Law of Demeter - 서비스 디스커버리 Service Discovery - 가상화와 컨테이너화 Virtualization and Containerization - 비동기 메시징 asynchronous messaging, 이벤트-주도 아키텍처 Event-driven architectures - 오케스트레이션 Orchestration, 코레오그래피 Choreography
요약
이 글은 객체지향 설계와 관련된 패턴과 원칙 중에서 가장 기본적인 GRASP 원칙에 대해 설명한 것이다.
소프트웨어에서 책임을 능숙하게 다루는 것의 핵심은 품질 높은 아키텍처와 코드를 구성하는 것이다. 변화에 유연하게 대응하는 잘 만들어진 시스템을 만들 수 있으려면 여러 패턴과 원칙을 조율해야 한다. 늘 그것을 준비해야 한다.