추상화 Abstraction
지금 맥락에 필요한 속성과 행동만 남기고 나머지는 지웁니다. 비행 시뮬레이터의 비행기와 좌석 예약 시스템의 비행기는 같은 대상이지만 전혀 다른 모델입니다.
교육 자료 · Object-Oriented Design
이미 검증된 22가지 설계 해법으로
바꾸기 쉬운 코드를 만드는 방법
원저 · Alexander Shvets 『디자인 패턴에 뛰어들기』(Refactoring.Guru)의 목차 구성을 바탕으로 재작성한 학습용 슬라이드입니다.
학습 지도
패턴을 외우기 전에, 패턴이 왜 필요한지부터 짚습니다. 원리 → 원칙 → 패턴 순서입니다.
클래스·객체·4대 기둥·객체 간 관계
6장패턴의 정의, 역사, 분류 체계
4장좋은 설계의 조건 · 3대 원칙 · SOLID
11장객체를 만들고 조립하는 12가지 해법
14장객체가 협업하는 10가지 해법 + 적용 가이드
14장PART 1
패턴은 객체지향 위에 세워집니다. 토대가 흔들리면 패턴도 무너집니다.
1-1 · OOP의 기초
클래스는 무엇을 가지고 무엇을 할 수 있는지를 적어 둔 설계도입니다. 객체는 그 설계도로 찍어 낸 실제 인스턴스입니다.
클래스 — 필드(상태)와 메서드(행동)의 묶음
객체 — 같은 청사진, 서로 다른 상태
필드에 담긴 값이 곧 객체의 상태이고, 메서드가 곧 객체의 행동입니다.
1-2 · OOP의 기초
자식은 부모의 필드와 메서드를 물려받습니다. 공통된 것을 위로 올릴수록 아래는 자기 차이만 갖게 됩니다.
1-3 · OOP의 기둥들
지금 맥락에 필요한 속성과 행동만 남기고 나머지는 지웁니다. 비행 시뮬레이터의 비행기와 좌석 예약 시스템의 비행기는 같은 대상이지만 전혀 다른 모델입니다.
내부는 감추고 정해진 인터페이스만 노출합니다. 자동차를 몰기 위해 점화 플러그를 알 필요는 없습니다.
이미 있는 클래스 위에 새 클래스를 세워 코드 중복을 없앱니다. 단, 부모의 인터페이스 전체를 함께 물려받습니다.
같은 메시지를 보내도 객체마다 다르게 반응합니다. 호출하는 쪽은 상대가 누구인지 몰라도 됩니다.
패턴 22가지는 결국 이 네 가지를 어떤 비율로 조합할 것인가에 대한 답들입니다.
1-4 · 추상화와 캡슐화
캡슐화는 단순한 접근 제어자 문제가 아닙니다. 바깥이 의존해도 되는 최소한의 표면을 정하는 설계 행위입니다.
추상화 수준의 기준은 맥락입니다. 같은 대상이라도 시스템의 목적이 다르면 남길 속성이 달라집니다.
1-5 · 상속과 다형성
상속은 코드 중복을 줄이는 가장 빠른 길이지만, 동시에 가장 강한 결합을 만듭니다.
필요 없는 메서드까지 함께 따라옵니다. 그것을 비워 두는 순간 리스코프 치환 원칙이 깨집니다.
겉으로는 무해해 보이는 부모의 수정이 모든 자식에게 예고 없이 번집니다.
대부분의 언어는 부모를 하나만 허용합니다. 두 축으로 확장해야 하는 순간 막힙니다.
실제 동작을 이해하려면 계층을 위로 계속 거슬러 올라가야 합니다.
그래서 뒤에서 다룰 원칙이 나옵니다 — 상속보다 합성을 사용하세요.
1-6 · 객체 간의 관계
UML 화살표는 장식이 아닙니다. 얼마나 강하게 묶여 있는가를 나타내는 척도입니다.
가로축은 결합도의 상대적 강도를 학습용으로 도식화한 값입니다. 위로 갈수록 느슨하고, 아래로 갈수록 바꾸기 어렵습니다.
PART 2
패턴은 복사해 붙여 넣는 코드가 아니라, 문제를 푸는 방식의 설명서입니다.
2-1 · 디자인 패턴이란?
레시피
청사진
그래서 패턴은 라이브러리처럼 가져다 쓰는 것이 아니라, 내 문제에 맞춰 매번 다시 그리는 것입니다.
2-2 · 패턴 설명의 구성
이 자료의 패턴 슬라이드도 같은 순서를 따릅니다.
패턴이 어떤 문제를 푸는지 한 문장으로 요약합니다. 이 한 줄로 후보를 좁힐 수 있어야 합니다.
패턴을 쓰지 않았을 때 코드가 겪는 구체적인 고통입니다. 내 코드에 같은 증상이 있는지 대조합니다.
구조를 어떻게 바꾸어 그 고통을 없애는지 설명합니다. 클래스가 아니라 관계가 바뀝니다.
참여하는 클래스와 그들이 주고받는 관계입니다. 이름은 바꿔도 관계는 유지해야 합니다.
언제 써야 하는지만큼 언제 쓰면 안 되는지가 중요합니다. 둘 다 확인하세요.
패턴은 언제나 무언가를 내주고 무언가를 얻습니다. 그 교환이 지금 남는 장사인지 판단합니다.
2-3 · 역사와 분류
패턴 개념 자체는 건축가 크리스토퍼 알렉산더에게서 왔습니다. 이를 소프트웨어로 옮긴 것이 'Gang of Four'의 카탈로그입니다.
GoF 원서는 23개를 다루지만, 인터프리터 패턴은 오늘날 활용 빈도가 낮아 이 자료에서는 22개를 다룹니다.
2-4 · 왜 배워야 할까요?
매번 처음부터 고민하는 대신, 수십 년간 다듬어진 접근법을 꺼내 씁니다. 함정도 이미 문서화되어 있습니다.
"프록시로 감싸자"는 한마디가 다이어그램 열 장을 대신합니다. 설계 논의의 대역폭이 완전히 달라집니다.
패턴을 배우면 모든 문제가 패턴으로 보입니다. 단순한 문제는 단순하게 푸는 것이 여전히 최선입니다. 패턴은 문제가 실제로 나타난 뒤에 꺼내는 도구이지, 미리 깔아 두는 바닥이 아닙니다.
PART 3
22개 패턴은 모두 이 원칙들을 서로 다른 방식으로 지켜 낸 결과물입니다.
3-1 · 좋은 디자인의 특징
코드를 다시 쓸 수 있게 만들수록 설계는 복잡해집니다. 재사용성과 단순함은 서로 당깁니다.
요구사항은 반드시 바뀝니다. 좋은 설계는 변화를 막는 설계가 아니라, 변화가 닿는 면적을 줄이는 설계입니다.
재사용을 높일수록 추상 계층이 늘고 읽기는 어려워집니다. 어느 지점에서 멈출지 결정하는 것이 설계자의 일입니다.
3-2 · 디자인 원칙
바뀔 부분을 미리 찾아 바뀌지 않는 부분과 분리해 두면, 수정의 충격이 그 안에서 멈춥니다.
method getOrderTotal(order)
total = 0
for each item in order.lineItems
total += item.price * item.quantity
if order.country == "KR"
total += total * 0.10
else if order.country == "US"
total += total * order.state.tax
...
return total세금 규칙이 하나 추가될 때마다 이 메서드를 엽니다.
method getOrderTotal(order)
total = 0
for each item in order.lineItems
total += item.price * item.quantity
total += total * getTaxRate(order)
return total
method getTaxRate(order)
// 세금 규칙만 이 안에서 자랍니다메서드로 뺀 다음 단계는 클래스로 빼는 것입니다. 그것이 전략 패턴입니다.
3-3 · 디자인 원칙
무엇을 할 수 있는지에 의존하고, 그것을 어떻게 하는지에는 의존하지 마세요.
3-4 · 디자인 원칙
축이 둘 이상이 되는 순간, 상속은 곱셈이 되고 합성은 덧셈이 됩니다.
축이 늘어날 때 필요한 클래스 수
브리지 · 전략 · 데코레이터 · 상태는 모두 이 원칙의 구체적인 적용 사례입니다.
3-5 · SOLID
각 원칙은 변경이 닥쳤을 때 무엇이 흔들리는가를 다른 각도에서 관리합니다.
클래스가 바뀌어야 할 이유는 하나뿐이어야 합니다
확장에는 열려 있고 변경에는 닫혀 있어야 합니다
자식은 부모 자리에 그대로 들어갈 수 있어야 합니다
쓰지 않는 메서드에 의존하도록 강요하지 마세요
상위 모듈이 하위 구현이 아닌 추상에 의존해야 합니다
원칙은 비용을 수반합니다. 모두를 항상 지키면 유연하지만 거대한 코드가 됩니다. 실제로 바뀔 만한 곳에만 적용하세요.
3-S · SOLID
클래스가 바뀌어야 할 이유는 오직 하나여야 합니다.
한 클래스가 여러 이유로 수정된다면, 서로 다른 팀이 같은 파일을 동시에 열게 됩니다. 책임을 쪼개면 변경의 이유마다 서로 다른 파일이 열립니다.
class Employee
getName()
printTimeSheetReport()
// 보고서 형식이 바뀌어도
// Employee가 열립니다class Employee
getName()
class TimeSheetReport
print(employee)
// 각자 자기 이유로만
// 바뀝니다3-O · SOLID
확장에는 열려 있고, 변경에는 닫혀 있어야 합니다.
새 기능을 넣기 위해 이미 동작하는 코드를 열지 않아도 되는 구조를 만듭니다. 이미 배포되어 다른 곳이 의존하는 코드일수록 값어치가 큽니다.
class Order
getShippingCost()
if type == "ground" ...
if type == "air" ...
// 배송사 추가 = 이 파일 수정interface Shipping
getCost(order)
class Ground implements Shipping
class Air implements Shipping
// 배송사 추가 = 새 파일 하나3-L · SOLID
자식 객체는 부모 객체를 대체할 수 있어야 합니다. 클라이언트 코드를 고치지 않고서 말입니다.
다섯 원칙 중 가장 형식적인 규칙을 가진 원칙입니다. 공개 라이브러리처럼 남이 내 클래스를 확장하는 상황에서 특히 중요합니다.
자식 메서드가 부모보다 더 좁은 타입을 요구하면 안 됩니다
자식이 부모보다 더 넓은 타입을 반환하면 클라이언트가 감당하지 못합니다
부모가 던지지 않던 예외를 자식이 던지면 호출부가 무너집니다
부모가 받던 입력을 자식이 거부하면 대체가 아닙니다
부모가 보장하던 결과를 자식이 지키지 않으면 안 됩니다
부모가 지키던 상태 규칙을 자식이 깨뜨리면 안 됩니다
템플릿 메서드 패턴에서 자식이 골격의 약속을 어길 때, 가장 자주 이 원칙이 깨집니다.
3-I · SOLID
클라이언트가 쓰지 않는 메서드에 의존하도록 강요하지 마세요.
인터페이스가 넓어질수록 구현체는 쓰지도 않을 메서드를 비워 두게 됩니다. 비워 둔 메서드는 언젠가 예외로 터집니다.
interface CloudProvider
storeFile()
getFile()
createServer()
listServers()
getCDNAddress()
// 스토리지만 하는 업체도
// 전부 구현해야 합니다interface CloudStorage
storeFile() / getFile()
interface Compute
createServer() / listServers()
interface CDN
getCDNAddress()
// 필요한 것만 골라 구현합니다3-D · SOLID
상위 모듈이 하위 모듈에 의존해서는 안 됩니다. 둘 다 추상에 의존해야 합니다.
보통 설계는 위에서 아래로 흐릅니다. 이 원칙은 그 화살표를 뒤집어, 비즈니스 로직이 데이터베이스를 부리도록 만듭니다.
BudgetReport ──▶ MySQLDatabase
// 비즈니스 로직이
// DB 구현에 못 박혀 있습니다
// DB 교체 = 로직 수정BudgetReport ──▶ interface Database ▲ MySQLDatabase / MongoDB // 인터페이스를 상위 계층이 소유합니다 // 화살표가 뒤집혔습니다
PART 4 · CREATIONAL · 5
객체를 어떻게 만들 것인가. 생성 코드를 사용 코드에서 떼어 내는 다섯 가지 방법입니다.
객체를 만드는 책임을 자식 클래스에 넘깁니다
코드 곳곳에 new 구상클래스()가 박혀 있으면, 새로운 타입 하나를 추가할 때마다 호출 지점을 전부 찾아 고쳐야 합니다.
생성자 직접 호출을 팩토리 메서드로 감싸고, 어떤 객체를 반환할지는 자식 클래스가 결정하게 합니다.
서로 어울리는 객체들을 한 벌씩 통째로 만듭니다
의자·소파·테이블처럼 스타일이 맞아야 하는 객체들을 따로 만들면, 어느 순간 모던 소파에 빅토리안 의자가 섞입니다.
제품군마다 팩토리를 하나씩 두고, 클라이언트는 추상 팩토리 인터페이스만 바라보게 합니다.
복잡한 객체를 단계별로 조립합니다
선택 항목이 많은 객체를 만들다 보면 생성자 매개변수가 끝없이 길어지고, 대부분의 호출에서 절반은 null이 됩니다.
조립 단계를 빌더 객체로 옮기고, 필요한 단계만 골라 호출한 뒤 결과를 받아 옵니다.
새로 만들지 않고 기존 객체를 복제합니다
객체를 복사하려면 모든 필드를 알아야 하는데, 비공개 필드는 바깥에서 보이지 않습니다. 게다가 구상 클래스에 묶이게 됩니다.
복제 책임을 객체 자신에게 맡기고, 공통 clone() 인터페이스로 통일합니다.
인스턴스는 하나, 접근점도 하나로 고정합니다
설정 객체나 연결 풀처럼 공유해야 할 자원을 여러 번 만들면, 같은 것을 가리켜야 할 상태가 조용히 갈라집니다.
생성자를 잠그고, 정적 메서드가 언제나 같은 인스턴스를 돌려주게 합니다.
가장 자주 남용되는 패턴입니다. 단일 책임 원칙을 함께 위반하기 쉬워 안티패턴으로 불리기도 합니다.
PART 4 · STRUCTURAL · 7
객체를 어떻게 조립할 것인가. 구조를 유연하게 유지하는 일곱 가지 방법입니다.
맞지 않는 인터페이스 사이에 통역사를 세웁니다
우리 코드는 JSON을 다루는데, 꼭 써야 하는 분석 라이브러리는 XML만 받습니다. 라이브러리는 고칠 수 없습니다.
한쪽 인터페이스를 받아 다른 쪽 형식으로 변환해 전달하는 중간 객체를 둡니다.
추상과 구현을 갈라 각각 따로 키웁니다
도형 × 색상처럼 두 축이 곱해지는 순간 클래스 수가 폭발합니다. 색을 하나 더하면 도형 수만큼 클래스가 늘어납니다.
한 축을 별도 계층으로 빼내고 상속 대신 합성으로 연결합니다. 곱셈이 덧셈으로 바뀝니다.
부분과 전체를 똑같은 방식으로 다룹니다
상자 안에 상자가 든 주문의 총액을 구하려면, 어떤 것이 상자이고 어떤 것이 제품인지 매번 확인하며 재귀를 헤집어야 합니다.
잎과 복합 노드가 같은 인터페이스를 구현하고, 복합 노드는 받은 작업을 자식들에게 그대로 위임합니다.
상속 대신 감싸서 기능을 더합니다
알림에 SMS·슬랙·메일을 조합하려고 상속을 쓰면, 가능한 조합의 수만큼 클래스를 만들어야 합니다.
같은 인터페이스를 가진 래퍼로 객체를 감싸고, 필요한 만큼 래퍼를 겹겹이 중첩합니다.
복잡한 서브시스템 앞에 문 하나를 냅니다
라이브러리 수십 개 클래스를 정해진 순서로 초기화하고 호출해야 하는데, 그 절차가 비즈니스 코드 전체에 번져 있습니다.
자주 쓰는 시나리오만 노출하는 단순한 인터페이스를 하나 만들어 그 뒤로 복잡도를 감춥니다.
공통 상태는 나누고, 고유 상태만 들고 다닙니다
총알 수만 개가 각자 텍스처와 색상 데이터를 복사해서 들고 있으면, 같은 내용이 수만 번 중복되어 메모리가 바닥납니다.
변하지 않는 내적 상태를 공유 객체로 빼내고, 변하는 외적 상태는 호출할 때 인수로 넘깁니다.
진짜 객체 앞에 대리인을 세웁니다
시작할 때마다 무거운 객체를 준비해 두지만, 실제로 쓰이는 것은 그중 일부뿐입니다.
같은 인터페이스를 구현한 대리 객체가 요청을 가로채, 필요한 순간에만 실제 객체를 만들고 전달합니다.
PART 5 · BEHAVIORAL · 10
객체가 어떻게 협업할 것인가. 책임을 나누고 소통을 설계하는 열 가지 방법입니다.
처리기를 줄 세우고 요청을 흘려보냅니다
인증·권한·속도 제한·캐시 검사가 한 함수 안에 겹겹이 쌓이면서, 순서를 바꾸는 것조차 위험해졌습니다.
각 검사를 독립된 핸들러로 만들어 사슬로 잇고, 처리하거나 다음 핸들러로 넘기게 합니다.
요청을 객체로 만들어 보관합니다
버튼·메뉴·단축키가 결국 같은 일을 하는데, UI 클래스마다 같은 로직이 복사되어 있습니다.
작업을 execute()를 가진 커맨드 객체로 감싸고, 호출자는 그 인터페이스만 알게 합니다.
내부 구조는 감추고 순회 방법만 건넵니다
배열·트리·그래프마다 순회 코드가 다른데, 컬렉션이 저장뿐 아니라 순회 기능까지 떠맡아 비대해집니다.
순회 상태와 알고리즘을 별도의 반복자 객체로 옮깁니다.
서로 직접 부르지 말고 관제탑을 거칩니다
폼 위젯들이 서로를 직접 참조하기 시작하면, 위젯 하나를 다른 화면에서 재사용하는 것이 불가능해집니다.
컴포넌트 간 통신을 중재자에게 몰아주고, 각 컴포넌트는 중재자 하나만 알게 합니다.
상태를 봉인해 저장하고 되돌립니다
실행 취소를 만들려면 객체 내부를 들여다봐야 하는데, 그러는 순간 캡슐화가 깨집니다.
객체가 스스로 스냅샷을 만들고, 관리자는 내용을 열어보지 못한 채 보관만 합니다.
관심 있는 대상에게만 자동으로 알립니다
신제품 입고를 알기 위해 매일 매장에 전화하거나, 반대로 관심 없는 사람에게까지 메일을 보내게 됩니다.
발행자가 구독자 목록을 관리하고, 상태가 바뀌면 등록된 구독자에게만 통지합니다.
상태를 조건문이 아니라 클래스로 표현합니다
상태가 하나 늘 때마다 모든 메서드에 분기가 추가되고, 결국 조건문이 코드 전체를 뒤덮습니다.
상태마다 클래스를 만들고, 컨텍스트는 현재 상태 객체에 행동을 위임합니다.
알고리즘을 갈아 끼울 수 있게 만듭니다
경로 안내에 도보·자동차·대중교통을 더할수록 하나의 클래스가 끝없이 부풀어 오르고, 한쪽 수정이 다른 쪽을 망가뜨립니다.
알고리즘을 각각 전략 클래스로 빼내고, 컨텍스트는 인터페이스를 통해 위임만 합니다.
뼈대는 고정하고 살만 바꿉니다
문서 종류마다 분석 절차가 거의 같은데, 클래스마다 같은 코드가 통째로 복제되어 있습니다.
알고리즘 골격을 부모의 템플릿 메서드로 정의하고, 달라지는 단계만 자식이 재정의합니다.
구조는 그대로 두고 연산만 바깥에서 더합니다
도시 그래프의 모든 노드를 XML로 내보내는 기능이 필요한데, 이미 운영 중인 노드 클래스들은 손댈 수 없습니다.
새 동작을 방문자 객체에 담고, 각 노드는 accept()로 자신에게 맞는 메서드를 불러 줍니다.
5-1 · 정리
5-2 · 적용 가이드
패턴 이름을 먼저 떠올리지 마세요. 증상에서 출발하면 패턴은 저절로 좁혀집니다.
new 구상클래스()가 흩어져 있다팩토리 메서드 · 추상 팩토리5-3 · 주의
패턴을 배운 직후가 가장 위험합니다. 모든 문제가 패턴처럼 보이기 시작합니다.
추상화를 더할수록 얻는 것과 잃는 것
정리
“이 코드에서 바뀔 것은 무엇이고, 바뀌지 않을 것은 무엇인가.”
22개 패턴은 이 질문에 대한 22가지 대답이었습니다.
지금 맡고 있는 코드에서 가장 자주 열리는 파일을 하나 고르고, 그 파일이 열리는 이유를 모두 적어 보세요.
증상이 명확한 곳에 패턴 하나만 적용하고, 코드 리뷰에서 그 이유를 설명해 보세요.
설계 논의에서 패턴 이름을 쓰기 시작하면, 그때부터 패턴은 지식이 아니라 도구가 됩니다.
원저 · Alexander Shvets 『디자인 패턴에 뛰어들기』(Refactoring.Guru) · 삽화 드미트리 자르트 · 옮긴이 황우진
이 슬라이드는 원서의 목차 구성을 참고해 교육용으로 새로 작성한 요약본입니다. 상세한 예제 코드와 설명은 원서를 참고하세요.