GOF 디자인패턴
GoF(Gang of Four) 디자인 패턴은 소프트웨어 설계의 바이블이다. 1994년 출판된 “Design Patterns: Elements of Reusable Object-Oriented Software”에서 에리히 감마, 리처드 헬름, 랄프 존슨, 존 블리시데스가 정리한 23가지 패턴을 Java 코드와 함께 살펴본다.
디자인 패턴이란?
디자인 패턴은 소프트웨어 설계에서 반복적으로 나타나는 문제에 대한 재사용 가능한 해결책이다.
패턴의 구성 요소
| 요소 | 설명 |
|---|---|
| 이름 | 패턴을 식별하는 명칭 |
| 문제 | 패턴이 해결하는 상황 |
| 해결 | 설계 요소들의 구조와 관계 |
| 결과 | 패턴 적용의 장단점 |
패턴 분류
┌─────────────────────────────────────────────────────────────┐
│ GoF 디자인 패턴 (23) │
├───────────────┬───────────────┬─────────────────────────────┤
│ 생성 패턴 (5) │ 구조 패턴 (7) │ 행동 패턴 (11) │
├───────────────┼───────────────┼─────────────────────────────┤
│ Singleton │ Adapter │ Strategy │ Observer │
│ Factory Method│ Bridge │ Template Method│ State │
│ Abstract Factory│ Composite │ Command │ Mediator │
│ Builder │ Decorator │ Iterator │ Memento │
│ Prototype │ Facade │ Chain of Resp.│ Visitor │
│ │ Flyweight │ Interpreter │ │
│ │ Proxy │ │ │
└───────────────┴───────────────┴───────────────┴────────────┘
생성 패턴 (Creational Patterns)
객체 생성 메커니즘을 다루며, 시스템이 어떤 구체 클래스를 사용하는지 감추고 객체 생성의 유연성을 높인다.
Singleton (싱글턴)
클래스의 인스턴스가 오직 하나만 존재하도록 보장하고, 전역 접근점을 제공한다.
문제 상황
- 데이터베이스 연결 풀, 로깅, 설정 관리 등 하나의 인스턴스만 필요한 경우
- 여러 인스턴스가 생성되면 리소스 낭비나 일관성 문제 발생
구현 방식
1. Eager Initialization (즉시 초기화)
public class EagerSingleton {
// 클래스 로딩 시점에 인스턴스 생성
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {
// private 생성자로 외부 생성 방지
}
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
2. Lazy Initialization with Double-Checked Locking
public class LazyDoubleCheckSingleton {
// volatile: 메모리 가시성 보장
private static volatile LazyDoubleCheckSingleton instance;
private LazyDoubleCheckSingleton() {}
public static LazyDoubleCheckSingleton getInstance() {
if (instance == null) { // 1차 검사 (락 없이)
synchronized (LazyDoubleCheckSingleton.class) {
if (instance == null) { // 2차 검사 (락 획득 후)
instance = new LazyDoubleCheckSingleton();
}
}
}
return instance;
}
}
3. Initialization-on-demand Holder (권장)
public class HolderSingleton {
private HolderSingleton() {}
// static inner class는 외부 클래스 로딩 시 초기화되지 않음
private static class Holder {
private static final HolderSingleton INSTANCE = new HolderSingleton();
}
public static HolderSingleton getInstance() {
return Holder.INSTANCE; // 이 시점에 Holder 클래스 로딩 및 초기화
}
}
4. Enum Singleton (가장 안전)
public enum EnumSingleton {
INSTANCE;
private final Connection connection;
EnumSingleton() {
// 초기화 로직
this.connection = createConnection();
}
private Connection createConnection() {
// 데이터베이스 연결 생성
return null;
}
public Connection getConnection() {
return connection;
}
}
// 사용
EnumSingleton.INSTANCE.getConnection();
실무 예시: Spring의 싱글턴 빈
@Configuration
public class AppConfig {
@Bean // 기본적으로 싱글턴 스코프
public DataSource dataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/mydb");
return new HikariDataSource(config);
}
}
주의사항
장점:
- 인스턴스가 하나임을 보장
- 전역 접근점 제공
- 지연 초기화 가능
단점:
- 전역 상태로 인한 테스트 어려움
- 멀티스레드 환경에서 동기화 필요
- 단일 책임 원칙 위반 가능성
Factory Method (팩토리 메서드)
객체 생성을 서브클래스에 위임하여, 생성할 객체의 클래스를 서브클래스가 결정하게 한다.
문제 상황
// 문제: 클라이언트 코드가 구체 클래스에 의존
public class OrderService {
public void processOrder(String type) {
Notification notification;
// 타입에 따라 직접 객체 생성 - OCP 위반
if (type.equals("email")) {
notification = new EmailNotification();
} else if (type.equals("sms")) {
notification = new SmsNotification();
} else if (type.equals("push")) {
notification = new PushNotification();
}
notification.send();
}
}
팩토리 메서드 적용
// 1. Product 인터페이스
public interface Notification {
void send(String message);
}
// 2. Concrete Products
public class EmailNotification implements Notification {
@Override
public void send(String message) {
System.out.println("이메일 발송: " + message);
}
}
public class SmsNotification implements Notification {
@Override
public void send(String message) {
System.out.println("SMS 발송: " + message);
}
}
public class PushNotification implements Notification {
@Override
public void send(String message) {
System.out.println("푸시 알림: " + message);
}
}
// 3. Creator (추상 팩토리)
public abstract class NotificationFactory {
// 팩토리 메서드 - 서브클래스에서 구현
public abstract Notification createNotification();
// 템플릿 메서드처럼 사용 가능
public void notify(String message) {
Notification notification = createNotification();
notification.send(message);
}
}
// 4. Concrete Creators
public class EmailNotificationFactory extends NotificationFactory {
@Override
public Notification createNotification() {
return new EmailNotification();
}
}
public class SmsNotificationFactory extends NotificationFactory {
@Override
public Notification createNotification() {
return new SmsNotification();
}
}
// 5. 사용
public class Client {
public static void main(String[] args) {
NotificationFactory factory = new EmailNotificationFactory();
factory.notify("주문이 완료되었습니다.");
factory = new SmsNotificationFactory();
factory.notify("배송이 시작되었습니다.");
}
}
Simple Factory (변형)
// 정적 팩토리 메서드
public class NotificationSimpleFactory {
public static Notification create(String type) {
return switch (type.toLowerCase()) {
case "email" -> new EmailNotification();
case "sms" -> new SmsNotification();
case "push" -> new PushNotification();
default -> throw new IllegalArgumentException("Unknown type: " + type);
};
}
}
// 사용
Notification notification = NotificationSimpleFactory.create("email");
실무 예시: Spring의 BeanFactory
public interface BeanFactory {
Object getBean(String name);
<T> T getBean(Class<T> requiredType);
// ...
}
// ApplicationContext가 BeanFactory의 구현체 역할
@Component
public class OrderService {
@Autowired
private ApplicationContext context;
public void process() {
// 팩토리를 통한 빈 조회
PaymentProcessor processor = context.getBean(PaymentProcessor.class);
processor.process();
}
}
Abstract Factory (추상 팩토리)
관련된 객체들의 가족(family)을 생성하기 위한 인터페이스를 제공한다.
팩토리 메서드 vs 추상 팩토리
Factory Method:
- 하나의 제품을 생성
- 상속을 통한 객체 생성
Abstract Factory:
- 관련된 제품군을 생성
- 구성(Composition)을 통한 객체 생성
구현 예시: UI 컴포넌트 팩토리
// 1. 추상 제품들
public interface Button {
void render();
void onClick();
}
public interface Checkbox {
void render();
void toggle();
}
public interface TextField {
void render();
String getValue();
}
// 2. Windows 구현
public class WindowsButton implements Button {
@Override
public void render() {
System.out.println("Windows 스타일 버튼 렌더링");
}
@Override
public void onClick() {
System.out.println("Windows 버튼 클릭");
}
}
public class WindowsCheckbox implements Checkbox {
@Override
public void render() {
System.out.println("Windows 스타일 체크박스 렌더링");
}
@Override
public void toggle() {
System.out.println("Windows 체크박스 토글");
}
}
// 3. MacOS 구현
public class MacButton implements Button {
@Override
public void render() {
System.out.println("Mac 스타일 버튼 렌더링");
}
@Override
public void onClick() {
System.out.println("Mac 버튼 클릭");
}
}
public class MacCheckbox implements Checkbox {
@Override
public void render() {
System.out.println("Mac 스타일 체크박스 렌더링");
}
@Override
public void toggle() {
System.out.println("Mac 체크박스 토글");
}
}
// 4. 추상 팩토리
public interface UIFactory {
Button createButton();
Checkbox createCheckbox();
TextField createTextField();
}
// 5. 구체 팩토리들
public class WindowsUIFactory implements UIFactory {
@Override
public Button createButton() {
return new WindowsButton();
}
@Override
public Checkbox createCheckbox() {
return new WindowsCheckbox();
}
@Override
public TextField createTextField() {
return new WindowsTextField();
}
}
public class MacUIFactory implements UIFactory {
@Override
public Button createButton() {
return new MacButton();
}
@Override
public Checkbox createCheckbox() {
return new MacCheckbox();
}
@Override
public TextField createTextField() {
return new MacTextField();
}
}
// 6. 클라이언트 코드
public class Application {
private final Button button;
private final Checkbox checkbox;
public Application(UIFactory factory) {
// 구체 클래스에 의존하지 않음
this.button = factory.createButton();
this.checkbox = factory.createCheckbox();
}
public void render() {
button.render();
checkbox.render();
}
}
// 7. 사용
public class Main {
public static void main(String[] args) {
UIFactory factory;
String os = System.getProperty("os.name").toLowerCase();
if (os.contains("win")) {
factory = new WindowsUIFactory();
} else {
factory = new MacUIFactory();
}
Application app = new Application(factory);
app.render();
}
}
실무 예시: JDBC DriverManager
// DriverManager는 추상 팩토리 패턴을 사용
// 각 DB 벤더가 Connection, Statement, ResultSet 등 관련 객체군을 생성
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost:3306/mydb", "user", "password"
);
// MySQL Driver가 MySQL용 Connection, Statement 등을 생성
Statement stmt = conn.createStatement(); // MySQLStatement
ResultSet rs = stmt.executeQuery("SELECT * FROM users"); // MySQLResultSet
Builder (빌더)
복잡한 객체의 생성 과정과 표현을 분리하여, 같은 생성 과정에서 다른 표현을 만들 수 있게 한다.
문제 상황
// 생성자 파라미터가 많은 경우
public class User {
public User(String firstName, String lastName, int age,
String phone, String address, String email,
boolean isActive, LocalDate birthDate,
String department, String position) {
// 파라미터 순서 혼동 가능
// 선택적 파라미터 처리 어려움
}
}
// 텔레스코핑 생성자 안티패턴
public User(String firstName, String lastName) { ... }
public User(String firstName, String lastName, int age) { ... }
public User(String firstName, String lastName, int age, String phone) { ... }
// 생성자가 기하급수적으로 증가
Builder 패턴 적용
public class User {
// 필수 필드
private final String firstName;
private final String lastName;
// 선택적 필드
private final int age;
private final String phone;
private final String email;
private final String address;
private final boolean isActive;
private final LocalDate birthDate;
private User(Builder builder) {
this.firstName = builder.firstName;
this.lastName = builder.lastName;
this.age = builder.age;
this.phone = builder.phone;
this.email = builder.email;
this.address = builder.address;
this.isActive = builder.isActive;
this.birthDate = builder.birthDate;
}
public static class Builder {
// 필수
private final String firstName;
private final String lastName;
// 선택 (기본값 설정)
private int age = 0;
private String phone = "";
private String email = "";
private String address = "";
private boolean isActive = true;
private LocalDate birthDate = null;
public Builder(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public Builder age(int age) {
this.age = age;
return this;
}
public Builder phone(String phone) {
this.phone = phone;
return this;
}
public Builder email(String email) {
this.email = email;
return this;
}
public Builder address(String address) {
this.address = address;
return this;
}
public Builder active(boolean isActive) {
this.isActive = isActive;
return this;
}
public Builder birthDate(LocalDate birthDate) {
this.birthDate = birthDate;
return this;
}
public User build() {
// 유효성 검증
validate();
return new User(this);
}
private void validate() {
if (firstName == null || firstName.isBlank()) {
throw new IllegalStateException("firstName is required");
}
if (age < 0) {
throw new IllegalStateException("age cannot be negative");
}
}
}
// Getters...
}
// 사용
User user = new User.Builder("홍", "길동")
.age(30)
.email("hong@example.com")
.phone("010-1234-5678")
.active(true)
.build();
Lombok @Builder
@Builder
@Getter
public class User {
private final String firstName;
private final String lastName;
@Builder.Default
private int age = 0;
@Builder.Default
private boolean isActive = true;
private String email;
private String phone;
}
// 사용
User user = User.builder()
.firstName("홍")
.lastName("길동")
.email("hong@example.com")
.build();
Director 패턴
// 복잡한 빌드 과정을 Director가 관리
public class UserDirector {
public User createDefaultAdmin(User.Builder builder) {
return builder
.active(true)
.build();
}
public User createGuestUser() {
return new User.Builder("Guest", "User")
.active(false)
.build();
}
}
실무 예시: StringBuilder, Stream.Builder
// StringBuilder
String result = new StringBuilder()
.append("Hello")
.append(" ")
.append("World")
.toString();
// Stream.Builder
Stream<String> stream = Stream.<String>builder()
.add("one")
.add("two")
.add("three")
.build();
// HttpRequest.Builder (Java 11+)
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.example.com/users"))
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(10))
.POST(HttpRequest.BodyPublishers.ofString(jsonBody))
.build();
Prototype (프로토타입)
기존 객체를 복제하여 새로운 객체를 생성한다.
사용 시점
- 객체 생성 비용이 높을 때 (DB 조회, 네트워크 요청 등)
- 런타임에 객체 타입이 결정될 때
- 클래스 계층이 복잡할 때
구현
// 1. Prototype 인터페이스
public interface Prototype<T> extends Cloneable {
T clone();
}
// 2. 구체 프로토타입
public class Document implements Prototype<Document> {
private String title;
private String content;
private List<String> authors;
private Map<String, Object> metadata;
public Document(String title, String content) {
this.title = title;
this.content = content;
this.authors = new ArrayList<>();
this.metadata = new HashMap<>();
}
// 복사 생성자
private Document(Document source) {
this.title = source.title;
this.content = source.content;
// 깊은 복사
this.authors = new ArrayList<>(source.authors);
this.metadata = new HashMap<>(source.metadata);
}
@Override
public Document clone() {
return new Document(this);
}
// Getters and Setters...
}
// 3. 사용
Document original = new Document("제목", "내용");
original.getAuthors().add("작성자1");
Document copy = original.clone();
copy.setTitle("복사본 제목");
copy.getAuthors().add("작성자2");
// original.authors: [작성자1]
// copy.authors: [작성자1, 작성자2]
프로토타입 레지스트리
public class DocumentRegistry {
private final Map<String, Document> prototypes = new HashMap<>();
public void register(String key, Document prototype) {
prototypes.put(key, prototype);
}
public Document create(String key) {
Document prototype = prototypes.get(key);
if (prototype == null) {
throw new IllegalArgumentException("Unknown prototype: " + key);
}
return prototype.clone();
}
}
// 사용
DocumentRegistry registry = new DocumentRegistry();
// 템플릿 등록
Document reportTemplate = new Document("월간 보고서", "");
reportTemplate.getMetadata().put("type", "report");
registry.register("monthly-report", reportTemplate);
// 복제하여 사용
Document januaryReport = registry.create("monthly-report");
januaryReport.setTitle("2024년 1월 보고서");
얕은 복사 vs 깊은 복사
public class ShallowVsDeep {
// 얕은 복사 - 참조만 복사
public static Document shallowCopy(Document source) {
Document copy = new Document(source.getTitle(), source.getContent());
copy.setAuthors(source.getAuthors()); // 같은 List 참조
return copy;
}
// 깊은 복사 - 객체 전체 복사
public static Document deepCopy(Document source) {
Document copy = new Document(source.getTitle(), source.getContent());
copy.setAuthors(new ArrayList<>(source.getAuthors())); // 새 List 생성
// 중첩 객체도 복사
Map<String, Object> metaCopy = new HashMap<>();
for (Map.Entry<String, Object> entry : source.getMetadata().entrySet()) {
// 값이 컬렉션이면 그것도 복사
if (entry.getValue() instanceof List) {
metaCopy.put(entry.getKey(), new ArrayList<>((List<?>) entry.getValue()));
} else {
metaCopy.put(entry.getKey(), entry.getValue());
}
}
copy.setMetadata(metaCopy);
return copy;
}
}
구조 패턴 (Structural Patterns)
클래스나 객체를 조합하여 더 큰 구조를 만드는 방법을 다룬다.
Adapter (어댑터)
호환되지 않는 인터페이스를 가진 클래스들이 함께 동작할 수 있게 한다.
문제 상황
// 기존 시스템의 인터페이스
public interface MediaPlayer {
void play(String audioType, String fileName);
}
// 새로 도입하려는 외부 라이브러리
public class VlcPlayer {
public void playVlc(String fileName) {
System.out.println("Playing vlc file: " + fileName);
}
}
public class Mp4Player {
public void playMp4(String fileName) {
System.out.println("Playing mp4 file: " + fileName);
}
}
어댑터 적용 (Object Adapter)
// 고급 미디어 플레이어 인터페이스
public interface AdvancedMediaPlayer {
void playAdvanced(String fileName);
}
// VLC 어댑터
public class VlcPlayerAdapter implements AdvancedMediaPlayer {
private final VlcPlayer vlcPlayer;
public VlcPlayerAdapter() {
this.vlcPlayer = new VlcPlayer();
}
@Override
public void playAdvanced(String fileName) {
vlcPlayer.playVlc(fileName);
}
}
// MP4 어댑터
public class Mp4PlayerAdapter implements AdvancedMediaPlayer {
private final Mp4Player mp4Player;
public Mp4PlayerAdapter() {
this.mp4Player = new Mp4Player();
}
@Override
public void playAdvanced(String fileName) {
mp4Player.playMp4(fileName);
}
}
// 통합 미디어 플레이어
public class AudioPlayer implements MediaPlayer {
@Override
public void play(String audioType, String fileName) {
AdvancedMediaPlayer player = switch (audioType.toLowerCase()) {
case "vlc" -> new VlcPlayerAdapter();
case "mp4" -> new Mp4PlayerAdapter();
default -> throw new IllegalArgumentException("Unsupported format: " + audioType);
};
player.playAdvanced(fileName);
}
}
Class Adapter (상속 방식)
// 다중 상속이 불가능한 Java에서는 인터페이스 + 상속 조합
public class VlcClassAdapter extends VlcPlayer implements AdvancedMediaPlayer {
@Override
public void playAdvanced(String fileName) {
// 부모 클래스의 메서드 호출
super.playVlc(fileName);
}
}
실무 예시: Spring HandlerAdapter
// Spring MVC의 HandlerAdapter
public interface HandlerAdapter {
boolean supports(Object handler);
ModelAndView handle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception;
}
// 각 핸들러 타입에 맞는 어댑터
public class RequestMappingHandlerAdapter implements HandlerAdapter {
@Override
public boolean supports(Object handler) {
return handler instanceof HandlerMethod;
}
@Override
public ModelAndView handle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
// @RequestMapping 메서드 실행
return invokeHandlerMethod(request, response, (HandlerMethod) handler);
}
}
실무 예시: Enumeration to Iterator
// Java의 Collections.enumeration()과 반대 방향
public class EnumerationIteratorAdapter<T> implements Iterator<T> {
private final Enumeration<T> enumeration;
public EnumerationIteratorAdapter(Enumeration<T> enumeration) {
this.enumeration = enumeration;
}
@Override
public boolean hasNext() {
return enumeration.hasMoreElements();
}
@Override
public T next() {
return enumeration.nextElement();
}
}
Bridge (브리지)
구현에서 추상을 분리하여 독립적으로 변형할 수 있게 한다.
문제 상황
상속으로 확장하면 조합 폭발:
- Shape: Circle, Square, Triangle
- Color: Red, Blue, Green
상속: RedCircle, BlueCircle, GreenCircle, RedSquare, BlueSquare...
→ 3 x 3 = 9개 클래스
브리지: Shape + Color 조합
→ 3 + 3 = 6개 클래스
브리지 패턴 적용
// 1. Implementor - 구현 계층
public interface Color {
String fill();
}
public class Red implements Color {
@Override
public String fill() {
return "빨간색";
}
}
public class Blue implements Color {
@Override
public String fill() {
return "파란색";
}
}
// 2. Abstraction - 추상 계층
public abstract class Shape {
protected Color color; // Bridge: 구현을 참조
protected Shape(Color color) {
this.color = color;
}
public abstract void draw();
}
// 3. Refined Abstraction
public class Circle extends Shape {
private final int radius;
public Circle(int radius, Color color) {
super(color);
this.radius = radius;
}
@Override
public void draw() {
System.out.println(color.fill() + " 원 (반지름: " + radius + ")");
}
}
public class Square extends Shape {
private final int side;
public Square(int side, Color color) {
super(color);
this.side = side;
}
@Override
public void draw() {
System.out.println(color.fill() + " 사각형 (변: " + side + ")");
}
}
// 4. 사용
Shape redCircle = new Circle(10, new Red());
Shape blueSquare = new Square(20, new Blue());
redCircle.draw(); // 빨간색 원 (반지름: 10)
blueSquare.draw(); // 파란색 사각형 (변: 20)
실무 예시: 리모컨과 디바이스
// Device 구현
public interface Device {
void turnOn();
void turnOff();
void setVolume(int volume);
int getVolume();
}
public class TV implements Device {
private boolean on = false;
private int volume = 30;
@Override
public void turnOn() { on = true; }
@Override
public void turnOff() { on = false; }
@Override
public void setVolume(int volume) { this.volume = volume; }
@Override
public int getVolume() { return volume; }
}
public class Radio implements Device {
// 비슷한 구현...
}
// Remote 추상화
public abstract class Remote {
protected Device device;
protected Remote(Device device) {
this.device = device;
}
public void togglePower() {
// device 상태에 따라 on/off
}
public void volumeUp() {
device.setVolume(device.getVolume() + 10);
}
public void volumeDown() {
device.setVolume(device.getVolume() - 10);
}
}
// 고급 리모컨
public class AdvancedRemote extends Remote {
public AdvancedRemote(Device device) {
super(device);
}
public void mute() {
device.setVolume(0);
}
}
// 사용
Device tv = new TV();
Remote remote = new AdvancedRemote(tv);
remote.volumeUp();
((AdvancedRemote) remote).mute();
실무 예시: JDBC Driver
// DriverManager(추상) + Driver(구현)의 브리지 구조
// 추상: java.sql.Connection, Statement, ResultSet
// 구현: MySQL Driver, Oracle Driver, PostgreSQL Driver
// 동일한 JDBC API로 다양한 DB 사용
Connection mysqlConn = DriverManager.getConnection("jdbc:mysql://...");
Connection oracleConn = DriverManager.getConnection("jdbc:oracle://...");
Composite (컴포지트)
객체들을 트리 구조로 구성하여 개별 객체와 복합 객체를 동일하게 다룬다.
구현
// 1. Component
public interface FileSystemComponent {
String getName();
long getSize();
void print(String prefix);
}
// 2. Leaf
public class File implements FileSystemComponent {
private final String name;
private final long size;
public File(String name, long size) {
this.name = name;
this.size = size;
}
@Override
public String getName() { return name; }
@Override
public long getSize() { return size; }
@Override
public void print(String prefix) {
System.out.println(prefix + "📄 " + name + " (" + size + " bytes)");
}
}
// 3. Composite
public class Directory implements FileSystemComponent {
private final String name;
private final List<FileSystemComponent> children = new ArrayList<>();
public Directory(String name) {
this.name = name;
}
public void add(FileSystemComponent component) {
children.add(component);
}
public void remove(FileSystemComponent component) {
children.remove(component);
}
@Override
public String getName() { return name; }
@Override
public long getSize() {
return children.stream()
.mapToLong(FileSystemComponent::getSize)
.sum();
}
@Override
public void print(String prefix) {
System.out.println(prefix + "📁 " + name + " (" + getSize() + " bytes)");
for (FileSystemComponent child : children) {
child.print(prefix + " ");
}
}
}
// 4. 사용
Directory root = new Directory("root");
Directory src = new Directory("src");
Directory test = new Directory("test");
src.add(new File("Main.java", 1000));
src.add(new File("Utils.java", 500));
test.add(new File("MainTest.java", 800));
root.add(src);
root.add(test);
root.add(new File("README.md", 200));
root.print("");
// 출력:
// 📁 root (2500 bytes)
// 📁 src (1500 bytes)
// 📄 Main.java (1000 bytes)
// 📄 Utils.java (500 bytes)
// 📁 test (800 bytes)
// 📄 MainTest.java (800 bytes)
// 📄 README.md (200 bytes)
실무 예시: 조직도
public interface OrganizationComponent {
String getName();
void showDetails();
int getHeadCount();
}
public class Employee implements OrganizationComponent {
private final String name;
private final String position;
// Leaf 구현...
@Override
public int getHeadCount() { return 1; }
}
public class Department implements OrganizationComponent {
private final String name;
private final List<OrganizationComponent> members = new ArrayList<>();
// Composite 구현...
@Override
public int getHeadCount() {
return members.stream()
.mapToInt(OrganizationComponent::getHeadCount)
.sum();
}
}
Decorator (데코레이터)
객체에 동적으로 새로운 책임을 추가한다. 상속 없이 기능을 확장할 수 있다.
상속 vs 데코레이터
상속의 문제:
- 컴파일 타임에 고정
- 조합 폭발 (MilkCoffee, SugarCoffee, MilkSugarCoffee...)
- 단일 상속 제한
데코레이터의 장점:
- 런타임에 동적 조합
- 단일 책임 원칙 준수
- 기존 코드 변경 없이 확장
구현
// 1. Component
public interface Coffee {
String getDescription();
double getCost();
}
// 2. Concrete Component
public class Espresso implements Coffee {
@Override
public String getDescription() { return "에스프레소"; }
@Override
public double getCost() { return 2000; }
}
public class Americano implements Coffee {
@Override
public String getDescription() { return "아메리카노"; }
@Override
public double getCost() { return 3000; }
}
// 3. Decorator (추상)
public abstract class CoffeeDecorator implements Coffee {
protected final Coffee coffee;
protected CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
@Override
public String getDescription() {
return coffee.getDescription();
}
@Override
public double getCost() {
return coffee.getCost();
}
}
// 4. Concrete Decorators
public class Milk extends CoffeeDecorator {
public Milk(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", 우유";
}
@Override
public double getCost() {
return coffee.getCost() + 500;
}
}
public class Whip extends CoffeeDecorator {
public Whip(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", 휘핑크림";
}
@Override
public double getCost() {
return coffee.getCost() + 700;
}
}
public class Shot extends CoffeeDecorator {
public Shot(Coffee coffee) {
super(coffee);
}
@Override
public String getDescription() {
return coffee.getDescription() + ", 샷 추가";
}
@Override
public double getCost() {
return coffee.getCost() + 500;
}
}
// 5. 사용
Coffee order = new Espresso();
order = new Milk(order);
order = new Whip(order);
order = new Shot(order);
System.out.println(order.getDescription()); // 에스프레소, 우유, 휘핑크림, 샷 추가
System.out.println(order.getCost()); // 3700.0
// Fluent 방식
Coffee order2 = new Shot(new Whip(new Milk(new Americano())));
실무 예시: Java I/O
// Java I/O는 데코레이터 패턴의 대표적 예
InputStream is = new FileInputStream("file.txt");
// BufferedInputStream이 FileInputStream을 데코레이트
InputStream buffered = new BufferedInputStream(is);
// DataInputStream이 BufferedInputStream을 데코레이트
DataInputStream data = new DataInputStream(buffered);
// 한 줄로 체이닝
DataInputStream stream = new DataInputStream(
new BufferedInputStream(
new FileInputStream("file.txt")
)
);
실무 예시: Spring의 HandlerInterceptor
// 인터셉터들이 핸들러를 데코레이트하는 것처럼 동작
public class LoggingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) {
log.info("Request: {} {}", request.getMethod(), request.getRequestURI());
return true;
}
}
Facade (퍼사드)
서브시스템에 대한 통합된 인터페이스를 제공하여 서브시스템을 더 쉽게 사용할 수 있게 한다.
구현
// 복잡한 서브시스템들
public class VideoFile {
private final String name;
private final String codecType;
public VideoFile(String name) {
this.name = name;
this.codecType = name.substring(name.lastIndexOf('.') + 1);
}
// ...
}
public class CodecFactory {
public static Codec extract(VideoFile file) {
String type = file.getCodecType();
if (type.equals("mp4")) {
return new MPEG4CompressionCodec();
} else {
return new OggCompressionCodec();
}
}
}
public class BitrateReader {
public static VideoFile read(VideoFile file, Codec codec) {
System.out.println("BitrateReader: 파일 읽기...");
return file;
}
public static VideoFile convert(VideoFile buffer, Codec codec) {
System.out.println("BitrateReader: 변환 중...");
return buffer;
}
}
public class AudioMixer {
public File fix(VideoFile result) {
System.out.println("AudioMixer: 오디오 믹싱...");
return new File("");
}
}
// Facade
public class VideoConversionFacade {
public File convertVideo(String fileName, String format) {
System.out.println("VideoConversionFacade: 변환 시작");
VideoFile file = new VideoFile(fileName);
Codec sourceCodec = CodecFactory.extract(file);
Codec destinationCodec;
if (format.equals("mp4")) {
destinationCodec = new MPEG4CompressionCodec();
} else {
destinationCodec = new OggCompressionCodec();
}
VideoFile buffer = BitrateReader.read(file, sourceCodec);
VideoFile intermediateResult = BitrateReader.convert(buffer, destinationCodec);
File result = new AudioMixer().fix(intermediateResult);
System.out.println("VideoConversionFacade: 변환 완료");
return result;
}
}
// 클라이언트 코드 - 매우 단순해짐
public class Client {
public static void main(String[] args) {
VideoConversionFacade converter = new VideoConversionFacade();
File mp4 = converter.convertVideo("video.ogg", "mp4");
}
}
실무 예시: SLF4J
// SLF4J는 다양한 로깅 프레임워크에 대한 Facade
// Log4j, Logback, JUL 등 실제 구현을 감춤
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyService {
private static final Logger log = LoggerFactory.getLogger(MyService.class);
public void doSomething() {
log.info("작업 시작"); // 내부적으로 어떤 로거가 쓰이는지 모름
}
}
실무 예시: Spring의 JdbcTemplate
// JdbcTemplate은 JDBC의 복잡함을 숨기는 Facade
@Repository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public User findById(Long id) {
// Connection 획득, PreparedStatement 생성, ResultSet 처리,
// 예외 변환, 자원 해제 등을 모두 JdbcTemplate이 처리
return jdbcTemplate.queryForObject(
"SELECT * FROM users WHERE id = ?",
(rs, rowNum) -> new User(
rs.getLong("id"),
rs.getString("name")
),
id
);
}
}
Flyweight (플라이웨이트)
많은 수의 유사한 객체를 효율적으로 공유하여 메모리 사용을 최소화한다.
개념
Intrinsic State (내재 상태):
- 객체 간에 공유되는 불변 데이터
- Flyweight 객체 내부에 저장
Extrinsic State (외재 상태):
- 각 객체마다 다른 컨텍스트 의존 데이터
- 클라이언트가 전달
구현
// 1. Flyweight - 공유되는 객체
public class TreeType {
private final String name;
private final String color;
private final String texture; // 용량이 큰 텍스처 데이터
public TreeType(String name, String color, String texture) {
this.name = name;
this.color = color;
this.texture = texture;
}
public void draw(int x, int y) {
System.out.printf("%s 나무를 (%d, %d)에 그립니다. 색상: %s%n",
name, x, y, color);
}
}
// 2. Flyweight Factory
public class TreeFactory {
private static final Map<String, TreeType> treeTypes = new HashMap<>();
public static TreeType getTreeType(String name, String color, String texture) {
String key = name + "_" + color + "_" + texture;
if (!treeTypes.containsKey(key)) {
treeTypes.put(key, new TreeType(name, color, texture));
System.out.println("새로운 TreeType 생성: " + key);
}
return treeTypes.get(key);
}
public static int getTypeCount() {
return treeTypes.size();
}
}
// 3. Context - 외재 상태를 가진 객체
public class Tree {
private final int x;
private final int y;
private final TreeType type; // Flyweight 참조
public Tree(int x, int y, TreeType type) {
this.x = x;
this.y = y;
this.type = type;
}
public void draw() {
type.draw(x, y);
}
}
// 4. 숲 (클라이언트)
public class Forest {
private final List<Tree> trees = new ArrayList<>();
public void plantTree(int x, int y, String name, String color, String texture) {
TreeType type = TreeFactory.getTreeType(name, color, texture);
Tree tree = new Tree(x, y, type);
trees.add(tree);
}
public void draw() {
for (Tree tree : trees) {
tree.draw();
}
}
}
// 5. 사용
Forest forest = new Forest();
// 100만 그루의 나무를 심어도 TreeType은 몇 개만 생성
for (int i = 0; i < 1_000_000; i++) {
int x = (int) (Math.random() * 1000);
int y = (int) (Math.random() * 1000);
// 나무 종류는 3가지뿐
String[] names = {"소나무", "참나무", "단풍나무"};
String[] colors = {"초록", "진초록", "빨강"};
int typeIndex = i % 3;
forest.plantTree(x, y, names[typeIndex], colors[typeIndex], "texture_" + typeIndex);
}
System.out.println("TreeType 개수: " + TreeFactory.getTypeCount()); // 3
// 100만 Tree 객체가 3개의 TreeType만 공유
실무 예시: String Pool
// Java의 String Pool이 Flyweight 패턴
String s1 = "hello"; // Pool에서 가져옴
String s2 = "hello"; // 같은 객체 참조
System.out.println(s1 == s2); // true
String s3 = new String("hello"); // 새 객체 생성
System.out.println(s1 == s3); // false
String s4 = s3.intern(); // Pool로 이동
System.out.println(s1 == s4); // true
실무 예시: Integer Cache
// Integer -128 ~ 127은 캐시됨
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false
Proxy (프록시)
다른 객체에 대한 대리자를 제공하여 접근을 제어한다.
프록시 종류
| 종류 | 목적 |
|---|---|
| Virtual Proxy | 지연 로딩 (비용이 큰 객체) |
| Protection Proxy | 접근 권한 제어 |
| Remote Proxy | 원격 객체 접근 |
| Caching Proxy | 결과 캐싱 |
| Logging Proxy | 로깅 추가 |
Virtual Proxy (지연 로딩)
// 1. Subject
public interface Image {
void display();
}
// 2. Real Subject
public class HighResolutionImage implements Image {
private final String filename;
private byte[] imageData;
public HighResolutionImage(String filename) {
this.filename = filename;
loadFromDisk(); // 무거운 작업
}
private void loadFromDisk() {
System.out.println("Loading image: " + filename);
// 실제로는 파일 읽기, 디코딩 등 시간이 걸림
try { Thread.sleep(2000); } catch (InterruptedException e) {}
this.imageData = new byte[1024 * 1024]; // 1MB 이미지 데이터
}
@Override
public void display() {
System.out.println("Displaying: " + filename);
}
}
// 3. Proxy
public class ImageProxy implements Image {
private final String filename;
private HighResolutionImage realImage; // 지연 생성
public ImageProxy(String filename) {
this.filename = filename;
// 실제 이미지는 아직 로딩하지 않음
}
@Override
public void display() {
if (realImage == null) {
realImage = new HighResolutionImage(filename);
}
realImage.display();
}
}
// 4. 사용
Image image1 = new ImageProxy("photo1.jpg");
Image image2 = new ImageProxy("photo2.jpg");
Image image3 = new ImageProxy("photo3.jpg");
// 아직 아무것도 로딩되지 않음
System.out.println("Images created");
// 이 시점에만 photo1.jpg 로딩
image1.display();
Protection Proxy (접근 제어)
public interface Document {
void read();
void write(String content);
}
public class SecureDocument implements Document {
private String content;
@Override
public void read() {
System.out.println("Content: " + content);
}
@Override
public void write(String content) {
this.content = content;
}
}
public class DocumentProxy implements Document {
private final SecureDocument document;
private final String currentUser;
private final Set<String> admins = Set.of("admin", "manager");
public DocumentProxy(SecureDocument document, String currentUser) {
this.document = document;
this.currentUser = currentUser;
}
@Override
public void read() {
document.read(); // 읽기는 모두 허용
}
@Override
public void write(String content) {
if (admins.contains(currentUser)) {
document.write(content);
} else {
throw new SecurityException("권한이 없습니다: " + currentUser);
}
}
}
Caching Proxy
public interface UserService {
User findById(Long id);
}
public class UserServiceImpl implements UserService {
@Override
public User findById(Long id) {
System.out.println("DB 조회: " + id);
// 실제 DB 조회
return new User(id, "User " + id);
}
}
public class CachingUserServiceProxy implements UserService {
private final UserService target;
private final Map<Long, User> cache = new ConcurrentHashMap<>();
public CachingUserServiceProxy(UserService target) {
this.target = target;
}
@Override
public User findById(Long id) {
return cache.computeIfAbsent(id, target::findById);
}
public void evict(Long id) {
cache.remove(id);
}
}
실무 예시: Spring AOP Proxy
// Spring은 @Transactional, @Cacheable 등을 프록시로 구현
@Service
public class OrderService {
@Transactional // 프록시가 트랜잭션 관리
public void createOrder(Order order) {
// 비즈니스 로직
}
@Cacheable("users") // 프록시가 캐시 관리
public User findUser(Long id) {
return userRepository.findById(id);
}
}
// Spring이 생성하는 프록시 (개념적)
public class OrderService$$Proxy extends OrderService {
@Override
public void createOrder(Order order) {
TransactionStatus tx = txManager.getTransaction();
try {
super.createOrder(order);
txManager.commit(tx);
} catch (Exception e) {
txManager.rollback(tx);
throw e;
}
}
}
JDK Dynamic Proxy
public class LoggingInvocationHandler implements InvocationHandler {
private final Object target;
public LoggingInvocationHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before: " + method.getName());
Object result = method.invoke(target, args);
System.out.println("After: " + method.getName());
return result;
}
}
// 프록시 생성
UserService proxy = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class<?>[] { UserService.class },
new LoggingInvocationHandler(new UserServiceImpl())
);
proxy.findById(1L);
// Before: findById
// DB 조회: 1
// After: findById
행동 패턴 (Behavioral Patterns)
객체 간의 책임 분배와 알고리즘을 다룬다.
Strategy (전략)
알고리즘을 캡슐화하여 런타임에 교체할 수 있게 한다.
구현
// 1. Strategy 인터페이스
@FunctionalInterface
public interface PaymentStrategy {
void pay(int amount);
}
// 2. Concrete Strategies
public class CreditCardPayment implements PaymentStrategy {
private final String cardNumber;
public CreditCardPayment(String cardNumber) {
this.cardNumber = cardNumber;
}
@Override
public void pay(int amount) {
System.out.println(amount + "원을 신용카드로 결제 (카드번호: " + cardNumber + ")");
}
}
public class KakaoPayPayment implements PaymentStrategy {
private final String phoneNumber;
public KakaoPayPayment(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
@Override
public void pay(int amount) {
System.out.println(amount + "원을 카카오페이로 결제 (전화번호: " + phoneNumber + ")");
}
}
public class NaverPayPayment implements PaymentStrategy {
@Override
public void pay(int amount) {
System.out.println(amount + "원을 네이버페이로 결제");
}
}
// 3. Context
public class ShoppingCart {
private final List<Item> items = new ArrayList<>();
public void addItem(Item item) {
items.add(item);
}
public int calculateTotal() {
return items.stream()
.mapToInt(Item::getPrice)
.sum();
}
public void checkout(PaymentStrategy paymentStrategy) {
int total = calculateTotal();
paymentStrategy.pay(total);
}
}
// 4. 사용
ShoppingCart cart = new ShoppingCart();
cart.addItem(new Item("키보드", 50000));
cart.addItem(new Item("마우스", 30000));
// 결제 방식 선택
cart.checkout(new CreditCardPayment("1234-5678-9012-3456"));
cart.checkout(new KakaoPayPayment("010-1234-5678"));
// 람다로 간단히
cart.checkout(amount -> System.out.println("포인트로 " + amount + "원 결제"));
실무 예시: 정렬 전략
public class SortingContext<T> {
private Comparator<T> strategy;
public void setStrategy(Comparator<T> strategy) {
this.strategy = strategy;
}
public List<T> sort(List<T> list) {
List<T> result = new ArrayList<>(list);
result.sort(strategy);
return result;
}
}
// 사용
List<Product> products = getProducts();
SortingContext<Product> context = new SortingContext<>();
// 가격순
context.setStrategy(Comparator.comparing(Product::getPrice));
List<Product> byPrice = context.sort(products);
// 이름순
context.setStrategy(Comparator.comparing(Product::getName));
List<Product> byName = context.sort(products);
// 복합 정렬
context.setStrategy(
Comparator.comparing(Product::getCategory)
.thenComparing(Product::getPrice)
);
Spring에서의 Strategy
// 인터페이스로 전략 정의
public interface DiscountPolicy {
int calculateDiscount(Order order);
}
@Component("fixedDiscount")
public class FixedDiscountPolicy implements DiscountPolicy {
@Override
public int calculateDiscount(Order order) {
return 1000;
}
}
@Component("rateDiscount")
public class RateDiscountPolicy implements DiscountPolicy {
@Override
public int calculateDiscount(Order order) {
return order.getTotal() * 10 / 100;
}
}
// 전략 주입
@Service
public class OrderService {
private final DiscountPolicy discountPolicy;
public OrderService(@Qualifier("rateDiscount") DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
}
// 또는 Map으로 모든 전략 주입
@Service
public class OrderService {
private final Map<String, DiscountPolicy> policies;
public OrderService(Map<String, DiscountPolicy> policies) {
this.policies = policies;
}
public int discount(String policyName, Order order) {
return policies.get(policyName).calculateDiscount(order);
}
}
Template Method (템플릿 메서드)
알고리즘의 골격을 정의하고, 일부 단계를 서브클래스에서 구현하게 한다.
구현
// 1. Abstract Class with Template Method
public abstract class DataParser {
// 템플릿 메서드 - final로 알고리즘 골격 보호
public final List<Record> parse(String filePath) {
String rawData = readFile(filePath);
String cleanedData = cleanData(rawData);
List<Record> records = parseData(cleanedData);
records = processRecords(records); // Hook
logResult(records);
return records;
}
// 공통 구현
private String readFile(String filePath) {
System.out.println("파일 읽기: " + filePath);
// 파일 읽기 로직
return "raw data";
}
// 추상 메서드 - 서브클래스에서 구현
protected abstract String cleanData(String rawData);
protected abstract List<Record> parseData(String data);
// Hook - 선택적 오버라이드
protected List<Record> processRecords(List<Record> records) {
return records; // 기본: 아무것도 안 함
}
// 공통 구현
private void logResult(List<Record> records) {
System.out.println("처리 완료: " + records.size() + "건");
}
}
// 2. Concrete Classes
public class CsvParser extends DataParser {
@Override
protected String cleanData(String rawData) {
return rawData.trim().replaceAll("\\s+", " ");
}
@Override
protected List<Record> parseData(String data) {
List<Record> records = new ArrayList<>();
String[] lines = data.split("\n");
for (String line : lines) {
String[] fields = line.split(",");
records.add(new Record(fields));
}
return records;
}
}
public class JsonParser extends DataParser {
@Override
protected String cleanData(String rawData) {
return rawData.strip();
}
@Override
protected List<Record> parseData(String data) {
// JSON 파싱 로직
ObjectMapper mapper = new ObjectMapper();
return mapper.readValue(data, new TypeReference<>() {});
}
@Override
protected List<Record> processRecords(List<Record> records) {
// JSON 특화 처리
return records.stream()
.filter(Record::isValid)
.collect(Collectors.toList());
}
}
// 3. 사용
DataParser csvParser = new CsvParser();
csvParser.parse("data.csv");
DataParser jsonParser = new JsonParser();
jsonParser.parse("data.json");
실무 예시: Spring의 JdbcTemplate
// JdbcTemplate 내부 구현 (개념적)
public abstract class JdbcTemplate {
public <T> T execute(ConnectionCallback<T> action) {
Connection con = DataSourceUtils.getConnection(getDataSource());
try {
return action.doInConnection(con); // 콜백
} catch (SQLException ex) {
throw translateException(ex);
} finally {
DataSourceUtils.releaseConnection(con, getDataSource());
}
}
public <T> T query(String sql, ResultSetExtractor<T> rse) {
return execute((Connection con) -> {
try (Statement stmt = con.createStatement();
ResultSet rs = stmt.executeQuery(sql)) {
return rse.extractData(rs); // 서브클래스/콜백에서 구현
}
});
}
}
Strategy vs Template Method
Template Method:
- 상속 기반
- 알고리즘 골격 고정, 일부 단계만 변경
- 컴파일 타임에 결정
Strategy:
- 구성(Composition) 기반
- 전체 알고리즘을 교체
- 런타임에 변경 가능
Command (커맨드)
요청을 객체로 캡슐화하여 파라미터화, 큐잉, 로깅, 취소 기능을 제공한다.
구현
// 1. Command 인터페이스
public interface Command {
void execute();
void undo(); // 실행 취소
}
// 2. Receiver
public class TextEditor {
private StringBuilder content = new StringBuilder();
public void write(String text) {
content.append(text);
}
public void delete(int length) {
if (length > content.length()) {
length = content.length();
}
content.delete(content.length() - length, content.length());
}
public String getContent() {
return content.toString();
}
}
// 3. Concrete Commands
public class WriteCommand implements Command {
private final TextEditor editor;
private final String text;
public WriteCommand(TextEditor editor, String text) {
this.editor = editor;
this.text = text;
}
@Override
public void execute() {
editor.write(text);
}
@Override
public void undo() {
editor.delete(text.length());
}
}
public class DeleteCommand implements Command {
private final TextEditor editor;
private final int length;
private String deletedText;
public DeleteCommand(TextEditor editor, int length) {
this.editor = editor;
this.length = length;
}
@Override
public void execute() {
String content = editor.getContent();
deletedText = content.substring(
Math.max(0, content.length() - length)
);
editor.delete(length);
}
@Override
public void undo() {
editor.write(deletedText);
}
}
// 4. Invoker
public class CommandHistory {
private final Deque<Command> history = new ArrayDeque<>();
private final Deque<Command> redoStack = new ArrayDeque<>();
public void execute(Command command) {
command.execute();
history.push(command);
redoStack.clear(); // 새 명령 실행 시 redo 스택 초기화
}
public void undo() {
if (!history.isEmpty()) {
Command command = history.pop();
command.undo();
redoStack.push(command);
}
}
public void redo() {
if (!redoStack.isEmpty()) {
Command command = redoStack.pop();
command.execute();
history.push(command);
}
}
}
// 5. 사용
TextEditor editor = new TextEditor();
CommandHistory history = new CommandHistory();
history.execute(new WriteCommand(editor, "Hello "));
history.execute(new WriteCommand(editor, "World"));
System.out.println(editor.getContent()); // Hello World
history.undo();
System.out.println(editor.getContent()); // Hello
history.redo();
System.out.println(editor.getContent()); // Hello World
실무 예시: 트랜잭션 스크립트
public interface TransactionCommand {
void execute() throws Exception;
void rollback();
}
public class CreateOrderCommand implements TransactionCommand {
private final OrderService orderService;
private final Order order;
private Long createdOrderId;
@Override
public void execute() throws Exception {
createdOrderId = orderService.create(order);
}
@Override
public void rollback() {
if (createdOrderId != null) {
orderService.delete(createdOrderId);
}
}
}
// 여러 커맨드를 트랜잭션으로 묶기
public class TransactionExecutor {
public void executeAll(List<TransactionCommand> commands) {
List<TransactionCommand> executed = new ArrayList<>();
try {
for (TransactionCommand command : commands) {
command.execute();
executed.add(command);
}
} catch (Exception e) {
// 역순으로 롤백
Collections.reverse(executed);
for (TransactionCommand command : executed) {
command.rollback();
}
throw new RuntimeException("Transaction failed", e);
}
}
}
Observer (옵저버)
객체 상태가 변경되면 의존 객체들에게 자동으로 알린다.
구현
// 1. Subject
public interface Subject {
void attach(Observer observer);
void detach(Observer observer);
void notifyObservers();
}
// 2. Observer
public interface Observer {
void update(String message);
}
// 3. Concrete Subject
public class NewsPublisher implements Subject {
private final List<Observer> observers = new ArrayList<>();
private String latestNews;
@Override
public void attach(Observer observer) {
observers.add(observer);
}
@Override
public void detach(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(latestNews);
}
}
public void publishNews(String news) {
this.latestNews = news;
notifyObservers();
}
}
// 4. Concrete Observers
public class EmailSubscriber implements Observer {
private final String email;
public EmailSubscriber(String email) {
this.email = email;
}
@Override
public void update(String message) {
System.out.println("이메일 발송 to " + email + ": " + message);
}
}
public class PushSubscriber implements Observer {
private final String deviceId;
public PushSubscriber(String deviceId) {
this.deviceId = deviceId;
}
@Override
public void update(String message) {
System.out.println("푸시 알림 to " + deviceId + ": " + message);
}
}
// 5. 사용
NewsPublisher publisher = new NewsPublisher();
publisher.attach(new EmailSubscriber("user@example.com"));
publisher.attach(new PushSubscriber("device-123"));
publisher.publishNews("속보: 새로운 Java 버전 출시!");
Java 내장 기능 활용
// PropertyChangeSupport 활용
public class Stock {
private final PropertyChangeSupport support = new PropertyChangeSupport(this);
private String symbol;
private double price;
public void addPropertyChangeListener(PropertyChangeListener listener) {
support.addPropertyChangeListener(listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
support.removePropertyChangeListener(listener);
}
public void setPrice(double newPrice) {
double oldPrice = this.price;
this.price = newPrice;
support.firePropertyChange("price", oldPrice, newPrice);
}
}
// 사용
Stock apple = new Stock("AAPL", 150.0);
apple.addPropertyChangeListener(event -> {
System.out.println("주가 변동: " + event.getOldValue() + " -> " + event.getNewValue());
});
apple.setPrice(155.0); // 주가 변동: 150.0 -> 155.0
실무 예시: Spring ApplicationEvent
// 1. 이벤트 정의
public class OrderCreatedEvent extends ApplicationEvent {
private final Order order;
public OrderCreatedEvent(Object source, Order order) {
super(source);
this.order = order;
}
public Order getOrder() { return order; }
}
// 2. 이벤트 발행
@Service
public class OrderService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public Order createOrder(OrderRequest request) {
Order order = orderRepository.save(new Order(request));
eventPublisher.publishEvent(new OrderCreatedEvent(this, order));
return order;
}
}
// 3. 이벤트 리스너
@Component
public class OrderEventListener {
@EventListener
public void handleOrderCreated(OrderCreatedEvent event) {
System.out.println("주문 생성됨: " + event.getOrder().getId());
}
@Async
@EventListener
public void sendNotification(OrderCreatedEvent event) {
// 비동기 알림 발송
notificationService.send(event.getOrder().getCustomerEmail());
}
}
State (상태)
객체의 내부 상태에 따라 행동을 변경한다. 상태를 객체로 표현한다.
구현
// 1. State 인터페이스
public interface OrderState {
void next(Order order);
void prev(Order order);
void printStatus();
}
// 2. Concrete States
public class OrderedState implements OrderState {
@Override
public void next(Order order) {
order.setState(new ShippedState());
}
@Override
public void prev(Order order) {
System.out.println("주문 상태가 가장 처음입니다.");
}
@Override
public void printStatus() {
System.out.println("주문 완료 - 배송 준비 중");
}
}
public class ShippedState implements OrderState {
@Override
public void next(Order order) {
order.setState(new DeliveredState());
}
@Override
public void prev(Order order) {
order.setState(new OrderedState());
}
@Override
public void printStatus() {
System.out.println("배송 중");
}
}
public class DeliveredState implements OrderState {
@Override
public void next(Order order) {
System.out.println("이미 배송 완료되었습니다.");
}
@Override
public void prev(Order order) {
order.setState(new ShippedState());
}
@Override
public void printStatus() {
System.out.println("배송 완료");
}
}
// 3. Context
public class Order {
private OrderState state = new OrderedState();
public void setState(OrderState state) {
this.state = state;
}
public void nextState() {
state.next(this);
}
public void prevState() {
state.prev(this);
}
public void printStatus() {
state.printStatus();
}
}
// 4. 사용
Order order = new Order();
order.printStatus(); // 주문 완료 - 배송 준비 중
order.nextState();
order.printStatus(); // 배송 중
order.nextState();
order.printStatus(); // 배송 완료
order.nextState(); // 이미 배송 완료되었습니다.
State vs Strategy
State:
- 상태에 따라 행동이 자동으로 변경
- 상태 전이 로직이 상태 객체 내부에 있음
- 클라이언트는 상태 변경을 직접 하지 않음
Strategy:
- 클라이언트가 전략을 직접 선택/변경
- 전략 간 의존성 없음
- 알고리즘 선택에 초점
Chain of Responsibility (책임 연쇄)
요청을 처리할 수 있는 기회를 체인의 각 객체에게 순차적으로 제공한다.
구현
// 1. Handler
public abstract class SupportHandler {
protected SupportHandler nextHandler;
public void setNext(SupportHandler handler) {
this.nextHandler = handler;
}
public abstract void handle(SupportTicket ticket);
}
// 2. Concrete Handlers
public class Level1Support extends SupportHandler {
@Override
public void handle(SupportTicket ticket) {
if (ticket.getSeverity() == Severity.LOW) {
System.out.println("Level 1 지원팀에서 처리: " + ticket.getDescription());
} else if (nextHandler != null) {
nextHandler.handle(ticket);
}
}
}
public class Level2Support extends SupportHandler {
@Override
public void handle(SupportTicket ticket) {
if (ticket.getSeverity() == Severity.MEDIUM) {
System.out.println("Level 2 지원팀에서 처리: " + ticket.getDescription());
} else if (nextHandler != null) {
nextHandler.handle(ticket);
}
}
}
public class Level3Support extends SupportHandler {
@Override
public void handle(SupportTicket ticket) {
if (ticket.getSeverity() == Severity.HIGH) {
System.out.println("Level 3 지원팀(엔지니어)에서 처리: " + ticket.getDescription());
} else if (nextHandler != null) {
nextHandler.handle(ticket);
} else {
System.out.println("처리할 수 없는 티켓: " + ticket.getDescription());
}
}
}
// 3. 체인 구성 및 사용
SupportHandler level1 = new Level1Support();
SupportHandler level2 = new Level2Support();
SupportHandler level3 = new Level3Support();
level1.setNext(level2);
level2.setNext(level3);
// 요청 처리
level1.handle(new SupportTicket("비밀번호 재설정", Severity.LOW));
// Level 1 지원팀에서 처리: 비밀번호 재설정
level1.handle(new SupportTicket("결제 오류", Severity.MEDIUM));
// Level 2 지원팀에서 처리: 결제 오류
level1.handle(new SupportTicket("시스템 장애", Severity.HIGH));
// Level 3 지원팀(엔지니어)에서 처리: 시스템 장애
실무 예시: Servlet Filter
public class LoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
System.out.println("Request 로깅");
chain.doFilter(request, response); // 다음 핸들러로
System.out.println("Response 로깅");
}
}
public class AuthenticationFilter implements Filter {
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException {
if (isAuthenticated(request)) {
chain.doFilter(request, response);
} else {
((HttpServletResponse) response).sendError(401);
}
}
}
Mediator (중재자)
객체들 간의 상호작용을 캡슐화하여 객체들이 서로를 직접 참조하지 않게 한다.
구현
// 1. Mediator
public interface ChatMediator {
void sendMessage(String message, User sender);
void addUser(User user);
}
// 2. Concrete Mediator
public class ChatRoom implements ChatMediator {
private final List<User> users = new ArrayList<>();
@Override
public void addUser(User user) {
users.add(user);
}
@Override
public void sendMessage(String message, User sender) {
for (User user : users) {
// 발신자 제외하고 메시지 전달
if (user != sender) {
user.receive(message, sender.getName());
}
}
}
}
// 3. Colleague
public abstract class User {
protected ChatMediator mediator;
protected String name;
public User(ChatMediator mediator, String name) {
this.mediator = mediator;
this.name = name;
}
public String getName() { return name; }
public abstract void send(String message);
public abstract void receive(String message, String from);
}
// 4. Concrete Colleagues
public class ChatUser extends User {
public ChatUser(ChatMediator mediator, String name) {
super(mediator, name);
}
@Override
public void send(String message) {
System.out.println(name + " 전송: " + message);
mediator.sendMessage(message, this);
}
@Override
public void receive(String message, String from) {
System.out.println(name + " 수신 [" + from + "]: " + message);
}
}
// 5. 사용
ChatMediator chatRoom = new ChatRoom();
User alice = new ChatUser(chatRoom, "Alice");
User bob = new ChatUser(chatRoom, "Bob");
User charlie = new ChatUser(chatRoom, "Charlie");
chatRoom.addUser(alice);
chatRoom.addUser(bob);
chatRoom.addUser(charlie);
alice.send("안녕하세요!");
// Alice 전송: 안녕하세요!
// Bob 수신 [Alice]: 안녕하세요!
// Charlie 수신 [Alice]: 안녕하세요!
Memento (메멘토)
객체의 상태를 저장하고 복원할 수 있게 한다.
구현
// 1. Memento
public class EditorMemento {
private final String content;
private final int cursorPosition;
private final LocalDateTime savedAt;
public EditorMemento(String content, int cursorPosition) {
this.content = content;
this.cursorPosition = cursorPosition;
this.savedAt = LocalDateTime.now();
}
// Package-private: Originator만 접근 가능
String getContent() { return content; }
int getCursorPosition() { return cursorPosition; }
// Public: 표시용
public LocalDateTime getSavedAt() { return savedAt; }
}
// 2. Originator
public class TextEditor {
private String content = "";
private int cursorPosition = 0;
public void write(String text) {
content = content.substring(0, cursorPosition)
+ text
+ content.substring(cursorPosition);
cursorPosition += text.length();
}
public EditorMemento save() {
return new EditorMemento(content, cursorPosition);
}
public void restore(EditorMemento memento) {
this.content = memento.getContent();
this.cursorPosition = memento.getCursorPosition();
}
public String getContent() { return content; }
}
// 3. Caretaker
public class EditorHistory {
private final Deque<EditorMemento> history = new ArrayDeque<>();
private static final int MAX_HISTORY = 50;
public void save(EditorMemento memento) {
if (history.size() >= MAX_HISTORY) {
history.removeLast();
}
history.push(memento);
}
public EditorMemento undo() {
if (history.isEmpty()) {
return null;
}
return history.pop();
}
public List<EditorMemento> getHistory() {
return new ArrayList<>(history);
}
}
// 4. 사용
TextEditor editor = new TextEditor();
EditorHistory history = new EditorHistory();
editor.write("Hello");
history.save(editor.save());
editor.write(" World");
history.save(editor.save());
editor.write("!");
System.out.println(editor.getContent()); // Hello World!
// Undo
EditorMemento previous = history.undo();
editor.restore(previous);
System.out.println(editor.getContent()); // Hello World
Iterator (반복자)
컬렉션의 내부 구조를 노출하지 않고 요소에 순차적으로 접근한다.
구현
// 1. Iterator 인터페이스
public interface Iterator<T> {
boolean hasNext();
T next();
}
// 2. Aggregate
public interface IterableCollection<T> {
Iterator<T> createIterator();
}
// 3. Concrete Iterator
public class BookIterator implements Iterator<Book> {
private final List<Book> books;
private int position = 0;
public BookIterator(List<Book> books) {
this.books = books;
}
@Override
public boolean hasNext() {
return position < books.size();
}
@Override
public Book next() {
return books.get(position++);
}
}
// 4. Concrete Aggregate
public class BookShelf implements IterableCollection<Book> {
private final List<Book> books = new ArrayList<>();
public void addBook(Book book) {
books.add(book);
}
@Override
public Iterator<Book> createIterator() {
return new BookIterator(books);
}
// 특수 이터레이터
public Iterator<Book> createReverseIterator() {
return new Iterator<>() {
private int position = books.size() - 1;
@Override
public boolean hasNext() { return position >= 0; }
@Override
public Book next() { return books.get(position--); }
};
}
}
// 5. 사용
BookShelf shelf = new BookShelf();
shelf.addBook(new Book("Design Patterns"));
shelf.addBook(new Book("Clean Code"));
shelf.addBook(new Book("Effective Java"));
Iterator<Book> it = shelf.createIterator();
while (it.hasNext()) {
System.out.println(it.next().getTitle());
}
Visitor (방문자)
객체 구조를 변경하지 않고 새로운 연산을 추가한다.
구현
// 1. Element
public interface DocumentElement {
void accept(DocumentVisitor visitor);
}
// 2. Concrete Elements
public class Paragraph implements DocumentElement {
private final String text;
public Paragraph(String text) { this.text = text; }
public String getText() { return text; }
@Override
public void accept(DocumentVisitor visitor) {
visitor.visit(this);
}
}
public class Image implements DocumentElement {
private final String url;
private final int width;
private final int height;
public Image(String url, int width, int height) {
this.url = url;
this.width = width;
this.height = height;
}
// Getters...
@Override
public void accept(DocumentVisitor visitor) {
visitor.visit(this);
}
}
public class Table implements DocumentElement {
private final List<List<String>> data;
public Table(List<List<String>> data) { this.data = data; }
public List<List<String>> getData() { return data; }
@Override
public void accept(DocumentVisitor visitor) {
visitor.visit(this);
}
}
// 3. Visitor
public interface DocumentVisitor {
void visit(Paragraph paragraph);
void visit(Image image);
void visit(Table table);
}
// 4. Concrete Visitors
public class HtmlExportVisitor implements DocumentVisitor {
private final StringBuilder html = new StringBuilder();
@Override
public void visit(Paragraph paragraph) {
html.append("<p>").append(paragraph.getText()).append("</p>\n");
}
@Override
public void visit(Image image) {
html.append(String.format("<img src=\"%s\" width=\"%d\" height=\"%d\" />\n",
image.getUrl(), image.getWidth(), image.getHeight()));
}
@Override
public void visit(Table table) {
html.append("<table>\n");
for (List<String> row : table.getData()) {
html.append(" <tr>");
for (String cell : row) {
html.append("<td>").append(cell).append("</td>");
}
html.append("</tr>\n");
}
html.append("</table>\n");
}
public String getResult() { return html.toString(); }
}
public class MarkdownExportVisitor implements DocumentVisitor {
private final StringBuilder markdown = new StringBuilder();
@Override
public void visit(Paragraph paragraph) {
markdown.append(paragraph.getText()).append("\n\n");
}
@Override
public void visit(Image image) {
markdown.append(String.format("\n\n", image.getUrl()));
}
@Override
public void visit(Table table) {
// Markdown 테이블 형식으로 출력
}
public String getResult() { return markdown.toString(); }
}
// 5. 사용
List<DocumentElement> document = List.of(
new Paragraph("제목입니다"),
new Image("logo.png", 100, 50),
new Table(List.of(
List.of("이름", "나이"),
List.of("홍길동", "30")
))
);
HtmlExportVisitor htmlVisitor = new HtmlExportVisitor();
for (DocumentElement element : document) {
element.accept(htmlVisitor);
}
System.out.println(htmlVisitor.getResult());
Interpreter (인터프리터)
언어의 문법을 클래스로 표현하고 해석한다.
구현
// 간단한 수식 인터프리터
public interface Expression {
int interpret();
}
public class NumberExpression implements Expression {
private final int number;
public NumberExpression(int number) {
this.number = number;
}
@Override
public int interpret() {
return number;
}
}
public class AddExpression implements Expression {
private final Expression left;
private final Expression right;
public AddExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret() {
return left.interpret() + right.interpret();
}
}
public class MultiplyExpression implements Expression {
private final Expression left;
private final Expression right;
public MultiplyExpression(Expression left, Expression right) {
this.left = left;
this.right = right;
}
@Override
public int interpret() {
return left.interpret() * right.interpret();
}
}
// 파서
public class ExpressionParser {
public Expression parse(String expression) {
// "(3 + 5) * 2" -> MultiplyExpression(AddExpression(3, 5), 2)
// 실제로는 토큰화, AST 구축 필요
return null;
}
}
// 사용: (3 + 5) * 2 = 16
Expression expr = new MultiplyExpression(
new AddExpression(
new NumberExpression(3),
new NumberExpression(5)
),
new NumberExpression(2)
);
System.out.println(expr.interpret()); // 16
패턴 선택 가이드
패턴별 사용 시점
| 문제 상황 | 추천 패턴 |
|---|---|
| 객체가 하나만 필요 | Singleton |
| 복잡한 객체 생성 | Builder |
| 객체 생성을 서브클래스에 위임 | Factory Method |
| 관련 객체군 생성 | Abstract Factory |
| 기존 객체 복제 | Prototype |
| 인터페이스 변환 | Adapter |
| 구현과 추상 분리 | Bridge |
| 트리 구조 | Composite |
| 동적 기능 추가 | Decorator |
| 복잡한 서브시스템 단순화 | Facade |
| 메모리 절약 (공유) | Flyweight |
| 접근 제어, 지연 로딩 | Proxy |
| 알고리즘 교체 | Strategy |
| 알고리즘 골격 정의 | Template Method |
| 요청을 객체로 | Command |
| 상태 변경 알림 | Observer |
| 상태에 따른 행동 변경 | State |
| 요청 처리 체인 | Chain of Responsibility |
| 객체 간 통신 중앙화 | Mediator |
| 상태 저장/복원 | Memento |
| 순회 추상화 | Iterator |
| 연산 추가 (구조 변경 없이) | Visitor |
| DSL, 문법 해석 | Interpreter |
패턴 조합
자주 함께 사용되는 패턴:
Factory + Singleton: 팩토리를 싱글턴으로
Abstract Factory + Factory Method: 팩토리 메서드로 제품 생성
Builder + Fluent Interface: 메서드 체이닝
Composite + Iterator: 트리 순회
Decorator + Strategy: 동적 알고리즘 적용
Observer + Mediator: 이벤트 중앙 관리
Command + Memento: Undo/Redo 구현
State + Strategy: 상태별 전략
정리
디자인 패턴은 도구다. 모든 상황에 패턴을 적용하려 하지 말고, 문제가 있을 때 적절한 패턴을 선택해야 한다.
패턴 사용 원칙:
1. 문제 먼저, 패턴은 그 다음
2. 단순한 해결책이 있으면 그것을 사용
3. 패턴은 의사소통 도구
4. 패턴을 위한 패턴은 금물
5. SOLID 원칙과 함께 적용
“Design patterns should be used with caution. Don’t use a pattern just because it exists.” — Gang of Four
댓글