개발자로 후회없는 삶 살기

디자인 패턴 PART.팩토리 메서드 패턴 본문

[백엔드]/[Java | 학습기록]

디자인 패턴 PART.팩토리 메서드 패턴

몽이장쥰 2023. 8. 13. 16:34

서론

※ 이 포스트는 다음 강의의 학습이 목표임을 밝힙니다.

https://www.inflearn.com/course/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4/dashboard 

 

코딩으로 학습하는 GoF의 디자인 패턴 - 인프런 | 강의

디자인 패턴을 알고 있다면 스프링 뿐 아니라 여러 다양한 기술 및 프로그래밍 언어도 보다 쉽게 학습할 수 있습니다. 또한, 보다 유연하고 재사용성이 뛰어난 객체 지향 소프트웨어를 개발할

www.inflearn.com

 

본론

-> 만드는 방법

먼저 팩토리 역할을 할 인터페이스(Creator)를 만들고 기본적인 구현을 구현 메서드로 만들고 제품마다 일부 바뀌어야할 부분을 추상 메서드로 만들어서 하위 구체 클래스에서 구현하도록 합니다. 하위 구체적인 클래스에서 구체적인 객체를 만들게 되는 것입니다.

 

또한 이에 대응하는 팩토리에서 만든 객체의 타입을 Product라고 하고 얼마든지 다양한 Product를 만들 수 있도록 Product도 인터페이스로 만들고 구체적인 (Concrete) 객체를 만들게합니다.

 

-> 코드

Ship whiteship = ShipFactory.orderShip("Whiteship", "hsb@mail.com");
System.out.println(whiteship);

Ship blackship = ShipFactory.orderShip("Blackship", "hsb@mail.com");
System.out.println(blackship);

고객이 화이트라는 배를 만들어 달라고 공장에 요청을 하고 다 만들면 이메일을 보내달라며 주문합니다.

 

// 대소문자를 무시하는 문자열 자체 비교
if (name.equalsIgnoreCase("whiteship")) {
    ship.setLogo("모터보트 로고");
} else if (name.equalsIgnoreCase("blackship")) {
    ship.setLogo("⚓");
}

if (name.equalsIgnoreCase("whiteship")) {
    ship.setColor("whiteship");
} else if (name.equalsIgnoreCase("blackship")) {
    ship.setColor("black");
}

공장에서는 orderShip 메서드에 이름에 따라서 다르게 입히는 코드가 한 곳에 모여있습니다. 만약 또 다른 제품이 추가되면 요구사항 때문에 생성의 책임을 가지고 있는 orderShip 코드에 else if가 계속 추가됩니다. 이는 OCP를 만족하지 않는 것으로 확장에는 열려있고 변경에는 닫혀있어야 하는데 닫혀있지 않는 것입니다.

 

- 팩토리 메서드 패턴 적용

이 코드를 고쳐서 확장을 해도 기존 코드는 변경하지 않도록 해보겠습니다.

 

ShipFactory에서 제품에 따라 달라지는 부분만 밖으로 빼서 인터페이스로 고칠 것입니다. 위 형태를 그대로 만들어보겠습니다.

 

-> ShipFactory 인터페이스

ShipFactory 인터페이스를 만들 것이기에 기존 구체적인 클래스는 WhiteShipFactory 클래스로 이름을 바꿉니다. 위 그림으로 보면 ShipFactory가 Creator 인터페이스이고 실제로 생성하는 구체적인 클래스인 WhiteShipFactory가 ConcreteCreator입니다.

 

그렇다면 ShipFactory에 templateMethod가 있어야하고 동일한 구조는 default 메서드로 가지고 변경이 될 부분은 추상 메서드로 가져야 하며 구체적인 WhiteShipFactory에서 추상 메서드를 구현해야 합니다.

 

// 기존
public static Ship orderShip(String name, String email) {
    // validate
    if (name == null || name.isBlank()) {
        throw new IllegalArgumentException("배 이름을 지어주세요.");
    }
    if (email == null || email.isBlank()) {
        throw new IllegalArgumentException("연락처를 남겨주세요.");
    }

    prepareFor(name);

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

    // Customizing for specific name
    if (name.equalsIgnoreCase("whiteship")) {
        ship.setLogo("\uD83D\uDEE5️");
    } else if (name.equalsIgnoreCase("blackship")) {
        ship.setLogo("⚓");
    }

    // coloring
    if (name.equalsIgnoreCase("whiteship")) {
        ship.setColor("whiteship");
    } else if (name.equalsIgnoreCase("blackship")) {
        ship.setColor("black");
    }

    // notify
    sendEmailTo(email, ship);

    return ship;
}

