Facade Pattern 정리

테사벨로그·2025년 10월 23일

Design Pattern

목록 보기
7/19

1. 왜 Facade Pattern이 생겨났는가?

문제 상황

// ❌ 나쁜 예: 클라이언트가 복잡한 서브시스템을 직접 다룸
public class Client {
    public void compileCode(InputStream input, BytecodeStream output) {
        // 클라이언트가 컴파일러의 모든 내부 구조를 알아야 함
        Scanner scanner = new Scanner(input);
        ProgramNodeBuilder builder = new ProgramNodeBuilder();
        Parser parser = new Parser();
        parser.Parse(scanner, builder);
        
        RISCCodeGenerator generator = new RISCCodeGenerator(output);
        ProgramNode parseTree = builder.GetRootNode();
        parseTree.Traverse(generator);
    }
}

문제점:

  • 클라이언트가 서브시스템의 모든 세부 클래스를 알아야 함 (강한 결합)
  • 서브시스템 사용법이 복잡하고 진입 장벽이 높음
  • 서브시스템이 변경되면 모든 클라이언트 코드 수정 필요
  • 너무 많은 옵션과 클래스로 인해 초보자가 어디서 시작해야 할지 모름

2. Facade Pattern의 핵심 개념

Facade란?

  • 복잡한 서브시스템에 대한 단순화된 단일 인터페이스를 제공
  • 건물의 "정면(Facade)"처럼 복잡한 내부를 감추고 깔끔한 외관만 보여줌
  • 기능을 추가하지 않고, 기존 기능을 더 쉽게 사용할 수 있게 함

구성 요소

요소역할
Facade서브시스템의 복잡한 클래스들을 통합하는 간단한 인터페이스 제공
Subsystem Classes실제 작업을 수행하는 개별 클래스들 (Facade를 알지 못함)
ClientFacade를 통해 서브시스템 사용 (서브시스템 직접 접근도 가능)

3. Facade vs Adapter vs Mediator

Facade vs Adapter

구분FacadeAdapter
목적복잡한 인터페이스를 단순화호환되지 않는 인터페이스를 변환
인터페이스 개수여러 인터페이스 → 하나의 간단한 인터페이스하나의 인터페이스 → 다른 인터페이스
기존 인터페이스꼭 필요하지 않음반드시 준수해야 할 인터페이스 존재

Facade vs Mediator

구분FacadeMediator
관계 방향단방향 (Client → Facade → Subsystem)양방향 (Colleagues ↔ Mediator)
인지 여부서브시스템은 Facade를 모름Colleagues는 Mediator를 알고 있음
기능 추가기능 추가 없음 (단순화만)새로운 협력 기능 추가 가능

4. Facade Pattern 핵심 구조

Client (여러 개)
   ↓
Facade (단일 진입점)
   ↓ ↓ ↓
SubsystemA  SubsystemB  SubsystemC
   ↓           ↓           ↓
 Class1     Class2      Class3

- 클라이언트는 Facade만 알면 됨
- Facade는 내부적으로 서브시스템 클래스들을 조율
- 고급 사용자는 원한다면 서브시스템에 직접 접근 가능

5. 예시 코드

문제 상황: 홈 시어터 시스템

// ❌ Facade 없이 사용 - 너무 복잡함!
public class Client {
    public void watchMovie() {
        // 11단계의 복잡한 과정...
        popper.on();
        popper.pop();
        
        lights.dim(10);
        
        screen.down();
        
        projector.on();
        projector.setInput(dvd);
        projector.wideScreenMode();
        
        amp.on();
        amp.setDvd(dvd);
        amp.setSurroundSound();
        amp.setVolume(5);
        
        dvd.on();
        dvd.play(movie);
    }
}

Step 1: 서브시스템 클래스들

// 서브시스템의 복잡한 클래스들
public class Amplifier {
    public void on() { System.out.println("앰프 켜기"); }
    public void setDvd(DvdPlayer dvd) { System.out.println("DVD 연결"); }
    public void setSurroundSound() { System.out.println("서라운드 사운드 모드"); }
    public void setVolume(int level) { System.out.println("볼륨: " + level); }
    public void off() { System.out.println("앰프 끄기"); }
}

public class DvdPlayer {
    public void on() { System.out.println("DVD 플레이어 켜기"); }
    public void play(String movie) { System.out.println("재생: " + movie); }
    public void stop() { System.out.println("DVD 정지"); }
    public void eject() { System.out.println("DVD 꺼내기"); }
    public void off() { System.out.println("DVD 플레이어 끄기"); }
}

