728x90

객체를 생성할 때, 특히 많은 필드를 가진 클래스에서는 생성자에 너무 많은 파라미터가 필요해지는 문제가 생깁니다. 이런 경우 가독성이 떨어지고, 실수를 유발하기 쉬운 코드가 되며, 유지보수도 점점 어려워집니다. 이 문제를 해결하기 위한 대표적인 디자인 패턴이 바로 Builder 패턴입니다. 요즘 많이 볼 수 있는 서브웨이(Subway)와 비슷하다고 생각합니다. 메뉴선택(builder) -> 재료선택 (필드 값 입력) -> 결재(Build)를 절차대로 진행하면서 다양한 조합으로 '샌드위치(객체)'를 만들 수 있는 점이 똑같지 않나요?

빌더 패턴은 왜 등장했을까?

너무 많은 생성자 파라미터

아래의 User 클래스를 만들었습니다.

public class User {
    private String name;
    private int age;
    private String phone;
    private String address;
    private String email;

    public User(String name, int age, String phone, String address, String email) {
        this.name = name;
        this.age = age;
        this.phone = phone;
        this.address = address;
        this.email = email;
    }
}

그러나 생성자에 전달해야 할 인자가 많을 경우 여러 문제가 발생합니다. 실제로 파라미터가 많아지면 순서가 맞았나 확인하는 경우가 매우 많습니다.

  • 코드의 가독성이 급격히 떨어짐
  • 파라미터 순서를 헷갈려 실수할 가능성이 높아짐
  • 생성자 오버로딩(overloading)이 기하급수적으로 늘어나며 유지보수 부담 증가

단계적이고 명시적인 객체 생성을 위한 고민

빌더 패턴은 객체를 단계적으로, 명확하게 생성할 수 있도록 도와줍니다. 또한 선택적 필드를 유연하게 설정하고, 불변 객체(Immutable Object)를 쉽게 만들 수 있는 장점이 있습니다. 이러한 필요와 문제를 해결하고자 Builder 패턴이 등장했습니다.


빌더 패턴의 구조

핵심 구성 요소

  • Product: 생성할 복잡한 객체 (예: User)
  • Builder: 객체 생성을 위한 메서드를 정의 (인터페이스 또는 추상 클래스)
  • ConcreteBuilder: Builder 구현 클래스
  • Director: 객체 생성의 순서를 정의 (굳이 없어도 된다.)

디렉터 객체를 사용하면 많이 사용하는 미리 선택된 조합으로 객체를 생성할 수 있습니다, 간편하게. 마치 썹픽처럼!

UML 다이어그램 설명

      +---------------------+
      |     Director        |
      +---------------------+
      | - builder: Builder  |
      | +construct()        |
      +---------------------+
               |
               v
      +---------------------+
      |     Builder         |<----------------+
      +---------------------+                 |
      | +setPartA()         |                 |
      | +setPartB()         |                 |
      | +build()            |                 |
      +---------------------+                 |
               ^                              |
               |                              |
      +---------------------+                 |
      |  ConcreteBuilder    |-----------------+
      +---------------------+
      | - product: Product  |
      | +setPartA()         |
      | +setPartB()         |
      | +build(): Product   |
      +---------------------+

Java 빌더 패턴 예제

public class User {
    private final String name;
    private final int age;
    private final String phone;
    private final String address;

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

    public static class Builder {
        private String name;
        private int age;
        private String phone;
        private String address;

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

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

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

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

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

실제 사용 예시

User user = new User.Builder("Tom")
    .age(30)
    .phone("010-1234-5678")
    .address("Seoul")
    .build();

필수 필드(name)는 Builder 생성자에서 받고, 선택 필드는 메서드를 통해 설정합니다. 어떤 필드를 설정하고 있는지 명확히하여 가독성을 향상할 수 있습니다.


빌더 패턴의 장단점

장점

  • 유연성 및 명확성 강화
    • 필드 순서와 관계 없이 설정 가능
    • 어떤 속성을 설정하는지 명확히 표현 가능
  • 불변성과 유지보수 용이성
    • final 필드를 통한 불변 객체 생성
    • 새로운 필드가 추가될 경우 Builder 클래스만 수정하면 됨

단점

  • 코드의 증가
    • 클래스 하나당 Builder를 작성해야 함
    • 필드를 Product와 Builder에 중복 선언해야 함
  • 런타임 오류 위험
    • 필수 값 누락 시 컴파일 단계에서 확인 불가능
    • build() 메서드에서 수동으로 검증 로직 필요

빌더 패턴을 언제 사용해야 할까?

  • 선택적 인자가 많은 객체를 생성할 때

  • 불변 객체를 만들고자 할 때

  • 객체 생성 과정을 더 명확하고 유연하게 표현하고 싶을 때

    Java에서는 Lombok과의 통합을 통해 보다 간편하기 빌더패턴을 사용할 수 있습니다. @Builder 애노테이션을 사용하면 Builder 클래스를 자동 생성해줍니다. 따라서, 코드의 간결성과 생산성 향상할 수 있습니다.

테스트 및 유효성 검증 팁

유효성 검증을 위해 build()메서드 내부에서 Null 체크나 조건 등을 검증하는 로직이 필요하게 됩니다. 필수 값이 누락되는 경우를 방지하기 위해서 입니다. 또한 테스트에서는 테스트 코드 작성을 유연하게 구성할 수 있도록 해줘 테스트 코드 작성을 쉽게 만들어 줍니다.


빌더 패턴은 복잡한 객체를 명확하고 안전하게 생성할 수 있게 해주는 디자인 패턴입니다. 특히 필수 값과 선택 값을 구분하고, 불변 객체를 생성해야 하는 상황에서 매우 유용하게 쓰입니다. 코드는 다소 길어질 수 있지만, 그만큼 가독성과 유지보수성, 안정성 면에서 장점이 많습니다.

728x90
728x90

행동(Behavioral) 디자인 패턴은 객체 간의 책임 분리와 협력을 강조하는 패턴들입니다. 방문자 패턴(Visitor Pattern)도 행동 디자인 패턴 중 하나입니다. 객체의 구조는 그대로 유지하면서, 객체에 수행할 새로운 연산을 손쉽게 추가할 수 있게 해줍니다.


방문자 패턴이란?

방문자 패턴은 객체 구조(Element)를 변경하지 않고, 새로운 행동(연산)을 방문자(Visitor) 클래스를 통해 추가하는 디자인 패턴입니다. 객체 구조 위에서 동작은 계속 변화할 수 있지만, 구조 자체는 안정적으로 유지하고 싶을 때 이상적입니다. 특히 다양한 연산이 자주 추가되어야 하는 시스템에서 유용하게 사용됩니다.


언제, 왜 사용하는가?

