[GoF의 디자인 패턴] 빌더 패턴 (Builder) (2/23)

Seyeong·2022년 12월 31일
0

GoF의 디자인패턴

목록 보기
3/5

이번 장에서는 빌더 패턴에 대해 알아봅시다.
먼저 빌더 패턴이 무엇인지 알아야겠죠.

빌더 패턴이란?

복합 객체의 생성 과정과 표현 방법을 분리하여 동일한 생성 절차에서 서로 다른 표현 결과를 만들 수 있게 하는 패턴입니다.

이 역시 이전 패턴 설명에서와 같이 이해가 가질 않는다.
쉽게 빌더 패턴의 구조에 대해 말하자면, 아래와 같다.

  • 상품

    빌더를 이용해서 만들 제품을 의미함
  • 빌더

    상품을 어떤식으로 만들지 이곳에서 정의합니다. 여기서 정의된 대로 상품이 제작되며, 같은 상품이라도 빌더마다 서로 다르게 제작할 수 있습니다. 예를 들어 같은 자동차를 만들더라도, 어느 빌더는 바퀴가 4개 달린 자동차를 만들고 어느 빌더는 바퀴가 8개인 자동차를 만들 수 있습니다. 마치 빌더는 상품의 제작서라고 볼 수 있습니다.
  • 디렉터

    빌더가 정의한 상품 내용대로 실제 상품을 제작합니다. 쉽게 말해 제작서 기능을 하는 빌더를 이용해 제작서에 명시된대로 상품을 제작하는 책임을 가진게 디렉터입니다.

위의 상품, 빌더, 디렉터를 읽으며 빌더 패턴에 대해 감이 잡히시나요?

아직 감이 잡히지 않아도 괜찮습니다. 예시 코드로 이해할 수 있으니까요.

인터페이스로 빌더 패턴 적용

먼저 빌더 패턴을 이용해서 무엇을 만들지를 정의해야 합니다.
여기서 예제는 Car 를 만든다고 가정해봅시다.

상품

public class Car {
}

우리는 빌더 패턴을 이용하여 자동차를 만들테니 이를 명시해줍시다.

public class Car {
    private String product;

    public Car() {
        this.product = "자동차";
    }
}

Car 객체를 생성하면 "자동차" 라는 문자열을 가진 상품이 생성됩니다.

이제 이 자동차를 어떻게 제작할지를 나타내기 위해 빌더를 만들겁니다.
우선, 여러 종류의 빌더를 사용할 수 있도록 빌더 인터페이스를 구현해봅시다.

빌더

인터페이스

public interface CarBuilder {
    void buildCar(); // 차를 만들래
    void buildWheel(); // 바퀴를 만들래
    void buildWing(); // 날개를 만들래
    Car getCar(); // 지금껏 만들어진 차를 제공할래
}

자동차를 만드는 빌더에 여러 오퍼레이션이 담겨있습니다. 이제 이 인터페이스를 상속받아서 여러 종류의 다양한 자동차 빌더를 만들어봅시다.

구현체

public class StandardCarBuilder implements CarBuilder {

    @Override
    public void buildCar() {
    }

    @Override
    public void buildWheel() {
    }

    @Override
    public void buildWing() {
    }

    @Override
    public Car getCar() {
        return null;
    }
}

이렇게 표준 자동차 빌더를 만들어주었습니다. 이제 각각의 메서드에 자동차에 대한 표준을 적어주면 될 것입니다. 예를 들어 buildWheel( ) 메서드에서는 바퀴가 4개가 달리도록 명세하면 되겠죠.

우선 제작하기 전에, Car 객체에서 이러한 옵션들을 받을 수 있게 메서드를 추가해주어야 합니다.

public class Car {
	...

    public void addOption(String option) { // 빌더가 요구하는 옵션 추가
        this.product = option + product;
    }
}

이제 이 옵션 추가 메서드를 이용하여 빌더가 요구하는 대로 자동차를 설계할 수 있습니다.

public class StandardCarBuilder implements CarBuilder {
    public Car car;

    @Override
    public void buildCar() {
        this.car = new Car();
    }

    @Override
    public void buildWheel() {
        car.addOption("바퀴가 4개 달린 ");
    }

