spring-framework-patterns について
Spring Framework Patterns は、Spring Framework と Spring Boot を扱う開発者やレビュー担当者向けのリファレンススキルです。Spring アプリケーションのアーキテクチャ、実装、セキュリティ、保守性に関する指針をまとめています。
主な機能
- 01依存性注入の指針
- 02Bean のライフサイクルとスコープ
- 03REST コントローラー設計
- 04Spring Data JPA のパターン
- 05セキュリティと例外処理
活用シーン
- 01Spring アプリケーションのコードレビュー
- 02Spring Boot アーキテクチャの設計
- 03コンポーネントの実装やリファクタリング
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ clostaunau/ holiday-card --skill spring-framework-patternsインストール後、Claude Code で/spring-framework-patternsを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
checklists/code-review-checklist.mdexamples/README.mdSecurityConfigExample.javaUserServiceExample.javatemplates/GlobalExceptionHandler.java.templateREADME.mdRepository.java.templateRestController.java.templateService.java.template詳細
Spring Framework のパターン
目的
このスキルでは、Spring Framework および Spring Boot 開発に関する包括的なパターンとベストプラクティスを紹介します。コードレビュー時のリファレンスガイドとして、Spring アプリケーションが業界標準に従い、保守性と拡張性を備え、エンタープライズ Java の規約に準拠していることを確認するために役立ちます。
このスキルを使用する場面:
- Spring/Spring Boot アプリケーションのコードレビューを行うとき
- Spring Boot アプリケーションのアーキテクチャを設計するとき
- 新しい Spring コンポーネント(コントローラー、サービス、リポジトリ)を作成するとき
- 既存の Spring アプリケーションをリファクタリングするとき
- Spring の設定やセットアップを評価するとき
- チームメンバーに Spring のベストプラクティスを教えるとき
コンテキスト
Spring Framework は、エンタープライズ Java アプリケーションにおける事実上の標準です。このスキルでは、Spring Boot 3.x 以降および Spring 6.x 以降を使用した、本番環境に対応できるパターンを次の点を重視して説明します。
- モジュール性:適切なレイヤー分けによる明確な関心の分離
- 保守性:理解や変更が容易なコード
- テスト容易性:簡単にテストできるコンポーネント
- パフォーマンス:Spring の機能を効率的に使用すること
- セキュリティ:デフォルトで安全なパターン
- 設定より規約:Spring Boot の自動設定を活用すること
このスキルは、コードレビュー時に uncle-duke-java エージェントが参照するほか、開発者が Spring アプリケーションを実装する際にも利用することを想定しています。
前提条件
必要な知識:
- Java の基礎(Java 17 以降)
- オブジェクト指向プログラミングの概念
- Spring の概念に関する基本的な理解
- Maven または Gradle の基礎
必要なツール:
- JDK 17 以降
- Spring Boot 3.x 以降
- Maven または Gradle
- IDE(IntelliJ IDEA、Eclipse、VS Code)
想定されるプロジェクト構成:
spring-boot-app/
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/example/app/
│ │ │ ├── Application.java
│ │ │ ├── config/
│ │ │ ├── controller/
│ │ │ ├── service/
│ │ │ ├── repository/
│ │ │ ├── model/
│ │ │ ├── dto/
│ │ │ ├── exception/
│ │ │ └── security/
│ │ └── resources/
│ │ ├── application.yml
│ │ ├── application-dev.yml
│ │ ├── application-prod.yml
│ │ └── db/migration/
│ └── test/
│ └── java/
├── pom.xml (or build.gradle)
└── README.md
手順
タスク 1:依存性注入のベストプラクティスを実践する
1.1 コンストラクターインジェクション(推奨)
ルール: 必須の依存関係には、必ずコンストラクターインジェクションを使用してください。本番コードではフィールドインジェクションを使用しないでください。
✅ 良い例:
@Service
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
// Constructor injection - preferred approach
public UserService(UserRepository userRepository, EmailService emailService) {
this.userRepository = userRepository;
this.emailService = emailService;
}
public User createUser(UserDTO dto) {
User user = new User(dto.getEmail());
User savedUser = userRepository.save(user);
emailService.sendWelcomeEmail(savedUser);
return savedUser;
}
}
良い理由:
- 依存関係が不変です(
finalフィールド) - 依存関係が必須であり、それらなしではインスタンスを作成できません
- テストが簡単です。テストでモックを注入できます
- テストでリフレクションが不要です
- すべての依存関係がコンストラクターに明示されます
❌ 悪い例:
@Service
public class UserService {
// Field injection - DON'T DO THIS
@Autowired
private UserRepository userRepository;
@Autowired
private EmailService emailService;
public User createUser(UserDTO dto) {
User user = new User(dto.getEmail());
User savedUser = userRepository.save(user);
emailService.sendWelcomeEmail(savedUser);
return savedUser;
}
}
悪い理由:
finalにできず、依存関係が変更可能です- 依存関係がなくてもインスタンスを作成できるため、
NullPointerExceptionのリスクがあります - テストが困難で、リフレクションまたはテストでの Spring コンテキストが必要です
- 依存関係が隠れており、何が必須なのか明確ではありません
- カプセル化に違反します
Lombok を使用する場合(許容されます):
@Service
@RequiredArgsConstructor // Generates constructor for final fields
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
public User createUser(UserDTO dto) {
User user = new User(dto.getEmail());
User savedUser = userRepository.save(user);
emailService.sendWelcomeEmail(savedUser);
return savedUser;
}
}
1.2 セッターインジェクションによるオプションの依存関係
ルール: セッターインジェクションは、オプションの依存関係にのみ使用してください。
✅ 良い例:
@Service
public class NotificationService {
private final EmailService emailService; // Required
private SmsService smsService; // Optional
public NotificationService(EmailService emailService) {
this.emailService = emailService;
}
@Autowired(required = false)
public void setSmsService(SmsService smsService) {
this.smsService = smsService;
}
public void notify(User user, String message) {
emailService.send(user.getEmail(), message);
if (smsService != null && user.getPhoneNumber() != null) {
smsService.send(user.getPhoneNumber(), message);
}
}
}
1.3 循環依存の回避
ルール: 循環依存は設計上の問題を示しています。@Lazy を使用するのではなく、リファクタリングしてください。
❌ 悪い例:
@Service
public class OrderService {
@Autowired
@Lazy // Band-aid solution
private PaymentService paymentService;
}
@Service
public class PaymentService {
@Autowired
@Lazy // Band-aid solution
private OrderService orderService;
}
✅ 良い例:
// Extract common logic to a new service
@Service
public class OrderProcessingService {
private final OrderRepository orderRepository;
private final PaymentRepository paymentRepository;
public OrderProcessingService(OrderRepository orderRepository,
PaymentRepository paymentRepository) {
this.orderRepository = orderRepository;
this.paymentRepository = paymentRepository;
}
public void processOrder(Order order, Payment payment) {
order.setStatus(OrderStatus.PROCESSING);
orderRepository.save(order);
payment.setOrderId(order.getId());
paymentRepository.save(payment);
}
}
@Service
public class OrderService {
private final OrderProcessingService processingService;
// ...
}
@Service
public class PaymentService {
private final OrderProcessingService processingService;
// ...
}
タスク 2:Bean のライフサイクルとスコープを理解する
2.1 Bean スコープ
利用可能なスコープ:
singleton(デフォルト):Spring コンテナーごとに 1 つのインスタンスprototype:Bean がリクエストされるたびに新しいインスタンスrequest:HTTP リクエストごとに 1 つのインスタンス(Web アプリケーション)session:HTTP セッションごとに 1 つのインスタンス(Web アプリケーション)application:ServletContext ごとに 1 つのインスタンス(Web アプリケーション)
✅ 良い例:
// Singleton (default) - stateless services
@Service
@Scope("singleton") // Can omit - it's default
public class UserService {
// Stateless - safe to share
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
// Prototype - stateful beans
@Component
@Scope("prototype")
public class ReportGenerator {
// Stateful - each user gets their own instance
private final List<String> reportLines = new ArrayList<>();
public void addLine(String line) {
reportLines.add(line);
}
public String generate() {
return String.join("\n", reportLines);
}
}
// Request scope - web layer
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
private String requestId;
private String userId;
// Getters and setters
}
❌ 悪い例:
// Singleton with mutable state - THREAD UNSAFE
@Service
public class UserService {
private User currentUser; // Shared across all requests - BAD!
public void processUser(User user) {
this.currentUser = user; // Race condition!
// Process...
}
}
2.2 Bean のライフサイクルコールバック
ルール: 初期化には @PostConstruct を、クリーンアップには @PreDestroy を使用してください。
✅ 良い例:
@Service
public class CacheService {
private final Map<String, Object> cache = new ConcurrentHashMap<>();
private ScheduledExecutorService scheduler;
@PostConstruct
public void initialize() {
System.out.println("Initializing cache service...");
scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(this::cleanupExpiredEntries, 1, 1, TimeUnit.HOURS);
}
@PreDestroy
public void cleanup() {
System.out.println("Cleaning up cache service...");
cache.clear();
if (scheduler != null) {
scheduler.shutdown();
}
}
private void cleanupExpiredEntries() {
// Cleanup logic
}
}
2.3 コンポーネントのステレオタイプ
ルール: 最も具体的なステレオタイプアノテーションを使用してください。
@Component // Generic Spring-managed component
@Service // Business logic layer
@Repository // Data access layer (adds exception translation)
@Controller // MVC controller (returns views)
@RestController // REST API controller (returns data)
@Configuration // Configuration class
✅ 良い例:
@Repository // Data access - enables exception translation
public interface UserRepository extends JpaRepository<User, Long> {
}
@Service // Business logic
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
}
@RestController // REST API
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
}
@Configuration // Configuration
public class SecurityConfig {
// Configuration beans
}
タスク 3:REST API コントローラーを設計する
3.1 コントローラーの構成
ルール: コントローラーは薄く保ち、ビジネスロジックはサービスに委譲してください。
✅ 良い例:
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<Page<UserDTO>> getAllUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "id") String sortBy) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy));
Page<UserDTO> users = userService.findAll(pageable);
return ResponseEntity.ok(users);
}
@GetMapping("/{id}")
public ResponseEntity<UserDTO> getUserById(@PathVariable Long id) {
return userService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@PostMapping
public ResponseEntity<UserDTO> createUser(@Valid @RequestBody CreateUserRequest request) {
UserDTO created = userService.createUser(request);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();
return ResponseEntity.created(location).body(created);
}
@PutMapping("/{id}")
public ResponseEntity<UserDTO> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
return userService.updateUser(id, request)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}
良い理由:
- RESTful な URL 構造
- 適切な HTTP メソッドとステータスコード
- ページネーションとソートのサポート
@Validによるバリデーション- 作成されたリソースの Location ヘッダー
- ロジックをサービスレイヤーに委譲
❌ 悪い例:
@RestController
public class UserController {
@Autowired
private UserRepository userRepository; // Controller accessing repository directly!
@GetMapping("/getUsers") // Non-RESTful URL
public List<User> getUsers() { // Returns entities, not DTOs
return userRepository.findAll(); // Business logic in controller
}
@PostMapping("/createUser") // Non-RESTful URL
public User createUser(@RequestBody User user) { // No validation
// Business logic in controller - BAD!
if (userRepository.findByEmail(user.getEmail()).isPresent()) {
throw new RuntimeException("Email exists"); // Poor error handling
}
return userRepository.save(user); // Returns 200 instead of 201
}
}
3.2 リクエスト/レスポンスのパターン
ルール: API コントラクトには DTO を使用します。エンティティを直接公開してはいけません。
✅ 良い例:
// Request DTOs
public class CreateUserRequest {
@NotBlank(message = "Email is required")
@Email(message = "Email must be valid")
private String email;
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100, message = "Name must be between 2 and 100 characters")
private String name;
@NotBlank(message = "Password is required")
@Size(min = 8, message = "Password must be at least 8 characters")
private String password;
// Getters and setters
}
// Response DTOs
public class UserDTO {
private Long id;
private String email;
private String name;
private LocalDateTime createdAt;
private boolean active;
// No password field - security
// Getters and setters
}
// Service layer
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final UserMapper userMapper;
public UserDTO createUser(CreateUserRequest request) {
// Validate
if (userRepository.existsByEmail(request.getEmail())) {
throw new EmailAlreadyExistsException(request.getEmail());
}
// Map DTO to entity
User user = new User();
user.setEmail(request.getEmail());
user.setName(request.getName());
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setActive(true);
user.setCreatedAt(LocalDateTime.now());
// Save
User saved = userRepository.save(user);
// Map entity to DTO
return userMapper.toDTO(saved);
}
}
3.3 HTTP ステータスコード
ルール: 適切な HTTP ステータスコードを使用します。
@RestController
@RequestMapping("/api/v1/orders")
@RequiredArgsConstructor
public class OrderController {
private final OrderService orderService;
// 200 OK - Successful GET/PUT
@GetMapping("/{id}")
public ResponseEntity<OrderDTO> getOrder(@PathVariable Long id) {
return orderService.findById(id)
.map(ResponseEntity::ok) // 200 OK
.orElse(ResponseEntity.notFound().build()); // 404 Not Found
}
// 201 Created - Successful POST
@PostMapping
public ResponseEntity<OrderDTO> createOrder(@Valid @RequestBody CreateOrderRequest request) {
OrderDTO created = orderService.createOrder(request);
URI location = ServletUriComponentsBuilder
.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(created.getId())
.toUri();
return ResponseEntity.created(location).body(created); // 201 Created
}
// 204 No Content - Successful DELETE
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteOrder(@PathVariable Long id) {
orderService.deleteOrder(id);
return ResponseEntity.noContent().build(); // 204 No Content
}
// 202 Accepted - Async processing
@PostMapping("/{id}/process")
public ResponseEntity<Void> processOrder(@PathVariable Long id) {
orderService.processOrderAsync(id);
return ResponseEntity.accepted().build(); // 202 Accepted
}
// 400 Bad Request - Validation failures (handled by @Valid)
// 401 Unauthorized - Not authenticated (handled by Security)
// 403 Forbidden - Not authorized (handled by Security)
// 404 Not Found - Resource doesn't exist
// 409 Conflict - Business rule violation
// 500 Internal Server Error - Unexpected errors
}
タスク 4:Spring Data JPA でデータアクセス層を実装する
4.1 リポジトリーインターフェース
ルール: 要件に応じて適切な Spring Data インターフェースを拡張します。
✅ 良い例:
// Simple CRUD - JpaRepository
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// Query methods - Spring Data generates implementation
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
List<User> findByActiveTrue();
// Custom query
@Query("SELECT u FROM User u WHERE u.createdAt > :date")
List<User> findRecentUsers(@Param("date") LocalDateTime date);
// Native query
@Query(value = "SELECT * FROM users WHERE email LIKE %:domain", nativeQuery = true)
List<User> findByEmailDomain(@Param("domain") String domain);
// Modifying query
@Modifying
@Query("UPDATE User u SET u.active = false WHERE u.lastLoginAt < :date")
int deactivateInactiveUsers(@Param("date") LocalDateTime date);
}
リポジトリーの階層:
Repository<T, ID>- マーカーインターフェース。メソッドはありませんCrudRepository<T, ID>- 基本的な CRUD 操作PagingAndSortingRepository<T, ID>- ページネーションとソートを追加JpaRepository<T, ID>- JPA 固有の機能(フラッシュ、バッチ操作)
4.2 Specifications によるカスタムクエリー
ルール: クエリー文字列を組み立てるのではなく、動的クエリーには Specifications を使用します。
✅ 良い例:
// Specification
public class UserSpecifications {
public static Specification<User> hasEmail(String email) {
return (root, query, cb) ->
email == null ? null : cb.equal(root.get("email"), email);
}
public static Specification<User> isActive() {
return (root, query, cb) -> cb.isTrue(root.get("active"));
}
public static Specification<User> createdAfter(LocalDateTime date) {
return (root, query, cb) ->
date == null ? null : cb.greaterThan(root.get("createdAt"), date);
}
public static Specification<User> nameLike(String name) {
return (root, query, cb) ->
name == null ? null : cb.like(cb.lower(root.get("name")), "%" + name.toLowerCase() + "%");
}
}
// Repository
@Repository
public interface UserRepository extends JpaRepository<User, Long>, JpaSpecificationExecutor<User> {
}
// Service
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public Page<User> searchUsers(UserSearchCriteria criteria, Pageable pageable) {
Specification<User> spec = Specification.where(null);
if (criteria.getEmail() != null) {
spec = spec.and(UserSpecifications.hasEmail(criteria.getEmail()));
}
if (criteria.isActiveOnly()) {
spec = spec.and(UserSpecifications.isActive());
}
if (criteria.getCreatedAfter() != null) {
spec = spec.and(UserSpecifications.createdAfter(criteria.getCreatedAfter()));
}
if (criteria.getName() != null) {
spec = spec.and(UserSpecifications.nameLike(criteria.getName()));
}
return userRepository.findAll(spec, pageable);
}
}
4.3 ページネーションとソート
ルール: リストを返すエンドポイントでは、常にページネーションをサポートします。
✅ 良い例:
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<Page<UserDTO>> getUsers(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(defaultValue = "id,desc") String[] sort) {
// Parse sort parameters
List<Sort.Order> orders = Arrays.stream(sort)
.map(s -> {
String[] parts = s.split(",");
String property = parts[0];
Sort.Direction direction = parts.length > 1 && parts[1].equalsIgnoreCase("desc")
? Sort.Direction.DESC
: Sort.Direction.ASC;
return new Sort.Order(direction, property);
})
.toList();
Pageable pageable = PageRequest.of(page, size, Sort.by(orders));
Page<UserDTO> users = userService.findAll(pageable);
return ResponseEntity.ok(users);
}
}
4.4 トランザクション管理
ルール: @Transactional はリポジトリーメソッドではなく、サービスメソッドに使用します。
✅ 良い例:
@Service
@RequiredArgsConstructor
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentRepository paymentRepository;
private final EmailService emailService;
@Transactional
public OrderDTO createOrder(CreateOrderRequest request) {
// Create order
Order order = new Order();
order.setUserId(request.getUserId());
order.setStatus(OrderStatus.PENDING);
Order savedOrder = orderRepository.save(order);
// Create payment
Payment payment = new Payment();
payment.setOrderId(savedOrder.getId());
payment.setAmount(request.getAmount());
paymentRepository.save(payment);
// If email fails, transaction rolls back
emailService.sendOrderConfirmation(savedOrder);
return mapToDTO(savedOrder);
}
@Transactional(readOnly = true) // Optimization for read-only operations
public Optional<OrderDTO> findById(Long id) {
return orderRepository.findById(id)
.map(this::mapToDTO);
}
@Transactional(isolation = Isolation.SERIALIZABLE) // For critical operations
public void processPayment(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
// Process payment with highest isolation level
order.setStatus(OrderStatus.PAID);
orderRepository.save(order);
}
}
トランザクション伝播:
@Service
public class UserService {
@Transactional(propagation = Propagation.REQUIRED) // Default - join existing or create new
public void updateUser(User user) {
// Uses existing transaction or creates new
}
@Transactional(propagation = Propagation.REQUIRES_NEW) // Always create new transaction
public void auditLog(String action) {
// Independent transaction - commits even if parent rolls back
}
@Transactional(propagation = Propagation.MANDATORY) // Must have existing transaction
public void criticalOperation() {
// Throws exception if no transaction exists
}
}
タスク 5:サービス層を設計する
5.1 サービスの境界
ルール: サービスはデータアクセスではなく、ビジネス機能を表すものにします。
✅ 良い例:
// Good service boundaries
@Service
@RequiredArgsConstructor
public class UserRegistrationService {
private final UserRepository userRepository;
private final EmailService emailService;
private final PasswordEncoder passwordEncoder;
@Transactional
public UserDTO register(RegistrationRequest request) {
// Validate
validateEmailNotExists(request.getEmail());
// Create user
User user = createUser(request);
User saved = userRepository.save(user);
// Send welcome email
emailService.sendWelcomeEmail(saved.getEmail());
return mapToDTO(saved);
}
private void validateEmailNotExists(String email) {
if (userRepository.existsByEmail(email)) {
throw new EmailAlreadyExistsException(email);
}
}
private User createUser(RegistrationRequest request) {
User user = new User();
user.setEmail(request.getEmail());
user.setName(request.getName());
user.setPassword(passwordEncoder.encode(request.getPassword()));
return user;
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Transactional(readOnly = true)
public Optional<UserDTO> findById(Long id) {
return userRepository.findById(id).map(this::mapToDTO);
}
@Transactional(readOnly = true)
public Page<UserDTO> findAll(Pageable pageable) {
return userRepository.findAll(pageable).map(this::mapToDTO);
}
}
5.2 DTO マッピング
ルール: エンティティから DTO へのマッピングロジックは、一か所にまとめます。
✅ MapStruct を使用する良い例:
@Mapper(componentModel = "spring")
public interface UserMapper {
UserDTO toDTO(User user);
List<UserDTO> toDTOs(List<User> users);
@Mapping(target = "id", ignore = true)
@Mapping(target = "createdAt", ignore = true)
User toEntity(CreateUserRequest request);
}
// Usage in service
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final UserMapper userMapper;
public UserDTO createUser(CreateUserRequest request) {
User user = userMapper.toEntity(request);
User saved = userRepository.save(user);
return userMapper.toDTO(saved);
}
}
✅ MapStruct を使用しない良い例:
@Service
public class UserService {
private final UserRepository userRepository;
private UserDTO mapToDTO(User user) {
UserDTO dto = new UserDTO();
dto.setId(user.getId());
dto.setEmail(user.getEmail());
dto.setName(user.getName());
dto.setCreatedAt(user.getCreatedAt());
dto.setActive(user.isActive());
return dto;
}
private User mapToEntity(CreateUserRequest request) {
User user = new User();
user.setEmail(request.getEmail());
user.setName(request.getName());
return user;
}
}
タスク 6:グローバル例外処理を実装する
6.1 カスタム例外
ルール: ビジネスエラーには、ドメイン固有の例外を作成します。
✅ 良い例:
// Base exception
public abstract class BusinessException extends RuntimeException {
private final String errorCode;
public BusinessException(String message, String errorCode) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
// Specific exceptions
public class ResourceNotFoundException extends BusinessException {
public ResourceNotFoundException(String resourceName, Long id) {
super(String.format("%s with id %d not found", resourceName, id), "RESOURCE_NOT_FOUND");
}
}
public class EmailAlreadyExistsException extends BusinessException {
public EmailAlreadyExistsException(String email) {
super(String.format("Email %s already exists", email), "EMAIL_EXISTS");
}
}
public class InsufficientBalanceException extends BusinessException {
public InsufficientBalanceException(Long accountId) {
super(String.format("Insufficient balance in account %d", accountId), "INSUFFICIENT_BALANCE");
}
}
6.2 @ControllerAdvice によるグローバル例外ハンドラー
ルール: @ControllerAdvice で例外処理を一元化します。
✅ 良い例:
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException ex) {
log.warn("Resource not found: {}", ex.getMessage());
ErrorResponse error = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.NOT_FOUND.value())
.error("Not Found")
.message(ex.getMessage())
.errorCode(ex.getErrorCode())
.build();
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
}
@ExceptionHandler(EmailAlreadyExistsException.class)
public ResponseEntity<ErrorResponse> handleEmailAlreadyExists(EmailAlreadyExistsException ex) {
log.warn("Email already exists: {}", ex.getMessage());
ErrorResponse error = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.CONFLICT.value())
.error("Conflict")
.message(ex.getMessage())
.errorCode(ex.getErrorCode())
.build();
return ResponseEntity.status(HttpStatus.CONFLICT).body(error);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ValidationErrorResponse> handleValidationErrors(MethodArgumentNotValidException ex) {
log.warn("Validation failed: {}", ex.getMessage());
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error ->
errors.put(error.getField(), error.getDefaultMessage())
);
ValidationErrorResponse response = ValidationErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.BAD_REQUEST.value())
.error("Validation Failed")
.message("Invalid request parameters")
.fieldErrors(errors)
.build();
return ResponseEntity.badRequest().body(response);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
log.error("Unexpected error occurred", ex);
ErrorResponse error = ErrorResponse.builder()
.timestamp(LocalDateTime.now())
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.error("Internal Server Error")
.message("An unexpected error occurred")
.build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(error);
}
}
// Error response DTOs
@Data
@Builder
public class ErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private String errorCode;
}
@Data
@Builder
public class ValidationErrorResponse {
private LocalDateTime timestamp;
private int status;
private String error;
private String message;
private Map<String, String> fieldErrors;
}
タスク 7:Spring Security を実装する
7.1 セキュリティ設定
ルール: セキュリティ設定を一元管理するため、セキュリティ設定クラスを使用します。
✅ 良い例(Spring Security 6+):
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/public/**").permitAll()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.requestMatchers("/api/v1/users/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.httpBasic(Customizer.withDefaults())
.formLogin(form -> form.disable())
.logout(logout -> logout
.logoutUrl("/api/v1/auth/logout")
.logoutSuccessHandler((request, response, authentication) ->
response.setStatus(HttpServletResponse.SC_OK))
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
}
7.2 JWT の実装
ルール: REST API のステートレス認証には JWT を実装します。
✅ 良い例:
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration:3600000}") // 1 hour default
private long expiration;
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
claims.put("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.toList());
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public String getUsernameFromToken(String token) {
return getClaimsFromToken(token).getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
return !isTokenExpired(token);
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
private Claims getClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
}
private boolean isTokenExpired(String token) {
Date expiration = getClaimsFromToken(token).getExpiration();
return expiration.before(new Date());
}
}
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = getTokenFromRequest(request);
if (token != null && tokenProvider.validateToken(token)) {
String username = tokenProvider.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getTokenFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
7.3 メソッドセキュリティ
ルール: きめ細かな認可には、メソッドセキュリティを使用します。
✅ 良い例:
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@PreAuthorize("hasRole('ADMIN')")
public List<UserDTO> findAll() {
return userRepository.findAll().stream()
.map(this::mapToDTO)
.toList();
}
@PreAuthorize("hasRole('USER') and #userId == authentication.principal.id")
public UserDTO findById(Long userId) {
return userRepository.findById(userId)
.map(this::mapToDTO)
.orElseThrow(() -> new ResourceNotFoundException("User", userId));
}
@PreAuthorize("hasAnyRole('ADMIN', 'MODERATOR')")
@PostAuthorize("returnObject.email == authentication.principal.username")
public UserDTO updateUser(Long id, UpdateUserRequest request) {
// Implementation
}
@Secured("ROLE_ADMIN")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
}
タスク 8:テスト戦略を実装する
8.1 サービスの単体テスト
ルール: モックした依存関係を使い、サービスを分離してテストします。
✅ 良い例:
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@Mock
private PasswordEncoder passwordEncoder;
@Mock
private EmailService emailService;
@InjectMocks
private UserService userService;
@Test
void createUser_WithValidData_ShouldCreateUser() {
// Arrange
CreateUserRequest request = new CreateUserRequest();
request.setEmail("[email protected]");
request.setPassword("password123");
User user = new User();
user.setId(1L);
user.setEmail(request.getEmail());
when(userRepository.existsByEmail(request.getEmail())).thenReturn(false);
when(passwordEncoder.encode(request.getPassword())).thenReturn("encodedPassword");
when(userRepository.save(any(User.class))).thenReturn(user);
// Act
UserDTO result = userService.createUser(request);
// Assert
assertNotNull(result);
assertEquals(1L, result.getId());
assertEquals("[email protected]", result.getEmail());
verify(userRepository).existsByEmail(request.getEmail());
verify(passwordEncoder).encode(request.getPassword());
verify(userRepository).save(any(User.class));
verify(emailService).sendWelcomeEmail(any(User.class));
}
@Test
void createUser_WithExistingEmail_ShouldThrowException() {
// Arrange
CreateUserRequest request = new CreateUserRequest();
request.setEmail("[email protected]");
when(userRepository.existsByEmail(request.getEmail())).thenReturn(true);
// Act & Assert
assertThrows(EmailAlreadyExistsException.class, () ->
userService.createUser(request));
verify(userRepository).existsByEmail(request.getEmail());
verify(userRepository, never()).save(any(User.class));
}
}
8.2 @SpringBootTest による統合テスト
ルール: 完全なコンテキストが必要な統合テストには、@SpringBootTest を使用します。
✅ 良い例:
@SpringBootTest
@AutoConfigureMockMvc
@Transactional // Rollback after each test
class UserControllerIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private UserRepository userRepository;
@Autowired
private ObjectMapper objectMapper;
@BeforeEach
void setUp() {
userRepository.deleteAll();
}
@Test
void createUser_WithValidData_ShouldReturn201() throws Exception {
// Arrange
CreateUserRequest request = new CreateUserRequest();
request.setEmail("[email protected]");
request.setName("Test User");
request.setPassword("password123");
// Act & Assert
mockMvc.perform(post("/api/v1/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andExpect(header().exists("Location"))
.andExpect(jsonPath("$.email").value("[email protected]"))
.andExpect(jsonPath("$.name").value("Test User"))
.andExpect(jsonPath("$.id").exists());
// Verify database
assertEquals(1, userRepository.count());
}
@Test
void createUser_WithInvalidEmail_ShouldReturn400() throws Exception {
// Arrange
CreateUserRequest request = new CreateUserRequest();
request.setEmail("invalid-email");
request.setName("Test User");
request.setPassword("password123");
// Act & Assert
mockMvc.perform(post("/api/v1/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.fieldErrors.email").exists());
}
}
8.3 @DataJpaTest によるリポジトリーのテスト
ルール: インメモリデータベースを使用するリポジトリーテストには、@DataJpaTest を使用します。
✅ 良い例:
@DataJpaTest
class UserRepositoryTest {
@Autowired
private UserRepository userRepository;
@Autowired
private TestEntityManager entityManager;
@Test
void findByEmail_WithExistingEmail_ShouldReturnUser() {
// Arrange
User user = new User();
user.setEmail("[email protected]");
user.setName("Test User");
entityManager.persist(user);
entityManager.flush();
// Act
Optional<User> found = userRepository.findByEmail("[email protected]");
// Assert
assertTrue(found.isPresent());
assertEquals("[email protected]", found.get().getEmail());
}
@Test
void findByEmail_WithNonExistingEmail_ShouldReturnEmpty() {
// Act
Optional<User> found = userRepository.findByEmail("[email protected]");
// Assert
assertFalse(found.isPresent());
}
}
8.4 @WebMvcTest によるコントローラーのテスト
ルール: 完全なコンテキストを必要としないコントローラーテストには、@WebMvcTest を使用します。
✅ 良い例:
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Autowired
private ObjectMapper objectMapper;
@Test
void getUserById_WithExistingId_ShouldReturnUser() throws Exception {
// Arrange
UserDTO user = new UserDTO();
user.setId(1L);
user.setEmail("[email protected]");
when(userService.findById(1L)).thenReturn(Optional.of(user));
// Act & Assert
mockMvc.perform(get("/api/v1/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.email").value("[email protected]"));
verify(userService).findById(1L);
}
@Test
void getUserById_WithNonExistingId_ShouldReturn404() throws Exception {
// Arrange
when(userService.findById(999L)).thenReturn(Optional.empty());
// Act & Assert
mockMvc.perform(get("/api/v1/users/999"))
.andExpect(status().isNotFound());
}
}
タスク 9:設定管理を実装する
9.1 外部設定
ルール: 設定には application.yml を使用し、複数のプロファイルに対応します。
✅ 良い application.yml:
spring:
application:
name: my-spring-app
profiles:
active: dev
datasource:
url: ${DB_URL:jdbc:postgresql://localhost:5432/myapp}
username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:password}
hikari:
maximum-pool-size: 10
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
jpa:
hibernate:
ddl-auto: validate
show-sql: false
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.PostgreSQLDialect
cache:
type: caffeine
caffeine:
spec: maximumSize=500,expireAfterAccess=600s
server:
port: 8080
error:
include-message: always
include-binding-errors: always
include-stacktrace: on_param
include-exception: false
logging:
level:
root: INFO
com.example.app: DEBUG
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss} - %msg%n"
# Custom application properties
app:
jwt:
secret: ${JWT_SECRET:default-secret-change-in-production}
expiration: 3600000
email:
from: [email protected]
features:
new-ui: false
application-dev.yml:
spring:
jpa:
show-sql: true
hibernate:
ddl-auto: update
logging:
level:
com.example.app: DEBUG
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
app:
features:
new-ui: true
application-prod.yml:
spring:
jpa:
show-sql: false
hibernate:
ddl-auto: validate
logging:
level:
root: WARN
com.example.app: INFO
app:
jwt:
expiration: 7200000 # 2 hours in production
9.2 @ConfigurationProperties
ルール: 型安全な設定には、@ConfigurationProperties を使用します。
✅ 良い例:
@ConfigurationProperties(prefix = "app")
@Validated
public class AppProperties {
@NotNull
private Jwt jwt;
@NotNull
private Email email;
@NotNull
private Features features;
@Data
public static class Jwt {
@NotBlank
private String secret;
@Min(60000) // At least 1 minute
private long expiration;
}
@Data
public static class Email {
@Email
private String from;
}
@Data
public static class Features {
private boolean newUi;
}
// Getters and setters
}
// Enable configuration properties
@Configuration
@EnableConfigurationProperties(AppProperties.class)
public class AppConfig {
}
// Usage
@Service
@RequiredArgsConstructor
public class EmailService {
private final AppProperties appProperties;
public void sendEmail(String to, String subject, String body) {
String from = appProperties.getEmail().getFrom();
// Send email logic
}
}
タスク 10:キャッシュを実装する
10.1 キャッシュを有効にする
ルール: 宣言的なキャッシュには、Spring Cache 抽象化を使用します。
✅ 良い例:
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager(
"users", "products", "orders"
);
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.recordStats());
return cacheManager;
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Cacheable(value = "users", key = "#id")
public Optional<UserDTO> findById(Long id) {
return userRepository.findById(id)
.map(this::mapToDTO);
}
@CachePut(value = "users", key = "#result.id")
public UserDTO updateUser(Long id, UpdateUserRequest request) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", id));
user.setName(request.getName());
User updated = userRepository.save(user);
return mapToDTO(updated);
}
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
@CacheEvict(value = "users", allEntries = true)
public void clearUserCache() {
// Cache cleared
}
}
10.2 キャッシュキーの戦略
ルール: 複雑なケースでは、明示的なキャッシュキーを使用します。
✅ 良い例:
@Service
public class ProductService {
// Simple key
@Cacheable(value = "products", key = "#id")
public Product findById(Long id) {
// ...
}
// Composite key
@Cacheable(value = "products", key = "#category + '-' + #priceRange")
public List<Product> findByCategoryAndPrice(String category, String priceRange) {
// ...
}
// Custom KeyGenerator
@Cacheable(value = "products", keyGenerator = "customKeyGenerator")
public List<Product> search(ProductSearchCriteria criteria) {
// ...
}
}
@Component("customKeyGenerator")
public class CustomKeyGenerator implements org.springframework.cache.interceptor.KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return target.getClass().getSimpleName() + "_"
+ method.getName() + "_"
+ Arrays.stream(params).map(String::valueOf).collect(Collectors.joining("_"));
}
}
タスク 11:非同期処理を実装する
11.1 非同期処理を有効にする
ルール: ブロッキングしない操作には、@Async を使用します。
✅ 良い例:
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-");
executor.initialize();
return executor;
}
}
@Service
@RequiredArgsConstructor
public class EmailService {
@Async("taskExecutor")
public void sendWelcomeEmail(User user) {
// Runs asynchronously
System.out.println("Sending email to: " + user.getEmail());
// Email sending logic
}
@Async("taskExecutor")
public CompletableFuture<EmailStatus> sendEmailWithResult(String to, String subject, String body) {
// Async with result
try {
// Send email
return CompletableFuture.completedFuture(EmailStatus.SENT);
} catch (Exception e) {
return CompletableFuture.completedFuture(EmailStatus.FAILED);
}
}
}
11.2 イベント駆動アーキテクチャ
ルール: コンポーネント間の結合を弱めるには、Spring Events を使用します。
✅ 良い例:
// Event
public class UserRegisteredEvent {
private final User user;
public UserRegisteredEvent(User user) {
this.user = user;
}
public User getUser() {
return user;
}
}
// Publisher
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final ApplicationEventPublisher eventPublisher;
@Transactional
public UserDTO register(RegistrationRequest request) {
User user = new User();
user.setEmail(request.getEmail());
User saved = userRepository.save(user);
// Publish event
eventPublisher.publishEvent(new UserRegisteredEvent(saved));
return mapToDTO(saved);
}
}
// Listeners
@Component
@Slf4j
public class UserRegistrationEventListener {
@Async
@EventListener
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void handleUserRegistered(UserRegisteredEvent event) {
log.info("User registered: {}", event.getUser().getEmail());
// Send welcome email
}
}
@Component
public class AuditEventListener {
@EventListener
public void handleUserRegistered(UserRegisteredEvent event) {
// Log to audit trail
}
}
タスク 12:AOP を実装する
12.1 ロギングアスペクト
ルール: ロギング、監査、パフォーマンス監視などの横断的関心事には、AOP を使用します。
✅ 良い例:
@Aspect
@Component
@Slf4j
public class LoggingAspect {
@Around("execution(* com.example.app.service.*.*(..))")
public Object logServiceMethods(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
log.debug("Entering {}.{}", className, methodName);
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long duration = System.currentTimeMillis() - startTime;
log.debug("Exiting {}.{} - Duration: {}ms", className, methodName, duration);
return result;
} catch (Exception e) {
log.error("Exception in {}.{}: {}", className, methodName, e.getMessage());
throw e;
}
}
@Before("@annotation(com.example.app.annotation.Audit)")
public void auditMethod(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
log.info("Audit: Method {} called with args: {}", methodName, Arrays.toString(args));
}
}
// Custom annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Audit {
}
// Usage
@Service
public class UserService {
@Audit
public UserDTO createUser(CreateUserRequest request) {
// Method will be audited
}
}
よくあるアンチパターン
アンチパターン 1:フィールドインジェクション
❌ 悪い例:
@Service
public class UserService {
@Autowired
private UserRepository userRepository; // Field injection
}
✅ 良い例:
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository; // Constructor injection
}
アンチパターン 2:サービスレイヤーのバイパス
❌ 悪い例:
@RestController
public class UserController {
@Autowired
private UserRepository userRepository; // Controller accessing repository directly
@GetMapping("/users")
public List<User> getUsers() {
return userRepository.findAll(); // Business logic in controller
}
}
✅ 良い例:
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService; // Access through service layer
@GetMapping("/users")
public ResponseEntity<List<UserDTO>> getUsers() {
return ResponseEntity.ok(userService.findAll());
}
}
アンチパターン 3:エンティティを直接公開する
❌ 悪い例:
@RestController
public class UserController {
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
return userService.findById(id); // Exposing entity
}
}
✅ 良い例:
@RestController
public class UserController {
@GetMapping("/users/{id}")
public ResponseEntity<UserDTO> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.findById(id)); // Return DTO
}
}
アンチパターン 4:リポジトリメソッドに @Transactional を付ける
❌ 悪い例:
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Transactional // Don't put @Transactional on repository
@Query("UPDATE User u SET u.active = false WHERE u.id = :id")
void deactivate(@Param("id") Long id);
}
✅ 良い例:
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
@Transactional // Put @Transactional on service methods
public void deactivateUser(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("User", id));
user.setActive(false);
userRepository.save(user);
}
}
アンチパターン 5:コネクションプーリングを使用しない
❌ 悪い例:
# No connection pooling configuration
spring:
datasource:
url: jdbc:postgresql://localhost:5432/myapp
✅ 良い例:
spring:
datasource:
url: jdbc:postgresql://localhost:5432/myapp
hikari:
maximum-pool-size: 10
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
アンチパターン 6:コントローラーに責務を持たせすぎる
❌ 悪い例:
@RestController
public class UserController {
@PostMapping("/users")
public User createUser(@RequestBody User user) {
// Validation in controller
if (user.getEmail() == null) {
throw new RuntimeException("Email required");
}
// Business logic in controller
if (userRepository.existsByEmail(user.getEmail())) {
throw new RuntimeException("Email exists");
}
// Data access in controller
User saved = userRepository.save(user);
// Email sending in controller
emailService.send(user.getEmail(), "Welcome!");
return saved;
}
}
✅ 良い例:
@RestController
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@PostMapping("/users")
public ResponseEntity<UserDTO> createUser(@Valid @RequestBody CreateUserRequest request) {
UserDTO created = userService.createUser(request);
return ResponseEntity.created(location).body(created);
}
}
@Service
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
private final EmailService emailService;
@Transactional
public UserDTO createUser(CreateUserRequest request) {
// All business logic in service
validateEmail(request.getEmail());
User user = createUserEntity(request);
User saved = userRepository.save(user);
emailService.sendWelcomeEmail(saved);
return mapToDTO(saved);
}
}
アンチパターン 7:ビジネスロジックをエンティティに格納する
❌ 悪い例:
@Entity
public class Order {
@Autowired // Don't inject dependencies in entities!
private PaymentService paymentService;
public void process() {
// Business logic in entity - BAD!
if (this.total > 1000) {
paymentService.processLargeOrder(this);
} else {
paymentService.processSmallOrder(this);
}
}
}
✅ 良い例:
@Entity
public class Order {
// Pure data model - no business logic
private Long id;
private BigDecimal total;
private OrderStatus status;
// Getters and setters
}
@Service
@RequiredArgsConstructor
public class OrderService {
private final PaymentService paymentService;
private final OrderRepository orderRepository;
@Transactional
public void processOrder(Long orderId) {
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new ResourceNotFoundException("Order", orderId));
if (order.getTotal().compareTo(BigDecimal.valueOf(1000)) > 0) {
paymentService.processLargeOrder(order);
} else {
paymentService.processSmallOrder(order);
}
}
}
チェックリスト
コードレビューでは、次のチェックリストを使用してください。
依存性注入
- 必須の依存関係にはコンストラクターインジェクションを使用している
- 可能な場合、フィールドを final にしている
- フィールドインジェクションを使用していない(フィールドへの @Autowired)
- 循環依存がない
- セッターインジェクションはオプションの依存関係にのみ使用している
コンポーネント構成
- 適切なステレオタイプアノテーションを使用している(@Service、@Repository、@Controller、@RestController)
- コントローラーが薄く、サービスに処理を委譲している
- サービスレイヤーにビジネスロジックがある
- リポジトリ経由でのみデータアクセスを行っている
- 適切なレイヤー構造になっている(Controller → Service → Repository)
REST API 設計
- RESTful な URL 構造になっている
- 適切な HTTP メソッドを使用している(GET、POST、PUT、DELETE)
- 適切な HTTP ステータスコードを使用している
- リクエスト/レスポンスに DTO を使用している(エンティティは使用しない)
- リクエストのバリデーションに @Valid を使用している
- リストを返すエンドポイントでページネーションに対応している
- 作成したリソースに Location ヘッダーを設定している
データアクセス
- 適切なリポジトリインターフェースを使用している(JpaRepository など)
- クエリメソッドが Spring Data の命名規則に従っている
- @Transactional をリポジトリではなくサービスメソッドに付けている
- 読み取り操作では readOnly=true を設定している
- コネクションプーリングを設定している(HikariCP)
- N+1 クエリ問題がない
設定
- 外部設定を application.yml に記述している
- プロファイル別の設定ファイルを用意している
- 型安全な設定に @ConfigurationProperties を使用している
- 機密データを環境変数に格納している
- すべてのプロパティに適切なデフォルト値を設定している
例外処理
- ビジネスエラーにカスタム例外を使用している
- グローバルな例外処理に @RestControllerAdvice を使用している
- 例外の種類に応じて適切な HTTP ステータスコードを返している
- バリデーションエラーを適切に処理している
- 再スローせずに汎用的な Exception を捕捉していない
セキュリティ
- @Configuration クラスでセキュリティを設定している
- パスワードエンコーダーを設定している(BCrypt)
- 必要な箇所でメソッドセキュリティを有効にしている
- CSRF 保護を有効にしている(または、無効にする理由を明記している)
- ステートレス認証に JWT を適切に実装している
テスト
- モックした依存関係を使用してサービスのユニットテストを実施している
- @SpringBootTest を使用して統合テストを実施している
- @DataJpaTest を使用してリポジトリのテストを実施している
- @WebMvcTest を使用してコントローラーのテストを実施している
- テストカバレッジが 80% を超えている
パフォーマンス
- 適切な箇所でキャッシュを有効にしている
- 時間のかかる処理に非同期処理を使用している
- 遅延読み込みを適切に設定している
- 頻繁にクエリされるフィールドにデータベースインデックスを設定している
コード品質
- コードの重複がない
- クラス、メソッド、変数に意味のある名前を付けている
- メソッドの目的が明確で、長すぎない
- 適切なロギングを行っている(System.out.println は使用しない)
- コメントアウトされたコードがない
関連スキル
- uncle-duke-java:このスキルを参考にする Java コードレビューエージェント
参考資料
公式ドキュメント
ベストプラクティスガイド
バージョン: 1.0 最終更新日: 2025-12-24 メンテナー: 開発チーム