추상 팩토리 패턴(Abstract Factory Pattern)
예제에 대한 전체 코드는 https://github.com/minseokLim/practice/tree/main/design-pattern에서 확인할 수 있다.
추상 팩토리 패턴(Abstract Factory Pattern)은 객체 생성과 관련된 디자인 패턴 중 하나로서, 구체적인 클래스에 의존하지 않고, 인터페이스를 통해 연관된 객체들을 생성할 수 있게 해준다. 이렇게 하면 코드의 유연성과 확장성을 높일 수 있다.
추상 팩토리 패턴을 구성하는 요소에는 다음과 같은 것들이 있다.
- 추상 팩토리(Abstract Factory) : 객체 생성을 담당하는 인터페이스를 선언한다. 구체적인 팩토리 클래스들이 이 인터페이스를 구현한다.
- 구체적인 팩토리(Concrete Factory) : 추상 팩토리 인터페이스를 구현하여 실제 객체를 생성한다. 각 구체적인 팩토리는 특정한 제품 객체들을 생성하도록 메서드를 재정의한다.
- 추상 제품(Abstract Product) : 생성될 객체들의 타입을 정의하는 인터페이스나 추상 클래스이다.
- 구체적인 제품(Concrete Product) : 추상 제품 인터페이스를 구현한 클래스들로, 실제로 생성되는 객체들이다.
예제를 통해 좀 더 자세히 이 패턴에 대해 알아보자.
아래는 예제 소스 코드의 클래스 다이어그램이다.
비행기로 적들을 격추하는 게임을 생각해보자. 작은 비행기들이 초반에 나오다가 스테이지 마지막에 보스가 나오는 식으로 진행이 될 것이다. 미사일만 쏘는 작은 비행기들도 있을 거고, 무작정 돌진하는 비행기가 있을 수도 있다. 보스 중에는 강한 공격을 하는 보스가 있을 수도 있고, 자가 복제를 하는 보스가 있을 수도 있다. 쉬운 스테이지에는 StrongAttackBoss, DashSmallFlight 가 나오고 어려운 스테이지에는 CloningBoss, MissileSmallFlight 가 나와야 한다고 생각해보자.
단순하게 생각하면 이 코드를 작성할 때 if-else 문을 통해 스테이지별로 다른 보스와 비행기들을 만들 수 있다고 생각할 수 있다. 하지만 스테이지가 추가되거나 생성 규칙이 달라지는 등의 경우가 생길 경우, 코드 수정이 어렵고 복잡해진다. 이를 해결할 수 있는 패턴이 바로 추상 팩토리 패턴이다.
아래와 같이 추상 팩토리 클래스를 정의하고, 각각의 스테이지에 맞는 구체 클래스들을 구현해보자. 예제는 코틀린으로 작성하였다.
interface EnemyFactory {
fun createBoss(): Boss
fun createSmallFlight(): EnemyFlight
}
class EasyEnemyFactory : EnemyFactory {
override fun createBoss(): Boss {
return StrongAttackBoss(20, 10)
}
override fun createSmallFlight(): EnemyFlight {
return DashSmallFlight(10, 5)
}
}
class HardEnemyFactory : EnemyFactory {
override fun createBoss(): Boss {
return CloningBoss(40, 20)
}
override fun createSmallFlight(): EnemyFlight {
return MissileSmallFlight(20, 10)
}
}
이제 코드를 사용하는 클라이언트 쪽에서는, 복잡한 조건문 대신 알맞은 팩토리 클래스를 주입하면 된다. 생성 규칙이 변경되거나 추가되는 경우에는 팩토리 클래스만 수정해주면 된다.
class Stage(
enemyFactory: EnemyFactory
) {
val boss = enemyFactory.createBoss()
val smallFlights = mutableListOf<EnemyFlight>().apply {
repeat(10) {
add(enemyFactory.createSmallFlight())
}
}
}
class StageTest {
@Test
fun create() {
// when
val easyStage = Stage(EasyEnemyFactory())
// then
assertTrue(easyStage.boss is StrongAttackBoss)
assertTrue(easyStage.smallFlights.all { it is DashSmallFlight })
// when
val hardStage = Stage(HardEnemyFactory())
// then
assertTrue(hardStage.boss is CloningBoss)
assertTrue(hardStage.smallFlights.all { it is MissileSmallFlight })
}
}
우리가 흔히 볼 수 있는 코드 중에 추상 팩토리 패턴이 적용된 대표적인 예가 자바의 JDBC API이다. Connection 인터페이스는 Abstract Factory에 해당하고 Statement와 PreparedStatement는 Abstract Product에 해당한다. Concrete Factory와 Concrete Product는 각각의 DBMS Driver에서 구현한다. 따라서 클라이언트가 사용할 DBMS를 변경해야 할 경우, 클라이언트 코드의 수정 없이 Driver만 교체해주면 가능하게 된다.
이 글은 "객체 지향과 디자인 패턴 - 최범균" 책을 기반으로 작성되었다.