Light Blue Pointer
본문 바로가기
TIL(CS)

Creational patterns

by 개발바닥곰발바닥!!! 2024. 4. 18.

Singleton Pattern

Instance가 딱 하나만 존재할 수 있는 패턴

class 안에 static instance가 있고

static getInstance() 메서드로

instance가 이미 존재한다면 그것을 return,

instance가 존재하지 않는다면 생성하고 static에 넣은 후에 그것을 return

Prototype Pattern (Clone)

그 클래스의 코드에 의존적이지 않은 채로 존재하는 객체를 복사할 수 있게 해준다

Prototype Pattern이 나온 배경

객체를 복사해서 똑같은 복제본을 만들고 싶은 상황

똑같은 클래스의 객체를 하나 더 생성한다

원래 객체의 모든 필드에서 값을 복사해다 새로운 객체에다가 붙여넣는다

이런 상황에서 생기는 문제점들

  • 어떤 필드는 private이라 접근이 불가능해 완전히 똑같은 복제본을 만들어낼 수 없다
  • 그 객체의 복사본을 만들려면 그 객체가 어떤 클래스인지 알아야 해서 코드가 그 클래스에 의존적이게 된다
  • 어떤 객체는 인터페이스로 받아와지기에 인터페이스만 알고있을 경우도 있다

프로토타입 패턴으로 해결

프로토타입 패턴은 복제하는 과정을 복제당하는 실제 객체에게 위임한다

복제를 지원하는 모든 객체에 공통적인 인터페이스를 지원한다

이 인터페이스는 그 객체를 복제하면서 그 클래스에 대해 의존성이 생기지 않게 한다(This interface lets you clone an object without coupling your code to the class of that object.)

그 객체 내의 함수이기에 private 필드들도 다 접근할 수 있고 본인의 클래스도 알고있다

복제를 지원하는 객체를 prototype이라고 부른다

구현

// Base prototype.
abstract class Shape is
    field X: int
    field Y: int
    field color: string

    // A regular constructor.
    constructor Shape() is
        // ...

    // The prototype constructor. A fresh object is initialized
    // with values from the existing object.
    constructor Shape(source: Shape) is
        this()
        this.X = source.X
        this.Y = source.Y
        this.color = source.color

    // The clone operation returns one of the Shape subclasses.
    abstract method clone():Shape


// Concrete prototype. The cloning method creates a new object
// in one go by calling the constructor of the current class and
// passing the current object as the constructor's argument.
// Performing all the actual copying in the constructor helps to
// keep the result consistent: the constructor will not return a
// result until the new object is fully built; thus, no object
// can have a reference to a partially-built clone.
class Rectangle extends Shape is
    field width: int
    field height: int

    constructor Rectangle(source: Rectangle) is
        // A parent constructor call is needed to copy private
        // fields defined in the parent class.
        super(source)
        this.width = source.width
        this.height = source.height

    method clone():Shape is
        return new Rectangle(this)


class Circle extends Shape is
    field radius: int

    constructor Circle(source: Circle) is
        super(source)
        this.radius = source.radius

    method clone():Shape is
        return new Circle(this)


// Somewhere in the client code.
class Application is
    field shapes: array of Shape

    constructor Application() is
        Circle circle = new Circle()
        circle.X = 10
        circle.Y = 10
        circle.radius = 20
        shapes.add(circle)

        Circle anotherCircle = circle.clone()
        shapes.add(anotherCircle)
        // The `anotherCircle` variable contains an exact copy
        // of the `circle` object.

        Rectangle rectangle = new Rectangle()
        rectangle.width = 10
        rectangle.height = 20
        shapes.add(rectangle)

    method businessLogic() is
        // Prototype rocks because it lets you produce a copy of
        // an object without knowing anything about its type.
        Array shapesCopy = new Array of Shapes.

        // For instance, we don't know the exact elements in the
        // shapes array. All we know is that they are all
        // shapes. But thanks to polymorphism, when we call the
        // `clone` method on a shape the program checks its real
        // class and runs the appropriate clone method defined
        // in that class. That's why we get proper clones
        // instead of a set of simple Shape objects.
        foreach (s in shapes) do
            shapesCopy.add(s.clone())

        // The `shapesCopy` array contains exact copies of the
        // `shape` array's children.

 

