디자인 패턴이란? 그리고 생성(Creational) 디자인 패턴 정리

송현진·2025년 7월 13일
0

CS공부

목록 보기
13/17

디자인 패턴이란?

디자인 패턴은 자주 발생하는 문제를 해결하기 위한 객체지향적인 설계의 정형화된 해결책이다. 개발 과정에서 반복적으로 마주치는 문제들을 효과적으로 해결할 수 있는 재사용 가능한 설계 템플릿이며 GoF(Gang of Four)라는 네 명의 소프트웨어 공학자가 23가지의 패턴을 정리하면서 널리 알려지게 되었다. 즉, 디자인 패턴은 "좋은 객체지향 설계를 위한 모범 사례(Best Practice)"라고 이해하면 된다.

왜 디자인 패턴을 사용할까?

  • 유지보수성과 확장성이 높은 설계를 가능하게 한다.
  • 캡슐화, 추상화, OCP, DIP 등 객체지향 원칙을 자연스럽게 따르게 도와준다.
  • 코드의 중복 제거와 재사용성이 증가한다.
  • 팀원 간 의사소통이 원활하다. (패턴 이름만으로 설계 의도를 파악할 수 있음)
  • 신뢰성 높은 검증된 해결책으로 활용된다.

디자인 패턴의 분류 (GoF 기준)

분류설명대표 패턴
생성(Creational)객체를 생성하는 방법과 관련된 패턴Singleton, Factory Method, Abstract Factory, Builder, Prototype
구조(Structural)클래스/객체의 조합 방법에 대한 패턴Adapter, Composite, Decorator, Facade, Proxy, Bridge
행위(Behavioral)객체들 간의 책임 분배/통신 방식Strategy, Observer, Template Method, Command, State

생성(Creational) 디자인 패턴

생성 패턴은 객체 생성의 책임과 과정을 캡슐화하여 유연하고 확장성 있는 객체 생성을 가능하게 하는 패턴들이다.

Singleton (싱글턴 패턴)

Singleton 패턴은 애플리케이션 전역에서 오직 하나의 인스턴스만 존재하도록 보장하는 패턴이다. 객체가 여러 번 생성되면 안 되는 경우, 또는 공유된 자원에 접근해야 할 경우에 주로 사용된다. 내부적으로는 생성자를 private으로 숨기고 클래스 내부에 static한 인스턴스를 하나 생성하여 getInstance()를 통해 외부에서 접근하도록 한다.

이 패턴은 전역 상태(Global State)를 유지하면서도 객체 지향의 원칙을 위반하지 않도록 돕는다. 단, 멀티스레드 환경에서는 동기화 이슈를 주의해야 하며 이중 체크 락 또는 enum 싱글턴 등으로 보완할 수 있다.

public class Singleton {
    private static final Singleton INSTANCE = new Singleton();

