TIL 8 | 팩토리 메서드 패턴 알아보기

dereck·2024년 11월 28일

TIL

목록 보기
8/21

들어가기 전에

new 연산자를 사용하면 구상 클래스의 인스턴스가 만들어진다. 특정 구현을 바탕으로 코딩하면 나중에 코드를 수정해야 할 가능성이 커지고, 유연성이 떨어지게 된다.

Duck duck = new MallardDuck();

인터페이스 Duck을 써서 코드를 유연하게 만들려고 하지만 그럼에도 구상 클래스의 인스턴스를 만들어야 한다. 만약 일련의 구상 클래스가 있다면 어쩔 수 없이 다음과 같이 코드를 만들어야 한다.

Duck duck;
if (picnic) {
	duck = new MallardDuck();
} else if (hunting) {
	duck = new DecoyDuck();
} else if (inBathTub) {
	duck = new RubberDuck();
}

이 코드를 보면 구상 클래스의 인스턴스가 여러 개 있으며, 실행 시에 주어진 조건에 따라 인스턴스의 형식이 결정된다는 것을 알 수 있다. 이런 코드를 변경 또는 확장하려고 하면 새로운 코드를 추가하거나 기존 코드를 삭제해야 한다. 관리와 갱신이 어려워지는 것이다.

하지만 팩토리 메서드 패턴을 사용하면 확장에는 열려있지만 변경에는 닫혀있게 만들 수 있다. 쉽게 말하면 새로운 기능 추가에는 열려있고, 기능 추가로 인한 기존 코드의 변경에는 닫혀있다고 생각하면 된다.

팩토리 메서드 패턴

팩토리 메서드 패턴이란?

팩토리 메서드 패턴은 객체 생성을 공장(Factory) 클래스로 캡슐화 처리하여 대신 생성하게 하는 객체 생성 관련 디자인 패턴이다.

즉, new 연산자를 통해 특정 제품 객체를 생성하는 것이 아닌, 제품 객체들을 도맡아 생성하는 공장 클래스를 만들고, 이를 상속하는 서브 공장 클래스의 메서드에서 여러가지 제품 객체 생성을 각각 책임지는 것이다.

팩토리 메서드 패턴 구조

빨간 구역이 공장 객체이고, 노란 구역이 제품 객체이다. 공장 객체와 제품 객체가 느슨한 결합 구조로 되어있다.

  • Creator: 최상위 공장 클래스로서, 팩토리 메서드를 추상화하여 서브 클래스로 하여금 구현하도록 한다.
    • someOperation(): 객체 생성에 관한 전/후처리를 템플릿화한 메서드
    • createProduct(): 서브 공장 클래스에서 재정의할 객체 생성 추상 메서드
  • ConcreteCreator: 각 서브 공장 클래스들은 이에 맞는 제품 객체를 반환하도록 생성 추상 메서드를 재정의한다. 즉, 제품 객체 하나마다 그에 맞는 생산 공장 객체가 위치된다.
  • Product: 제품 구현체를 추상화
  • ConcreteProduct: 제품 구현체

팩토리 메서드 패턴 흐름

제품 코드

public interface Product {
    void doSomething();
}

public class ConcreteProductA implements Product {
    @Override
    public void doSomething() {
        System.out.println("ConcreteProductA.doSomething()");
    }
}

public class ConcreteProductB implements Product {
    @Override
    public void doSomething() {
        System.out.println("ConcreteProductB.doSomething()");
    }
}

인터페이스인 Product의 구현체인 ConcreteProductAConcreteProductB 클래스이다.

공장 코드

public interface Creator {

    default Product someOperation() {
        Product product = createProduct();  // 서브 클래스에서 구체화한 팩토리 메서드 실행
        product.doSomething();
        return product;
    }

    Product createProduct();
}

// 공장 객체 A
public class ConcreteCreatorA implements Creator {
    @Override
    public Product createProduct() {
        return new ConcreteProductA();  // 제품 구현 객체 A를 생성한 뒤 반환
    }
}

// 공장 객체 B
public class ConcreteCreatorB implements Creator {
    @Override
    public Product createProduct() {
        return new ConcreteProductB();  // 제품 구현 객체 B를 생성한 뒤 반환
    }
}

Creator 인터페이스를 구현하고 있는 ConcreteCreatorAConcreteCreatorB이다. 여기서 Creator를 추상 클래스가 아닌 인터페이스로 만들었는데, 자바 8이상이라면 default 메서드를 사용해서 인터페이스 안에 공통적으로 돌아가는 메서드를 구현할 수 있기 때문이다. 자바 9이상부터는 private도 사용할 수 있다.

코드를 잠깐 살펴보면 CreatorsomeOperation()을 보면 createProduct()를 사용해서 Product 인터페이스 타입의 객체를 생성하고 있는 것을 볼 수 있다. 그리고 createProduct()는 추상 메서드이기 때문에 하위 객체에서 무조건 구현해야 한다.

즉, Creator를 구현하는 구현체에서 Product 타입의 구현 객체를 지정해서 생성한 뒤 반환할 수 있게 되는 것이다. 그리고 해당 객체에 맞는 추가적인 메서드(doSomething())가 실행되는 것이다.

실행 코드와 결과

public class Client {
    public static void main(String[] args) {
        Creator creatorA = new ConcreteCreatorA();
        Creator creatorB = new ConcreteCreatorB();

        Product productA = creatorA.createOperation();
        Product productB = creatorB.createOperation();
    }
}
ConcreteProductA.doSomething()
ConcreteProductB.doSomething()
  1. 서브 공장 클래스(ConcreteCreatorA, ConcreteCreatorB)로 Creator 구현
  2. 각 공장에 맞는 제품 객체를 생성하는 메서드인 createOperation() 호출
  3. Product를 구현하고 있는 제품을 생성하고, 추가적인 공정(doSomething())을 지나 새로운 객체를 반환