  • 객체의 구조는 바꾸지 않고, 다양한 연산이 자주 추가되어야 할 때
  • 클래스마다 복잡한 if-else 또는 instanceof 조건 분기가 반복될 때
  • 관심사 분리(Separation of Concerns)를 통해 유지보수를 간편하게 하고 싶을 때

방문자 패턴의 주요 구성 요소

  1. Element 인터페이스: accept(visitor) 메서드를 통해 Visitor를 받아들입니다.
  2. ConcreteElement 클래스: 각 Element의 구체 구현입니다.
  3. Visitor 인터페이스: 각 ConcreteElement에 대해 수행할 연산을 선언합니다.
  4. ConcreteVisitor 클래스: 실제 연산을 구현하는 클래스입니다.

UML 구조 설명

Element (인터페이스)
 └─ accept(visitor: Visitor)

ConcreteElementA / ConcreteElementB (구현 클래스)
 └─ accept(visitor: Visitor) {
       visitor.visit(this)
     }

Visitor (인터페이스)
 └─ visit(ConcreteElementA)
 └─ visit(ConcreteElementB)

ConcreteVisitor1 / ConcreteVisitor2 (구현 클래스)
 └─ 각 visit 메서드에서 필요한 작업 수행

방문자 패턴 작동 방식

  1. Element 객체는 자신을 Visitor에게 넘겨주는 accept() 메서드를 갖습니다.
  2. Visitor 객체는 해당 Element를 인자로 받아 visit() 메서드를 수행합니다.
  3. 이로 인해 Element는 Visitor를 받아들이고, Visitor는 해당 Element에 대한 동작을 수행합니다.

즉, Element와 Visitor가 서로를 알고 있어야 하며, 이는 더블 디스패치로 이어집니다.

방문자 패턴과 더블 디스패치(Double Dispatch)

일반적인 객체지향 언어(Java 등)는 싱글 디스패치(Single Dispatch)만을 지원합니다. 즉, 메서드는 객체의 실제 타입에 따라 하나만 선택됩니다. 하지만 방문자 패턴은 두 타입 모두를 고려해야 합니다:

  • 방문 대상(Element)의 실제 타입
  • 방문자(Visitor)의 실제 타입

이 때 사용하는 것이 바로 더블 디스패치(Double Dispatch)입니다. element.accept(visitor)에서 호출된 메서드는 Visitor의 객체 타입에 따라 실행될 visit() 메서드를 결정하며, 이 메서드 내부에서 visitor.visit(this)가 호출되면서 this가 참조하는 실제 Element 객체의 타입에 따라 정확한 visit 메서드가 실행됩니다.

즉, accept에서는 Visitor의 타입이 결정되고, visit에서는 Element의 타입이 결정되어, 두 객체의 실제 타입을 모두 고려한 정확한 메서드 실행이 가능해지는 것이 바로 더블 디스패치의 핵심입니다.


Java 코드 예제

// Visitor 인터페이스
interface Visitor {
    void visit(Book book);
    void visit(Fruit fruit);
}

// Element 인터페이스
interface ItemElement {
    void accept(Visitor visitor);
}

// ConcreteElement 클래스
class Book implements ItemElement {
    int price;
    String isbn;

    Book(int price, String isbn) {
        this.price = price;
        this.isbn = isbn;
    }

    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

class Fruit implements ItemElement {
    int pricePerKg;
    int weight;
    String name;

    Fruit(int pricePerKg, int weight, String name) {
        this.pricePerKg = pricePerKg;
        this.weight = weight;
        this.name = name;
    }

    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

// 첫 번째 Visitor: 쇼핑카트 비용 계산
class ShoppingCartVisitorImpl implements Visitor {
    public void visit(Book book) {
        System.out.println("Book ISBN: " + book.isbn + " | Cost: $" + book.price);
    }

    public void visit(Fruit fruit) {
        int cost = fruit.pricePerKg * fruit.weight;
        System.out.println("Fruit: " + fruit.name + " | Cost: $" + cost);
    }
}

// 두 번째 Visitor: 텍스트 설명 출력
class ItemDescriptionVisitor implements Visitor {
    public void visit(Book book) {
        System.out.println("[Book] ISBN: " + book.isbn + ", Price: $" + book.price);
    }

    public void visit(Fruit fruit) {
        System.out.println("[Fruit] " + fruit.name + " - " + fruit.weight + "kg at $" + fruit.pricePerKg + "/kg");
    }
}

// 클라이언트 코드
public class VisitorPatternDemo {
    public static void main(String[] args) {
        ItemElement[] items = new ItemElement[] {
            new Book(20, "1234"),
            new Fruit(2, 5, "Banana")
        };

        Visitor costVisitor = new ShoppingCartVisitorImpl();
        Visitor descriptionVisitor = new ItemDescriptionVisitor();

        System.out.println("-- 비용 계산 Visitor --");
        for (ItemElement item : items) {
            item.accept(costVisitor);
        }

        System.out.println("
-- 설명 출력 Visitor --");
        for (ItemElement item : items) {
            item.accept(descriptionVisitor);
        }
    }
}

방문자 패턴의 장단점

장점

  1. 새로운 연산을 쉽게 추가 가능 → 기존 객체 구조를 변경하지 않아도 됨 (OCP 만족)
  2. 연산 로직을 집중화 → 코드 응집도 향상 및 유지보수 용이
  3. 객체 책임 분리 → Element 클래스는 구조에만 집중

단점