    @Override
    public void buildWing() {
        car.addOption("날개가 없는 ");
    }

    @Override
    public Car getCar() {
        return car;
    }
}

자동차에 대한 빌더(설계서) 를 만들었습니다. 이제 이 설계서를 건네주고 실제로 설계를 맡아 책임 질 디렉터를 만들어야 합니다.

디렉터

public class CarDirector {
    private CarBuilder builder;

    public CarDirector(CarBuilder builder) {
        this.builder = builder;
    }
}

자동차 디렉터는 빌더(설계서) 를 생성자로 받아옵니다. 그리고 이 설계에 적힌대로 자동차를 생산할 책임을 가지게 되죠. 실제 자동차를 만들어봅시다.

 public class CarDirector {
 	...
    
    public void build() {
        builder.buildCar();
        builder.buildWheel();
        builder.buildWing();
    }

    public Car getCar() {
        return builder.getCar();
    }
}

디렉터는 현재 빌더에 어떤 표현이 되어있는지 모릅니다. 즉, 디렉터는 빌더가 바퀴를 몇개 생성하길 원하는지, 날개가 존재하는지 여부를 전혀 알 수 없습니다. 그저 설계서에 적힌대로 자동차를 설계할 뿐입니다.

build( ) 메서드를 통해 자동차 생성이 완료되면 getCar( ) 를 통해 빌더로부터 생성된 자동차를 넘겨받을 수 있습니다.

이제 Main 함수에서 실제로 자동차를 생산해봅시다.

public class BuilderMain {
    public static void main(String[] args) {
    	// 디렉터에게 표준 자동차 설계서를 쥐어준다.
        CarDirector director = new CarDirector(new StandardCarBuilder());
        
        // 만들어라
        director.build();
    }
}

디렉터에게 표준 자동차 설계서를 생성자로 넘겨주고 만들도록 지시합니다.
이렇게 생성된 자동차를 가져와봅시다.

public class BuilderMain {
    public static void main(String[] args) {
        CarDirector director = new CarDirector(new StandardCarBuilder());
        director.build();

        Car car = director.getCar(); // 만들어진 자동차를 가져와라
        car.show(); // 어떻게 만들어졌는지 보여줘라
    }
}

아직 Car 클래스에 show( ) 메서드를 구현하지 않았습니다만, 주석을 읽어보면 알 수 있듯이 만들어진 자동차를 받아와 어떻게 만들어졌는지를 출력하도록 하고 있습니다. 이제 마지막으로 Car#show( ) 메서드를 만들고 출력을 확인해봅시다.

public class Car {
	...

    public void show() {
        System.out.println(this.product);
    }
}

결과

날개가 없고 바퀴가 4개 달린 표준 자동차를 만들어 보았습니다.
만약 여기서 새로운 자동차 설계서를 만들고 싶다면 어떻게 하면 될까요?

Builder 인터페이스를 상속받은 또 다른 클래스를 생성해주고 거기에 설계 명세서를 작성해주면 새로운 설계서가 생길 것입니다. 이제 그 새로운 설계서를 디렉터에게 전달해주면 만들어주겠죠.

이제 하나 더 만들어 볼게 있습니다. 방금 위에서 말한것과 비슷한데, 이번엔 인터페이스로 만들지 않을 것입니다. 추상 클래스로 만들거에요.

그 이유는 인터페이스는 모든 메서드가 추상 메서드로 구성되어 있습니다. 따라서 인터페이스에서는 어떠한 구현도 할 수 없으며 오직 상속을 통한 구현체에서만 메서드를 구현할 수 있죠. 바로 이게 문제입니다.

만약, 만들고 싶은 자동차들이 모두 날개는 없지만 바퀴수가 제각각 일 수도 있습니다. 우리가 하고 싶은건 표준 설계서에서 벗어난 부분만을 따로 재정의해주고, 어차피 표준 설계서대로 지킬 부분들은 재정의하지 않아도 지켜졌으면 하는데 인터페이스는 그것을 방해합니다. 표준 설계서대로 만들 부분까지도 모두 재정의 해야하죠.

GoF 디자인 패턴 책에서도 이 부분을 지적합니다.