Prototype Pattern을 사용하는 경우

코드가 복제해야 하는 객체의 구체적인 클래스에 의존하지 않아야 할 때

각각의 객체를 초기화하는 방식만 다른 하위 클래스의 수를 줄이고 싶을 때

복잡한 구성을 가진 클래스는 중복을 줄이기 위해 하위 클래스를 많이 생성한다

여러 하위 클래스를 생성하고 일반적인 구성 코드를 그 생성자에 넣어서 중복 문제를 해결할 수 있지만 더미 하위 클래스가 많아진다

Prototype 패턴을 사용하면 다양한 방식으로 구성된 미리 빌드된 객체 세트를 프로토타입으로 사용할 수 있다

특정 구성과 일치하는 파위 클래스를 인스턴스화하는 대신 클라이언트는 적절한 프로토타입을 찾아 복제할 수 있다

장점

  • 객체를 복제할 때 구체적인 클래스에 결합하지 않고 복제할 수 있다
  • 복제된 미리 빌드된 프로토타입을 사용하여 반복되는 초기화 코드를 제거할 수 있다
  • 복잡한 객체를 더 편리하게 생성할 수 있다

단점

  • 원형 참조를 가진 복잡한 객체를 복제하는 것은 매우 까다로울 수 있다

Builder

복잡한 객체를 단계별로 만들 수 있게 해준다 → Spring 의 @Builder

Builder Pattern이 나온 배경

많은 필드를 가지는 객체를 생성하려면 생성자가 길고 지저분해진다

1/10의 인스턴스만 사용하는 필드에도 값을 넣으려면 9/10의 경우는 사용하지 않을 필드에 값을 넣는 코드도 작성해야 한다

해결

객체 생성 코드를 클래스 밖으로 빼서 Builder로 만든다

이 패턴은 객체 생성 과정을 단계별로 나누고, 필요한 단계만 사용할 수 있게 한다

구조

구현

// Using the Builder pattern makes sense only when your products
// are quite complex and require extensive configuration. The
// following two products are related, although they don't have
// a common interface.
class Car is
    // A car can have a GPS, trip computer and some number of
    // seats. Different models of cars (sports car, SUV,
    // cabriolet) might have different features installed or
    // enabled.

class Manual is
    // Each car should have a user manual that corresponds to
    // the car's configuration and describes all its features.

// The builder interface specifies methods for creating the
// different parts of the product objects.
interface Builder is
    method reset()
    method setSeats(...)
    method setEngine(...)
    method setTripComputer(...)
    method setGPS(...)

// The concrete builder classes follow the builder interface and
// provide specific implementations of the building steps. Your
// program may have several variations of builders, each
// implemented differently.
class CarBuilder implements Builder is
    private field car:Car

    // A fresh builder instance should contain a blank product
    // object which it uses in further assembly.
    constructor CarBuilder() is
        this.reset()

    // The reset method clears the object being built.
    method reset() is
        this.car = new Car()

    // All production steps work with the same product instance.
    method setSeats(...) is
        // Set the number of seats in the car.

    method setEngine(...) is
        // Install a given engine.

    method setTripComputer(...) is
        // Install a trip computer.

    method setGPS(...) is
        // Install a global positioning system.

    // Concrete builders are supposed to provide their own
    // methods for retrieving results. That's because various
    // types of builders may create entirely different products
    // that don't all follow the same interface. Therefore such
    // methods can't be declared in the builder interface (at
    // least not in a statically-typed programming language).
    //
    // Usually, after returning the end result to the client, a
    // builder instance is expected to be ready to start
    // producing another product. That's why it's a usual
    // practice to call the reset method at the end of the
    // `getProduct` method body. However, this behavior isn't
    // mandatory, and you can make your builder wait for an
    // explicit reset call from the client code before disposing
    // of the previous result.
    method getProduct():Car is
        product = this.car
        this.reset()
        return product

// Unlike other creational patterns, builder lets you construct
// products that don't follow the common interface.
class CarManualBuilder implements Builder is
    private field manual:Manual

    constructor CarManualBuilder() is
        this.reset()

    method reset() is
        this.manual = new Manual()

    method setSeats(...) is
        // Document car seat features.

    method setEngine(...) is
        // Add engine instructions.

    method setTripComputer(...) is
        // Add trip computer instructions.

    method setGPS(...) is
        // Add GPS instructions.

    method getProduct():Manual is
        // Return the manual and reset the builder.

