티스토리 뷰

Backend/Java8

#37 Design Pattern - Builder Pattern

RadderNepa 2022. 10. 26. 00:16

● 도입

디자인 패턴의 종류

- 디자인 패턴은 크게 3가지로 나눌 수 있다.

1. 생성 패턴(Creational Patterns) : 오브젝트의 생성에 관련된 패턴

2. 구조 패턴(Structural Patterns) : 상속을 이용해 클래스 / 오브젝트를 조합하여 더 거대한 & 발전된 구조로 만드는 패턴

3. 행동 패턴(Behavioral Patterns) : 필요한 작업을 여러 객체에 분배하여 객체간 결합도를 줄이게 해주는 패턴

 

Builder Pattern?

- 대표적인 생성 패턴으로써 객체의 생성에 대한 로직과 표현에 대한 로직을 분리해준다.
- 객체의 생성 과정을 유연하게 해준다.
- 객체의 생성 과정을 정의하고 싶거나 객체의 필드가 많아 constructor가 복잡해질 때 유용하다.

 

 

● 실습

[기존 User class의 모습]

package com.fastcampus.functionalprogramming.chapter8.model;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

public class User {
	private int id;
	private String name;
	private String emailAddress;
	private boolean isVerified;
	private LocalDateTime createdAt;
	private List<Integer> friendUserIds;
	
	public int getId() {
		return id;
	}
	
	public User setId(int id) {
		this.id = id;
		return this;
	}
	
	public String getName() {
		return name;
	}
	
	public User setName(String name) {
		this.name = name;
		return this;
	}
	
	public Optional<String> getEmailAddress() {
		return Optional.ofNullable(emailAddress);
	}
	
	public User setEmailAddress(String emailAddress) {
		this.emailAddress = emailAddress;
		return this;
	}
	
	public boolean isVerified() {
		return isVerified;
	}
	
	public User setVerified(boolean isVerified) {
		this.isVerified = isVerified;
		return this;
	}
	
	public LocalDateTime getCreatedAt() {
		return createdAt;
	}
	
	public User setCreatedAt(LocalDateTime createdAt) {
		this.createdAt = createdAt;
		return this;
	}
	
	public List<Integer> getFriendUserIds() {
		return friendUserIds;
	}
	
	public User setFriendUserIds(List<Integer> friendUserIds) {
		this.friendUserIds = friendUserIds;
		return this;
	}
}

- 지금까지 User class의 instance를 만들때는 아래와 같이 선언을 한 후 setter를 이용해 각 필드(field)들을 set 했다.

User user = new User();

- setter 함수가 존재하면 instance의 필드값이 변경될 가능성이 항상 남아있기에 immutable 하지 못하다고 할 수 있다.

- 이 문제를 해결하려면 setter 함수 자체를 만들지 않으면 된다.

- 그렇다면 setter 함수가 없다면 instance의 필드값을 어떻게 설정할 수 있을까?

- 추가적으로 필드의 수가 많은 class라면 constructor가 비대해지는 문제도 있다.

- 이러한 문제를 해결해주는 것이 바로 Builder Pattern이다.

 

1.

- Builder Pattern을 만들어보자

- Builder Pattern을 위해서는 User class안에 Builder 라는 'inner class'를 정의해야한다.

package com.fastcampus.functionalprogramming.chapter10.model;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;

public class User {
	private int id;
	private String name;
	private String emailAddress;
	private boolean isVerified;
	private LocalDateTime createdAt;
	private List<Integer> friendUserIds;
	
	// User class constructor
	// 모든 필드의 값을 Builder에서 가져와 객체를 생성하는 것이다.
	public User(Builder builder) {
		this.id = builder.id;
		this.name = builder.name;
		this.emailAddress = builder.emailAddress;
		this.isVerified = builder.isVerified;
		this.createdAt = builder.createdAt;
		this.friendUserIds = builder.friendUserIds;
	}
	