    private Singleton() {} // 외부 생성 금지

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

활용 예시로는 설정을 저장하는 Configuration 클래스, 로그를 출력하는 Logger, 데이터베이스 커넥션 풀 등 애플리케이션 전역에서 하나의 인스턴스만 유지하는 것이 중요한 객체에서 자주 사용된다.

Factory Method (팩토리 메서드 패턴)

Factory Method 패턴은 객체 생성을 서브클래스에 위임하여 유연성을 확보하는 패턴이다. 클라이언트 코드가 객체 생성 방식에 대해 알 필요 없이 인터페이스나 추상 클래스만을 통해 객체를 생성할 수 있도록 도와준다. 즉, 어떤 클래스의 인스턴스를 생성할지를 서브클래스에서 결정하고 클라이언트는 공통된 상위 타입만 보고 객체를 사용할 수 있게 된다.

이는 OCP(개방-폐쇄 원칙)을 지키는 대표적인 예로 새로운 객체 유형을 추가해도 기존 코드를 수정할 필요가 없다. 팩토리 메서드는 특히 프레임워크 설계 시 템플릿 메서드 패턴과 함께 많이 사용되며 다양한 구현체 중 특정 조건에 따라 생성 객체를 분기해야 할 때 유용하다.

abstract class Dialog {
    public void render() {
        Button okButton = createButton();
        okButton.render();
    }
    public abstract Button createButton();
}

class WindowsDialog extends Dialog {
    public Button createButton() {
        return new WindowsButton();
    }
}

활용 예시로는 JDBC의 DriverManager.getConnection()이 대표적이며 내부적으로 MySQL, Oracle, H2 등 각각의 구현체를 반환한다. 또한 Spring의 BeanFactoryApplicationContext 내부에서도 Bean을 생성할 때 이 패턴이 활용된다. 객체 생성을 캡슐화함으로써 의존성과 결합도를 줄이고 유연한 구조를 만들 수 있다.

Abstract Factory (추상 팩토리 패턴)

Abstract Factory 패턴은 서로 관련 있거나 의존적인 객체들을 일관성 있게 생성할 수 있도록 도와주는 패턴이다. 개별 객체가 아닌 제품군(Product Family)을 생성해야 할 때 유용하며 구체적인 클래스에 의존하지 않고 인터페이스를 통해 객체 생성을 추상화한다. 이로 인해 클라이언트는 생성 방식에 구애받지 않고 다양한 환경에 맞게 제품군을 교체할 수 있다.

예를 들어 운영체제에 따라 버튼(Button)이나 체크박스(Checkbox) 스타일이 달라져야 할 경우 각각을 하나의 팩토리로 구성하고 OS에 따라 팩토리를 교체함으로써 코드 수정을 최소화할 수 있다.

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

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

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

활용 예시로는 운영체제, 테마, 데이터베이스 종류에 따라 객체 구성을 바꾸어야 하는 UI 라이브러리나 커넥션 모듈에 자주 활용된다. 프레임워크 내부에서도 다양한 구현체를 전환하기 위한 전략으로 사용된다.

Builder (빌더 패턴)

Builder 패턴은 복잡한 객체를 단계별로 생성할 수 있도록 도와주는 패턴이다. 생성자에 많은 인자가 필요한 경우나 일부 값만 선택적으로 설정해야 하는 경우에 유용하다. 또한 메서드 체이닝을 통해 가독성이 뛰어난 객체 생성 코드를 작성할 수 있고 불변 객체와의 궁합도 매우 좋다. 특히, 필수 파라미터와 선택 파라미터를 구분하여 유연하게 객체를 구성할 수 있으며 생성 로직을 분리해 관심사를 구분할 수 있다.

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

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

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

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

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

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

활용 예시로는 Lombok의 @Builder 어노테이션, HTTP 요청 객체, SQL 쿼리 생성기, 직렬화 옵션 설정 등에서 볼 수 있다. 특히 빌더 패턴은 가독성, 유연성, 명확성 측면에서 많은 이점을 제공하기 때문에 실무에서 매우 자주 사용된다.

Prototype (프로토타입 패턴)

Prototype 패턴은 기존 객체를 복제(clone)하여 새로운 객체를 생성하는 방식의 디자인 패턴이다. 새로운 객체를 매번 생성하는 대신 기존 객체를 복사하여 재사용함으로써 성능을 최적화할 수 있다. 객체 생성 비용이 큰 경우나 복잡한 초기화가 필요한 객체를 효율적으로 복제하는 데 유용하다.

Java에서는 Cloneable 인터페이스와 Object.clone() 메서드를 사용하여 구현할 수 있으며 얕은 복사와 깊은 복사 방식 모두 적용할 수 있다.

public class Document implements Cloneable {
    private String content;

    public Document(String content) {
        this.content = content;
    }

    public Document clone() {
        try {
            return (Document) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }
}

활용 예시로는 그래픽 에디터의 도형 복사, 게임 캐릭터 복제, 템플릿 기반 문서/폼 생성 등이 있으며 상태가 유사한 객체를 반복 생성해야 할 때 유리하다. 성능 최적화와 객체 복제의 일관성을 제공할 수 있는 패턴이다.

📝 배운점

이번 TIL을 통해 객체를 생성하는 데도 다양한 패턴이 존재하고 각각의 패턴이 해결하고자 하는 문제 상황이 다르다는 점을 명확히 이해할 수 있었다. 예를 들어 Singleton은 인스턴스 개수를 제어하고 싶을 때, Factory Method는 객체 생성 로직을 서브클래스로 분리하고 싶을 때 사용된다. Abstract Factory는 여러 개의 객체를 하나의 제품군처럼 생성해야 할 때 매우 유용하고 Builder는 복잡한 객체 생성을 가독성 있게 처리할 수 있도록 도와주며 Prototype은 고비용 객체를 효율적으로 복제할 수 있는 수단이 된다.

이러한 생성 패턴들을 학습하면서 객체 생성의 책임을 어떻게 분리하고 어떤 방식으로 유연하게 만들 수 있을지를 고민하게 되었고 실제로 Spring, Lombok, Jackson 등 다양한 라이브러리에서 이 패턴들이 쓰이고 있다는 사실을 통해 실무와 이론이 연결된다는 점이 흥미로웠다. 앞으로 코드 작성 시 new 키워드만 생각하는 것이 아니라 "이 객체는 어떤 방식으로 생성되어야 가장 유연할까?"라는 질문을 던져보며 더 좋은 설계를 할 수 있도록 노력해야겠다.


참고

profile
개발자가 되고 싶은 취준생

0개의 댓글