들어가며
프로그래밍에서 객체지향 설계의 핵심은 "책임을 어떻게 나누고, 각 객체에게 얼마나 자율성을 주느냐"에 있습니다.
객체의 책임이 자율적일수록 협력이 이해하기 쉬워지고 유연하게 변경할 수 있게 됩니다. 이를 피자 만들기를 예로 들어 자세히 살펴보겠습니다.
1. 자율적인 책임은 협력을 단순하게 만든다
피자를 주문하는 손님(클라이언트)은 피자를 직접 만드는 방법을 알 필요가 없습니다.
자율적인 책임은 의도를 명확하게 표현함으로써 협력을 단순하고 이해하기 쉽게 만듭니다. 구체적인 수행 방식을 지시하는 대신 목적(WHAT)을 선언적으로 전달합니다. 세부적인 방식은 객체 내부에서 결정하므로 외부와의 협력은 단순해집니다.
interface PizzaMaker {
makePizza(): string;
}
class CheesePizzaMaker implements PizzaMaker {
makePizza(): string {
return "치즈 피자 완성!";
}
}
class PepperoniPizzaMaker implements PizzaMaker {
makePizza(): string {
return "페퍼로니 피자 완성!";
}
}
// 손님은 단순히 '피자 만들어줘'라고 말함
function orderPizza(pizzaMaker: PizzaMaker) {
console.log(pizzaMaker.makePizza());
}
orderPizza(new CheesePizzaMaker());
orderPizza(new PepperoniPizzaMaker());
손님은 "피자 만들어줘(makePizza)"라는 메시지만 전달합니다. 치즈를 어떻게 넣고 어떻게 굽는지는 몰라도 됩니다. 이렇게 책임이 적절하게 추상화되어 협력이 단순해집니다.
2. 자율적인 책임은 외부와 내부를 명확하게 분리한다
손님은 피자가 "어떻게" 만들어지는지 알 필요가 없습니다. 오직 "피자를 만든다"는 결과만 알면 됩니다.
객체는 책임을 수행할 방법을 자율적으로 선택할 수 있습니다. 중요한 것은 책임만 완수하면 어떤 방법을 선택할지는 전적으로 객체 내부에서 결정한다는 점입니다.
class WoodFirePizzaMaker implements PizzaMaker {
makePizza(): string {
// 나무 장작 오븐 사용
return "장작불 피자 완성!";
}
}
class ElectricPizzaMaker implements PizzaMaker {
makePizza(): string {
// 전기 오븐 사용
return "전기 오븐 피자 완성!";
}
}
손님 입장에서는 makePizza()만 호출하면 되므로, 내부적으로 장작불을 쓰든 전기 오븐을 쓰든 상관없습니다.
외부에 노출된 책임과 내부의 구현 방식이 분리되어 있어, 객체가 몰라도 되는 부분이 캡슐화됩니다. 이것이 바로 인터페이스와 구현의 분리입니다.
3. 내부 방법이 바뀌어도 외부에는 영향이 없다
가게 주인이 피자 굽는 오븐을 바꿔도, 손님이 피자 주문하는 방식은 변하지 않습니다.
만약 책임이 구체적이고 타율적이라면 조건을 하나하나 명시해야 하고, 두 객체는 긴밀한 커플링이 발생하여 변경의 파급 효과가 커집니다.
반면 책임이 자율적이라면 구체적인 방법은 내부에서 정의되므로 외부 인터페이스는 변하지 않습니다. 변경의 파급 효과가 객체 내부로 캡슐화되어 두 객체 간 결합도가 낮아집니다.
// 새로운 구현: 돌 오븐
class StoneOvenPizzaMaker implements PizzaMaker {
makePizza(): string {
return "돌 오븐 피자 완성!";
}
}
// 클라이언트 코드는 전혀 수정할 필요 없음
orderPizza(new StoneOvenPizzaMaker());
4. 자율적인 책임은 협력의 대상을 다양하게 선택할 수 있다
같은 인터페이스를 구현하기만 하면, 새로운 피자 종류도 쉽게 추가할 수 있습니다.
동일한 인터페이스를 만족한다면 다른 객체가 책임을 수행할 수 있습니다. 인터페이스는 책임의 명세이며, 이를 만족할 경우 대체될 수 있습니다.
책임이 자율적일수록 협력이 유연해지고 다양한 문맥에서 재활용될 수 있습니다. 인터페이스가 추상적이고 책임이 자율적일수록 설계가 유연해지고 재사용성이 높아집니다.
class BulgogiPizzaMaker implements PizzaMaker {
makePizza(): string {
return "불고기 피자 완성!";
}
}
// 새로운 피자 추가도 기존 코드 수정 없이 가능
orderPizza(new BulgogiPizzaMaker());
5. 객체의 역할이 명확해진다
각 클래스는 단순하고 명확한 책임을 가집니다:
CheesePizzaMaker→ 치즈 피자 만들기PepperoniPizzaMaker→ 페퍼로니 피자 만들기StoneOvenPizzaMaker→ 돌 오븐으로 피자 굽기
"이 객체는 무슨 역할을 하는지?"가 명확하게 드러납니다.
객체가 수행하는 책임들이 자율적일수록 객체의 존재 이유를 명확하게 표현할 수 있습니다. 객체는 동일한 목적을 달성하는 강하게 연관된 책임으로 구성되므로, 책임이 자율적일수록 객체의 응집도를 높은 상태로 유지하기 쉬워집니다.
마무리
좋은 설계의 핵심은 다음과 같습니다:
인터페이스는 추상적이어야 하며, 세부 내용은 각 구현체에 위임하고, 협력은 추상화된 인터페이스를 통해 이루어져야 한다
책임이 자율적일수록:
- ✅ 협력 단순화 → 손님은 "피자 만들어줘"만 요청
- ✅ 인터페이스와 구현 분리 → 장작불/전기/돌 오븐 여부는 내부 결정
- ✅ 변경 파급 효과 최소화 → 오븐 교체해도 주문 방식은 동일
- ✅ 협력 유연성 → 새로운 피자 종류 쉽게 확장 가능
- ✅ 명확한 역할 → 클래스 이름만 봐도 하는 일을 알 수 있음
설계 품질 체크 포인트
다음 질문들로 설계 품질을 확인해보세요:
- 이 메서드는 HOW(어떻게)가 아니라 WHAT(무엇을) 표현하고 있는가?
- 새로운 구현을 추가할 때 기존 코드를 수정해야 하는가?
- 객체의 역할이 명확하게 드러나는가?
결국 "책임이 얼마나 자율적인가?"가 설계 품질을 결정합니다.
'typescript' 카테고리의 다른 글
| satistfies 문법 그래서 언제 쓰는 건가요? (1) | 2025.12.29 |
|---|---|
| 브랜드 타입(Brand Type)을 이용한 타입 좁히기 (0) | 2025.12.23 |
| 타입스크립트의 공변성과 반변성 (2) | 2024.07.13 |
| any 타입이 유용한 경우 (1) | 2024.07.13 |
| Generic을 활용한 복잡한 구조의 타입 추론하기 (0) | 2024.07.05 |