[Design Pattern] 빌더 패턴 (Builder Pattern)

반달곰수·2024년 5월 21일
0

빌더 패턴 ?


빌더 패턴은 복잡한 객체를 단계별로 생성할 수 있게 해주는 디자인 패턴으로 , 객체의 생성 과정과 표현 방법을 분리하여 , 동일한 생성과정에서 서로 다른 표현을 만들 수 있게 해준다.

빌더 클래스의 메서드를 체이닝 형태로 호출함으로써 자연스럽게 인스턴스를 구성하고 마지막에 build() 메서드를 통해 최종적으로 객체를 생성하도록 되어 있다

public void builder() {
        Iphone iphone = new IphoneBuilder()
                .name("Iphone 15 pro")
                .phoneNumber("010-xxxx-yyyy")
                .build();
    }

빌더 클래스 구현


Iphone Class

public static class Iphone {
        private String phoneNumber;
        private String name;

        public Iphone(String phoneNumber, String name) {
            this.phoneNumber = phoneNumber;
            this.name = name;
        }
    }

1 . 만들고자 하는 클래스의 필드와 똑같은 필드를 갖는 Builder 클래스를 만든다

public static class IphoneBuilder {
        private String phoneNumber;
        private String name;
}

2. 각 멤버에 대한 setter 메서드를 구현 (이때 메서드 이름은 필드 이름으로 , 반환값은 this)

public static class IphoneBuilder {
        private String phoneNumber;
        private String name;

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

        public IphoneBuilder name(String name) {
            this.name = name;
            return this;
        }
 }
  • this 를 반환 함으로써 메서드 호출 후 연속적으로 빌더 메서드들을 체이닝 하여 호출할 수 있게 해준다

3. 만들고자 하는 클래스를 만들어주는 build 메서드를 구성해준다

public static class IphoneBuilder {
        private String phoneNumber;
        private String name;

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

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

        public Iphone build() {
            return new Iphone(phoneNumber, name);
        }
    }

빌더 클래스 실행

public void builder() {
        Iphone iphone = new IphoneBuilder()
                .name("Iphone 15 pro")
                .phoneNumber("010-xxxx-yyyy")
                .build();
    }

빌더 패턴 장점


1. 객체 생성 과정을 일관된 프로세스로 표현

생성자 방식으로 객체를 생성하면 매개변수가 많아질수록 가독성이 급격하게 떨어진다

빌더 패턴을 적용하면 직관적으로 어떤 데이터에 어떤 값이 설정 되는지 한눈에 파악할 수 있게 된다

@Transactional
    public void uploadDiary(Long userId, Long exhibitId, DiaryUploadRequest request) {
        Exhibit exhibit = exhibitService.getExhibitByIdAndUserId(exhibitId, userId);
        User user = userService.getUserById(userId);

        Diary diary = Diary.builder()
                .title(request.getTitle())
                .content(request.getContent())
                .exhibit(exhibit)
                .imageUrl(s3Service.uploadImage(request.getImage()))
                .user(user)
                .build();

        diaryRepository.save(diary);

        exhibit.addDiaries(diary);
        exhibitRepository.save(exhibit);
    }

2. 디폴트 매개변수 생략을 간접적으로 지원

기본적으로 자바에서는 메서드에 대한 디폴트 매개변수를 지원하지 않는다

따라서 디폴트 매개변수를 구현하려면 클래스 필드 변수에 초기값을 미리 세팅하고 , 초기값이 세팅된 인자를 제외시키고 생성자를 따로 구현하는 식으로 설계해야 한다

하지만 빌더 패턴에서는 객체 생성 전용 클래스를 이용해서 , 디폴트 매개변수가 설정된 필드를 설정하는 메서드를 호출하지 않는 방식으로 디퐅트 매개변수를 생략하고 호출하는 효과를 간접적으로 구현할 수 있게 된다

public static class IphoneBuilder {
        private String phoneNumber;
        private String name = "Iphone";
        ...
}
public void builder() {
        Iphone iphone = new IphoneBuilder()
                .phoneNumber("010-xxxx-yyyy")
                .build();
    }

3 . 필수 멤버와 선택적 멤버를 분리 가능

