자격증/정보처리기사

[정보처리기사 실기 이론] 디자인 패턴 유형 | 구조패턴(Structural Pattern) 쉽게 이해하기

꾸행일기 2025. 7. 2. 17:22

✅ 구조 패턴 (Structural Patterns) - 7가지

여러 객체나 클래스를 연결해서 더 큰 구조를 만들고, 유지보수가 쉬운 코드를 작성하게 도와주는 패턴.
코드를 더 유연하고, 확장성 있게 만들기 위한 설계 방식이라고 생각하면 된다.

[어브릿컴 데퍼싸 플록시]

패턴 핵심 키워드
댑터 패턴
(Adapter)
- 서로 다른 인터페이스를 호환되도록 변환해주는 패턴
- 호환되지 않는 인터페이스 클래스들을 함께 이용할 수 있도록
 타 클래스의 인터페이스를 기존 인터페이스에 덧씌움
- 기존 클래스 재사용 가능
브릿지 패턴
(Bridge)
- bridge 다리를 놓다, 연결하다
- 기능 클래스 계층구현 클래스 계층연결
- 구현부에서 추상층 분리

- 추상부(기능)와 구현부(구체)가 독립적으로 확장할 수 있음
- 기능과 구체 각각 기능 확장 가능
포지트 패턴
(Composite)
- com 함께 + posite 놓이다
- 부분과 전체 계층으로 표현
- 개별 객체(파일)와 복합 객체(폴더) 모두 동일하게(열기기능) 취급
코레이터 패턴
(Decorator)
- 객체 간 결합으로 기능 확장
- 기존 클래스에 필요 기능 추가
퍼싸드 패턴
(Facade)
-  facade 건물의 외관, 정면
- 복잡한 서브 시스템을 간단한 인터페이스 하나로 감싸는(Wrapper) 패턴
- 결합도를 낮추어 시스템 구조 파악을 쉽게함
- 통합된 인터페이스 제공
라이웨이트 패턴
(Flyweight)
- flyweight 가볍다
- 공통 데이터를 공유해서 메모리를 절약함(가볍게 함)
- 클래스의 경량화
- 여러 개의 가상 인스턴스 제공
록시 패턴
(Proxy)
- proxy 대리인
- 직접 접근하지 않고 대리 객체를 통해 접근하도록 한다
- 실제 이용할 때 메모리 할당
- 특정 객체로의 접근을 제어하는 용도로 사용

 

[☞ 생성패턴 공부하러 가기]

[☞ 행위패턴 공부하러 가기]


  어댑터 패턴 (Adapter Pattern)

🧠 개념 요약

인터페이스(사용 방식)가 호환되지 않는 두 객체를 연결할 수 있게 해줌.

🎓 왜 쓰는가?

기존 코드를 수정하지 않고, 새 코드에 맞춰서 연결하고 싶을 때 사용한다.

🧃 비유

“220V 전자제품을 110V 콘센트에 꽂을 때 변환 어댑터 사용하는 것”

🔧 예시

// 기존 시스템은 MediaPlayer 인터페이스를 사용
interface MediaPlayer { void play(String fileName); }

// 새로운 시스템은 AdvancedPlayer만 지원
class Mp4Player {
    void playMp4(String fileName) { ... }
}

// 어댑터를 통해 둘을 연결
class MediaAdapter implements MediaPlayer {
    private Mp4Player player = new Mp4Player();
    void play(String fileName) { player.playMp4(fileName); }

