우선 디자인 패턴과 프로그래밍 패러다임에 대해 정리해보자.
디자인 패턴
위키의 정의를 보면 "소프트웨어 디자인 패턴(software design pattern)은 소프트웨어 공학의 소프트에어 디자인에서 특정 문맥에서 공통적으로 발생하는 문제에 대해 재사용 가능한 해결책이다." 라고 정의되어 있다.
쉽게 말하자면 선배 개발자들이 개발하면서 어려웠던 부분(반복되는 코드, 확장했을 때의 어려움 등)을 해결하기 위한 노하우를 패턴화 시켰다고 이해하면 될 것 같다.
1. 싱글톤 패턴
싱글톤 패턴은 하나의 클래스에 오직 하나의 인스턴스만 가지는 패턴이다. DB 커넥션을 얻을 때 싱글톤 패턴을 주로 적용한다. 만약 싱글톤이 아니라면 DB를 사용할 때마다 커넥션을 생성하면서 비용이 발생하게 되는데 싱글톤 패턴은 이러한 문제점을 해결해주기 때문이다.
하지만 싱글톤 패턴에도 단점이 있다. 대표적인 단점이 TDD인데, TDD는 테스트마다 서로 독립적이고, 순서에 상관없이 테스트가 동작해야 하지만 싱글톤 패턴은 인스턴스가 1개이기 때문에 서로 독립적인 테스트를 작성하는게 어렵다. 그리고 인스턴스가 1개이기 때문에 사용하는 곳에서는 의존성이 강하게 연결될 수 밖에 없다. 이러한 의존성 문제는 DI(Dependency Injection)로 어느정도 해결이 가능하다.
아래의 코드는 Java의 샘플코드인데 싱글톤 인스턴스를 static으로 선언하면서 클래스 로드시점에 생성하게 했다. 만약 최적화를 위해 INSTANCE를 null로 초기화하고 필요한 시점에 싱글톤으로 생성하고 싶다면 synchronized 를 사용해야 하기 때문에 상황에 맞게 코드를 변경해야 한다.
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
// 생성자를 private로 직접 생성 및 상속을 못하도록 제한
private Singleton() {}
public static Singleton getInstance() {
return INSTANCE;
}
}
2. 팩토리 패턴
객체를 사용하는 코드에서 객체 생성 부분을 떼어내 추상화한 패턴으로 팩토리 메소드 패턴과 추상 팩토리 패턴이 있다.
2-1. 팩토리메소드 패턴
객체를 생성하기 위한 인터페이스를 정의하며 어떤 클래스의 인스턴스를 생성할지에 대한 결정은 서브 클래스에서 구현하도록 하는 패턴이다.
하지만 팩토리 메소드 패턴을 알아보면서 이해되지 않는 부분이 있었는데 분명 패턴의 정의는 "인스턴스 생성에 대한 결정은 '서브' 클래스에서 구현한다"로 되어있다. 하지만 실제 많은 포스팅에서 Factory 클래스에서 static 메소드를 정의한 뒤 Parameter로 처리하는 예제를 많이 보여주고 있었다.
<이해안되는 샘플 코드>
// 메인 클래스(클라이언트)
public class FactoryMain {
public static void main(String[] args) {
Robot powerRobot = RobotFactory.create("POWER");
System.out.println("powerRobot = " + powerRobot.getName());
Robot speedRobot = RobotFactory.create("SPEED");
System.out.println("speedRobot = " + speedRobot.getName());
Robot balanceRobot = RobotFactory.create("BALANCE");
System.out.println("balanceRobot = " + balanceRobot.getName());
}
}
→ 클라이언트에서 RobotFactory를 사용하여 객체 생성을 하고 있다.
// 팩토리 클래스
public class RobotFactory {
public static Robot create(String type) {
switch (type) {
case "POWER" :
return new PowerRobot();
case "SPEED":
return new SpeedRobot();
case "BALANCE":
return new BalanceRobot();
default:
throw new IllegalArgumentException("invalid type: "+type);
}
}
}
→ RobotFactory 내에서는 파라미터를 구분하여 알맞은 객체 생성을 담당하고 있다.
위 예제는 부모 ↔ 자식의 관계는 Robot 인터페이스와 구현체의 관계일 뿐이며 생성에 관한 관계는 확인되지 않는다. RobotFactory는 단순히 객체 생성의 역할을 담당하며 유지보수의 편리함을 위해 사용되는 것 까지는 이해할 수 있지만 "팩토리 메소드 패턴"의 정의와는 맞지 않다고 생각한다.
<내가 생각한 팩토리 메소드 패턴>
public interface RobotFactory {
Robot createRobot();
}
→ 팩토리를 생성하는 인터페이스
public class PowerRobotFactory implements RobotFactory {
@Override
public Robot createRobot() {
return new PowerRobot();
}
}
→ RobotFactory를 구현하며 파워로봇을 생성하는 팩토리
public class SpeedRobotFactory implements RobotFactory{
@Override
public Robot createRobot() {
return new SpeedRobot();
}
}
→ RobotFactory를 구현하며 스피드로봇을 생성하는 팩토리
public class BalanceRobotFactory implements RobotFactory{
@Override
public Robot createRobot() {
return new BalanceRobot();
}
}
→ RobotFactory를 구현하며 밸런스로봇을 생성하는 팩토리
public class FactoryMethodPatternTest {
@Test
@DisplayName("로봇 팩토리 별로 객체가 정상적으로 생성되었는지 확인")
public void robotFactory() {
RobotFactory powerRobotFactory = new PowerRobotFactory();
Robot powerRobot = powerRobotFactory.createRobot();
RobotFactory speedRobotFactory = new SpeedRobotFactory();
Robot speedRobot = speedRobotFactory.createRobot();
RobotFactory balanceRobotFactory = new BalanceRobotFactory();
Robot balanceRobot = balanceRobotFactory.createRobot();
assertEquals(powerRobot.getClass(), PowerRobot.class);
assertEquals(speedRobot.getClass(), SpeedRobot.class);
assertEquals(balanceRobot.getClass(), BalanceRobot.class);
}
}
→ RobotFactory 인터페이스를 구현한 각 Factory 클래스에서 다른 타입의 로봇을 생성하고 있다. 어떤 로봇을 생성하는지는 RobotFactory 인터페이스를 구현한 클래스에서 내부적으로 결정하고 있다. 패턴의 정의에 맞는 올바른 구조의 코드이다.
2-2. 추상 팩토리 패턴
다양한 구성 요소 별로 '객체의 집합'을 생성해야 할 때 유용하고 상황에 알맞은 객체 집합을 생성할 수 있다. 예를 들어 키보드/마우스 공장이 있다고 했을 때 서울 공장에서는 삼성의 마우스와 키보드를 생산하고, 미국 공장에서는 로지텍의 마우스와 키보드를 생산해야만 한다고 했을 때 '추상 팩토리 패턴'을 사용하면 도움이 될 수 있다.
(※ 자세한 샘플코드는 깃허브에서 확인)
public class AbstractFactoryPatternTest {
@Test
public void keyboardMouseFactoryTest() {
// 서울 공장
KeyboardMouseFactory seoulFactory = new SeoulKeyboardMouseFactory();
Keyboard keyboard1 = seoulFactory.createKeyboard();
Mouse mouse1 = seoulFactory.createMouse();
assertEquals(keyboard1.getClass(), SamsungKeyboard.class);
assertEquals(mouse1.getClass(), SamsungMouse.class);
// 미국 공장
KeyboardMouseFactory USAFactory = new USAKeyboardMouseFactory();
Keyboard keyboard2 = USAFactory.createKeyboard();
Mouse mouse2 = USAFactory.createMouse();
assertEquals(keyboard2.getClass(), DellKeyboard.class);
assertEquals(mouse2.getClass(), DellMouse.class);
}
}
아래는 추상 팩토리 패턴의 UML이다.(※ 출처 위키피디아)
