JustDoEat

Builder 패턴 직접 만들어 써봐, 왜 쓰는지 아니 ? 난 몰랐었어 본문

카테고리 없음

Builder 패턴 직접 만들어 써봐, 왜 쓰는지 아니 ? 난 몰랐었어

kingmusung 2024. 10. 15. 01:05

개요

객체를 생성할 때, 무분별한 Setter를 사용했었다, 이 방법은 영 좋지 않다는 걸 알았다.

 

왜냐면 객체가 생성이 되어진 시점에서 setName(), setTitle() 이런 식으로 객체의 상태를 바꾸게 된다면 "객체의 불변성"이라는 원칙을 깨버릴 수도 있기 때문이다, 한번 만들어진 객체는 가급적 수정이 일어나면 안되기 때문이다.

 

아 이걸 깨달은 건 다른 레포지토리들을 보면 @Builder 어노테이션을 사용한 걸 눈여겨보다 이 친구는 뭐 하는 친구인지 너무 궁금했다.

 

@Builder 어노테이션은 결국 Builder패턴을 구현한 거기 때문에, 본래의 것을 쓸 줄 알아야 의미가 있다고 생각한다.

(이러고 그냥 쓰는 것도 많지요~.. 하하..)

 


 


객체생성과 동시에 생성자를 이용해 값을 초기화, 기본생성자로 객체 생성 후 Setter로 값을 주입하는 과정을 가볍게 보고 이게 왜 문제이고 이 문제를 해결하기 위해 Builder 패턴을 사용해서 같은 결과를 도출해 보겠다.

 

즉 우리의 목표는 같은 황금사과이지만 야무진 방법으로 만드는 것이다.

 


"위 조합법을 코드로 그대로 옮겨보자"

 

객체를 생각하는 과정을 생각을 해보자.

 

생성은 객체 생성과 동시에 생성자로 필드값을 초기화할 것이다.

 

Apple이라고 예를 들겠다.

 

Public Class Apple{
	private String name;
    private String color
    
    Public Apple(String name,String Color){
    this.name = name;
    this.color = color
    }
}

Apple goldApple = new Apple("황금사과","Gold")

 

대략 이런 느낌일 거다, 객체 생성과 동시에 객체의 속성값을 전부 넣어서 초기화를 해준다.

 

일단 해당 사진을 첨부한 마인크래프트 게임에서는, 저 황금사과를 한번 만들면 보라색사과, 파란색사과로 만들 수 없다.

 

즉! 객체의 불변성이 보존이 되는 것이다. 생성시점에 값을 넣으면 그 객체는 불변하다는 것이다.

 

"이 방법의 문제점들을 나열해 보자"

 

위와 반대로

Apple goldApple = new Apple("Gold","황금사과")

 

객체 생성을 할 때 생성자의 인자 값을 위와 반대로 넣어보자, 둘 다 String타입이라 컴파일 에러는 안 나오지만.

 

내가 의도하는 결과를 얻지 못할 것이다.

 

"문제점을 하나만 더 추가해 볼까? "

필드에 속성을 5개 더 늘렸다.

public class Apple {
    private String name;
    private String color;
    private String size; // 크기
    private double weight; // 무게
    private String taste; // 맛
    private String origin; // 원산지
    private double price; // 가격

    public Apple(String name, String color, String size, double weight, String taste, String origin, double price) {
        this.name = name;
        this.color = color;
        this.size = size;
        this.weight = weight;
        this.taste = taste;
        this.origin = origin;
        this.price = price;
    }
}

Apple goldApple = new Apple("황금사과", "Gold", "Large", 150.0, "Sweet", "마인크레프트", 50000);

 

생성하는 방식에는 문제가 없어 보인다, 근데 여러분들은 이제 사과를 만들 때마다. 

Apple goldApple = new Apple("황금사과", "Gold", "Large", 150.0, "Sweet", "USA", 1.99);

 

몇 번째에 어떤 인자가 들어가야 하는지 알고 있어야 오류를 피할 수 있다.

 

"그럼 Setter 쓰면 되잖아, Setter를 사용해서 똑같은 결과를 도출해 보자"

그럼 이제 객체를 생성 후 Setter를 이용하여 값을 넣는다고 생각해 보자.

 public class Apple {
    private String name;
    private String color;
    private String size; 
    private double weight; 
    private String taste; 
    private String origin; 
    private double price; 


}

        Apple goldApple = new Apple();
        goldApple.setName("황금사과");
        goldApple.setColor("Gold");
        goldApple.setSize("Large");
        goldApple.setWeight(150.0);
        goldApple.setTaste("Sweet");
        goldApple.setOrigin("마인크레프트");
        goldApple.setPrice(50000);

 

사과 객체를 생성 후 setter를 이용해 값을 넣는다고 해보자, 언뜻 보면 문제는 없어 보인다.

 

