Spring Bootのサンプルコードを読んでいると @Data@RequiredArgsConstructor がほぼ毎回出てきますよね。便利な反面、JPA Entityで @Data を使って痛い目を見た方も多いはず。この記事では、実務で本当に使うLombokのアノテーションを用途別に整理して、安全な使い分けを押さえていきます。

Lombokって結局何をしてくれるのか

Lombokはアノテーションプロセッサです。コンパイル時にゲッターやセッター、コンストラクタなどのコードを生成してくれるので、ソース上はアノテーションだけでもバイトコードには普通のメソッドが入っています。

Spring Bootの公式 spring-boot-starter-parent でも依存管理に含まれており、spring init で生成したプロジェクトのテンプレートにもデフォルトで入ることがあります。それくらい日常的に使われている前提のライブラリです。

依存追加とビルド設定

Mavenの場合、provided 相当として扱い、Spring Boot Maven Pluginのexcludeで成果物から除外しておくのが定番です。

<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <optional>true</optional>
</dependency>

<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <configuration>
    <excludes>
      <exclude>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
      </exclude>
    </excludes>
  </configuration>
</plugin>

Gradleでは compileOnlyannotationProcessor の両方に指定するのを忘れずに。片方だけだとビルドは通っても生成が走らないことがあります。

dependencies {
  compileOnly 'org.projectlombok:lombok'
  annotationProcessor 'org.projectlombok:lombok'
  testCompileOnly 'org.projectlombok:lombok'
  testAnnotationProcessor 'org.projectlombok:lombok'
}

@Dataの中身を分解して理解する

@Data は実は5つのアノテーションをまとめたショートカットです。

  • @Getter
  • @Setter
  • @ToString
  • @EqualsAndHashCode
  • @RequiredArgsConstructor

つまり「全フィールドのゲッター・セッターを生やし、equals/hashCodeとtoStringも自動生成し、finalフィールドのコンストラクタも作る」という大盤振る舞いです。手軽な反面、用途を選ぶアノテーションでもあります。

@Data
public class UserDto {
    private Long id;
    private String name;
}

// 上は下と等価
@Getter
@Setter
@ToString
@EqualsAndHashCode
@RequiredArgsConstructor
public class UserDto {
    private Long id;
    private String name;
}

DTOのような単純なデータコンテナなら @Data で十分ですが、後述するようにJPA Entityでは避けたいケースがあります。

DIは @RequiredArgsConstructor + final が定番

Spring 4.3以降、単一コンストラクタなら @Autowired を省略できます。これに @RequiredArgsConstructor を組み合わせると、サービスクラスがぐっとスッキリします。

@Service
@RequiredArgsConstructor
public class OrderService {

    private final OrderRepository orderRepository;
    private final PaymentClient paymentClient;

    public Order place(OrderRequest request) {
        // ...
        return orderRepository.save(new Order(request));
    }
}

final を付けたフィールドだけがコンストラクタ引数になるので、依存関係が明示的になり、テストでのモック注入もしやすくなります。フィールドインジェクション(@Autowired private SomeService svc;)はテストしにくいので避けましょう。DIの考え方は DIとは何か で深掘りしています。

@Builderの落とし穴は @Builder.Default

@Builder はDTOの組み立てを読みやすくしてくれますが、フィールドの初期値が無視されるという罠があります。

@Builder
public class SearchCriteria {
    private int page;
    @Builder.Default
    private int size = 20;
    @Builder.Default
    private List<String> tags = new ArrayList<>();
}

SearchCriteria c = SearchCriteria.builder()
    .page(0)
    .build();
// size=20, tags=[] になる

@Builder.Default を付け忘れると size が0、tags がnullになります。継承を扱うなら @SuperBuilder を使いますが、親子両方に付ける必要があるので注意してください。

JPA Entityで @Data を使ってはいけない理由

ここが一番伝えたいところです。Entityに @Data を付けると、次の3つの問題が一気にやってきます。

1. 双方向関連で @ToString が無限再帰する

UserList<Order> を持ち、OrderUser を持つような構造で、両方に @ToString があると互いを呼び続けてStackOverflowです。