public class Projector {
    public void on() { System.out.println("프로젝터 켜기"); }
    public void wideScreenMode() { System.out.println("와이드스크린 모드"); }
    public void off() { System.out.println("프로젝터 끄기"); }
}

public class TheaterLights {
    public void dim(int level) { System.out.println("조명 밝기: " + level + "%"); }
    public void on() { System.out.println("조명 켜기"); }
}

public class Screen {
    public void down() { System.out.println("스크린 내리기"); }
    public void up() { System.out.println("스크린 올리기"); }
}

public class PopcornPopper {
    public void on() { System.out.println("팝콘 기계 켜기"); }
    public void pop() { System.out.println("팝콘 튀기는 중..."); }
    public void off() { System.out.println("팝콘 기계 끄기"); }
}

Step 2: Facade 클래스 구현

// ✅ Facade: 복잡한 서브시스템을 간단한 인터페이스로 통합
public class HomeTheaterFacade {
    // 서브시스템 컴포넌트들
    private Amplifier amp;
    private DvdPlayer dvd;
    private Projector projector;
    private TheaterLights lights;
    private Screen screen;
    private PopcornPopper popper;
    
    // 생성자에서 모든 서브시스템 컴포넌트 초기화
    public HomeTheaterFacade(Amplifier amp, DvdPlayer dvd,
                            Projector projector, TheaterLights lights,
                            Screen screen, PopcornPopper popper) {
        this.amp = amp;
        this.dvd = dvd;
        this.projector = projector;
        this.lights = lights;
        this.screen = screen;
        this.popper = popper;
    }
    
    // 간단한 메서드: 영화 보기 (내부적으로 복잡한 과정 처리)
    public void watchMovie(String movie) {
        System.out.println("\n영화 볼 준비 중...\n");
        
        popper.on();
        popper.pop();
        
        lights.dim(10);
        screen.down();
        
        projector.on();
        projector.wideScreenMode();
        
        amp.on();
        amp.setDvd(dvd);
        amp.setSurroundSound();
        amp.setVolume(5);
        
        dvd.on();
        dvd.play(movie);
        
        System.out.println("\n준비 완료! 영화를 즐기세요!\n");
    }
    
    // 간단한 메서드: 영화 끝내기
    public void endMovie() {
        System.out.println("\n홈 시어터 종료 중...\n");
        
        popper.off();
        lights.on();
        screen.up();
        
        projector.off();
        
        amp.off();
        
        dvd.stop();
        dvd.eject();
        dvd.off();
        
        System.out.println("\n홈 시어터 종료 완료!\n");
    }
    
    // 음악 듣기 기능도 추가 가능
    public void listenToMusic(String album) {
        System.out.println("\n음악 듣기 모드...\n");
        
        lights.on();
        amp.on();
        amp.setVolume(5);
        // ... 음악 관련 설정
    }
}

Step 3: 클라이언트 사용

public class HomeTheaterTest {
    public static void main(String[] args) {
        // 서브시스템 컴포넌트 생성
        Amplifier amp = new Amplifier();
        DvdPlayer dvd = new DvdPlayer();
        Projector projector = new Projector();
        TheaterLights lights = new TheaterLights();
        Screen screen = new Screen();
        PopcornPopper popper = new PopcornPopper();
        
        // ✅ Facade 생성
        HomeTheaterFacade homeTheater = 
            new HomeTheaterFacade(amp, dvd, projector, lights, screen, popper);
        
        // 간단한 메서드 호출로 복잡한 과정 실행!
        homeTheater.watchMovie("인셉션");
        
        // ... 영화 감상 ...
        
        homeTheater.endMovie();
    }
}

출력 결과

영화 볼 준비 중...

팝콘 기계 켜기
팝콘 튀기는 중...
조명 밝기: 10%
스크린 내리기
프로젝터 켜기
와이드스크린 모드
앰프 켜기
DVD 연결
서라운드 사운드 모드
볼륨: 5
DVD 플레이어 켜기
재생: 인셉션

준비 완료! 영화를 즐기세요!

홈 시어터 종료 중...

팝콘 기계 끄기
조명 켜기
스크린 올리기
프로젝터 끄기
앰프 끄기
DVD 정지
DVD 꺼내기
DVD 플레이어 끄기

홈 시어터 종료 완료!

6. 실전 예제: Compiler Facade

// 서브시스템 클래스들
class Scanner { 
    public Scanner(InputStream input) { /* ... */ }
}

class Parser { 
    public void Parse(Scanner scanner, ProgramNodeBuilder builder) { /* ... */ }
}

class ProgramNodeBuilder {
    public ProgramNode GetRootNode() { /* ... */ return null; }
}

