Factory Method Pattern

👨🏼‍💻 팩토리 메서드 패턴이란 객체 생성을 공장 클래스로 캡슐화 처리하여
대신 생성하게 하는 생성 디자인 패턴이다.

클라이언트에서 new 를 통해 제품 객체를 생성하는 것이 아닌, 제품 객체들을
도맡아 생성하는 공장 클래스를 만들고, 이를 상속하는 서브 공장 클래스의 메서드에서
여러 제품 객체 생성을 각각 책임지는 형태이다.

또한 객체 생성에 필요한 과정을 템플릿처럼 미리 구성해놓고, 객체 생성에 대한
전처리나 후처리를 통해 생성 과정을 다양하게 처리하여 객체를 유연하게 정할 수
있는 특징도 있다.

객체를 생성하기 위해 인터페이스를 정의하지만, 어떤 클래스의 인스턴스를 생성할지에 대한 결정은 하위클래스가 담당

팩토리 메서드 패턴 특징

패턴 사용 시기

  • 클래스 생성과 사용의 처리 로직을 분리하여 결합도를 낮추고자 할 때
  • 코드가 동작해야 하는 객체의 유형과 종속성을 캡슐화를 통해 정보 은닉 처리 할 경우
  • 라이브러리 혹은 프레임워크 사용자에게 구성 요소를 확장하는 방법을 제공하려는 경우
    • 기존 객체를 재구성하는 대신 기존 객체를 재사용하여 리소스를 절약하고자
      하는 경우
      • 상황에 따라 적절한 객체를 생성하는 코드는 자주 중복될 수 있다.
        그리고 객체 생성 방식의 변화는 해당되는 모든 코드 부분을 변경해야 하는
        문제가 발생한다.
      • 객체의 생성 코드를 별도의 클래스 / 메서드로 분리함으로써 객체 생성의
        변화에 대해 대비할 수 있다.
      • 특정 기능의 구현은 별개의 클래스로 제공되는 것이 바람직한 설계이다.

패턴 장점

  • 생성자와 구현 객체의 강한 결합을 피할 수 있다.
  • 팩토리 메서드를 통해 객체의 생성 후 공통으로 할 일을 수행하도록 지정할 수 있다.
  • 캡슐화, 추상화를 통해 생성되는 객체의 공통적인 타입을 감출 수 있다.
  • SRP 준수 : 객체 생성 코드를 한 곳으로 모아 코드 유지보수를 용이하게 할 수 있다.
  • OCP 준수 : 기존 코드를 수정하지 않고 새로운 유형의 제품 인스턴스를 도입할 수 있다.
  • 생성에 대한 인터페이스와 구현 부분을 나누었기 때문에 개별로 협업하여 개발 가능

패턴 단점

  • 각 제품 구현체마다 팩토리 객체들을 모두 구현해주어야 하기 때문에, 구현체 증가시 팩토리 클래스가 증가하고 서브 클래스 수가 많아진다.
  • 코드의 복잡성이 증가한다.

사과를 사용하는 클래스 만들기

식당에서는 사과를 디저트로 제공한다.

Restaurant

public class Restaurant {
	public Apple servingApple() {
		Apple apple = new Busa();
		apple.wash();
		apple.peel();
		apple.slice();
		return apple;
	}
}

Apple

public abstract class Apple {
    public abstract void wash();
    public abstract void peel();
    public abstract void slice();
}

Busa

public class Busa extends Apple {
    @Override
    public void wash() {
        System.out.println("Busa : wash");
    }

    @Override
    public void peel() {
        System.out.println("Busa : peel");
    }

    @Override
    public void slice() {
        System.out.println("Busa : slice");
    }
}

Hongok

public class Hongok extends Apple {
    @Override
    public void wash() {
        System.out.println("Hongok : wash");
    }

    @Override
    public void peel() {
        System.out.println("Hongok : peel");
    }

    @Override
    public void slice() {
        System.out.println("Hongok : slice");
    }
}

집에서는 사과를 아침으로 먹는다.

Home

public class Home {
    public Apple getAppleForBreakFast() {
        Apple apple = new Hongok();
        apple.wash();
        return apple;
    }
}

