目次
一般原則
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.*
) を避ける - 未使用のインポートを削除
- 以下の順序でグループ化:
- Java標準ライブラリ
- サードパーティライブラリ(Spring含む)
- プロジェクト内のクラス
- 各グループ内ではアルファベット順に並べる
3.7 クラス構造
- 以下の順序で記述:
- 静的変数
- インスタンス変数
- コンストラクタ
- メソッド(公開メソッドから始めて、関連するメソッドをグループ化)
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などでモニタリング
参考資料
- Oracle Java コーディング規約
- Spring Framework リファレンスドキュメント
- Spring Boot リファレンスドキュメント
- Clean Code by Robert C. Martin
- Effective Java by Joshua Bloch