디자인패턴 - 생성 패턴

김상운(개발둥이)·2022년 10월 9일
1

디자인패턴

목록 보기
1/3
post-thumbnail

생성 패턴의 종류

생성패턴이란: 생성패턴은 인스턴스를 만드는 절차를 추상화하는 패턴으로 객체를 생성 합성하는 방법이나 객체의 표현 방법과 시스템을 분리해준다.

  1. 싱글톤 패턴
  2. 팩토리 메서드 패턴
  3. 추상 팩토리 패턴
  4. 빌더 패턴
  5. 프로토타입 패턴

싱글톤 패턴

설명

인스턴스를 오직 한개만 제공하는 클래스

시스템 런타임, 환경 세팅에 대한 정보 등, 인스턴스가 여러개 일 때 문제가 생길 수 있는 경우가 있다. 인스턴스를 오직 한개만 만들어 제공하는 클래스가 필요하다.

구현 1

private 생성자에 static 메서드

public class Settings1 {

    private static Settings1 instance;

    private Settings1() { }

    public static Settings1 getInstance() {
        if (instance == null) {
            instance = new Settings1();
        }

        return instance;
    }

}

기본 생성자를 private 으로 하고 static 멤버 변수를 자기 참조형으로 하여 static 메서드를 통해 멤버 변수가 초기화 되지 않으면 초기화 하고 초기화가 되어있다면 세팅된 값을 반환한다.

문제점

멀티 쓰레드 환경에서 동시에 getInstance 메서드에 접근 시 쓰레드별 사용하는 객체가 서로다른 참조 값을 가지고 있을 수 있다.

구현 2

동기화 사용하여 멀티 쓰레드 환경에서 안전하게 만들기

public static synchronized Settings getInstance() {
 if (instance == null) {
 instance = new Settings();
 }
 return instance;
}

static 메서드인 getInstance 에 synchronized 키워드를 추가하여 lock을 걸어 메서드에 하나의 쓰레만 접근하게 한다.

문제점

getInstance 를 통해 객체를 생성 할 때마다 synchronized 키워드로 인해 lock 이 사용되므로 처리속도 및 메모리가 낭비된다.

구현 3

static inner 클래스 사용하기

public class Settings {

    private Settings() {
    }
    
    //멀티쓰레드 환경에서도 안전하다, lazy 로딩도 가능하다.
    private static class SettingsHolder {
        private static final Settings INSTANCE = new Settings();
    }

    public static Settings getInstance() {
        return SettingsHolder.INSTANCE;
    }

}

마찬가지로 기본 생성자를 private 하게 막는다.

static inner 클래스 내부에서 static 멤버 변수로 값을 세팅해놓기 때문에 언제나 같은 참조값을 같는 같은 객체이다.

static inner 클래스는 getInstance 메서드를 통해 호출할 때 jvm 에 클래스 로더에 의해서 로딩되므로 lazy 하게 초기화 된다. 싱글톤 객체가 진짜 필요할 때까지 초기화를 늦춘다.

팩토리 메서드 패턴

설명

  • 구체적으로 어떤 인스턴스를 만들지는 서브 클래스가 정한다.

  • 구체적인 팩토리에서 구체적인 제품 객체를 생성하는 패턴.

  • 다양한 구현체 (Product)가 있고, 그중에서 특정한 구현체를 만들 수 있는 다양한 팩토리 (Creator)를 제공할 수 있다.

기존 코드를 수정하지 않고 새로운 인스턴스를 여러 방법으로 생성할 수 있는 "확장에 열려있고 변경에 닫혀있는 객체 지향 원칙"을 만족하는 객체 생성 방법

구현

product

Ship

public class Ship {

    private String name;

    private String color;

    private String logo;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getColor() {
        return color;
    }

    public void setColor(String color) {
        this.color = color;
    }

    public String getLogo() {
        return logo;
    }

    public void setLogo(String logo) {
        this.logo = logo;
    }

    @Override
    public String toString() {
        return "Ship{" +
                "name='" + name + '\'' +
                ", color='" + color + '\'' +
                ", logo='" + logo + '\'' +
                '}';
    }
}

WhiteShip

public class WhiteShip extends Ship {

    public WhiteShip() {
        setName("whiteShip");
        setLogo("\uD83D\uDEE5️");
        setColor("white");
    }

}

BlackShip

public class BlackShip extends Ship {

    public BlackShip() {
        setName("blackShip");
        setLogo("⚓");
        setColor("black");
    }
}