  브리지 패턴 (Bridge Pattern)

🧠 개념 요약

**기능(추상화)**과 **구현(플랫폼)**을 분리해서 독립적으로 확장할 수 있게 해줌.

다시 말해, 기능(리모컨)과 플랫폼(TV)을 분리해서 따로 바꿀 수 있게 함

📘 Bridge의 어원부터 이해하기

Bridge = “다리를 놓다”, “연결하다”

→ 두 개의 독립된 영역 사이를 연결하는 다리 역할
즉, 기능(추상)과 구현(구체) 사이를 분리하고 연결하는 구조.

🎓 왜 쓰는가?

기능과 구현이 동시에 늘어날 경우, 상속보다 조합으로 처리하는 게 낫기 때문이야.

🧃 비유

리모컨을 여러 TV에 사용할 수 있고, TV도 여러 리모컨을 받을 수 있도록 설계

🔧 예시

// 1. 구현부 (실제 동작 - TV 종류별)
interface TV {
    void turnOn();  // 전원 켜기
    void turnOff(); // 전원 끄기
}

// 삼성 TV
class SamsungTV implements TV {
    public void turnOn() {
        System.out.println("🔵 삼성 TV 켜짐");
    }
    public void turnOff() {
        System.out.println("🔵 삼성 TV 꺼짐");
    }
}

// LG TV
class LGTV implements TV {
    public void turnOn() {
        System.out.println("🟢 LG TV 켜짐");
    }
    public void turnOff() {
        System.out.println("🟢 LG TV 꺼짐");
    }
}

// 2. 추상화 (리모컨 - 기능 제공)
abstract class RemoteControl {
    protected TV tv;  // 구현부와 연결되는 다리 (bridge!)

    RemoteControl(TV tv) {
        this.tv = tv;
    }

    abstract void pressPower(); // 리모컨 기능 정의
}

// 3. 구체 리모컨 (기능 확장 가능)
class BasicRemote extends RemoteControl {
    BasicRemote(TV tv) {
        super(tv);
    }

    void pressPower() {
        System.out.println("🔘 리모컨 전원 버튼 누름");
        tv.turnOn(); // 실제 TV의 켜기 실행
    }
}

-----------------------------------------------------------------------
// 삼성 TV에 리모컨 연결
TV samsung = new SamsungTV();
RemoteControl remote = new BasicRemote(samsung);
remote.pressPower();  // "삼성 TV 켜짐"

// LG TV로 교체해도 리모컨 코드는 그대로 사용 가능
remote = new BasicRemote(new LGTV());
remote.pressPower();  // "LG TV 켜짐"

  컴포지트 패턴 (Composite Pattern)

🧠 개념 요약

객체를 트리 구조로 만들어, 개별 객체와 복합 객체를 같은 방식으로 다룰 수 있게 해줌....

📘 Composite의 어원부터 이해하기

composite = com(함께) + posite(put, 놓다) → “함께 놓인 것”, “복합체”, “합성된 것”

즉, 여러 개의 구성 요소(부분)가 모여서 하나의 전체(복합체)를 이루는 구조를 의미한다.
→ 이 패턴 이름이 “부분-전체(Part-Whole)” 관계를 하나처럼 다룬다”는 뜻이다.

 

컴포지트 패턴 개별 객체(ex. 파일)와 그 객체들을 포함하는 그룹(ex. 폴더)을 동일한 방식으로 다루는 구조를 만드는 패턴이다.

 

예를 들면, 📂 폴더도 더블클릭해서 열리고, 📄 파일도 더블클릭해서 열잖아?
→ 둘 다 “열기(open)”라는 동일한 행동을 하는 객체처럼 다룰 수 있는 것,
이게 바로 “부분과 전체를 동일하게 다룬다”는 의미다!!

🔧 예시

// 공통 기능 정의: 파일이든 폴더든 "열기(open)" 가능해야 함
interface FileSystemItem {
    void open();  // 공통 인터페이스
}

// 리프 객체: 실제 파일
class File implements FileSystemItem {
    String name;

    File(String name) {
        this.name = name;
    }

    public void open() {
        System.out.println("📄 파일 열기: " + name);
    }
}

// 컴포지트 객체: 폴더 (안에 여러 개 아이템을 포함할 수 있음)
class Folder implements FileSystemItem {
    String name;
    List<FileSystemItem> items = new ArrayList<>();

    Folder(String name) {
        this.name = name;
    }

    // 폴더에 파일/폴더 추가
    public void add(FileSystemItem item) {
        items.add(item);
    }

    public void open() {
        System.out.println("📁 폴더 열기: " + name);
        for (FileSystemItem item : items) {
            item.open(); // 폴더 안의 모든 항목을 동일한 방식으로 열기!
        }
    }
}

