Programming

빌더 패턴, 진짜 어떻게 돌아가는지 직접 구현해보기 (쉽다 쉬워..)

송코딩 songcoding 2024. 11. 18. 09:31

안녕하세요 여러분~!

김송아입니다.

 

 

 

여러분 잘 오셨습니다.

오늘은 빌더에 대해 직접! 구현해보는 시간을 가지려 합니다.

맨날 말로만 들었지 빌더 빌더....😇

 

 

사실 스프링에서 빌더 쓰고 싶으면, 보통 롬복으로 그냥 @builder 이렇게 만들어달라고 하곤 하잖아요?

💡롬복
자바 세상에서 정말 많이 사용되고 있는 라이브러리로, 우리 대신 주로 코드 생성을 해주는 친구입니다.
우리는 어노테이션으로 명령할거예요.
예를 들어 @Setter 이렇게만 적으면, setter 메소드들을 알아서 다 만들어주는 기가 막힌 친구랍니다.

 

 

그래서~ 이게 어떻게 돌아가는 코드인지는 알아야! 의미 있을 것 같아 준비해보았습니다.

 

 

그럼 시작할게요?


 

Chapter 1. 생성자 오버로딩으론 해결되지 않는.. (feat. 핸드폰 없는 사람 아닌 사람)

 

자바 예시를 들 때 가장 좋은 예시죠. 사람 객체를 만들기 위한 클래스 Person을 한번 준비해보겠습니다.

사람은 기본 필드로 아래 세가지를 가진다고 가정하겠습니다.

  • 이름
  • 나이
  • 핸드폰번호
public class Person {
    private String name;
    private int age;
    private String phone;
    
    public Person(String name, int age, String phone) {
        this.name = name;
        this.age = age;
        this.phone = phone;
    }
}

 

 

자 그럼, 이 클래스를 활용해서 사람 객체를 한번 만들어볼까요?

Demo라는 실행용 클래스를 별도로 만들어서, 객체를 생성하도록 하겠습니다.

class Demo {
    public static void main(String[] args) {
        Person songa = new Person("songa", 20, "010-1234-5678");
    }
}

 

 

여기까진 아주 쉽게 잘 오셨을 것 같아요.

좋습니다! 그럼 이제 슬슬 하나씩 깊게 들어가보겠습니다.

 

 

만약 핸드폰이 없다면 이라는 가정을 해보도록 하겠습니다.

혹시 핸드폰이 없다면.. 사람이 아닌가요..?

 

 

우린 지금 생성자가 하나밖에 없기 때문에 객체를 만들 수가 없어yo...

시켜줘 사람..

 

자 그럼 핸드폰이 없어도 사람이 될 수 있도록 도와주는 방법으로,

우리는 크게 2가지 방법을 생각할 수 있습니다.

 

1️⃣ 생성자에 맞게 "" 이렇게 빈 값을 넣어줄 수도 있겠죠.

Person songa = new Person("songa", 20, "");

 

2️⃣ 아니면 핸드폰이 없어도 생성될 수 있게 생성자 오버로딩을 해줄 수도 있겠습니다.

public class Person {
    private String name;
    private int age;
    private String phone;

