팩토리 메서드 패턴
특정 인스턴스를 생성하는 책임을 구체적인 클래스가 아닌, 추상적인 인터페이스의 메서드로 감싸는 것.
왜?
예를 들어 어떤 회사가 A라는 자동차를 만들어서 제품을 판매하는데, 장사가 잘 되서 B라는 자동차도 만들어서 판매해보려 한다.
그렇다면 이미 존재하는 A자동차를 만드는 클래스 안에, if-else문이 추가 되면서 새로운 기능과 디자인을 추가한 B자동차를 만드는 로직이 추가될 것이다. 이렇게 하나하나 추가하다 보면 코드가 지저분해질 것이다.
이러한 클래스는 concrete클래스라고 하는데, 이러한 concrete클래스에 모두 담아두지 말고, 추상화된 클래스에 담아둔다.
(추상 클래스에 담아둔다고는 했지만, 자바8부터는 인터페이스에 기본 메서드를 정의할 수 있기 때문에 추상화된 인터페이스 안에 기본 메서드를 넣을 것이다.)
즉 추상화된 클래스에 기본 메서드와 추상 메서드를 만들고, 이를 각 클래스가 구현하게 하는 것이다.
코드로 살펴보기:
먼저 팩토리 메서드 패턴을 적용하지 않은 코드부터 보자. 예시는 whiteCar와 blackCar를 만드는 CarFactory와, 자동차 클래스인 Car, 자동차를 만들어달라고 요청하는 Client로, 총 3개의 클래스가 있다.
실행결과:
현재 하나의 factory에서 두 종류의 Car를 만들고 있다. 만약 더 많은 종류의 car가 추가된다면 코드는 더욱 길어지고, 복잡해질 것이다.
현재의 코드는 요구사항이 바뀌면 기존의 코드가 변경되어야한다. 객체지향 설계 원칙인 "확장에는 열려있으나 변경에 닫혀있어야 한다." 중 변경에 열려있는 문제가 현재 코드에서 발생한 것이다.
해결하기 : 변경에 닫혀 있는 코드로 만들기 (OCP지키기)
위의 코드를 변경에 닫혀 있도록 만들어보자.
먼저 CarFactory를 인터페이스로 분리한다.
먼저 orderCar 메소드가 defalut메소드로 선언되었다. 이렇게 하면 구현하는 다른 CarFactory들은 orderCar의 default메소드를 그대로 사용할 수 도 있고, 추가적으로 다른 로직을 삽입할 수도 있다.
또한 createCar는 추상 메서드로 선언되어 CarFactory를 구현하는 클래스는 이를 반드시 구현하도록 하였다. Car를 생산하는 코드는 자동차의 종류마다 제각각 다를 것이 분명하다. 확장에는 열려있고, 변경에는 닫혀 있도록 하기 위해 이와 같이 하였다.
아래에서 이전의 CarFactory와 인터페이스가 된 CarFactory를 비교해보자.
이제 여러 종류의 자동차를 만드는 Factory들이 CarFactory를 implements하면 되는 것이다.
WhiteCarFactory
BlackCarFactory
현재 whiteCar와 blackCar는 자동차의 이름, 색, 로고만 다르므로 위와 같이 createCar만을 오버라이드하여 각각의 자동차를 생성하여 반환하도록 해주었다.
이렇게 하면 BlackCar를 만들기 위해 WhiteCar를 만드는 로직을 건드리지도 않았고, CarFactory의 로직을 건드리지도 않았다.
드디어 확장에는 열려있으나, 변경에는 닫혀있는 코드가 된것이다.
WhiteCar와 BlackCar는 Car를 상속받는다.
WhiteCar
BlackCar
이제 클라이언트 코드를 보자.
클라이언트는 CarFactory가 아닌 CarFactory를 상속한 White, BlackCarFactory를 통해 주문한다.
클래스 다이어그램:
<<inteferface>>Creator는 CarFactory, ConcreteCreator는 BlackCarFactory와 WhiteCarFactory,
<<inteferface>>Product는 Car, ConcreteProduct는 BlackCar, WhiteCar이다.
실행결과는 이전과 같다.
그런데, 새로운 제품이 생길 때 마다, 차의 종류에 따른 Factory를 new하여 자동차를 주문해야 한다.
어쨋든 클라이언트가 자동차를 주문하는 코드가 변경된다. 그렇다면 정말 지금까지 만든 코드가 변경에 닫혀있는 코드라고 할 수 있을까?
이와 같은 문제는 외부에서 의존성을 주입하는 방법으로 해결할 수 있다. 가령 스프링 프레임워크가 그러한 방법을 제공한다. 여기서는 Car를 생성하는 부분을 인터페이스로 분리하면서 새로운 Car가 추가되더라도, 기존의 코드를 수정하지 않는다는 것에 의의를 둔다.
물론 Client코드의 수정을 최소화하기 위해서는 아래와 같이 할 수는 있을 것이다. (하지만 Client코드의 수정은 현재로서는 불가피하다.)
Client에 CarFactory 인터페이스 타입으로 모든 Factory들을 받을 수 있는 메소드를 추가하였다.
팩토리 메소드 패턴을 적용했을 때의 장점과 단점은?
장점: OCP원칙을 지킬 수 있게 해준다. 기존의 인스턴스를 생성하는 로직을 변경하지 않고, 새로운 인스턴스를 다른 방법으로 확장성 있게 추가하는 것이 가능하다. 이와 같이 할 수 있는 이유는 loosely coupled.
즉 인스턴스creator(CarFactory)와 Car인스턴스들(WhiteCar, BlackCar)이 느슨한 결합력을 가지고 협력하기 때문이다.
--> CarFactory는 구체적인 Car의 구현체를 직접 new 하지 않는다. 해서 이들은 느슨한 결합을 하고 있다고 볼 수 있다.
단점: 새로운 자동차가 추가될 수록, 클래스의 수가 계속해서 늘어난다.
"확장에 열려있고, 변경에 닫혀있는 객체 지향 원칙(OCP)"은 무엇인가?
확장에 열려있다 : 새로운 기능을 추가할 수 있다. (CarFactory에 새로운 Car를 만드는 것을 추가할 수 있다.)
변경에 닫혀있다 : 기존의 코드를 변경하지 않는다. (새로운 BlackCar가 추가되어도 기존의 CarFactory, WhiteCar의 코드가 수정되어서는 안된다.)
자바8에 추가된 default 메소드는 무엇인가?
자바8 이전에는 인터페이스에는 추상메서드만을 정의하고, 해당 인터페이스를 implements하는 클래스에서만 이를 직접 구현하여 사용할 수 있었다. 하지만 자바8부터는 인터페이스에 default가 추가되면서 기본 구현체를 만들 수 있게 되었다. 그렇게 되면서 이제는 드 인터페이스를 구현하는 클래스, 상속받는 또다른 인터페이스도 해당하는 기능을 사용할 수 있게 되었다.
추가로 자바9부터는 private 메소드도 구현할 수 있도록 되었는데, 이는 하나의 인터페이스 내에서 default에 중복되는 코드들이 담겨 있다면, 이를 공통화하여 private 메소드로 만들어 사용할 수 있다.
실제 스프링에서 사용되는 예시
스프링에서는 BeanFactory가 위에서 배운 팩토리 메서드 패턴을 가지고 있다.
<<inteferface>>Creator는 BeanFactory,
ConcreteCreator는 ClassPathXmlApplicationContext와 AnnotationConfigApplicationContext,
<<inteferface>>Product는 Object(getBean은 Object타입을 반환한다.), ConcreteProduct는 hello, hello2일 것이다.
출처: 인프런 백기선님 '코딩으로 학습하는 GoF 강의'
https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4
'Design Pattern' 카테고리의 다른 글
6. 어댑터 패턴(Adapter pattern) (0) | 2021.12.17 |
---|---|
5. 프로토타입 패턴(Prototype Pattern) (0) | 2021.12.16 |
4. 빌더 패턴 (Builder Pattern) (0) | 2021.11.18 |
3. 추상 팩토리 패턴 (Abstract factory Pattern) (0) | 2021.11.11 |
1. 싱글톤 패턴 (Singleton Pattern) (0) | 2021.10.30 |