  데코레이터 패턴 (Decorator Pattern)

📘 어원

decorate = 꾸미다, 장식하다
→ 기존 기능에 "장식처럼" 새 기능을 겹겹이 추가하는 패턴

🧠 개념 요약

기존 객체에 기능을 동적으로 추가할 수 있게 해줌. 상속없이 기능을 확장할 수 있다.

 

🎓 왜 쓰는가?

기능 조합을 동적으로 바꾸고 싶을 때 사용한다.

🧃 비유

“기본 커피에 휘핑, 시럽, 샷 추가 등 옵션을 조합해서 음료를 만드는 것”

🔧 예시

// 커피에 옵션 추가하기

// 커피 인터페이스 (공통 동작)
interface Coffee {
    String getDescription();
    int cost();
}

// 기본 커피 클래스
class BasicCoffee implements Coffee {
    public String getDescription() {
        return "기본 커피 ☕";
    }

    public int cost() {
        return 2000;
    }
}

// 데코레이터: 휘핑 추가
class WhipDecorator implements Coffee {
    private Coffee base;  // 안에 감싸는 기존 커피

    WhipDecorator(Coffee base) {
        this.base = base;
    }

    public String getDescription() {
        return base.getDescription() + " + 휘핑크림";
    }

    public int cost() {
        return base.cost() + 500;
    }
}

// 실제 사용
Coffee coffee = new BasicCoffee();
coffee = new WhipDecorator(coffee);
System.out.println(coffee.getDescription()); // 기본 커피 + 휘핑크림
System.out.println(coffee.cost()); // 2500

 퍼사드 패턴 (Facade Pattern)

📘 어원

facade = 건물의 "정면", 외관
→ 복잡한 내부를 감추고 간단한 외부 인터페이스만 제공하는 것

🧠 개념 요약

복잡한 서브시스템을 간단한 인터페이스 하나로 감싸는 패턴.
클라이언트는 내부 구조를 몰라도 됨.

🎓 왜 쓰는가?

외부에 복잡함을 숨기고, 간단하게 사용하도록 하기 위함.

🧃 비유

“리모컨 하나로 TV + 스피커 + 조명까지 모두 켜기”
→ 내부 동작은 복잡하지만, 사용자 입장에서는 버튼 하나

🔧 예시

// 컴퓨터 켜기
// 복잡한 내부 구성
class CPU {
    void start() { System.out.println("CPU 켜기"); }
}
class Memory {
    void load() { System.out.println("메모리 로딩"); }
}
class Disk {
    void spin() { System.out.println("디스크 회전"); }
}

// 퍼사드 클래스
class Computer {
    CPU cpu = new CPU();
    Memory memory = new Memory();
    Disk disk = new Disk();

    void startComputer() {
        cpu.start();
        memory.load();
        disk.spin();
    }
}

Computer myPC = new Computer();
myPC.startComputer();  // 버튼 하나로 모두 시작

 플라이웨이트 패턴 (Flyweight Pattern)

📘 어원

flyweight = "가볍다", 복싱에서 '라이트급 선수'
 가볍게 공유해서 메모리 아끼기

🧠 개념 요약

공통되는 데이터를 공유해서 메모리를 절약하는 패턴. 수많은 객체가 생성될 때 성능 최적화에 효과적.

🎓 왜 쓰는가?

메모리 사용량이 너무 많을 때, 동일한 부분은 공유해서 효율을 높임.

🧃비유

“게임에서 똑같은 나무를 1000개 그릴 때, 나무 모양은 공유하고 위치만 따로 저장”

🔧 예시

// 나무의 공통 정보 (공유됨)
class TreeType {
    String name;
    String texture;

    TreeType(String name, String texture) {
        this.name = name;
        this.texture = texture;
    }

    void draw(int x, int y) {
        System.out.println(name + " 나무를 " + x + "," + y + "에 그림");
    }
}

// 개별 나무는 위치만 다름
class Tree {
    int x, y;
    TreeType type; // 공유됨

