디자인패턴-생성패턴

Hyeseong Kim·2022년 6월 23일
0

Design Pattern

목록 보기
1/1
post-thumbnail

생성패턴(추상 객체 인스턴스화)

인스턴스를 만드는 절차를 추상화하는 패턴. (시스템으로부터 객체의 생성/합성 방법을 분리)
시스템이 어떤 구체 클래스를 사용하는지, 또한 인스턴스들이 어떻게 만들어지고 어떻게 합성되는지에 대한 정보를 완전히 가려준다.

팩토리 메서드(Factory Method)

객체를 생성하기 위해 인터페이스를 정의하지만 어떤 클래스의 인스턴스를 생성할지는 서브 클래스가 결정한다.

로직을 구현할 때 특정 부분에서 어떤 인터페이스(또는 추상 클래스)를 구현한 클래스의 인스턴스가 필요하다는 것은 정의되었으나, 구체적으로 어떤 클래스의 인스턴스가 쓰일지 예측이 불가능할 때가 있다.

예제

우리는 놀이동산을 만들고 일정 시간이 지나면 놀이동산을 폐쇄하는 프로그램을 만들 것이다. 그런데 그 놀이동산은 비스킷으로 만들어질 수도, 젤리로 만들어질 수도 있다.

  • 놀이동산 클래스
    public class AmusementPark {
    	public void open() {
    		System.out.println(toString() + "이(가) 생겼습니다.");
    	}
    	public void close() {
    		System.out.println(toString() + "이(가) 폐쇄되었습니다.");
    	}
    }
    public class JellyAmusementPark extends AmusementPark {
    	@Override
    	public String toString() {
    		return "젤리로 된 놀이동산";
    	}
    }
    public class BiscuitAmusementPark extends AmusementPark {
    	@Override
    	public String toString() {
    		return "비스킷으로 된 놀이동산";
    	}
    }
  • 놀이동산 운영 클래스
    public class AmusementParkOperator {
    	// 놀이동산을 만들고 5초가 지나면 폐쇄한다.
    	public void operate() throws InterruptedException {
    		AmusementPark amusementPark = new JellyAmusementPark();
    		amusementPark.open();
      		Thread.sleep(5000);
    		amusementPark.close();
    	}
    }

우리는 지금 방금 젤리로 된 놀이동산을 운영시켰다. 이제 비스킷으로 된 놀이동산을 운영시키기 위해서 우리는 위의 new JellyAmusementPark() 부분을 new BiscuitAmusementPark()로 바꿔줘야 한다.

적용

우리는 이제 팩토리 메소드 패턴 을 적용하여 놀이동산을 생성하는 부분을 아예 별도의 메소드로 분리하고 난 후 상속을 통해 그때그때 서브클래스가 자신이 운영할 놀이동산의 종류를 결정하도록 바꿔줄 것이다.

public abstract class AmusementParkOperator {
	public void operate() throws InterruptedException {
		AmusementPark amusementPark = makeAmusementPark();
		amusementPark.open();
		Thread.sleep(5000);
		amusementPark.close();
	}
	public abstract AmusementPark makeAmusementPark();
}
public class BiscuitAmusementParkOperator extends AmusementParkOperator {
	@Override
	public AmusementPark makeAmusementPark() {
		return new BiscuitAmusementPark();
	}
}
public class JellyAmusementParkOperator extends AmusementParkOperator {
	@Override
	public AmusementPark makeAmusementPark() {
		return new JellyAmusementPark();
	}
}

이제 우리는 재료가 바뀔 때마다 AmusementParkOperator 코드를 변경해주지 않아도 된다. 실행부에서 선택하는 AmusementParkOperator 종류에 따라 코드의 변경 없이도 놀이동산의 재료를 바꿔줄 수 있게 되었다.
혹시라도 사탕으로 된 놀이동산이 필요하다 하더라도 AmusementParkOperator를 변경할 필요 없이 상속하여 구현해주면 되는 것이다.

