본문 바로가기
Programing/Java

상속 구조의 모델 객체에서 Lombok의 @Builder, @Delegate 사용기

by Tomining 2017. 4. 21.
아래 글은 Lombok 1.16.16 기반에서 작성되었다.

Lombok에서 @Builder를 사용하다 보니 상속 구조에서 불필요한 코드를 작성해야 하는 경우가 있었다.
예를 들어 아래와 같이 두 모델 객체가 있다고 하자.
@Data
@AllArgsConstructor
public class User {
  protected String id;
  protected String name;
}
@Data
public class AdminUser extends User {
  private String roleId;
  private String roleName;

  @Builder  
  public AdminUser(String id, String name, String roleId, String roleName) {
    super(id, name);
    this.roleId = roleId;
    this.roleName = roleName;
  }
}

@Builder를 사용하면서 상속 구조를 가지려면 AdminUser와 같이 생성자를 생성해 주어야 한다.
public class AdminUserTest {
  @Test  public void 어드민_사용자_생성() {
    AdminUser adminUser = AdminUser.builder()
      .id("id")
      .name("name")
      .roleId("roleId")
      .roleName("roleName")
      .build();
    System.out.println(adminUser);
    System.out.println(adminUser.getId());
  }
}



실행 결과를 보면 AdminUser에 id, name 속성이 노출되지 않는다. 이는 Lombok이 아래와 같이 toString()을 구현해 두어서 그런 것이다. 실제로 id, name에 접근이 가능하다.
public String toString() {
    return "AdminUser(roleId=" + this.getRoleId() + ", roleName=" + this.getRoleName() + ")";
}

위 방법 외에도 User(부모 클래스)에 기본 생성자를 만들어 두고 AdminUser(자식 클래스)에서 가져다 사용하는 방법도 있으나, 어떤 방법이든 생성자를 별도로 만들어 줘야 하는 불편함이 있다.

상속이 아닌 합성(Composite)으로 위와 같은 기능을 구현할 수 있다. (상속과 합성의 차이는 별도로 찾아보자.)
합성을 이용하면 별도의 생성자를 만들지 않아도 @Builder를 사용할 수 있다.
다만 부모 클래스의 빌더에서 지원하는 함수는 함께 사용할 수 없다. 위 예제처럼 id(“id”) 나 name(“name”)은 적용할 수 없다는 것이다.

예제를 살펴보자.
@Data
@Builder
public class User {
  protected String id;
  protected String name;
}
@Data
@Builder
public class AdminUser {
  @Delegate  
  private User user;
  private String roleId;
  private String roleName;
}
public class AdminUserTest {
  @Test  
  public void 어드민_사용자_생성() {
    AdminUser adminUser = AdminUser.builder()
      .id("id")
      .name("name")
      .roleId("roleId")
      .roleName("roleName")
      .build();
    System.out.println(adminUser);
  }
}
public class AdminUserTest {
  @Test  
  public void 어드민_사용자_생성() {
    AdminUser adminUser = AdminUser.builder()
      .user(User.builder().id("id").name("name").build())
      .roleId("roleId")
      .roleName("roleName")
      .build();
    System.out.println(adminUser);
    System.out.println(adminUser.getId());
  }
}

붉은 부분의 코드는 컴파일 오류가 발생한다. @Delegate를 사용한 경우 .user()로 별도의 User 객체를 만들어 주어야 한다.
속성 접근 같은 getId(), getName() 모두 자식 클래스에서 사용할 수 있다.

@Delegate에는 exclude 옵션이 있는데, 이는 모든 method를 위임하지 않고 특정 메서드는 제외하고 AdminUser에서 구현하여 사용할 수도 있다.
예제는 lombok @Delegate 공식 문서를 참고하자.

결론

상속이든 합성이든 장단점이 있는 것 같다.
생성자를 별도로 만들어 주던지 아니면 Builder시 객체를 생성해서 넣어주던지 둘 중에 어느 방법이 나을지는 개인의 판단에 달린 것 같다. 개인마다 다르겠지만 요즘은 상속보다는 합성을 더 많이 사용하는 경향이 있는 것 같은 느낌이다.(근거는 없다… 개인적인 의견)

P.S 나중에라도 상속 또는 합성에서 별도의 작업을 하지 않고 적용하는 방법을 찾아봐야겠다.


참고