티스토리 뷰
‘개발자가 반드시 정복해야 할 객체 지향과 디자인 패턴' 책을 보고 정리한 글입니다.
비행기를 조정하고 미사일을 발사해서 적을 미사일로 잡는 슈팅 게임을 가정하자.
이런 게임은 흔히 다음과 같은 여러 종류의 적이 출현할 수 있다.
- 특별 공격으로 작은 분신을 만들어 내는 보스
- 강력한 미사일을 발사하는 보스
- 미사일을 발사하는 적기
- 자폭하는 적기
- 장애물
또한, 적마다 공격력과 방어력이 달라질 수 있다.
위 같은 적을 구현하기 위해 Boss, SmallFight, Obstacle 클래스 및 하위 클래스를 구성했다.
실제 게임 플레이를 진행하는 Stage 클래스는 몇 단계인지에 따라 서로 다른 적기, 장애물 또는 보스를 생성해야 한다.
이를 처리하기 위해 Stage 클래스의 코드는 다음과 같다.
public class Stage {
private static final int ENEMY_CONT = 10;
private int stageLevel = 1;
private final EnemyFlight[] enemies = new EnemyFlight[ENEMY_CONT];
private final Boss boss;
private void createEmemies() {
for (int i = 0; i <= ENEMY_CONT; i++) {
if (stageLevel == 1) {
enemies[i] = new DashSmallFilght(1, 1); // 공격력 수비력
} else if (stageLevel == 2) {
enemies[i] = new MissileSmallFlight(1, 1); // 공격력 수비력
}
}
if(stageLevel == 1) {
boss = new StrongAttackBoss(1, 10);
} else if (stageLevel == 2) {
boss = new CloningBoss(5, 20);
}
}
}
위 코드의 문제는 단계별로 적기, 보스, 장애물을 생성하는 규칙이 Stage 클래스에 포함되어 있다는 점이다.
새로운 적 클래스가 추가되거나 각 단계의 보스 종류가 바뀔 때 Stage 클래스를 함께 수정해 줘야 하고, 각 단계별로 적기 생성 규칙이 달라질 경우에도 Stage 클래스를 수정해 주어야 한다.
또한 중첩되거나 연속된 조건문으로 인해 코드가 복잡해지기 쉽고 코드 수정을 어렵게 만드는 원인이 된다.
추상 팩토리 패턴은 Stage 클래스로부터 객체 생성 책임을 분리함으로써 위 문제를 해소시켜준다.
추상 팩토리 패턴에서는 관련된 객체 군을 생성하는 책임을 갖는 타입을 별도로 분리한다.
EnemyFactory 클래스는 Boss, SmallFight, Obstacle 객체를 생성해주는 메서드를 정의하고 있다.
여기서 EnemyFactory 클래스는 객체 생성 메서드를 선언하는 추상 타입으로서 팩토리에 해당되며, 팩토리가 생성하는 대상인 Boss, SmallFight, Obstacle은 제품 타입이 된다.
EnemyFactory.getFactory() 메서드는 정적 메서드로서 파라미터로 전달받은 레벨에 따라 알맞은 EnemyFactory 객체를 리턴하도록 정의하였다.
public abstract class EnemyFactory {
public static EnemyFactory getFactory(int level) {
if (level == 1) {
return EasyStageEnemyFactory();
} else {
return HardEnemyFactory();
}
}
// 객체 생성을 위한 팩토리 메서드
public abstract Boss createBoss();
public abstract SmallFlight createSmallFlight();
public abstract Obstacle createObstacle();
}
팩토리인 EnemyFactory를 구현한 콘크리트 팩토리 클래스는 아래 코드와 같이 알맞은 객체를 생성한다.
public class EasyStageEnemyFactory extends EnemyFactory {
@Override
public Boss createBoss() {
return new StrongAttackBoss();
}
@Override
public SmallFlight createSmallFlight() {
return new DashSmallFlight();
}
@Override
public Obstacle createObstacle() {
return new RockObstacle();
}
}
public class HardEnemyFactory extends EnemyFactory {
@Override
public Boss createBoss() {
return new CloningAttackBoss();
}
@Override
public SmallFlight createSmallFlight() {
return new MissileSmallFlight();
}
@Override
public Obstacle createObstacle() {
return new BombObstacle();
}
}
Stage 클래스는 객체 생성이 필요한 경우 직접 생성하기보다는 추상 팩토리 타입인 EnemyFactory를 이용해서 객체를 생성한다.
변경된 Stage 클래스는 더 이상 StrongAttackBoss 클래스와 DashSmallFlight 클래스와 같은 콘크리트 제품 클래스를 사용하지 않는다.
단지, 추상 타입인 Boss, SmallFlight, Obstacle만 사용할 뿐이다.
각 콘크리트 제품 클래스를 사용해서 객체를 생성하는 코드는 EnemyFactory 팩토리의 하위 타입 클래스로 옮겨졌다.
Stage 클래스는 EnemyFactory 클래스의 정적 메서드인 getFactory() 메서드를 이용해서 사용할 EnemyFactory 클래스를 구하고 있다.
따라서 1 레벨에서 사용되는 적 객체를 완전히 다른 타입으로 변경하고 싶다면 Stage 클래스를 변경할 필요 없이, 새로운 EnemyFactory 구현 클래스를 만들고 EnemyFactory.getFactory() 메서드에서 이 클래스의 객체를 리턴하도록 수정해 주면 된다.
public abstract class EnemyFactory {
public static EnemyFactory getFactory(int level) {
if (level == 1) {
// 적 생성 규칙 변경 시, 새로운 팩토리 클래스를 만들면
return SomethingNewEnemyFactory();
} else {
return HardEnemyFactory();
}
}
// 객체 생성을 위한 팩토리 메서드
public abstract Boss createBoss();
public abstract SmallFlight createSmallFlight();
public abstract Obstacle createObstacle();
}
위 코드에서는 EnemyFactory 객체를 구하는 기능을 EnemyFactory 클래스에 정의했는데, DI를 사용해도 된다.
DI를 사용하면 아래 코드처럼 생성자나 설정 메서드를 통해서 EnemyFactory 객체를 전달받게 되므로, EnemyFactory 클래스에 getFactory() 메서드를 정의할 필요가 없어진다.
따라서 EnemyFactory 추상 클래스를 인터페이스로 전환할 수 있게 된다.
public class Stage {
private int level;
private EnemyFactory enemyFactory;
// DI를 적용하면 팩토리를 구하는 기능을 EnemyFactory에 구현할 필요가 없음
public Stage(int level, EnemyFactory enemyFactory) {
this.level = level;
this.enemyFactory = enemyFactory;
}
}
추상 팩토리 패턴의 사용할 때의 장점은 클라이언트에 영향을 주지 않으면서 사용할 제품(객체) 군을 교체할 수 있다.
만약 팩토리가 생성하는 객체가 늘 동일한 상태를 갖는다면, 프로토타입 방식으로 팩토리를 구현할 수 있다.
프로토타입 방식은 아래 코드처럼 생성할 객체의 원형 객체를 등록하고, 객체 생성 요청이 있으면 원형 객체를 복제해서 생성한다.
// 프로토타입 방식의 팩토리
public class Factory {
private ProductA productAProto;
private ProductB productBProto;
public Factory(ProductA productAProto, ProductB productBProto) {
this.productAProto = productAProto;
this.productBProto = productBProto;
}
public ProductA createA() {
return (ProductA) productAProto.clone();
}
public ProductB createB() {
return (ProductB) productBProto.clone();
}
}
프로토타입 방식의 팩토리를 사용하면, 객체 군마다 팩토리 클래스를 작성할 필요 없이 객체 군마다 팩토리 객체를 생성해주면 된다.
public class Main {
Factory famaily1Factory = new Factory(new HighProductA(), new HighProductB());
ProductA a = famaily1Factory.createA(); // HighProductA 객체 복제본 생성
Factory famaily2Factory = new Factory(new LowProductA(), new LowProductB());
ProductB b = famaily1Factory.createB(); // LowProductB 객체 복제본 생성
}
프로토타입 방식을 사용하면 추상 팩토리 타입과 콘크리트 팩토리 클래스를 따로 만들 필요가 없어 구현이 쉽지만, 반면에 제품 객체의 생성 규칙이 복잡할 경우 적용할 수 없는 한계가 있다.
우리가 흔히 볼 수 있는 코드 중에 추상 팩토리 패턴을 적용한 대표적인 예가 자바의 JDBC API이다.
클라이언트는 Connection을 이용해서 Statement 객체와 PreparedStatement 객체를 생성한다.
즉, Connection이 팩토리에 해당하고 Statement와 PreparedStatement가 제품에 해당한다.
DBMS 별로 알맞은 Statement 콘크리트 클래스와 PreparedStatement 콘크리트 클래스를 제공하며, 이들 콘크리트 클래스의 객체를 생성해 주는 Connection 구현 클래스를 제공한다.
따라서 클라이언트가 사용할 DBMS를 변경해야 할 경우, 클라이언트 수정 없이 팩토리에 해당하는 Connection 객체만 교체해 주면 된다.
'JAVA > 디자인 패턴' 카테고리의 다른 글
널(Null) 객체 패턴 (0) | 2022.03.07 |
---|---|
컴포지트(Composite) 패턴 (0) | 2022.03.06 |
파사드(Facade) 패턴 (0) | 2022.03.05 |
옵저버(Observer) 패턴 (0) | 2022.03.03 |
어댑터(Adapter) 패턴 (0) | 2022.03.01 |
- Total
- Today
- Yesterday
- Java
- Kotlin
- 코테
- 이펙티브 자바
- BOJ
- Effective Java
- C++
- 백준
- kotest
- 테라폼
- Spring Boot
- AWS
- 클린 코드
- kkoon9
- BAEKJOON
- 이팩티브 자바
- Olympiad
- Spring
- 알고리즘
- 디자인 패턴
- 디자인패턴
- node.js
- Algorithm
- 정규표현식
- MSA
- programmers
- JPA
- 클린 아키텍처
- 프로그래머스
- 객체지향
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | ||||||
2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |