0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Java および Spring Boot コーディング規約

Last updated at Posted at 2025-03-08

目次

  1. 一般原則
  2. 命名規則
  3. コード構造とフォーマット
  4. コメントとドキュメント
  5. 例外処理
  6. Spring Boot 特有のガイドライン
  7. パッケージ構造
  8. テスト
  9. セキュリティ対策
  10. パフォーマンス最適化

一般原則

1.1 SOLID原則の遵守

  • 単一責任の原則(SRP): クラスは単一の責任を持つべき
  • オープン・クローズドの原則(OCP): 拡張に対してオープンで修正に対してクローズド
  • リスコフの置換原則(LSP): サブタイプはその基本型と置換可能であるべき
  • インターフェイス分離の原則(ISP): クライアントは使用しないインターフェイスに依存すべきでない
  • 依存性逆転の原則(DIP): 上位モジュールは下位モジュールに依存すべきでない。両方とも抽象に依存すべき

1.2 設計原則

  • DRY (Don't Repeat Yourself): コードの重複を避ける
  • KISS (Keep It Simple, Stupid): シンプルさを保つ
  • YAGNI (You Aren't Gonna Need It): 必要になるまで機能を追加しない
  • 明示的なコードを書く: 暗黙の動作や魔法のような振る舞いを避ける

1.3 可読性とメンテナンス性

  • 自己説明的なコード: コードを読むだけで何をしているかわかる
  • 一貫性: 一貫したスタイルとパターンを使用
  • 適切な抽象化: 適切なレベルの抽象化を選択

命名規則

2.1 一般ガイドライン

  • 全ての識別子は英語で記述
  • 省略形の使用を最小限に抑える(一般的に知られているものを除く:HTMLなど)
  • 意図を明確に表現する名前を選択

2.2 クラス名

  • パスカルケース (PascalCase) を使用
  • 名詞または名詞句を使用
  • インターフェースはプレフィックス「I」を付けない
  • 例: UserService, OrderProcessor, PaymentController

2.3 変数名

  • キャメルケース (camelCase) を使用
  • 意味のある名前を選択
  • 例: userId, orderCount, isActive

2.4 定数

  • スネークケース + 大文字 (UPPER_SNAKE_CASE) を使用
  • 例: MAX_RETRY_COUNT, DEFAULT_TIMEOUT

2.5 メソッド名

  • キャメルケース (camelCase) を使用
  • 動詞または動詞句で始める
  • 例: findUserById(), processPayment(), validateInput()

2.6 パッケージ名

  • 全て小文字
  • ドット区切りの階層構造
  • 例: com.company.project.module

2.7 Spring Boot 特有の命名

2.7.1 コンポーネント

  • Controller: サフィックスとして使用(例: UserController
  • Service: サフィックスとして使用(例: UserService
  • Repository: サフィックスとして使用(例: UserRepository
  • Config: サフィックスとして使用(例: SecurityConfig

2.7.2 その他のBean

  • Impl: 実装クラスのサフィックスとして使用しない(例: DefaultUserService の方が UserServiceImpl より好ましい)
  • Factory: 適切な場所でファクトリークラスに使用(例: UserFactory

2.7.3 テスト

  • テストクラスは Test サフィックスを使用
  • 例: UserServiceTest, OrderControllerTest

コード構造とフォーマット

3.1 インデント

  • 空白4文字を使用(タブではなく)
  • 行の継続の場合は8文字インデント

3.2 行の長さ

  • 120文字を超えないようにする
  • 長い行は適切に分割する

3.3 改行

  • 各ステートメントは新しい行に記述
  • メソッド間は1行空ける
  • 関連するコードブロック間は1行空ける
  • クラス、インターフェース間は2行空ける

3.4 括弧

  • 開き括弧は前の行の末尾に配置
  • 閉じ括弧は独立した行に配置
  • if, for などの制御構造でも必ず括弧を使用(単一行でも)
// 良い例
if (condition) {
    doSomething();
}

// 悪い例
if (condition) doSomething();

3.5 空白

  • 二項演算子の前後に空白を入れる
  • カンマ、セミコロンの後に空白を入れる
  • メソッド名と開き括弧の間に空白を入れない
  • キャスト式の括弧内に空白を入れない
// 良い例
int x = y + z;
method(a, b, c);
for (int i = 0; i < 10; i++) {

// 悪い例
int x=y+z;
method(a,b,c);
for(int i=0;i<10;i++){

3.6 imports

  • ワイルドカードインポート (import java.util.*) を避ける
  • 未使用のインポートを削除
  • 以下の順序でグループ化:
    1. Java標準ライブラリ
    2. サードパーティライブラリ(Spring含む)
    3. プロジェクト内のクラス
  • 各グループ内ではアルファベット順に並べる

3.7 クラス構造

  • 以下の順序で記述:
    1. 静的変数
    2. インスタンス変数
    3. コンストラクタ
    4. メソッド(公開メソッドから始めて、関連するメソッドをグループ化)

3.8 ラムダ式とStream API

  • 短いラムダ式は1行で記述
  • 長いラムダ式は複数行に分割
  • Stream操作は適切に改行
// 良い例 - 短いラムダ
list.forEach(item -> System.out.println(item));

// 良い例 - 長いラムダ
list.forEach(item -> {
    String processed = processItem(item);
    System.out.println(processed);
    logProcessing(item, processed);
});

// 良い例 - Stream操作
return users.stream()
        .filter(user -> user.isActive())
        .map(User::getName)
        .collect(Collectors.toList());

コメントとドキュメント

4.1 Javadoc

  • すべての公開API(publicクラス、メソッド)にJavadocを付ける
  • Javadocは「何を」するかを説明し、「どのように」するかの詳細は必要に応じて
  • パラメータ、戻り値、例外をすべて文書化
/**
 * ユーザーIDに基づいてユーザーを検索します。
 *
 * @param userId 検索するユーザーの一意識別子
 * @return 見つかったユーザー、存在しない場合はnull
 * @throws IllegalArgumentException userIdがnullまたは空の場合
 */
public User findUserById(String userId) {
    // 実装
}

4.2 実装コメント

  • 複雑なロジックや非自明な実装にのみコメントを付ける
  • TODOコメントには担当者と期限を含める
// TODO(yamada): このロジックをリファクタリングする - 2023/12/31まで

4.3 不要なコメント

  • 自明なコードにコメントを付けない
  • コメントとコードの同期を保つ(古くなったコメントを避ける)

例外処理

5.1 例外の選択

  • 回復可能な状況には検査例外 (checked exception) を使用
  • プログラミングエラーには非検査例外 (unchecked exception) を使用
  • 独自例外クラスを適切に定義(特に業務ロジックエラー用)

5.2 例外処理のベストプラクティス

  • 例外を無視しない(空のcatchブロックを避ける)
  • 例外を適切な抽象化レベルに変換(低レベルの例外を上位レベルにラップ)
  • finally ブロックでリソースを確実に閉じる(または try-with-resources を使用)
// 良い例
try (Connection conn = dataSource.getConnection()) {
    // データベース操作
} catch (SQLException e) {
    throw new DatabaseException("データの取得に失敗しました", e);
}

// 悪い例
try {
    // 何らかの操作
} catch (Exception e) {
    // 何もしない
}

5.3 Spring Boot での例外処理

  • @ExceptionHandler@ControllerAdvice を使用して一元的に例外を処理
  • REST APIでは適切なHTTPステータスコードを返す
  • 例外メッセージはユーザーフレンドリーで、かつセキュリティを考慮したものにする

Spring Boot 特有のガイドライン

6.1 アプリケーション設定

  • 環境固有の設定は外部化(application-{profile}.properties または YAML)
  • 機密情報は環境変数または安全な方法で管理
  • @ConfigurationProperties を活用して設定をタイプセーフに
@Configuration
@ConfigurationProperties(prefix = "app.mail")
public class MailProperties {
    private String host;
    private int port;
    private String username;
    private String password;
    
    // getterとsetter
}

6.2 依存性注入

  • コンストラクタ注入を優先(フィールド注入より)
  • 複数のコンストラクタがある場合は @Autowired を明示
  • 循環依存を避ける
// 良い例
@Service
public class UserServiceImpl implements UserService {
    private final UserRepository userRepository;
    private final EmailService emailService;

    public UserServiceImpl(UserRepository userRepository, EmailService emailService) {
        this.userRepository = userRepository;
        this.emailService = emailService;
    }
    
    // メソッド実装
}

// 悪い例
@Service
public class UserServiceImpl implements UserService {
    @Autowired
    private UserRepository userRepository;
    
    @Autowired
    private EmailService emailService;
    
    // メソッド実装
}

6.3 RESTfulコントローラ

  • URI設計は名詞ベース、適切な階層構造
  • HTTPメソッドを正しく使用(GET、POST、PUT、DELETE)
  • 適切なHTTPステータスコードを返す
  • バージョニングを考慮
@RestController
@RequestMapping("/api/v1/users")
public class UserController {
    private final UserService userService;
    
    public UserController(UserService userService) {
        this.userService = userService;
    }
    
    @GetMapping("/{id}")
    public ResponseEntity<UserDto> getUser(@PathVariable Long id) {
        UserDto user = userService.findById(id);
        return ResponseEntity.ok(user);
    }
    
    @PostMapping
    public ResponseEntity<UserDto> createUser(@Valid @RequestBody UserCreateDto userDto) {
        UserDto created = userService.create(userDto);
        URI location = ServletUriComponentsBuilder
                .fromCurrentRequest()
                .path("/{id}")
                .buildAndExpand(created.getId())
                .toUri();
        return ResponseEntity.created(location).body(created);
    }
    
    // 他のエンドポイント
}

6.4 データアクセス

  • リポジトリインターフェースには Spring Data JPA を活用
  • 複雑なクエリには @Query アノテーションを使用
  • ページネーションを適切に実装
  • N+1問題を避けるためJOIN FETCHを適切に使用
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByEmail(String email);
    
    @Query("SELECT u FROM User u WHERE u.status = :status")
    List<User> findByStatus(@Param("status") UserStatus status);
    
    @Query("SELECT u FROM User u JOIN FETCH u.roles WHERE u.id = :id")
    Optional<User> findByIdWithRoles(@Param("id") Long id);
}

6.5 サービス層

  • トランザクション境界を適切に設定
  • ビジネスロジックをここに集中
  • ドメインオブジェクトとDTOの変換を管理
@Service
@Transactional(readOnly = true)
public class OrderServiceImpl implements OrderService {
    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    
    // コンストラクタ注入
    
    @Override
    @Transactional
    public OrderDto createOrder(OrderCreateDto orderDto) {
        // ビジネスロジック
        // バリデーション
        // リポジトリ呼び出し
        // DTOへの変換
    }
    
    // 他のメソッド
}

6.6 バリデーション

  • Bean Validationを活用
  • カスタムバリデーションには @Validated@Valid を使用
  • コントローラーでのリクエストバリデーションを徹底
public class UserCreateDto {
    @NotBlank(message = "ユーザー名は必須です")
    @Size(min = 3, max = 50, message = "ユーザー名は3〜50文字である必要があります")
    private String username;
    
    @NotBlank(message = "メールアドレスは必須です")
    @Email(message = "有効なメールアドレスを入力してください")
    private String email;
    
    @NotBlank(message = "パスワードは必須です")
    @Pattern(regexp = "^(?=.*[0-9])(?=.*[a-z])(?=.*[A-Z])(?=.*[@#$%^&+=])(?=\\S+$).{8,}$",
            message = "パスワードは最低8文字で、少なくとも1つの数字、大文字、小文字、特殊文字を含む必要があります")
    private String password;
    
    // getterとsetter
}

パッケージ構造

7.1 レイヤードアーキテクチャ

com.company.project
  ├── config        // アプリケーション設定
  ├── controller    // RESTコントローラー
  ├── dto           // データ転送オブジェクト
  ├── entity        // JPA エンティティ
  ├── repository    // データアクセス層
  ├── service       // ビジネスロジック層
  ├── exception     // カスタム例外
  ├── util          // ユーティリティクラス
  └── Application.java  // メインクラス

7.2 モジュラーモノリス(機能別)

com.company.project
  ├── common        // 共通機能
  │   ├── config
  │   ├── exception
  │   └── util
  ├── user          // ユーザー管理機能
  │   ├── controller
  │   ├── dto
  │   ├── entity
  │   ├── repository
  │   └── service
  ├── order         // 注文管理機能
  │   ├── controller
  │   ├── dto
  │   ├── entity
  │   ├── repository
  │   └── service
  └── Application.java

7.3 ドメイン駆動設計(DDD)

com.company.project
  ├── application           // アプリケーションサービス
  ├── domain                // ドメインモデル
  │   ├── model
  │   ├── service
  │   ├── repository        // リポジトリインターフェース
  │   └── event
  ├── infrastructure        // 技術的な実装
  │   ├── config
  │   ├── persistence
  │   └── messaging
  ├── interfaces            // ユーザーインターフェース
  │   ├── rest
  │   ├── web
  │   └── dto
  └── Application.java

テスト

8.1 テスト構造

  • 本番コードと同じパッケージ構造を使用(testディレクトリ内)
  • テストクラス名は対象クラス名 + Test サフィックス

8.2 単体テスト

  • JUnitとMockitoを使用
  • 各メソッドに対して複数のテストケース(正常系、異常系)
  • Given-When-Thenパターンを使用
@ExtendWith(MockitoExtension.class)
public class UserServiceTest {
    @Mock
    private UserRepository userRepository;
    
    @InjectMocks
    private UserServiceImpl userService;
    
    @Test
    void findById_UserExists_ReturnsUser() {
        // Given
        User user = new User();
        user.setId(1L);
        user.setUsername("testuser");
        when(userRepository.findById(1L)).thenReturn(Optional.of(user));
        
        // When
        UserDto result = userService.findById(1L);
        
        // Then
        assertNotNull(result);
        assertEquals(1L, result.getId());
        assertEquals("testuser", result.getUsername());
        verify(userRepository).findById(1L);
    }
    
    @Test
    void findById_UserNotExists_ThrowsException() {
        // Given
        when(userRepository.findById(1L)).thenReturn(Optional.empty());
        
        // When & Then
        assertThrows(EntityNotFoundException.class, () -> userService.findById(1L));
        verify(userRepository).findById(1L);
    }
}

8.3 統合テスト

  • @SpringBootTest を使用
  • テスト用のプロファイルと設定
  • TestContainersを活用してデータベースなどを統合テスト
@SpringBootTest
@ActiveProfiles("test")
public class UserRepositoryIntegrationTest {
    @Autowired
    private UserRepository userRepository;
    
    @Test
    void findByEmail_UserExists_ReturnsUser() {
        // Given
        User user = new User();
        user.setUsername("testuser");
        user.setEmail("test@example.com");
        userRepository.save(user);
        
        // When
        Optional<User> result = userRepository.findByEmail("test@example.com");
        
        // Then
        assertTrue(result.isPresent());
        assertEquals("testuser", result.get().getUsername());
    }
}

8.4 APIテスト

  • MockMvcを使用
  • HTTPリクエスト/レスポンスをテスト
  • JSONアサーションを活用
@WebMvcTest(UserController.class)
public class UserControllerTest {
    @Autowired
    private MockMvc mockMvc;
    
    @MockBean
    private UserService userService;
    
    @Test
    void getUser_UserExists_ReturnsUser() throws Exception {
        // Given
        UserDto userDto = new UserDto(1L, "testuser", "test@example.com");
        when(userService.findById(1L)).thenReturn(userDto);
        
        // When & Then
        mockMvc.perform(get("/api/v1/users/{id}", 1L))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(1L))
                .andExpect(jsonPath("$.username").value("testuser"))
                .andExpect(jsonPath("$.email").value("test@example.com"));
        
        verify(userService).findById(1L);
    }
}

セキュリティ対策

9.1 認証と認可

  • Spring Securityを適切に設定
  • JWTまたはOAuth2を適切に実装
  • 権限ベースのアクセス制御を実装
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    private final JwtTokenProvider jwtTokenProvider;
    
    public SecurityConfig(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .csrf().disable()
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                .antMatchers("/api/v1/auth/**").permitAll()
                .antMatchers("/api/v1/users/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
                .apply(new JwtConfigurer(jwtTokenProvider))
                .and()
                .build();
    }
    
    // 他の設定
}

9.2 データバリデーション

  • すべてのユーザー入力を検証
  • XSS、SQLインジェクションなどを防止
  • 出力エンコーディングを適用

9.3 機密情報の管理

  • パスワードはBCryptなどで適切にハッシュ化
  • APIキーなどの機密情報は適切に保護
  • ログに機密情報を出力しない

パフォーマンス最適化

10.1 データベースアクセス

  • インデックスを適切に設定
  • N+1問題を回避
  • ページネーションを実装
  • 大量データ処理にはバッチ処理を使用

10.2 キャッシング

  • Spring Cacheを活用
  • 適切なキャッシュ戦略を選択
  • TTL(Time to Live)を適切に設定
@Service
public class ProductServiceImpl implements ProductService {
    private final ProductRepository productRepository;
    
    public ProductServiceImpl(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }
    
    @Override
    @Cacheable(value = "products", key = "#id")
    public ProductDto findById(Long id) {
        return productRepository.findById(id)
                .map(this::convertToDto)
                .orElseThrow(() -> new EntityNotFoundException("Product not found"));
    }
    
    @Override
    @CacheEvict(value = "products", key = "#productDto.id")
    public ProductDto update(ProductDto productDto) {
        // 更新ロジック
    }
    
    // 他のメソッド
}

10.3 非同期処理

  • @Async アノテーションを活用
  • WebFluxを検討(適切な場合)
  • スレッドプールを適切に設定
@Service
public class EmailServiceImpl implements EmailService {
    private final JavaMailSender mailSender;
    
    public EmailServiceImpl(JavaMailSender mailSender) {
        this.mailSender = mailSender;
    }
    
    @Async
    @Override
    public CompletableFuture<Void> sendEmail(String to, String subject, String content) {
        MimeMessage message = mailSender.createMimeMessage();
        try {
            MimeMessageHelper helper = new MimeMessageHelper(message, true);
            helper.setTo(to);
            helper.setSubject(subject);
            helper.setText(content, true);
            mailSender.send(message);
            return CompletableFuture.completedFuture(null);
        } catch (MessagingException e) {
            return CompletableFuture.failedFuture(new EmailSendException("Failed to send email", e));
        }
    }
}

10.4 アプリケーションプロファイリング

  • Spring Boot Actuator を活用
  • Meterレジストリでメトリクスを収集
  • Prometheusなどでモニタリング

参考資料

  1. Oracle Java コーディング規約
  2. Spring Framework リファレンスドキュメント
  3. Spring Boot リファレンスドキュメント
  4. Clean Code by Robert C. Martin
  5. Effective Java by Joshua Bloch
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?