생성디자인패턴(2) - 추상 팩토리패턴, 빌더패턴, 프로토타입 패턴

추상 팩토리 패턴

서로 관련있는 여러객체를 만들어주는 인터페이스
클라이언트쪽에 초점이있는 패턴
목적은 클라이언트코드 (팩토리에서 인스턴스를 만드는) 를 인터페이스기반으로 만들수있게끔 해주는 패턴

public class WhiteshipFactory extends DefaultShipFactory {

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

클라이언트의 코드, 구체적인 코드에 의존하고있다.(new WhiteAnchor()) , new WhiteWheel())
WhiteAnchor, WhiteWheel 이아닌 다른것으로 스타일을 바꾸고싶다면 그럴때마다 코드가 변경이된다.
이부분을 변경하지않고 새 제품을 늘릴수있는방법에 대한문제를 해결할수있다.


public interface Anchor {}
public interface Wheel {}

public interface ShipPartsFactory {// 추상팩토리

    Anchor createAnchor();
    Wheel createWheel(); 
    
    
}

public class WhiteshipPartsFactory implements ShipPartsFactory { 
// 추상팩토리의 구체적인 팩토리

    @Override
    public Anchor createAnchor() {
        return new WhiteAnchor();
    }

    @Override
    public Wheel createWheel() {
        return new WhiteWheel();
    }
}
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;
    }
}

이상태에서 pro제품을 추가로만들것이다. 이전같으면
ship.setAnchor(new WhiteAnchorPro()) 처럼 바꿧어야했지만,

public class WhiteAnchorPro implements Anchor{
}
public class WhiteWheelPro implements Wheel {
}

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

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

코드는변경하지않고 다음과같이 인터페이스와 클래스만 추가해주면된다.

  public static void main(String[] args) {
        ShipFactory shipFactory = new WhiteshipFactory(new WhiteshipPartsFactory());
        Ship ship = shipFactory.createShip();
        System.out.println(ship.getAnchor().getClass());
        System.out.println(ship.getWheel().getClass());
    }

WhiteAnchor, WhiteWheel 이 들어간것을 확인할수있다.

추상팩토리메소드 패턴과 다른점은?

팩토리메서드 - 객체를 만드는과정을 추상화, 구체적인타입의 인스턴스를 만드는과정을 숨기고 그위에 추상화되어있
팩토리를 제공하는것.
팩토리를 구현하는방법에 초점을둔다
구체적인 객체생성과정을 하위또는 구체적인 클래스로 옮기는것이 목적

추상팩토리 - 객체를만드는과정을 추상화, 클라이언트의 관점에서 팩토리를통해서 추상화된인터페이스만 클라이언트가 쓸수있게해줌
팩토리를 사용하는방법에 초점을둔다
여러 객체를 구체적인 클래스에 의존하지않고 만들수있게 해주는것이 목적

빌더패턴

public static void main(String[] args){
      TourPlan shortTrip = new TourPlan();
        shortTrip.setTitle("영주여행 ");
        shortTrip.setStartDate(LocalDate.of(2021, 11, 06));
        
       TourPlan tourPlan = new TourPlan();
        tourPlan.setTitle("칸쿤 여행");
        tourPlan.setNights(2);
        tourPlan.setDays(3);
        tourPlan.setStartDate(LocalDate.of(2020, 12, 9));
        tourPlan.setWhereToStay("리조트");
        tourPlan.addPlan(0, "체크인 이후 짐풀기");
        tourPlan.addPlan(0, "저녁 식사");
        tourPlan.addPlan(1, "조식 부페에서 식사");
        tourPlan.addPlan(1, "해변가 산책");
        tourPlan.addPlan(1, "점심은 수영장 근처 음식점에서 먹기");
        tourPlan.addPlan(1, "리조트 수영장에서 놀기");
        tourPlan.addPlan(1, "저녁은 BBQ 식당에서 스테이크");
        tourPlan.addPlan(2, "조식 부페에서 식사");
        tourPlan.addPlan(2, "체크아웃"); 
        
        
}

다음과같은 예시로 인해 인스턴스가 불안정한상태로 만들어질수있고,
생성자를 만들어야할때 경우의수도 많아서 장황해질수있다.

빌더패턴은 프로세스를 거쳐 다양한 구성의 인스턴스를 만드는 방법이며, 복잡한 객체를 만드는 프로세스를 독립적으로 분리할수있다.

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();

}

다음과같이 빌더의 리턴타입으로 여러가지만든다
빌더를 통해 메소드 체이닝
클라이언트쪽에선 nightsAndDays 라는 메소드를 호출하게되면 TourPlanBuilder 타입의 인스턴스를 받게되고
그럼 그안에서 제공하는 또다른 메소드를 사용해야된다.

getPlan을 만나기전까지
getPlan에선 인스턴스의 상태를 검증, 불안정한지아닌지 검증도 할수있다.

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);
    }
}
//클라이언트쪽 코드 
 public static void main(String[] args) {
        TourPlanBuilder builder = new DefaultTourBuilder();
        
        builder.title("칸쿤여행")
        .nightsAndDays(2,3)
        .startDate(LocalDate.of(2021,11,6))
        .whereToStay("리조트")
        .addPlan(0,"체크인하고 짐풀기")
        .getPlan();
        
        TourPlan longBeachTrip = builder.title("롱비치")
        .startDate(LocalDate.of(2021,06,15))
        .getPlan();
        
        
    }