이처럼 팩토리 메소드 패턴 은 구체 클래스들이 병렬구조를 이루어 그때그때 교체하여 사용하면 되기 때문에 프로그램에 유연성을 제공해준다. 소프트웨어가 우리들의 코드에 종속되지 않도록 해주는 것이다.

빌더(Builder)

생성자에 매개변수가 많을 때 빌더 패턴을 사용하여 코드를 깨끗이 한다.

생성자에 매개변수가 많고 또 그 매개변수가 모두 필수 정보가 아닐 때, 빌더 패턴을 적용하여 코드의 가독성을 높여주고 객체 생성의 안전성을 높여준다.

적용

  • 방 클래스
    public class Room {
    		private Floor floor;
    		private Map<Direction, Wall> walls;
    		private Map<Direction, Door> doors;
    		private Map<Direction, Window> windows;
    		// 빌더로 필드 세팅
    		public Room(RoomBuilder roomBuilder) {
    			this.floor = roomBuilder.getFloor();
    			this.walls = roomBuilder.getWalls();
    			this.doors = roomBuilder.getDoors();
    			this.windows = roomBuilder.getWindows();
    		}
           
    		// 출력을 위함
    		@Override
    		public String toString() {
    			StringBuffer buffer = new StringBuffer(floor.toString()).append("\n");
    			for (Direction direction : walls.keySet()) {
    				buffer.append(direction.getValue()).append("쪽의 ").append(walls.get(direction).toString()).append("\n");
    			}
    			for (Direction direction : doors.keySet()) {
    				buffer.append(direction.getValue()).append("쪽의 ").append(doors.get(direction).toString()).append("\n");
    			}
    			for (Direction direction : windows.keySet()) {
    				buffer.append(direction.getValue()).append("쪽의 ").append(windows.get(direction).toString()).append("\n");
    			}
    			return buffer.toString();
    		}
    }
  • 방 빌더 클래스
      ```java
      public class RoomBuilder {
          private Floor floor;
          private Map<Direction, Wall> walls = new HashMap<>();
          private Map<Direction, Door> doors = new HashMap<>();
          private Map<Direction, Window> windows = new HashMap<>();
      
          public RoomBuilder() {
              this.floor = new Floor();
          }
      
          public RoomBuilder buildWalls(Direction direction) {
              this.walls.put(direction, new Wall());
              return this;
          }
      
          public RoomBuilder buildDoors(Direction direction) {
              this.doors.put(direction, new Door());
              return this;
          }
      
          public RoomBuilder buildWindows(Direction direction) {
              this.windows.put(direction, new Window());
              return this;
          }
      
          public Floor getFloor() {
              return floor;
          }
      
          public Map<Direction, Wall> getWalls() {
              return walls;
          }
      
          public Map<Direction, Door> getDoors() {
              return doors;
          }
      
          public Map<Direction, Window> getWindows() {
              return windows;
          }
      
          public Room build() {
              return new Room(this);
          }
      }
      ```
      
    빌더 패턴 은 객체를 사용하는 클라이언트가 필요한 객체를 직접 만드는 것이 아니라 빌더에게 객체를 받게 된다. 클라이언트는 필수 매개변수만으로 빌더 객체를 생성하고, 빌더를 통해 다른 선택 필드들을 쌓아올리고 마지막으로 빌더 객체에게 최종 객체를 받게된다.빌더 패턴 을 통해 클라이언트는 코드를 작성하기 쉬워지며 개발자가 보기에도 가독성이 좋아진다. 특히 빌더 패턴은 계층적으로 설계되어 있는 클래스에 적절하게 쓰인다. 추상 클래스에는 추상 빌더를, 구체 클래스에게는 구체 빌더를 정의하여 계층별로 사용하는 것이다.
  • 추상 팩토리(Abstract Factory) 상세화된 서브클래스를 정의하지 않고도 서로 관련성이 있거나 독립적인 여러 객체의 군을 생성하기 위한 인터페이스를 제공한다. 객체가 생성/구성되거나 표현이 되는 방식에 전혀 상관없이 시스템을 독립적으로 만들고자 할 때 유용하게 사용한다. 특히 여러 개의 관련된 제품들이 군을 이루고, 여러 제품군 중에서 하나를 선택하여 사용할 때 더욱 유용하다. 또한, 이미 구성됐다 하더라도 일부 제품을 다른 것으로 대체하고자 할 때도 유연하게 대처할 수 있다.

    예제

    우리는 아까와 동일하게 바닥과 벽, 문, 창문으로 방을 만들어내는데, ‘~~로 만든 방’이라는 개념을 도입하여 제품을 라인화시킬 것이다. 방을 단순하게 만드는 것이 아니라 ‘나무로 만든 방’, ‘철제로 만든 방’ 등과 같이 방의 종류를 지어주는 것이다. 그렇다면 방의 구성 요소가 되는 바닥, 벽, 문, 창문 역시 모두 ‘나무로 만든 바닥’, ‘철제로 만든 창문’ 등등으로 바뀌어야 한다. 가장 먼저, 기존에 사용했던 바닥, 벽, 문, 창문 객체를 추상화 시켜본다.
    • 바닥

      public interface Floor { }
      public class SteelFloor implements Floor {
          @Override
          public String toString() { return "철제로 된 바닥"; }
      }
      public class WoodenFloor implements Floor {
          @Override
          public String toString() { return "나무로 된 바닥"; }
      }
    • public interface Wall { }
      public class SteelWall implements Wall {
          @Override
          public String toString() { return "철제로 된 벽"; }
      }
      public class WoodenWall implements Wall {
          @Override
          public String toString() { return "나무로 된 벽"; }
      }
    • public interface Door { }
      public class SteelDoor implements Door {
          @Override
          public String toString() { return "철제로 된 문"; }
      }
      public class WoodenDoor implements Door {
          @Override
          public String toString() { return "나무로 된 문"; }
      }
    • 창문

      ```java
      public interface Window { }
      ```
      
      ```java
      public class SteelWindow implements Window {
          @Override
          public String toString() { return "철제로 된 창문"; }
      }
      ```
      
      ```java
      public class WoodenWindow implements Window {
          @Override
          public String toString() { return "나무로 된 창문"; }
      }
      ```

      가장 먼저, ‘철제로 만든 방’부터 만들어보자.

      (방의 구조는 다음과 같다고 해보자. ‘사방에 벽이 있고, 남쪽에 문, 북쪽에 창문이 있다.’)

    • 방 생성 클래스

      ```java
      public class RoomCreator {
          public Room createRoom() {
              Floor floor = new SteelFloor();
      
              // 사방에 생성
              Map<Direction, Wall> walls = new HashMap<>();
              walls.put(Direction.EAST, new SteelWall());
              walls.put(Direction.WEST, new SteelWall());
              walls.put(Direction.NORTH, new SteelWall());
              walls.put(Direction.SOUTH, new SteelWall());
      
              // 남쪽에 문 생성
              Map<Direction, Door> doors = new HashMap<>();
              doors.put(Direction.NORTH, new SteelDoor());
      
              // 북졲에 창문 생성
              Map<Direction, Window> windows = new HashMap<>();
              windows.put(Direction.SOUTH, new SteelWindow());
      
              return new Room(floor, walls, doors, windows);
          }
      }
      ```

      위에서는 steel로 된 벽, 문, 창문을 만들어주었는데 나무로 바꾸고 싶다면 일일이 RoomCreator를 수정해주어야 한다. 이를 해결하기 위해 앞에서 배운 팩토리 메서드를 사용할 수 있다. 하지만 팩토리 메서드를 사용하더라도 벽, 문, 창문 각각이 일관성이 없기 때문에 추상 팩토리 패턴을 사용하여야 한다.

      추상 팩토리 패턴은 제품들의 객체를 생성하는 과정과 책임을 캡슐화하고 추상화시킨다. 객체를 생성하는 부분을 특정 클래스가 감사고 그 과정들을 추상화하여 인터페이스 형태로 제공한다.

      그리하여 실제 방을 생성하는 로직이 담겨있는 RoomCreator는 구체적인 클래스가 아니라 인터페이스를 통해서만 인스턴스를 조작하기 대문에 방의 종류에 대해서 자유로워진다.

    • 팩토리 클래스

      ```java
      public interface RoomFactory {
          Floor makeFloor();
          Door makeDoor();
          Wall makeWall();
          Window makeWindow();
      }
      ```
      
      ```java
      public class SteelRoomFactory implements RoomFactory {
          @Override
          public Floor makeFloor() {  return new SteelFloor();  }
          @Override
          public Door makeDoor() {  return new SteelDoor(); }
          @Override
          public Wall makeWall() {  return new SteelWall(); }
          @Override
          public Window makeWindow() {  return new SteelWindow(); }
      }
      ```
      
      ```java
      public class WoodenRoomFactory implements RoomFactory {
          @Override
          public Floor makeFloor() {  return new WoodenFloor();  }
          @Override
          public Door makeDoor() {  return new WoodenDoor(); }
          @Override
          public Wall makeWall() {  return new WoodenWall(); }
          @Override
          public Window makeWindow() {  return new WoodenWindow(); }
      }
      ```

      우리는 각각의 제품들을 한 팩토리에서 관리하도록 변경해주었다. 따라서 SteelRoomFactory를 사용하면 SteelFloorSteelWallSteelDoorSteelWindow가, WoodenRoomFactory를 사용하면 WoodenFloorWoodenWallWoodenDoorWoodenWindow가 생성된다. 하나의 상품을 만들기 위해 모든 제품들(Floor, Wall, Door, Window)이 모두 일관성을 갖게 된 것이다.

      이처럼, 추상 팩토리 패턴 은 제품 사이의 일관성을 증진시킨다. 하나의 군(또는 집합) 안에 속한 객체들이 서로 함께 동작하도록 되어 있을 때, 그리하여 시스템에서 하나의 군을 선택하도록 되어있을 때, 객체들의 일관성을 증진시키기 위하여 주로 추상 팩토리 패턴 을 적용한다. 이는 제품군을 쉽게 대체할 수 있다는 장점이 있다. 철제로 된 방에서 나무로 된 방으로 바꾸고 싶으면 내부의 여러 객체들을 일일히 수정할 것이 아니라 SteelRoomFactory를 선택했던 것을 WoodenRoomFactory로 바꾸어 주면 되는 것이다. 이처럼 ‘추상 팩토리’가 앞에서 필요했던 모든 것을 다 생성해주기 때문에 제품군이 한번에 변경될 수 있다.

      물론 장점만 있는 것은 아니다. 추상 팩토리 패턴은 패턴 특성 상 서브클래싱을 해줄 수밖에 없기 때문에 새로운 제품이 추가되어 인터페이스에 메소드를 추가해줘야 하는 경우가 생기면 모든 서브클래스가 이를 반영해줘야 한다. 예를 들어, 방 구조에 ‘베란다’라는 개념이 추가되었다면, 인터페이스에 makeVeranda라는 메소드가 추가되어야 하고 이를 구현/상속하고 있는 모든 서브클래스를 찾아 이를 반영 및 수정해주어야 한다. 하지만 추상 팩토리 패턴 은 관련된 객체들이 서로 함께 사용되게 되어있을 때, 그 객체들의 일관성을 쉽게 제공해주기 때문에 자주 사용되는 패턴이다.

  • 프로토타입(Prototype) 프로토타입이 될 인스턴스를 생성하여 앞으로 생성할 객체의 종류를 명시하고, 그 인스턴스로부터 새로운 인스턴스를 복제한다. 인스턴스들이 서로 다른 상태 값 또는 서로 다른 조합으로 지속적으로나 주기적으로 필요할 때, 나중의 인스턴스 생성을 위해 복제의 견본이 될 원형 인스턴스를 준비해둔다. 그 후 새로운 인스턴스의 생성 요청이 오거나 필요할 때마다 미리 만들어둔 견본을 복제하여 사용한다.

    예제

    게임을 예로 들어보자. 우리는 특정 위치에서 지속적으로 몬스터들을 출현시킬 것이다.
    이 몬스터들은 각자 정해진 체력이 있고, 이 체력이 모두 다하면 죽는다. 또한, 구역별로 초기 체력의 양이 다르다. 몬스터들의 유형별로 견본을 준비해놓고, 필요할 때마다 그 견본으로부터 복제해가며 사용하면 된다. java에서는 객체의 복제를 위해 Object 클래스에 이미 clone이라는 메소드가 존재한다.
    Cloneable 인터페이스를 상속받고 clone이라는 메소드를 오버라이딩하면 인스턴스의 복제가 가능하다.

    적용

    • 위치 정보 클래스 : Location

      public class Location implements Cloneable {
      
          private int x;
          private int y;
      
          public Location(int x, int y) {
              this.x = x;
              this.y = y;
          }
      
          // getters
          public int getX() { return x; }
          public int getY() { return y; }
      
          // 위치정보 복제
          @Override
          public Location clone() throws CloneNotSupportedException {
              return (Location) super.clone();
          }
      }
    • 몬스터 클래스 : Monster

      ```java
      public class Monster implements Cloneable {
      
          private Location location;
          private int health;
      
          public Monster(Location location, int health) {
              this.location = location;
              this.health = health;
          }
      
          // getters
          public Location getLocation() { return location; }
          public int getHealth() { return health; }
      
          // 몬스터 복제
          @Override
          public Monster clone() throws CloneNotSupportedException {
              Monster clonedMonster = (Monster) super.clone();
              clonedMonster.location = location.clone();
              return clonedMonster;
          }
      
      }
      ```

      clone과 같이 복사를 수행할 메소드를 반드시 구현해줘야 한다는 단점도 존재하지만 매번 필요한 상태 조합을 수동적으로 초기화하지 않는다는 점에서 장점도 존재한다.

  • 싱글턴(Singleton) 오직 하나의 인스턴스만을 갖도록 하며, 이에 대한 전역적인 접근을 허용한다. 특정 클래스의 인스턴스가 반드시 하나여야 하나 여러 곳에서 사용하는 경우에 사용하는 패턴. 또한, 생성된 인스턴스를 여러 곳에서 공유하여 사용해도 무리가 없다면 메모리 낭비를 방지하기 위해 싱글턴 패턴을 적용하기도 한다.

    문제점

    1. 상속할 수 없다.

      java에서는 생성자를 private으로 선언하면 상속을 할 수 없다. 이는 곧 객체지향 프로그램의 핵심인 상속과 다형성을 해치는 개념이다.

    2. 강제로 전역 상태

      애초에 공유의 목적으로 생성된 클래스이기 때문에 객체를 요청하는 메소드를 public으로 강제할 수밖에 없다. 특정 메소드가 정보의 은닉 범위, 공개 수준 등등에 전혀 상관없이 public으로 선언할 것을 강제했기 때문에 객체지향 프로그램의 또 다른 핵심인 ‘정보 은닉‘을 해친다.

    3. 객체가 하나인 것을 보장할 수 없다.

      사실 싱글턴 패턴 의 핵심은 싱글턴인 것을 보장할 수 있어야 한다는 것이다. 하지만 java의 고전적 싱글턴 패턴 은 객체가 하나인 것을 보장할 수 없다.멀티쓰레드에서 해당 인스턴스는 공유돼서 사용되기 때문에 여러 개의 쓰레드가 동시에 접근하여 메소드를 호출할 수 있다. 문제는 2개 이상의 쓰레드가 동시에 객체 생성을 하게 되면 2개 이상의 객체가 생성된다는 것이다.

0개의 댓글