코딩하는 개굴이

[디자인 패턴] 팩토리 메서드 패턴 (Factory Method Pattern) 구현하기 본문

기본기 채우기/디자인 패턴

[디자인 패턴] 팩토리 메서드 패턴 (Factory Method Pattern) 구현하기

개굴이모자 2022. 6. 18. 21:39
반응형

해당 내용은 Inflearn 의 코딩으로 학습하는 GoF의 디자인 패턴 강좌를 기반으로 정리되었음을 알립니다.
해당 내용은 주로 JAVA 기반으로 코드 설명이 진행됨을 알립니다.

객체 생성 관련 디자인 패턴

팩토리 메서드 패턴

어떤 객체를 생성하는 책임을 구체적인 클래스가 아니라 추상적인 인터페이스의 메서드로 감싸는 디자인 패턴이다.
만일 특정 클래스에서 모든 것을 구현한다면 로그를 보는 등으로 절차를 따라가야만하고, 로직이 복잡해진다.


또한, 만일 특정 클래스에서 모든 것을 구현할 경우, 확장을 위해서는 기존 코드의 변경이 불가피한 구조가 될 수 있다. 예를들어, Cat 이라는 클래스 하나에서 생성자 등의 분기로 KoreanCat, BritishCat 이라는 두 종류를 받는다고 한다면, 분기하며 구현되어야하기에 기존 코드의 변경이 필요해진다.


본래 확장에는 열려있고, 변경에는 닫혀있는 코드가 좋은 코드라고 할 수 있다. 기존 코드는 변경이 되지 않으면서 즉, Cat 이 바뀌지 않으면서 BritishCat으로 확장할 수 있어야한다.

 

추상화되어있는 팩토리 메서드는 로직을 더 간단히 보이게 할 수 있고, 위 모든 문제를 해결한다는 장점이 있다.

팩토리 메서드는 우선 아래와 같은 특징을 가진다.

  • 팩토리 역할을 할 Creater인 interface를 만든다.
  • interface 안에 기본적인 구현을 넣고, 그중에서 일부 바뀌어야하는 부분들을 추상 메서드로 빼내, Concrete Creator인 하위 클래스에서 구현할 수 있도록 한다.
  • 팩토리에서 만들어낸(반환하는) Type은 여러가지가 될 수 있으며, Product 라고 한다.

 

위의 특징으로 팩토리 메서드 패턴을 구현해보자.

아래와 같이 Cat 의 형태를 가진 객체를 생성해주는 Creator인 interface CatFarm 클래스를 만든다.

//Cat.java
public class Cat {
    private String name;
    private String eyeColor;
    private String furColor;

    public String getEyeColor() { return eyeColor;}

    public void setEyeColor(String eyeColor) { this.eyeColor = eyeColor; }

    ...
}

//CatFarm.java
public interface CatFarm {
    default Cat adoptCat(String name, String phoneNumber) { // Client 가 호출하는 부분
        validate(name, phoneNumber); //로직의 호출되는 순서를 명확하게 볼 수 있다.
        prepareFor(name);
        Cat cat = bringCat();
        sendMessageTo(phoneNumber, cat);
        return cat;
    }
    /*
        만일 java 8버전을 사용한다면, DefaultCatFactory라는 CatFarm 을 상속 받는 추상 클래스를 생성하여, private 로 생성된 구현부를 모두 그쪽에서 오버라이딩하게 하여 구현하고 KoreanCatFactory, BritishCatFactory가 DefaultCatFactory를 상속하게 하여 동일하게 사용할 수 있다.
    */

    private void validate(String name, String phoneNumber) {
        if (name == null || name.isEmpty()) {
            throw new IllegalArgumentException("고양이 이름을 지어주세요 :)");
        }
        if (phoneNumber == null || phoneNumber.isEmpty()) {
            throw new IllegalArgumentException("연락처를 입력해주세요 :)");
        }
    }

    private void prepareFor(String name) {
        System.out.println(name + " 을 입양하는 중");
    }

    Cat bringCat(); //반드시 하위 클래스가 정의 해주어야함

    private static void sendMessageTo(String phoneNumber, Cat cat) {
        System.out.println(phoneNumber + "님 " + cat.getName() + " 을 입양 할 준비가 되었습니다.");
    }
}

위처럼 bringCat이라는 메서드 구현부는 하위 클래스에 위임한다. 경우에 따라, 어떤 Type의 Cat을 가져올지 달라지는 부분이기 때문이다. 그 외의 로직은 공통적인 부분으로, interface의 default 에서 일괄 구현되어있다.

 

 

Cat을 상속하는 KoreanCat이라는 클래스를 생성해보도록하자. 아래와 같은 속성을 가진 KoreanCat가 Creator 에서 반환되기 위해서는 CatFarm을 상속하는 KoreanCatFarm 이라는 하위 클래스에서 bringCat을 오버라이딩 하여 KoreanCat을 리턴해야한다.