// The director is only responsible for executing the building
// steps in a particular sequence. It's helpful when producing
// products according to a specific order or configuration.
// Strictly speaking, the director class is optional, since the
// client can control builders directly.
class Director is
    // The director works with any builder instance that the
    // client code passes to it. This way, the client code may
    // alter the final type of the newly assembled product.
    // The director can construct several product variations
    // using the same building steps.
    method constructSportsCar(builder: Builder) is
        builder.reset()
        builder.setSeats(2)
        builder.setEngine(new SportEngine())
        builder.setTripComputer(true)
        builder.setGPS(true)

    method constructSUV(builder: Builder) is
        // ...

// The client code creates a builder object, passes it to the
// director and then initiates the construction process. The end
// result is retrieved from the builder object.
class Application is

    method makeCar() is
        director = new Director()

        CarBuilder builder = new CarBuilder()
        director.constructSportsCar(builder)
        Car car = builder.getProduct()

        CarManualBuilder builder = new CarManualBuilder()
        director.constructSportsCar(builder)

        // The final product is often retrieved from a builder
        // object since the director isn't aware of and not
        // dependent on concrete builders and products.
        Manual manual = builder.getProduct()

Builder Pattern을 사용하는 경우

Telescoping constructor 를 제거하기 위해

Telescoping constructor 필수 매개변수 1개만 받는 생성자, 필수 매개변수 1개와 선택 매개변수 1개를 받는 생성자, 선택 매개변수 2개를 받는 생성자 등의 형태로 매개변수 개수만큼 생성자를 오버로딩하는 방식

**class** **Pizza** {
    Pizza(**int** size) { ... }
    Pizza(**int** size, **boolean** cheese) { ... }
    Pizza(**int** size, **boolean** cheese, **boolean** pepperoni) { ... }
    // ...

Product를 다양하게 표현할 수 있게 생성하는 코드를 원할 때

돌로 된 집과 나무로 된 집을 동일한 코드를 사용해서 표현할 수 있다

빌더 패턴은 제품의 다양한 표현을 구성하는 것이 유사한 단계를 포함한다

세부사항만 다른 경우에 적용할 수 있다

기본 빌더 인터페이스는 모든 가능한 구성 단계를 정의하고, 구체적인 빌더는 제품의 특정 표현을 구성하기 위해 이러한 단계를 구현한다

디렉터 클래스는 구성 순서를 안내한다

// Product 클래스
class Car {
    private String engine;
    private String body;
    private String wheels;

    // 생성자는 private로 선언하여 외부에서 직접 생성하지 않도록 합니다.
    private Car(String engine, String body, String wheels) {
        this.engine = engine;
        this.body = body;
        this.wheels = wheels;
    }

    @Override
    public String toString() {
        return "Car [engine=" + engine + ", body=" + body + ", wheels=" + wheels + "]";
    }

    // Car 클래스의 빌더 클래스 정의
    static class Builder {
        private String engine;
        private String body;
        private String wheels;

        public Builder() {
            // 기본값 설정 또는 초기화 작업
            this.engine = "Basic Engine";
            this.body = "Basic Body";
            this.wheels = "Basic Wheels";
        }

        // 엔진 설정 메서드
        public Builder withEngine(String engine) {
            this.engine = engine;
            return this;
        }

        // 바디 설정 메서드
        public Builder withBody(String body) {
            this.body = body;
            return this;
        }

        // 휠 설정 메서드
        public Builder withWheels(String wheels) {
            this.wheels = wheels;
            return this;
        }

        // Car 객체 생성 메서드
        public Car build() {
            return new Car(engine, body, wheels);
        }
    }
}

// Main 클래스
public class Main {
    public static void main(String[] args) {
        // 다양한 표현의 자동차를 만드는 예시
        Car basicCar = new Car.Builder().build();
        System.out.println("Basic Car: " + basicCar);

        Car sportsCar = new Car.Builder()
                            .withEngine("Powerful Engine")
                            .withBody("Sporty Body")
                            .withWheels("Alloy Wheels")
                            .build();
        System.out.println("Sports Car: " + sportsCar);

        Car familyCar = new Car.Builder()
                            .withBody("Spacious Body")
                            .build();
        System.out.println("Family Car: " + familyCar);
    }
}

Composite tree나 다른 복잡한 객체를 구성할 때 빌더를 사용

빌더 패턴을 사용하면 제품을 단계적으로 구성할 수 있다

일부 단계를 나중에 실행해도 최종 Product에는 영향을 미치지 않는다

재귀적으로 단계를 호출할 수도 있어서 객체 트리를 구축할 때 유용하다

빌더는 구성 단계를 실행하는 동안 미완성된 Product를 노출하지 않아서 클라이언트 코드가 불완전한 결과를 가져오지 못하게 한다

builder 패턴을 이용하면 아래처럼 생성 단계를 끊어서 만들 수 있다

Java Spring 예시)

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class User {
    private String username;
    private String email;
    private boolean isActive;
    private int age;
}

