[스터디 1주차] Gof - 생성패턴

600g (Kim Dong Geun)·2021년 5월 30일
0

생성패턴

  • 생성패턴은 인스턴스를 만드는 절차를 추상화하는 패턴.

  • 객체의 표현방법과 시스템을 분리해주는 것

클래스 생성패턴이 인스턴스로 만들 클래스를 다양하게 만들기 위한 용도로 상속을 하는반면, 객채생성패턴 은 인스턴스화 작업을 다른 객체에게 떠넘길 수 있다.

  • 생성 패턴이 중요해지는 이유는 시스템이 상속보다는 복합방법(인터페이스같은..?)을 사용하는 쪽으로 진화되어 가면서, 클래스를 하나만드는 것은 하나의 클래스를 인스턴스화 하는 일 이상의 품이 들어간다.
  • 생성패턴을 사용하면 다음과 같은 장점을 가질수 있습니다.

    • 시스템이 어떤 클래스를 사용하는지에 대한 정보를 캡슐화합니다.
    • 이들 클래스의 인스턴스들이 어떻게 만들고 어떻게 서로 맞붙는지에 대한 부분을 가려준다

    즉, 무엇이 생성되고 누가 만들며, 어떻게 생성되고 언제 생성되어지는지를 결정하는데 유연성을 제공한다.

  • 생성패턴으로 분류되는 패턴은 여러개인데 이런 생성패턴들은 보완적 역할, 경쟁적 역할 을 서로 할 수 있으므로, 잘 알고 사용해야 한다.
  • 이 장에서 다룰 패턴은 다음과 같습니다.
    • 추상 팩토리 패턴 (Abstract Factory Pattern)
    • 빌더 패턴(Builder Pattern)
    • 팩토리 메소드 패턴(Factory Method Pattern)
    • 프로토 타입 패턴(ProtoType Pattern)
    • 싱글톤 타입 패턴(SingletonType Pattern)
  • 예시로 미로에 대한 클래스 다이어그램을 간단히 제시하고, 생성패턴으로 보완한다라고 가정하고 있는데 해당 부분을 Java로 바꾸면 다음과 같습니다.
enum Direction {North, South, East, West};

public abstract class MapSite{
  public abstract void Enter();
}

public class Room extends MapSite{
  private final int _roomNumber;
  private MapSite[] _sides;
  
  public Room(int roomNumber){ this._roomNumber = roomNumber; }
  
  public abstract void Enter();
  public abstract void setSide();
  public abstract void getSide();
}

public class Wall extends MapSite{
  public Wall(){ };
  
  public abstract void Enter();
}

public class Door extends MapSite {
  private final Room _room1,_room2;
  private boolean _isOpen;
  
  public Door(Room room1, Room room2){
    this._room1 = room1;
    this._room2 = room2;
  }
  
 	public abstract void Enter();
  public abstract otherSideFrom(Room room);
  
}

public class Maze {
  public Maze(){};
  
  public abstract void addRoom(Room room);
  public abstract Room roomNo(int roomNumber);
}

라고 가정했을 경우 다음과 같은 코드를 제시할 수 있습니다.

class MazeGame{
 	public Maze createMaze() {
 		Maze maze = new Maze();
    Room r1 = new Room(1);
    Room r2 = new Room(2);
    Door theDoor = new Room(r1,r2);
    
    maze.addRoom(r1);
    maze.addRoom(r2);
    
    r1.setSide(North, new Wall());
    r1.setSide(East,theDoor);
    r1.setSide(South, new Wall());
    r1.setside(West, new Wall());
    
    r2.setSide(North, new Wall());
    r2.setSide(East, new Wall());
    r2.setSide(South, new Wall());
    r2.setSide(West, theDoor);
    
    return maze;
 }
  
}

여기서 미로하나를 생성하는데 있어서

    r1.setSide(North, new Wall());
    r1.setSide(East,theDoor);
    r1.setSide(South, new Wall());
    r1.setside(West, new Wall());
    
    r2.setSide(North, new Wall());
    r2.setSide(East, new Wall());
    r2.setSide(South, new Wall());
    r2.setSide(West, theDoor);