빌더에 정의된 메서드를 의도적으로 가상 함수로 정의하지 않는다. 그 이유는 서브클래스에서 모든 가상 함수가 아니고 필요한 메서드만 재정의하기 위해서이다.

이를 이용해 추상 클래스로 새로운 빌더(설계서) 를 만들어봅시다.

추상 클래스로 빌더 패턴 적용

현재 만든 CarBuilder 인터페이스는 이제 필요 없습니다.
이번엔 인터페이스를 이용하지 않고 추상 클래스를 이용할 것이기 때문이죠.

빌더

public abstract class AbstractCarBuilder {
    Car car;

    public void buildCar() {
        car = new Car();
    }

    public abstract void buildWheel(); // 바퀴 수 재정의 요구

    public abstract void buildWing(); // 날개 재정의 요구

    public Car getCar() {
        return this.car;
    }
}

다른 빌더들이 공통으로 사용하는 buildCar( ) , getCar( ) 같은 메서드는 재정의할 필요가 없다. 그래서 바퀴 개수나 날개 여부만 재정의하도록 하였습니다. 참고로 아까 만들었던 CarBuilder 인터페이스를 그대로 추상 클래스로 변경할까 고민했지만, 혹시나 코드를 같이 치며 공부하는 분들을 위해 인터페이스를 남겨둬서 추상 클래스와 비교하며 볼 수 있도록 새로 만들어 주었습니다. 이 추상 클래스는 그냥 CarBuilder 라고 생각하시면 됩니다.

public class SpecialCarBuilder extends AbstractCarBuilder {
    @Override
    public void buildWheel() {
    }

    @Override
    public void buildWing() {
    }
}

이를 상속받은 클래스들은 위의 메서드들만 재정의 하면 됩니다.

public class SpecialCarBuilder extends AbstractCarBuilder {
    @Override
    public void buildWheel() {
        car.addOption("바퀴가 12개 달린 ");
    }

    @Override
    public void buildWing() {
        car.addOption("날개가 무수히 달린 ");
    }
}

여기서 참조하는 car는 부모 클래스인 추상 클래스에서 선언한 car를 의미합니다.

이제 이러한 빌더(제작서) 를 이용하여 자동차를 제작하도록 디렉터에게 요청해봅시다.

public class BuilderMain {
    public static void main(String[] args) {
        CarDirector director = new CarDirector(new SpecialCarBuilder());
        
        ...
    }
}

제작서까지 잘 전달해주었으니 이제 디렉터가 제작을 할 수 있게 되었습니다.
그런데 현재 디렉터는 받을 수 있는 제작서의 종류로 CarBuilder 인터페이스로 정의해주었기 때문에 오류가 날 수 있습니다.
이 부분을 추상 클래스로 생성자를 통해 제작서를 받아오도록 수정해주면 끝납니다.

디렉터

public class CarDirector {
    private AbstractCarBuilder builder; // 변경 발생

    public CarDirector(AbstractCarBuilder builder) {
        this.builder = builder;
    }
 	
    ...
}

결과

특징

빌더 패턴의 특징은 다음과 같습니다.

  1. 제품에 대한 내부 표현을 다양하게 변화할 수 있습니다. 빌더를 사용하면 제품이 어떤 요소에서 복합되는지, 그리고 각 요소들의 표현 방법이 무엇인지 가릴 수 있게 됩니다. 즉, 어떤 요소로 전체 제품을 복합하고 그 요소들이 어떤 타입들로 구현되는지 알고 있는 쪽은 빌더뿐입니다.

  2. 생성과 표현에 필요한 코드를 분리합니다. 빌더 패턴을 사용하면, 복합 객체를 생성하고 복합 객체의 내부 표현 방법을 별도의 모듈로 정의할 수 있습니다.

  3. 복합 객체를 생성하는 절차를 좀더 세밀하게 나눌 수 있습니다. 빌더 패턴은 디렉터의 통제 아래 하나씩 내부 구성요소들을 만들어 나갑니다. 디렉터가 빌더에서 만든 전체 복합 객체를 되돌려받을 때까지 제품 복합의 과정은 계속 됩니다. 그렇기 때문에 Builder 클래스의 인터페이스는 이 제품을 생성하는 과정 자체가 반영되어 있습니다.

0개의 댓글