[객체 생성 패턴] Chapter 2-1. Factory Method Pattern : 패턴 소개

 

✍️ 팩토리 메서드 패턴, 패턴 소개

팩토리 메서드 패턴이 해결하려는 문제는, 인스턴스를 생성하는 책임을 구체적인 클래스가 아닌 추상적인 인터페이스의 메서드로 감싸는 것이다.

 

스토리 텔링으로 이해해 보자.

여기 거북선"" 만드는 공장인 ShipFactory 클래스가 있다. 이 공장은 거북선만 만들 계획으로 세워진 공장으로 배를 제작하는 static orderShip 메서드는 오직 거북선을 만들기 위해 작성했던 메서드였다. 그런데 얼마 후 사업이 너무 잘 돼서 공장에서 토끼선도 만들 계획이 생겼다. 이제는 orderShip 메서드는 더 이상 거북선만이 아닌 토끼선도 만들 수 있어야 한다.

가장 쉽고 단순한 방법으로, orderShip 메서드 내에서 주문한 배의 이름이 토끼선이라면 거북선과 다른 토끼 로고를 달고, 색을 다르게 칠하는 등 과정은 비슷하지만 orderShip 메서드 내부에 분기문이 추가돼 코드가 점점 복잡해질 것이다. 그리고 이후에 사업이 너무너무 잘 돼서 잠수함까지 만들어야 한다면... 이 모든 과정을 하나의 공장(클래스)에 담아두기엔 로직이 복잡해진다. 

 

다시 이론으로 돌아와서, 팩토리 메서드 패턴은 이러한 문제를 추상화된 Factory로 문제를 해결한다. 

먼저 공장 역할을 할 인터페이스(혹은 추상 클래스)를 만든다. 인터페이스에 기본적인 구현이 포함되고 일부 변경돼야 할 부분들을 메서드 형태로 추상화시켜 하위 클래스에서 구현하도록 한다. 하위 클래스에선 구체적인 인스턴스(거북선, 토끼선)를 만들어 인터페이스에 제공한다. 단, 인터페이스는 하위 팩토리가 제공한 인스턴스의 구체적인 타입은 모르고 Product() 인터페이스로 담아둔다. 

 

백번의 말보다 한 번의 코드가 더 이해가 쉽다. 코드를 보며 문제점을 이해하도록 하자.

 

 팩토리 메서드 패턴, 적용 전 코드

ShipFactory 클래스는 인자로 받은 name을 기준으로 거북선인지 토끼선인지 분기해서 배를 만들고 있다. 사실 이 자체만으로도 배의 종류가 많아지면 분기문도 선형적으로 늘어난다는 문제점을 가지고 있으며 향후에 Ship 클래스 자체가 변한다면 문제는 더 복잡해진다.

만약 일반적인 배가 아닌 잠수함 혹은 항공모함을 만들어야 한다면 클래스의 속성이 변하거나 새롭게 잠수함 클래스를 만들어 orderShip 내부에서 인스턴스를 만들기 위한 분기문을 또다시 추가해야 한다.

 

Ship class

public class Ship {
    // 편의상 한정자를 public으로
    public String name;
    public String color;
    public String logo;

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

 

ShipFactory class

public class ShipFactory {
    public static Ship orderShip(String name, String email) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("배 이름을 지어주세요");
        }
        if (email == null || email.isBlank()) {
            throw new IllegalArgumentException("이메일을 남겨주세요");
        }

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

        if (name.equalsIgnoreCase("turtleship")) {
            ship.logo = "\uD83D\uDC22";
        } else if (name.equalsIgnoreCase("rabbitship")) {
            ship.logo = "\uD83D\uDC07";
        }

        if (name.equalsIgnoreCase("turtleship")) {
            ship.color = "green";
        } else if (name.equalsIgnoreCase("rabbitship")) {
            ship.color = "pink";
        }

        // 이메일로 노티
        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.name + " 다 만들었습니다.");
    }
}

 

 

Client class

public class Client {
    public static void main(String[] args) {
        Ship rabbitship = ShipFactory.orderShip("rabbitship", "kangworld@mail.com");
        System.out.println(rabbitship);

        Ship turtleship = ShipFactory.orderShip("turtleship", "kangworld@mail.com");
        System.out.println(turtleship);
    }
}

 

결과

rabbitship 다 만들었습니다.
Ship{name='rabbitship', color='pink', logo='🐇'}
turtleship 다 만들었습니다.
Ship{name='turtleship', color='green', logo='🐢'}

 

🐢 정리

이러한 구조는 프로그래밍의 개방-폐쇄 원칙을 만족하지 못한다. 이는 확장엔 열려있고 수정엔 닫혀있어야 한다는 원칙으로 쉽게 말하면 새로운 기능을 추가할 때 기존 코드들이 변경되지 않는 구조를 말한다.

 

정리하면 위 코드는 확장 시 분기문을 추가해야 하므로 수정에 열려있다는 단점이 있다. 새로운 종류의 배를 만든다거나 배를 만드는 공정을 변경할 수 있으면서도 기존에 작성했던 코드를 유지할 수 있는 새로운 구조로 변경해야 한다.