

App
public class App {
public static void main(String[] args) {
TourPlan shortTrip = new TourPlan();
shortTrip.setTitle("오레곤 롱비치 여행");
shortTrip.setStartDate(LocalDate.of(2021, 7, 15));
TourPlan tourPlan = new TourPlan();
tourPlan.setTitle("칸쿤 여행");
tourPlan.setNights(2);
tourPlan.setDays(3);
tourPlan.setStartDate(LocalDate.of(2020, 12, 9));
tourPlan.setWhereToStay("리조트");
tourPlan.addPlan(0, "체크인 이후 짐풀기");
tourPlan.addPlan(0, "저녁 식사");
tourPlan.addPlan(1, "조식 부페에서 식사");
tourPlan.addPlan(1, "해변가 산책");
tourPlan.addPlan(1, "점심은 수영장 근처 음식점에서 먹기");
tourPlan.addPlan(1, "리조트 수영장에서 놀기");
tourPlan.addPlan(1, "저녁은 BBQ 식당에서 스테이크");
tourPlan.addPlan(2, "조식 부페에서 식사");
tourPlan.addPlan(2, "체크아웃");
}
}
TourPlan
public class TourPlan {
private String title;
private int nights;
private int days;
private LocalDate startDate;
private String whereToStay;
private List<DetailPlan> plans;
public TourPlan() {
}
public TourPlan(String title, int nights, int days, LocalDate startDate, String whereToStay, List<DetailPlan> plans) {
this.title = title;
this.nights = nights;
this.days = days;
this.startDate = startDate;
this.whereToStay = whereToStay;
this.plans = plans;
}
@Override
public String toString() {
return "TourPlan{" +
"title='" + title + '\'' +
", nights=" + nights +
", days=" + days +
", startDate=" + startDate +
", whereToStay='" + whereToStay + '\'' +
", plans=" + plans +
'}';
}
+) getter, setter 추가
DetailPlan
public class DetailPlan {
private int day;
private String plan;
public DetailPlan(int day, String plan) {
this.day = day;
this.plan = plan;
}
public int getDay() {
return day;
}
public void setDay(int day) {
this.day = day;
}
public String getPlan() {
return plan;
}
public void setPlan(String plan) {
this.plan = plan;
}
@Override
public String toString() {
return "DetailPlan{" +
"day=" + day +
", plan='" + plan + '\'' +
'}';
}
}
Before: 기존 설계의 문제점
1. 복잡한 객체 생성:
2.가독성 저하:
3. 유지보수 어려움:
4. 객체 생성의 일관성 부족:

