Notice
Recent Posts
Recent Comments
Link
«   2026/06   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
Tags
more
Archives
Today
Total
관리 메뉴

Soyeon Park 님의 블로그

GoF Design Pattern 본문

소프트웨어공학

GoF Design Pattern

chief.park 2026. 4. 18. 01:50

디자인 패턴은 아키텍처 스타일보다 낮은 수준에서 구체화를 위해서 적용된다. 요구사항을 고려하여 아키텍처를 확장한 후 아키텍처의 컴포넌트들을 구현하는 단계에서 적용된다. 컴포넌트 안의 클래스의 역할과 동작이 결정된 후 발생하는 설계 이슈에 대하여 해법이 되는 것이 디자인패턴이다. 최상위 설계(아키텍처 스타일) → 중위 설계(디자인 패턴) → 하위 설계(알고리즘과 자료구조)의 단계이다.

 

디자인 패턴은 캡슐화, 응집, 추상화와 같은 설계 원리와 객체지향 SOLID 원칙을 잘 지킨 모범 사례이다. 디자인 패턴을 적용한다면 앞서 정리한 설계 원칙을 잘 따를 수 있게 된다.

 

 

 

1. 싱글톤 패턴

 

객체를 강제적으로 하나만 생성하려는 목적을 가지고 있다. 원래 클래스를 이용하는 클라이언트가 생성자를 접근할 수 있다면 그 클래스의 객체를 얼마든지 만들어낼 수 있다. 싱글톤 패턴은 클래스 생성에 대한 통제를 하는 것이다. 단일 객체를 만들어야 할 때 필요로 한다.

 

하나의 클래스 인스턴스만 원하고 모든 클라이언트가 단일 동일 인스턴스를 공유하려면 싱글톤 패턴을 적용한다. 예를 들어, DB 커넥션을 위한 인터페이스가 있다. 클라이언트마다 열어주는게 아니라 DB 커넥션 객체를 하나만 두고, 계속 재사용하겠다는 의미이다. DB 입장에서 커넥션을 여러개 관리하는 것이 낭비이다. - 자원 절약하자

 

  • 클래스 자체를 정적(static)변수로 둔다.
    자바에서 static이 붙은 변수는 객체마다 생기는 것이 아니라, 클래스 자체에서 딱 1개만 생성된다. 외부에서 new로 객체를 만들 수 없으니, 자기 클래스 내부에서 객체를 만들고 static으로 두는 것이다.
  • 클래스의 생성자는 private으로 선언한다.
    클래스 생성자가 public이면, 어디서든 new Class()로 객체를 찍어낼 수 있다.
  • 유일하게 객체를 접근하는 정적 메서드를 둔다.

 

 

 

2. 반복자(Iterator) 패턴

 

집합 클래스의 자료구조와 상관없이 집합에 소속된 요소들을 쉽게 접근하기 위하여 반복자에게 위임한다. 집합 내부 요소의 저장 방법과 반복적 접근 알고리즘 구현에 관계없이 집합 요소에 순차적으로 액세스하는 방법을 제공한다. 클라이언트가 특정 집합 유형과 유형별 접근하고 집계하는 방법을 신경쓰지 않아도 된다.

  • <<interface>> Aggregate에서 Iterator 객체를 만들어서 반환한다.
  • ConcreteAggregate는 Aggregate 인터페이스를 구현한 클래스이다. List, LinkedList 등 다양한 구체적인 집합이 된다.
  • <<interface>> Iterator는 데이터를 순서대로 보기 위해서 필요한 메서드를 정의해두었다. 클라이언트는 getFirst(), getNext()...로 접근한다.
  • ConcreteIterator는 Iterator 인터페이스를 구현한 클래스이다. array면 인덱스를 움직이고, linkedlist면 노드 포인터를 움직이는 식으로 내부 데이터 구조에 맞게 탐색을 수행한다.
  • Client는 <<interface>>에만 접근을 한다. 내부가 어떻게 동작하는지는 관심이 없다. 그저 getFirst()하면 첫번째 요소가 나오면 되고, getNext()를 하면 다음 요소가 나오면 된다.

 

 

 

3. 어댑터 패턴

 