WhiteShip, BlackShip 은 Ship 을 상속 받는다.

factory

ShipFactory

public interface ShipFactory {

    default Ship orderShip(String name, String email) {
        validate(name, email);

        prepareFor(name);

        Ship ship = createShip();

        sendEmailTo(email, ship);

        return ship;
    }

    Ship createShip();

    void validate(String name, String email);

    void prepareFor(String name);

    void sendEmailTo(String email, Ship ship);

}

공통적인 성질을 갖고 있는 제품 객체를 생성하는 팩토리를 추상화한다.

DefaultShipFactory

public abstract class DefaultShipFactory implements ShipFactory {

    @Override
    public void sendEmailTo(String email, Ship ship) {
        System.out.println(ship.getName() + " 다 만들었습니다.");
    }

    @Override
    public void validate(String name, String email) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("배 이름을 지어주세요.");
        }
        if (email == null || email.isBlank()) {
            throw new IllegalArgumentException("연락처를 남겨주세요.");
        }
    }

    @Override
    public void prepareFor(String name) {
        System.out.println(name + " 만들 준비 중");
    }

}

인터페이스의 default 메서드 에서 사용하는 메서드를 오버라이딩하여 구현한다.

WhiteShipFactory

public class WhiteShipFactory extends DefaultShipFactory {

    @Override
    public Ship createShip() {
        return new WhiteShip();
    }

}

BlackShipFactory

public class BlackShipFactory extends DefaultShipFactory {

    @Override
    public Ship createShip() {
        return new BlackShip();
    }
}

구체적인 factory 클래스이다. 여기서 product 객체를 생성한다.

client

public class Client {

    public static void main(String[] args) {
        Client client = new Client();
        client.print(new WhiteShipFactory(), "Whiteship", "keesun@mail.com");
        client.print(new BlackShipFactory(), "blackship", "keesun@mail.com");
    }

    private void print(ShipFactory shipFactory, String name, String email) {
        System.out.println(shipFactory.orderShip(name, email));
    }

}

다이어그램

추상 팩토리 패턴

설명

  • 서로 관련있는 여러 객체를 만들어주는 인터페이스.
  • 구체적으로 어떤 클래스의 인스턴스를(concrete product)를 사용하는지 감출 수 있다.
  • 클라이언트 코드에서 구체적인 클래스의 의존성을 제거한다.

팩토리 메소드 패턴을 사용하는 경우 직접 객체를 생성해 사용하는 것을 방지하고 서브 클래스에 위임함으로써 보다 효율적인 코드 제어를 할 수 있고 의존성을 제거한다

추상 팩토리 패턴은 팩토리 메소드 패턴에서 구현체 객체를 직접 생성하지 않고 서브 클래스에 위임하여 구체적인 클래스에 의존하지 않고 만들 수 있게 한다.

구현

위 예제인 팩토리 메소드 패턴의 팩토리 클래스의 부분이다.

public class WhiteshipFactory extends DefaultShipFactory {

    @Override
    public Ship createShip() {
        Ship ship = new Whiteship();
        ship.setAnchor(new WhiteAnchor());
        ship.setWheel(new WhiteWheel());
        return ship;
    }
}

문제

setAnchor 에서 WhiteAnchor 타입이 아닌 다른 타입으로 바뀔경우 전부 수정하여야 한다. -> OCP 위반

해결

Anchor

public interface Anchor {
}
public class WhiteAnchorPro implements Anchor{
}

Wheel

public interface Wheel{
}
public class WhiteWheelPro implements Wheel {
}

PartsFactory

public interface ShipPartsFactory {

    Anchor createAnchor();

    Wheel createWheel();

}
public class WhitePartsProFactory implements ShipPartsFactory {
    @Override
    public Anchor createAnchor() {
        return new WhiteAnchorPro();
    }

    @Override
    public Wheel createWheel() {
        return new WhiteWheelPro();
    }
}

factory

public class WhiteshipFactory extends DefaultShipFactory {

    private ShipPartsFactory shipPartsFactory;

    public WhiteshipFactory(ShipPartsFactory shipPartsFactory) {
        this.shipPartsFactory = shipPartsFactory;
    }

    @Override
    public Ship createShip() {
        Ship ship = new Whiteship();
        ship.setAnchor(shipPartsFactory.createAnchor());
        ship.setWheel(shipPartsFactory.createWheel());
        return ship;
    }
}

