Study/Etc

[Design Pattern] 디자인 패턴의 개념과 종류 (1) - 생성 패턴

jonghne 2024. 1. 12. 14:56

디자인 패턴이란 ?

디자인 패턴이란 개발 과정에서 자주 발생하던 문제에 대한 해결책을 재사용 가능한 형태로 정리해 놓은 검증된 설계 방법 또는 솔루션이다.

 

디자인 패턴의 장점으로는 일관된 구조를 사용하기 때문에 팀원 간 의사소통을 쉽게 만들어주고 유지보수성이 좋다. 

 

또한 검증된 방법이기 때문에 신뢰하고 애플리케이션을 개발할 수 있고, 시스템의 확장성이 높아진다.

 

디자인 패턴은 크게 생성 패턴, 구조 패턴, 행위 패턴 3가지가 있다. 

https://m.hanbit.co.kr/channel/category/category_view.html?cms_code=CMS8616098823

이번 게시글에서는 생성 패턴의 종류에 대해 간략하게 알아본다. 

생성 패턴

생성 패턴은 객체의 생성과 초기화에 관련된 패턴으로, 클래스의 인스턴스화를 다양한 방법으로 다루기 위한 디자인 패턴이다.

 

주로 객체 생성과 관련된 복잡성을 줄이고, 객체의 종류를 시스템에서 유연하게 확장할 수 있게 한다. 

 

생성 패턴의 주요 패턴으로는 싱글톤, 팩토리 메서드, 추상 팩토리, 빌더, 프로토 타입 패턴이 있다.

 

싱글톤 패턴

하나의 클래스 당 하나의 인스턴스만 가지도록 하는 디자인 패턴으로 최초 생성 시 한번만 인스턴스가 메모리에 생성되고, 그 후로는 해당 인스턴스를 공유해서 사용한다. 

  • 장점 : 하나의 인스턴스만 생성하기 때문에 인스턴스 생성 비용이 줄어든다. (즉, 메모리를 덜 차지한다)
  • 단점 : 멀티 스레드 환경에서 여러 스레드가 접근하는 경우 데이터 정합성이 깨질 수 있다. (동기화 처리를 잘 해야한다)
  • 예시 : DB 커넥션이나 Pool을 담당하는 클래스, Logger 클래스 등 (Spring Bean도 기본적으로는 싱글톤이다)

 

싱글 톤 클래스 적용 예시

아래 예시에서는 Singleton 인스턴스를 Static으로 생성한 뒤, getInstance() 메서드를 통해 생성된 인스턴스를 공유하게 설정해 두었다. (생성자를 private로 제한해서 다른 인스턴스 생성을 못하게 제한)

public class Singleton {
    // 정적 멤버 변수로 유일한 인스턴스를 저장
    private static Singleton instance;

    // private 생성자로 외부에서의 인스턴스 생성 방지
    private Singleton() {
        // 생성자의 내용
    }

