개발/Java|Spring

Builder 패턴과 Lombok @Builder 사용 시 주의사항

달리초이 2023. 2. 20. 15:36

 

자바 개발을 하다보면 대부분 Lombok을 사용하여 개발 편의성을 올리곤 한다. Lombok이 편한 건 맞지만 @Data 어노테이션을 잘못 사용했다간 객체 생성부터 어긋나기 시작해 버린다. Getter, Setter, RequiredArgsConstructor, ToString, EqualsAndHashCode, Value를 모두 한번에 적용하는데, 위험하지 않은 게 이상한 일이다.

 

무분별한 Setter 사용과 ToString으로 인한 순환 참조 문제 등 @Data나 @Setter 어노테이션의 위험성은 익히 알려져 있는 것 같은데, 함께 많이 쓰는 @Builder 어노테이션에 대해서는 상대적으로 위험성이 덜 알려진 거 같다.(내 기분이..) 나처럼 하수들이 막 쓰면 안될 정도로 '이거 좀 위험한데' 싶은 구석이 많아서 오늘은 디자인패턴 중 빌더(Builder) 패턴에 대해 간단하게 살펴보고 lombok의 @Builder 어노테이션 사용 시 주의사항을 알아보자.

 

 


 

Builder 패턴

디자인 패턴 중 빌더(Builder) 패턴은 객체 생성 단계를 캡슐화할 때 사용한다. 객체의 생성 과정과 표현 방법을 분리해서 동일한 생성 과정이라도 각각 다른 표현 결과를 만들 수 있다. 주로 복합 객체 구조를 구축하는데 많이 쓰인다.

 

Builder 패턴 구조

 

빌더(builder) 패턴

 

Builder 패턴 객체

AbstractBuilder - Foo 객체를 생성하기 위한 추상 인터페이스 정의

ConcreateBuilder - AbstractBuilder 추상 인터페이스를 구현. builder part들을 모아 실제 객체를 만들어서 복합 구조를 만든다.

Client - 클라이언트가 빌더에게 객체 생성 요청. 빌더 인터페이스를 사용하는 객체를 만든다. 최종적으로 getResultFoo() 메소드를 호출하여 완성된 객체를 가져온다.

 

 

Builder 패턴 예제 코드

- User 클래스

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;

	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;
	}

	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;
	}

	public static Builder builder(int id, String name) {
		return new Builder(id, name);
	}
	public static class Builder {
		private int id;
		private String name;
		public String emailAddress;
		public boolean isVerified;
		public LocalDateTime createdAt;
		public List<Integer> friendUserIds = new ArrayList<>();  // 기본값(빈값) 설정

		public 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;
		}

		private Builder(int id, String name) {
			this.id = id;
			this.name = name;
		}

		// 하나로 합쳐서 build 가능한 메서드
		public Builder with(Consumer<Builder> consumer) {
			consumer.accept(this);
			return this;
		}

		public Builder withEmailAddress(String emailAddress) {
			this.emailAddress = emailAddress;
			return this;
		}

		public Builder withIsVerified(boolean isVerified) {
			this.isVerified = isVerified;
			return this;
		}

		public Builder withCreatedAt(LocalDateTime createdAt) {
			this.createdAt = createdAt;
			return this;
		}

		public Builder withFriendUserids(List<Integer> friendUserIds) {
			this.friendUserIds = friendUserIds;
			return this;
		}

		public User build() {
			return new User(this);
		}

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

 

- User 클래스 빌더 사용 예제

// 개별적으로 생성
User user1 = User.builder(101, "Alice")
        .withEmailAddress("alice@gmail.com")
        .withCreatedAt(LocalDateTime.now())
        .withIsVerified(true)
        .build();

// with 메서드 하나로 생성
User user2 = User.builder(102, "Bob")
        .with((builder) -> {
            builder.isVerified = true;
            builder.friendUserIds = Arrays.asList(101, 103, 104);
            builder.emailAddress = "Bob@gmail.com";
            builder.createdAt = LocalDateTime.now();
        }).build();

 

위에선 2가지 유형을 보여주고 있다. 개별적으로 하나씩 입력하는 것과 with 메서드 하나를 이용하는 방식이다. 보통 lombok으로 자동 생성한 빌더 패턴은 첫번째 방식과 유사하다.

 

 


 

Lombok @Builder 어노테이션

그럼 lombok의 @Builder 어노테이션을 클래스 위에 붙이면 어떻게 되는지 확인 해 보자.

 

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

import lombok.Builder;

@Builder
public class Foo {
	private int id;
	private String name;
	private String emailAddress;
	private boolean isVerified;
	private LocalDateTime createdAt;
	private List<Integer> friendUserIds;
}

 

아래는 lombok이 컴파일 시점에 바이트코드를 변환하여 빌더 패턴을 주입한 결과물이다.

 

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

public class Foo {
  private int id;
  private String name;
  private String emailAddress;
  private boolean isVerified;
  private LocalDateTime createdAt;
  private List<Integer> friendUserIds;