목적에 따라 초기화가 필수인 멤버 변수 , 선택적인 멤버 변수가 있을 수 있다

만약 Iphon 클래스의 name 필드가 인스턴스화 할때 필수적이라면 기존 생성자 방식은 전체 멤버를 인자로 받는 생성자를 선언하고 ,매개 변수에 null을 받는 식으로 구성하여야 한다.

하지만 빌더 패턴을 사용하면 초기화가 필수인 멤버는 빌더의 생성자로 받게하여 필수 멤버를 설정 해주어야 빌터 객체가 생성되도록 유도하고 , 선택적인 멤버는 빌더의 메서드로 받도록 하면 사용자로 하여금 필수 멤버와 선택 멤버를 구분하여 객체 생성을 유도할 수 있다

public static class IphoneBuilder {
        private String phoneNumber;
        private String name ;

        public IphoneBuilder(String name) {
            this.name = name;
        }

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

        public Iphone build() {
            return new Iphone(phoneNumber, name);
        }
    }
public void builder() {
        Iphone iphone = new IphoneBuilder("Iphone")
                .phoneNumber("010-xxxx-yyyy")
                .build();
    }

4. 객체 생성 단계를 지연할 수 있음

말 그대로 객체 생성을 단계별로 구성하거나 , 구성 단계를 지연시킬 수 있음

public void builder() {
        IphoneBuilder iphone = new IphoneBuilder("Iphone")
                .phoneNumber("010-xxxx-yyyy");
                
    }

5. 초기화 검증을 멤버별로 분리

생성자로 부터 멤버 값을 받는 형태라면 , 각 생성자 매개변수에 대한 검증 로직을 생성자 메서드 마다 구현해야하고 , 이는 생성자의 크기가 커지는 결과를 만든다

빌더를 이용하면 , 생성될 객체의 멤버 변수의 초기화와 검증을 각각의 멤버별로 분리해서 작성할 수 있다

public static class IphoneBuilder {
        private String phoneNumber;
        private String name;

        public IphoneBuilder() {
        }

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

        public IphoneBuilder name(String name) {
            if (name.equals("galaxy")) {
                throw new RuntimeException();
            }
            this.name = name;
            return this;
        }

        public Iphone build() {
            return new Iphone(phoneNumber, name);
        }
    }

빌더 패턴 단점


1. 코드 복잡성 증가

빌더 패턴을 적용하려면 N개 클래스에 대해 N개의 새로운 빌더 클래스를 만들어야한다. 따라서 클래스수가 늘어나서 관리해야할 클래스가 많아지고 구조가 복잡해질 수 있다

2. 생성자 보다 성능이 떨어짐

매번 메서드를 호출하여 빌더를 거쳐 인스턴스화 하기 때문에 생성자 보다는 성능이 떨어진다. 물론 생성 비용자체가 크지는 않다

확장 가능성을 고려하면 빌더 패턴으로 설계하고 시작하는 편이 나을 때가 많다고 한다. !

Lombok의 @Builder

개발자가 좀 더 편하게 빌더 패턴을 이용하기 위해 Lombok에서는 별도의 어노테이션을 지원한다.

클래스에 @Builder 어노테이션만 붙여주면 클래스를 컴파일 할 때 자동으로 빌더를 이너 클래스로 만들어준다

@Builder
public class Water {
    private String water;
    private String brand;
}
public void water(){
        Water water = Water.builder()
                .water("water")
                .brand("samdasu")
                .build();
    }

필수 파라미터 빌더 구현

@Builder
public class Water {
    private String water;
    private String brand;

    public static WaterBuilder builder(String water) {
        if (!water.equals("water")) {
            throw new IllegalArgumentException("물이 아님");
        }
        return new WaterBuilder().water(water);
    }
}

builder() 정적 메서드를 구현해서 , 빌더 객체를 생성하기전 필수 파라미터 설정을 유도할 수 있고 , 파라미터 검증 로직도 추가해줄 수 있다

참고


💠 빌더(Builder) 패턴 - 완벽 마스터하기

[생성 패턴] 빌더 패턴(Builder pattern) 이해 및 예제

코드


https://github.com/bandalgomsu/Examples

0개의 댓글