팩토리 패턴 개선기

Kim Dong Kyun·2023년 10월 30일
1

넥스트 스텝의 "좌표계산기" 구현 미션에, 아래와 같은 과제가 있다.

public class FigureFactory {
    static Figure getInstance(List<Dot> dots) {
        if (dots.size() == Line.LINE_DOT_SIZE) {
            return new Line(dots);
        }

        if (dots.size() == Triangle.TRIANGLE_DOT_SIZE) {
            return new Triangle(dots);
        }

        if (dots.size() == Rectangle.RECTANGLE_DOT_SIZE) {
            return new Rectangle(dots);
        }

        throw new IllegalArgumentException("유효하지 않은 도형입니다.");
    }
}

위 클래스에서 if 를 제거하시오 (같은 동작을 해야함)

  • Figure 의 구현체 클래스는 Line, Triangle, Rectangle이 있다.

생각 1 - List형태의 생성자 주입, 스프링에서의 사용법처럼?

스프링에서 인터페이스의 하위 구상 클래스 들을 의존 주입 할 때, 다음과 같은 방법이 사용 가능하다. (Foo 는 인터페이스이며, 하위 구상 클래스들의 List를 주입이 가능)

1. Factory class

@Component
public class ProductProviderFactory {
    private final List<ProductService> products;

    private HashMap<ProductProvider, ProductService> map = new HashMap<>();

    public ProductProviderFactory(List<ProductService> products) {
        this.products = products;
        for (ProductService product : products) {
            this.map.put(product.getProvider(), product);
        }
    }

    public ProductService getService(ProductProvider provider){
        return this.map.get(provider);
    }
}
  • ProductService를 생성자 의존주입받는다. (List)
  • HashMap 에다가 식별자(provider)와 실제 빈을 넣는다.
  • getService는 getInstance()와 같은 역할. 서비스 빈을 리턴한다.

2. Client(팩토리를 사용하는 측)

@Component
public class Client {
    private final ProductProviderFactory productProviderFactory;

    public Client(ProductProviderFactory productProviderFactory) {
        this.productProviderFactory = productProviderFactory;
    }

    public void doService(ProductProvider provider){
        ProductService service = productProviderFactory.getService(provider);

        service.use();
    }
}
  • 컴포넌트인 Factory를 의존 주입받아서
  • Factory.getService() 로 사용한다.

그래서 어떻게한다고?

public class FigureFactory {
    private final List<Figure> figureList;
    private static final Map<Integer, Figure> figureMap = new HashMap<>();

    public FigureFactory(List<Figure> figureList) {
        this.figureList = figureList;
        for (Figure figure : figureList) {
            figureMap.put(figure.size(), figure);
        }
    }

    static Figure getInstance(List<Dot> points) {
        return figureMap.get(points.size());
    }
}
  • 위와 같은 식.

이 방식의 단점은?

  • 스프링의 경우 생성자 주입을 스프링 컴포넌트가 대신 해 준다.
  • 이 방법의 경우에는 팩토리 외부에서 의존성을 주입해줘야 한다 는 단점이 있다.(figureList)
  • 외부에서 해당 피규어리스트를(Line, Triangle, Square) 넣어준다는 것이 너무 이상했다.

생각 2. 스프링 의존주입은 리플렉션을 사용한다. 그렇다면?

나도 한 번 리플렉션을 사용해보는걸로 하자.

리플렉션을 사용하는 버전

public class FigureFactory {
    private FigureFactory() {
        throw new IllegalArgumentException("util class");
    }

    private static final Map<Integer, Class<? extends Figure>> figureMap = new HashMap<>();

    static {
        figureMap.put(FigureEnum.LINE.getSize(), Line.class);
        figureMap.put(FigureEnum.TRIANGLE.getSize(), Triangle.class);
        figureMap.put(FigureEnum.SQUARE.getSize(), Square.class);
    }

    public static Figure getInstance(List<Dot> points) {
        Class<? extends Figure> figureClass = figureMap.get(points.size());
        if (figureClass == null) {
            throw new IllegalArgumentException("잘못된 입력");
        }
        try {
            return figureClass.getConstructor(List.class).newInstance(points);
        } catch (Exception e) {
            throw new IllegalArgumentException("유효하지 않은 도형");
        }
    }
}
  • Class<? extends Foo> 와 같은 형태는 Foo의 하위 타입 "클래스"를 명시한다.
