Search

7장. 퍼사드 패턴

SOLID 법칙 중 첫번째 원칙에 따라 각 책임별로 클래스를 만든결과
하지만 클라이언트에서 이 세가지 클래스를 다루는게 번거로울 수 있다
이 세가지 클래스를 한번에 묶어 퍼사드 시스템으로 구성한다. 그래서 서브 시스템의 생성까지 Facade가 알아서 한다.
퍼사드 클래스는 서브시스템 클래스를 캡슐화하지 않는다. 서브시스템의 기능을 사용할 수 있는 간단한 인터페이스르 제공할 뿐이다. 클라이언트에서 특정 인터페이스가 필요하다면 서브 시스템 클래스를 그냥 사용하면 된다.
→ 단순화된 인터페이스를 제공하면서도 클라이언트에서 필요로 한다면 시스템의 모든 기능ㅇ르 사용할수있도록 해준다.
어떻게 하면 여러객체에 영향력을 주지 않을 수 있을까?
객체 자체
메소드에 매개변수로 전달된 객체
메소드를 생성하거나 인스턴스를 만든 객체
객체에 속하는 구성요소
다른 메소드를 호출해서 리턴받은 객체의 메서드를 호출하는 일도 바람직하지 않다.
public float getTemp(){ Thermomenter thermometer = station.getThermometer(); return thermomether.getTemperature() }
TypeScript
복사
station으로부터 thermometer객체를 받은 다음 그 객체의 getTemperature 메서드를 직접 호출한다.
원칙을 따르는 경우의 예이다.
public float fetTemp(){ return station.getTemperature() }
TypeScript
복사
최소 지식 원칙을 적용해서 thermomenter에게 요청을 전달하는 메서드를 station클래스에 추가했다. 이러면 의존해야 하는 클래스 개수를 줄일 수 있다.
Q. 패턴이 겉으로 보기엔 단일책임 원칙에 위배되는 것 처럼 보이는데

단일 책임 원칙의 진짜 의미

SRP에서 말하는 책임변경의 이유를 의미합니다. Robert C. Martin의 말을 빌리면:
"A class should have only one reason to change"
Facade 클래스가 여러 하위 시스템을 조율하더라도, 그 클래스가 변경되는 이유는 단 하나입니다. 클라이언트가 필요로 하는 인터페이스가 변경될 때 → Facade에 특정한 변경 이유임 (대부분의 함수/클래스의 경우 변경 이유는 각자 다르다)
Facade가 변경되는 경우: 워크플로우, 호출 순서, 어떤 서비스를 조합할지가 바뀔 때
하위 시스템이 변경되는 경우: 각 서비스의 내부 로직이 바뀔 때
왜 이렇게 책임, 변경의 이유가 하나여야 한다는걸까? "왜 굳이?"라고 생각해요. 메서드로 분리해도 충분하지 않을까?

문제 1: 변경의 파급효과 (Ripple Effect)

같은 클래스에 있을 때