	// Builder class의 instance를 만들 수 있게 해주는 역할
	// Builder class의 constructor는 외부에서 접근할 수 없으므로(private) 해당 method가 필요하다.
	// cf) private는 같은 class 내에서는 접근가능하므로 여기서는 당연히 Builder class constructor에 접근할 수 있다.
	public static Builder builder(int id, String name) {
		// (id, name)은 Builder constructor가 parameter로 가지고 있으므로 반드시 넣어줘야 한다. 
		return new Builder(id, name);
	}
	
	public int getId() {
		return id;
	}
	
	public String getName() {
		return name;
	}
	
	public Optional<String> getEmailAddress() {
		return Optional.ofNullable(emailAddress);
	}
	
	public boolean isVerified() {
		return isVerified;
	}

	public LocalDateTime getCreatedAt() {
		return createdAt;
	}
	
	public List<Integer> getFriendUserIds() {
		return friendUserIds;
	}
	
	// Builder라는 inner class를 정의
	public static class Builder {
		// Builder class도 User class와 동일한 field를 갖는다.
		private int id;
		private String name;
		private String emailAddress;
		private boolean isVerified;
		private LocalDateTime createdAt;
		private List<Integer> friendUserIds;
		
		// Builder class의 instance를 만들어 주는 constructor
		// 생성 시 id와 name을 필수로 받도록 parameter로 넣었다.
		// 결론적으로 User class의 instance를 만들때 id와 name은 필수로 있어야한다는 것
		private Builder(int id, String name) {
			this.id = id;
			this.name = name;
		}
		
		// 필수가 아닌 필드를 정의하는 method 1
		public Builder withEmailAddress(String emailAddress) {
			this.emailAddress = emailAddress;
			return this;
		}
		
		// 필수가 아닌 필드를 정의하는 method 2
		public Builder withVerified(boolean isVerified) {
			this.isVerified = isVerified;
			return this;
		}
		
		// 필수가 아닌 필드를 정의하는 method 3
		public Builder withCreatedAt(LocalDateTime createdAt) {
			this.createdAt = createdAt;
			return this;
		}
		
		// 필수가 아닌 필드를 정의하는 method 4
		public Builder withFriendUserIds(List<Integer> friendUserIds) {
			this.friendUserIds = friendUserIds;
			return this;
		}
		
		// Builder class와 User constructor를 연결해주는 역할
		public User build() {
			// 자기 자신(this)을 념겨 User object를 만드는 것이다.
			// User constructor의 parameter가 (Builder builder)이기에 this를 argument로 넣어줘야한다.
			return new User(this);
		}
	}

	@Override
	public String toString() {
		return "User [id=" + id + ", " + (name != null ? "name=" + name + ", " : "")
				+ (emailAddress != null ? "emailAddress=" + emailAddress + ", " : "") + "isVerified=" + isVerified
				+ ", " + (createdAt != null ? "createdAt=" + createdAt + ", " : "")
				+ (friendUserIds != null ? "friendUserIds=" + friendUserIds : "") + "]";
	}
}

 

- 이제 Builder를 이용해 User class의 instance를 만들어보자

package com.fastcampus.functionalprogramming.chapter10;

import com.fastcampus.functionalprogramming.chapter10.model.User;
import com.fastcampus.functionalprogramming.chapter10.model.User.Builder;

public class Chapter10Section1 {
	public static void main(String[] args) {
		// 첫 줄의 builder method는 static이므로 instance 생성 없이도 호출 가능
		User user = User.builder(1, "Alice")
					.withEmailAddress("Alice@garden.co.kr")
					.withVerified(true)
					.build(); // 최종적으로 Builder class의 build method를 호출해 User instance를 반환한다.
		System.out.println(user);
	}
}

 

- 아래의 순서를 거쳐서 User class의 instance가 만들어지는 것이다.