// 이후
default Ship orderShip(String name, String email) {
    validate(name, email);
    prepareFor(name);
    Ship ship = createShip();
    sendEmailTo(email, ship);
    return ship;
}

WhiteShipFactory에서 하던 OrderShip을 인터페이스로 옮깁니다. OrderShip을 템플릿 메서드로 바꿀 것입니다. 팩토리 역할을 할 인터페이스(Creator)를 만들고 동일한 부분의 구현은 기본 메서드로 만들고 제품마다 일부 바뀌어야할 부분을 추상 메서드로 만들어서 하위 클래스에서 구현하도록 할 것이라고 했습니다.

 

// 동일한 부분
private void validate(String name, String email) {
    if (name == null || name.isBlank()) {
        throw new IllegalArgumentException("배 이름을 지어주세요");
    }

    if (email == null || email.isBlank()) {
        throw new IllegalArgumentException("연락처를 남겨주세요");
    }
}

private void prepareFor(String name) {
    System.out.println(name + " 만들 준비 중");
}

private static void sendEmailTo(String email, Ship ship) {
    System.out.println(ship.getName() + " 다 만들었습니다.");
}

가장 먼저 검증을 하고 prepareFor을 호출하며 이것들은 동일한 부분이기에 기본 메서드로 만듭니다.

 

Ship createShip();

그 다음은 ship을 만듭니다. 하지만 배를 만드는 것은 제품마다 바뀌어야할 부분이라서 하위 클래스에 위임을 할 것이라서 createShip이라는 추상 메서드를 만듭니다. 이 부분이 위 그림에서 createProduct() 추상 메서드입니다.

 

그 후 원래는 이름을 넣고 로고를 만들고, 색을 칠하는데 이 둘도 특정한 배에 특화되어있는 로직이기 때문에 하위 클래스에 위임하기 위해 createShip에 넣을 것이고 send 이메일만 ShipFactory 인터페이스에 넣습니다.

 

검증 > 준비 > 이름 > 로고 > 색깔 > 이메일 알림

위가 전부 다 구체 클래스에 있어서 제품이 바뀔 때마다 코드의 추가가 필요했는데 이를 막기 위해 인터페이스로(Creator) 감싸고 템플릿 메서드를 만들고

 

검증 > 준비 > ,,, > 이메일

위 3가지 공통 기능은 인터페이스에 가지도록 하고

 

이름 > 로고 > 색깔

특정 제품에 특화된 로직은 인터페이스에 createShip() 추상 메서드를 가지도록 해서 이를 구현하는 구현 클래스에서 만들도록 하였습니다. 이전과 같은 코드인데 한 곳에 정리를 해두었기에 읽기도 편합니다.

 

-> Product

제품도 Product 인터페이스를 만들고 구체적인 객체를 만들 것이라고 했습니다. 클래스로 해도 상관없습니다. 지금은 Ship 클래스를 Product 인터페이스라고 생각합니다. 

 

public class Ship {


public class Whiteship extends Ship{