팩토리 메서드 패턴 장단점

장점

  • 확장에 열려있고, 변경에 닫혀있는 객체 지향 원칙을 적용해서 기존 코드를 건드리지 않고, 기능을 추가할 수 있다.
    • 이것이 가능해지게 된 이유는 ProductCreator간의 결합도를 느슨하게 가져갔기 때문이다.
    • 기존 코드를 건드리지 않기 때문에 코드가 간결해지고, 기존 코드가 복잡해지지 않는다.
  • 객체 생성 작업을 팩토리 클래스로 캡슐화 했기 때문에 구현을 변경할 때 팩토리 클래스 하나만 고치면 된다.

단점

  • 팩토리 패턴을 적용하게 되면 각자의 역할을 나누다보니 클래스의 개수가 늘 수 밖에 없다.

팩토리 메서드 패턴 예제

위의 내용을 이해하기 쉽게 예제로 다시 한 번 보자.

패턴 적용 이전 코드

Ship 클래스

public class Ship {

    private String name;
    private String email;
    private String logo;
    private String color;

	// getter & setter

    @Override
    public String toString() {
        return "Ship{" +
            "name='" + name + '\'' +
            ", logo='" + logo + '\'' +
            ", color='" + color + '\'' +
            '}';
    }
}

ShipFactory 클래스


public class ShipFactory {

    public static Ship orderShip(String name, String email) {
        // validate
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("배 이름을 지어주세요");
        }
        if (email == null || email.isBlank()) {
            throw new IllegalArgumentException("연락처를 남겨주세요");
        }

        prepareFor(name);

        Ship ship = new Ship();
        ship.setName(name);

        // Customizing for specific name
        if (name.equalsIgnoreCase("whiteship")) {
            ship.setLogo("⛴");
        } else if (name.equalsIgnoreCase("blackship")) {
            ship.setLogo("⚓");
        }

        // coloring
        if (name.equalsIgnoreCase("whiteship")) {
            ship.setColor("white");
        } else if (name.equalsIgnoreCase("blackship")) {
            ship.setColor("black");
        }

        // notify
        sendEmailTo(email, ship);

        return ship;
    }

    private static void prepareFor(String name) {
        System.out.println(name + " 만들 준비 중...");
    }

    private static void sendEmailTo(String email, Ship ship) {
        System.out.println(ship.getName() + " 다 만들었습니다.");
    }

}

실행 클래스와 결과

public class Client {
    public static void main(String[] args) {
        Ship whiteShip = ShipFactory.orderShip("WhiteShip", "myemail123@email.com");
        System.out.println(whiteShip);

        Ship blackShip = ShipFactory.orderShip("BlackShip", "myemail123@email.com");
        System.out.println(blackShip);
    }
}
WhiteShip 만들 준비 중...
WhiteShip 다 만들었습니다.
Ship{name='WhiteShip', logo='⛴', color='white'}
BlackShip 만들 준비 중...
BlackShip 다 만들었습니다.
Ship{name='BlackShip', logo='⚓', color='black'}

패턴 적용 후 코드

Product 관련 클래스들

public class Ship {

    private String name;
    private String email;
    private String logo;
    private String color;

	// getter & setter

    @Override
    public String toString() {
        return "Ship{" +
            "name='" + name + '\'' +
            ", logo='" + logo + '\'' +
            ", color='" + color + '\'' +
            '}';
    }
}

public class WhiteShip extends Ship {
    public WhiteShip() {
        setName("WhiteShip");
        setColor("white");
        setLogo("⛴");
    }
}

public class BlackShip extends Ship {
    public BlackShip() {
        setName("BlackShip");
        setColor("black");
        setLogo("⚓");
    }
}

Factory 관련 클래스들

public interface ShipFactory {

    Ship createShip();

    default Ship orderShip(String name, String email) {
        validate(name, email);
        prepareFor(name);
        Ship ship = createShip();
        sendEmailTo(email, ship);
        return ship;
    }

    private void validate(String name, String email) {
        // validate
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("배 이름을 지어주세요");
        }
        if (email == null || email.isBlank()) {
            throw new IllegalArgumentException("연락처를 남겨주세요");
        }
    }

    private static void prepareFor(String name) {
        System.out.println(name + " 만들 준비 중...");
    }

    private static void sendEmailTo(String email, Ship ship) {
        System.out.println(ship.getName() + " 다 만들었습니다.");
    }
}

public class WhiteShipFactory implements ShipFactory {
    @Override
    public Ship createShip() {
        return new WhiteShip();
    }
}

public class BlackShipFactory implements ShipFactory {
    @Override
    public Ship createShip() {
        return new BlackShip();
    }
}

실행 클래스와 결과

public class Client {
    public static void main(String[] args) {
        Client client = new Client();
        client.print(new WhiteShipFactory(), "whiteship", "myemail@email.com");
        client.print(new BlackShipFactory(), "blackship", "myemail@email.com");
    }

    private void print(ShipFactory shipFactory, String name, String email) {
        System.out.println(shipFactory.orderShip(name, email));
    }
}
whiteship 만들 준비 중...
WhiteShip 다 만들었습니다.
Ship{name='WhiteShip', logo='⛴', color='white'}
blackship 만들 준비 중...
BlackShip 다 만들었습니다.
Ship{name='BlackShip', logo='⚓', color='black'}

References

0개의 댓글