  Foo(int id, String name, String emailAddress, boolean isVerified, LocalDateTime createdAt, List<Integer> friendUserIds) {
    this.id = id;
    this.name = name;
    this.emailAddress = emailAddress;
    this.isVerified = isVerified;
    this.createdAt = createdAt;
    this.friendUserIds = friendUserIds;
  }

  public static FooBuilder builder() {
    return new FooBuilder();
  }

  public static class FooBuilder {
    private int id;
    private String name;
    private String emailAddress;
    private boolean isVerified;
    private LocalDateTime createdAt;
    private List<Integer> friendUserIds;

    FooBuilder() {
    }

    public FooBuilder id(int id) {
      this.id = id;
      return this;
    }

    public FooBuilder name(String name) {
      this.name = name;
      return this;
    }

    public FooBuilder emailAddress(String emailAddress) {
      this.emailAddress = emailAddress;
      return this;
    }

    public FooBuilder isVerified(boolean isVerified) {
      this.isVerified = isVerified;
      return this;
    }

    public FooBuilder createdAt(LocalDateTime createdAt) {
      this.createdAt = createdAt;
      return this;
    }

    public FooBuilder friendUserIds(List<Integer> friendUserIds) {
      this.friendUserIds = friendUserIds;
      return this;
    }

    public Foo build() {
      return new Foo(this.id, this.name, this.emailAddress, this.isVerified, this.createdAt, this.friendUserIds);
    }

    public String toString() {
      return "Foo.FooBuilder(id=" + this.id + ", name=" + this.name + ", emailAddress=" + this.emailAddress + ", isVerified=" + this.isVerified + ", createdAt=" + this.createdAt + ", friendUserIds=" + this.friendUserIds + ")";
    }
  }
}

 

생성자에 매개변수를 받을 수도 있고, 아래처럼 빌더를 통해 값을 구성해 객체를 생성할 수도 있다.

 

Foo foo = Foo.builder()
    .name("달리")
    .emailAddress("12345")
    .build();

 

lombok을 이용해 어노테이션 하나 붙여줬을 뿐인데 이렇게 편하다니!

 

하지만, 뭐든지 너무 편하면 부작용을 의심해 봐야 한다. 그럼 앞서 언급했던 주의해야 할 점은 뭐가 있을까.

 

 

Lombok @Builder 사용 시 주의사항

위에서 보다시피 클래스 위에 @Builder 어노테이션을 작성할 경우 모든 멤버 변수를 받는 기본 생성자를 만들고 그에 맞춰 빌더 패턴 코드가 완성된다. DB와 연동한다 했을 때 통상 id나 createdAt 값 등 DB가 자동적으로 생성해 주는 값에 의존하는 경우가 많은데 이때 이 값들을 지정할 수 있도록 열려있는 구조는 문제가 될 여지가 다분하다.

 

Foo foo = Foo.builder()
    .id(100) // id를 자동으로 생성해준다면 이렇게 id를 넣어줄 필요가 없다.
    .createdAt(LocalDateTime.now().minusDays(1) // 의도하지 않은 다른 값을 넣을 수 있다.
    .build();  // (정책적으로 정해둔) 필수값이 빠져도 생성됨

 

만약 객체 생성 때 필수값이 있더라도, 모든 멤버 변수를 대상으로 하는 빌더에선 필수값을 넣지 않아도 생성되는 불상사가 발생할 수 있다.

 

그래서 결론적으로,

작업에 필요한 매개변수를 설정한 생성자 위에 @Builder 어노테이션을 붙여서 사용하도록 하자.

 

@Builder
public Foo(String name, String emailAddress) {
    this.name = name;
    this.emailAddress = emailAddress;
    this.isVerified = false;
    this.createdAt = LocalDateTime.now();
    this.friendUserIds = new ArrayList<>();
}

 

위의 예제에선 초기 고정값이 없는 name과 email만 입력받고 있다. 이렇게 컴파일 해보면,

 

 

위 이미지처럼 다른 멤버 변수는 닫혀 있어 실수를 방지할 수 있다. 세상에 완벽한 사람이 없듯이 뭐든 트레이드오프가 있다는 걸 명심하고, 미연의 사고는 시스템으로 해결할 수 있도록 주의하자.

 

빌더 메서드 이름도 변경 할 수 있다. 아래처럼 @Builder 어노테이션 옆에 옵션을 줄 수 있다. 이로써 빌더가 다양할 때 보다 명시적으로 사용할 수 있게 되었다.

@Builder(builderMethodName = "createFoo")
// 사용할 때
Foo.createFoo()
    .name("Bob")
    .emailAddress("가나다라@gmail.com")
    .build();

 

 

 

덧.

아래 Yun님의 글이 정리가 잘 되어 있어서 추천드린다.

 

 

 

 

[참고]

Yun Blog - 실무에서 Lombok 사용법

GoF 디자인 패턴

헤드퍼스트 디자인패턴 - 한빛미디어

728x90
반응형