개요
프로그래밍 개발을 진행하다가 보면,
생성자를 통해 객체를 많이 생산하게 됩니다.
객체가 가지고 있는 인자들 많을 경우 그 인자들이 어떠한 값인지 헷갈릴 경우가 있습니다.
또한 생성자 호출을 위해서 설정하길 원하지 않는 매개변수의 값까지 지정해줘야 하는 불편함이 있습니다.
즉, 생성자를 빌더패턴으로 생성하지 않았을 경우
코드를 읽을 때 각 값의 의미가 무엇인지 헷갈릴 수 있고,
매개변수가 변개인지 세어보며 항상 확인해야 하며,
타입이 같은 매개변수가 연속으로 있으면 버그 발생 가능성이 높아지며,
실수로 매개변수의 순서가 바뀌더라도 컴파일러가 해당 에러를 잡지 못하여 런타임 에러로 이어지게 될 수 있습니다.
이러한 것을 해결 할 수 있는 Builder 패턴에 대해서 알아보도록 하겠습니다.
Builder 패턴이란?
빌더 패턴(Builder pattern) 이란 복합 객체의 생성 과정과 표현 방법을 분리하여 동일한 생성 절차에서 서로 다른 표현 결과를 만들 수 있게 하는 패턴입니다.
생성자 인자로 너무 많은 인자가 넘 져지는 경우 어떠한 인자가 어떠한 값을 나타내는지 확인하기 힘듭니다.
또 어떠한 인스턴스의 경우에는 특정 인자만으로 생성해야 하는 경우가 발생합니다.
특정 인자에 해당하는 값을 null로 전달해줘야 하는데, 이는 코드의 가독성 측면에서 매우 좋지 않다는 것을 알 수 있습니다.
이러한 문제를 해결하기 위해서 빌더패턴을 사용할 수 있습니다.
Lombok 없이 Java로 생성
생성자의 매개변수가 아래와 같이 많을 경우 보통 Builder 패턴을 이용하여 객체를 생성한다.
public Hero(Profession profession, String name, HairType hairType, HairColor hairColor, Armor armor, Weapon weapon) {
}
위와 같이 생성자 매개변수의 수는 빠르게 처리할 수 없고 매개변수의 배열을 이해하기 어려워질 수 있습니다,
또한 앞으로 더 많은 옵션을 추가하려는 경우 이 매개변수 목록이 계속 늘어날 수 있는데,
그렇게 되면 관리하고 이미 생성되어 있는 것들을 수정하기 어려워집니다.
빌더 패턴으로 생성자 Class 생성
public final class Hero {
private final Profession profession;
private final String name;
private final HairType hairType;
private final HairColor hairColor;
private final Armor armor;
private final Weapon weapon;
private Hero(Builder builder) {
this.profession = builder.profession;
this.name = builder.name;
this.hairColor = builder.hairColor;
this.hairType = builder.hairType;
this.weapon = builder.weapon;
this.armor = builder.armor;
}
}
빌더 Class 생성
public static class Builder {
private final Profession profession;
private final String name;
private HairType hairType;
private HairColor hairColor;
private Armor armor;
private Weapon weapon;
public Builder(Profession profession, String name) {
if (profession == null || name == null) {
throw new IllegalArgumentException("profession and name can not be null");
}
this.profession = profession;
this.name = name;
}
public Builder withHairType(HairType hairType) {
this.hairType = hairType;
return this;
}
public Builder withHairColor(HairColor hairColor) {
this.hairColor = hairColor;
return this;
}
public Builder withArmor(Armor armor) {
this.armor = armor;
return this;
}
public Builder withWeapon(Weapon weapon) {
this.weapon = weapon;
return this;
}
public Hero build() {
return new Hero(this);
}
}
빌더 패턴으로 생성자 생성.
Hero mage = new Hero
.Builder(Profession.MAGE, "Riobard")
.withHairColor(HairColor.BLACK)
.withWeapon(Weapon.DAGGER)
.build();
Lombok을 이용하여 생성.
위와 같이 생성을 하게 되면, 클래스를 만들 때마다 코드를 만들어야 하기 때문에 코드가 굉장히 길어지고, 불편해질 수 있습니다.
따라서 Lombok 라이브러리를 활용해 보았습니다.
@Builder 어노테이션을 사용하면 위와 같이 길게 따로 Builder Class를 만들지 않아도 됩니다.
Builder 패턴을 적용할 클래스.
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder(builderMethodName = "HeroBuilder")
@ToString
public class Hero {
private final Profession profession;
private final String name;
private final HairType hairType;
private final HairColor hairColor;
private final Armor armor;
private final Weapon weapon;
public static HeroBuilder builder(String name) {
if(name == null) {
throw new IllegalArgumentException("필수 파라미터 누락");
}
return HeroCheckListBuilder().name(name);
}
}
Builder 패턴을 이용하여 생성자 생성.
public class MainClass {
public static void main(String[] args) {
// 빌더패턴을 통해 어떤 필드에 어떤 값을 넣어주는지 명확히 눈으로 확인할 수 있다!
Hero hero = Hero.builder("아이언맨")
.profession(Profession.MAGE, "Riobard")
.hairType("Paris flight ticket")
.hairColor(HairColor.BLACK)
.armor("1235-5345")
.weapon(Weapon.DAGGER)
.build();
System.out.println("빌더 패턴 적용하기 : " + Hero.toString());
}
}
- @AllArgsConstructor(access = AccessLevel.PRIVATE)
- @Builder 어노테이션을 선언하면 전체 인자를 갖는 생성자를 자동으로 만듭니다.
- @AllArgsConstructor는 전체 인자를 갖는 생성자를 만드는데, 접근자를 private으로 만들어서 외부에서 접근할 수 없도록 만듭니다.
- @Builder
- 위에서 설명한 듯이 Builder 패턴을 자동으로 생성해주는데, builderMethodName에 들어간 이름으로 빌더 메소드를 생성해줍니다.
- 클래스 내부 builder 메소드
- 필수로 들어가야 할 필드들을 검증하기 위해 만들어졌습니다.
- 꼭 name이 아니더라도 해당 클래스를 객체로 생성할 때 필수적인 필드가 있다면 활용할 수 있습니다.
- PK를 보통 지정합니다.
Builder 패턴의 장점.
- 필요한 데이터만 설정할 수 있음.
- 유연성을 확보할 수 있음.
- 가독성을 높일 수 있음.
- 불변성을 확보할 수 있음.
1. 필요한 데이터만 설정할 수 있음.
위의 Hero 객체를 생성해야 하는데 armor 필드와 hairType의 필드가 필요 없는 상황이 있다고 가정을 한다면
우리는 armor필드와 hairType필드에 더미 값을 넣어주거나 그 두 개의 필드를 가지고 있지 않은 생성자를 새로 만들어주어야 합니다.
이러한 작업이 반복이 된다면 많은 시간 낭비와 코드의 양이 증가될 수 있습니다.
이럴 때 빌더를 이용하여 동적으로 처리할 수 있습니다.
Hero hero = Hero.builder("아이언맨")
.profession(Profession.MAGE, "Riobard")
.hairColor(HairColor.BLACK)
.weapon(Weapon.DAGGER)
.build();
그리고 이렇게 필요한 데이터만 설정할 수 있는 빌더의 장점은 테스트용 객체를 생성할 때 용이하게 해 주고, 불필요한 코드의 양을 줄이는 등의 이점을 안겨줍니다.
2. 유연성을 확보할 수 있음.
Hero 클래스에 나이를 나타내는 새로운 변수 age를 추가해야 하는데, 이미 아래와 같이 생성자로 객체를 만드는 코드가 있을 경우
// age 추가 해야하는 수정이 필요함.
Hero hero = new Hero("아이언맨",Profession.MAGE, "Riobard","Paris flight ticket",HairColor.BLACK,"1235-5345",Weapon.DAGGER)
// age 추가 해야하는 수정이 필요함.
Hero hero = new Hero("스파이더맨",Profession.MAGE, "Riobard","Paris flight ticket",HairColor.BLACK,"1235-5345",Weapon.DAGGER)
새롭게 추가되는 변수 때문에 기존의 코드를 수정해야 하는 상황에 직면하게 됩니다.
기존에 작성된 코드의 양이 방대하다면 감당하기 어려울 수 있습니다.
하지만 빌더 패턴을 이용하면 새로운 변수가 추가되는 등의 상황에 직면하여도 기존의 코드에 영향을 주지 않을 수 있습니다.
3. 가독성을 높일 수 있음.
빌더 패턴을 사용하면 매개변수가 많아져도 가독성을 높일 수 있습니다.
생성자로 객체를 생성하는 경우에는 매개변수가 많아질수록 코드 리딩이 급격하게 떨어집니다.
Hero hero = new Hero("아이언맨",Profession.MAGE, "Riobard","Paris flight ticket",HairColor.BLACK,"1235-5345",Weapon.DAGGER)
위의 코드를 보면 각각의 값들이 무엇을 의미하는지 바로 파악이 힘들고, 클래스 변수가 많아지면 더욱 코드를 읽기 힘들어집니다.
하지만 아래와 같이 빌더 패턴을 적용하면 가독성을 높일 수 있고,
직관적으로 어떤 데이터에 어떤 값이 설정되는지 쉽게 파악할 수 있습니다.
Hero hero = Hero.builder("아이언맨")
.profession(Profession.MAGE, "Riobard")
.hairType("Paris flight ticket")
.hairColor(HairColor.BLACK)
.armor("1235-5345")
.weapon(Weapon.DAGGER)
.build();
4. 불변성을 확보할 수 있음.
개발을 진행하면서 많은 개발자들이 흔히 수정자 패턴(Setter)을 흔히 사용합니다.
저도 물론 그랬으며, 굉장히 편하다는 장점이 있지만 Setter를 구현한다는 것은 불필요하게 확장 가능성을 열어두는 것입니다.
이는 Open-Closed 법칙에 위배되고, 불필요한 코드 리딩 등을 유발합니다. 그렇기 때문에 클래스 변수를 final로 선언하고 객체의 생성은 빌더에 맡기는 것이 좋습니다.
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@Builder
@ToString(exclude = "User")
public static final class User
{
@Setter(AccessLevel.NONE)
@Builder.Default
@NotNull
private String userkey;
@NotNull
private String name;
@Setter(AccessLevel.NONE)
private String number;
}
위와 같이 Setter에 AccessLevel.NONE을 두어 Setter lombok이 메소드를 생성하지 않게 할 수 도 있습니다.
그렇게 되면 이 Class의 생성에서 수정은 오로지 빌더에 의해서만 적용이 되어질 수 있습니다.
결론
이번에는 빌더 패턴에 대해서 알아보았습니다.
이 빌더 패턴은 프로젝트를 진행하면서, 생성자의 매개변수가 많을 경우 주로 유용하게 사용될 수 있으며,
그 외에도 여러 상황에 따라 각각 유용하게 사용될 수 있습니다.
'서버 > Spring' 카테고리의 다른 글
[Spring] RestTemplate, MultiValueMap, HttpEntity 이용하여 외부 API 호출하기 (0) | 2021.10.06 |
---|---|
[Spring] 외부 API 연동 (Java REST API방식과 Ajax 방식) 코드 및 설명 (0) | 2021.09.28 |
[Spring] - Service를 Interface로 생성 하는 이유. (AOP, 결합도) (2) | 2021.08.11 |
[Spring] 생명주기 분석 및 - 생명주기에 따른 코드 (Ajax, Json 사용) (0) | 2021.08.06 |
[Spring] Mybatis (Interface생성 및 xml 파일과 Mapping) (1) | 2021.08.03 |