사용 가능한 서비스의 인터페이스를 클라이언트가 예상하는 인터페이스에 맞게 조정한다. 클라이언트와 서비스가 호환되지 않는 인터페이스를 가지고 있지만 함께 작동하고 싶을 때 사용한다. 즉, 어댑터는 서비스가 제공하는 인터페이스를 클라이언트가 기대하는 인터페이스로 변환을 해준다.

 

Client는 interfaceClientExpects 인터페이스를 기대하고 있다. Client는 자신이 기대하는 인터페이스에서 제공하는 methodClientExpects()만 사용하고자 한다. 서비스는 interfaceAvailable 인터페이스를 제공하고 있다. ServiceAvailable은 해당 인터페이스를 구현하고 있다. 내부적으로 methodAvailable() 메서드를 구현한다. 서로 다른 규격(메서드)을 가지고 있다. 그래서 중간에 Adaptor를 둔다. interfaceClientExpects를 상속받는다. methodClientExpects()를 구현하게 될텐데, 그 안에 서비스에서 제공하고 있는 methodAvailable() 메서드가 동작하게 된다. Client쪽은 수정하지 않고 확장할 수 있는 OCP 원칙을 잘지켰다.

데이터 변환하는 case가 다양할 때 어댑터만 갈아끼우면 된다. 아래 코드를 보면 clinet는 자신한테 들어오는 데이터가 무엇인지에 따라서, 메서드를 달리 호출하지 않는다. 그저 convert()만 호출한다. Xml xml = adapter.convert(), Csv csv = adaptor.convert() 이런식으로 말이다. 클라이언트가 원하는 인터페이스는 동이라고, 어댑터가 그 안에서 ServiceAvailable 메서드인 converToXML()과 이어주는 역할을 한다.

interface IDataAdapter {
    Xml convert(Json json);
}

// client가 바로 이걸 호출하게 하지 않는다.
class Json {
    public Json(){}
    Xml convertToXML(){
        // Logic to convert the data into XML
    }
}

// client는 convert 메서드만 알고 싶다.
class JsonToXmlAdapter implements IDataAdapter {
    private Json json;
    public JsonToXmlAdapter(Json json) {
        this.json = json;
    }
    
    public Xml convert(Json json) {
        // Logic to convert json to Xml
        this.json.convertToXML() // ServiceAvailable 메서드와 연결
    }
}

class Main {
    public static void main(String[] args) {
        Json json = new Json("some json data");
        Csv csv = new Csv("some csv data");
        Xml xml = new Xml("some xml data");
        Bson bson = new Bson("some bson data");
        
        // Convert from Json to Xml
        IDataAdapter adapter = new JsonToXmlAdapter(json);
        Xml xml = adapter.convert();
        
        // Convert Json to csv
        adapter = new JsontoCsvAdapter(json);
        Csv csv = adapter.convert();
        
        // Convert Csv to bson
        adapter = new CsvToBsonAdapter(csv);
        Bson bson = adapter.convert();
        
        // Call the calculate tax API using the XML data
        Decimal tax = caculateTax(xml)
    }
}

 

 

 

4. 데코레이터 패턴

 

클래스의 동작을 확장하고 싶다. 단순히 새로운 동작을 포함하도록 클래스를 수정할 수 있다. 하지만 이는 OCP(Open Close Principle)에 위배된다. 그러면 상속에 의해서 동작을 확장해본다면? 정적으로 결정해놓기 때문에, 런타임에 확장이 불가능하고 무엇보다도 기능의 조합 수 만큼의 서브클래스가 필요해진다. 그래서 나온 것이 데코레이터 패턴이다. 동작을 동적으로 만들어서(데코레이트해서) 보여준다.

 

데코레이터 패턴의 구성 요소는 (1) component 클래스(장식대상)와 확장 기능이 담긴 (2) 데코레이터이다. 상속된 구체적인 component1 클래스는 decorator가 가진 기능으로 확장되거나 장식될 기본 기능을 가진다. 핵심 로직을 가진 클래스고 이 클래스를 decorator들로 장식한다고 생각하자. 데코레이터 객체에는 Component1 또는 다른 데코레이터 객체의 참조가 포함되어 확장된다. 서브클래스를 수많큼 만들 필요가 없고, 데코레이터 체인을 사용하여 장식 요소를 래핑할 수 있게 된다. (b)를 보면, Decorator1 객체가 Component1 객체를 래핑하고, Decorator2 객체가 래핑되는 예시이다. 데코레이터 객체가 재귀 합성(순서가 중요)으로 component 객체를 래핑하게 된다.

