본문 바로가기
Android/Java

[Java] 자바 빌더 패턴: 복잡한 객체 생성의 단순화와 한계점 탐구

by quessr 2024. 4. 9.

 

회사에서 담당하게 된 부분의 코드 분석 중, 자바에서 많이 사용되는 디자인패턴 중 하나인 "빌더 패턴"에 대해 알게 되었다.
이에 빌더패턴의 사용 목적, 방법 그리고 이점에 대해 알아본 내용들을 정리해 두고자 한다.

 

자바에서 빌더 패턴(Builder Pattern) 주로 복잡한 객체의 생성 과정을 단순화하기 위해 사용된다.

패턴은 객체의 생성과 표현을 분리함으로써, 동일한 생성 과정을 통해 서로 다른 표현 결과를 얻을 있게 해 준다.

빌더 패턴은 특히 많은 수의 매개변수를 가진 객체를 생성할 때, 그리고 이 매개변수 일부는 필수적이고 일부는 선택적일  유용하다.

 

빌더 패턴사용의 목적과 결과

  • 목적: 복합 객체의 생성 과정을 단계별로 진행하여 최종적으로 객체를 안정적으로 생성한다.
  • 결과: 클라이언트 코드의 복잡성을 방지하고, 객체 생성 관련 코드의 가독성과 유지보수성을 향상시킨다.

빌더 패턴 사용 전후 비교

 

• 빌더 패턴을 사용하지 않은 경우

 

생성자를 통해 객체를 생성할 , 매개변수가 많아지면 매개변수가 무엇을 의미하는지 파악하기 어려울 있다.

예를 들어, Person 클래스가 있고, 이를 생성하는 생성자가 다음과 같이 정의되어 있다고 가정해 보자.

public class Person {
    private String firstName;
    private String lastName;
    private int age;
    private String phoneNumber;
    private String address;

    public Person(String firstName, String lastName, int age, String phoneNumber, String address) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.age = age;
        this.phoneNumber = phoneNumber;
        this.address = address;
    }
}

 

생성자를 사용하여 객체를 생성하면 다음과 같다.

Person person = new Person("John", "Doe", 30, "012-345-6789", "123 Main St");

 

코드만 보고 매개변수가 무엇을 의미하는지 즉시 이해하기는 어렵다. 특히,  매개변수가 많아질수록 문제는 심각해진다.

 

 빌더 패턴을 사용한 경우

이제 빌더 패턴을 적용해 같은 Person 클래스의 인스턴스를 생성하는 예를 보자.

public class Person {
    private String firstName;
    private String lastName;
    private int age;
    private String phoneNumber;
    private String address;

    private Person(Builder builder) {
        this.firstName = builder.firstName;
        this.lastName = builder.lastName;
        this.age = builder.age;
        this.phoneNumber = builder.phoneNumber;
        this.address = builder.address;
    }

    public static class Builder {
        private String firstName;
        private String lastName;
        private int age;
        private String phoneNumber;
        private String address;

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

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

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

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

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

        public Person build() {
            return new Person(this);
        }
    }
}

 

빌더 패턴을 사용하여 Person 인스턴스를 생성하면 다음과 같다.

Person person = new Person.Builder()
                    .firstName("John")
                    .lastName("Doe")
                    .age(30)
                    .phoneNumber("012-345-6789")
                    .address("123 Main St")
                    .build();

 

이 코드는 각 매개변수가 무엇을 의미하는지 명확하게 보여준다.

메서드 체이닝을 통해 각 속성을 설정하는 방식은 읽기 쉽고, 어떤 속성이 설정되었는지 한눈에 파악할 수 있게 해 준다.

또한, 필요한 속성만 선택적으로 설정할 수 있어서 유연성도 제공한다.

이러한 이유로, 빌더 패턴은 복잡한 객체의 생성 과정에서 가독성과 사용성 측면에서 이점을 제공한다.

 

하지만 계속 들여다보니 빌더패턴의 한계 역시 보였는데, 새로운 필드를 추가할 때마다 해당 필드를 처리하기 위한 코드를 빌더 클래스도 추가해 줘야 한다는 점이다. 

 

빌더 패턴의 한계

 

 필드의 일부만 설정하고 싶을 빌더 패턴을 사용하지 않은 경우

 

빌드패턴을 사용하지 않았을 때 필드의 일부만 변경을 하고 싶다면 setter 메서드를 사용할 수 있는데
setter 메서드를 사용하는 예는 다음과 같다.

public class Person {
    private String name;
    private int age;
    private String email;
    private String address;
    private String phoneNumber;
    private String job;

    public Person() {}

    // Setter 메서드
    public void setName(String name) {
        this.name = name;
    }

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

    public void setEmail(String email) {
        this.email = email;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    public void setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    public void setJob(String job) {
        this.job = job;
    }

    // Getter 생략...
}

 

사용 예:

Person person = new Person();
person.setName("홍길동");
person.setAge(30);
person.setEmail("hong@example.com");

 

 필드의 일부만 설정하고 싶을  빌더 패턴을 사용한 경우

 

위 글의, 빌더 패턴 사용 전후 비교의 빌더패턴을 사용한 경우 예시를 보자.

 

빌더 패턴을 사용하는 경우와 사용하지 않는 경우를 비교해보면비교해 보면, 양쪽 모두 새로운 필드를 추가할 때마다 해당 필드를 처리하기 위한 코드를 추가해야 한다는 점에서 유사하다. 이는 빌더 패턴을 사용하더라도, 새로운 필드를 클래스나 빌더에 추가할 때마다 관련 코드를 업데이트해야 한다는 점에서 유지보수 비용의 증가라는 공통적인 요소를 가지고 있다.

다시 말해, 빌더 패턴을 사용함으로써 얻을  있는 여러 이점들이 있음에도 불구하고, 새로운 필드 추가와 같은 구체적인 상황에서 유지보수 비용 측면에서 뚜렷한 개선을 제공하지는 않는다.

 

그러나, 이 문제에 대한 해결책으로 Lombok 라이브러리의 @Builder 어노테이션을 활용하는 방법이 있다.

Lombok의 @Builder 어노테이션을 사용하면 컴파일 시점에서 자동으로 빌더 패턴 코드를 생성해 준다.

이는 새로운 필드를 클래스에 추가할 때마다 빌더 클래스에 해당 필드를 처리하는 코드를 직접 작성하지 않아도 되는 것을 의미한다. Lombok이 자동으로 이러한 작업을 처리해 주기 때문에, 개발자는 유지보수의 부담을 크게 줄일 수 있다.

 

이러한 Lombok 라이브러리의 활용 방안에 대해서는 추후에 직접 사용하고 경험한 후에 자세히 정리해보는 것으로 하겠다.

 

결론 및 요약

 

결론적으로, 빌더 패턴은 복잡한 객체의 생성 과정을 단순화하고, 가독성 유지보수성을 향상시키는 역할을 한.

또한, Lombok 라이브러리를 활용하면 빌더 패턴의 한계를 극복하고, 개발자의 부담을 더욱 줄일 있다.

따라서, 복잡한 객체를 안정적으로 생성하고자 빌더 패턴의 활용을 적극적으로 고려해 보는 것이 좋다.

이러한 점을 고려하면, 빌더 패턴은 객체 생성 과정의 가독성과 유지보수성을 극대화하는 있어 매우 유용한 도구임을 있다.