객체 생성 디자인 패턴 (점층적 생성자 패턴, 정적 팩토리 메서드, 빌더 패턴)
객체 생성에 사용되는 다양한 디자인 패턴에 대해서 알아보자.
RequestDto 를 사용
1. 점층적 생성자를 통한 객체 생성
객체를 생성할 때, 아래와 같이 new 키워드와 생성자를 통해 객체를 생성할 수 있음
자바를 처음 배우는 사람에게 익숙한 이 디자인 패턴은 점층적 생성자 패턴 임
하지만 이 방식은 매개 변수가 늘어나면서 코드 작성이 어렵고 가독성이 떨어져 혼동하기 쉽다는 단점이 있음
public class NutritionFact {
// 필수 인자
private final int servingSize;
private final int servings;
// 선택 인자
private final int calories;
private final int fat;
private final int sodium;
private final int carbohydrate;
// 필수 인자 생성자
public NutritionFact(int servingSize, int servings) {
this(servingSize, servings, 0); // 필수 + 선택 인자1 생성자 호출
}
// 필수 + 선택 인자1 생성자
public NutritionFact(int servingSize, int servings, int calories) {
this(servingSize, servings, calories, 0); // 필수 + 선택 인자2 생성자 호출
}
// 필수 + 선택 인자2 생성자
public NutritionFact(int servingSize, int servings, int calories, int fat) {
this(servingSize, servings, calories, fat, 0); // 필수 + 선택 인자3 생성자 호출
}
// 필수 + 선택 인자3 생성자
public NutritionFact(int servingSize, int servings, int calories, int fat, int sodium) {
this(servingSize, servings, calories, fat, sodium, 0); // 전체 인자 생성자 호출
}
// 전체 인자 생성자
public NutritionFact(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate) {
this.servingSize = servingSize;
this.servings = servings;
this.calories = calories;
this.fat = fat;
this.sodium = sodium;
this.carbohydrate = carbohydrate;
}
}
NutritionFact cocaCola = new NutritionFact(240, 8, 3, 35, 27);
aws 배포 를 위해 프로젝트 파일을 빌드해야함
테스트 @SpringBoot 용
2. 정적 팩토리 메서드
객체의 생성을 담당하는 클래스 메서드로 생성자 호출 방식이 아닌 메서드 호출 방식으로 객체를 생성하는 것
아래 코드 처럼 new 키워드로만 객체를 생성할 수 있는 줄 알았는데 메소드 호출로 어떻게 객체를 생성할 수 있지? 라는 생각이 들수있다.
String str1 = new String("hello")
//사실 new String은 생략하고 String str1 = "hello"라고 해도 되지만 new를 통한 객체 생성의
//이해를 돕기 위해 명시하겠다.
아래 코드의 valueOf() 메소드는 파라미터로 값을 받으면, 그 값을 기반으로 String 객체로 만들어 반환해줌
hello 라는 문자열을 갖고 있는 String 객체를 만들려면 String.valueOf("hello")를 사용하면 됨
이렇듯 valueOf()를 사용해서 객체를 만들 수 있는데 사실 new를 직접적으로 사용하지 않을 뿐, 정적 팩토리 메서드라는 클래스 내에 선언되어있는 메서드를 내부의 new를 이용해 객체를 생성해 반환하는 것!
생성자와 정적 팩토리 메서드 사용해보기
정적 팩토리 메서드 장점
1. 이름을 가질 수 있음
2. 호출 될 때마다 인스턴스를 새로 생성하지 않아도 됨
3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있음
4. 입력 매개 변수에 따라 매번 다른 클래스의 객체를 반환할 수 있음
5. 정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 됨
장점 1 : 정적 팩토리 메소드는 이름을 가질 수 있음
class Person {
private String name;
private Double height;
public Person() {
}
public Person(String name, Double height) {
this.name = name;
this.height = height;
{
public static Person nameAndHeightOf(String name, Double height) {
return new Person(name, height);
}
}
위 예제에서 public 생성자를 이용하여 Person 인스턴스를 생성하려면 new 키워드로 생성자에 매개변수를 넣어 생성한다. 이때 매개변수의 입력값이 어떤 항목을 뜻하는지 알기 어렵다.
이를 해결하기 위해 정적 팩토리 메서드가 고안되었다.
장점 2 : 정적 팩토리 메소드를 쓰면 호출할 때마다 새로운 객체를 생성할 필요가 없음
코드 내부를 보면 원시값 b에 따라 미리 만들어 둔 TRUE, FALSE 중 하나의 값(인스턴스)을 리턴하는 것을 볼 수 있다. 정적 팩토리 메서드를 사용하면 매번 새로운 인스턴스를 반환할 수 있지만 이처럼 미리 만들어둔 인스턴스를 반환할 수 있게되면서 메모리 낭비를 줄일 수도 있다.
즉 객체를 재사용할 수 있음
public static Boolean valueOf(boolean b) {
return b ? Boolean.TRUE : Boolean.FALSE;
}
장점 3 : 반환값 자료형의 하위 자료형 객체를 반환할 수 있음
반환되는 객체의 클래스를 유연하게 결정할 수 있다.
그 예로 java.util.Collections이 있음
public class Collections {
...
public class <T> Collection<T> unmodifiableCollection(Collection<? extends T> c) {
return new UnmodifiableCollection<>(c);
}
...
static class UnmodifiableCollection<E> implements Collection<E>, Serializable {
...
}
}
장점 4 : 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있음
반환 타입은 해당 타입의 하위 클래스이기만 하면 되기 때문에 어떤 클래스의 객체를 반환 받는지 클라이언트는 알 필요가 없고 철저히 인터페이스에 의존한 개발을 할 수 있게 된다.
public interface Person {
static Person of(int y) {
Person instance;
if (y > 50) {
instance = new Old(y);
} else {
instance = new Young(y);
}
return instance;
}
String getGeneration();
class Young implements Person {
private int value;
Young(int y) {
this.value = y;
}
@Override
public String getGeneration() {
return "청년";
}
}
class Old implements Person {
private int value;
Old(int y) {
this.value = y;
}
@Override
public String getGeneration() {
return "중장년";
}
}
}
위 예제에서 Person은 정적 팩토리 메서드에 주어진 나이 인자에 따라 Old 혹은 Young의 인스턴스를 반환받게 된다. 하지만 두 인스턴스 모두 세대 정보를 얻을 수 있는 getGeneration() 메소드를 포함하고 있기 때문에 클라이언트는 어떤 클래스의 인스턴스인지는 신경쓰지 않고 Person 인터페이스에 의존한 개발을 할 수 있다.
Person person = Person.of(27);
String generation = person.getGeneration();
Person person1 = Person.of(56);
String generation1 = person1.getGeneration();
// Person 인스턴스가 Young이든 Old이든 신경쓰지 않고 getGeneration 호출 가능
단점
1. 생성자 없이 정적 팩토리 메서드만 제공한다면 상속을 할 수 없음
상속은 public 혹은 protected 생성자가 필요한데, 이러한 생성자 없이 팩토리 메서드만 제공한다면 상속을 할 수 없는 문제가 발생
2. 정적 팩토리 메서드와 생성자는 선택적 매개변수가 많을 때 적절히 대응하기 어렵다.
3. 빌더 패턴 (Builder Pattern)
빌더 패턴은 점층적 생성자 패턴의 안정성에 가독성을 결합한 형태임
객체 필드의 개수가 늘어나면 클라이언트 코드를 작성하기 어려워지고, 무엇보다 읽기 어려운 코드가 됨
이런 문제를 해결하기 위해 빌더 패턴가 고안됨
필요한 객체를 직접 생성하는 대신 클라이언트는 먼저 필수 인자들을 생성자에 전부 전달하여 빌더 객체를 만들고 빌더 객체에 정의된 설정 메소드를 통해 선택적 인자들을 추가해나감
그리고 마지막으로 아무런 인자 없이 build 메서드를 호출하여 immutable 객체를 만듦
public class PersonInfo {
private String name; //필수적으로 받야할 정보
private int age; //선택적으로 받아도 되는 정보
private int phonNumber; //선택적으로 받아도 되는 정보
private PersonInfo() {
}
public static class Builder {
private String name;
private int age;
private int phonNumber;
public Builder(String name) { // 필수변수는 생성자로 값을 넣는다.
this.name = name;
}
// 멤버변수별 메소드 - 빌더클래스의 필드값을 set하고 빌더객체를 리턴한다.
public Builder setAge(int age) {
this.age = age;
return this;
}
public Builder setPhonNumber(int phonNumber) {
this.phonNumber = phonNumber;
return this;
}
// 빌더메소드
public PersonInfo build() {
PersonInfo personInfo = new PersonInfo();
personInfo.name = name;
personInfo.age = age;
personInfo.phonNumber = phonNumber;
return personInfo;
}
}
위와같이 코드를 작성하면 다음과 같이 객체를 생성할 수 있다.
setter에 리턴자료형을 Builder객체로 지정함으로써, 메서드체이닝기법을 적용하고 정보를 다 넣은경우 build()메서드로 객체를 생성한다.
build( ) 메서드를 쓴 이후 에는 PersonInfo 클래스의 멤버변수를 변경 할 수 있는 방법은 리플렉션 기법(동적시점에 변경) 빼곤 존재하지 않는다.
따라서, 데이터 일관성, 객체불변성 등을 만족시킨다. 또한 코드 가독성 역시 올라간다.
PersonInfo personinfo = new PersonInfo
.Builder("SeungJin") // 필수값 입력 ( 빌더클래스 생성자로 빌더객체 생성)
.setAge(25) // 값 set
.setPhoneNumber(1234)
.build() // build() 가 객체를 생성해 돌려준다.
Lombok @Builder를 이용한 편리한 빌더 패턴 구현
@Builder
public class Person {
private final String name;
private final int age;
private final int phone;
}
Person person = Person.builder() // 빌더어노테이션으로 생성된 빌더클래스 생성자
.name("seungjin")
.age(25)
.phone(1234)
.build();
빌더 패턴은
빌더 패턴은 인자가 많은 생성자나 정적 팩토리가 필요한 클래스를 설계할 때, 대부분의 인자가 선택적 인자인 상황에 유용하다. 개인적으로도 예전부터 생각했었지만.. 다음 OS에서는 builder 패턴을 적용해보는게 목표이기도 하다.
참고
정적 팩토리 메서드(Static Factory Method)
정적 팩토리 메서드란 무엇인가?
velog.io
https://mangkyu.tistory.com/163
[Java] 빌더 패턴(Builder Pattern)을 사용해야 하는 이유
객체를 생성하기 위해서는 생성자 패턴, 정적 메소드 패턴, 수정자 패턴, 빌더 패턴 등을 사용할 수 있습니다. 개인적으로 객체를 생성할 때에는 반드시 빌더 패턴을 사용해야 한다고 생각하는
mangkyu.tistory.com
https://esoongan.tistory.com/82
[JAVA] 빌더패턴 (Builder Pattern) , @Builder
entity나 Dto객체에 값을 넣어줄때 롬복의 빌더 애노테이션(@Builder)을 종종 사용하곤 하는데 완벽히 이해를 하지 못한것같아 정리해보았다! 빌더패턴이란? 디자인패턴중 하나로, 생성과 표현의 분
esoongan.tistory.com