[Design Pattern] 팩토리 패턴 총정리
Factory를 활용한 패턴은 객체생성의 역할을 별도의 클래스에 위임하는 것이 가장 큰 목표이다.
흔하게 실무에서 사용하는 패턴 중 팩토리를 사용하는 패턴은 팩토리 메서드 와 추상 팩토리 패턴이다.
우선 패턴을 제대로 알기전에 기본적으로 팩토리가 어떻게 사용되는지를 알아보자.
1. Simple Factory
팩토리의 기본적인 개념은 생성자 호출을 별도의 클래스인 팩토리에게 시킴으로써 호출하는 쪽이 객체의 생성자에 직접 의존하지 않도록 하는것이다. 만약 직접 의존하는 경우는 나중에 코드가 변경될 때 수정해야하는 코드의 범위가 늘어나게 된다.
간단한 예를 통해 구체적으로 알아보자. 예시는 결제시스템 개발을 위해 신용카드 정보를 생성하는 것으로 가정한다.
public interface CreditCard {
}
public class ShinhanCard implements CreditCard {
}
public class HyundaiCard implements CreditCard {
}
팩토리를 활용하지 않는다면 기본적으로 아래와 같이 객체를 생성하게 될 것이다.
CreditCard card1 = new ShinhanCard();
CreditCard card2 = new HyundaiCard();
이렇게 직접적으로 구현 클래스에 의존하고 있으면 해당클래스의 생성자나 전처리 부분이 변경되면 모든 클라이언트의 코드를 변경해줘야할 것이다. (큰 시스템에서 일일이 이런 구현부를 찾는 것도 쉽지가 않다...)
이를 팩토리를 이용하여 구현클래스에 직접 의존하지 않도록 해주자.
public class CreditCardFactory {
public CreditCard createCreditCard(CreditCard.CardType cardType) {
switch (cardType) {
case SHINHAN:
return new ShinhanCard();
case HYUNDAI:
return new HyundaiCard();
default:
throw new IllegalArgumentException("not exist card type.");
}
}
}
이렇게 구현 해주면 나중에 ShinhanCard 나 HyundaiCard 클래스의 코드가 변경되어도 우리는 팩토리 내부만 수정해주면 된다.
하지만 이러한 Simple Factory는 한계가 분명하다.
만약에 새로운 카드가 추가되었다고 가정해보자. 그러면 결국 팩토리 내부의 switch 문에 해당 클래스를 추가 해줘야한다. 즉 확장할 때 기존의 코드에 영향을 준다는 것이다. 이는 객체지향의 OCP 원칙을 위반한다.
이러한 한계는 팩토리 메서드 패턴이나 추상팩토리 패턴을 통해 해결이 가능하다.
2. 팩토리 메서드 패턴
우선 팩토리 메서드 패턴을 알아보자.
위의 다이어그램 처럼 팩토리 메서드 패턴은 객체의 생성을 서브클래스에게 위임한다. 이렇게 해두면 새로운 구현 클래스가 만약 추가 되어도 기존의 코드 수정 없이 새로운 팩토리를 만들어 내기만 하면된다.
예시를 통해 알아보자.
- Product
public interface CreditCard {
void createPayMethod();
}
public class ShinhanCard implements CreditCard{
@Override
public void createPayMethod() {
System.out.println("신한카드 결제 수단 생성");
}
}
public class HyundaiCard implements CreditCard{
@Override
public void createPayMethod() {
System.out.println("현대카드 결제 수단 생성");
}
}
Product 인터페이스를 구현한 클래스는 내부에서 오버라이드한 메서드를 각각의 특성에 맞게 구현한다.
-Creator
public abstract class CreditCardFactory {
public CreditCard newInstance() {
CreditCard card = createCard();
card.createPayMethod();
return card;
}
protected abstract CreditCard createCard();
}
추상 클래스로 팩토리 클래스를 정의한다. 외부에서 CreditCard 객체를 생성하려면 newInstance()를 호출해서 생성할수 있고, 실제 어떤 객체를 생성할지는 추상 메서드를 통해 하위클래스에서 정의된다.
public class ShinhanCardFactory extends CreditCardFactory{
@Override
protected CreditCard createCard() {
return new ShinhanCard();
}
}
public class HyundaiCardFactory extends CreditCardFactory{
@Override
protected CreditCard createCard() {
return new HyundaiCard();
}
}
실제 각 특성에 맞는 팩토리 (서브 클래스)에서 객체가 생성되는것을 볼 수 있다.
CreditCardFactory shinhanCardFactory = new ShinhanCardFactory();
CreditCard card1 = shinhanCardFactory.newInstance();
CreditCardFactory hyundaiCardFactory = new HyundaiCardFactory();
CreditCard card2 = hyundaiCardFactory.newInstance();
클라이언트 쪽에서는 위와 같이 사용해주면 된다.
그리고 실제로 새로운 스펙이 추가되더라도 기존의 코드 들 수정 없이 새로운 코드만 추가해서 사용해주면된다.
분명히 기존의 간단한 방식보다 코드량은 증가하나 OCP (수정에는 닫혀있고 확장에는 열려있음) 원칙을 지킬수가 있다.
3. 추상 팩토리 패턴
추상 팩터리 패턴은 개념적으로는 팩터리 메서드 패턴과 비슷하다.
그런데 여기서 팩터리 자체를 추상화 시켜 연관된 객체들을 모아둔다. 팩터리 메서드 패턴이 어떤 객체를 누가 생성할지에 초점이 맞춰져 있다면, 추상 팩토리 패턴은 어떤 객체가 연관이 있는지에 대해 초점이 맞춰져 있다.
아주 쉽게 설명을 하기 위해 아래와 같은 상황의 예시를 준비해봤다.
만약 여러분이 H 자동차 공장을 소프트웨어 시스템으로 구현한다고 가정해보자.
해당 공장에서는 아반테 공장과 소나타 공장이 있다.
아반테든 소나타든 자동차 이기 때문에 엔진, 연료탱크, 조향장치, 현수장치, 변속장치가 자동차의 핵심 구성요소이다.
- Product
public interface Engine {
}
public class AvanteEngine implements Engine {
}
public class SonataEngine implements Engine {
}
public interface FuelTank {
}
public class AvanteFuelTank implements FuelTank {
}
public class SonataFuelTank implements FuelTank {
}
public interface Steer {
}
public class AvanteSteer implements Steer{
}
public class SonataSteer implements Steer {
}
public interface Suspension {
}
public class AvanteSuspension implements Suspension{
}
public class SonataSuspension implements Suspension{
}
public interface Transmission {
}
public class AvanteTransmission implements Transmission{
}
public class SonataTransmission implements Transmission{
}
아반테 , 소나타 두 대의 자동차가 존재하기 때문에 각각의 구현 클래스가 인터페이스마다 각각 구현되었다.
-Factory
public interface CarFactory {
Engine createEngine();
FuelTank createFuelTank();
Steer createSteer();
Suspension createSuspension();
Transmission createTransmission();
}
public class AvanteFactory implements CarFactory{
@Override
public Engine createEngine() {
return new AvanteEngine();
}
@Override
public FuelTank createFuelTank() {
return new AvanteFuelTank();
}
@Override
public Steer createSteer() {
return new AvanteSteer();
}
@Override
public Suspension createSuspension() {
return new AvanteSuspension();
}
@Override
public Transmission createTransmission() {
return new AvanteTransmission();
}
}
public class SonataFactory implements CarFactory{
@Override
public Engine createEngine() {
return new SonataEngine();
}
@Override
public FuelTank createFuelTank() {
return new SonataFuelTank();
}
@Override
public Steer createSteer() {
return new SonataSteer();
}
@Override
public Suspension createSuspension() {
return new SonataSuspension();
}
@Override
public Transmission createTransmission() {
return new SonataTransmission();
}
}
각각의 부품은 자동차의 부품이라는 공통점으로 묶어 CarFactory라는 팩토리 클래스를 정의하였다.
아반테의 엔진, 연료탱크, 조향장치, 현수장치, 변속장치 를 제작하는 AvanteFactory 와
소나타의 엔진, 연료탱크, 조향장치, 현수장치, 변속장치 를 제작하는 SonataFactory 를 CarFactory를 상속받아 사용한다.
실제 사용은 아래와 같이 할수 있다.
-Client
public class Client {
public static void main(String args[]) {
Car avante = createCar(new AvanteFactory());
Car sonata = createCar(new SonataFactory());
}
private static Car createCar(CarFactory factory) {
Engine engine = factory.createEngine();
FuelTank fuelTank = factory.createFuelTank();
Steer steer = factory.createSteer();
Suspension suspension = factory.createSuspension();
Transmission transmission = factory.createTransmission();
return new Car(engine, fuelTank, steer, suspension, transmission);
}
}
만약에 여기서 그랜저를 생산하는 팩토리를 추가한다면 어떻게 하면될까?
단순하게 그랜저용 부품을 구현하고 그랜저용 팩토리를 CarFactory를 상속받아 만들어 주기만 하면된다.
팩토리 메서드 패턴과 마찬가지로 추상 팩토리 패턴을 구현하면 OCP 원칙을 철저히 지킬 수 있다. 또한 위의 예시처럼 비슷한 객체의 집합생성을 해야하는 경우라면 추상 팩토리 패턴을 통해 굉장히 확장성 있는 설계를 진행할 수 있다.
오늘 이 자료를 본 것에만 그치지말고 나름대로 한가지 주제를 잡고 예제 코드를 꼭 짜보시면서 꼭 이해하셨으면 한다.
끝.