디자인 패턴의 목적과 선택
문제와 변화 지점을 기준으로 Strategy·Adapter를 실행하고 GoF 23개 분류와 비용을 설명합니다.
모임 추천 기능에서 ‘먼저 등록된 모임’과 ‘현재 인원이 적은 모임’을 번갈아 선택하려고 합니다. 이때 추천 호출 전체를 두 벌 만들기보다 달라지는 선택 규칙을 교체할 수 있습니다. 디자인 패턴은 반복되는 설계 문제와 해결 구조를 이름 붙여 설명하는 도구이며 모든 코드를 복잡한 클래스 계층으로 바꾸라는 규칙은 아닙니다.
모듈·계약을 읽을 수 있고 함수를 다른 함수에 전달할 수 있으면 진행할 수 있습니다. 이번 본문은 Strategy와 Adapter를 직접 실행하고, 패턴을 사용할 이유와 쓰지 않아도 되는 경우를 비교합니다.
문제·맥락·결과를 먼저 읽는다
패턴을 고를 때는 이름보다 반복되는 문제가 필요합니다. 어떤 부분이 바뀌고 어떤 부분은 안정되어야 하는지, 기존 계약이 무엇인지, 선택으로 생길 비용이 무엇인지를 적습니다. 이름만 알고 코드를 붙이면 문제에 없는 확장 지점과 호출 단계가 늘 수 있습니다.
Strategy(전략)는 달라질 알고리즘이나 정책을 같은 호출 계약 뒤에 두고 선택하는 구조입니다. 이 사례에서는 후보 필터링은 같고 후보 중 하나를 고르는 정책이 달라집니다. Adapter(어댑터)는 이미 가진 구현의 접점을 소비자에게 필요한 계약으로 맞추는 구조입니다. 기존 read/write를 새 load/save로 연결하는 예입니다.
패턴 이름은 협업 언어로도 도움이 됩니다. ‘선택 정책을 Strategy로 나누자’라는 말 뒤에는 정책 입력·결과·실패 의미를 설명해야 합니다. 모든 패턴이 코드의 같은 모양으로만 나타나는 것은 아니며 JavaScript에서는 함수 전달로 충분한 경우도 있습니다.
SOFTWARE ENGINEERING · CH7-2
변하는 선택 정책을 같은 계약으로 교체한다
추천 후보 준비는 유지하고 하나를 고르는 규칙만 바꿉니다.
추천 호출은 같은 후보 선택 계약을 사용하고 첫 후보 전략 또는 인원 최소 전략을 대입할 수 있으며 빈 후보 결과는 둘 다 null입니다.
GoF 분류를 지도로 사용한다
GoF는 Erich Gamma·Richard Helm·Ralph Johnson·John Vlissides의 『Design Patterns』에서 정리한 대표적인 객체지향 설계 패턴 묶음입니다. 원자료 p149의 25개 표기는 23개로 바로잡습니다. 원 출판사 Pearson 안내도 23개로 설명합니다.
| 목적 | 패턴 이름 | 처음 읽을 질문 |
|---|---|---|
| 생성·5개 | Abstract Factory, Builder, Factory Method, Prototype, Singleton | 생성 과정·제품 묶음·복제를 어떤 계약으로 분리할까 |
| 구조·7개 | Adapter, Bridge, Composite, Decorator, Facade, Flyweight, Proxy | 기존 객체의 연결·조합·접근을 어떻게 바꿀까 |
| 행위·11개 | Chain of Responsibility, Command, Interpreter, Iterator, Mediator, Memento, Observer, State, Strategy, Template Method, Visitor | 요청·알고리즘·변경 알림·상태별 행동을 어떻게 협력시킬까 |
이 표는 23개를 모두 구현했다는 뜻이 아닙니다. 생성에서 Factory Method는 생성 선택, Builder는 단계별 조립, Prototype은 기존 객체의 복사, Singleton은 하나의 인스턴스 접근이라는 서로 다른 문제를 봅니다. 구조에서 Facade는 복잡한 기능에 단순한 창구를, Decorator는 같은 접점의 기능 조합을, Proxy는 접근의 중개를 생각할 수 있습니다.
행위에서 Observer는 구독자에게 변화 알림을, Command는 요청을 별 객체로 다루는 것을, State는 대상 상태에 따른 행동 변화를 봅니다. 여기서는 두 패턴을 깊이 실행하고 나머지는 필요한 문제가 생겼을 때 찾는 지도에 둡니다. 이름 목록을 암기했다고 선택 능력을 검증한 것은 아닙니다.
Strategy: 변하는 선택 규칙을 인수로 받는다
추천 후보의 OPEN과 자리 조건을 공통으로 필터링하고 select 함수에 후보 배열을 전달합니다. 두 전략은 모두 ‘후보 중 하나 또는 null 반환’ 계약을 지킵니다. fewestParticipants는 원본 후보 순서를 바꾸지 않도록 사본을 정렬합니다.
추천은 참가 등록이 아닙니다. 후보를 보여 준 뒤 실제 신청할 때 상태가 바뀔 수 있으므로 register가 다시 정책을 확인해야 합니다. 추천에서 자리가 있다고 계산한 것이 좌석 예약을 보장하지 않습니다.
Adapter: 같은 이름보다 같은 의미를 맞춘다
기존 저장소가 단순한 동기 read/write를 제공하면 새 코드에서 사용할 비동기 load/save로 감싸 줍니다. async는 동기 반환값도 Promise 결과로 만들어 호출자가 await할 수 있게 합니다.
그러나 이름만 바꾸면 모든 구현이 호환되는 것은 아닙니다. 기존 write가 append라면 새 save가 전체 교체라는 약속과 다릅니다. 값의 복사·실패 처리·저장된 값이 없을 때의 결과도 계약에 맞추어야 합니다. 이 예제의 기존 구현은 전체 상태 교체와 사본 읽기를 이미 지킵니다.
저장소 최상위 폴더에서 다음 명령을 실행합니다. Node.js가 설치되어 있어야 하며 패키지를 추가로 설치하지 않습니다. 두 번째 인수를 생략하면 이 장의 예제를 모두 실행합니다.
node dev-note/scripts/software-engineering-lab/examples/ch7.mjs ch7-2function chooseSession(sessions, select) {
const available = sessions.filter((s) =>
s.status === "OPEN" && s.participants.length < s.capacity);
return select(available);
}
function firstAvailable(sessions) {
return sessions[0] ?? null;
}
function fewestParticipants(sessions) {
return [...sessions].sort((a, b) =>
a.participants.length - b.participants.length)[0] ?? null;
}
function adaptLegacyStore(legacy) {
return {
async load() { return legacy.read(); },
async save(next) { legacy.write(next); },
};
}
const sessions = [
{ id: "A", status: "OPEN", capacity: 3, participants: ["m1", "m2"] },
{ id: "B", status: "OPEN", capacity: 3, participants: [] },
];
console.log("첫 모임:", chooseSession(sessions, firstAvailable).id);
console.log("인원 적은 모임:", chooseSession(sessions, fewestParticipants).id);
let legacyValue = { id: "legacy", participants: [] };
const legacy = {
read() { return structuredClone(legacyValue); },
write(next) { legacyValue = structuredClone(next); },
};
const store = adaptLegacyStore(legacy);
await store.save({ id: "legacy", participants: ["m3"] });
console.log("Adapter 저장 결과:", (await store.load()).participants.join(","));출력은 첫 모임: A, 인원 적은 모임: B, Adapter 저장 결과: m3입니다. 선택 규칙을 바꿨지만 후보 필터링은 바꾸지 않았고, 저장 호출자는 기존 read/write를 직접 사용하지 않았습니다.
후보가 없으면 두 전략 모두 null을 반환합니다. 결과가 없을 때 예외인지 null인지 계약을 정하지 않으면 호출자가 .id를 읽다가 실패할 수 있습니다. 실무 호출부는 반환값이 없는 경우 안내를 별도로 처리해야 합니다.
비용과 사용하지 않을 때
전략 함수가 하나이고 바뀔 요구도 없다면 별 클래스·팩터리·등록 관리자를 미리 만드는 이점은 작을 수 있습니다. 간단한 조건문이 현재 책임을 명확히 설명하면 그 상태로 시작할 수 있습니다. 변화가 실제로 반복되어 호출부와 정책이 함께 커질 때 분리를 검토합니다.
Singleton으로 모든 저장 상태를 전역에 모으면 쉽게 접근할 수 있지만 테스트와 요청 사이에 상태가 섞일 수 있습니다. ‘한 저장소가 필요하다’는 요구가 반드시 전역 Singleton 패턴을 요구하지는 않습니다. 시작점에서 객체 하나를 생성해 필요한 곳에 전달할 수도 있습니다.
패턴 적용에는 간접 호출·새 계약·추적할 파일의 비용이 있습니다. 유연성이 늘었다는 주장도 새 구현을 넣어 기존 호출과 결과가 유지되는지로 확인합니다. 사용할 패턴 수를 품질 점수처럼 세지 않습니다.
오개념 반례: 모든 조건을 State로 바꾼다
입력의 capacity 숫자가 달라질 때마다 클래스를 만드는 것은 상태별 행동 설계와 다릅니다. 숫자 비교 한 번이 필요한 곳에는 비교가 충분할 수 있습니다. 반면 모집 상태마다 허용 연산과 처리 방식이 복잡하게 반복된다면 State 패턴을 검토할 이유가 생깁니다. 문제의 변화 축을 먼저 설명해야 합니다.
독립 문제: 출력 형식과 오래된 메일 도구
출석 요약은 같은 데이터를 CSV 또는 사람이 읽는 텍스트로 출력해야 합니다. 오래된 메일 도구는 send(recipient, title, body)를 제공하지만 새 호출자는 deliver({ to, subject, text })를 원합니다.
두 변화에 적합한 패턴 또는 단순 구조를 제안하고 입력·결과 계약을 적습니다. ‘CSV 함수를 복사해 텍스트 함수를 만들고 두 곳에서 출석률도 계산한다’는 구현의 문제도 설명합니다. 메일 도구가 실패를 반환하는 방식이 다르면 무엇을 추가로 확인할지도 적습니다.
힌트
출력 형식은 서로 바꿀 정책이고 메일 인수 형태는 서로 맞출 접점입니다. 데이터 계산은 형식과 별도로 유지할 수 있습니다.
풀이
요약 계산은 한 번 수행하고 format(summary) → string 계약의 CSV/텍스트 포맷터를 전달하면 Strategy 관점으로 표현할 수 있습니다. 메일은 새 객체 인수를 기존 세 인수로 풀어 보내는 Adapter가 적합합니다. 기능이 아주 작으면 단순 함수로 이 경계를 구현해도 됩니다.
출석률 계산까지 복사하면 공식 변경 때 두 결과가 달라질 수 있습니다. 실패가 boolean 반환인지 예외인지 확인해 새 deliver의 성공·실패 약속에 맞춥니다. 인수 이름만 바꾸고 실패를 무조건 성공 처리하면 Adapter 계약이 깨집니다.
다음 실습으로 연결
패턴은 변경을 설명하는 이름이고 구현의 정확성은 계약과 실행으로 확인합니다. 다음 본문은 저장소 계약에 메모리·JSON 구현을 대입해 무엇이 유지되고 어떤 운영 한계가 새로 생기는지 살펴봅니다.
원자료와 더 읽기
장원사이버평생교육원 「소프트웨어공학」 PDF 물리 149–158·181–185쪽의 주제를 바탕으로 설명·도표·실습을 새로 구성했습니다.
Pearson의 GoF 원 도서 안내는 패턴 수를 확인하는 근거입니다. 코드 예제는 이 교재의 독서 모임 사례에 맞게 새로 작성했습니다.