우리가 ERD를 먼저 설계하고, 속성 중심으로 객체를 정의한 뒤 개발을 시작하면 어떤 문제가 발생할까?
가장 큰 문제는 해당 객체가 어떤 문맥(context)에서 사용될지를 고려하지 않은 채 데이터를 설계하게 된다는 점이다. 이렇게 되면 이후 개발 과정에서 억지로 데이터를 껴맞추는 상황이 발생하게 된다. 예를 들어, 복잡한 조인을 포함한 2중, 3중의 SQL을 작성하고 있다면 이미 냄새가 풍기고 있는 셈이다.
객체를 똑똑하게 만들기 위해서는 객체가 사용될 문맥, 즉 사용 시나리오를 고려해야 한다. 이 지점에서 책임(responsibility)이라는 개념이 등장한다.
그렇다면 여기서 말하는 문맥(context)이란 무엇일까? 이를 이해하기 위해 절차지향 방식의 문제점부터 다시 짚어보자.
절차지향 프로그래밍의 한계
절차지향 방식에서는 객체의 내부 속성(데이터)을 먼저 결정하기 때문에, 그 데이터에 강하게 결합된 방식으로 코딩하게 된다. 이렇게 되면 @Service 클래스에 비즈니스 로직이 집중되고, 도메인 객체는 단순한 데이터 전달 객체(즉, Getter/Setter만 존재하는 객체)로 전락하게 된다.
예를 들어, 포인트 충전 및 사용 로직을 PointService에서 처리하고 있다고 가정해보자. 원래는 UserPoint 객체 내부에 있어야 할 비즈니스 로직이 모두 서비스 레이어에 존재하게 되는 것이다.
그렇다면 이 비즈니스 로직을 도메인 객체로 옮기면 어떻게 될까?
도메인 객체가 행동(behavior)을 갖게 되고, 그 행동은 외부에 어떤 인터페이스를 제공할지를 스스로 결정하게 된다. 예를 들어 외부에서 userPoint.charge(...)를 호출하게 되는 식이다. 이처럼 행동은 외부와의 협력 안에서 결정된다.
즉, 객체는 혼자 존재하는 것이 아니라, 협력 속에서 자신의 책임을 수행해야 한다.
협력과 문맥, 그리고 데이터의 우선순위
객체가 어떤 행동을 할 것인지 먼저 결정하고, 그에 필요한 데이터는 나중에 결정해야 한다.
만약 데이터를 먼저 결정해버리면, 내부 데이터에 종속적인 코드(getter/setter 기반의 코드)를 작성하게 된다. 이는 협력보다는 구조적 결합에 집중된 설계다.
따라서 객체는 협력 안에서 사용될 행동을 중심으로 설계되어야 하며, 협력을 통해 자연스럽게 필요한 데이터가 결정되는 방식으로 설계되어야 한다.
책임이란 무엇인가?
책임은 객체의 행동에서 비롯된다.
객체는 스스로 해야 할 일이 무엇인지(Doing), 그리고 무엇을 알아야 하는지(Knowing)를 책임으로 가진다.
- Doing (하는 것)
- 객체 스스로 어떤 계산이나 로직을 수행하는 것
- 다른 객체의 행동을 요청하거나
- 그 행동을 제어하고 조율하는 것
- Knowing (아는 것)
- 자신의 상태(캡슐화된 데이터)를 아는 것
- 연관된 다른 객체에 대해 아는 것
- 상태나 연관 객체를 통해 유추할 수 있는 것
이처럼 책임은 협력을 수행하기 위한 단위이며, 객체의 존재 이유이기도 하다.
책임이 중요한 이유는?
책임은 객체 간 협력을 조율하는 핵심 요소다. 모든 행동이 하나의 객체에 집중되어 있다면, 협력 구조는 왜곡되고 유지보수는 어려워진다.
예를 들어, 결제와 관련된 책임은 결제 객체에, 주문과 관련된 책임은 주문 객체에 분산되어야 한다.
이처럼 책임이 올바르게 분산되면, 객체는 스스로 설명 가능한 단위(self-descriptive)가 되며, 코드의 이해도와 유지보수성이 크게 향상된다.
협력은 외부에서 객체를 바라보는 관점이라면, 책임은 객체 내부에서 자신이 해야 할 일을 판단하는 기준이라 할 수 있다.
그렇다면 역할(Role)은 무엇인가?
객체가 협력 안에서 책임을 수행한다고 할 때, 우리는 동일한 협력을 서로 다른 방식으로 수행할 수 있다. 이때 공통된 협력 방식에 이름을 붙인 것이 바로 역할(Role)이다.
예를 들어보자.
이커머스 시스템에서 고객은 제품을 구매할 때 할인을 받을 수 있다.
어떤 날은 정액 할인으로 1,000원을 할인받고, 다른 날은 정률 할인으로 10%를 할인받는다고 하자.
고객 입장에서는 “할인을 받았다”는 사실만 중요할 뿐, 그것이 정액인지 정률인지는 중요하지 않다. 즉, 두 할인 방식은 서로 다른 방식의 구현체이지만, 같은 역할, 즉 할인 정책이라는 추상화된 역할을 수행하고 있다.
이처럼 역할은 협력 관점에서 바라본 객체의 공통적인 책임 묶음이다.
그리고 이 역할을 코드 상에서는 인터페이스로 표현할 수 있다.
그 아래에는 정액 할인, 정률 할인과 같은 구체적인 구현체들이 존재하게 된다.
정리
- 문맥은 객체가 사용되는 실제 상황이다.
- 행동은 문맥 안에서 객체가 수행하는 일이며,
- 책임은 그 행동을 근거로 객체가 가져야 할 역할이다.
- 협력은 객체들이 함께 문제를 해결하는 과정이며,
- 역할은 그 협력 과정에서 객체들이 수행하는 공통된 책임의 추상화다.
이러한 관점에서 객체를 바라보고 설계하면, 변화에 유연하고 협력이 자연스러운 시스템을 만들 수 있다.
'가치관 쌓기' 카테고리의 다른 글
내가 테스트 코드를 작성하지 못하는 이유에 대해서 (0) | 2024.12.01 |
---|---|
계약에 의한 설계(Contract By Design) 더 잘 활용하기(with java) (0) | 2021.04.05 |
Getter와 Setter는 왜 써야 할까? 꼭 써야될까? (0) | 2021.02.27 |
댓글