[객체지향 패러다임] OOD와 GRASP 패턴, 웹 개발에서의 객체지향 적용
이 게시글을 검색해서 탐색했다는 것은, 객체지향 패러다임 중에서도 OOD와, 그 중간에 사용되는 GRASP 패턴의 목적에 대해 찾아보고자 했다고 가정한다.
실제로 코드를 짜면서 쉽게 알게되는 부분을 제외하고, OOD(Object Oriented Design) 관점에서의 객체지향에 대해 설명하도록 하겠다.
다소 추상적이고 이론적으로 들리지만, 그만큼 의식적이고 체계적으로 적용하게 되면 일관성 있는 코드가 나온다.
웹 구현에서 객체지향적인 코드는 어떻게 나오는지에 대한 고찰을 담았다.
- 권장하는 이전 글 - SOLID 원칙
목차
- OOP(Object-Orientied Paradigm)이란?
- OOP에서 이야기하는 "책임"이 무엇인가?
- 분석 - OOA
- 설계 - OOD
- 설계 - Interaction Diagram
- 적절한 책임의 할당 - GRASP 패턴
- 설계 - Class Diagram
OOP(Object-Orientied Paradigm)
객체지향(OO)에서 이야기하는 프로젝트의 구현 과정은 다음과 같다.
- 분석(OOA, Object-Oriented Analysis)
- 설계(OOD, Object-Oriented Design)
- 구현(OOP, Object-Oriented Programming)
위와 같은 프로세스를 통해, 개발자는 산출물을 만들게 된다.
Object-Oriented Paradigm과 Object-Oriented Programming을 혼동하지 말자.
본 글에서는 Object-Oriented Paradigm 을 중점적으로 다룬다.
OOP에서 이야기하는 객체지향이란 무엇일까?
객체지향이란, 객체 간의 책임을 분리하고, "연관관계"를 이용한 상호작용으로 코드를 작성하는 개발 방법론을 의미한다.
이는 전역 상태를 분리하여 유지보수성을 향상시키고, "정보 전문가"에게 기능의 실행을 맡긴다는 것을 의미한다.
그렇다면, 객체지향에서 자주 거론되는 “책임”은 무엇을 의미하는걸까?
책임 - Object-Oriented Paradigm
책임에는 다음 두가지 종류가 있다.
- Doing - 뭘 하는가?
- Knowing - 뭘 아는가?
Doing - 뭘 하는가?
실제로 해당 객체가 “어떠한 행위를 하는가”에 대한 책임을 의미한다.
다음과 같은 종류가 있으며, 아래로 내려갈수록 “강한 책임”을 갖게 된다.
- 스스로 무언가를 하는 것
- 다른 객체의 초기화를 수행하는 것
- 다른 객체의 행위를 제어하는 것
Knowing - 뭘 아는가?
실제로 해당 객체가 “어디까지 아는가”에 대한 책임을 의미한다.
다음과 같은 종류가 있으며, 아래로 내려갈수록 “강한 책임”을 갖게 된다.
- 다른 객체가 유도하거나 계산하는 값을 아는 것
- 다른 객체의 연관된 객체를 아는 것
- 다른 객체의 private인, 캡슐화된 데이터를 아는 것
OOP는 객체 간의 "상호작용"을 권장하며, 그렇기 때문에 강한 책임으로 인해 발생하는 신 객체(God Object)을 지양한다.
이 책임의 할당을 평가하는 기준이 GRASP 패턴이다.
지금부터 GRASP 패턴에 대해 천천히 알아가보도록 하자.
분석 - OOA
OOD에 대해 이해하기 전에, OOA에 대해 가볍게 알아보자.
OOP에서 진행하는 “분석”이란, 주어진 요구사항에서, 어떠한 "행위"들이 존재해야 하는지를 뽑아내는 과정을 의미한다.
이 과정에서, Use Case, Domain Model 등을 이용해 각 기능들을 뽑아내고 정의하고, 각 도메인에 존재해야 하는 "책임"을 정의한다.
이 글은 OOA에 대해 다루는 게시글이 아니기 때문에,
따라서 OOA를 어떻게 하는건지만 작성하고, 왜 이러한 순서대로 적용하는지는 작성하지 않는다.
직접 이대로 프로젝트를 진행해보면, 유용함을 알 수 있다.
OOA는 크게 다음과 같은 과정을 통해 수행된다.
이 내용도 꽤나 깊은 주제로 탐구할 수 있다. 따라서 차후 OOA 관련 게시글로 따로 작성할 예정이다.
추후 작성할 OOA 관련 게시글의 목차를 남겨둔다.
- Use Case 작성
- Actor 정의
- 시스템 바운더리 설정
- Use Case 크기 결정
- 성공 시나리오/실패 시나리오 작성
- 도메인 모델 설계
- 도메인 엔티티 설계 - 명사 추출법, 범주 추출법
- 연관관계 설계 - 동사 추출법, Need to know
- 도메인 모델 설계 시 주의할 점
- 계약에 의한 설계
- 사전조건
- 사후조건
- supertype, subtype과 covariance, invariance, contravariance - 계약이 지켜야 하는 조건
- 제네릭과의 연관성
이 목차는, 글이 작성되면 작성한 게시글 링크로 바뀔 예정이다.
계약 관련 내용은 OOA뿐만 아니라 OOD에서도 중요하게 다뤄지는데, 여기서는 필요한 만큼만 다루도록 하겠다.
계약에 의한 설계
계약에 의한 설계 - 사전조건
- 메소드가 수행되기 전 지켜야 하는 조건
- 호출자가 지켜야 하는 조건
- 개발자에겐 이익이 되는 조건이고, 클라이언트에겐 책임이 되는 조건
계약에 의한 설계 - 사후조건
- 메소드가 수행되고 난 후의 상태 변화
- 공급자가 지켜야 하는 조건
- 개발자에겐 책임이 되는 조건이고, 클라이언트에겐 이익이 되는 조건
계약은 어떻게 작성해야 하는가?
사전조건과 사후조건은, 기본적으로 도메인 모델에 정의된 용어로 작성이 이루어져야 한다.
이것이 계약에 의한 설계 이전에, 도메인 모델이 먼저 정의되어야 하는 이유이다.
이때, 사전조건은 시스템이 입력받는 데이터의 형태를 정의해야 한다.
이는 우리가 API를 설계할 때의 파라미터가 될 수 있다.
또한, 사후조건은 일어나야 하는 상태 변화를 정의해야 한다.
이때, 발생할 수 있는 사후조건의 종류로는 다음과 같은 것들이 있다.
- 객체의 생성/삭제
- 객체의 속성의 수정
- 연관관계의 생성/삭제
이렇게 작성된 사전조건과 사후조건은 100% 맞진 않겠지만, 생산성을 해치지 않는 선에서 최대한 노력해야 한다는 것만큼은 분명하다.
설계 - OOD
객체지향은 행위 우선이다.
- Interaction Diagram은 동적 다이어그램이다.
- Class Diagram은 정적 다이어그램이다.
각 다이어그램은 Interaction Diagram이 Class Diagram을 끌고 가는 형태로 작성해야 한다. 객체지향에선 이것을 "책임 주도 설계" 라고 부른다.
Interation Diagram
Interaction Diagram을 그릴 땐, OOA에서 수행하여 얻어낸 Operation을 각 도메인에 할당해야 한다. 이때, 도메인 객체 간의 "계약"을 설계해야 한다.
Class Diagram
Class Diagram을 그릴 땐, 도메인 모델과 Interaction Diagram 을 작성하며 얻어낸 정보를 통해, 유지보수가 용이한 정적 클래스 모델을 생성해야 한다.
정적 구조 및 정적 의존관계 표현에 집중하게 된다.
GoF 디자인 패턴때문에 더 유명한건 Class Diagram이지만, OO 관점에서 더욱 중요한건 Interaction Diagram이다.
Interaction Diagram
책임의 할당은 Interaction Diagram을 그릴 때 이루어지며, 어느 객체가 어느 다른 객체에 메시지를 보내는지 화살표의 시작점과 종착점을 결정할 때 결정하게 된다.
Sequence Diragram
시퀀스 다이어그램은 대표적인 “시간의 순서”를 강조하는 다이어그램으로, 위부터 차례대로 읽어나가면서 각 객체가 어떤 순서로 상호작용하는지를 파악할 수 있게 돕는 다이어그램이다.
다만, 상호작용하는 객체가 많아질 경우, 시작점과 종착점의 거리가 멀어져 객체 간의 관계의 이해를 위한 직관성이 떨어지는 단점이 있다.
따라서, 일반적으로 최종 보고서 형태에서 시각적인 자료로 표현 시 사용하게 된다.
Communication Diagram
커뮤니케이션 다이어그램은, 시간의 순서를 단순 번호로 표현하고, 객체의 위치를 자유롭게 표현함으로써 공간의 효율성을 챙기는 Interaction Diagram이다.
다만, 다음 상호작용 순서를 바로 파악하기 어렵고 직접 다음 번호를 찾아야 하기 때문에, 순서를 파악하기에는 직관성이 떨어지는 단점이 존재한다.
따라서, 일반적으로 초기 프로젝트에서 책임 할당을 위한 Brainstorming 시 사용하게 된다.
책임이 어떤 방식으로 부여되는지 알았으니, 이제 본격적으로 해당 책임의 부여를 평가하기 위해 GRASP 패턴에 대해 알아보자.
책임의 할당 - GRASP 패턴
GRASP 패턴은 5가지 핵심 패턴과 4가지 추가 패턴으로 이루어져 있다.
- 5가지 핵심 패턴
- Information Expert
- Creator
- Evaluation Pattern
- High Cohesion
- Low Coupling
- Controller
- 4가지 추가 패턴
- Protected Variance
- Polymorphism
- Pure Fabrication
- Indirection
Information Expert
GRASP 패턴의 핵심 패턴이자 가장 중요한 패턴이고, OOP를 관통하는 패턴으로, “항상 직접 하지 말고, 전문가 객체에게 위임하라” 라는 패턴이다.
"필요한 정보가 있으니 이 행위를 하자"가 아니라, "이 행위가 필요한데, 이를 수행하기에 가장 적합한(필요한 정보를 가진) 객체가 누구인가?"라는 관점으로 접근해야 한다는 뜻이다.
중요한 만큼, 직접 책임을 할당해보며 정보 전문가 패턴을 이해해보자.
예시
- 현재 판매 정보(Sale)이 각 판매한 상품 품목(SalesLineItem)을 갖고 있고, 각 SalesLineItem은 상품 정보(ProductDescription)을 통해 해당 상품 품목의 정보를 조회하게 된다.
- 다음과 같은 상황에서 getTotal() 메소드는 누구에게 주어야 할까?
풀이
- 일단 getSubTotal() 메소드가 필요하다.
- 이건 SaleLineItem 에게 주고,
- 그리고 getPrice() 메소드가 필요하다.
- 이건 ProductDescription 에게 주자.
- SalesLineItem 을 모두 아는 객체가 계산하는게 좋겠다는 판단이 가능하다.
- 따라서 getTotal() 메소드는 Sale 객체에게 주자.
Creator
Creator 패턴은 객체 생성의 책임을 할당하는 패턴으로, 다음과 같은 상황에서, B객체를 생성할 책임을 A클래스에 할당하는 것을 요구한다.
- A가 B를 포함하거나 참조
- A가 B를 기록
- A가 B를 밀접하게 사용
- A가 B의 초기화 데이터를 가짐
왜 위와 같은 기준으로 생성의 책임을 할당하는 것일까?
의존성!
이미 B가 A에 구조적 관계를 갖고 있으므로, 해당 객체를 직접 생성한다고, 추가적인 관계가 생성되지 않는다.
이를 통해, 의존성을 줄여 결합도를 줄일 수 있게 된다.
단, 생성 자체가 복잡한 비즈니스 로직을 포함하게 된다면, 팩토리와 같은 Pure Fabrication을 생성하여 객체 생성의 책임을 할당하는 것이 옳다.
Evaluation Pattern - 높은 응집도, 낮은 결합도
높은 응집도와 낮은 결합도는 “평가 패턴”이라는 이름으로 불리우며, 해당 도메인 엔티티가 다른 객체와 적절한 연관관계를 맺었는지, 적절한 책임을 지녔는지 평가하는 기준으로 작용하게 된다.
High Cohesion - 높은 응집도
High Cohesion 패턴은 클래스가 단일하고 잘 정의된 목적을 가지도록, 클래스의 책임이 밀접하게 연관되어 있고 집중되어 있어야 한다는 것을 의미한다.
이는 SRP와 매우 직접적인 연관성을 갖는다.
예시
위와 같이, Register가 Controller + Sale 에 Payment 등록하는 형태보다,
이와 같이, Register는 컨트롤러 역할에 집중하고, Sale 이 직접 Payment 생성해서 등록하도록 하는 것이 좋다.
High Cohesion 원칙은 “메소드 이름에 집중하지 말고, 정보와 책임에 집중하라.”는 가르침을 준다.
Low Coupling - 낮은 결합도
느슨한 결합, Indirection과는 다름에 유의하자.
Low Coupling 패턴은 클래스 간의 의존성의 갯수를 최소화하여 변경의 영향을 줄이는 패턴으로, 한 클래스의 변경이 다른 클래스에 미치는 영향을 최소화하는 것을 목적으로 갖는다.
왜?
의존성이 많으면, 코드의 이해가 어려워진다. 또한, 유지보수할 껀덕지도 많아진다.
- 이것보다 (Register 2개의 객체에 종속)
- 이게 낫다! (Register 1개의 객체에 종속)
많은 클래스가 특정 객체에 의존하는 상황은, 변경에 대한 두려움을 몹시 강화시킨다.
Controller
Controller는 Non-GUI Object를 두어, 시스템 이벤트를 "받을(Handle)" 책임을 따로 분리해야 한다는 패턴이다.
이는 자주 변경되는 연결부와 변경되지 않는 비즈니스 로직을 분리하여, 시스템 이벤트를 처리하는 첫 번째 객체로, UI 계층과 도메인 계층을 연결하는 역할을 수행하게 된다.
주로 사용자의 요청을 처리하고 적절한 도메인 객체에 작업을 위임하는 방식으로 구현한다.
이를 Model-View Separation이라고 한다.
예시
- 이렇게 바로 직접 처리하지 말고
- 다음과 같이
:Register
라는 중간다리를 만들어라.
Presentation Layer의 Domain Layer 변경 확인 방식이 바로 MVC 패턴이다.
Protected Variance
4개의 추가 패턴 중 하나인 Protected Variance는 4개의 추가 패턴 중 가장 중요한 패턴으로, 변화로부터의 보호를 주장한다.
Protected Variance 는 나머지 3개의 추가 패턴을 통해 구현되며, 변경될 수 있는 부분을 식별하고 이를 안정적인 인터페이스 뒤로 캡슐화하고, 변경의 영향을 최소화하기 위해 인터페이스나 추상 클래스를 활용할 것을 권장한다.
다음과 같이 변하는 부분과 변하지 않는 부분을 구별하고, 그 사이를 "이음새 인터페이스" 로 추상화하여 연결하는 것을 권장한다.
지금부터 이 Protected Variance를 구현하는 세가지 방법에 대해 알아보자.
Polymorphism
Polymorphism이란, 타입에 따라 달라지는 행동을 다형성을 통해 처리함으로써 항상 다형성을 통해 코드를 단순하게 만들 것을 권장하는 패턴이다.
이는 실제 내부 구현에 의존하지 않고, 상위 인터페이스에 의존하게 만들 것을 의미한다.
Pure Fabrication
도메인에 존재하지 않는 클래스를 도입하여, 과밀화된 책임을 분리하는 패턴을 의미한다.
단, 이 패턴은 함부로 사용하면 안되는데, 의존하는 클래스가 하나 추가되면서, 결합도가 올라갈 수 있고, 정적 클래스 모델이 추가되어야 하기 때문에, 예상치 못한 새로운 패턴의 변화에 한층 더 경직된 모델로 변화할 수 있기 때문이다.
써야 하는 경우
- 기존 도메인 모델에 없는 연관관계가 구현 상에서 생성되어야 할 때
- 한 객체가 여러 책임을 감당하게 됐을 때
- 비즈니스가 아닌, 구현 상에 필요한 로직이 반복될 때
대표적으로 도메인 서비스, 애플리케이션 서비스, GoF의 각종 디자인 패턴이 이에 해당된다.
Indirection
Indirection 패턴이란, 두 요소 사이에 중재자를 두어 직접 참조를 막고 결합도를 낮추는 패턴이다.
예시
다음과 같은 직접 참조를 피하고
다음과 같이 방향성을 제거할 것을 권장한다.
이를 통해 “느슨한 결합”을 가능하게 하며, 주로 명세와 구현의 분리를 통해 Indirection을 구현하게 된다.
Class Diagram
Class Diagram을 설계할 땐, 다음과 같은 규칙과 패턴들이 존재한다.
- KISS
- YAGNI
- DRY
- 느슨한 결합
- 최소 지식 원칙
- 할리우드 원칙
- SOLID 원칙
- GoF 디자인 패턴
다양한 규칙과 패턴들이 있지만, 위 자료는 인터넷에 이미 충분히 많이 존재한다.
또한, OOD에서 “책임의 할당” 만큼이나 중요한 것이 객체 간의 연관관계의 구성 방법이다.
따라서 본 글에선, 연관관계의 구성 종류에 대해 좀 더 자세하게 알아보도록 하자.
연관관계
Class Diagram을 그리는 과정은 도메인 모델 내에서 Association만으로 구성했던 연관관계에서, 좀 더 구체적인 객체 간의 상호작용 방식을 결정짓는 과정이다.
연관관계의 설정
연관관계는 크게 두가지 정보를 갖고 있다.
- 방향성 - 누가 누구를 알고 있어야 하는가?
- 관계성 - 상대방이 한개 있을때, 내가 몇개 있을 수 있는가?
방향성
연관관계는 방향성을 띌 수 있다.
- 양방향
- 단방향
만약 A와 B가 서로 참조를 하는 상황인데, A→B로 참조하는 상황이 90%이고, B→A로 참조하는 상황이 10%라면, Pure Fabrication을 이용해 B→A로 참조하는 방향을 제거하고, A→B로 가는 단방향 연관관계만을 남겨두는 것이 좋다.
하지만 Pure Fabrication은 분명 남발하게 될 경우 클래스 구조를 복잡하게 만들어 코드의 가독성을 떨어트리므로, 주의해서 사용하는 것이 좋다. 이를 잘 설계하는 것이 객체지향 개발자의 역량이라고 할 수 있다.
관계성
간단한 예시를 들며 설명하겠다.
- 비행기는 승객 한명이 존재할 때, 자신은 하나만 존재 가능하다.
- 한명의 승객을 태우는 여러개의 비행기는 존재할 수 없다.
- 승객은 비행기 한 대가 존재할 때, 자신은 여러명이 존재 가능하다.
- 한대의 비행기는 여러명의 승객을 태울 수 있다.
연관관계의 종류
연관관계는 크게 다음과 같은 종류들로 구성된다.
- Dependency
- Association
- Aggregation
- Composition Aggregation
- Generalization
- Realization
지금부터 각 연관관계에 대해 알아보자.
Dependency
일시적으로 사용하고 사라지는 관계를 의미한다.
비 구조적인, 가장 약한 연관관계이다.
Association
Association 관계는 가장 기초적인 구조적 연관관계이다.
여기부터는 has-a 관계를 갖는다.
특정 도메인 간에, 구조적으로 관계가 설정된 경우 사용하며, 임시로 사용하고 사라지는 형태가 아닌 경우에 사용한다.
잘 모르겠으면 Association 관계로 표현하면 된다.
Aggregation
Aggregation 관계는 Part - Whole 관계 중 하나로, has-a 관계의 한 종류 불린다.
위 그림에선 Class 1이 Class 2를 Aggregation하고 있다고 할 수 있다.
대표적으로 “노래”와 “플레이리스트”를 예시로 분석해보자.
- 플레이리스트의 경우, 여러개의 노래를 갖는다.
- 여러 플레이리스트가 한 노래를 가질 수 있다.
- 또한, 플레이리스트가 아니더라도, 노래는 독립적으로 존재가 가능하다.
- 하지만, 의미론적으로 플레이리스튼 노래를 소유하는 개념이다.
사실, Aggregation 관계는 의미상으론 Association과 큰 다를 바가 없다. 하지만, Association 관계보다 has-a 관계를 강조하는 용도로 사용된다.
Composition Aggregation
Composition Aggregation 관계는, Part-Whole 관계에서, 완전한 소유 개념이 들어간 관계를 의미한다.
이는 가장 강한 has-a 관계를 의미하며, Part 는 공유되지 않는다.
주요 특징으로는, 상위 타입이 하위 타입의 생명주기를 완전히 관리한다. 상위 타입이 하위 타입의 생명주기를 결정할 수 있으며, 자신이 파괴될 때 하위 타입을 전부 파괴할 수도 있고, 자신이 소유한 Part 를 다른 Whole 에게 건네줄 수 있다.
Generalization
Specialization 관계라고도 불리우는 Generalization 관계는, Supertype/Subtype 간의 관계의 표현에 사용되는 관계이다.
구체 클래스 간 상속 관계를 표현할 때 일반적으로 자주 사용된다.
Composition-Aggregation 관계와 같은 수준의 강한 의존성을 갖게 된다.
Realization
Realization 관계는 Abstract Type의 실체화 표현에 사용되는 관계이며, 일반적으로 인터페이스의 구현체를 표현하는데 사용된다.
좋은 연관관계 vs 나쁜 연관관계
그렇다면, 연관관계를 평가하는 기준은 어떻게 잡을 수 있을까?
- 좋은 연관관계
- 안정적인 도메인에 의존하는 연관관계
- 변하지 않는 도메인에 의존하는 연관관계
- 나쁜 연관관계
- 자주 변하는 도메인에 의존하는 연관관계
- 해당 도메인이 변화할때마다 같이 수정되기 때문에, 피해야 한다.
GRASP 패턴을 떠올려보면, 매우 타당한 기준이라고 평가할 수 있다.
자주 변하는 외부 인프라와 같은 부분을 내부 도메인 모델과 분리하고, 안정적인 부분에 불안전한 부분이 의존하도록 설계해야 한다.
객체지향의 일반적인 웹 백엔드 개발에서의 적용
스프링을 배우면, 주입식으로 MVC부터 학습하게 된다. 덕지덕지 달라붙은 프레임워크로 돌아가는 웹 개발 생태계에서, 이 객체지향은 어떻게 쓰이는걸까?
사실, 프레임워크 없이 돌아가는 계층은 분명히 존재한다. 이는 우리를 "도메인 레이어"라고 부른다.
클린 아키텍처를 예시로, 각 계층이 존재하는 이유를 분석해보자.
인프라스트럭처
외부 기술로 구현된 요소들이 존재하는 계층이다.
가장 외부 레이어에 존재해야 하며, 어느 계층도 해당 계층을 직접 의존해서는 안된다.
웹 개발에선 스프링, DB 구현체, JPA 와 같은 영속화 인프라가 해당 계층에 속하게 된다.
어댑터(인터페이스)
외부 모듈과 상호작용하기 위한 계층이다.
모든 외부 모듈은 해당 인터페이스를 통해 이 모듈과 통신해야 하며, 이외의 계층에 직접 접근해서는 안된다.
웹 개발에선 컨트롤러, RESTful API와 같은 API가 이 계층에 존재한다.
애플리케이션 (Use Case)
도메인 엔티티에 존재하는 상호작용을 실제 Use Case 로 구현하기 위해 존재하는 계층이다.
도메인 엔티티 내에 존재하는 기능들을 적극 활용하여 해당 계층을 구현하게 된다.
@Transactional
어노테이션을 사용하는 서비스들과 같이, 운영 흐름 상에서 발생하는 기능(락 관리, 이벤트 발행, 비동기 처리)들이 계층에 속하게 된다.
도메인
어느 계층에도 의존하면 안되는, “가장 순수한 비즈니스 로직 계층”이다.
다른 레이어도 객체지향적으로 접근이 가능하긴 하지만, 우리가 특별히 집중해야 하는 것은 "도메인 레이어"이다.
도메인 레이어에서 리포지토리의 역할 중 하나는, DB의 영속화된 데이터를 한번에 객체로 가져오는 역할이다.
이로써, "객체지향적인 코드"를 작성할 준비가 완료되고, 이후부터는, 객체 간의 상호작용으로 표현이 가능해진다.
이를 우리는 "도메인 모델 패턴"이라고 부른다.