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では compileOnly と annotationProcessor の両方に指定するのを忘れずに。片方だけだとビルドは通っても生成が走らないことがあります。
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 が無限再帰する
User が List<Order> を持ち、Order が User を持つような構造で、両方に @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 か
@Value は final クラスかつ全フィールド 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で足を踏み外しやすいライブラリでもあります。アノテーションが何を生成しているかを頭に入れておけば、安心して恩恵だけ受け取れますよ。