컴퓨터 과학 기초발행일 2025. 12. 29.원본 https://blog.naver.com/jword_/224127095254 ↗

Design Pattern 정리하기

Design Pattern 정리하기 — #DesignPattern #Singleton #FactoryMethod #Builder #Strategy #Observer #TemplateMethod Des...

#Computer Science#Naver Blog

#DesignPattern #Singleton #FactoryMethod #Builder #Strategy #Observer #TemplateMethod

​

Design pattern

소프트웨어 설계에서 자주 발생하는 문제에 대한 재사용 가능한 해결책입니다. 객체지향 프로그래밍에서 사용됩니다.

​

어떤 문제가 발생했고 문제를 해결하기 위해 어떤 사고를 했으며, 어떻게 문제를 해결했는지를 위주로 정리하였습니다.

​

Singleton

문제:

애플리케이션 코드 내에서 데이터 베이스 연결을 여러 곳에서 사용하고 있습니다. DB 연결은 비용이 크고, 하나의 연결로 충분합니다.

​

사고:

여러 개를 만들지 못하게 시스템 자체에서 강제하고, 이미 만든 객체를 재활용할 수 없을까?

​

해결:

java 코드 예제
                                    class DatabaseConnection {
    private static DatabaseConnection instance;

    private DatabaseConnection() {} // 외부에서 new 불가

    public static DatabaseConnection getInstance() {
        if (instance == null) {
            instance = new DatabaseConnection();
        }
        return instance;
    }
 }

// 모든 곳에서 같은 인스턴스 사용
DatabaseConnection db1 = DatabaseConnection.getInstance();
DatabaseConnection db2 = DatabaseConnection.getInstance();

Factory Method

문제:

다음 예시 코드에서 문제가 발생합니다.