    public Whiteship() {
        setName("whiteship");
        setLogo("\uD83D\uDEE5️");
        setColor("white");
    }
}

Ship을 상속받는 것으로 해서 WhiteShip 클래스를 만들었습니다. 생성자에서 기존 orderShip에서 만들었던 제품에 특화된 로직을 다 셋팅합니다.

 

-> WhiteShipFactory

제품 생산을 하는 구체적인 클래스(ConcreteCreator)는 이제 orderShip은 인터페이스에 있고 Creator(ShipFactory)에 있는 추상 메서드만 구현하면 됩니다.

 

 

// 구체 Product
public class Whiteship extends Ship{
    public Whiteship() {
        setName("whiteship");
        setLogo("\uD83D\uDEE5️");
        setColor("white");
    }
}

// 구체 Creator
public class WhiteShipFactory implements ShipFactory {
    @Override
    public Ship createShip() {
        return new Whiteship();
    }
}

그러면 createShip에서 할 일은 new WhiteShip()이 전부입니다. 배를 만드는 공정이 이 특정 객체에만 특화되어있는 공정을 생성자에서 다 했기 때문에 팩토리에서 별다르게 해줄 것이 없습니다. 제품을 실제로 생성하는 코드는 구체 Creator에 두고 전체 과정을 추상 Creator로 한 번 감싼 것입니다.

 

// 기존
Ship ship = new Ship();
ship.setName(name);

// 대소문자를 무시하는 문자열 자체 비교
if (name.equalsIgnoreCase("whiteship")) {
    ship.setLogo("모터보트 로고");
} else if (name.equalsIgnoreCase("blackship")) {
    ship.setLogo("⚓");
}

if (name.equalsIgnoreCase("whiteship")) {
    ship.setColor("white");
} else if (name.equalsIgnoreCase("blackship")) {
    ship.setColor("black");
}

기존에 orderShip에서 제품마다 다르게 해야할 코드를 특정 객체의 생성자에서 해서 if문이 필요없습니다.

 

default Ship orderShip(String name, String email) {
    Ship ship = createShip();
}

이를 인터페이스에서 받아서 ship을 생성합니다.

 

-> 클라이언트

// 기존
Ship whiteship = ShipFactory.orderShip("Whiteship", "hsb@mail.com");
System.out.println(whiteship);

// 이후
Ship whiteship = new WhiteShipFactory().orderShip("whiteship", "hsb@mail.com");

이제는 인터페이스로 생성을 하는게 아니라 구체적인 WhiteShipFactory에서 가져와야 합니다. 화이트 쉽을 만들어보면 동일하게 만들어집니다. 여기서 관건은 블랙쉽을 만들 때 화이트를 만들 때의 코드가 바뀌냐 안 바뀌냐입니다. 

 

여기서 변경에 닫혀있는 구조라면 블랙쉽을 만드는 공정을 추가할 때 기존 코드가 변경이 되면 안됩니다.

 

-> 블랙쉽 제품 추가

public class Blackship extends Ship{
    public Blackship() {
        setName("Blackship");
        setLogo("\uD83D\uDEE5️");
        setColor("Black");
    }
}

기존 코드는 수정할 것이 없고 생산할 블랙쉽 구체 Product만 추가하고 Ship을 상속받은 후 생성자에서 구체적으로 생성하면 됩니다.

 

-> 블랙십 공장 추가

public class BlackShipFactory implements ShipFactory{
    @Override
    public Ship createShip() {
        return new BlackShip();
    }
}

// 클라
Ship blackship = new BlackShipFactory().orderShip("Blackship", "hsb@mail.com");

BlackShipFactory를 만들고 ShipFactory를 구현해서 BlackShip을 리턴하면 됩니다. 이러면 기존 코드를 전혀 건드리지 않고 새로운 공장과 제품을 추가했습니다. 확장을 했고 기존 코드를 변경하지 않아서 OCP를 만족합니다.

 

클라이언트 코드는 바뀌지 않나요? ✅

맞습니다. 그러면 이게 변경에 닫혀있는 게 맞는지 의문을 가질 수 있습니다. 따라서 이런 것들을 의존성 주입으로 인터페이스 기반으로 코드를 작성하고 구체적인 클래스를 의존성 주입하는 코드로 작성하면 클라이언트 코드도 최대한 변경되지 않도록 고칠 수 있습니다.

 

- 인터페이스 적용하기

클라이언트가 주문을 할 때 구체적인 클래스를 알고 new로 생성한 후 주문을 해야하기 때문에 클라이언트의 코드는 얼마든지 바뀝니다. 하지만 클라이언트 코드도 인터페이스 기반으로 구현이 되어있다면 이를 해결할 수 있습니다.

 

public class Client {
    public static void main(String[] args) {
        Client client = new Client();
        client.print(new WhiteShipFactory(), "whiteship", "hsb@mail.com");
        client.print(new BlackShipFactory(), "blackship", "hsb@mail.com");
    }
    
    private void print(ShipFactory shipFactory, String name, String email) {
        System.out.println(shipFactory.orderShip(name, email));
    }
}

client의 print 코드는 절대로 바뀌지 않을 것이고 main 메서드만 밖에 작성해주면 클라이언트 코드도 변경을 없앨 수 있습니다. DI를 사용하여 코드 변경을 없앤 것이라고 볼 수 있습니다.

 

결과적으로 동일한 결과가 나옵니다. 위 코드로 봤을 때 Product를 반드시 인터페이스로 해야하는 것은 아니고 현재처럼 class로 하고 extends해도 괜찮습니다. 중요한 것은 Creator, Product 둘 다 계층 구조가 있어서 팩토리 안에서 구체적인 제품을 createShip()으로 만들어내는 것이 중요한 것입니다.

Comments