    Tree(int x, int y, TreeType type) {
        this.x = x;
        this.y = y;
        this.type = type;
    }

    void draw() {
        type.draw(x, y);
    }
}

 


  프록시 패턴 (Proxy Pattern)

📘 어원

proxy = 대리인, 대리 처리자
→ 누군가 대신 처리해주는 객체

🧠 개념 요약

어떤 객체에 직접 접근하지 않고, 대리 객체(Proxy)를 통해 접근하도록 하는 패턴

🎓 왜 쓰는가?

  • 상황예시
    ❗ 접근 제한이 필요할 때 관리자만 접근 가능한 기능
    ❗ 실제 객체 생성을 늦춰야 할 때 무거운 파일, 대용량 이미지 등
    ❗ 네트워크/리소스를 줄이고 싶을 때 캐시 프록시, 로컬 대리

🧃 비유

1) 연예인 매니저

  • 연예인에게 직접 연락하는 건 어렵고, 매니저에게 연락해서 스케줄을 잡음
  • 매니저(Proxy)가 요청을 대신 받고, 내부적으로 연예인(Real Object)과 연결

2) 이미지 미리보기

  • 무거운 이미지 파일은 필요할 때만 로딩하고,
  • 미리보기에서는 프록시 객체가 대기하다가 실제 요청 시 이미지 로딩

🔧 예시

// 1. 진짜 이미지 클래스 (무거운 객체)
// 진짜 이미지: 로딩에 시간이 오래 걸림
class RealImage {
    String filename;

    RealImage(String filename) {
        this.filename = filename;
        System.out.println("📸 고화질 이미지를 로딩 중...: " + filename);
    }

    void display() {
        System.out.println("🖼️ 화면에 이미지 보여줌: " + filename);
    }
}
//설명
//생성자(RealImage 생성 시점)에서 이미지를 불러오는 무거운 작업 수행
//display()를 호출해야 이미지를 화면에 보여줌


------------------------------------------------------------------------------------
// 2. 프록시 이미지 클래스 (대리인 역할)
// 프록시 이미지: 진짜 이미지를 대신 관리
class ProxyImage {
    RealImage realImage;  // 실제 이미지를 담을 변수
    String filename;

    ProxyImage(String filename) {
        this.filename = filename;
    }

    void display() {
        // 이미지가 아직 로딩 안됐으면, 그때서야 로딩함!
        if (realImage == null) {
            realImage = new RealImage(filename);
        }

        // 진짜 이미지의 display() 호출
        realImage.display();
    }
}

//설명
//처음에는 realImage가 null이야 (아직 안 만들어짐!)
//display()를 호출하는 순간 → 진짜 이미지를 그때서야 생성
//이후부터는 기존에 만든 이미지로 바로 보여줌


public class Main {
    public static void main(String[] args) {
        System.out.println("1️⃣ ProxyImage 객체 생성 중...");
        ProxyImage image = new ProxyImage("cute-cat.jpg");

        System.out.println("\n2️⃣ 아직 display()를 안 했으니까 이미지 로딩 안 됨");

        System.out.println("\n3️⃣ 이제 display() 호출!");
        image.display();  // 여기서 처음으로 RealImage 생성됨

        System.out.println("\n4️⃣ 두 번째 display(): 이미지 재사용");
        image.display();  // 여기선 RealImage가 이미 있어서 바로 출력됨
    }
}
------------------------------------------------------------------------------------

 

Main함수 출력 결과

1️⃣ ProxyImage 객체 생성 중...

2️⃣ 아직 display()를 안 했으니까 이미지 로딩 안 됨

3️⃣ 이제 display() 호출!
📸 고화질 이미지를 로딩 중...: cute-cat.jpg
🖼️ 화면에 이미지 보여줌: cute-cat.jpg

4️⃣ 두 번째 display(): 이미지 재사용
🖼️ 화면에 이미지 보여줌: cute-cat.jpg

 

 

[☞ 생성패턴 공부하러 가기]

[☞ 행위패턴 공부하러 가기]