java 코드 예제
                                    class NotificationService {
    public void send(String type, String message) {
        if (type.equals("email")) {
            EmailSender sender = new EmailSender();
            sender.send(message)
        } else if (type.equals("sms")) {
            SMSSender sender = new SMSSender();
            sender.send(message);
        } else if (type.equals("push") {
            PushSender sender = new PushSender();
            sender.send(message);
        }
    }
}

Notification의 알림이 증가할 때마다 새로운 코드를 수정해야합니다. 객체지향 원칙인 OCP(Open-Closed Principle)에 따르면 확장에는 최대한 열리도록, 수정에는 최대한 닫히도록 구성하는게 좋습니다.

​

사고:

객체 생성 로직을 분리하면 어떨까 ?

​

해결:

java 코드 예제
                                    interface Notification {
    void send(String message);
}

interface NotificationFactory {
    Notification create();
}

class EmailNotification implements Notification {
    public void send(String message) {
        System.out.println("이메일: " + message);
    }
}

class EmailNotificationFactory implements NotificationFactory {
    public Notification create() {
        return new EmailNotification();
    }
}

class SMSNotification implements Notificaion {
    public void send(String message) {
        System.out.println("SMS: " + message);
    }
}

class SMSNotificationFactory implements NotificationFactory {
    public Notification create() {
        return new SMSNotification();
    }
}

새로운 알림 기능이 추가될 때, 기존 코드의 수정 없이 새로운 객체를 만들어서 사용하면 됩니다.

Builder

문제:

생성자에 파라미터가 너무 많습니다. 파라미터를 하나 하나 기억하는건 불가능에 가깝습니다.\\

​

인스턴스를 생성하기 위해 파라미터를 계속 보고 하나씩 만들어야 하므로 작업량이 너무 많습니다. 보지 않고 만들기에는 실수하기가 쉽습니다.

​

​

사고:

생성 할때 email("example.com")처럼 명시적으로 보여주면 좋겠다고 생각합니다.

​

해결

java 코드 예제
                                    class User {
    private String name;
    private int age;
    private String email;

    private User(Builder builder) {
        this.name = builder.name;
        this.age = builder.age;
        this.email = builder.email;
    }

    public static class Builder {
        private String name;
        private int age;
        private String email;

        public Builder name(String name) {
            this.name = name;
            return this;
        }

        public Builder age(int age) {
            this.age = age;
            return this;
        }

        public Builder email(String email) {
            this.email = email;
            return this;
        }

        public User Build() {
            return new User(this);
        }
    }
}

이제 인스턴스를 생성할 때마다 파라미터 값을 명시적으로 확인할 수 있습니다.

text 코드 예제
                                    User user = new User.Builder()
    .name("myname")
    .age(12)
    .email("example.com")
    .build()

Strategy

문제:

문제는 다음 코드에서 발생합니다.

java 코드 예제
                                    class ShoppingCart {
    public void checkout(String paymentType) {
        if(paymentType.equals("card") {
            // 카드 로직 50줄 ...
        }
        else if (paymentType.equals("kakao")) {
            // 카카오페이 로직 100줄 ...
        }
        else if (paymentType.equals("naver")) {
            // naver pay 30 줄 ...
        }
    }
}

이와 같은 코드 작성 방식은 크게 두가지 문제를 야기합니다.

  1. 결제 수단 추가시 이 클래스를 매번 수정해야한다.
  2. 하나의 클래스에 로직이 너무 많아 비대해진다.
  • 이는 가독성에 매우 불리

​

사고:

  • 결제 방식은 다르지만 "결제한다"는 추상적인 개념은 동일하다.
  • 각 결제 방식을 독립적인 객체로 분리하면 어떨까?
  • 런타임에 결제 방식을 교체할 수 있다면?

​

​

해결

java 코드 예제
                                    interface PaymentStrategy {
    void pay(int amount);
}

class Cardpayment implements PaymentStrategy {
    public void pay(int amount) {
        System.out.println("카드 결제: " + amount);
    }
}

class KakaoPayment implements PaymentStrategy {
    public void pay(int amount) {
        System.out.println("카카오페이: " + amount);
    }
}

// usage
class ShoppingCart {
    private PaymentStrategy stragegy;

    public void setPatmentStrategy(PaymentStrategy strategy) {
        this.strategy = strategy;
    }

    public void checkout(int amoumt) {
        strategy.pay(amount);
    }
}

cart.setPaymentStrategy(new CardPayment());
cart.checkout(10000);

cart.setPaymentStrategy(new KakaoPayment());
cart.checkout(5000);

Observer

문제:

특정 이벤트가 발생시 영향을 받는 객체가 많습니다. 이 경우 해당 이벤트를 발생하면 다음과 같이 코드로 알릴 수 있습니다.

java 코드 예제
                                    class InventorySystem {
    public void updateStock(Product product, int quantity) {
        product.setStock(quantity);

        // 재고 변경 시 알려야 할 곳이 많음
        emailService.sendLowStockAlert(product);
        slackService.notifyTeam(product);
        dashboardService.updateChart(product);

        // 알림 대상 추가할 때마다 이 코드 수정 필요
    }
}
  • 알림 대상이 추가될 때마다 코드 수정이 필요합니다.

​

사고:

발행-구독 구조를 사용해서 느슨한 연결을 사용해보는건 어떨까?

​

해결:

java 코드 예제
                                    // 옵저버 인터페이스
interface StockObserver {
    void onStockChange(Product product, int quantity);
}

// 구독자들
class EmailAlert implements StockObserver {
    public void onStockChange(Product product, int quantity) {
        System.out.println("이메일 발송: " + product.getName());
    }
}

class SlackNotifier implements StockObserver {
    public void onStockChange(Product product, int quantity) {
        System.out.println("슬랙 알림: " + product.getName());
    }
}

// 발행자
class InventorySystem {
    private List<StockObserver> observers = new ArrayList<>();

    public void addObserver(StockObserver observer) {
        observers.add(observer);
    }

    public void removeObserver(StockObserver observer) {
        observers.remove(observer);
    }

    public void updateStock(Product product, int quantity) {
        product.setStock(quantity);

        // 등록된 모든 옵저버에게 알림
        for (StockObserver observer : observers) {
            observer.onStockChange(product, quantity);
        }
    }
}

// 옵저버 등록/해제 — 기존 코드 수정 없음
inventory.addObserver(new EmailAlert());
inventory.addObserver(new SlackNotifier());

Template Method

문제:

두개의 class에서 대부분의 동일 메서드를 사용하는 구조이나, 특정 부분만 다릅니다.

java 코드 예제
                                    class CSVDataProcessor {
    public void process() {
        openFile();
        readCSV();      // CSV 전용
        parseData();    // CSV 전용
        saveToDatabase();
        closeFile();
    }
}

class JSONDataProcessor {
    public void process() {
        openFile();
        readJSON();     // JSON 전용
        parseJSON();    // JSON 전용
        saveToDatabase();
        closeFile();
    }
}

// 문제: 전체 흐름은 같은데 코드 중복

사고:

동일한 부분은 상위구조에 놓고, 다른 부분만 하위부분에서 구현하면 어떨까?

​

해결:

java 코드 예제
                                    abstract class DataProcessor {
    public final void process() {
        openFile();
        readData();
        parseData();
        saveToDatabase();
        closeFile();
    }

    private void openFile() {
        System.out.printlin("파일 열기");
    }

    protected abstract void readData(); // 하위 클래스가 구현
    protected abstract void parseData(); // 하위 클래스가 구현

    private void saveToDatabase() {
        System.out.println("DB 저장");
    }

    private void closeFile() {
        System.out.println("파일 닫기");
    }
}

class CSVProcessor extends DataProcessor {
    protected void readData() { System.out.println("CSV 읽기"); }
    protected void parseData() { System.out.println("CSV 파싱"); }
}

class JSONProcessor extends DataProcessor {
    protected void readData() { System.out.println("JSON 읽기"); }
    protected void parseData() { System.out.println("JSON 파싱"); }
}

Proxy

문제:

다음 상황에서 문제가 발생합니다.

java 코드 예제
                                    class HighResolutionImage {
    private String filename;
    private byte[] imageData;

    public HighResolutionImage(String filename) {
        this.filename = filename;
        loadFromDisk();  // 10MB 이미지 로딩 — 5초 소요
    }

    private void loadFromDisk() {
        System.out.println("로딩 중... " + filename);
        // 무거운 작업
        this.imageData = readFile(filename);
    }

    public void display() {
        System.out.println("표시: " + filename);
    }
}

// 문제 상황
class Gallery {
    public void showGallery() {
        // 이미지 100개 생성 — 실제로 볼 건 몇 개뿐인데 전부 로딩
        List<HighResolutionImage> images = new ArrayList<>();
        for (int i = 0; i < 100; i++) {
            images.add(new HighResolutionImage("image" + i + ".jpg"));
            // 100개 × 5초 = 500초 대기...
        }
    }
}
  • 이미지 전체를 불러오는 메서드를 사용하면 객체 생성 비용이 큽니다.
  • 메모리 , 시간, 네트워크 등

​

사고

  • 실제로 생성할 때가지 생성을 미루는 것은 어떨까?
  • 접근 전에 권한 체크를 하고 싶다면?
  • 실제 객체 대신 대리자를 둬보자!

​

해결

java 코드 예제
                                    // 공통 인터페이스
interface Image {
    void display();
}

// 실제 객체 — 무거움
class HighResolutionImage implements Image {
    private String filename;

    public HighResolutionImage(String filename) {
        this.filename = filename;
        loadFromDisk();
    }

    private void loadFromDisk() {
        System.out.println("로딩 중... " + filename);
        // 무거운 작업
    }

    public void display() {
        System.out.println("표시: " + filename);
    }
}

// 프록시 — 가벼움, 실제 객체를 대신함
class ImageProxy implements Image {
    private String filename;
    private HighResolutionImage realImage;  // 처음엔 null

    public ImageProxy(String filename) {
        this.filename = filename;
        // 여기서 로딩 안 함!
    }

    public void display() {
        // 실제 필요한 순간에만 로딩
        if (realImage == null) {
            realImage = new HighResolutionImage(filename);
        }
        realImage.display();
    }
}

// 사용
class Gallery {
    public void showGallery() {
        List<Image> images = new ArrayList<>();

        // 프록시만 생성 — 즉시 완료
        for (int i = 0; i < 100; i++) {
            images.add(new ImageProxy("image" + i + ".jpg"));
        }

        // 사용자가 클릭한 이미지만 로딩
        images.get(0).display();  // 이때 image0.jpg만 로딩
    }
}
  • 이렇게 프록시 객체만 생성하고, 실제로 클릭시에만 disk에서 실제 이미지를 가져오도록 구현할 수 있습니다.

​

Proxy의 종류

  1. 무거운 객체 생성 지연

위의 예시

​

  1. Protection Proxy (보호 프록시)

접근 권한을 제어

java 코드 예제
                                    interface Document {
    void read();
    void write(String content);
}

class RealDocument implements Document {
    public void read() { System.out.println("문서 읽기"); }
    public void write(String content) { System.out.println("문서 쓰기: " + content); }
}

class ProtectionProxy implements Document {
    private RealDocument realDocument;
    private User user;

    public ProtectionProxy(RealDocument doc, User user) {
        this.realDocument = doc;
        this.user = user;
    }

    public void read() {
        realDocument.read();  // 누구나 읽기 가능
    }

    public void write(String content) {
        if (!user.hasRole("ADMIN")) {
            throw new AccessDeniedException("관리자만 수정 가능");
        }
        realDocument.write(content);
    }
}
  1. Remote Proxy

원격 객체를 로컬처럼 사용

java 코드 예제
                                    interface PaymentService {
    void pay(int amount);
}

// 실제로는 원격 서버에 있음
class RemotePaymentProxy implements PaymentService {
    private String serverUrl;

    public RemotePaymentProxy(String serverUrl) {
        this.serverUrl = serverUrl;
    }

    public void pay(int amount) {
        // HTTP 호출로 원격 서버에 요청
        HttpClient.post(serverUrl + "/pay", amount);
    }
}

// 사용하는 쪽은 로컬 객체처럼 사용
PaymentService payment = new RemotePaymentProxy("https://api.payment.com");
payment.pay(10000);

유저가 사용하는 객체를 실제로는 원격 서버에 있지만, 내 서버에 있다고 생각하게 만들어줍니다.

​

4. Caching Proxy (캐싱 프록시)

결과를 캐싱해서 재사용

java 코드 예제
                                    interface UserRepository {
    User findById(Long id);
}

class RealUserRepository implements UserRepository {
    public User findById(Long id) {
        System.out.println("DB 조회...");
        // 느린 DB 쿼리
        return queryFromDatabase(id);
    }
}

class CachingProxy implements UserRepository {
    private RealUserRepository realRepo;
    private Map<Long, User> cache = new HashMap<>();

    public CachingProxy(RealUserRepository realRepo) {
        this.realRepo = realRepo;
    }

    public User findById(Long id) {
        if (cache.containsKey(id)) {
            System.out.println("캐시 히트!");
            return cache.get(id);
        }

        User user = realRepo.findById(id);
        cache.put(id, user);
        return user;
    }
}

// 사용
UserRepository repo = new CachingProxy(new RealUserRepository());
repo.findById(1L);  // DB 조회...
repo.findById(1L);  // 캐시 히트!
repo.findById(1L);  // 캐시 히트!

캐시 구현 방법중 일부로 Proxy가 사용됩니다.