public class Main {
    public static void main(String[] args) {
        // 중간에 빌더를 사용하여 일부 필드만 설정
        User user = User.builder()
                        .username("john")
                        .build();

        // 일부 작업 수행
        // ...

        // 다른 작업을 수행한 후 다시 빌더를 사용하여 나머지 필드 설정
        user = User.builder()
                   .from(user) // 이전에 만든 빌더로부터 상태를 가져옴
                   .email("john@example.com")
                   .isActive(true)
                   .age(30)
                   .build();

        System.out.println(user);
    }
}

장점

  • 객체를 단계적으로 만들 수 있다, 혹은 재귀적으로도 만들 수 있다
  • 동일한 코드로 다양한 Product 표현을 할 수 있다.
  • Single Responsibility Principle(단일 책임 원칙) : Product의 비즈니스 로직으로부터 복잡한 생성 코드를 분리할 수 있다

단점

  • builder 패턴은 여러 새로운 클래스를 생성해야 하므로 코드가 복잡해진다

Factory Method = Virtual Constructor

팩토리 메서드는 상위클래스에서 객체를 생성하는 인터페이스를 제공한다, 하위클래스가 만들어질 객체의 타입을 바꿀 수 있다

객체 생성 호출을 factory method를 호출하는 것으로 변경한다

factory method 내에서 생성자를 호출한다

factory method가 반환하는 객체들을 product라고 한다

구조

  • Product : 생성자 및 해당 서브클래스에서 생성될 수 있는 모든 객체에 대한 공통 인터페이스를 선언
  • Creator : 새로운 제품 객체를 반환하는 팩토리 메서드를 선언, 이 메서드의 리턴 타입은 Product 인터페이스와 일치해야 한다
  • createProduct() 메서드를 추상으로 선언하여 모든 서브클래스가 자체 버전의 메서드를 구현하도록 강제
  • Product 생성은 생성자의 주요 책임이 아니라서 그런 로직을 구체적인 Product 클래스에서 분리함
  • 팩토리 메서드는 새 인스턴스 말고도 기존 객체를 반환할 수도 있다

구현

// 음료 추상 클래스
abstract class Beverage {
    // 제조 공정을 나타내는 추상 메서드
    abstract void brew();

    // 음료를 서빙하는 메서드
    void serve() {
        System.out.println("서빙되는 중...");
    }
}

// 커피 클래스
class Coffee extends Beverage {
    @Override
    void brew() {
        System.out.println("커피를 내리는 중...");
    }
}

// 차 클래스
class Tea extends Beverage {
    @Override
    void brew() {
        System.out.println("차를 우리는 중...");
    }
}

// 공장 인터페이스
interface BeverageFactory {
    // Factory Method
    Beverage createBeverage();
}

// 커피 공장
class CoffeeFactory implements BeverageFactory {
    @Override
    public Beverage createBeverage() {
        return new Coffee();
    }
}

// 차 공장
class TeaFactory implements BeverageFactory {
    @Override
    public Beverage createBeverage() {
        return new Tea();
    }
}