// Component
public interface Notifier {
    public void send(String message);
}

// Decorator
public class BaseDecorator implements Notifier {
    private Notifier notifier;
    public void send(String message) {
        // 기본 메세지 로직
    }
}

public class SMSDecorator extends BaseDecorator {
    public SMSDecorator(Notifier notifier) {
        super(notifier)
        public void send (String message) {
            message += "SMS Format message";
            super.send(message);
        }
    }
}

BaseDecorator base = new SMSDecorator(new FBDecorator(new BaseDecorator()));
base.send(message);
// SMS -> FB -> 최종 전송

클라이언트가 SMS에게 "hello"라고 보낸다. SMSDecorator에서 "hello"+"SMS Format message"로 전처리 하고, super.send("hello SMS Format message") 호출한다. FBDecorator는 "hello SMS Format message"+"FB Format message"로 전처리하고 super.send("hello SMS Format message FB Format message") 호출한다. 그러면 이제 마지막 BaseDecorator가 send()를 한다.

 

 

 

5. 팩토리 메서드 패턴

 

팩토리 메서드 패턴은 클래스의 새로운 객체를 생성할 때 사용된다. 클라이언트에서 사용할 클래스의 객체를 생성하는 책임을 분리하여 새로운 변화가 있을 때 기존 코드는 수정할 필요 없이 확장할 수 있게 돕는 것이다.

 

객체를 생성하기 위한 팩토리 메서드(createProduct)를 포함하는 추상클래스(AbstractCreator)를 정의한다. Client는 AbstractCreator와 AbstractProduct만 보고 있다. 실제로 객체가 만들어지는 방법과 그 구체적인 객체는 Client가 모른다. 팩토리 메서드에 따라서 아래에서 구현되고 있다.

 

 

 

6. 추상 팩토리 패턴

 

추상 팩토리 패턴은 앞서 본 팩토리 메서드를 모아둔 패밀리로 작성하는 것이다. 따라서 이 또한 객체를 사용할 클라이언트에서 구체적인 객체 생성을 지정하는 책임을 분리하기 위해 추상 인터페이스를 이용하여 관련 객체 패밀리를 생성한다. 클래스 외부로 객체를 만드는 책임을 이동시키는 방식으로 객체 생성에 융통성을 준다.

 

Client는 AbstractProduct와 AbstractFactory에만 접근하고, 나머지 구체적인 부분에는 관여하지 

public class Demo {
    /**
     * Application picks the factory type and creates it in run time (usually at
     * initialization stage), depending on the configuration or environment 
     * variables.
     */
    private static Application configureApplication() {
        Application app;
        GUIFactory factory;
        String osName = System.getProperty("os.name").toLowerCase();
        if (os.name.contains("mac")) {
            factory = new MacOSFactory();
        } else {
            factory = new WindowsFactory();
        }
        app = new Application(factory);
        return app;
    }
    
    public static void main(String[] args) {
        Application app = configureApplication();
        app.print();
    }
}

os에 따라서 동적으로 GUI를 구현하는 코드이다. MacOSFactory()와 WindowsFactory()를 사용하고 있다. 팩토리 패턴을 사용하지 않았다면, mac/windows에서 어떻게 구현하고 있는지를 여기에 다 들어갔을 것이다. 불필요하게 fat해진다.

 

 

 

7. 상태 패턴

 

상태에 따라 객체의 동작을 변경해야 하는 경우 사용한다. 객체에 상태 변수가 있고 if-else로 상태에 따라 다른 동작을 수행하도록 하는 방법을 생각할 수 있을 텐데, 이는 상태가 프로그램 구조에 잘 드러나지 않고 상태 변경과 추가에 대하여 영향을 많이 받는 단점이 있다. 그래서 상태 패턴은 맥락과 상태를 별도로 구현하여 결합도를 낮춘다.

 

Document가 상태를 직접 결정하는게 아니다. State 클래스를 포함하고 있고, 상태는 State에서 결정된다.

 