총 8줄의 코드를 일일이 생성해줘야 된다는 단점이 있습니다. 이러한 코드를 일일이 각 벽마다 설정해줄 필요 없고, 단지 연결되는 부분

r1.setSide(East, theDoor);
r2.setSide(West, theDoor);

정도만 있으면 되는 것이죠.

생성패턴은 이런상황에서 유연한 설계 를 할 수 있도록 제공해줍니다.

추상팩토리

  • 의도 : 상세화된 서브클래스를 정의하지 않고도 서로 관련성이 있거나 독립적인 여러 객체의 군을 생성하기 위한 인터페이스를 제공.

  • 동기 : 여러 응용 프로그램을 보면 서로다른 표준 락앤필을 가지는 경우가 많다. 스크롤바, 윈도우 버튼 제각각 모양도 다르고, 동작방식도 다르게 되는 경우가 많다. 이런 응용 프로그램이 서로 다른 록앤필 표준에 상관없이 이식성을 가지려면 각 사용자 인터페이스 툴킷에서 제공하는 위젯을 직접 사용하지 못하도록 해야 합니다. (// 이부분 추후 보완할 것)

  1. 룩앤필(look and feel)이란?
    소프트웨어 디자인에서 룩앤필은 GUI(Graphical User Interface)측면과 디자인의 형태 구성에서 사용되는 용어로 색상, 모양, 레이아웃, 서체(look)와 같은 요소와 버튼, 박스, 메뉴(feel)와 같은 동적 요소의 행위를 포함한다.

  • 활용성
    • 객체가 생성되거나 구성, 표현되는 방식과 무관하게 시스템을 독립적으로 만들고자 할 때
    • 여러 제품군 중 하나를 선택해서 시스템을 설정해야 하고 한번 구성한 제품이 다른 것으로 대체할 수 있을때
    • 관련된 제품 객체들이 함께 사용되도록 설계되었고 이부분에 대한 제약이 외부에서도 지켜지도록 하고 싶을때
    • 제품에 대한 라이브러리를 제공하고, 그들의 구현이 아닌 인터페이스를 노출시키고 싶을때(인터페이스를 제약하겠다.)
  • 구성요소

    • AbstractFactory : 개념적 제품에 대한 객체를 생성하는 연산으로 인터페이스를 정의
    • ConcreteFactory : 구체적인 제품에 대한 객체를 생성하는 연산을 구현
    • AbstractProduct : 개념적 제품 객체에 대한 인터페이스를 정의
    • ConcreteProduct : 구체적으로 팩토리가 생성할 객체를 정의하고, AbstractProduct가 정의하는 인터페이스를 구현
  • 장점

    • 구체적인 클래스를 분리

      응용프로그램이 생성할 객체의 클래스를, 팩토리의 클래스로 한정지을 수 있습니다. 즉, 제품 객체를 생성하는 과정과 책임을 캡슐화 한 것이기 때문에, 구체적인 구현 클래스가 사용자에게서 분리 됩니다.

    • 높은 이식성

      팩토리 클래스는 응용 프로그램에서 한번만 나타나기 때문에 응용 프로그램이 사용할 팩토리를 변경하기 쉽습니다.

    • 일관성 유지

      팩토리 클래스 내에서 객체의 생성을 관리하고, 그 객체들은 추상 팩토리에서 제시한 인터페이스 내에서 구현하도록 설계 되어야 하기 때문에 프로그램이 일관성을 가질 수 있도록 합니다.

  • 단점

    • 낮은 확장성

      추상 팩토리가 제공하고 있는 인터페이스의 변경시, 추상팩토리가 제공하고 있는 모든 서브클래스에서 해당 코드가 구현되어져야함.

  • 예제코드

다음은 위 미로코드를 추상 팩토리 패턴으로 관리했을때의 예시입니다.

abstract class MazeFactory {
  public MazeFactory(){};
  
  public abstract Maze makeMaze();
  public abstract Wall makeWall();
  public abstract Room makeRoom();
  public abstract Door makeDoor(r1,r2);
  
}

라는 클래스가 존재한다면, 다음과 같이 구현할 수 있습니다.

class MazeGame{
  
  void Maze createMaze(MazeFactory factory){
    Maze maze = factory.makeMaze();
    Room r1 = factory.makeRoom(1);
    Room r2 = factory.makeRoom(2);
    Door door = factory.makeDoor(r1,r2);
    
    maze.addRoom(r1);
    maze.addRoom(r2);
    
    r1.setSide(North, factory.makeWall());
    r1.setSide(East,door);
    r1.setSide(South, factory.makeWall());
    r1.setside(West, factory.makeWall());
    
    r2.setSide(North, factory.makeWall());
    r2.setSide(East, factory.makeWall());
    r2.setSide(South, factory.makeWall());
    r2.setSide(West, door);
  }
}

라고 구현을 할 수 있으며, 각각의 기능에 맞는 MazeFactory만 구현함으로써 내부가 어떻게 구현되어있는지 상관없이 미로를 변경할수 있습니다.

빌더 패턴

  • 동기 : 복잡한 객체를 생성하는 방법표현하는 방법을 정의하는 클래스를 별도로 분리

    즉 생성하는 방법과 구현하는 부의 책임을 서로 분리해서 간결한 코드를 제작하겠다는 것.

  • 표현하는 곳에서는 해당 객체가 어떻게 생성됐는지에 대한 관심은 없고, 객체를 어떻게 이용하겠다만 서술

  • 반대로 생성하는 곳에서는 어떻게 이용하는지는 관심없고, 객체를 어떻게 생성하겠다만을 서술

위와같이 결합을 느슨하게 함으로써, 클래스 생성방법이 변경되더라도 이를 이용하는 나머지 클래스에는 변경없이 코드를 작성할 수 있다.

  • 활용성
    • 복합 객체의 생성 알고리즘이 이를 합성하는 요소 객체들이 무엇인지, 이들의 조립 방법에 독립적일때
    • 합성할 객체들의 표현이 서로 다르더라도 생성 절차에서 이를 지원해야 할 때
  • 구성요소
    • Builder : Product 객체의 일부 요소들을 생성하기 위한 추상 인터페이스 정의
    • ConcreteBuilder : Builder 클래스에 정의된 인터페이스를 구현하며, 제품의 부품들을 모아 빌더를 조합
    • Director : Builder 인터페이스를 사용하는 객체를 합성
    • Product : 생성할 객체를 표현, ConcreteBuilder는 제품의 내부 표현을 구축하고 복합 객체가 어떻게 구성되는지에 대한 절차를 정의
  • 장점

    • 낮은 결합성(Loose Coupling)

      클래스 내부를 알고있는 것은 Builder 객체 밖에 없습니다. 따라서 클래스 내부를 어떻게 표현하고 싶은지에 대해서 사용자들은 몰라도 됩니다. 즉 빌더가 어떤 제품을 만드는지 만 알면 됩니다.

    • 생성과 표현에 필요한 코드를 분리

      추후 클래스 내부의 변경이 필요하다면, 클래스 전체를 변경할 필요없이 생성부와 관련된 클래스만 변경하면 되기때문에, 변경에 대한 Side Effect를 최소화할 수 있습니다.

    • 생성하는 절차를 세밀하게 나눌 수 있음

  • 예제코드
abstract class MazeBuilder{
  public abstract void buildMaze();
  public abstract void buildRoom(int room);
  public abstract void buildDoor(int roomFrom, int roomTo);
  
  public abstract Maze getMaze();
}

//다음과 같이 이용할 수 있음
Maze createMaze(MazeBuilder builder){
  builder.buildMaze();
  builder.buildRoom(1);
  builder.buildRoom(2);
  builder.buildDoor(1,2);
  
  return builder.getMaze();
}
  • 사실 빌더 패턴을 사용하는 예로는, 객체 생성을 유연하게 하기위함이 가장 크다라고 본다.

  • 쉬운예제, (자바의 StringBuilder)

class Member(){
  private final String name;
 	private final int age;
  private final Gender gender;
  
  private Member(Builder builder){
    this.name = builder.name;
    this.age = builder.age;
    this.gender = builder.gender;
  }
  
  public static class Builder{
    private String name;
    private int age;
    private Gender gender;
    
    public Builder name(String name){
      this.name = name;
    }
    
    public Builder age(int age){
      this.age = age;
    }
    
    public Builder gender(Gender gender){
      this.gender = gender;
    }
    
    public Member build(){
      return new Member(this);
    }
  }
  
}

//이렇게 함으로써 객체 생성에 대한 변화에 유연하게 대처가능.
Member member = new Member.Builder()
  .name("김동근")
  .age(28)
  .gender(Gender.Man)
  .build();

팩토리 메소드

  • 의도 : 객체를 생성하기 위해 인터페이스를 정의하지만, 어떤 클래스의 인스턴스를 생성할지에 대한 결정은 서브 클래스가 한다.

  • 활용성

    • 어떤 클래스가 자신이 생성해야 하는 객체의 클래스를 예측할 수 없을 때
    • 생성할 객체를 기술하는 책임을 자신의 서브클래스가 지정했으면 할 때
    • 객체 생성의 책임을 몇 개의 보조 클래스 가운데 하나에게 위임하고, 어떤 서브클래스가 위임자인지에 대한 정보를 국소화 시키고 싶을때
  • 구성요소

    • Product : 팩토리 메소드가 생성하는 객체의 인터페이스를 정의
    • ConcreteProduct : Product 클래스에 정의된 인터페이스를 실제로 구현
    • Creator : Product 타입의 객체를 반환하는 팩토리 메소드를 선언, Crator 클래스는 팩토리 메소드를 기본적으로 구현하는데, 이 구현에서는 ConcreteProduct 객체를 반환
    • ConcreteCreator : 팩토리 메소드를 재정의하여 ConcreteProduct의 인스턴스를 반환
  • 장점

    • 서브클래스에 대한 훅 메소드를 제공(높은 이식성)

      클래스 내부에서 객체를 생성하는 것이 객체를 직접생성하는 것보다 훨씬 응용성이 높아짐

      간단한 예로, 로그인이라는 클래스가 있다면 이 클래스를 상속해서 네이버 로그인, 카카오 로그인으로 분리해서 생각할 수 있다는 것.

    • 병렬적인 클래스 계통을 연결하는 역할을 담당

      특정 클래스만이 팩토리 메소드를 호출하게 되어있습니다. 하지만 꼭 이럴때만 유용한 것이 아니고, 클래스가 자신의 책임을 분리되니 다른 클래스에 위임할때, 유연하게 대처할 수 있음

  • 단점

    • ConcreteProduct 객체 하나만 만들려 할 때에도 Creator 클래스를 서브클래싱해야 할지 모른다는점

      (즉, 단일 상속 개체에도 굳이 책임을 분리해야하나 말아야하나)

public abstract class MazeGame{
  abstract public Maze makeMaze();
  abstract public Room makeRoom(int n);
  abstract public Wall makeWall();
  abstract public Door makeDoor(Room r1, Room r2);
 
  public void Maze createMaze(MazeFactory factory){
    Maze maze = makeMaze();
    Room r1 = makeRoom(1);
    Room r2 = makeRoom(2);
    Door door = makeDoor(r1,r2);
    
    maze.addRoom(r1);
    maze.addRoom(r2);
    
    r1.setSide(North, makeWall());
    r1.setSide(East,door);
    r1.setSide(South, makeWall());
    r1.setside(West, makeWall());
    
    r2.setSide(North, makeWall());
    r2.setSide(East, makeWall());
    r2.setSide(South, makeWall());
    r2.setSide(West, door);
  }
}

여기서 다른 게임을 만든다면 다음과 같이 구현하면됨

public class FiredWallMazeGame(){
  @Override
  public Maze makeMaze(){
    return new FireWall();
  }
  
  @Override
  public Room makeRoom(int n){
    return new FireRoom(n);
  }
  ...
}

오직 미로가 어떻게 동작하는지에 대한 매커니즘은 그대로 두고, 필요한 부분의 메소드만 변경함으로써 원하는 동작을 구현할 수 있습니다.

다만 클래스를 상속해서 책임을 분리해야 한다는 과정때문에 번거로움이 많이 발생하고 자바는 interface를 이용한 Strategy 패턴을 주로 사용

원형 패턴(ProtoType Pattern)

  • 의도 : 원형이 되는 인스턴스를 사용하여 생성할 객체의 종률 명시하고, 이렇게 만든 견본을 복사해서 새로운 객체를 생성.

  • 활용성

    • 인스턴스화할 클래스를 런타임에 지정할때
    • 제품 클래스 계통과 병렬적으로 만드는 팩토리 클래스를 피하고 싶을때
    • 클래스의 인스턴스들이 서로 다른 상태 조합 중에 어느 하나일때
  • 구조

  • 구성요소
    • Prototype : 자신을 복제하는데 필요한 인터페이스를 정의
    • ConcretePrototype : 자신을 복제하는 연산을 구현
  • 결과
    • 추상팩토리빌더 와 비슷한 결과를 가짐.
    • 사용자 쪽에는 어떤 구체적인 제품이 있는지 알리지 않아도 되기 때문에 사용자 쪽에서 상대해야하는 클래스 수가 적음.
  • 장점

    • 런타임에 새로운 제품을 추가하고 삭제할 수 있음

      원형패턴을 이용하면 사용자에게 원형으로 생성되는 인스턴스를 등록하는 것만으로도 시스템에 새로운 클래스를 추가하는 것과 같은 효과를 가짐

    • 값들을 다양화함으로써 새로운 객체를 명세

      고도로 동적화된 시스템에서는 새로운 클래스를 생성할 필요없이 인스턴스의 조합만으로 새로운 객체를 정의

    • 구조를 다양화 함으로써 새로운 객체를 명세

    • 서브클래스의 수를 줄임

    • 클래스에 따라 응용프로그램을 설정할 수 있음.

class MazePrototypeFactory extends MazeFactory {
  private Maze prototypeMaze;
  private Room prototypeRoom;
  private Wall prototypeWall;
  private Door prototypeDoor;
  
  public MazePrototypeFactory(Maze m, Wall w, Room r, Door d){
    this.prototypeMaze = m;
    this.prototypeWall = w;
    this.prototypeRoom = r;
    this.prototypeDoor = d;
  }
  
  public Wall makeWall(){
    return prototypeWall.clone();
  }
  
  public Door(room r1, room r2){
    Door door = porototypeDoor.clone();
    door.initialize(r1,r2);
    return door;
  }  
}

//다음과 같이 사용가능합니다.
public class Main(){
  public static void main(String ...args){
    MazeGame game = new MazeGame();
    MazePrototypeFactory simpleMazeFactory =
      new MazePrototypeFactory(new Maze(), new Wall(), new room(), new Door());
    Maze maze = game.createMaze(simpleMazeFactory);
    
    //만약 게임을 변경하고 싶다.
    maze = game.createMaze(new Maze(), new FireWall(), new FireRoom(), newFireDoor());
  }
}

사용자는 Clone() 연산의 반환값을 자신이 원하는 타입으로 다운캐스트할 필요가 절대로 없어야 함.

abstract class Car implements Cloneable {

    public Frame frame;
    public Wheel wheel;

    public Car(Frame frame, Wheel wheel) {
        this.frame = frame;
        this.wheel = wheel;
    }

    @Override
    public String toString() {
        return "Car{" +
                "frame=" + frame +
                ", wheel=" + wheel +
                '}';
    }

    @Override
    protected Object clone() {
        Object obj = null;
        try {
            obj = super.clone();
        }catch (Exception e) {
            e.printStackTrace();
        }
        return obj;
    }
}

class Avante extends Car{

    public Avante(Frame frame, Wheel wheel) {
        super(frame, wheel);
    }

    public void changeFrame(Frame frame) {
        this.frame = frame;
    }

    @Override
    public String toString() {
        return "Avante{" +
                "frame=" + frame +
                ", wheel=" + wheel +
                '}';
    }
}


class Frame {

    private String name;
    private String color;

    public Frame(String name, String color) {
        this.name = name;
        this.color = color;
    }

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

class Wheel {

    private String name;
    private int size;

    public Wheel(String name, int size) {
        this.name = name;
        this.size = size;
    }

    @Override
    public String toString() {
        return "Wheel{" +
                "name='" + name + '\'' +
                ", size=" + size +
                '}';
    }
}



public class Main {

    public static void main(String[] args) {

        Frame frame = new Frame("avante V1", "red");
        Wheel wheel = new Wheel("avante V1", 18);

        Avante redAvante = new Avante(frame, wheel);

        Avante newAvante = (Avante) redAvante.clone(); // <- 객체 복사
        Frame newFrame = new Frame("avante V2", "white");
        newAvante.changeFrame(newFrame);

        System.out.println(redAvante);
        System.out.println(newAvante);

    }
}
//출처 : https://github.com/sup2is/study/tree/master/design-pattern/prototype-pattern

단일체(Singleton)

  • 의도 : 오직 하나의 클래스 인스턴스만을 갖도록 보장하고, 이에 대한 전역적인 접근을 제공

  • 활용성 :

    • 클래스의 인스턴스가 오직 하나여야 함을 보장하고, 잘 정의된 접근점으로 모든 사용자가 접근할 수 있도록 사용해야 될때
    • 유일한 인스턴스가 서브클래싱으로 확장되어야 하며, 사용자는 코드의 수정없이 서브클래스의 인스턴스를 사용할 수 있어야 될때
  • 장점

    • 유일하게 존재하는 인스턴스로 접근 통제

      Singleton 클래스 자체가 인스턴스를 캡슐화 하기 때문에, 이 클래스에서 사용자가 언제, 어떻게 인스턴스로 접근할 수 있을지 제어.

    • 네임스페이스 절약

      전역변수를 사용하기 때문에 namepsace 를 따로 망치는 일을 없애준다.

    • 나머지는 장점인지는 잘...

  • 구현

    인스턴스가 유일해야함을 보장

    이것을 보장할 수없기때문에 안티패턴으로 분류

  • 예제
class MazeFactory {
  private MazeFactory insatnace;
  private MazeFactory(){}
  
  public static MazeFactory getInstance(){
    if(instance == null)
      this.instance = new MazeFactory();
    return this.instance;
  }
}
  • 싱글톤 패턴은 일반적으로 안티패턴으로 분류

    그 이유는 싱글코어, 단일쓰레드로 동작하는 시대가 아니기때문에, 인스턴스가 유일함을 보장할 수 없음.

    • 또한 간단한 코드인 경우 다음과 같이 대응할 수 있는데, 이 또한 코드가 복잡해질 경우 Deadlock 을 발생시킬 우려가 있다.

      class MazeFactory {
        private MazeFactory insatnace;
        private MazeFactory(){}
        
        //동기화하여 유일 객체 생성 보장
        public static syncronized MazeFactory getInstance(){
          if(instance == null)
            this.instance = new MazeFactory();
          return this.instance;
        }
      }
    • 또한 private한 생성자를 가지고 있기 때문 상속을 사용할 수 없음.

    • 따라서 싱글톤 패턴은 그 값이 변하지 않는 읽기전용 의 경우에만 사용하는 것이 적절.

profile
수동적인 과신과 행운이 아닌, 능동적인 노력과 치열함

0개의 댓글