GoF 패턴 - Builder Pattern

김민건·2022년 4월 12일

빌더 패턴

빌더 패턴이란?

빌더 패턴은 GoF 패턴 중 생성 패턴에 속하는 패턴으로, 내부 인스턴스 변수를 결정하는 부분을 별도의 클래스(Builder)로 분리 시켜 유연하게 인스턴스를 생성시킬 수 있도록 해주는 패턴을 의미합니다.

그럼 이 패턴을 적용시킴으로써 어떻게 이런 유연성을 챙기는걸까?

빌더 패턴을 사용하는 이유

빌더 패턴의 사용 이유를 알아보기 위해서는 먼저 기존 방법으로 우리가 인스턴스를 생성했을 때 마주치는 문제들에 대해서 확인해봐야합니다.

기존 방식의 문제점

예시를 하나 들어보자.

public class Student {
	// required
    private String studentId;
    private String name;
    private int grade;
    // optional
    private String phoneNum;
    
    public Student(String studentId, int grade, String name, String phoneNum) {
        this.studentId = studentId;
        this.grade = grade;
        this.name = name;
        this.phoneNum = phoneNum;
    }
    
    public Student(String studentId, int grade, String name) {
        this.studentId = studentId;
        this.grade = grade;
        this.name = name;
    }

    public String getStudentId() {
        return studentId;
    }

    public int getGrade() {
        return grade;
    }

    public String getName() {
        return name;
    }
    
    public String getPhoneNum() {
        return phoneNum;
    }
}
public class Test {
	public static void main(String[] args) {
    	Student student1 = new Student("1234", 1, "김삿갓", "01012345678");
        Student student2 = new Student("1235", 2, "이수근", null);
        Student student3 = new Student("1365", 2, "박대기");
    }
}

위와 같은 클래스가 있는 경우, 우리는 객체를 만들기 위해서는 정의된 생성자를 사용해야한다. 하지만 이와 같은 생성자를 통해 만드는 경우 크게 2가지 문제가 발생한다.

1. 인스턴스 변수를 초기화시켜주는 작업을 실행할 때마다, 해당 클래스의 생성자 파라미터 순서에 맞게 입력해야한다.

위의 예시로 예를 들어보면, studentId, grade, name, phoneNum과 같은 순서를 지켜줘야한다는 것이다. 따라서 매번 해당 클래스의 생성자를 확인해주면서 입력해야하고, 차후 해당 생성자를 보면서 인스턴스의 값을 확인하려 할때도 이 순서에 따라서 분석해야한다.

코드 생산성이 저하되고, 에러가 발생할 확률이 높아진다는 문제가 발생한다.

2. 사용하지 않는 변수가 있는 경우, 일일이 null 값을 넘겨주거나, 별도의 생성자 함수를 만들어줘야 한다.

만약 phoneNum 변수가 optional이라고 가정해보자. 그럼 생성자를 통해 이 값을 정의해주기 위해서는 기존 생성자를 사용하여 new Student("202036312", 3, "kmg", null)로 객체를 생성하거나, 새로운 생성자 함수를 만들어주어야한다.

불필요한 코드가 늘어나고, 번잡해지는 문제가 발생한다.

빌더 패턴 사용 방법

빌더 패턴은 기존 클래스를 생성 클래스(StudentBuilder)와 표현 클래스(Student)로 나누어서 만들어줌으로써, 위의 문제점들을 해결합니다.

public class Student {
    private String studentId;
    private int grade;
    private String name;
    private String phoneNum;
    
    // Student 클래스의 모든 생성자는 외부에서 접근하지 못하도록 private로 정의
    // 모든 데이터를 포함하는 생성자 함수 생성
    private Student(String studentId, int grade, String name, String phoneNum) {
        this.studentId = studentId;
        this.grade = grade;
        this.name = name;
        this.phoneNum = phoneNum;
    }

	// Builder에서 편하게 값을 넣어줄 수 있는 생성자 함수 생성
    private Student(StudentBuilder studentBuilder) {
        this(
            studentBuilder.studentId,
            studentBuilder.grade,
            studentBuilder.name,
            studentBuilder.phoneNum
        );
    }

    public String getStudentId() {
        return studentId;
    }

    public int getGrade() {
        return grade;
    }

    public String getName() {
        return name;
    }

    public String getPhoneNum() {
        return phoneNum;
    }

	// 클래스 내에 정적 클래스 추가
    public static class StudentBuilder {
        // required
        private String studentId;
        private int grade;
        private String name;
        
        // optional
        private String phoneNum;
    
    	// required 변수들은 생성자로 할당하게끔 정의
        public StudentBuilder(String studentId, int grade, String name) {
            this.studentId = studentId;
            this.grade = grade;
            this.name = name;
        }
    
    	// optional 변수들에 대해 각각 setter 함수 선언
        public StudentBuilder setPhoneNum(String phoneNum) {
            this.phoneNum = phoneNum;
            return this;
        }
    
    	// 최종적으로 결정된 변수 데이터로 Student 인스턴스 생성 후 반환
        public Student build() {
            Student student = new Student(this);
            return student;
        }
    }
}
public class Test {
	public static void main(String[] args) {
    	Student student1 = new Student.StudentBuilder("1234", 1, "김삿갓")
        	.setPhoneNum("01012345678")
            .build();
        Student student2 = new Student.StudentBuilder("1235", 2, "이수근")
        	.build();
        Student student3 = new Student.StudentBuilder("1365", 2, "박대기")
        	.build();
    }
}

빌더 패턴을 사용한 경우에는 마찬가지로 required 부분에는 순서를 지켜서 작성해줘야한다는 문제가 남아있다.

하지만 위에서 얘기했던 두 가지 문제점에 대해서 개선된 부분을 확인할 수 있다.

  • 문제점1 개선 - 나머지 optional 부분에 대해서는 명확한 필드명과 함께 데이터를 넣어주기 때문에 가독성이 좋아진다.
  • 문제점2 개선 - optional 변수 중 특정 하나에 대해서 입력하는 것도 나머지 부분에 대해서 null을 넣어주거나, 별도의 생성자를 만들어줄 필요가 없다.
profile
백엔드 꿈나무

0개의 댓글