2. @EqualsAndHashCode がIDで動的に変わる

Lombokが生成するequals/hashCodeは全フィールドベース。IDがnullの状態でHashSetに入れた後に永続化してIDが採番されると、hashCodeが変わって取り出せなくなります。

3. Setterが集約の不変条件を壊す

@Setter で全フィールドが自由に書き換え可能になり、ドメインの整合性を守れなくなります。

推奨は @Getter を中心にして、equals/hashCodeはIDだけに限定する形です。

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class User {

    @Id
    @GeneratedValue
    @EqualsAndHashCode.Include
    private Long id;

    private String name;

    @OneToMany(mappedBy = "user")
    private List<Order> orders = new ArrayList<>();

    public User(String name) {
        this.name = name;
    }
}

Entityの設計については JPAエンティティの関連マッピング も合わせて読むと理解が深まります。

ロガーは @Slf4j で1行宣言

private static final Logger log = LoggerFactory.getLogger(...) を毎回書くのは面倒ですよね。@Slf4j を付けるだけで log 変数が自動で使えるようになります。

@Service
@RequiredArgsConstructor
@Slf4j
public class PaymentService {

    public void charge(Order order) {
        log.info("charging order id={}", order.getId());
        // ...
    }
}

Spring BootはデフォルトでLogback + SLF4Jなので、@Slf4j を選んでおけば素直に動きます。Log4j 2に切り替えている場合は @Log4j2 を使います。

不変オブジェクトは @Value か record か

@Valuefinal クラスかつ全フィールド private final を生成する不変版の @Data です。Java 16以降は record が使えるので、新規はrecordを優先するのがシンプルでしょう。

@Value
public class Money {
    String currency;
    BigDecimal amount;
}

public record Money(String currency, BigDecimal amount) {}

recordはJacksonの逆シリアル化やBean Validationとも問題なく動きます。ただしJPA Entityには使えない(プロキシ化できない)ので、その場合は従来通りクラス + Lombokの組み合わせになります。DTOとEntityの分離は DTOとMapStructでマッピングを整理する も参考にしてください。

@SneakyThrowsは原則使わない

@SneakyThrows はチェック例外を throws 宣言なしに投げられるようにする魔法ですが、呼び出し側からは例外の存在が見えなくなります。

さらにSpringの宣言的トランザクション(@Transactional)はデフォルトでunchecked例外しかロールバックしないため、checked例外を @SneakyThrows で隠してしまうとロールバックの挙動を見誤ることがあります。便利でも本番コードでは避けるのが無難です。

IDEで赤線が出るとき

コンパイルは通るのにIDEで getName() が見つからない、という症状はLombokのプラグイン未設定が原因です。

  • IntelliJ IDEA: Lombokプラグイン(最近はバンドル)を有効化し、Settings > Build, Execution, Deployment > Compiler > Annotation Processorsで「Enable annotation processing」をオン。
  • Eclipse: Lombok JARをダブルクリックしてインストーラーを実行し、Eclipseの本体を指定。
  • VS Code: Java拡張パックに含まれるLombokサポートが自動で動くはずです。動かなければ Lombok Annotations Support 拡張を入れます。

いざという時のdelombok

ライブラリを配布するときなど、Lombok依存を残したくない場面ではdelombokが使えます。Lombokを展開したプレーンなJavaソースを生成してくれるので、配布物からLombok依存を消せます。MavenやGradleプラグインから呼べるので、必要になった時に調べれば十分でしょう。

まとめ

実務で押さえておきたいポイントを並べておきます。

  • Entityでは @Data を使わず、@Getter + ID限定の @EqualsAndHashCode にする
  • DIは @RequiredArgsConstructor + final フィールドが基本形
  • DTOは @Builder + @Builder.Default、新規はrecordも選択肢
  • ログは @Slf4j で統一
  • @SneakyThrows は避ける

Lombokは便利ですが、生成されるコードを意識せず使うとJPAやDIで足を踏み外しやすいライブラリでもあります。アノテーションが何を生成しているかを頭に入れておけば、安心して恩恵だけ受け取れますよ。