반응형

개요

 

프로그래밍 개발을 진행하다가 보면,

생성자를 통해 객체를 많이 생산하게 됩니다.

객체가 가지고 있는 인자들 많을 경우 그 인자들이 어떠한 값인지 헷갈릴 경우가 있습니다.

또한 생성자 호출을 위해서 설정하길 원하지 않는 매개변수의 값까지 지정해줘야 하는 불편함이 있습니다.

즉, 생성자를 빌더패턴으로 생성하지 않았을 경우 

코드를 읽을 때 각 값의 의미가 무엇인지 헷갈릴 수 있고,

매개변수가 변개인지 세어보며 항상 확인해야 하며,

타입이 같은 매개변수가 연속으로 있으면 버그 발생 가능성이 높아지며,

실수로 매개변수의 순서가 바뀌더라도 컴파일러가 해당 에러를 잡지 못하여 런타임 에러로 이어지게 될 수 있습니다.

 

이러한 것을 해결 할 수 있는 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. 필요한 데이터만 설정할 수 있음.
  2. 유연성을 확보할 수 있음.
  3. 가독성을 높일 수 있음.
  4. 불변성을 확보할 수 있음.

 

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의 생성에서 수정은 오로지 빌더에 의해서만 적용이 되어질 수 있습니다.

 

 

 


결론

 

이번에는 빌더 패턴에 대해서 알아보았습니다.

이 빌더 패턴은 프로젝트를 진행하면서, 생성자의 매개변수가 많을 경우 주로 유용하게 사용될 수 있으며,

그 외에도 여러 상황에 따라 각각 유용하게 사용될 수 있습니다.

 

 

반응형

+ Recent posts