    // 정적 메소드로 유일한 인스턴스에 접근
    public static Singleton getInstance() {
        // 인스턴스가 없는 경우에만 생성
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

    // 다른 메소드들
    public void someMethod() {
        System.out.println("싱글톤의 메소드 호출");
    }
}

// 클라이언트 코드
public class SingletonClient {
    public static void main(String[] args) {
        // 유일한 인스턴스에 접근
        Singleton singleton = Singleton.getInstance();

        // 메소드 호출
        singleton.someMethod();
    }
}

 

팩토리 메서드 패턴

객체 생성을 서브 클래스(Factory)에 위임해서, 구체적인 인스턴스 타입을 서브클래스에서 결정하도록 하는 패턴이다.

 

클라이언트가 구현 클래스의 타입을 몰라도 되기 때문에 의존성이 낮아지고 또한 객체 생성 로직을 캡슐화 해서 확장이 쉽고 유지보수하기 좋다. 

 

팩토리 메서드 패턴 적용 예시

아래 예시에서 메인 메서드에서 SedanCar, SUVCar 타입의 객체를 생성할 때, CarFactory 서브 클래스에게 객체 생성을 위임하는 것을 볼수 있다. 

 

이렇게 팩토리 메소드 패턴을 적용한다면 클라이언트 코드에서는 인터페이스 타입으로 인스턴스 타입을 설정 하고, 팩토리 클래스를 통해 동적으로 구현 클래스를 고를 수 있다 (단, DI가 적용된 것은 아님)

// 간단한 팩토리 클래스
class CarFactory {
    public Car createCar(String type) {
        if ("Sedan".equalsIgnoreCase(type)) {
            return new SedanCar();
        } else if ("SUV".equalsIgnoreCase(type)) {
            return new SUVCar();
        } else {
            throw new IllegalArgumentException("Invalid car type");
        }
    }
}

// 간단한 팩토리에서 생성되는 인터페이스를 구현한 클래스들
interface Car {
    void start();
}

class SedanCar implements Car {
    @Override
    public void start() {
        System.out.println("Sedan car started");
    }
}

class SUVCar implements Car {
    @Override
    public void start() {
        System.out.println("SUV car started");
    }
}

// 클라이언트 코드
public class SimpleFactoryExample {
    public static void main(String[] args) {
        CarFactory carFactory = new CarFactory();

        Car sedan = carFactory.createCar("Sedan");
        sedan.start();  // Sedan car started

        Car suv = carFactory.createCar("SUV");
        suv.start();    // SUV car started
    }
}

 

추상 팩토리 패턴

서로 관련이 있는 객체들의 집합을 생성하는 인터페이스를 제공하는 패턴이다.

 

팩토리 메서드 패턴은 객체 생성에 집중한다면, 추상 팩토리 패턴은 연관된 객체들을 모아둔다는 것에 집중한다.

 

추상 팩토리 패턴은 아래와 같이 관련이 있는 객체들(ProductA, ProductB)을 추상 팩토리 클래스 내에 메서드로 구현하게 하고, Client에서는 해당 객체들을 추상 팩토리 클래스를 통해 생성한다.

 

추상 팩토리 패턴 적용 예시

아래 예시는 도형의 모양과 색상을 결정하는 추상 팩토리를 통해 객체를 생성하는 예시이다

 

도형의 모양을 나타내는 Shape 인터페이스와 구현체 Circle과 Rectangle이 있고, 색상을 나타내는 Color 인터페이스와 구현체 Red와 Blue가 있다. 

 

메인 메서드에서 도형의 색깔과 모양을 결정하는 AbstractFactory 인터페이스와 구현체 AbstractFactoryImpl 클래스를 통해 인스턴스를 생성하고 있다.

 

아래의 예시에는 Color 인터페이스 구현체 중 Red를 , Shape 인터페이스 구현체 중 Circle을 선택해서 도형을 생성하도록 설정해 두었다. 만약 도형을 다른 조합(구현체)으로 생성하고 싶다면, AbstractFactory 인터페이스의 구현 클래스를 새로 생성하거나 수정하면 된다.

// 도형을 나타내는 인터페이스
interface Shape {
    void draw();
}
// 원을 구현하는 클래스
class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("원을 그립니다.");
    }
}

// 사각형을 구현하는 클래스
class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("사각형을 그립니다.");
    }
}

// 색상을 나타내는 인터페이스
interface Color {
    void fill();
}

// 빨간색을 구현하는 클래스
class Red implements Color {
    @Override
    public void fill() {
        System.out.println("빨간색으로 칠합니다.");
    }
}

// 파란색을 구현하는 클래스
class Blue implements Color {
    @Override
    public void fill() {
        System.out.println("파란색으로 칠합니다.");
    }
}


// 도형과 관련된 추상 팩토리 인터페이스
interface AbstractFactory {
    Shape createShape();
    Color createColor();
}

// 도형과 색상의 조합을 생성하는 구체적인 팩토리 클래스
class AbstractFactoryImpl implements AbstractFactory {
    @Override
    public Shape createShape() {
        return new Circle(); // 예시로 원을 생성
    }

    @Override
    public Color createColor() {
        return new Red(); // 예시로 빨간색을 생성
    }
}

// 클라이언트 코드
public class Client {
    public static void main(String[] args) {
        // 팩토리를 생성
        AbstractFactory factory = new AbstractFactoryImpl();

        // 도형 생성
        Shape shape = factory.createShape();
        shape.draw();

        // 색상 생성
        Color color = factory.createColor();
        color.fill();
    }
}

 

빌더 패턴

빌더 패턴은 객체를 표현하는 클래스와 생성하는 클래스를 분리해서, 다양한 구성의 인스턴스를 만들 수 있게 하는 패턴이다.

 