문제점

사과의 종류가 변경되거나 추가될 경우, RestaurantHome 의 코드가 변경되어야 한다.
사과의 인스턴스를 생성하는 모든 코드에서 아래와 같은 작업을 반복할 필요가 있다.

public class Restaurant {
    public Apple servingApple(String kind) {
        Apple apple = null;
        if (kind.equals("busa")) {
            apple = new Busa();
        } else if (kind.equals("hongok")) {
            apple = new Hongok();
        }

        apple.wash();
        apple.peel();
        apple.slice();
        return apple;
    }
}

이렇듯 변경이 자주 일어나는 부분은 분리하여 클래스로 캡슐화할 필요가 있다.

해결책 - 클래스로 캡슐화

AppleFactory

public class AppleFactory {
    public static Apple getApple(String kind) {
        Apple apple = null;
        if (kind.equals("busa")) {
            apple = new Busa();
        } else if (kind.equals("hongok")) {
            apple = new Hongok();
        } else if (kind.equals("hongro")) {
            apple = new Hongro();
        }
        return apple;
    }
}

Restaurant

public class Restaurant {
    public Apple servingApple(String kind) {
        Apple apple = AppleFactory.getApple(kind);
        apple.wash();
        apple.peel();
        apple.slice();
        return apple;
    }
}

위와 같이 팩토리 메서드 패턴을 활용하면 새로운 사과 종류가 추가되어도,
팩토리 클래스만 변경하면 되고 실제 사과를 사용하는 클라이언트는 영향을 받지 않는다.

개선 - Factory 클래스 추상화, 분기문을 구현체로

AppleFactory

public interface AppleFactory {
    Apple getApple();
}

BusaFactory

public class BusaFactory implements AppleFactory {
    @Override
    public Apple getApple() {
        return new Busa();
    }
}

HongokFactory

public class HongokFactory implements AppleFactory {
    @Override
    public Apple getApple() {
        return new Hongok();
    }
}

개선 - Thread-Safe한 싱글톤 Factory

팩토리 객체들은 여러 개가 존재할 필요가 없다. 따라서 Lazy-Holder 패턴과 같은
싱글톤 패턴을 이용하여 구현하는 것이 메모리를 최적화시킨다.

public class BusaFactory implements AppleFactory {
    private BusaFactory() {
    }

    public static class InstanceHolder {
        private static final BusaFactory INSTANCE = new BusaFactory();
    }

    public static BusaFactory getInstance() {
        return InstanceHolder.INSTANCE;
    }

    @Override
    public Apple getApple() {
        return new Busa();
    }
}

참고 - 최상위 팩토리 인터페이스로 구성

Java 8버전 이후 추가된 default method 와 Java 9이후에 추가된 private
메서드를 통해 인터페이스로 추상 클래스를 통해 구현되어야 하는 기본 연산들을
대체할 수 있다.

팩토리 메서드 패턴 사용 사례

NumberFormat의 getInstance()

국가 또는 화폐에 따라 다른 표현 방식을 커버하기 위해 팩토리 메서드 패턴으로
디자인되어 있다.

NumberFormat 을 구현하는 클래스로는 DecimalFormat , ExponentialFormat 등이 있다.

public static void main(String[] args) {
    // 팩토리 메서드로 구현체를 생성하여 반환
    NumberFormat currencyFormatter = NumberFormat.getCurrencyInstance();
    NumberFormat percentFormatter = NumberFormat.getPercentInstance();

    double x = 0.2;

    System.out.println(currencyFormatter.format(x)); // $0.20를 출력한다.
    System.out.println(percentFormatter.format(x)); // 20%를 출력한다.
}

Calendar의 getInstance()

getInstance() 를 호출할 때마다 새로운 Calendar 객체가 생성. 그레고리안, 줄리안 형식이 있는데, 이 두 가지 경우를 모두 커버하기 위해 팩토리 메서드 패턴으로
디자인 되었다.

참고

profile
도전을 성과로

0개의 댓글

Powered by GraphCDN, the GraphQL CDN