[생각정리] @Builder 사용에 대한 고찰

jeyong·2024년 8월 5일
0

공부 / 생각 정리  

목록 보기
110/120
post-custom-banner


최근 Entity를 생성할 때 자주 사용하던 @Builder 어노테이션에 대한 생각이 변했다. 이번 글에서는 그 이유와 함께 좀 더 자세히 고찰해보고자 한다.

1. '@Builder' 사용의 이점과 문제점

@Builder
public MemberEntity(final String name, final OAuth2Type oAuth2, final String username,
                    final String profileImage, final RoleType role, final int batteryCount) {
    this.name = name;
    this.oAuth2 = oAuth2;
    this.username = username;
    this.profileImage = profileImage;
    this.role = role;
    this.batteryCount = batteryCount;
    this.chatBots = EnumSet.noneOf(ChatBotItem.class);
}

@Builder를 사용하여 Entity를 생성하면, 코드의 가독성과 유연성을 확보할 수 있다. 하지만 이 방식에는 몇 가지 문제가 존재한다.

문제점

memberRepository.save(
    MemberEntity.builder()
        .name(name)
        .oAuth2(oAuth2Response.getProvider())
        .username(oAuth2Response.getName())
        .role(ROLE_USER)
        .batteryCount(0)
        .build()
);

위 예시에서 profileImage가 누락되어 null로 데이터베이스에 입력될 수 있다. 이는 데이터 무결성에 문제를 일으킬 수 있다.

profile_image 칼럼이 null 값을 허용하지 않도록 설정되어 있지만, 실제로 null 값이 저장되려고 했기 때문에 데이터베이스의 무결성 제약 조건을 위반하여 오류가 반환되었다.

개발 중에 사용자 정보에 필드를 추가한다고 가정했을 때, 모든 Builder 사용 지점에 해당 필드를 추가하지 않으면, 단위 테스트에서 이를 감지하지 못하고 통합 테스트나 E2E 테스트에서야 발견되는 경우가 많다.

2. '@NonNull'을 이용한 해결책

@Builder
public MemberEntity(@NonNull final String name, @NonNull final OAuth2Type oAuth2, @NonNull final String username,
                    @NonNull final String profileImage, @NonNull final RoleType role, final int batteryCount) {
    this.name = name;
    this.oAuth2 = oAuth2;
    this.username = username;
    this.profileImage = profileImage;
    this.role = role;
    this.batteryCount = batteryCount;
    this.chatBots = EnumSet.noneOf(ChatBotItem.class);
}

@NonNull 어노테이션을 사용하면 null 값이 입력되는 것을 방지할 수 있다.

Lombok의 @NonNull 어노테이션은 메서드 인자에 null값이 들어온다면 NullPointerException을 발생시켜준다. 때문에 데이터베이스에 삽입되기 전에 발견 될 것이고, 사이드 이펙트를 방지하는 것에 도움을 줄 것이다.

즉, @NonNull을 이용한다면 단위 테스트에서 오류를 통해 충분히 검증가능 할 것이다.
하지만 아직도 문제가 남아있다.

여전히 남아있는 문제점

memberRepository.save(
    MemberEntity.builder()
        .name(name)
        .oAuth2(oAuth2Response.getProvider())
        .username(oAuth2Response.getName())
        .profileImage(oAuth2Response.getProfileImage())
        .role(ROLE_USER)
        .build()
);

이 경우, 어떤 필드가 누락되었는지 파악하기 어렵다. 예를 들어, 여기서는 배터리 개수가 입력되지 않았다. 코드의 명확성이 부족하고, 누락된 필드를 확인하기 어렵다.

배터리 개수는 기본타입이기 때문에 초기값으로 null이 아닌 0이 입력되고, 데이터베이스에 까지 저장되는 모습이다. 본인의 의지와는 상관없이 저장되는 것이다.

3. 생성자를 이용한 해결 방안

private MemberEntity(final String name, final OAuth2Type oAuth2, final String username,
                     final String profileImage, final RoleType role, final int batteryCount) {
    this.name = name;
    this.oAuth2 = oAuth2;
    this.username = username;
    this.profileImage = profileImage;
    this.role = role;
    this.batteryCount = batteryCount;
    this.chatBots = EnumSet.noneOf(ChatBotItem.class);
}

public static MemberEntity of(final String name, final OAuth2Type oAuth2, final String username,
                              final String profileImage, final RoleType role, final int batteryCount) {
    return new MemberEntity(name, oAuth2, username, profileImage, role, batteryCount);
}

생성자를 통해 Entity를 생성하면 필드 누락 가능성을 줄일 수 있다. 이 방법은 데이터의 일관성을 유지하는 데 도움이 된다.

memberRepository.save(
    MemberEntity.of(name, oAuth2Response.getProvider(), oAuth2Response.getName(),
                    oAuth2Response.getProfileImage(), ROLE_USER, 0)
);

이 방법은 필수 필드가 모두 포함되었는지 쉽게 확인할 수 있어, 코드의 안전성과 유지보수성을 높인다.

가독성의 문제도 요즘은 IDE의 도움을 받는다면 어렵지 않게 확인할 수있다. IntelliJ 기준으로 Ctrl + P 를 입력시, 확인 가능하다.

4. 결론

@Builder를 사용하면 인스턴스 생성이 유연하지만, 이는 때때로 개발자의 실수를 유발할 수 있다. 또한, 가독성은 IDE의 도움을 받으면 충분히 해결될 수 있다. 따라서, @Builder보다는 생성자를 이용하는 방식을 선호하게 되었다. 역시 튜닝의 끝은 순정인가 보다.

profile
노를 젓다 보면 언젠가는 물이 들어오겠지.
post-custom-banner

0개의 댓글