[1. User.builder(1, "Alice") 부분]
- static method인 builder method를 호출해 Builder constructor에 값을 넣어준후 return한다.
- Builder class에 있는 Builder constructor는 접근제한자가 private이기에 해당 method를 이용해 Builder constructor에 접근해야한다.


public static Builder builder(int id, String name) {
    // (id, name)은 Builder constructor가 parameter로 가지고 있으므로 반드시 넣어줘야 한다. 
    return new Builder(id, name); // Builder를 return 한다.
}
[2. .withEmailAddress("Alice@garden.co.kr").withVerified(true) 부분]
- 1번에서 Builder를 return 받았으므로 Builder instance가 가지고 있는 setter method를 호출해 필드값을 정의한다.


// 필수가 아닌 필드를 정의하는 method 1
public Builder withEmailAddress(String emailAddress) {
    this.emailAddress = emailAddress;
    return this;
}

// 필수가 아닌 필드를 정의하는 method 2
public Builder withVerified(boolean isVerified) {
    this.isVerified = isVerified;
    return this;
}
[3. .build(); 부분]
- 최종적으로 반환 받아야하는 값의 형태는 'User'이므로 Builder class와 User constructor를 연결해주는 역할인
build method를 호출한다.

// Builder class 안에 있는 method
public User build() {
    // 자기 자신(this)을 념겨 User object를 만드는 것이다.
    // User constructor의 parameter가 (Builder builder)이기에 this를 argument로 넣어줘야한다.
    return new User(this);
}


// return new User(this); 로 인해 아래 코드가 실행되는 것이다.
public User(Builder builder) {
	// builder에서 설정된 필드값을 가져와 설정한다.
    this.id = builder.id;
    this.name = builder.name;
    this.emailAddress = builder.emailAddress;
    this.isVerified = builder.isVerified;
    this.createdAt = builder.createdAt;
    this.friendUserIds = builder.friendUserIds;
}

 

2.

- 현재 Builder inner class 안에 필드를 설정하는 method가 너무 많다.

→  withEmailAddress, withVerified, withCreatedAt, withFriendUserIds

[현재 User class 안에 있는 Builder class 상태]

public static class Builder {
    // Builder class도 User class와 동일한 field를 갖는다.
    private int id;
    private String name;
    private String emailAddress;
    private boolean isVerified;
    private LocalDateTime createdAt;
    private List<Integer> friendUserIds;

    // Builder class의 instance를 만들어 주는 constructor
    // 생성 시 id와 name을 필수로 받도록 parameter로 넣었다.
    // 결론적으로 User class의 instance를 만들때 id와 name은 필수로 있어야한다는 것
    private Builder(int id, String name) {
        this.id = id;
        this.name = name;
    }

    // 필수가 아닌 필드를 정의하는 method 1
    public Builder withEmailAddress(String emailAddress) {
        this.emailAddress = emailAddress;
        return this;
    }

    // 필수가 아닌 필드를 정의하는 method 2
    public Builder withVerified(boolean isVerified) {
        this.isVerified = isVerified;
        return this;
    }

    // 필수가 아닌 필드를 정의하는 method 3
    public Builder withCreatedAt(LocalDateTime createdAt) {
        this.createdAt = createdAt;
        return this;
    }

    // 필수가 아닌 필드를 정의하는 method 4
    public Builder withFriendUserIds(List<Integer> friendUserIds) {
        this.friendUserIds = friendUserIds;
        return this;
    }

    // Builder class와 User constructor를 연결해주는 역할
    public User build() {
        // 자기 자신(this)을 념겨 User object를 만드는 것이다.
        // User constructor의 parameter가 (Builder builder)이기에 this를 argument로 넣어줘야한다.
        return new User(this);
    }
}

- 필드의 개수가 많아질수록 이러한 method 들의 숫자도 더 많아질 것이고 그러면 Builder class 또한 비대해질 것이다.

- 그렇다면 함수형 프로그래밍을 이용해 하나의 method로 모든 필드의 값을 설정해보자

