✅ 구조 패턴 (Structural Patterns) - 7가지
여러 객체나 클래스를 연결해서 더 큰 구조를 만들고, 유지보수가 쉬운 코드를 작성하게 도와주는 패턴.
코드를 더 유연하고, 확장성 있게 만들기 위한 설계 방식이라고 생각하면 된다.
① 어댑터 패턴 (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