layered-architecture について
Spring アプリケーション開発者向けのレイヤードアーキテクチャガイドです。コントローラー、サービス、リポジトリ、DTO の責務を定め、マッピングや設定、横断的関心事の扱いも説明します。
主な機能
- 01コントローラー・サービス・リポジトリの責務
- 02リクエスト DTO とレスポンス DTO の分離
- 03専用マッパーと静的ファクトリーメソッド
- 04設定レイヤーの構成規則
- 05トランザクション・検証・例外処理の規則
活用シーン
- 01Spring アプリのレイヤー責務の設計
- 02コントローラーから業務ロジックを分離
- 03エンティティからレスポンス DTO へのマッピング設計
- 04トランザクションと例外処理の配置整理
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ rrezartprebreza/ spring-boot-skills --skill layered-architectureインストール後、Claude Code で/layered-architectureを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
agents/openai.yamlexamples/bad-service.javagood-service.javatemplates/CreateOrderRequest.javaOrderMapper.javaOrderResponse.java詳細
レイヤードアーキテクチャ
レイヤーのルール
@RestController ← HTTP only. No business logic. No JPA entities in responses.
↓ DTOs
@Service ← All business logic lives here. Orchestrates repositories.
↓ Domain objects / Entities
@Repository ← Data access only. No business logic. Returns entities or projections.
↓ JPA / JDBC
Database
コントローラーレイヤー
- HTTPを処理します。リクエストの解析、入力の検証(
@Valid)、レスポンスの返却を行います - エンドポイントごとに呼び出すサービスメソッドは1つだけです。コントローラーでオーケストレーションを行いません
@Entityクラスを直接返しません。必ずレスポンスDTOにマッピングします@Repositoryを注入しません。必ず@Serviceを経由します- 例外処理には
@ControllerAdviceを使用します。コントローラー内で try/catch を使用しません
// ✅ GOOD
@PostMapping("/orders")
public ResponseEntity<OrderResponse> createOrder(@Valid @RequestBody CreateOrderRequest request) {
Order order = orderService.createOrder(request);
return ResponseEntity.status(HttpStatus.CREATED).body(OrderResponse.from(order));
}
// ❌ BAD — business logic in controller
@PostMapping("/orders")
public ResponseEntity<Order> createOrder(@RequestBody CreateOrderRequest request) {
if (request.getItems().isEmpty()) throw new RuntimeException("No items");
Order order = orderRepository.save(new Order(request)); // direct repo access
return ResponseEntity.ok(order); // returning entity
}
サービスレイヤー
- すべてのビジネスロジック、検証ルール、オーケストレーションを含みます
@Transactionalはここに配置し、コントローラーやリポジトリには配置しません- コンストラクターインジェクションのみを使用し、
@Autowiredによるフィールドインジェクションは使用しません - 集約ルートごとにサービスを1つ作成します(OrderAndPaymentService ではなく OrderService)
- ドメインオブジェクトまたはDTOを返し、
HttpServletRequest/HttpServletResponseは返しません
// ✅ GOOD
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final InventoryService inventoryService;
@Transactional
public Order createOrder(CreateOrderRequest request) {
inventoryService.reserve(request.items()); // records use accessor names, not getX()
Order order = Order.from(request);
return orderRepository.save(order);
}
}
// ❌ BAD — field injection, HTTP concern in service
@Service
public class OrderService {
@Autowired private OrderRepository orderRepository;
public ResponseEntity<Order> createOrder(...) { ... } // HTTP type in service
}
リポジトリレイヤー
JpaRepository<Entity, ID>またはCrudRepositoryを継承します- カスタムクエリには
@Queryまたはクエリ派生を使用します。やむを得ない場合を除き、生のSQLは使用しません - エンティティまたはSpring Data Projectionを返し、生の
Object[]は返しません - ビジネスロジックは含めず、データアクセスのみを行います
DTO
- Request DTOとResponse DTOは分け、同じクラスを両方に使用しません
- 検証アノテーション(
@NotNull、@Sizeなど)はRequest DTOのみに付けます - マッピングには、静的ファクトリーメソッド
ResponseDto.from(Entity entity)を使用します - 不変DTOにはレコードを使用します(Java 16以降)
// ✅ GOOD
public record OrderResponse(UUID id, String status, List<LineItemResponse> items, Instant createdAt) {
public static OrderResponse from(Order order) {
return new OrderResponse(order.getId(), order.getStatus().name(),
order.getItems().stream().map(LineItemResponse::from).toList(),
order.getCreatedAt());
}
}
マッパーパターン
- マッピングロジックをコントローラーやサービスに含めず、専用のマッパークラスまたは静的ファクトリーメソッドを使用します
- マッパーは通常のクラスまたはユーティリティとし、依存関係の注入が必要でない限り、Spring Beanにはしません
- Entity → Response DTO:レスポンスDTOの静的メソッド(
OrderResponse.from(order))を使用します - Request DTO → Entity:エンティティの静的ファクトリー(
Order.from(request))またはマッパークラスを使用します - コレクションのマッピングには
.stream().map(OrderResponse::from).toList()を使用し、手動ループは使用しません
// ✅ GOOD — dedicated mapper for complex mappings
public class OrderMapper {
public static OrderResponse toResponse(Order order) {
return new OrderResponse(
order.getId(),
order.getStatus().name(),
order.getItems().stream().map(OrderMapper::toLineItem).toList(),
order.getCreatedAt()
);
}
public static Order toEntity(CreateOrderRequest request, User user) {
Order order = Order.create(request.customerEmail(), user);
request.items().forEach(item ->
order.addItem(item.productId(), item.quantity()));
return order;
}
private static LineItemResponse toLineItem(OrderItem item) {
return new LineItemResponse(item.getProductId(), item.getQuantity(), item.getPrice());
}
}
設定レイヤー
@Configurationクラスはconfig/パッケージに配置し、service/やcontroller/には配置しません- 設定クラスからサービスクラスやコントローラークラスをインポートしません
- 型安全な設定には
@ConfigurationPropertiesを使用し、関連する設定のグループに生の@Valueを使用しません - Bean定義はインフラストラクチャ関連のものに限定します(RestTemplate、ObjectMapper、SecurityFilterChain)
横断的関心事
- ロギングには
@Slf4jを使用し、System.out.printlnは使用しません - バリデーション:コントローラーパラメーターに
@Validを付け、カスタムバリデーターには@Componentを付けます - 例外処理:
@RestControllerAdviceクラスは1つにし、コントローラー内で try/catch を使用しません - 監査:
@EnableJpaAuditingとともに@CreatedDate/@LastModifiedDateを使用します
注意点
- エージェントは
@Transactionalをコントローラーに付けがちです。サービスに移動してください - エージェントは
@Autowiredによるフィールドインジェクションを使用します。必ずコンストラクターインジェクション(@RequiredArgsConstructor)を使用してください - エージェントはコントローラーから
List<Entity>を返します。必ずList<ResponseDto>にマッピングしてください - エージェントは
OrderAndInventoryServiceのような巨大なクラスを作成します。集約ごとに分割してください - エージェントはコントローラー内にマッピングロジックを記述します。マッパークラスまたはDTOのファクトリーメソッドに切り出してください
- エージェントは
@ServiceBeanに依存する@Configurationクラスを作成します。設定クラスではインフラストラクチャのみを配線してください
コメント
Markdown 対応