- Consumer interface를 사용할 것이다.

package com.fastcampus.functionalprogramming.chapter10.model;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Consumer;

public class User {
	private int id;
	private String name;
	private String emailAddress;
	private boolean isVerified;
	private LocalDateTime createdAt;
	private List<Integer> friendUserIds;
	
	// User class constructor
	// 모든 필드의 값을 Builder에서 가져와 객체를 생성하는 것이다.
	public User(Builder builder) {
		this.id = builder.id;
		this.name = builder.name;
		this.emailAddress = builder.emailAddress;
		this.isVerified = builder.isVerified;
		this.createdAt = builder.createdAt;
		this.friendUserIds = builder.friendUserIds;
	}
	
	// Builder class의 instance를 만들 수 있게 해주는 역할
	// Builder class의 constructor는 외부에서 접근할 수 없으므로(private) 해당 method가 필요하다.
	// cf) private는 같은 class 내에서는 접근가능하므로 여기서는 당연히 Builder class constructor에 접근할 수 있다.
	public static Builder builder(int id, String name) {
		// (id, name)은 Builder constructor가 parameter로 가지고 있으므로 반드시 넣어줘야 한다. 
		return new Builder(id, name);
	}
	
	public int getId() {
		return id;
	}
	
	public String getName() {
		return name;
	}
	
	public Optional<String> getEmailAddress() {
		return Optional.ofNullable(emailAddress);
	}
	
	public boolean isVerified() {
		return isVerified;
	}

	public LocalDateTime getCreatedAt() {
		return createdAt;
	}
	
	public List<Integer> getFriendUserIds() {
		return friendUserIds;
	}
	
	// Builder라는 inner class를 정의
	public static class Builder {
		
		// 밖에서 접근이 가능해야 하므로 접근제한자를 public으로 변경 by withAllField method
		public int id;
		public String name;
		public String emailAddress;
		public boolean isVerified;
		public LocalDateTime createdAt;
		public List<Integer> friendUserIds;
		
		// Builder class의 instance를 만들어 주는 constructor
		// 생성 시 id와 name을 필수로 받도록 parameter로 넣었다.
		// 결론적으로 User class의 instance를 만들때 id와 name은 필수로 있어야한다는 것
		private Builder(int id, String name) {
			this.id = id;
			this.name = name;
		}
		
		// 단 하나의 setter method(모든 필드의 값을 세팅할 수 있다.)
		public Builder withAllField(Consumer<Builder> consumer) {
			// consumer 안에서 Builder class의 모든 필드들을 set 할 것이다.
			consumer.accept(this);
			return this;
		}
		
		// Builder class와 User constructor를 연결해주는 역할
		public User build() {
			// 자기 자신(this)을 념겨 User object를 만드는 것이다.
			// User constructor의 parameter가 (Builder builder)이기에 this를 argument로 넣어줘야한다.
			return new User(this);
		}
	}

	@Override
	public String toString() {
		return "User [id=" + id + ", " + (name != null ? "name=" + name + ", " : "")
				+ (emailAddress != null ? "emailAddress=" + emailAddress + ", " : "") + "isVerified=" + isVerified
				+ ", " + (createdAt != null ? "createdAt=" + createdAt + ", " : "")
				+ (friendUserIds != null ? "friendUserIds=" + friendUserIds : "") + "]";
	}
}

package com.fastcampus.functionalprogramming.chapter10;

import com.fastcampus.functionalprogramming.chapter10.model.User;
import com.fastcampus.functionalprogramming.chapter10.model.User.Builder;

public class Chapter10Section1 {
	public static void main(String[] args) {
		User user2 = User.builder(2, "Bob")
					.withAllField(builder -> {
						builder.emailAddress = "Bob@garden.co.kr";
						builder.isVerified = true;
					})
					.build();
		System.out.println(user2);
	}
}

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
글 보관함