//KoreanCat.java
public class KoreanCat extends Cat {
    public KoreanCat() {
        // KoreanCat은 Cat의 속성에 대해 아래와 같은 값을 가진다.
        setEyeColor("Green");
        setFurColor("Yellow");
        setName("KoreanCat");
    }
}

//KoreanCatFarm.java
public class KoreanCatFarm implements CatFarm {
    @Override
    public Cat bringCat() {
        //Korean Cat Farm 만의 특별한 로직을 추가 가능
        return new KoreanCat();
    }
}

이때, BritishCat이라는 타입을 추가하고 싶다고 하더라도 어렵지 않다.
위의 KoreanCat을 리턴하기 위한 절차 그대로 구현하면 된다.

//BritishCat.java
public class BritishCat extends Cat {
    public BritishCat() {
        setEyeColor("Blue");
        setFurColor("Grey");
        setName("BritishCat");
    }
}

//BritishCatFarm.java
public class BritishCatFarm implements CatFarm {
    @Override
    public Cat bringCat() {
        //British Cat Farm 만의 특별한 과정을 추가 가능
        return new BritishCat();
    }
}

물론, 클라이언트의 구현부는 아래와 같이 BritishCatFarm 을 new로 생성하는 부분이 추가되는 것이 반복 되는 것처럼 변화가 조금씩 필요하다.

 

보다싶이 현재 구조로는 매번 새로운 팩토리 (creator의 하위 클래스)나 Product가 생길 때마다 Client 코드는 변경이 필요하다는 단점이 있다. 이는 팩토리 메서드 패턴은 Product와 Creator 를 확장할 때, 기존 Factory 코드가 변경되지 않아야한다는 점에 초점이 되어있기 때문이다.

public Class Main {
    public static void main(String[] args) {
        Cat koreanCat = new KoreanCatFarm().adoptCat("KoreanCat", "01012345678");
        System.out.println(koreanCat.getName() + " 의 눈 색상 " + koreanCat.getEyeColor() + " , 털 색상 " + koreanCat.getFurColor());

        Cat britishCat = new BritishCatFarm().adoptCat("BritishCat", "01098765432");
        System.out.println(britishCat.getName() + " 의 눈 색상 " + britishCat.getEyeColor() + " , 털 색상 " + britishCat.getFurColor());
    }
}

구조를 해치지 않는 선에서 인터페이스 기반에서 메서드를 생성하여 의존성을 넘겨 클라이언트의 변경을 줄이는 방법도 존재한다.

public class Main {
    public static void main(String[] args) {
        Main main = new Main();
        main.print(new KoreanCatFarm(), "KoreanCat", "01012345678");
        main.print(new BritishCatFarm(), "BritishCat", "01098765432");
    }

    private void print(CatFarm catFarm, String name, String phoneNumber) { 
        // print 메서드에서 사용할 의존성을 호출부에 넘긴다.
        Cat cat = catFarm.adoptCat(name, phoneNumber);
        System.out.println(cat.getName() + " 의 눈 색상 " + cat.getEyeColor() + " , 털 색상 " + cat.getFurColor());
    }
}

Factory Method Pattern 은 항상 interface, class 구조이어야하는 것은 아니다. 어떤 방식이든 동일하게 Product 군에도 Creator 군에도 계층 구조가 존재하여, Factory 안에서 구체적인 Product 를 만들어내는 구조를 띄는 것이 중요하다.


팩토리 메소드 패턴 복습

  • 팩토리 메소드 패턴을 적용했을 때의 장/단점?
  • 확장에 열려있고 변경에 닫혀있는 객체 지향 원칙에 대해 설명?
  • 자바 8에 추가된 default 메서드에 대해 설명?


답안

더보기

 

  • 팩토리 메서드 패턴을 적용했을 때의 장/단점?
    • 장점 : Creator와 Product 간의 coupling 을 느슨하게 가져가는 구조(Loose Coupling)로써 확장에는 열려있고 변경에는 닫혀있는 방식을 사용해, 기존 instance 를 만드는 로직을 건드리지 않고 새로운 instance 를 다른 방법으로 확장이 가능하다. 따라서, 코드는 더 간결해지고, 기존 코드가 복잡해지지 않도록 한다.
    • 단점 : 역할의 구체화로 클래스의 개수가 많아지고 확장에 따라 클라이언트 단의 코드가 변경되어야함은 그대로이다.

  • 확장에 열려있고 변경에 닫혀있는 객체 지향 원칙에 대해 설명?
    • 기존 코드를 변경하지 않으면서 새로운 기능을 얼마든지 확장할 수 있는 구조의 객체 지향 원칙을 말하며, 팩토리 메서드 패턴은 그런 구조를 지향하며 구현이 가능하다.

  • 자바 8 에 추가된 default 메서드에 대해 설명?
    • 자바 8 에 들어가는 default 기능은, 인터페이스에 기본적으로 들어가는 구현체를 만들 수 있는 기능으로, 상속받는 또 다른 구현체도 해당 부분을 똑같이 이용할 수 있도록 한다. (그 전에는 추상클래스를 생성하여 일부 구현하는 방식으로 사용하였다.)
반응형
Comments