위에서 만든 ShipPartsFactory 를 주입받아 Ship 객체를 생성하는 과정에서 추상화를 사용하여 더 유연하게 생성한다.

다이어 그램

팩토리 메소드 패턴과 차이

  • 관점이 다르다.

    • 팩토리 메소드 패턴은 “팩토리를 구현하는 방법 (inheritance)”에 초점을 둔다.
    • 추상 팩토리 패턴은 “팩토리를 사용하는 방법 (composition)”에 초점을 둔다.
  • 목적이 조금 다르다.

    • 팩토리 메소드 패턴은 구체적인 객체 생성 과정을 하위 또는 구체적인 클래스로 옮기는 것이 목적.
    • 추상 팩토리 패턴은 관련있는 여러 객체를 구체적인 클래스에 의존하지 않고 만들 수 있게 해주는 것이 목적.

빌더 패턴

설명

  • 동일한 프로세스를 거쳐 다양한 구성의 인스턴스를 만드는 방법.
  • (복잡한) 객체를 만드는 프로세스를 독립적으로 분리할 수 있다.

생성자로 객체를 생성하는 경우에는 매개변수가 많아질수록 코드 리딩이 급격하게 떨어집니다. 빌더 패턴을 사용하면 매개변수가 많아져도 가독성을 높일 수 있습니다.

구현

TourPlan

public class TourPlan {

    private String title;

    private int nights;

    private int days;

    private LocalDate startDate;

    private String whereToStay;

    private List<DetailPlan> plans;

    public TourPlan() {
    }

    public TourPlan(String title, int nights, int days, LocalDate startDate, String whereToStay, List<DetailPlan> plans) {
        this.title = title;
        this.nights = nights;
        this.days = days;
        this.startDate = startDate;
        this.whereToStay = whereToStay;
        this.plans = plans;
    }

    @Override
    public String toString() {
        return "TourPlan{" +
                "title='" + title + '\'' +
                ", nights=" + nights +
                ", days=" + days +
                ", startDate=" + startDate +
                ", whereToStay='" + whereToStay + '\'' +
                ", plans=" + plans +
                '}';
    }

    public void addPlan(int day, String plan) {
        this.plans.add(new DetailPlan(day, plan));
    }
}

TourPlan 객체를 생성자로 생성할 경우 매개변수도 많고 복잡하게 생성하여야 한다. -> 빌더 패턴을 적용해보자.

builder - interface

public interface TourPlanBuilder {

    TourPlanBuilder nightsAndDays(int nights, int days);

    TourPlanBuilder title(String title);

    TourPlanBuilder startDate(LocalDate localDate);

    TourPlanBuilder whereToStay(String whereToStay);

    TourPlanBuilder addPlan(int day, String plan);

    TourPlan getPlan();

}

builder - concrete

public class DefaultTourBuilder implements TourPlanBuilder {

    private String title;

    private int nights;

    private int days;

    private LocalDate startDate;

    private String whereToStay;

    private List<DetailPlan> plans;

    @Override
    public TourPlanBuilder nightsAndDays(int nights, int days) {
        this.nights = nights;
        this.days = days;
        return this;
    }

    @Override
    public TourPlanBuilder title(String title) {
        this.title = title;
        return this;
    }

    @Override
    public TourPlanBuilder startDate(LocalDate startDate) {
        this.startDate = startDate;
        return this;
    }

    @Override
    public TourPlanBuilder whereToStay(String whereToStay) {
        this.whereToStay = whereToStay;
        return this;
    }

    @Override
    public TourPlanBuilder addPlan(int day, String plan) {
        if (this.plans == null) {
            this.plans = new ArrayList<>();
        }

        this.plans.add(new DetailPlan(day, plan));
        return this;
    }

    @Override
    public TourPlan getPlan() {
        return new TourPlan(title, nights, days, startDate, whereToStay, plans);
    }
}

빌더 패턴을 사용하기 위해 만들고자 하는 객체의 클래스와 동일한 필드를 가진 builder 클래스를 생성한다.

그리고 필드와 대응하는 메서드를 통해 builder 객체의 값을 하나하나 채운다.

빌더 객체를 통해 만들고자 하는 객체를 생성하고 싶은 경우 getInstance 메서드를 호출하여 builder 객체 내부에 만들고자 하는 인스턴스와 동일한 필드를 가지고 있으므로 builder 객체의 필드 값을 통해 새로운 객체를 생성한다.

director

public class TourDirector {

    private TourPlanBuilder tourPlanBuilder;