빌더패턴이 없었으면 장황한 생성자들과, 생성자속 많은 null값을이 포함되어있는 보였을것이다.
위코드를 디렉터로 따로 빼서 줄일수도있다.

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(0, "저녁 식사")
                .getPlan();
    }

    public TourPlan longBeachTrip() {
        return tourPlanBuilder.title("롱비치")
                .startDate(LocalDate.of(2021, 7, 15))
                .getPlan();
    }
}

디렉터를 만들어놓고

  public static void main(String[] args) {
        TourDirector director = new TourDirector(new DefaultTourBuilder());
        TourPlan tourPlan = director.cancunTrip();
        TourPlan tourPlan1 = director.longBeachTrip();
    }

빌더패턴의 장점

  • 만들기 복잡한객체를 순차적으로 만들수있음
    => 빌더패턴을 하나만 만들었지만, 빌더패턴안에 빌더패턴을 넣을수도있다.

  • 복잡한 객체를 만드는 구체적 과정을 숨길수있다.
    => 디렉터를 통해 숨겼다.

  • 동일한 프로세스를 통해 각기 다르게 구성된 객체를 만들수 있다.
    => TourPlan 타입이지만 TourPlan의 하위타입의 내용이나올수도있고,
    ex) 칸쿤여행중VIP만갈수있는 여행에대한 객체를 만들수있다던지

  • 불완전한 객체를 사용하지못하도록 방지할수있다.
    => getPlan()을통해 객체를 검증할수도있다

단점

  • TourPlan을 만들기위해 디렉터나 빌드를 먼저 만들어야한다.
    => 객체를 좀더 만들어야하니까 단점이될수있다.

  • 대부분의 디자인패턴이 가지고있지만, 구조가 복잡해진다.
    =>적용전 모습과 달리 패턴적용후에는 빌더,디렉터까지 거쳐서 만들어야한다.

빌더패턴 적용사례

StringBuilder

 public static void main(String[] args) {
        StringBuilder stringBuilder = new StringBuilder();
        String result = stringBuilder.append("yj").append("youngjin").toString();
        System.out.println(result);
    }

Stream

 public static void main(String[] args) {
        Stream<String> names = Stream.<String>builder().add("yj").add("youngjin").build();
        names.forEach(System.out::println);
    }

등 ~~builder 로끝나는 네임들은 빌더패턴

프로토타입 패턴

기존 인스턴스를 복제하여 새로운 인스턴스를 만드는방법

복제기능을 갖추고있는 기존인스턴스를 프로토타입으로 사용해 새 인스턴스를 만들수있음

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 {

    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());
    }
}


  public static void main(String[] args) {
        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 githubIssue2 = githubIssue.clone(); //이런식으로 
사용하고싶다.34의 이슈가 나올때 
githubIssue.setId(2);
githubIssue.setTitle("2주차 과제:");

클론을 만들었다고 가정했을때
GithubIssue clone = githubIssue.clone();
clone == githubIssue => false
clone.equals(githubIssue) => true

Object의 clone() 메소드를 마냥 쓸수없는 이유는
Object안에clone()의 접근제어자가 protected 로되어있기 때문이다.

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);
    }
}

자바가 제공하는 clone의 기본기능을 사용하는것도 가능하다 (이때는 얕은복제)

clone의 기본기능을 사용할경우

GithubIssue clone = (GIthubIssue) githubIssue.clone();

clone을 사용하면 좋은이유는 결국
새로운 이슈가 나올때마다 반복되었던 코드들이 db를거치거나 네트워크를 거쳐서 리소스를 많이잡아먹는경우가 된다면 그 부분의 코드를 반복하지않게되어 이득을 볼수있다.

자바가 기본제공하는 클론은 '얕은복사'다

https://velog.io/@dudwls0505/clone-%EB%A6%AC%ED%94%8C%EB%A0%89%EC%85%98
클론에대해서 애매하다면 허접하지만 이글을참조..

프로토타입의 장점

  • 복잡한 객체를 만드는과정을 숨길수있다.
    => clone 메소드 안에

  • 기존객체를 복제하는과정이 새 인스턴스를 만드는것보다 비용면에서 효율적일수있다.

  • 추상적인 타입을 리턴할수있다.

단점

  • 복잡한 객체를 만드는 과정 자체가 복잡할수있다. (순환참조가 있는경우 특히 그렇다.)

프로토패턴 사용예제

자바 컬렉션

  public static void main(String[] args) {
        Student keesun = new Student("keesun");
        Student whiteship = new Student("whiteship");
        List<Student> students = new ArrayList<>();
        students.add(keesun);
        students.add(whiteship);

        List<Student> clone = new ArrayList<>(students);
        System.out.println(clone);
    }

List는 클론을 지원하지않고, ArrayList는 클론을 지원하기때문에 쓰는방법 (엄밀히 따지면 프로토패턴을 사용하는것은 아니지만)

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

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

        ModelMapper modelMapper = new ModelMapper();
        GithubIssueData githubIssueData = modelMapper.map(githubIssue, GithubIssueData.class);
        System.out.println(githubIssueData);
    }

api를만들거나 응답타입이나 요청으로 들어오는 본문을 파싱받을때 종종 생길때, 쓰이는 modelmapper
리플렉션으로 객체에 담겨져있는 녀석들을 데이터를 매핑해서 넣어주는것

참조: https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4/dashboard

좋은 웹페이지 즐겨찾기