    public Person(String name, int age, String phone) {
        this.name = name;
        this.age = age;
        this.phone = phone;
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

 

 

오 쉽네?

싶으시다면, 아주 잘 오셨습니다.

 

 

이제! 극단적으로 상황을 몰고 가봅니다.

어떤 기술의 장점이 생각나지 않을 때 우린 극단적으로 생각하면 그 장점을 찾기 쉽거든요!

 

 

✔️ 예를 들어 볼게요.

사람이 가지는 정보로 이름, 나이, 핸드폰번호, 주소, 혈액형, MBTI, 좋아하는 음식, 좋아하는 가수 등등 한 뭐 필드가 100개 있다고 생각해보죠.

그럼 여러분들은 혹시 저 중 하나라도 빠지면 사람을 만들 수 없으니까...

 

모~든 경우의 수를 오버로딩으로 다 만들어주실 건가요? 

 

ㅋㅋㅋㅋㅋㅋ @충격 짤

 

 

정말 극단적이죠? (만-족)

자, 드디어! 우리는 이때 빌더를 사용하여 빛을 발할 수 있습니다⭐️

 

등장합니다.

 


Chapter 2. 소리질러 갓.빌.더🤘🏻

 

2-1. 나에게.. 빌더란..?

 

빌 to 더

 

 

빌더란, ✅ 문법적인 장점으로 얘기하자면 생성자 오버로딩을 하지 않고, 필요한 데이터만 설정하여 객체를 생성할 수 있는 방법입니다.

무한~~한 (사실은 유한한) 경우의 수를 모두 고려하여 오버로딩하지 않아도 우리가 원하는 객체를 생성할 수 있다는 겁니다!!

 

 개념적인 장점은 뭐냐구요?

바로. 빌더 라는 겁니다.

 

김송아 뭐라노.. @충격 짤2222

 

 

빌더. 말 그대로 빌드(build)를 해주는 -er(것)이라는 겁니다.

무엇을 빌드 해준다는 걸까요?

 

그렇죠. 객체를 대신 빌드 즉, 생성해주겠다는 녀석입니다🔥

 

 

어떻게 구현하냐구요?

사실 여러분들이 이미 알고 있는 방법인데.. 아주 조금.. 아주 조금! 멋지게 표현한 것 뿐입니다.

그러니, 겁 먹지 마시고! 하나씩 가보겠습니다🔥

 

 

2-2. 빌더에게 객체 생성 책임 전가하기🔥🔥

 

Person 대신 Person 객체를 생성해줄 클래스를 하나 등장시켜봅니다.

이름하여 PersonBuilder 입니다. ㅋㅋㅋㅋㅋㅋ 직관적인 네이밍

class PersonBuilder {
}

 

Person 객체를 대신 만들어줘야 한다면,

이 친구가 new Person 생성자를 호출해줘야한다는 뜻이겠죠?

 

자, 그럼 일단 틀을 만들어봅니다.

build라는 메소드를 만들어서 Person 객체를 생성한 다음, 반환해주는 메소드를 만들어보자구요!

class PersonBuilder {    
    public Person build() {
        return new Person();
    }
}

 

 

자, 그렇다면 지금부터 Person 객체를 생성하기 위해선

PersonBuilder 객체의 build() 메소드를 호출해야하는 겁니다. 책임을 위임했네요!

 

즉, 실행용 클래스 Demo에서 이렇게 new Person() 을 직접 호출하는 것이 아니라는 거고!

class Demo {
    public static void main(String[] args) {
        Person songa = new Person("songa", 20, "010-1234-5678");
    }
}

 

PersonBuilder를 통해서 Person을 만들라는 거겠죠.

그럼 우린 이렇게 PersonBuilder 객체를 생성한 다음, build() 메소드를 호출해주면 되겠습니다.

class Demo {
    public static void main(String[] args) {
        Person songa = new PersonBuilder().build();
//        Person songa = new Person("songa", 20, "010-1234-5678");
    }
}

 

 

여기까지 되셨나요?

여기까지는 PersonBuilder가 Person 객체를 대신 생성해주는 책임만 가져갔어요.

 

 

2-3. 빌더로 객체에 값 넣어보기

 

이제 한단계 더 가봅니다🔥🔥🔥

Person 객체 필드에 값을 넣어볼게요.

 

지금 생각으로는.. 그냥 build 메소드가 이렇게 매개변수로 name, age, phone 값을 받아서

Person 객체 생성할 때 넣어주기만 하면 될 것 같은데..

public Person build(String name, int age, String phone) {
    return new Person(name, age, phone);
}

 

 

그럼 굉장히 아쉬운 건 뭐냐면..

빌더나.. 생성자나.. 실행용 클래스에서 사용할 땐 뭐.. 똑같다는 겁니다😇

class Demo {
    public static void main(String[] args) {
        Person songa = new PersonBuilder().build("songa", 20, "010-1234-5678");

 

심지어 아까 생성자와 마찬가지로, 여전히.. 핸드폰 번호 하나 없으면 객체 생성이 안 되겠는걸요...?

 

 

그럼 어떻게 하냐구요?

자 지금부터 본격적으로 빌더 클래스를 만들어봅니다💡

 

 

build() 메소드가 직접 매개변수를 받을 게 아니라면,

PersonBuilder의 다른 메소드에서 값을 받아서, 필드에 저장한 다음!

그 필드를 활용해야한다는 뜻이죠?

 

그럼 우선 아래와 같이 필드를 마련해줄게요. 

class PersonBuilder {
    private String name;
    private int age;
    private String phone;
    
    public Person build() {
        return new Person(name, age, phone);
    }
}

 

이제 저 필드에 우리 대신 값을 받아줄 메소드를 만들어보겠습니다.

필드에 값을 받아준다라..🤔

 

필드에 값을 대입하는 방법은 크게 세가지가 있습니다.

1️⃣ 생성자

2️⃣ 세터(Setter)

3️⃣ 필드에 직접 대입

 

 

보시다시피 이미 습관적으로 private으로 선언된 필드는 3️⃣번 방법을 사용할 순 없을 것 같구요,

1️⃣번 방법을 쓰려고 보니.. 그럼 아래와 같이 나온다는 건데..

그럼 결국 또 phone 하나 없으면 생성이 안 되니까.. 이것도 안될 것 같습니다.

class PersonBuilder {
    private String name;
    private int age;
    private String phone;
    
    public PersonBuilder(String name, int age, String phone) {
        this.name = name;
        this.age = age;
        this.phone = phone;
    }

 

 

그럼 남은 방법은 바로 2️⃣ 세터(Setter) 입니다.

그냥 name, age, phone을 따로따로 필요할 때마다 받아버리는 겁니다!

 

필요한 값을 필드에 다 대입해둔 다음!

build() 메소드에서 가져다 쓴다면?

 

값이 있으면 들어갈 거고, 값이 없다면 기본값이 들어가겠죠.

바로 이렇게 구현할 수 있겠네요!!

class PersonBuilder {
    private String name;
    private int age;
    private String phone;

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public void phone(String phone) {
        this.phone = phone;
    }

    public Person build() {
        return new Person(name, age, phone);
    }
}

 

 

좋습니다!

그럼 이제 실행용 클래스 Demo에서 PersonBuilder를 통해 Person을 한번 만들어볼까요?

class Demo {
    public static void main(String[] args) {
        PersonBuilder personBuilder = new PersonBuilder();
        personBuilder.setName("songa");
        personBuilder.setAge(20);
        personBuilder.setPhone("010-1234-5678");
        
        Person songa = personBuilder.build();
    }
}

 

만약 핸드폰을 넣지 않는다고 해도?

맞아요. 아주 잘 돌아갑니다.

class Demo {
    public static void main(String[] args) {
        PersonBuilder personBuilder = new PersonBuilder();
        personBuilder.setName("songa");
        personBuilder.setAge(20);

        Person songa = personBuilder.build();
    }
}

 

 

사실 빌더 패턴은 이게 끝입니다!

여기까지 코드가 이해되셨다면, 여러분들은 빌더를 아주 제대로 이해하신 거예요!

 


Chapter 3. 여기까지 오셨다니.. 빌더 고도화💖

⚠️ 다시 한번 말씀드려요. 지금까지 코드가 이해되셨다면 빌더 패턴은 이해하신 겁니다!
지금부터는 코드를 예쁘게 만드는 것 뿐이에요.

 

 

그치만 아마 여러분들이 조금 아쉬운 부분이 있을 거예요.

 

아니.. Person 객체 하나 만들자고..
PersonBuilder를 이렇게 길게 구현하면;;;;

 

 

 

맞죠?ㅋㅋㅋㅋㅋ 자 그래서 우리는 이 코드를 좀 더 예쁘게 만들어봅니다.

 

 

3-1. PeronBuilder가 주인공도 아니고..

Person을 만드는데.. PersonBuilder 객체를 저렇게 별도로 떡하니 만들어야 한다는 게 마음에 안 듭니다.

사실 Person을 위해서 만드는 거 뿐이잖아요?

 

그래서 Person이 PersonBuilder를 호출하는 형태로 만들어볼게요.

Person에 빌더를 부르는 메소드 builder를 만들고, 그 안에서 PersonBuilder 객체를 생성해서 반환하도록 할게요.

public class Person {
    private String name;
    private int age;
    private String phone;

    public Person(String name, int age, String phone) {
        this.name = name;
        this.age = age;
        this.phone = phone;
    }

    public PersonBuilder builder() {
        return new PersonBuilder();
    }
}

 

근데 이렇게만 만들잖아요?

Demo에서는 그럼 이렇게 불러야한다는 겁니다..

class Demo {
    public static void main(String[] args) {
        PersonBuilder personBuilder = new Person("songa", 20, "010-1234-5678").builder();
        personBuilder.setName("songa");
        personBuilder.setAge(20);

 

이게 뭐옄ㅋㅋㅋㅋㅋㅋ 아니 Person 객체 대신 Person 객체 만들어달랬더니.. 코드가 아주 엉맹징챙입니다.

 

그래서 우린 static을 사용해봅니다.

Person 클레스의 builder() 메소드에 static을 선언하면

Person 객체를 생성하지 않고도 우린 builder() 메소드를 호출할 수 있으니까요!

public class Person {
    private String name;
    private int age;
    private String phone;

    public Person(String name, int age, String phone) {
        this.name = name;
        this.age = age;
        this.phone = phone;
    }

    public static PersonBuilder builder() {
        return new PersonBuilder();
    }
}

 

class Demo {
    public static void main(String[] args) {
        PersonBuilder personBuilder = Person.builder();
        personBuilder.setName("songa");
        personBuilder.setAge(20);

        Person songa = personBuilder.build();

 

휴.. 이제 마음이 좀 편하네요

 

 

3-2. 아직도 빌더가 주인공이야.. 🤴🏻

 

자 그래도 조금 모자랍니다. 더 표현하고 싶어요..

PersonBuilder가 Person 때문에 나온거다.. Person 때문에 나온거다 왜 말을 못해..

 

왜 말을 못훼!!!! @파리의 연인 (출처 : 메일리)

 

 

그래서 이런 생각을 한번 해봅니다.

personBuilder가 필요한 만큼 set을 다 하면, Person을 딱! 생성해서 돌려주는 거죠.

 

그림으로 보면, 아래와 같이 그릴 수 있겠네요.

 

 

 

그럼 이걸 어떻게 코드로 표현할까.. 싶죠?

그니까.. 김송아말은.. 이 빨간 줄이 한번에 딱! 되었으면 좋겠다 이런 말인 것 같은데.. 그쵸?

 

 

이 때 활용할 수 있는 방법이 바로 체이닝입니다🔥🔥🔥🔥

 

💡체이닝이란?
메소드가 객체를 반환하게 함으로써, 메소드를 체인처럼 쭉쭉 이어서 호출할 수 있게 하는 방법을 말합니다.

ps. 필요하신 분들은 댓글 남겨주시면 다음 아티클로 좀 더 상세하게 설명해드릴게요!

 

 

자 이제 이 Setter가 일을 좀 더 해볼게요.

public void setName(String name) {
    this.name = name;
}

 

혼자 필드에 값을 대입하고 끝내는 것이 아니라,

1️⃣ 필드에 값을 대입한 다음

2️⃣ 다른 필드도 값을 대입할 수 있게 다시 객체를 반환해주는 겁니다.

 

이제 set 이상의 일을 하니까, 이름도 그냥 name으로 바꿔놓도록 하겠습니다 (사실 빌드업)

다음과 같이 자기 자신 객체를 반환해줌으로써, 메소드 뒤에 메소드가 또 붙을 수 있게 해주는 거죠.

public PersonBuilder name(String name) {
    this.name = name;// 필드값을 set 하고 끝! 하는 것이 아니라,
    return this;     // 객체 자신을 return함으로써 메소드를 체이닝(Chaining) 시켜줍니다.
}

 

 

그럼 이제, 이 체이닝 기법을 활용해서! PersonBuilder를 전체적으로 바꿔보면?

class PersonBuilder {
    private String name;
    private int age;
    private String phone;

    public PersonBuilder name(String name) {
        this.name = name;// 필드값을 set 하고 끝! 하는 것이 아니라,
        return this;     // 객체 자신을 return함으로써 메소드를 체이닝(Chaining) 시켜줍니다.
    }

    public PersonBuilder age(int age) {
        this.age = age;
        return this;
    }

    public PersonBuilder phone(String phone) {
        this.phone = phone;
        return this;
    }

    public Person build() {
        return new Person(name, age, phone);
    }
}

 

 

이렇게 바꿀 수 있습니다🎉🎉🎉

그럼 실행용 클래스 Demo에서 어떻게 호출할 수 있는지 알아요..?

 

바로.. 이렇게 멋지게 바뀝니다..🫢

class Demo {
    public static void main(String[] args) {
        Person songa = Person.builder()
                .name("songa")
                .age(20)
                .phone("010-1234-5678")
                .build();
    }
}

 

 

맞아요.

구글에 빌더 패턴 이라고 치면 나오는 코드랑 완전 똑같이 생겼죠?🕵🏻‍♂️

 

이제 우리는 모든 경우에 수를 고려한 생성자 없이도

원하는 값만 넣은 객체를 생성할 수 있게 되었습니다 야호🥳

 


 

 

너무 대단한 걸요 여러분?

이 글을 위에서부터 차근차근 따라오며 읽은 분들이라면, 빌더가 어떻게 생겼는지 이젠 자신있게! 구현까지 할 수 있겠네요.

 

빌더 패턴 너란 녀석.. 다들 아마 이렇게 날 것의 빌더 패턴을 보기는 처음이실 것 같아요!

그런 만큼, 다시 위로 올라서 꼭꼭 자기만의 코드로 다시 한번 확인해보시길 바라며!!

 

너무너무 고생하셨으니, 이번주는 더더 잘 보내셔야 합니다🙈

 

 

그럼 또 아티클 주제 제보 기다릴게요? 😉

여러분들에게 조금이나마 제 글이 도움이 되길 바라며,

 

 

안녕~~~~~~~

 

 

이 글은 제 블로그에 장미를 두고 가신 분과 패스트캠퍼스 백엔드 부트캠프의 멋진 수강생분들의 요청으로 제작된 글입니다💓