    public TourDirector(TourPlanBuilder tourPlanBuilder) {
        this.tourPlanBuilder = tourPlanBuilder;
    }

    public TourPlan cancunTrip() {

        return tourPlanBuilder
                .title("칸쿤 여행")
                .nightsAndDays(2, 3)
                .startDate(LocalDate.of(2020, 12, 9))
                .whereToStay("리조트")
                .addPlan(0, "맘마")
                .addPlan(1, "식사")
                .getPlan();
    }

}

director 를 통해 미리 세팅해놓은 객체를 반환할 수 있다.

다이어 그램

장점과 단점

장점

• 만들기 복잡한 객체를 순차적으로 만들 수 있다.
• 복잡한 객체를 만드는 구체적인 과정을 숨길 수 있다.
• 동일한 프로세스를 통해 각기 다르게 구성된 객체를 만들 수도 있다.
• 불완전한 객체를 사용하지 못하도록 방지할 수 있다.

단점

• 원하는 객체를 만들려면 빌더부터 만들어야 한다.
• 구조가 복잡해 진다. (트레이드 오프)

프로토타입 패턴

설명

  • 기존 인스턴스를 복제하여 새로운 인스턴스를 만드는 방법
  • 객체를 만들 때마다 데이터를 매번 파싱하는 대신에, 프로토타입 패턴을 사용하여 이미 있는 객체를 복사하여 새로운 객체를 만들어 낼 수 있다.

구현

public class GithubRepository {

    private String user;

    private String name;

    public String getUser() {
        return user;
    }

    public void setUser(String user) {
        this.user = user;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

복사 대상

public class GithubIssue implements Cloneable {

    private int id;

    private String title;

    private GithubRepository repository;

    public GithubIssue(GithubRepository repository) {
        this.repository = repository;
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public GithubRepository getRepository() {
        return repository;
    }

    public String getUrl() {
        return String.format("https://github.com/%s/%s/issues/%d",
                repository.getUser(),
                repository.getName(),
                this.getId());
    }

    @Override
    protected Object clone() throws CloneNotSupportedException {
        GithubRepository repository = new GithubRepository();
        repository.setUser(this.repository.getUser());
        repository.setName(this.repository.getName());

        GithubIssue githubIssue = new GithubIssue(repository);
        githubIssue.setId(this.id);
        githubIssue.setTitle(this.title);

        return githubIssue;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        GithubIssue that = (GithubIssue) o;
        return id == that.id && Objects.equals(title, that.title) && Objects.equals(repository, that.repository);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, title, repository);
    }
}

Object 의 clone 메서드를 사용하기 위해서는 복사 대상 클래스는 Clonable 인터페이스를 구현하여야 한다.

protected native Object clone() throws CloneNotSupportedException;

Object 클래스의 clone 메서드는 위와 같이 protected 이며 CloneNotSupportedException 을 예외로 던지고 있으므로 바로 사용할 수 없다.

client 코드

public class App {

    public static void main(String[] args) throws CloneNotSupportedException {
        GithubRepository repository = new GithubRepository();
        repository.setUser("whiteship");
        repository.setName("live-study");

        GithubIssue githubIssue = new GithubIssue(repository);
        githubIssue.setId(1);
        githubIssue.setTitle("1주차 과제: JVM은 무엇이며 자바 코드는 어떻게 실행하는 것인가.");

        String url = githubIssue.getUrl();
        System.out.println(url);

        GithubIssue clone = (GithubIssue) githubIssue.clone();
        System.out.println(clone.getUrl());

        repository.setUser("Keesun");

        System.out.println(clone != githubIssue);
        System.out.println(clone.equals(githubIssue));
    }

}

Object 의 clone 메서드는 얕은 복사이다. 깊은 복사를 위해서는 따로 clone 메서드에 구현해줘야 한다.

장점과 단점

장점

  • 복잡한 객체를 만드는 과정을 숨길 수 있다.
  • 기존 객체를 복제하는 과정이 새 인스턴스를 만드는 것보다 비용(시간 또는 메모리)적인 면에서 효율적일 수도 있다.
  • 추상적인 타입을 리턴할 수 있다.

단점

  • 복제한 객체를 만드는 과정 자체가 복잡할 수 있다. (특히, 순환 참조가 있는 경우)
profile
공부한 것을 잊지 않기 위해, 고민했던 흔적을 남겨 성장하기 위해 글을 씁니다.

0개의 댓글