설계 변경
public interface TourPlanBuilder {
TourPlanBuilder nightsAndDays(int nights, int days);
TourPlanBuilder title(String title);
TourPlanBuilder startDate(LocalDate localDate);
TourPlanBuilder whereToStay(String whereToStay);
TourPlanBuilder addPlan(int day, String plan);
TourPlan getPlan();
}
("체이닝 방식"은 메서드 호출을 연결하여 코드의 간결성과 가독성을 높이는 빌더 패턴의 중요한 특징이다. 각 메서드가 this를 반환함으로써 다음 메서드를 이어서 호출할 수 있게 해준다.)
public class DefaultTourBuilder implements TourPlanBuilder {
private String title;
private int nights;
private int days;
private LocalDate startDate;
private String whereToStay;
private List<DetailPlan> plans;
@Override
public TourPlanBuilder nightsAndDays(int nights, int days) {
this.nights = nights;
this.days = days;
return this;
}
@Override
public TourPlanBuilder title(String title) {
this.title = title;
return this;
}
@Override
public TourPlanBuilder startDate(LocalDate startDate) {
this.startDate = startDate;
return this;
}
@Override
public TourPlanBuilder whereToStay(String whereToStay) {
this.whereToStay = whereToStay;
return this;
}
@Override
public TourPlanBuilder addPlan(int day, String plan) {
if (this.plans == null) {
this.plans = new ArrayList<>();
}
this.plans.add(new DetailPlan(day, plan));
return this;
}
@Override
public TourPlan getPlan() {
return new TourPlan(title, nights, days, startDate, whereToStay, plans);
}
}
public class TourDirector {
private TourPlanBuilder tourPlanBuilder;
public TourDirector(TourPlanBuilder tourPlanBuilder) {
this.tourPlanBuilder = tourPlanBuilder;
}
public TourPlan cancunTrip() {
return tourPlanBuilder.title("칸쿤 여행")
.nightsAndDays(2, 3)
.startDate(LocalDate.of(2020, 12, 9))
.whereToStay("리조트")
.addPlan(0, "체크인하고 짐 풀기")
.addPlan(0, "저녁 식사")
.getPlan();
}
public TourPlan longBeachTrip() {
return tourPlanBuilder.title("롱비치")
.startDate(LocalDate.of(2021, 7, 15))
.getPlan();
}
}
public class App {
public static void main(String[] args) {
TourDirector director = new TourDirector(new DefaultTourBuilder());
TourPlan tourPlan = director.cancunTrip();
TourPlan tourPlan1 = director.longBeachTrip();
}
}
1) 만들기 복잡한 객체를 순차적으로 만들 수 있다.
2) 복잡한 객체를 만드는 구체적인 과정을 숨길 수 있다.
3) 동일한 프로세스를 통해 각기 다르게 구성된 객체를 만들 수도 있다.
4) 불완전한 객체를 사용하지 못하도록 방지할 수 있다.
1) 원하는 객체를 만들려면 빌더부터 만들어야 한다.
2) 구조가 복잡해 진다. (트레이드 오프)
1.1 자바 8 Stream.Builder API
Stream API에서 빌더 패턴이 활용된다.Stream.Builder를 사용해 스트림을 점진적으로 구성.예제:
Stream<String> stream = Stream.<String>builder()
.add("Java")
.add("Python")
.add("Kotlin")
.build();
stream.forEach(System.out::println);
특징:
add)을 통해 스트림 요소를 추가.build() 호출로 스트림 생성.1.2 StringBuilder
StringBuilder는 문자열을 효율적으로 조합하기 위한 클래스이다.StringBuilder가 빌더 패턴일까?StringBuilder는 문자열 조합에 특화된 클래스로, 단일 책임만 수행.예제:
StringBuilder sb = new StringBuilder();
sb.append("Hello, ")
.append("world!")
.append(" How are you?");
System.out.println(sb.toString());
특징:
1.3 롬복의 @Builder
@Builder는 빌더 패턴 구현을 자동화하는 애노테이션이다.예제:
@Getter
@Builder
public class User {
private String name;
private int age;
private String email;
}
public class Main {
public static void main(String[] args) {
User user = User.builder()
.name("John Doe")
.age(30)
.email("john.doe@example.com")
.build();
System.out.println(user);
}
}
특징:
예제:
String url = UriComponentsBuilder.fromHttpUrl("http://example.com")
.path("/api")
.queryParam("name", "John")
.queryParam("age", 30)
.build()
.toUriString();
System.out.println(url);
// Output: http://example.com/api?name=John&age=30
특징:
MockMvc와 WebClient를 연결하기 위한 빌더.예제:
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new MyController()).build();
WebClient webClient = MockMvcWebClientBuilder.mockMvcSetup(mockMvc).build();
특징:
MockMvc를 사용하여 HTTP 요청을 시뮬레이션.MockMvcWebClientBuilder를 통해 MockMvc와 WebClient를 연결.RestTemplate의 초기화 및 커스터마이징.
Before 코드에서는 프로토타입 패턴이 적용되지 않았다.
객체를 복제하는 기능 없이, 수동으로 각 객체를 생성하고 초기화해야 한다.
GithubRepositoryGithubRepository는 GitHub의 저장소(repository).user: 저장소의 소유자.name: 저장소 이름.GithubIssue 객체에서 참조된다.public class GithubRepository {
private String user;
private String name;
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
GithubIssueGithubIssue는 특정 저장소에 등록된 GitHub 이슈(issue)를 나타낸다.
주요 속성:
id: 이슈의 고유 ID.title: 이슈 제목.repository: 해당 이슈가 속한 GithubRepository.이 클래스는 getUrl() 메서드를 통해 이슈의 GitHub URL을 동적으로 생성한다.
public class GithubIssue {
private int id;
private String title;
private GithubRepository repository;
public GithubIssue(GithubRepository repository) {
this.repository = repository;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public GithubRepository getRepository() {
return repository;
}
public String getUrl() {
return String.format("https://github.com/%s/%s/issues/%d",
repository.getUser(),
repository.getName(),
this.getId());
}
}
App 클래스는 실행을 위한 클라이언트 코드이다.GithubRepository와 GithubIssue 객체를 생성하고 초기화한다.public class App {
public static void main(String[] args) {
GithubRepository repository = new GithubRepository();
repository.setUser("whiteship");
repository.setName("live-study");
GithubIssue githubIssue = new GithubIssue(repository);
githubIssue.setId(1);
githubIssue.setTitle("1주차 과제: JVM은 무엇이며 자바 코드는 어떻게 실행하는 것인가.");
String url = githubIssue.getUrl();
System.out.println(url);
}
}
복제 기능 없음:
예를 들어, 동일한 GithubIssue를 복제하려면 다음과 같은 방식으로 모든 속성을 수동으로 복사해야 한다:
GithubIssue clonedIssue = new GithubIssue(repository);
clonedIssue.setId(githubIssue.getId());
clonedIssue.setTitle(githubIssue.getTitle());
Deep Copy 구현 없음:
GithubRepository가 다른 객체에서 공유될 경우, 원본 객체와 복제 객체가 동일한 참조를 가진다.예:
repository.setUser("newUser"); // 원본 변경
System.out.println(githubIssue.getUrl()); // 복제 객체도 영향을 받음
객체 생성 복잡성:
객체 비교 어려움:

1.
Cloneable인터페이스 구현
GithubIssue
public class GithubIssue implements Cloneable {
private int id;
private String title;
private GithubRepository repository;
public GithubIssue(GithubRepository repository) {
this.repository = repository;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public GithubRepository getRepository() {
return repository;
}
public String getUrl() {
return String.format("https://github.com/%s/%s/issues/%d",
repository.getUser(),
repository.getName(),
this.getId());
}
@Override
protected Object clone() throws CloneNotSupportedException {
GithubRepository repository = new GithubRepository();
repository.setUser(this.repository.getUser());
repository.setName(this.repository.getName());
GithubIssue githubIssue = new GithubIssue(repository);
githubIssue.setId(this.id);
githubIssue.setTitle(this.title);
return githubIssue;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GithubIssue that = (GithubIssue) o;
return id == that.id && Objects.equals(title, that.title) && Objects.equals(repository, that.repository);
}
@Override
public int hashCode() {
return Objects.hash(id, title, repository);
}
}
코드 분석
1. GithubRepository 복제:
2. 새로운 GithubIssue 객체 생성:
2. Deep Copy 구현:
GithubIssue 클래스는 Cloneable 인터페이스를 구현하고, clone() 메서드를 오버라이드하여 객체를 복제한다.public class GithubIssue implements Cloneable {
@Override
protected Object clone() throws CloneNotSupportedException {
GithubRepository repository = new GithubRepository();
repository.setUser(this.repository.getUser());
repository.setName(this.repository.getName());
GithubIssue githubIssue = new GithubIssue(repository);
githubIssue.setId(this.id);
githubIssue.setTitle(this.title);
return githubIssue;
}
}
3.
equals()및hashCode()구현
equals()와 hashCode()를 재정의.@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
GithubIssue that = (GithubIssue) o;
return id == that.id && Objects.equals(title, that.title) && Objects.equals(repository, that.repository);
}
@Override
public int hashCode() {
return Objects.hash(id, title, repository);
}
App
public class App {
public static void main(String[] args) throws CloneNotSupportedException {
GithubRepository repository = new GithubRepository();
repository.setUser("whiteship");
repository.setName("live-study");
GithubIssue githubIssue = new GithubIssue(repository);
githubIssue.setId(1);
githubIssue.setTitle("1주차 과제: JVM은 무엇이며 자바 코드는 어떻게 실행하는 것인가.");
String url = githubIssue.getUrl();
System.out.println(url);
GithubIssue clone = (GithubIssue) githubIssue.clone();
System.out.println(clone.getUrl());
repository.setUser("Keesun");
System.out.println(clone != githubIssue); //true
System.out.println(clone.equals(githubIssue)); //true
System.out.println(clone.getClass() == githubIssue.getClass()); //true
System.out.println(clone.getRepository() == githubIssue.getRepository()); //true (deep copy)
System.out.println(clone.getUrl());
}
}
GithubIssue 객체를 생성하고 각 필드를 수동으로 설정.GithubIssue githubIssue = new GithubIssue(repository);
githubIssue.setId(1);
githubIssue.setTitle("1주차 과제: JVM은 무엇이며 자바 코드는 어떻게 실행하는 것인가.");
clone() 메서드를 호출하여 간단히 복제 가능.GithubIssue clone = (GithubIssue) githubIssue.clone();
객체 복제
clone() 메서드를 통해 기존 GithubIssue 객체를 복제.독립적인 변경
clone)는 원본 객체(githubIssue)와 독립적인 속성을 가짐.repository.setUser("Keesun")로 변경해도 복제된 객체는 영향을 받지 않음.객체 비교
clone != githubIssue: 복제된 객체는 원본 객체와 다른 메모리 주소를 가짐.clone.equals(githubIssue): 객체 내용(ID, 제목, 리포지토리 등)은 동일하므로 equals()는 true.https://github.com/whiteship/live-study/issues/1
https://github.com/whiteship/live-study/issues/1
true // clone != githubIssue (다른 객체)
true // clone.equals(githubIssue) (내용 동일)
true // clone.getClass() == githubIssue.getClass() (동일 클래스)
true // clone.getRepository() == githubIssue.getRepository() (Deep Copy로 구현)
https://github.com/Keesun/live-study/issues/1
장점
복잡한 객체 생성 간소화:
객체 생성 비용 절감:
유연성:
객체 일관성 유지:
단점
구현 복잡성:
Cloneable 인터페이스의 한계:
Cloneable 인터페이스는 복제 로직에 대한 명확한 지침을 제공하지 않음.clone() 메서드를 직접 오버라이드해야 함.상속 관계의 어려움:
clone() 메서드 구현이 어려울 수 있음.프로토타입 패턴은 복잡한 객체를 효율적으로 복제하여 생성하는 데 유용한 패턴으로, 실무에서도 다양한 방식으로 활용된다. 이 패턴은 자바의 clone() 메서드, Cloneable 인터페이스, 그리고 객체 매핑 라이브러리 등을 통해 구현된다.
clone() 메서드와 Cloneable 인터페이스Cloneable 인터페이스Cloneable은 객체를 복제하기 위한 마커 인터페이스이다.Cloneable을 구현하지 않으면, clone() 메서드 호출 시 CloneNotSupportedException이 발생한다.clone() 메서드Object 클래스에서 기본적으로 제공되며, 객체를 복제하는 역할을 한다.clone() 메서드는 기본적으로 얕은 복사(Shallow Copy)를 수행한다.예제:
public class Person implements Cloneable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 얕은 복사
}
@Override
public String toString() {
return "Person{name='" + name + "', age=" + age + "}";
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Person original = new Person("John", 30);
Person cloned = (Person) original.clone();
System.out.println(original); // Person{name='John', age=30}
System.out.println(cloned); // Person{name='John', age=30}
System.out.println(original == cloned); // false (다른 객체)
}
}
clone() 메서드는 얕은 복사를 수행한다.문제점:
예제:
class Address {
String city;
Address(String city) {
this.city = city;
}
}
class Employee implements Cloneable {
String name;
Address address;
Employee(String name, Address address) {
this.name = name;
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone(); // 얕은 복사
}
}
public class Main {
public static void main(String[] args) throws CloneNotSupportedException {
Address address = new Address("New York");
Employee original = new Employee("Alice", address);
Employee cloned = (Employee) original.clone();
cloned.address.city = "San Francisco";
System.out.println(original.address.city); // San Francisco (원본도 영향 받음)
}
}
예제:
class Employee implements Cloneable {
String name;
Address address;
Employee(String name, Address address) {
this.name = name;
this.address = address;
}
@Override
protected Object clone() throws CloneNotSupportedException {
// Deep Copy
Employee cloned = (Employee) super.clone();
cloned.address = new Address(this.address.city);
return cloned;
}
}
예제:
import org.modelmapper.ModelMapper;
class Person {
private String name;
private int age;
// Getter, Setter, Constructor
}
public class Main {
public static void main(String[] args) {
ModelMapper modelMapper = new ModelMapper();
// 원본 객체
Person original = new Person("Alice", 25);
// 복제된 객체
Person cloned = modelMapper.map(original, Person.class);
System.out.println(original); // Person{name='Alice', age=25}
System.out.println(cloned); // Person{name='Alice', age=25}
System.out.println(original == cloned); // false (다른 객체)
}
}