// 클라이언트 클래스
public class Client {
    public static void main(String[] args) {
        // 커피를 만드는 공장 생성
        BeverageFactory coffeeFactory = new CoffeeFactory();
        // 커피 생성
        Beverage coffee = coffeeFactory.createBeverage();
        // 커피 제조 공정 실행
        coffee.brew();
        // 커피 서빙
        coffee.serve();

        // 차를 만드는 공장 생성
        BeverageFactory teaFactory = new TeaFactory();
        // 차 생성
        Beverage tea = teaFactory.createBeverage();
        // 차 제조 공정 실행
        tea.brew();
        // 차 서빙
        tea.serve();
    }
}

Factory Method를 사용하는 경우

코드가 작업해야 하는 객체의 정확한 유형 및 종속성을 사전에 알 수 없을 때

Factory Method는 제품 생성 코드를 실제로 제품을 사용하는 코드로부터 분리한다

따라서 나머지 코드와 독립적으로 제품 생성 코드를 확장하기가 더 쉽다

예를 들어, 앱에 새로운 제품 유형을 추가하려면 새로운 생성자 하위 클래스를 만들고 그 안에 팩토리 메서드를 재정의하기만 하면 된다

라이브러리 또는 프레임워크의 내부 구성 요소를 확장할 수 있는 방법을 제공하려는 경우

라이브러리 또는 프레임워크의 기본 동작을 확장하는 가장 쉬운 방법은 상속이다

이 경우에 프레임워크는 어떻게 서브클래스를 사용해야 하는지를 알 수 있을까?

프레임워크 전반에 걸쳐 구성요소를 구성하는 코드를 단일 팩토리 메서드로 줄이고 이 메서드를 재정의할 수 있도록 한다

이렇게 하면 구성 요소 자체를 확장하는 것 외에도 누구나 이 메서드를 재정의할 수 있다

예시

오픈 소스 UI 프레임워크를 사용하여 앱을 작성한다고 가정해보자

앱에는 둥근 버튼이 필요하지만 프레임워크는 네모 모양의 버튼만 제공한다

표준 Button 클래스를 RoundButton 서브클래스로 확장하면 이제 메인 UIFramework 클래스가 기본 버튼 대신 새 버튼 서브클래스를 사용하도록 설정해야 한다

이를 위해 기본 프레임워크 클래스에서 UIWithRoundButtons 하위 클래스를 생성하고 createButton 메서드를 재정의한다. 이 메서드는 기본 클래스에서 Button 객체를 반환하지만, 서브 클래스에서는 RoundButton 객체를 반환하도록 한다

이제 UIFramework 대신 UIWithRoundButtons 클래스를 사용하면 된다

객체를 재사용하여 시스템 리소스를 절약하려는 경우

대규모이고 리소스 집약적인 객체인 데이터베이스 연결, 파일 시스템 및 네트워크 리소스와 같은 객체를 다룰 때 종종 발생하는 경우이다

기존 객체를 재사용하려면 필요한 단계

  • 생성된 모든 객체를 추적할 저장소를 생성
  • 누군가 객체를 요청하면 프로그램은 해당 풀 내에서 무료 객체를 찾아야 함
  • 그것을 클라이언트 코드에 반환함
  • 무료 객체가 없는 경우 프로그램은 새로운 객체를 만들어야 함
  • 그리고 그것을 풀에 추가함

장점

  • 생성자와 구체적인 제품 간의 강한 결합을 피할 수 있다
  • Single Responsibility Principle(단일 책임 원칙). 프로그램에서 제품 생성 코드를 한 곳으로 이동하여 코드를 더 쉽게 유지할 수 있다
  • Open/Closed Principle(개방/폐쇄 원칙). 기존 클라이언트 코드를 손상시키지 않고 프로그램에 새로운 유형의 제품을 도입할 수 있다

단점

  • 패턴을 구현하기 위해 많은 새로운 하위클래스를 도입해야 하므로 코드가 복잡해질 수 있다
  • 최상의 경우는 기존의 생성자 클래스 계층 구조에 패턴을 도입할 때이다

'TIL(CS)' 카테고리의 다른 글

클러스터링과 샤딩의 차이점  (0) 2024.05.28
NoSQL과 RDBMS의 차이  (0) 2024.05.28
디자인 패턴  (0) 2024.04.17
HTTP/HTTPS  (0) 2024.04.11
SSL/TLS  (0) 2024.04.11