  1. Element가 추가될 때마다 Visitor 수정 필요 → Visitor가 모든 Element를 알아야 함
  2. Visitor 인터페이스가 커질 수 있음 → Element가 많을수록 복잡도 증가

방문자 패턴은 코드에 새로운 기능을 추가하고자 할 때 좋습니다. 특히 더블 디스패치를 활용해 객체 타입과 연산 타입을 함께 고려할 수 있는 점은 큰 장점입니다. 객체 구조와 연산을 분리하여 유연하고 확장 가능한 시스템을 설계하고자 한다면, 방문자 패턴을 고려해보는 것도 좋다고 생각됩니다. 하지만, 방문자 패턴을 적용할 때는 책임을 명확하게 하고, 적당한 Element와 공통의 Visitor로직은 추상 클래스에 구현하는 것을 유의하면 될 것 같습니다.

728x90
728x90

추상 팩토리 패턴(Abstract Factory Pattern)은 서로 관련된 객체들의 집합(Product Family)을 생성하는 인터페이스를 제공하지만, 구체적인 클래스는 지정하지 않는 것이 핵심입니다. 즉, 클라이언트는 어떤 클래스의 인스턴스를 생성할지 알 필요 없이 제품군을 생성할 수 있습니다. 추상 팩토리 패턴이 무엇이고, 왜 필요한지, 코드 예제를 통해서 알아보겠습니다.

왜 사용해야 하는가?

관련성이 높은 객체의 모음을 써야 할 때 사용됩니다. 예를 들어, Mac 스타일 UI를 만든다면 버튼, 체크박스, 스크롤바 등이 모두 같은 스타일을 가져야 합니다. 이를 제품군으로 묶어서 함께 생성하면 일관성을 유지할 수 있습니다.

클라이언트 코드에서 구체 클래스를 숨기기 위해 사용됩니다. 코드를 더 유연하고 확장 가능하게 만들기 위해서는, 클라이언트가 구체 클래스에 의존하지 않도록 하는 것이 중요합니다.

호환성 있는 객체 집합을 생성해야 할 때 사용됩니다. 버튼과 체크박스가 서로 호환되도록 만들고 싶다면, 동일한 팩토리에서 생성된 객체들이 함께 동작할 수 있도록 보장해야 합니다.


구조와 구성요소

UML 구조 다이어그램

           +-------------------------+
           |   AbstractFactory       |<--------------------+
           +-------------------------+                     |
           | + createProductA(): A   |                     |
           | + createProductB(): B   |                     |
           +-------------------------+                     |
                  /|\                                        |
                   |                                         |
+------------------------+         +------------------------+
|  ConcreteFactory1      |         |  ConcreteFactory2      |
+------------------------+         +------------------------+
| + createProductA(): A1 |         | + createProductA(): A2 |
| + createProductB(): B1 |         | + createProductB(): B2 |
+------------------------+         +------------------------+
         |                                    |
         v                                    v
 +----------------+                 +----------------+
 |  ProductA1     |                 |  ProductA2     |
 +----------------+                 +----------------+
 | implements A   |                 | implements A   |
 +----------------+                 +----------------+
 +----------------+                 +----------------+
 |  ProductB1     |                 |  ProductB2     |
 +----------------+                 +----------------+
 | implements B   |                 | implements B   |
 +----------------+                 +----------------+
  • AbstractFactory: 제품 생성을 위한 인터페이스 GUIFactory
  • ConcreteFactory: 실제 객체들을 생성하는 클래스 WinFactory, MacFactory
  • AbstractProduct: 제품의 인터페이스 또는 추상 클래스 Button, Checkbox
  • ConcreteProduct: 제품의 실제 구현체 WinButton, MacButton
  • Client: 팩토리를 통해 객체를 사용하는 사용자 측 코드 Main

예제 코드

제품 인터페이스 정의

interface Button {
    void click();
}

interface Checkbox {
    void check();
}

구체 제품 클래스 구현

class WinButton implements Button {
    public void click() {
        System.out.println("Windows Button Clicked!");
    }
}

class MacButton implements Button {
    public void click() {
        System.out.println("Mac Button Clicked!");
    }
}

class WinCheckbox implements Checkbox {
    public void check() {
        System.out.println("Windows Checkbox Checked!");
    }
}

class MacCheckbox implements Checkbox {
    public void check() {
        System.out.println("Mac Checkbox Checked!");
    }
}

추상 팩토리와 구체 팩토리 구현

interface GUIFactory {
    Button createButton();
    Checkbox createCheckbox();
}

class WinFactory implements GUIFactory {
    public Button createButton() {
        return new WinButton();
    }
    public Checkbox createCheckbox() {
        return new WinCheckbox();
    }
}

class MacFactory implements GUIFactory {
    public Button createButton() {
        return new MacButton();
    }
    public Checkbox createCheckbox() {
        return new MacCheckbox();
    }
}

클라이언트 코드 구성

class Application {
    private Button button;
    private Checkbox checkbox;

    public Application(GUIFactory factory) {
        button = factory.createButton();
        checkbox = factory.createCheckbox();
    }

    public void render() {
        button.click();
        checkbox.check();
    }
}

public class Main {
    public static void main(String[] args) {
        GUIFactory factory = new MacFactory(); // 또는 new WinFactory();
        Application app = new Application(factory);
        app.render();
    }
}

실행 결과 예시

Mac Button Clicked!
Mac Checkbox Checked!

장단점

장점

  • 제품군 간 일관성 유지
  • 구현과 사용의 분리 → 유연한 구조
  • 확장 용이: 새 팩토리를 추가해도 클라이언트는 변경 없음

단점

  • 새로운 제품군 추가 어려움 → 인터페이스 자체 수정 필요
  • 클래스 수 증가 → 구조 복잡해짐

팩토리 메서드 패턴과의 비교

항목 추상 팩토리 패턴 팩토리 메서드 패턴
목적 제품군 생성 개별 제품 생성
생성 방법 여러 제품을 생성하는 팩토리 제공 서브클래스가 생성 책임 가짐
확장성 제품군 추가 어려움 단일 제품 계열 확장에 적합
구조 여러 팩토리 메서드 필요 하나의 팩토리 메서드 제공
예시 Mac/Win UI 제품군 문서 편집기, 알림 팝업 등

팩토리 메서드 패턴과 추상 팩토리 패턴은 유사합니다. 어떤 객체를 생성하기 위한 패턴이며 그것을 구체적인 클래스에 의존하지 않고 생성하는 공통점을 가지고 있습니다. 그러나, 가장 큰 차이는 '모음'입니다. 특정 제품군을 생성하기 위해 사용되느냐 아니냐가 가장 큰 차이라고 생각됩니다.


실전 활용 예시

