인스턴스를 만드는 절차를 추상화하는 패턴. (시스템으로부터 객체의 생성/합성 방법을 분리)
시스템이 어떤 구체 클래스를 사용하는지, 또한 인스턴스들이 어떻게 만들어지고 어떻게 합성되는지에 대한 정보를 완전히 가려준다.
객체를 생성하기 위해 인터페이스를 정의하지만 어떤 클래스의 인스턴스를 생성할지는 서브 클래스가 결정한다.
로직을 구현할 때 특정 부분에서 어떤 인터페이스(또는 추상 클래스)를 구현한 클래스의 인스턴스가 필요하다는 것은 정의되었으나, 구체적으로 어떤 클래스의 인스턴스가 쓰일지 예측이 불가능할 때가 있다.
우리는 놀이동산을 만들고 일정 시간이 지나면 놀이동산을 폐쇄하는 프로그램을 만들 것이다. 그런데 그 놀이동산은 비스킷으로 만들어질 수도, 젤리로 만들어질 수도 있다.
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
를 변경할 필요 없이 상속하여 구현해주면 되는 것이다.
이처럼 팩토리 메소드 패턴 은 구체 클래스들이 병렬구조를 이루어 그때그때 교체하여 사용하면 되기 때문에 프로그램에 유연성을 제공해준다. 소프트웨어가 우리들의 코드에 종속되지 않도록 해주는 것이다.
생성자에 매개변수가 많을 때 빌더 패턴을 사용하여 코드를 깨끗이 한다.
생성자에 매개변수가 많고 또 그 매개변수가 모두 필수 정보가 아닐 때, 빌더 패턴을 적용하여 코드의 가독성을 높여주고 객체 생성의 안전성을 높여준다.
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);
}
}
```
빌더 패턴 은 객체를 사용하는 클라이언트가 필요한 객체를 직접 만드는 것이 아니라 빌더에게 객체를 받게 된다. 클라이언트는 필수 매개변수만으로 빌더 객체를 생성하고, 빌더를 통해 다른 선택 필드들을 쌓아올리고 마지막으로 빌더 객체에게 최종 객체를 받게된다.빌더 패턴 을 통해 클라이언트는 코드를 작성하기 쉬워지며 개발자가 보기에도 가독성이 좋아진다. 특히 빌더 패턴은 계층적으로 설계되어 있는 클래스에 적절하게 쓰인다. 추상 클래스에는 추상 빌더를, 구체 클래스에게는 구체 빌더를 정의하여 계층별로 사용하는 것이다.바닥
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
를 사용하면 SteelFloor
, SteelWall
, SteelDoor
, SteelWindow
가, WoodenRoomFactory
를 사용하면 WoodenFloor
, WoodenWall
, WoodenDoor
, WoodenWindow
가 생성된다. 하나의 상품을 만들기 위해 모든 제품들(Floor, Wall, Door, Window)이 모두 일관성을 갖게 된 것이다.
이처럼, 추상 팩토리 패턴 은 제품 사이의 일관성을 증진시킨다. 하나의 군(또는 집합) 안에 속한 객체들이 서로 함께 동작하도록 되어 있을 때, 그리하여 시스템에서 하나의 군을 선택하도록 되어있을 때, 객체들의 일관성을 증진시키기 위하여 주로 추상 팩토리 패턴 을 적용한다. 이는 제품군을 쉽게 대체할 수 있다는 장점이 있다. 철제로 된 방에서 나무로 된 방으로 바꾸고 싶으면 내부의 여러 객체들을 일일히 수정할 것이 아니라 SteelRoomFactory
를 선택했던 것을 WoodenRoomFactory
로 바꾸어 주면 되는 것이다. 이처럼 ‘추상 팩토리’가 앞에서 필요했던 모든 것을 다 생성해주기 때문에 제품군이 한번에 변경될 수 있다.
물론 장점만 있는 것은 아니다. 추상 팩토리 패턴은 패턴 특성 상 서브클래싱을 해줄 수밖에 없기 때문에 새로운 제품이 추가되어 인터페이스에 메소드를 추가해줘야 하는 경우가 생기면 모든 서브클래스가 이를 반영해줘야 한다. 예를 들어, 방 구조에 ‘베란다’라는 개념이 추가되었다면, 인터페이스에 makeVeranda
라는 메소드가 추가되어야 하고 이를 구현/상속하고 있는 모든 서브클래스를 찾아 이를 반영 및 수정해주어야 한다. 하지만 추상 팩토리 패턴 은 관련된 객체들이 서로 함께 사용되게 되어있을 때, 그 객체들의 일관성을 쉽게 제공해주기 때문에 자주 사용되는 패턴이다.
위치 정보 클래스 : 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과 같이 복사를 수행할 메소드를 반드시 구현해줘야 한다는 단점도 존재하지만 매번 필요한 상태 조합을 수동적으로 초기화하지 않는다는 점에서 장점도 존재한다.
상속할 수 없다.
java에서는 생성자를 private으로 선언하면 상속을 할 수 없다. 이는 곧 객체지향 프로그램의 핵심인 상속과 다형성을 해치는 개념이다.
강제로 전역 상태
애초에 공유의 목적으로 생성된 클래스이기 때문에 객체를 요청하는 메소드를 public으로 강제할 수밖에 없다. 특정 메소드가 정보의 은닉 범위, 공개 수준 등등에 전혀 상관없이 public으로 선언할 것을 강제했기 때문에 객체지향 프로그램의 또 다른 핵심인 ‘정보 은닉‘을 해친다.
객체가 하나인 것을 보장할 수 없다.
사실 싱글턴 패턴 의 핵심은 싱글턴인 것을 보장할 수 있어야 한다는 것이다. 하지만 java의 고전적 싱글턴 패턴 은 객체가 하나인 것을 보장할 수 없다.멀티쓰레드에서 해당 인스턴스는 공유돼서 사용되기 때문에 여러 개의 쓰레드가 동시에 접근하여 메소드를 호출할 수 있다. 문제는 2개 이상의 쓰레드가 동시에 객체 생성을 하게 되면 2개 이상의 객체가 생성된다는 것이다.