class RISCCodeGenerator {
    public RISCCodeGenerator(BytecodeStream output) { /* ... */ }
}

class ProgramNode {
    public void Traverse(CodeGenerator generator) { /* ... */ }
}

// ✅ Compiler Facade
class Compiler {
    public Compiler() {}
    
    // 단 하나의 간단한 메서드로 전체 컴파일 과정 수행
    public void Compile(InputStream input, BytecodeStream output) {
        Scanner scanner = new Scanner(input);
        ProgramNodeBuilder builder = new ProgramNodeBuilder();
        
        Parser parser = new Parser();
        parser.Parse(scanner, builder);
        
        RISCCodeGenerator generator = new RISCCodeGenerator(output);
        ProgramNode parseTree = builder.GetRootNode();
        parseTree.Traverse(generator);
    }
}

// 클라이언트 사용
public class Main {
    public static void main(String[] args) {
        Compiler compiler = new Compiler();
        
        // 복잡한 내부 과정은 신경 쓰지 않고 간단하게 사용!
        compiler.Compile(inputStream, outputStream);
    }
}

7. Law of Demeter (최소 지식 원칙)

Facade Pattern은 Law of Demeter를 잘 따릅니다.

원칙: "가까운 친구하고만 대화하라"

메서드 m은 다음 객체의 메서드만 호출해야 함:
1. 객체 자신 (this)
2. 메서드의 매개변수
3. 메서드 내에서 생성한 객체
4. 객체의 직접적인 컴포넌트
5. 전역 변수

❌ 나쁜 예: Law of Demeter 위반

// 두 개의 점(.) 사용 - 위반!
public float getTemp() {
    Thermometer thermometer = station.getThermometer();  // 첫 번째 호출
    return thermometer.getTemperature();                 // 두 번째 호출
}

문제점:

  • station의 내부 구조(Thermometer)를 알아야 함
  • Thermometer가 변경되면 이 코드도 수정 필요

✅ 좋은 예: Law of Demeter 준수

// 하나의 점(.) 사용 - Facade 역할!
public float getTemp() {
    return station.getTemperature();  // station이 Facade처럼 동작
}

// Station 클래스 내부에서 처리
class Station {
    private Thermometer thermometer;
    
    public float getTemperature() {
        return thermometer.getTemperature();  // 내부에서 처리
    }
}

장점:

  • 클라이언트는 Station만 알면 됨
  • Thermometer 구현이 변경되어도 클라이언트 코드는 안전

8. 핵심 정리

Facade Pattern의 특징

요소설명
목적복잡한 서브시스템에 대한 간단한 인터페이스 제공
핵심기능 추가 없이 사용성만 개선
관계Client → Facade → Subsystems (단방향)
장점낮은 결합도, 쉬운 사용, 서브시스템 변경에 강함

언제 사용하는가?

  • 복잡한 서브시스템을 간단하게 사용하고 싶을 때
  • 클라이언트와 구현 사이의 의존성을 줄이고 싶을 때
  • 계층화된 시스템을 만들 때 (각 계층의 진입점)
  • 초보자도 쉽게 사용할 수 있는 인터페이스가 필요할 때
  • 레거시 시스템을 감싸서 새 시스템과 통합할 때

핵심 원칙

  1. 단순화: 복잡한 것을 간단하게 (기능 추가 X)
  2. 느슨한 결합: 클라이언트 ↔ 서브시스템 분리
  3. 접근성: 고급 사용자는 서브시스템 직접 접근 가능
  4. 최소 지식: 클라이언트는 Facade만 알면 됨

실제 사용 사례

  • 웹 애플리케이션: Web Server가 Application Server의 Facade 역할
  • 데이터베이스 접근: DAO(Data Access Object)가 복잡한 DB 작업의 Facade
  • 라이브러리/프레임워크: 복잡한 내부 구조를 감춘 간단한 API
  • 운영체제: 시스템 콜이 복잡한 하드웨어의 Facade

9. Facade Pattern 체크리스트

Facade가 필요한지 확인:

  • 서브시스템이 복잡한가?
  • 클라이언트가 여러 클래스를 알아야 하는가?
  • 초보자가 사용하기 어려운가?
  • 서브시스템 변경이 클라이언트에 영향을 주는가?

Facade 구현 시:

  • 자주 사용되는 작업을 메서드로 제공했는가?
  • 고급 사용자를 위해 서브시스템 직접 접근이 가능한가?
  • 새로운 기능을 추가하지 않았는가? (단순화만!)
  • Law of Demeter를 준수하는가?
profile
다들 응원합니다.

0개의 댓글