아래 코드는 안좋은 예시이다. Document 클래스 안에 전부 다 들어있다. 만약 새로운 state를 추가하고 싶다면...publish 메서드를 다시 작성해야한다. Document 클래스를 수정하게 된다.

 

이렇게 바꿨더니 if-else문이 사라졌다. 상태 패턴을 잘 적용한 것이다.

interface PlayerState {
    void play();
    void pause();
    void stop();
}

// 각 state에 맞춰서 play, pause, stop을 구현
class PlayingState implements PlayerState {
    @Override
    public void play() {
        System.out.println("Already playing");
    }
    @Override
    public void pause() {
        System.out.println("Pausing music");
        // Pause playback logic
    }
    @Override
    public void stop() {
        System.out.println("Stopping music");
        // Stop playback logic
    }
}

class PausedState implements PlayerState {
    @Override
    public void play() {
        System.out.println("Resuming playback");
        //...
    }
    @Override
    public void pause() {
        System.out.println("Already paused");
    }
    @Override
    public void stop() {
        System.out.println("Stopping music");
    }
}

class StoppedState implements PlayerState {
    @Override
    public void play() {
        System.out.println("Starting playback");
    }
    @Override
    public void pause() {
        System.out.println("Can't pause when stopped");
    }
    @Override
    public void stop() {
        System.out.println("Already stopped");
    }
}

class MusicPlayer {
    private PlayerState currentState;
    
    public MusicPlayer() {
        this.currentState = new StoppedState();
    }
    
    public void play() {
        currentState.play();
    }
    public void pause() {
        currentState.pause();
    }
    public void stop() {
        currentState.stop();
    }
    
    public void setState(PlayerState newState) {
        this.currentState = newState;
    }
}

public class Client {
    public static void main(String[] args) {
        MusicPlayer player = new MusicPlayer();
        player.setState(new PlayingState());
        
        // Play music
        player.play();
        //Pause music
        player.pause();
        //Stop music
        player.stop();
    }
}

 

 

 

8. 전략 패턴

 

전략 패턴은 목적을 달성하기 위해 여러 가지 전략이 있을 때, 이 전략들을 각각 독립적인 클래스로 캡슐화하여 실행 중에 자유롭게 갈아 끼울 수 있게 해준다. 전략 간의 관계가 없다. 단지 어떤 것을 선택할 것이냐를 결정하는 것이다. 생상 단계에서 전략이 결정된다.

 

전략 패턴의 장점은 OCP 적용 가능, 전략별 알고리즘 분리, 객체 알고리즘 동적 취사 선택 가능하다는 것이다. 단점은 전략이 소수일 때 과한 구조이며, 클라이언트가 어떤 전략이 적절한지를 알고 있어야한다.

 

이렇게만 들으면 전략 패턴이 상태 패턴과 비슷해보인다. 클래스에서 상태를 분리한다, 전략을 분리한다... 그러나 아래 그림을 보자. 상태 패턴은 상태 간의 전환이 이루어져 의존 관계가 있다. render를 한 후 publish를 해야하는 것처럼 상태 간의 의존 관계가 있다. 반면, 전략 패턴은 전략 간의 전환이나 의존이 없다. 그저 취사선택할 수 있는 다른 전략들일 뿐이다.

 

 

 

9. 옵서버 패턴

 

데이터를 보관하고 있는 Subject가 그 데이터를 이용하는 Observers와 효과적으로 통신하면서도 어떻게 하면 느슨하게 결합할 수 있는가라는 문제를 다룬다. 옵서버가 계속 데이터의 변경을 체크하는 것이 아니라 데이터(Subject)가 변경될 때마다 관찰하는 옵서버들에게 통지하면 느슨한 결합이 된다.

  • Subject 클래스: 옵서버 목록을 유지, 변경을 고지
  • Observer 클래스: 변경을 통지 받고 접근을 요청

'소프트웨어공학' 카테고리의 다른 글

SOLID - 코드로 이해하기  (0) 2026.04.14
객체지향 설계 원리  (0) 2026.04.14
결합도와 응집도 (Coupling and Cohension)  (0) 2026.04.14
전통적인 설계 원리  (0) 2026.04.13