결과만 놓고 봤을 때는 지금 했던 모든 방법들이 다 똑같기 때문이다. 똑같은 황금사과이다.

 

"그래서 이 방법이 뭐가 문제인데? 값만 잘 들어가고 결과만 같으면 되잖아"

 

객체는 한번 만들어진 상태에서 가급적 수정이 안 일어나는 게 가장 Best Errort이다. 그래서 Setter를 사용을 하면 누군가 임의로 만들어진 황금사과를 보라색사과로 바꿀 수 있다. => 캡슐화의 원칙, 불변성을 어기는 꼴임.

 

그래서! 객체생성시점에 생성자에 값을 넣어서 초기화를 할 수 있다. 물론 이 방법이 잘못된 건 아니라고 생각한다. 다만! 위에서 본 문제점을 상기해 보자, 일단  생성자에 들어가는 인자들의 순서를 기억해야 한다. 

    public Apple(String name, String color, String size, double weight, String taste, String origin, double price) {
        this.name = name;
        this.color = color;
        this.size = size;
        this.weight = weight;
        this.taste = taste;
        this.origin = origin;
        this.price = price;
    }
}

Apple goldApple = new Apple("황금사과", "Gold", "Large", 150.0, "Sweet", "마인크레프트", 50000);

 

바로 위 코드는 오류가 나지 않는다. 왜냐면 생성자에 들어가는 인자들의 순서를 맞추어주었기 때문이다.

public Apple(String name, String color, String size, double weight, String taste, String origin, double price)

 

그러면 잘못된 예시를 볼까?

Apple goldApple = new Apple("마인크레프트", "Gold", "Large", 150.0, "Sweet", "황금사과", 50000);

 

이 경우에는 오류는 일으키지 않지만 내가 의도한 결과가 안 나온다, 사과의 이름에 "마인크래프트"가 들어가고

원산지에 "황금사과"가 들어가기 때문이다.

 

Apple goldApple = new Apple("황금사과", "Gold", "Large", 150.0, "Sweet", 50000, "마인크레프트");

 

이 경우는 에러를 일으킨다, Double 자료형을 인자로 받는 곳에 "마인크래프트"라는 문자열을 집어넣었고, String 자료형을 받는 곳에 50000이라는 숫자를 입력했기 때문이다.

 

이처럼 생성자의 인자가 많은 경우 생성자를 이용해 객체의 초기값을 설정해 주는 건 사용자의 실수에 따라 위험요소가 있기 때문이다.


그래서 등장 한 Builder 패턴

장점

Builder 패턴은 객체 생성시점에 값을 넣어주지만, 생성자처럼 인자를 기억할 필요가 없고, 또한 초기화를 해줄 필요가 없는 필드값에 대해서 별도의 처리를 해줄 필요가 없다는 것이다.

 

"황금사과를 빌더패턴으로 만들어보자"

public class Apple {
    private String name;
    private String color;

    public static class Builder {
        private String name;
        private String color;

        public Builder name(String name) {
            this.name = name;
            return this;
        }

        public Builder color(String color) {
            this.color = color;
            return this;
        }

        public Apple build() {
            Apple apple = new Apple();
            apple.name = this.name;
            apple.color = this.color;
            return apple;
        }
    }
}


Apple apple = new Apple.Builder()
        .name("황금사과")
        .color("노랑")
        .build();

 

클래스 내부에 정적 클래스를 선언

부모 클래스 Apple 내부에 Builder라는 이름을 가진 정적 클래스를 선언.

Builder 클래스 안에(정적 클래스) 필드를 부모 클래스와 동일하게 설정

Builder 클래스 내부에 부모 클래스의 필드와 동일한 변수를 선언. 

Setter와 유사한? 메서드를 생성

부모클래스 안에 만들어 둔 Builder 클래스 안에 있는 각각에 필드에 값을 설정하는 메서드를 만들어준다. 

여기서 핵심은 반환값으로 " 자기 자신의 타입 = Builder " 이유는 메서드체 이닝을 이용해서 값을 쭉 세팅을 할 예정이기 때문이다. 

부모 클래스 타입의 객체를 반환하는 메서드를 생성

마지막으로 build()라는 메서드를 만들어서, Apple 객체를 반환.


정리

메시드 체이닝을 이용해, 자기 자신을 반환함으로 자기 자신이 가지고 있는 메서드를 연속으로 실행시킬 수 있다.

자기 자신이 가지고 있는 메서드라 함은 방금 설명한 " Setter와 유사한? 메서드 " 겠다.

 

값을 세팅 -> 자기 자신 리턴 -> 세팅 -> 자기 자신의 값을 부모객체에 주입 -> 부모객체 리턴

 

이와 같은 방식으로 구현이 가능하다.

 


 

 

마무리