  • GUI 툴킷 구현 시 적용: 운영체제(OS)에 따라 다른 UI 구성 요소를 제공할 때
  • 게임 개발에서 UI 구성 요소 분리: 플랫폼에 따라 메뉴, 버튼, HUD 등을 분리 가능
  • 제품 디자인 시스템에서 활용: 동일한 디자인 가이드를 따르는 여러 UI 구성 요소를 자동 생성

언제 추상 팩토리를 고려해야 할까?

  • 제품군 단위로 객체를 생성해야 할 때
  • 플랫폼 독립적인 객체 생성을 원할 때
  • 테스트를 위한 별도의 Mock 팩토리를 만들어 쉽게 테스트 가능
  • 팀 간 UI 책임 분리
  • 구체 클래스 변경이 클라이언트 코드에 영향 없음

추상 팩토리 패턴은 제품군 간의 일관성을 유지하고, 클라이언트와 구현체 간의 결합도를 낮춰 유지보수성과 확장성에 큰 장점을 가집니다. GUI 시스템, 게임 개발, 디자인 시스템 등 다양한 분야에서 활용 가능하며, 복잡한 객체 생성 요구가 있을 때 유용한 선택이 될 수 있습니다. 팩토리 메소드 패턴과 추상 팩토리에 대해서 알아봤습니다. 두 패턴에 대해서 적절히 사용하여 객체를 생성한다면 보다 좋은 코드를 만들 수 있을 것이라 생각합니다.

 

관련 글
팩토리 메소드(https://h1116jeon.tistory.com/88)

728x90
728x90

Mediator(중재자) 패턴은 여러 객체가 직접 서로 통신하는 대신, 중재자 객체를 통해 간접적으로 통신하게 하여 객체 간의 의존성을 줄이는 행동(Behavioral) 디자인 패턴입니다. 중재자 패턴을 통해 컴포넌트를 재사용 가능하도록 만들고, 의존관계를 줄여 유지보수성과 확장성을 높일 수 있습니다.

핵심 아이디어

여러 객체 간의 강한 결합을 제거하고, 통신을 하나의 중앙 객체(Mediator)가 책임지도록 합니다.


Mediator 없이 발생하는 문제

  • 객체들이 서로 직접 통신하면 강한 결합(tight coupling)이 생기게 됩니다.
  • 시스템이 복잡해지고, 유지보수가 어려워 집니다.

GUI 프로그램을 예시로 들어보겠습니다. 어떤 윈도우에서 버튼, 체크박스, 텍스트박스 등이 서로 상태를 감지하고 다른 객체를 직접 조절하면 객체 간 의존성이 매우 높아집니다.구체적으로 체크박스를 체크하면 텍스트박스가 활성화 되도록 한다던지, 버튼은 눌렀을 때, 모든 입력사항을 점검하는 기능을 구현할 때, 체크박스에서 텍스트박스 활성화를 직접제어하거나, 버튼에 모든 입력사항을 점검하는 기능을 구현한다면 시스템이 복잡해지고, 유지보수가 어렵습니다.


구조 및 구성 요소

주요 구성 요소

  • Mediator (중재자 인터페이스): 동료 객체 간의 통신을 정의
  • ConcreteMediator (구체 중재자): 실제 중재 로직 구현 및 동료 객체 참조 보유
  • Component (컴포넌트): Mediator를 통해 다른 객체와 통신하는 개체

UML 다이어그램

     +-------------------+
     |     Mediator      |<----------------------+
     +-------------------+                       |
              ▲                                  |
              |                                  |
 +------------------------+         +------------------------+
 |  ConcreteMediator      |         |   Component            |
 +------------------------+         +------------------------+
 | - components[]         |         | - mediator: Mediator   |
 | + notify(sender, msg)  |         | + send(msg)            |
 | + register(component)  |         | + receive(msg)         |
 +------------------------+         +------------------------+
              ▲                                  ▲
              |                                  |
     +-------------------+             +-------------------+
     | ConcreteComponent1|             | ConcreteComponent2|
     +-------------------+             +-------------------+

코드 예제

여러 사용자가 채팅방(중재자)을 통해 메시지를 주고받는 프로그램

// Mediator 인터페이스
interface ChatMediator {
    void sendMessage(String msg, User user);
    void addUser(User user);
}

// 구체 Mediator
class ChatRoom implements ChatMediator {
    private List<User> users = new ArrayList<>();

    public void addUser(User user) {
        users.add(user);
    }

    public void sendMessage(String msg, User sender) {
        for (User user : users) {
            if (user != sender) {
                user.receive(msg);
            }
        }
    }
}

// 추상 Component
abstract class User {
    protected ChatMediator mediator;
    protected String name;

    public User(ChatMediator m, String name) {
        this.mediator = m;
        this.name = name;
    }

    abstract void send(String msg);
    abstract void receive(String msg);
}

// 구체 Component
class ChatUser extends User {
    public ChatUser(ChatMediator m, String name) {
        super(m, name);
    }

    public void send(String msg) {
        System.out.println(name + " sends: " + msg);
        mediator.sendMessage(msg, this);
    }

    public void receive(String msg) {
        System.out.println(name + " receives: " + msg);
    }
}

클라이언트 실행 예시

public class Main {
    public static void main(String[] args) {
        ChatMediator mediator = new ChatRoom();

        User alice = new ChatUser(mediator, "Alice");
        User bob = new ChatUser(mediator, "Bob");
        User charlie = new ChatUser(mediator, "Charlie");

        mediator.addUser(alice);
        mediator.addUser(bob);
        mediator.addUser(charlie);

        alice.send("Hi everyone!");
    }
}

Mediator 패턴의 장단점

장점

  • 낮은 결합도: 객체들이 서로 직접 통신하지 않고 Mediator를 통해 간접 통신을 합니다.
  • 유지보수 용이성: 중재자만 수정하면 되고, 동료 객체는 영향 없습니다.
  • 의사소통 중앙화: 통신 로직이 한 곳에 모여 있어 관리 용이합니다.

단점

  • 중재자 복잡도 증가: 많은 객체를 중재하면 Mediator가 비대해질 수 있습니다.
  • 단일 책임 원칙 위배 가능성: Mediator가 너무 많은 책임을 질 경우 문제가 될 수 있습니다.

사용 예시

사례 설명
GUI 이벤트 처리 컴포넌트 간 이벤트를 중재자 통해 전달
채팅 시스템 사용자 간 메시지 중재
항공 관제 시스템 여러 항공기의 상태 및 위치를 중앙에서 조율
게임 매칭 시스템 플레이어 간 상호작용 및 상태 조정

다른 디자인 패턴과의 비교

패턴 차이점
Observer 이벤트 발생 시 여러 구독자에게 알림 (1:N) 구조
Mediator 객체 간 상호작용을 중앙 집중화하여 N:N 구조 제어
Facade 복잡한 시스템의 인터페이스를 단순화함 (통신 아님)

Mediator 패턴은 여러 객체 간의 복잡한 통신을 단순화하고, 유지보수가 쉬운 구조로 만들 수 있도록 도와줍니다. 특히 GUI나 실시간 시스템처럼 많은 객체가 상호작용하는 환경에서 유용하게 사용할 수 있습니다. 일부 객체들이 다른 객체와 긴밀하게 연결되어 재사용이 어려울 때, 반복되는 동작을 다양한 콘텍스트에서 사용하기 위해 여러 컴포넌트 자식 클래스를 사용하고 있다면 한번 고려해보는게 좋은 패턴입니다.

728x90
728x90

플라이웨이트 패턴이란?

플라이웨이트 패턴은 공통되는 상태를 공유하고, 구별되는 다른 상태만 개별 객체로 관리하는 구조 패턴입니다. 이 패턴은 많은 수의 비슷한 객체를 생성할 때 메모리 사용량을 줄일 수 있습니다. 공통된 상태는 한 번만 생성하여 공유하고, 달라지는 부분은 외부에서 주입해 자원을 효율적으로 사용합니다.

  • 공유 가능한 상태 (Intrinsic State): 변하지 않으며, 모든 객체에서 공유할 수 있는 속성입니다.
  • 개별 상태 (Extrinsic State): 객체마다 다르며, 외부에서 주입되거나 런타임에 설정되는 속성입니다.

왜 플라이웨이트 패턴이 생겨났는가?

수천, 수만 개의 객체가 필요할 경우 메모리 낭비가 심각해질 수 있습니다. 예를 들어, 문서 편집기에서 수천 개의 문자를 표시해야 할 때, 각 문자가 글꼴, 크기, 색상 등의 정보를 따로 갖고 있다면 중복된 데이터가 많아져 비효율적입니다. 그래서 등장한 해결책이 바로 플라이웨이트 패턴입니다. 중복된 정보는 공유하고, 개별 정보만 별도로 처리하여 성능 최적화와 메모리 절약을 동시에 달성할 수 있습니다.


구성 요소

  1. Flyweight (공유 인터페이스): 공통 기능을 정의 Font
  2. ConcreteFlyweight (공유 객체): 공유되는 실제 객체 ConcreteFont
  3. FlyweightFactory (객체 풀): 객체를 생성하고 재사용 FontFactory
  4. Client (클라이언트): 외부 상태를 전달하고 사용하는 측 Main

    출처 - 리팩토링 구루

예시코드

아래는 얄코님의 예시코드 Font, ConcreteFont, FontFactory, Main 를 사용한 예제입니다. 글자의 폰트, 사이즈, 색을 공유하는 폰트를 생성하는 폰트렌더링 구현입니다.

// Flyweight 인터페이스
interface Font {
    void apply(String text);
}

// ConcreteFlyweight 클래스
class ConcreteFont implements Font {
    private String font;
    private int size;
    private String color;

    public ConcreteFont(String font, int size, String color) {
        this.font = font;
        this.size = size;
        this.color = color;
    }

    @Override
    public void apply(String text) {
        System.out.println("Text: '" + text + "' with Font: " + font + ", Size: " + size + ", Color: " + color);
    }
}

// FlyweightFactory
class FontFactory {
    private static final Map<String, Font> fontMap = new HashMap<>();

    public static Font getFont(String font, int size, String color) {
        String key = font + size + color;
        Font fontObject = fontMap.get(key);

        if (fontObject == null) {
            fontObject = new ConcreteFont(font, size, color);
            fontMap.put(key, fontObject);
            System.out.println("Creating font: " + key);
        } else {
            System.out.println("Reusing font: " + key);
        }
        return fontObject;
    }
}

// 클라이언트
public class Main {
    public static void main(String[] args) {
        Font font1 = FontFactory.getFont("Arial", 12, "Black");
        font1.apply("Hello, World!");

        Font font2 = FontFactory.getFont("Arial", 12, "Black");
        font2.apply("Flyweight Pattern");

        Font font3 = FontFactory.getFont("Times New Roman", 14, "Blue");
        font3.apply("Design Patterns");

        Font font4 = FontFactory.getFont("Arial", 12, "Black");
        font4.apply("Another Text");
    }
}

실행 결과

Creating font: Arial12Black
Text: 'Hello, World!' with Font: Arial, Size: 12, Color: Black
Reusing font: Arial12Black
Text: 'Flyweight Pattern' with Font: Arial, Size: 12, Color: Black
Creating font: Times New Roman14Blue
Text: 'Design Patterns' with Font: Times New Roman, Size: 14, Color: Blue
Reusing font: Arial12Black
Text: 'Another Text' with Font: Arial, Size: 12, Color: Black
  • font, size, color는 공유 상태 (intrinsic)
  • text는 외부 상태 (extrinsic)입니다.

언제 사용하는가?

플라이웨이트 패턴은 다음과 같은 상황에서 많이 사용합니다.

  • 게임 개발: 수천 개의 총알, 몬스터, 배경 타일 등 반복되는 개체 처리 시
  • 문서 편집기/텍스트 렌더링: 수많은 문자를 효율적으로 관리할 때
  • 그래픽 에디터: 벡터 도형, 아이콘 등 반복되는 그래픽 요소
  • 캐시, 아이콘, 이모지 등 동일 요소의 반복 사용 시

장단점

장점

  • 메모리 절감: 중복 객체 생성을 피함
  • 성능 향상: 객체 재사용으로 인한 GC 부담 감소
  • 객체 수 감소: 가벼운 객체 관리가 가능
    단점
  • 구조가 복잡해질 수 있음: 팩토리와 인터페이스 등 부가 구조 필요
  • 외부 상태 관리 필요: 외부에서 데이터를 매번 전달해야 함
  • 스레드 환경에서 주의: 공유 객체 사용 시 동기화 필요 가능성

플라이웨이트 패턴은 시스템에서 반복적으로 사용되는 객체들의 메모리 사용량을 획기적으로 줄일 수 있습니다. 객체 수가 많고, 그 중 많은 부분이 공통된 속성을 가지고 있다면 플라이웨이트 패턴을 사용해보시기 바랍니다. 하지만, 플라이웨이트는 최적화를 위한 수단으로 사용되므로, 자원이 비효율적으로 사용되는 것이 확인했을 때, 사용해보시기 바랍니다.

728x90
728x90

옵저버 패턴이란?

옵저버(Observer) 패턴은 객체 간의 일대다(One-to-Many) 의존성을 정의하여, 하나의 객체의 상태가 변경되었을 때 그와 의존 관계에 있는 객체들에게 자동으로 알림이 가도록 하는 디자인 패턴입니다.

"내가 상태 바뀌면 너희들한테 알려줄게!" 라는 메시지가 핵심 아이디어입니다.

핵심 구성요소

  • Subject (관찰 대상): 상태를 관리하고, 상태가 변경되면 Observer에게 알립니다.
  • Observer (관찰자): Subject의 상태 변경을 감지하고 이에 대응합니다.

    <출처:Refactoring Guru>

옵저버 패턴을 사용하는 이유

복잡한 의존 관계를 단순화

객체 간 상태 변경을 수동으로 관리하면 결합도가 높아지고 유지보수가 어려워집니다. 옵저버 패턴은 이를 느슨하게 연결하여 코드의 유연성과 확장성을 높여줍니다.

대표적인 사용처와 시나리오

사용 예시

  1. GUI 프레임워크: 이벤트 기반 인터페이스 설계
  2. MVC 아키텍처: Model과 View의 연결 고리
  3. 알림 시스템: 상태 변경 시 다양한 사용자에게 알림 전송
  4. 게임 개발: 상태 변화에 따른 UI, AI, 사운드 등 연동
  5. 데이터 스트리밍/센서 시스템: 실시간 데이터 처리 구조

사용 시나리오

  • GUI 이벤트 처리: 버튼 클릭 → UI 반응
  • 주식 앱: 주가 변경 → 차트 갱신
  • MVC 아키텍처: Model 변경 → View 자동 업데이트

옵저버 패턴의 장단점

장점

  • 저결합 구조: Subject와 Observer가 인터페이스를 통해 연결되므로, 서로의 구현에 의존하지 않음
  • 확장 용이: 새로운 Observer를 동적으로 추가 가능
  • 자동 동기화: Subject의 상태 변경 시 모든 Observer가 자동으로 최신 상태 유지

단점

  • 예측 어려움: 어떤 Observer가 등록되어 있는지 파악하기 어려움
  • 디버깅 어려움: 내부 동작이 감춰져 있어 문제 파악에 시간 소요
  • 성능 저하 가능성: 많은 Observer에 대한 알림 처리 비용이 증가할 수 있음

옵저버 패턴

// Subject 인터페이스
public interface Subject {
    void attach(Observer o);
    void detach(Observer o);
    void notifyObservers();
}

// ConcreteSubject
public class NewsAgency implements Subject {
    private List<Observer> observers = new ArrayList<>();
    private String news;

    public void setNews(String news) {
        this.news = news;
        notifyObservers();
    }

    public void attach(Observer o) {
        observers.add(o);
    }

    public void detach(Observer o) {
        observers.remove(o);
    }

    public void notifyObservers() {
        for (Observer o : observers) {
            o.update(news);
        }
    }
}

// Observer 인터페이스
public interface Observer {
    void update(String news);
}

// ConcreteObserver
public class NewsChannel implements Observer {
    private String news;

    public void update(String news) {
        this.news = news;
        System.out.println("Received news: " + news);
    }
}

옵저버를 관리하기 위한 전략들

상태가 많을 때

조건부 알림

모든 Observer에게 무조건 알림을 보내는 대신, 관심 있는 상태에만 반응하도록 필터링합니다.

public void notifyObservers(String stateChanged) {
    for (Observer o : observers) {
        if (o.isInterestedIn(stateChanged)) {
            o.update(stateChanged);
        }
    }
}

상태 캡슐화

문자열이 아닌 클래스를 사용하여 상태의 의미를 명확히 해야합니다.

class State {
    String name;
    Object payload;
}

옵저버가 많을 때

채널/토픽 기반 그룹화 (Pub/Sub)

모든 옵저버를 한 리스트에 넣는 게 아니라, 주제별 그룹으로 묶는 방식입니다. 주제별로 관리할 수 있다는 점과 옵저버를 새로 추가/제거하는게 간편하다는 점이 장점입니다.

Map<String, List<Observer>> topicObservers = new HashMap<>();

public void attach(String topic, Observer o) {
    topicObservers.computeIfAbsent(topic, k -> new ArrayList<>()).add(o);
}

public void notifyObservers(String topic, String message) {
    for (Observer o : topicObservers.getOrDefault(topic, Collections.emptyList())) {
        o.update(message);
    }
}

비동기 처리

옵저버가 많으면 동기적으로 하나씩 처리하면 성능 저하가 발생할 수 있습니다. 이벤트 큐 또는 쓰레드 풀을 사용해서 비동기 처리로 분산하는 방법도 있습니다.

ExecutorService executor = Executors.newFixedThreadPool(4);

public void notifyObservers(String event) {
    for (Observer o : observers) {
        executor.submit(() -> o.update(event));
    }
}

약한 참조 사용

WeakReference<Observer> weakObserver = new WeakReference<>(observer);

약한 참조를 사용하면 옵저버 객체가 사용되지 않을때, GC가 제거하여 메모리누수를 방지할 수 있다.

중앙 관리 객체 도입: EventManager

옵저버 관리 책임을 분리하는 방식은 Subject(발행자)가 직접 Observer를 관리하지 않고, 그 역할을 별도의 EventManager(또는 NotificationCenter)에게 맡기는 구조입니다. 이렇게 하면 옵저버 등록, 제거, 알림 같은 로직이 한 곳에 집중되고, 확장성과 재사용성이 향상됩니다.

public class EventManager {
    Map<String, List<Observer>> observers = new HashMap<>();

    public void subscribe(String eventType, Observer o) { ... }
    public void unsubscribe(String eventType, Observer o) { ... }
    public void notify(String eventType, String data) { ... }
}

옵저버 패턴은 복잡한 상태 변화와 의존 관계를 효율적으로 관리할 수 있는 도구입니다. 하지만 성능, 디버깅, 메모리 관리 측면에서 고려할 사항이 많습니다. 필터링, 채널 분리, 비동기 처리 등의 전략을 잘 조합하면 훨씬 강력하고 안정적인 시스템을 만들 수 있습니다.

728x90
728x90

어떤 객체에 직접 접근하는 대신 "중간에서 뭔가를 대신 해주는 객체"가 필요할 때가 있습니다. 바로 이럴 때 사용하는 것이 Proxy(프록시) 디자인 패턴입니다.


프록시 패턴이란?

프록시(Proxy) 패턴은 어떤 객체에 대한 접근을 제어하거나 객체의 기능을 확장하고 싶을 때 사용하는 디자인 패턴입니다.
간단히 말하면, 실제 객체 대신 '대리 객체'가 일을 처리하는 구조입니다.


구조

프록시 패턴은 다음과 같은 구조를 가집니다

  • Subject: 실제 객체와 프록시 객체가 공통으로 구현하는 인터페이스. Image
  • RealSubject: 실제로 동작하는 객체. RealImage
  • Proxy: RealSubject의 대리 객체. 필요에 따라 RealSubject를 생성하고 호출을 위임. ProxyImage

💡 Java 예제

얄코님의 디자인 패턴 예시를 가져와서 프록시에 대해 살펴보겠습니다.

// Subject 인터페이스
public interface Image {
    void display();
}

// 실제 객체
public class RealImage implements Image {
    private String filename;

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

    private void loadFromDisk() {
        System.out.println("Loading " + filename);
    }

    public void display() {
        System.out.println("Displaying " + filename);
    }
}

// 프록시 객체
public class ProxyImage implements Image {
    private RealImage realImage;
    private String filename;

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

    public void display() {
        if (realImage == null) {
            realImage = new RealImage(filename); // 실제 객체는 필요할 때 생성
        }
        realImage.display();
    }
}

프록시 패턴은 왜 생겨났을까?

프록시 패턴은 "리소스 낭비를 줄이고, 보안 및 성능을 강화"하기 위해 생겨났습니다.

  • 대용량 이미지나 파일을 불필요하게 미리 로딩하고 싶지 않을 때
  • 민감한 데이터를 가진 객체에 모든 사용자가 접근하면 안 될 때
  • 로그나 성능 측정 등 부가적인 처리를 객체 동작에 끼워넣고 싶을 때

이럴 때 프록시 객체를 통해 중간 제어를 하는 방식이 효과적입니다.


언제 사용하면 좋을까?

상황 설명
객체 생성 비용이 클 때 대용량 이미지, 무거운 DB 연결 등. 실제 객체는 사용 시점에 생성 (Lazy Loading)
접근 제어가 필요할 때 인증/권한 체크 등. 사용자가 객체에 접근하기 전에 검사
로깅, 모니터링을 하고 싶을 때 메서드 호출 시간 측정, 로그 남기기 등
원격 객체를 추상화하고 싶을 때 네트워크를 통한 서비스 호출을 로컬 객체처럼 사용 (예: RMI, REST API)
캐싱이 필요할 때 동일 요청 시 프록시가 결과를 저장하고 재사용

예시 – 로딩 지연 (Lazy Initialization)

Image image = new ProxyImage("big-poster.jpg");

// 이 시점에서는 RealImage는 생성되지 않음
image.display(); // 여기서 처음으로 RealImage가 생성됨

image.display(); // 두 번째 호출에서는 캐시된 객체 사용

Lazy Loading을 위한 프록시입니다. RealImage는 실제로 사용될 때까지 생성되지 않으며, 이후에는 동일 인스턴스를 재사용합니다.


프록시 패턴은 리소스 절약, 보안 강화, 기능 확장이라는 다양한 문제를 깔끔하게 해결할 수 있는 구조적 패턴입니다. 특히 Spring AOP, 보안 인증 처리(Spring Security), 네트워크 통신 등 실무에서도 널리 활용되고 있습니다. 프로젝트에서 무거운 객체, 민감한 데이터, 인증, 로깅 등이 있다면 프록시 패턴을 사용해보면 좋겠습니다.

728x90
728x90

팩토리 메소드 패턴은 객체 생성을 서브 클래스에 위임하여, 클라이언트 코드가 어떤 구체적인 클래스의 인스턴스를 생성할지 몰라도 되도록 만드는 패턴입니다. 즉, 객체 생성을 캡슐화하여, "어떤 객체를 생성할 것인지"에 대한 책임을 별도의 클래스(팩토리)로 분리하는 것이 핵심입니다.


왜 사용하는가?

  • OCP(Open/Closed Principle): 기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있습니다.
  • 결합도 감소: 클라이언트 코드가 구체 클래스에 의존하지 않아 유지보수가 쉬워집니다.
  • 유연성 증가: 조건이나 환경에 따라 다른 객체를 생성할 수 있습니다.

🏭팩토리 메소드 패턴 구조

Creator (추상 클래스 또는 인터페이스)
    └── createProduct() 메소드 정의

ConcreteCreator (구현 클래스)
    └── createProduct() 오버라이드하여 실제 객체 생성

Product (인터페이스)
    └── ConcreteProductA, ConcreteProductB 등 다양한 구현체


출처:리팩토링 구루


예시 코드

얄코님의 디자인 패턴 예시코드를 바탕으로 팩토리 메소드 패턴을 살펴보겠습니다.
온라인 쇼핑몰에서 사용자가 결제 수단으로 신용카드, 계좌이체, 페이팔을 선택할 수 있다고 가정해 봅시다. 이 때, 우리는 다음과 같은 방식으로 결제 객체를 만들어야 합니다

  • 신용카드 결제 → CreditCardPayment
  • 계좌이체 결제 → BankTransferPayment
  • 페이팔 결제 → PayPalPayment

이때, 각 방법 마다 서브 클래스를 추가하고 팩토리 메소드를 사용하여 객체를 생성합니다. 각 방법 마다 필요한 내용이 다르지만 팩토리 메소드를 사용하여 객체 생성과정을 구체적으로 알 필요가 없습니다. 또한, 다른 방법이 추가되더라도 새로운 서브클래스만 추가하여 객체를 생성하면 됩니다.

// Payment.java
interface Payment {
    void processPayment(double amount);
}

// CreditCardPayment.java
public class CreditCardPayment implements Payment {
    private String creditCardNumber;

    public CreditCardPayment(String creditCardNumber) {
        this.creditCardNumber = creditCardNumber;
    }

    @Override
    public void processPayment(double amount) {
        System.out.println("Credit card: $" + amount);
    }
}

// BankTransferPayment.java
public class BankTransferPayment implements Payment {
    private String bankAccountNumber;
    public BankTransferPayment(String bankAccountNumber) {
        this.bankAccountNumber = bankAccountNumber;
    }

    @Override
    public void processPayment(double amount) {
        System.out.println("Bank transfer: $" + amount);
    }
}

// PayPalPayment.java
public class PayPalPayment implements Payment {
    private String payPalEmail;

    public PayPalPayment(String payPalEmail) {
        this.payPalEmail = payPalEmail;
    }

    @Override
    public void processPayment(double amount) {
        System.out.println("PayPal: $" + amount);
    }
}

// PaymentFactory.java
public interface PaymentFactory {
    Payment createPayment(FinancialInfo info);
}

// CreditCardPaymentFactory.java
public class CreditCardPaymentFactory implements PaymentFactory {
    @Override
    Payment createPayment(FinancialInfo info) {
        return new CreditCardPayment(info.creditCardNumber);
    }
}

// BankTransferPaymentFactory.java
public class BankTransferPaymentFactory implements PaymentFactory {
    @Override
    Payment createPayment(FinancialInfo info) {
        return new BankTransferPayment(info.bankAccountNumber);
    }
}

// PayPalPaymentFactory.java
public class PayPalPaymentFactory implements PaymentFactory {
    @Override
    Payment createPayment(FinancialInfo info) {
        return new PayPalPayment(info.payPalEmail);
    }
}

// FinancialInfo.java
public class FinancialInfo {
    String creditCardNumber;
    String payPalEmail;
    String bankAccountNumber;

    public FinancialInfo(
        String creditCardNumber,
        String payPalEmail,
        String bankAccountNumber
    ) {
        this.creditCardNumber = creditCardNumber;
        this.payPalEmail = payPalEmail;
        this.bankAccountNumber = bankAccountNumber;
    }
}

클라이언트 코드 (Main.java):

public class Main {
    public static void main(String[] args) {
        FinancialInfo userInfo = new FinancialInfo(
            "1234-5678-9012-3456",
             "user@example.com",
              "987654321"
        );
        PaymentFactory factory = new CreditCardPaymentFactory();
        Payment payment = factory.createPayment(userInfo);
        payment.processPayment(100.0);

        factory = new PayPalPaymentFactory();
        payment = factory.createPayment(userInfo);
        payment.processPayment(200.0);

        factory = new BankTransferPaymentFactory();
        payment = factory.createPayment(userInfo);
        payment.processPayment(300.0);
    }
}

팩토리 메소드 vs 단순한 if문

실제로 개발할 때는 하나의 팩토리 클래스 안에서 if-else 또는 switch문을 이용해 다양한 객체를 생성할 수도 있습니다.

public class PaymentSimpleFactory {
    public static Payment create(String type) {
        if ("card".equals(type)) return new CreditCardPayment();
        else if ("bank".equals(type)) return new BankTransferPayment();
        else return new PayPalPayment();
    }
}

이 방법도 간단한 상황에서는 유효합니다. 하지만 클래스가 많아지고 변경이 잦아지면 이러한 구조는 유지보수가 어려울 수 있습니다. 그리고, 단순히 객체를 생성하는 것이 아니라, 객체를 생성할 때, 복잡한 로직이 동반된다면 다양한 객체에 따라 복잡한 로직이 얽히므로 가독성이 떨어지고 복잡해 질 수 있습니다. 이럴 때 팩토리 메소드 패턴 사용을 고려해보는 것이 좋다고 생각합니다.


팩토리 메소드 활용

팩토리 메소드 패턴은 다음과 같은 상황에서 사용을 고려하면 됩니다.

  • 동일한 로직을 다양한 객체로 수행해야 할 때
  • 환경(웹, 모바일 등)에 따라 결과가 달라지거나, 런타임에 어떤 객체를 생성할지 결정할 때
  • 결합도를 낮추고 확장성을 높이고 싶을 때
  • 객체 생성을 캡슐화하고 싶을 때

팩토리 패턴을 다음과 같이 결합해서 많이 사용합니다.

  • 팩토리 + 전략 패턴: 전략을 팩토리로 생성하거나, 생성된 객체에 전략을 주입하여 유연한 로직 실행
  • 팩토리 + 파사드 패턴: 환경에 따라 팩토리를 선택하고, 공통 로직에 객체를 전달
  • 스프링 DI 컨테이너: 팩토리의 역할을 스프링이 해줌 (빈 등록 및 주입)

예시: 환경에 따라 팩토리 선택

PaymentFactory factory;
if (isMobile()) {
    factory = new MobilePaymentFactory();
} else {
    factory = new WebPaymentFactory();
}
Payment payment = factory.createPayment();
payment.pay(info);

팩토리 메소드 패턴은 객체 생성을 캡슐화하여, 확장성과 유지보수성을 확보하는 데 매우 유용한 패턴입니다. 특히 다양한 구현 객체를 조건에 따라 유연하게 생성하고자 할 때 유용합니다. 이번에 생성 패턴으로 팩토리 메서드에 대해서 알아보았습니다. 다음에 팩토리 메소드와 유사한 추상 팩토리 패턴에 대해서도 알아보도록 하겠습니다.

728x90

+ Recent posts