return figureClass.getConstructor(List.class).newInstance(points);
  • 해당 코드는
    1. Class에서 Constructer(생성자) 를 가져온다.

    2. 해당 생성자를 호출해서 .newInstance(Argument) 를 리턴한다. 위에서는 Figure의 하위 타입을 리턴 가능하다. (다형성)

이 방법의 단점은 무엇일까?

    1. 런타임에 클래스 정보를 불러오고, 생성자를 호출해야 한다
      : 비싸고, 불안정하다 (컴파일 타임에 에러가 안잡힌다)
    1. public 한 생성자가 꼭 필요하다.

위 사진은 Figure의 하위 구현체인 Line 클래스이다. 이 클래스의 생성자는 원래 protected 였다.

그러나, getConstructer() 매소드를 호출하기 위해 public으로 설정해야만 했다.
(getConstructer() 매서드의 설명을 적어본다.)

"The constructor to reflect is the public constructor of the class represented by this Class object whose formal parameter types match those specified by parameterTypes."


검색을 거친 후

함수형 인터페이스를 이용해서 해결 가능하다는 것을 알게 되었다. 어떤 방법인가 하면

Function 을 사용한 코드

public class FigureFactoryLambda {
    private FigureFactoryLambda() {
        throw new IllegalArgumentException("util class");
    }
    private static final Map<Integer, Function<List<Dot>, Figure>> figureMap = new HashMap<>();
    static {
        figureMap.put(2, Line::new);
        figureMap.put(3, Triangle::new);
        figureMap.put(4, Square::new);
    }
    public static Figure getInstance(List<Dot> dotList){
        Function<List<Dot>, Figure> figureFunction = figureMap.get(dotList.size());
        return figureFunction.apply(dotList);
    }
}
  • Fucntion 은 "함수" 라는 이름에 걸맞게, 어떤 인자를 넘겨주면 어떤 타입을 리턴해주는 역할을 수행한다.

  • 즉 Function<List, Figure> 는 List 이라는 인자를 넘겼을 때 Figure 를 리턴하는 함수이다.

  • 이 함수를 이용해서, Fucntion 인터페이스가 가지는 추상 매소드인 .apply 를 통해 리턴되는 Figure 객체를 얻을 수 있다.

위 방식이 깔끔해보이긴 한다. 그런데, 내가 이전에 작성한 코드가 생각났다.


이전에 작성한 자바 자동차 경주 게임의 람다

직접 선언한 함수형 인터페이스

@FunctionalInterface
public interface MovingStrategy {
    boolean move();
}
  • 이 함수형 인터페이스는 인자를 받지 않고, boolean 객체를 리턴하는 함수형 인터페이스이다.

  • 즉, 자바에서 제공하는 함수형 인터페이스 Supplier 와 같은 역할을 하는, 내가 명시적으로 이름을 지어줄 수 있는 인터페이스인 것이다.

  • 이 점을 참고해서 다시 한 번 팩토리 클래스를 손봐주었다.


최종 버전

@FunctionalInterface
public interface FigureCreator {
    Figure create(List<Dot> dotList);
}

...

public class FigureFactoryLambda {
    private FigureFactoryLambda() {
        throw new IllegalArgumentException("util class");
    }
    private static final Map<Integer, FigureCreator> figureMap2 = new HashMap<>();
    static {
        figureMap2.put(2, Line::new);
        figureMap2.put(3, Triangle::new);
        figureMap2.put(4, Square::new);
    }
    public static Figure getInstance(List<Dot> dotList){
        FigureCreator figureCreator = figureMap2.get(dotList.size());
        return figureCreator.create(dotList);
    }
}
  • 위아 같은 형태로 사용하게 되었다.

결론

  • 함수형 인터페이스, 람다, 익명 클래스에 대한 개념이 좀 더 쌓이게 된 경험이었다.

  • "익명 클래스, 람다" 는 추상을 재정의(Override) 해서 인스턴스처럼 사용할 수 있다는 사실을 좀 더 피부로 깨달았다.

  • 함께 스터디하는 분들을 위해 작성했다. 이상한 부분, 비약적인 부분, 모르는 부분은 언제든지 질문 환영!

2개의 댓글

comment-user-thumbnail
2023년 10월 31일

고민이 담긴 좋은 글 칭찬합니다~

1개의 답글