class PaymentService { process(order: Order) { this.chargeCard(order); this.sendEmail(order.email); this.updateInventory(order.items); } private chargeCard(order: Order) { // 결제 로직 } private sendEmail(email: string) { // 이메일 로직 } private updateInventory(items: Item[]) { // 재고 로직 } }
TypeScript
복사
시나리오: 이메일 템플릿만 바꾸고 싶다
// 이메일팀이 수정 class PaymentService { // ← 이 클래스 전체를 건드려야 함 // ... private sendEmail(email: string) { // HTML 템플릿 변경 const template = newHTMLTemplate(); // 수정 smtp.send(email, template); } }
TypeScript
복사
문제점:
1.
PaymentService 전체를 다시 컴파일
2.
PaymentService 전체를 다시 테스트 (결제, 이메일, 재고 모두!)
3.
PaymentService 전체를 다시 배포
4.
결제 기능은 안 바꿨는데도 결제팀이 리뷰해야 함
5.
이메일 버그가 생기면 결제 기능도 롤백

다른 클래스로 분리했을 때

class PaymentService { chargeCard(order: Order) { // 결제 로직만 } } class EmailService { send(email: string) { // 이메일 로직만 } } class InventoryService { update(items: Item[]) { // 재고 로직만 } }
TypeScript
복사
시나리오: 이메일 템플릿만 바꾸고 싶다
// 이메일팀이 수정 class EmailService { // ← 이 클래스만 건드림 send(email: string) { const template = newHTMLTemplate(); // 수정 smtp.send(email, template); } } // PaymentService - 수정 안 함! // InventoryService - 수정 안 함!
TypeScript
복사
장점:
1.
EmailService만 컴파일
2.
EmailService만 테스트
3.
EmailService만 배포
4.
결제팀은 신경 안 써도 됨
5.
이메일 버그가 생겨도 결제는 영향 없음

문제 2: 테스트의 복잡도

같은 클래스에 있을 때

describe('PaymentService', () => { it('should charge card', () => { // 결제 테스트인데... const emailMock = jest.fn(); // 이메일 모킹 필요 const inventoryMock = jest.fn(); // 재고 모킹 필요 // 결제만 테스트하고 싶은데 다 신경써야 함 service.chargeCard = realImplementation; service.sendEmail = emailMock; service.updateInventory = inventoryMock; service.process(order); // 결제만 확인하고 싶은데... expect(service.chargeCard).toHaveBeenCalled(); // 이것도 확인해야 함 expect(emailMock).toHaveBeenCalled(); expect(inventoryMock).toHaveBeenCalled(); }); it('should send email', () => { // 이메일 테스트인데 결제, 재고도 모킹... }); it('should update inventory', () => { // 재고 테스트인데 결제, 이메일도 모킹... }); });
TypeScript
복사

다른 클래스로 분리했을 때

describe('PaymentService', () => { it('should charge card', () => { // 결제만 테스트 service.chargeCard(order); expect(paymentGateway.charge).toHaveBeenCalled(); }); }); describe('EmailService', () => { it('should send email', () => { // 이메일만 테스트 service.send(email); expect(smtp.send).toHaveBeenCalled(); }); }); // 각각 독립적으로 테스트 가능!
TypeScript
복사

문제 3: 팀 협업 충돌

같은 클래스에 있을 때

// 월요일 // 결제팀 개발자가 작업 시작 class PaymentService { chargeCard(order: Order) { // 작업 중... } } // 화요일 // 이메일팀 개발자도 같은 파일 수정 class PaymentService { chargeCard(order: Order) { // 결제팀 작업 중 } sendEmail(email: string) { // 이메일팀 작업 중 } } // 수요일 - Merge Conflict 발생! // 같은 파일을 두 팀이 동시에 수정함
TypeScript
복사

다른 클래스로 분리했을 때

// 월요일 - 결제팀 class PaymentService { // payment-service.ts chargeCard(order: Order) { // 작업 중... } } // 화요일 - 이메일팀 class EmailService { // email-service.ts send(email: string) { // 작업 중... } } // 수요일 - 충돌 없음! // 다른 파일이라 동시에 작업 가능
TypeScript
복사

문제 4: 재사용성

같은 클래스에 있을 때

class PaymentService { process(order: Order) { this.chargeCard(order); this.sendEmail(order.email); this.updateInventory(order.items); } private chargeCard(order: Order) { } private sendEmail(email: string) { } private updateInventory(items: Item[]) { } } // 다른 곳에서 이메일만 쓰고 싶은데... class RefundService { refund(order: Order) { // 이메일만 쓰고 싶은데 PaymentService 전체를 의존해야 함 this.paymentService.sendEmail(order.email); // private이라 접근 불가! // 어쩔 수 없이 중복 구현 smtp.send(order.email, 'Refund confirmed'); } }
TypeScript
복사

다른 클래스로 분리했을 때

class EmailService { send(email: string, message: string) { } } class PaymentService { constructor(private emailService: EmailService) {} process(order: Order) { this.chargeCard(order); this.emailService.send(order.email, 'Payment confirmed'); } } class RefundService { constructor(private emailService: EmailService) {} // 같은 EmailService 재사용! refund(order: Order) { this.processRefund(order); this.emailService.send(order.email, 'Refund confirmed'); } }
TypeScript
복사

문제 5: 의존성 추적

같은 클래스에 있을 때

class PaymentService { process(order: Order) { this.chargeCard(order); this.sendEmail(order.email); this.updateInventory(order.items); } // 결제팀: "SMTP 서버 바꿔야 해요" // → PaymentService 수정 // → 결제 기능도 다시 테스트해야 함 // 재고팀: "데이터베이스 바꿔야 해요" // → PaymentService 수정 // → 결제, 이메일도 다시 테스트해야 함 }
TypeScript
복사

다른 클래스로 분리했을 때

class EmailService { // SMTP 서버만 바꿈 // EmailService만 테스트 } class InventoryService { // 데이터베이스만 바꿈 // InventoryService만 테스트 } // PaymentService는 전혀 안 건드림!
TypeScript
복사

Facade와의 차이

여기가 핵심입니다:
// ❌ 이건 Facade가 아니라 God Object class OrderService { placeOrder(order: Order) { // 직접 구현 const encrypted = aes.encrypt(order.card); axios.post('/payment', encrypted); db.query('UPDATE inventory'); smtp.send(order.email); } } // 변경: 이메일 템플릿 바꾸기 // → OrderService 수정 // → OrderService 전체 테스트 // → OrderService 전체 배포 // → 결제/재고도 영향받음 // ✅ 진짜 Facade class OrderFacade { placeOrder(order: Order) { this.payment.process(order); // 위임만 this.inventory.reserve(order); // 위임만 this.email.send(order.email); // 위임만 } } // 변경: 이메일 템플릿 바꾸기 // → EmailService만 수정 // → EmailService만 테스트 // → EmailService만 배포 // → OrderFacade는 안 건드림!
TypeScript
복사

실제 시나리오

시나리오: 결제 PG사 변경 (토스 → 네이버페이)

같은 클래스:
1. PaymentService.ts 수정 2. PaymentService 전체 빌드 3. 결제 테스트 + 이메일 테스트 + 재고 테스트 모두 실행 4. 전체 통합 테스트 5. PaymentService 배포 6. 이메일/재고 기능도 함께 배포됨 (의도치 않음) 7. 버그 발생시 전체 롤백
Plain Text
복사
다른 클래스:
1. PaymentService.ts만 수정 2. PaymentService만 빌드 3. 결제 테스트만 실행 4. PaymentService만 배포 5. 이메일/재고는 그대로 6. 버그 발생시 PaymentService만 롤백
Plain Text
복사

핵심 정리

메서드로 분리 vs 클래스로 분리:
관점
메서드 분리 (같은 클래스)
클래스 분리
수정 범위
클래스 전체
해당 클래스만
테스트
전체 테스트 필요
해당 클래스만
배포
전체 배포
독립 배포 가능
팀 협업
충돌 가능성 높음
독립 작업 가능
재사용
어려움 (private)
쉬움
롤백
전체 롤백
부분 롤백
결론: "메서드로 나눴으니 괜찮다"가 아니라, **"다른 이유로 변경되는 코드는 다른 클래스에 있어야 한다"**는 게 핵심입니다.
이제 왜 SRP가 중요한지 와닿으시나요?
파사드 패턴
정현님 발표
퍼사드 는 복잡성을 숨기고 사용하기 편리한 높은 수준의 인터페이스를 제공
책으로만 이해하기에 정보 부족
혜리님
파사드 추상화의 바탕이라고 함
지나님
파사드를 의식하고 한건 아니지만 리액트 훅에서 조합해서 필요한 기능을 내보내었던 실무 경험을 예로 들어주셨음
믹스인 패턴
서브클래스가 쉽게 상속받아 재사용할 수 있도록 하는 클래스
리액트에서 믹스인 패턴을 초기에 도입- 공식문서
혜리님 : 책에서 CarAnimaor, PersonAnimator의 역할?
지나님 : 기능을 확장할 때 동적으로 만들수 있다.
데코레이터 패턴
시스템의 내부코드를 변경하지 않고도 기능 추가 가능
필수가 아닌 추가 옵션이란 점에 기인해서 데코레이터 패턴을 생각
상속이 아닌 래핑해서 사용한다.
플라이웨이트 패턴
메모리효율
내부상태를 공유할수 있는 객체
중복되는 데이터들을 좀 줄여서 메모리를 덜 사용하게
내재적 상태와 외재적 상태(파라미터로 받아 올수있는것) 인터페이스로 정의해서, 내부상태와 외부상태를 정의해서 factory
캐시상태를 만들어서 없으면 새로 만들고 있으면 캐시에서 꺼내서 메모리를 절약한다.
메모리 측면에서 접근하는게 다른 패턴들과 특이점
중복된 내부상태가 있을 때 인스턴스가 있으면 중복적으로 발생 → 니즈를 느끼게 됨
카카오 SDK가져와서 쓸 때 이런패턴이 적용될것 같다.
how? 마커찍기,마커찍는걸 동일한 상태값인데 위치만 다른경우,
해당 패턴들은 구조 패턴의 범주에 속하므로 클래스와 개체를 더 큰 구조로 만들수 있게 하는 목적성을 염두하기 ## 7.11 Facade > 퍼사드 패턴은 심층적인 복잡성을 숨기고, 사용하기 편리한 높은 수준의 인터페이스를 제공하는 패턴 - jQuery나 자바스크립트 라이브러리에서 흔히 볼 수 있는 구조 패턴 - 숨겨진 하위 시스템이 아닌, 밖에 나타난 퍼사드와 직접 상호작용 가능- 예를 들어, jQuery의 $(el).css, $(el).animate() 같은 메서드 사용시 퍼사드를 사용하는것. - 퍼사드로 만들어진 시스템에 의해 main 에서 실행되는 클라이언트는 서브시스템 클래스들 각각의 내부 구현에 대해 자세히 알필요가 없다.- 서브시스템의 사용법이 변경되더라도 클라이언트 코드가 아닌 퍼사드 코드만 맞춰 수정하면된다(?)느낀점 - `Facade는 "조율자"로, 실제 일은 서브시스템으로`- Facade 패턴은 이미 잘 설계된 서브시스템들 위에 단순한 인터페이스를 제공하는 것이라고 생각함.## 7.12 Mixin- 기능의 확장을 위해 믹스인의 상속을 이용 - 동적으로 부모 클래스를 받아 확장하는 Mixins함수 만들기```javascriptconst MyMixins = superclass => class extends superclass{ moveup(){ console.log("move up") } moveDown(){ console.log("move down") } stop(){ console.log("stop! in the name of love!") }}```CarAnimatior, PersonAnimator = MyMixin을 사용해 기존 클래스의 기능 +추가기능```javascriptclass CarAnimator{ moveleft(){ console.log("move left") }}class PersonAnimator{ moveRandomly(){ /***.... */ }}class MyAnimator extends MyMixin(CarAnimator){}const myAnimator = new myAnimator()myAnimator.moveleft() ```- 반복된 기능을 줄이고, 새로운 기능을 확장 할 수 있음#### 믹스인패턴의 논쟁 - 프로토 타입 오염과 함수출처에 대한 불확실성 - 리액트 es6 클래스 도입 이전, 컴포넌트에 기능추가하기 위해 믹스인 사용-> 유지보수와 재사용 복잡서으로 믹스인 반대 -> 고차 컴포넌트나 Hooks장려 ## 7.13 데코레이터 패턴 - 객체에 동적으로 새로운 기능을 추가하는 패턴- 기존 코드를 수정하지 않고 기능 확장- 상속 대신 조합(Composition) 사용```javascript// 사용법const myMacbookPro = new MacbookPro()//맥북에 케이스 데코레이터 추가const decorateMacbookPro = new CaseDecorator(myMacbookPro)```#### 주의사항- 자잘한 객체가 많이 추가 되므로, 복잡도가 높아짐 - 협업시 패턴에 익숙하지 않은 개발자가 사용목적을 파악하기 어려워 관리가 힘들어짐느낀점 - 데코레이터를 생각하면 어떤 몸체가 있고 거기에 꾸며주는 역할 -> 기능을 확장 - 이 때 몸체는 그대로(기존코드 수정없이)- 그럼 같은 원리로 컴포넌트를 생각해보면, 몸체 컴포넌트가 있고 컴포넌트를 받아 기능을 확장하는 새로운 컴포넌트를 리턴한다.- 리액트 컴포넌트에선 Higher Order Component 같은 원리, Higher Order Function - 자바스크립트## 플라이웨이트 패턴- 반복되고 비효율적으로 데이터를 공유하는 코드를 최적화- 연관된 객체끼리 데이터를 공유하면서 어플리케이션의 메모리를 최소화하는 목적 #### 사용법- 데이터 레이어에서 메모리에 저장된 비슷한 객체사이로 데이터 공유- DOM레이어에서 비슷한 동작을 하는 이벤트 핸들러를 부모 요소같은 중앙 이벤트 관리자에게 위임#### 플라이워이트 패턴과 DOM객체- 이벤트 감지 방법 : 이벤트 캡처링, 버블링 - 캡처링은 이벤트가 상위요소에서 감지되어 ->하위요소로 전파 - 버블링은 이벤트가 하위요소에서 감지되어 -> 상위요소로 전파 - 여기서 플라이웨이트는 이벤트 버블링 과정을 추가조정하는데 사용 -> 반복되는 이벤트 핸들러를 상위 요소에 위임