빌더 패턴은 인스턴스 생성 시, 필수로 초기화해야 하는 변수와 초기화가 필요없는 변수를 나눠서 유연하게 인스턴스를 생성할 수 있다. 

 

메서드 체이닝을 지원하고, 객체 생성을 지연할 수 있다는 장점이 있다.

 

  • 장점
    • 생성자 패턴과 자바 빈 패턴의 문제점인 오버로딩 열거, 생성자 인자 순서 파악 및 잘못된 순서로 값을 넣는 실수 발생 문제를 해결한다.
    • 필수 멤버 변수와 선택적 멤버 변수를 분리해서 받을 수 있다 (필수 멤버변수는 빌더 생성자로 받고, 선택 멤버변수는 메서드 체이닝을 통해 선택적으로 받게)
    • 객체 생성 과정을 일관된 프로세스로 표현 가능하다. 
    • 객체 생성을 지연할 수 있고, Setter 메서드를 제공하지 않으므로 변경 가능성을 최소화한다
  • 단점
    • N개의 클래스에 N개의 빌더 클래스를 만들어야 해서 코드 복잡성이 증가한다. (롬복 라이브러리로 해결 가능)
  • 언제 사용할까 ?
    • 객체 생성 시 지정해야 할 멤버변수가 많은 경우 
    • 객체의 일관성 / 불변성을 지켜야 하는 경우 
    • 객체 생성을 다양한 멤버 변수의 조합으로 구성하고 싶을 때

 

빌더 패턴 적용 예시

// User 클래스
public class User {
    private final String username; // 필수 필드
    private final String password; // 필수 필드
    private final String email;    // 선택적 필드
    private final int age;         // 선택적 필드

    private User(UserBuilder builder) {
        this.username = builder.username;
        this.password = builder.password;
        this.email = builder.email;
        this.age = builder.age;
    }

    // Getter 메서드들...

    // 빌더 클래스
    public static class UserBuilder {
        private final String username; // 필수 필드
        private final String password; // 필수 필드
        private String email;           // 선택적 필드
        private int age;                // 선택적 필드

        public UserBuilder(String username, String password) {
            this.username = username;
            this.password = password;
        }

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

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

        // 빌더 패턴을 통한 객체 생성 메서드
        public User build() {
            return new User(this);
        }
    }
}

// 사용 예시
public class BuilderPatternExample {
    public static void main(String[] args) {
        User user1 = new User.UserBuilder("john_doe", "password123")
                .email("john@example.com")
                .age(25)
                .build();

        User user2 = new User.UserBuilder("jane_doe", "pass456")
                .email("jane@example.com")
                .build();
    }
}

 

프로토 타입 패턴

기존 객체를 복사해서 새로운 객체를 생성하는 디자인 패턴이다

  • 장점 
    • 객체 생성 비용이 감소한다. 
    • 객체 생성 과정에서 발생할 수 있는 오버헤드 감소 
    • 객체 생성 과정이 복잡한 경우 간소화 할 수 있다.
  • 단점
    • Cloneable 인터페이스를 구현해야 한다. 
    • 객체가 또 다른 객체를 참고하고 있는 경우, 얕은 복사/깊은 복사 문제에 대한 관리가 필요하다. 
    • 다른 객체와 복잡하게 관계가 맺어져 있는 경우 복제하기 어렵다

 

프로토 타입 패턴 적용 예시

// Cloneable 인터페이스를 구현한 프로토타입 클래스
class Sheep implements Cloneable {
    private String name;

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

    // Getter 및 Setter 메서드들...

    // clone 메서드 오버라이드
    @Override
    protected Sheep clone() {
        try {
            return (Sheep) super.clone();
        } catch (CloneNotSupportedException e) {
            // CloneNotSupportedException을 처리하는 코드
            return null;
        }
    }
}

// 프로토타입 패턴을 사용하는 예시 클래스
public class PrototypeExample {
    public static void main(String[] args) {
        // 원본 객체 생성
        Sheep originalSheep = new Sheep("Dolly");

        // 원본 객체를 복제하여 새로운 객체 생성
        Sheep clonedSheep = originalSheep.clone();

        // 객체 비교
        System.out.println("Original Sheep: " + originalSheep.getName());
        System.out.println("Cloned Sheep: " + clonedSheep.getName());
    }
}