Spring Boot面接質問30選: Java開発者のための完全ガイド

auto-configuration、スターター、Spring Data JPA、セキュリティ、テストを網羅した30問でSpring Boot面接に備えます。

Java開発者向けSpring Boot面接質問30選

Spring Bootは、エンタープライズJava開発のデファクトスタンダードです。技術面接では、内部の仕組み、ベストプラクティス、Springエコシステムへの理解が問われます。本ガイドでは、基礎概念から発展的なトピックまで、頻出の30問をまとめます。

準備のヒント

面接官は、各機能の「なぜ」を理解している候補者を高く評価します。文法だけでなく、Spring Bootが解決する課題まで説明しましょう。

Spring Bootの基礎

1. SpringとSpring Bootの違いは何ですか?

Springは、依存性注入、トランザクション管理、多数の技術との統合を提供するモジュール型フレームワークです。Spring Bootは、その上に位置する抽象レイヤーで、設定とアプリケーション起動を簡略化します。

Application.javajava
// With Spring Boot: a single annotation to start
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        // SpringApplication configures and starts the Spring context
        SpringApplication.run(Application.class, args);
    }
}

Spring Bootはauto-configuration、依存スターター、組み込みサーバー、Actuatorを通じた本番メトリクスを提供します。狙いは、アイデアから動くコードまでを数分で実現することです。

2. auto-configurationはどのように動作しますか?

auto-configurationはクラスパスを解析し、必要なBeanを自動的に構成します。中核は@EnableAutoConfigurationアノテーション(@SpringBootApplicationに内包)と、auto-configurationクラスで定義された条件です。

CustomAutoConfiguration.javajava
@Configuration
@ConditionalOnClass(DataSource.class) // Activates if DataSource is on classpath
@ConditionalOnMissingBean(DataSource.class) // Only acts if no DataSource exists
public class CustomAutoConfiguration {

    @Bean
    @ConditionalOnProperty(name = "app.datasource.enabled", havingValue = "true")
    public DataSource dataSource() {
        // Default DataSource configuration
        return DataSourceBuilder.create()
            .driverClassName("org.h2.Driver")
            .url("jdbc:h2:mem:testdb")
            .build();
    }
}

Spring Bootは条件(@ConditionalOn*)を見て、生成するBeanを決めます。これにより衝突を防ぎ、デフォルト設定の上書きが容易になります。

3. スターターとは何で、どう作りますか?

スターターは、ある機能に必要な依存と設定をまとめたMaven/Gradleモジュールです。例えばspring-boot-starter-webはSpring MVC、Jackson、組み込みTomcat、バリデーションを含みます。

xml
<!-- pom.xml for a custom starter -->
<project>
    <artifactId>my-company-starter</artifactId>
    <dependencies>
        <!-- Base Spring Boot dependency -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <!-- Company-specific libraries -->
        <dependency>
            <groupId>com.mycompany</groupId>
            <artifactId>logging-utils</artifactId>
        </dependency>
    </dependencies>
</project>

カスタムスターターを作るには、auto-configurationクラスを定義し、META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsに登録します。

4. application.propertiesとapplication.ymlの違い

どちらも設定を外部化する手段です。YAMLは入れ子構造に向いた読みやすい構文を持ち、propertiesはフラットな構成で簡潔です。

properties
# application.properties
server.port=8080
spring.datasource.url=jdbc:postgresql://localhost:5432/mydb
spring.datasource.username=admin
spring.jpa.hibernate.ddl-auto=update
yaml
# application.yml - clear hierarchical structure
server:
  port: 8080

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/mydb
    username: admin
  jpa:
    hibernate:
      ddl-auto: update

プロファイルは環境ごとに設定を切り替えます: application-dev.ymlapplication-prod.yml。有効化はspring.profiles.activeで行います。

5. @SpringBootApplicationはどのように機能しますか?

このアノテーションは、Spring Bootアプリの動作を決める3つのアノテーションを束ねます。

DemoApplication.javajava
// @SpringBootApplication is equivalent to these three annotations:
@SpringBootConfiguration // Equivalent to @Configuration
@EnableAutoConfiguration // Activates auto-configuration
@ComponentScan // Scans components in package and sub-packages
public class DemoApplication {

    public static void main(String[] args) {
        // Creates context, runs configurations, and starts server
        ConfigurableApplicationContext context =
            SpringApplication.run(DemoApplication.class, args);

        // The context contains all configured beans
        UserService userService = context.getBean(UserService.class);
    }
}

このクラスをパッケージのルートに置くことが重要です: @ComponentScanはそのパッケージとサブパッケージ全体をスキャンしてコンポーネントを探索します。

Spring Dataと永続化

6. JpaRepository、CrudRepository、PagingAndSortingRepositoryの違い

これらのインターフェースは、データアクセス機能の階層を形作ります。

UserRepository.javajava
// CrudRepository: basic CRUD operations
public interface UserCrudRepository extends CrudRepository<User, Long> {
    // save(), findById(), findAll(), deleteById(), count(), existsById()
}

// PagingAndSortingRepository: adds pagination and sorting
public interface UserPagingRepository extends PagingAndSortingRepository<User, Long> {
    // Inherits from CrudRepository + findAll(Sort), findAll(Pageable)
}

// JpaRepository: complete JPA features
public interface UserRepository extends JpaRepository<User, Long> {
    // Inherits from previous + flush(), saveAndFlush(), deleteInBatch()

    // Derived query methods
    List<User> findByEmailContaining(String email);

    Optional<User> findByUsernameAndActiveTrue(String username);
}

ほとんどの場面で、JPAアプリ開発に必要な機能が揃うJpaRepositoryが推奨されます。

7. @Queryでカスタムクエリを書くには?

@Queryアノテーションは、派生メソッドでは足りないケースでJPQLやネイティブSQLを書けるようにします。

OrderRepository.javajava
public interface OrderRepository extends JpaRepository<Order, Long> {

    // JPQL with named parameters
    @Query("SELECT o FROM Order o WHERE o.customer.id = :customerId AND o.status = :status")
    List<Order> findCustomerOrdersByStatus(
        @Param("customerId") Long customerId,
        @Param("status") OrderStatus status
    );

    // Native SQL query for specific needs
    @Query(value = "SELECT * FROM orders WHERE created_at > NOW() - INTERVAL '7 days'",
           nativeQuery = true)
    List<Order> findRecentOrders();

    // Modifying query with @Modifying
    @Modifying
    @Transactional
    @Query("UPDATE Order o SET o.status = :status WHERE o.id IN :ids")
    int updateOrderStatuses(@Param("ids") List<Long> ids, @Param("status") OrderStatus status);
}

JPQLはDB間で移植性があり、ネイティブクエリはDBMS固有の機能にアクセスできます。

8. @Transactionalによるトランザクション管理

このアノテーションはメソッドやクラスのトランザクション境界を宣言します。Springはコミット、ロールバック、伝播を自動処理します。

PaymentService.javajava
@Service
public class PaymentService {

    private final AccountRepository accountRepository;
    private final TransactionLogRepository logRepository;

    // Transaction with custom configuration
    @Transactional(
        propagation = Propagation.REQUIRED,      // Creates or joins a transaction
        isolation = Isolation.READ_COMMITTED,    // Isolation level
        timeout = 30,                            // Timeout in seconds
        rollbackFor = PaymentException.class     // Rollback on this exception
    )
    public void transferMoney(Long fromId, Long toId, BigDecimal amount) {
        Account from = accountRepository.findById(fromId)
            .orElseThrow(() -> new AccountNotFoundException(fromId));
        Account to = accountRepository.findById(toId)
            .orElseThrow(() -> new AccountNotFoundException(toId));

        from.debit(amount);  // Throws exception if insufficient balance
        to.credit(amount);

        accountRepository.save(from);
        accountRepository.save(to);

        // Log will be rolled back with everything else if an exception occurs
        logRepository.save(new TransactionLog(fromId, toId, amount));
    }
}

伝播レベル(REQUIREDREQUIRES_NEWNESTEDなど)は、トランザクションメソッドの入れ子呼び出し時に新規/既存どちらに参加するかを決めます。

よくある落とし穴

@TransactionalはSpringプロキシを通る呼び出しでのみ機能します。同じクラス内の内部呼び出しはプロキシをバイパスし、アノテーションは無視されます。

9. Lazy/Eager Loadingの関連をどう扱いますか?

関連の読み込み方法は性能に直結します。Lazy Loadingはアクセス時まで読み込みを遅延し、Eager Loadingは即時読み込みします。

User.javajava
@Entity
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    // Eager: roles are loaded with the user
    @ManyToMany(fetch = FetchType.EAGER)
    private Set<Role> roles;

    // Lazy: orders are only loaded on access
    @OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
    private List<Order> orders;
}
UserService.javajava
@Service
public class UserService {

    @Transactional(readOnly = true)
    public UserDTO getUserWithOrders(Long userId) {
        User user = userRepository.findById(userId).orElseThrow();

        // Accessing orders triggers an additional SQL query
        // This works because the session is open (@Transactional)
        List<OrderDTO> orderDTOs = user.getOrders().stream()
            .map(this::toDTO)
            .collect(Collectors.toList());

        return new UserDTO(user, orderDTOs);
    }

    // Alternative: query with JOIN FETCH to avoid N+1
    public UserDTO getUserWithOrdersOptimized(Long userId) {
        User user = userRepository.findByIdWithOrders(userId);
        // Orders are already loaded, no additional query
        return new UserDTO(user, user.getOrders());
    }
}

N+1問題は、コレクション要素ごとにクエリが発行される現象です。JOIN FETCHやプロジェクションで解決できます。

10. N+1問題とは何で、どう解決しますか?

初回クエリ(1)に続いて、各エンティティの関連を読み込むためにN件の追加クエリが発行される現象です。

ArticleRepository.javajava
public interface ArticleRepository extends JpaRepository<Article, Long> {

    // PROBLEM: this query generates N+1 queries
    List<Article> findAll();

    // SOLUTION 1: JOIN FETCH in JPQL
    @Query("SELECT a FROM Article a JOIN FETCH a.author JOIN FETCH a.comments")
    List<Article> findAllWithDetails();

    // SOLUTION 2: Entity Graph
    @EntityGraph(attributePaths = {"author", "comments"})
    @Query("SELECT a FROM Article a")
    List<Article> findAllWithGraph();
}
Article.javajava
@Entity
@NamedEntityGraph(
    name = "Article.withDetails",
    attributeNodes = {
        @NamedAttributeNode("author"),
        @NamedAttributeNode("comments")
    }
)
public class Article {
    @Id
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    private Author author;

    @OneToMany(mappedBy = "article", fetch = FetchType.LAZY)
    private List<Comment> comments;
}

@EntityGraphJOIN FETCHを使うと、N+1件のクエリがJOIN付きの1件にまとまり、性能が大幅に向上します。

Spring Bootの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

REST APIとWeb

11. バリデーション付きRESTコントローラを作るには?

Spring Bootは@RestControllerとBean Validationを組み合わせ、入力検証を自動で行う堅牢なAPIを提供します。

UserController.javajava
@RestController
@RequestMapping("/api/users")
@Validated // Activates validation on method parameters
public class UserController {

    private final UserService userService;

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public UserResponse createUser(@Valid @RequestBody CreateUserRequest request) {
        // Validation is performed automatically before execution
        return userService.createUser(request);
    }

    @GetMapping("/{id}")
    public UserResponse getUser(
        @PathVariable @Min(1) Long id // Validation on PathVariable
    ) {
        return userService.findById(id)
            .orElseThrow(() -> new UserNotFoundException(id));
    }

    @GetMapping
    public Page<UserResponse> listUsers(
        @RequestParam(defaultValue = "0") @Min(0) int page,
        @RequestParam(defaultValue = "20") @Max(100) int size
    ) {
        return userService.findAll(PageRequest.of(page, size));
    }
}
CreateUserRequest.javajava
public record CreateUserRequest(
    @NotBlank(message = "Name is required")
    @Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
    String name,

    @NotBlank(message = "Email is required")
    @Email(message = "Invalid email format")
    String email,

    @NotBlank
    @Pattern(regexp = "^(?=.*[A-Z])(?=.*[0-9]).{8,}$",
             message = "Password must contain at least 8 characters, one uppercase and one digit")
    String password
) {}

バリデーションメッセージは、国際化のためメッセージファイルへ外部化できます。

12. @ControllerAdviceで例外をグローバルに扱うには?

例外ハンドラを集中管理することで、エラーレスポンスを統一しコード重複を避けます。

GlobalExceptionHandler.javajava
@RestControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    // Handle validation errors
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleValidationErrors(MethodArgumentNotValidException ex) {
        Map<String, String> errors = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .collect(Collectors.toMap(
                FieldError::getField,
                FieldError::getDefaultMessage,
                (existing, replacement) -> existing
            ));

        return new ErrorResponse("VALIDATION_ERROR", "Invalid data", errors);
    }

    // Handle resource not found
    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ErrorResponse handleNotFound(ResourceNotFoundException ex) {
        return new ErrorResponse("NOT_FOUND", ex.getMessage(), null);
    }

    // Handle unexpected errors
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResponse handleUnexpectedError(Exception ex) {
        log.error("Unexpected error", ex);
        return new ErrorResponse("INTERNAL_ERROR", "An error occurred", null);
    }
}
ErrorResponse.javajava
public record ErrorResponse(
    String code,
    String message,
    Map<String, String> details,
    Instant timestamp
) {
    public ErrorResponse(String code, String message, Map<String, String> details) {
        this(code, message, details, Instant.now());
    }
}

この方式により、すべてのエラーが同じフォーマットになり、クライアント側の処理が容易になります。

13. @RequestParam、@PathVariable、@RequestBodyの違い

これら3つはHTTPリクエストの異なる位置からデータを取得します。

ProductController.javajava
@RestController
@RequestMapping("/api/products")
public class ProductController {

    // @PathVariable: extracts values from the URL
    // GET /api/products/123
    @GetMapping("/{id}")
    public Product getProduct(@PathVariable Long id) {
        return productService.findById(id);
    }

    // @RequestParam: extracts query parameters
    // GET /api/products?category=electronics&minPrice=100&page=0
    @GetMapping
    public Page<Product> searchProducts(
        @RequestParam(required = false) String category,
        @RequestParam(defaultValue = "0") BigDecimal minPrice,
        @RequestParam(defaultValue = "0") int page
    ) {
        return productService.search(category, minPrice, PageRequest.of(page, 20));
    }

    // @RequestBody: deserializes the JSON request body
    // POST /api/products with {"name": "...", "price": ...}
    @PostMapping
    public Product createProduct(@Valid @RequestBody CreateProductRequest request) {
        return productService.create(request);
    }

    // Combination of all three in the same method
    // PUT /api/products/123?notify=true with JSON body
    @PutMapping("/{id}")
    public Product updateProduct(
        @PathVariable Long id,
        @RequestParam(defaultValue = "false") boolean notify,
        @Valid @RequestBody UpdateProductRequest request
    ) {
        Product updated = productService.update(id, request);
        if (notify) {
            notificationService.sendUpdateNotification(updated);
        }
        return updated;
    }
}

@PathVariableは特定のリソースを示し、@RequestParamは絞り込みやページングに、@RequestBodyは複雑なデータ送信に使います。

14. APIのバージョニングはどう実装しますか?

REST APIのバージョニングには、それぞれ利点のある複数の戦略があります。

java
// Version via URL (recommended for clarity)
@RestController
@RequestMapping("/api/v1/users")
public class UserControllerV1 {
    @GetMapping("/{id}")
    public UserV1Response getUser(@PathVariable Long id) {
        return userService.findByIdV1(id);
    }
}

@RestController
@RequestMapping("/api/v2/users")
public class UserControllerV2 {
    @GetMapping("/{id}")
    public UserV2Response getUser(@PathVariable Long id) {
        // V2 includes additional fields
        return userService.findByIdV2(id);
    }
}
java
// Version via custom header
@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping(value = "/{id}", headers = "X-API-Version=1")
    public UserV1Response getUserV1(@PathVariable Long id) {
        return userService.findByIdV1(id);
    }

    @GetMapping(value = "/{id}", headers = "X-API-Version=2")
    public UserV2Response getUserV2(@PathVariable Long id) {
        return userService.findByIdV2(id);
    }
}
java
// Version via media type (content negotiation)
@RestController
@RequestMapping("/api/users")
public class UserMediaTypeController {

    @GetMapping(value = "/{id}", produces = "application/vnd.myapi.v1+json")
    public UserV1Response getUserV1(@PathVariable Long id) {
        return userService.findByIdV1(id);
    }

    @GetMapping(value = "/{id}", produces = "application/vnd.myapi.v2+json")
    public UserV2Response getUserV2(@PathVariable Long id) {
        return userService.findByIdV2(id);
    }
}

URLによるバージョニングは、シンプルでログやドキュメントでも見やすく、最も広く使われます。

15. Spring BootでCORSをどう設定しますか?

CORS(Cross-Origin Resource Sharing)は、異なるオリジンからのリクエストを制御します。複数の設定レベルが利用できます。

CorsConfig.java - Global configurationjava
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
            .allowedOrigins("https://frontend.example.com", "https://admin.example.com")
            .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
            .allowedHeaders("*")
            .exposedHeaders("X-Total-Count", "X-Page-Count")
            .allowCredentials(true)
            .maxAge(3600); // Cache preflight for 1 hour
    }
}
java
// Per-controller or method configuration
@RestController
@RequestMapping("/api/public")
@CrossOrigin(origins = "*", maxAge = 3600)
public class PublicApiController {

    @GetMapping("/data")
    public DataResponse getPublicData() {
        return dataService.getPublicData();
    }

    // Override at method level
    @CrossOrigin(origins = "https://specific-client.com")
    @PostMapping("/submit")
    public SubmitResponse submitData(@RequestBody SubmitRequest request) {
        return dataService.submit(request);
    }
}

本番では許可するオリジンを限定し、HTTPSを使うべきです。*allowCredentials(true)は併用しないでください。

Spring Security

16. Spring SecurityをJWTで構成するには?

Spring Security 6+では、関数型のセキュリティ設定が採用されています。

SecurityConfig.javajava
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtAuthenticationFilter jwtFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
            .csrf(csrf -> csrf.disable()) // Disabled for stateless API
            .sessionManagement(session ->
                session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            )
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/admin/**").hasRole("ADMIN")
                .requestMatchers("/api/**").authenticated()
            )
            .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
            .build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
JwtAuthenticationFilter.javajava
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtService jwtService;
    private final UserDetailsService userDetailsService;

    @Override
    protected void doFilterInternal(
        HttpServletRequest request,
        HttpServletResponse response,
        FilterChain chain
    ) throws ServletException, IOException {

        String authHeader = request.getHeader("Authorization");

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            chain.doFilter(request, response);
            return;
        }

        String jwt = authHeader.substring(7);
        String username = jwtService.extractUsername(jwt);

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);

            if (jwtService.isTokenValid(jwt, userDetails)) {
                UsernamePasswordAuthenticationToken authToken =
                    new UsernamePasswordAuthenticationToken(
                        userDetails, null, userDetails.getAuthorities()
                    );
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }

        chain.doFilter(request, response);
    }
}

この構成では、各リクエストにAuthorizationヘッダーで有効なJWTトークンを付与する、ステートレスなAPIになります。

17. @PreAuthorizeと@PostAuthorizeでメソッドを保護するには?

メソッドレベルのセキュリティは、ロール、権限、データに基づくきめ細かい制御を可能にします。

UserService.javajava
@Service
@PreAuthorize("isAuthenticated()") // All methods require authentication
public class UserService {

    // Only ADMINs can list all users
    @PreAuthorize("hasRole('ADMIN')")
    public List<User> findAll() {
        return userRepository.findAll();
    }

    // User can view their profile OR admins can view any profile
    @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
    public User findById(Long id) {
        return userRepository.findById(id)
            .orElseThrow(() -> new UserNotFoundException(id));
    }

    // Post-execution check: user can only see their own data
    @PostAuthorize("returnObject.owner.id == authentication.principal.id or hasRole('ADMIN')")
    public Document getDocument(Long documentId) {
        return documentRepository.findById(documentId)
            .orElseThrow(() -> new DocumentNotFoundException(documentId));
    }

    // Collection filtering: returns only authorized elements
    @PostFilter("filterObject.owner.id == authentication.principal.id")
    public List<Project> getUserProjects() {
        return projectRepository.findAll();
    }
}
SecurityConfig.java - Activationjava
@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
    // Additional configuration if needed
}

@PreAuthorizeは実行前に検証し、@PostAuthorizeは実行後に検証します。SpEL式で複雑なルールも書けます。

18. CSRF攻撃からエンドポイントを守るには?

CSRF(Cross-Site Request Forgery)は、認証済みユーザーのセッションを悪用します。セッションベースのアプリでは保護がデフォルトで有効です。

SecurityConfig.javajava
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
            // CSRF configuration for session-based applications
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
                .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
                // Exclude certain endpoints from CSRF protection
                .ignoringRequestMatchers("/api/webhooks/**")
            )
            .build();
    }
}
CsrfController.java - Endpoint to retrieve token (SPA)java
@RestController
public class CsrfController {

    @GetMapping("/api/csrf")
    public CsrfToken csrf(CsrfToken token) {
        // Returns CSRF token to frontend
        return token;
    }
}

JWTを用いるステートレスなREST APIでは、悪用すべきセッションクッキーがないためCSRFを無効化できます。従来のセッションベースアプリでは保護が必須です。

ステートレスAPI vs セッションベースアプリ

JWT APIはトークンを各リクエストに明示的に付与するため、CSRFに対して構造的に保護されています。セッションクッキーを使うアプリではアクティブなCSRF保護が必要です。

Spring Bootのテスト

19. @SpringBootTestと@WebMvcTestの違い

これらのアノテーションは、用途に合わせてテストコンテキストを構築します。

UserControllerIntegrationTest.javajava
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class UserControllerIntegrationTest {

    // Loads full context: all beans, database, etc.
    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void shouldCreateUser() {
        CreateUserRequest request = new CreateUserRequest("John", "john@example.com");

        ResponseEntity<User> response = restTemplate.postForEntity(
            "/api/users", request, User.class
        );

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody().getName()).isEqualTo("John");
    }
}
UserControllerUnitTest.javajava
@WebMvcTest(UserController.class) // Loads only the web layer
class UserControllerUnitTest {

    @Autowired
    private MockMvc mockMvc;

    @MockitoBean // Replaces @MockBean (deprecated)
    private UserService userService;

    @Test
    void shouldReturnUser() throws Exception {
        // Mock configuration
        when(userService.findById(1L))
            .thenReturn(Optional.of(new User(1L, "John", "john@example.com")));

        mockMvc.perform(get("/api/users/1"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("John"))
            .andExpect(jsonPath("$.email").value("john@example.com"));
    }

    @Test
    void shouldReturn404WhenUserNotFound() throws Exception {
        when(userService.findById(999L)).thenReturn(Optional.empty());

        mockMvc.perform(get("/api/users/999"))
            .andExpect(status().isNotFound());
    }
}

@WebMvcTestは指定したコントローラとその直接依存だけを読み込むため高速です。完全な統合テストには@SpringBootTestを用います。

20. @DataJpaTestでリポジトリをテストするには?

このアノテーションは、組み込みDBで永続化層をテストするための最小限のコンテキストを構築します。

UserRepositoryTest.javajava
@DataJpaTest
@AutoConfigureTestDatabase(replace = Replace.NONE) // Use Testcontainers
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    void shouldFindByEmail() {
        // Arrange: create and persist an entity
        User user = new User("John", "john@example.com");
        entityManager.persistAndFlush(user);
        entityManager.clear(); // Clear L1 cache

        // Act
        Optional<User> found = userRepository.findByEmail("john@example.com");

        // Assert
        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("John");
    }

    @Test
    void shouldFindActiveUsersWithOrders() {
        // Testing a complex query with joins
        User activeUser = entityManager.persistAndFlush(new User("Active", "active@test.com", true));
        User inactiveUser = entityManager.persistAndFlush(new User("Inactive", "inactive@test.com", false));

        entityManager.persistAndFlush(new Order(activeUser, BigDecimal.valueOf(100)));

        List<User> result = userRepository.findActiveUsersWithOrders();

        assertThat(result).hasSize(1);
        assertThat(result.get(0).getEmail()).isEqualTo("active@test.com");
    }
}

TestEntityManagerはJPAテスト向けのユーティリティを提供し、特にpersistAndFlush()はDBへの即時書き込みを保証します。

21. Testcontainersを統合テストで使うには?

Testcontainersは、テスト中に実DBやサービスのDockerコンテナを起動します。

IntegrationTestBase.javajava
@SpringBootTest
@Testcontainers
abstract class IntegrationTestBase {

    @Container
    @ServiceConnection
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
        .withDatabaseName("testdb")
        .withUsername("test")
        .withPassword("test");

    @Container
    @ServiceConnection
    static RedisContainer redis = new RedisContainer("redis:7");
}
OrderServiceIntegrationTest.javajava
class OrderServiceIntegrationTest extends IntegrationTestBase {

    @Autowired
    private OrderService orderService;

    @Autowired
    private OrderRepository orderRepository;

    @Test
    void shouldProcessOrderWithRealDatabase() {
        // Create an order
        Order order = orderService.createOrder(
            new CreateOrderRequest(1L, List.of(
                new OrderItem(101L, 2),
                new OrderItem(102L, 1)
            ))
        );

        // Verify in database
        Order saved = orderRepository.findById(order.getId()).orElseThrow();
        assertThat(saved.getItems()).hasSize(2);
        assertThat(saved.getStatus()).isEqualTo(OrderStatus.PENDING);
    }

    @Test
    void shouldCacheOrderInRedis() {
        Order order = orderService.createOrder(createSampleOrderRequest());

        // First call: database
        Order fetched1 = orderService.findById(order.getId());

        // Second call: Redis cache
        Order fetched2 = orderService.findById(order.getId());

        assertThat(fetched1).isEqualTo(fetched2);
        // Verify cache metrics if needed
    }
}

@ServiceConnectionは接続プロパティを自動構成し、@DynamicPropertySourceを不要にします。

Spring Bootの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

設定とモニタリング

22. @ConfigurationPropertiesで設定を外部化するには?

このアノテーションはプロパティを型付きJavaオブジェクトにバインドし、検証を行います。

MailProperties.javajava
@ConfigurationProperties(prefix = "app.mail")
@Validated
public record MailProperties(
    @NotBlank String host,
    @Min(1) @Max(65535) int port,
    @NotBlank String username,
    @NotBlank String password,
    @Valid SenderConfig sender,
    @Valid RetryConfig retry
) {
    public record SenderConfig(
        @NotBlank @Email String address,
        @NotBlank String name
    ) {}

    public record RetryConfig(
        @Min(1) int maxAttempts,
        @Min(100) long delayMs
    ) {
        public RetryConfig {
            // Default values
            if (maxAttempts == 0) maxAttempts = 3;
            if (delayMs == 0) delayMs = 1000;
        }
    }
}
yaml
# application.yml
app:
  mail:
    host: smtp.example.com
    port: 587
    username: ${MAIL_USERNAME}
    password: ${MAIL_PASSWORD}
    sender:
      address: noreply@example.com
      name: MyApp Notifications
    retry:
      max-attempts: 3
      delay-ms: 1000
MailConfig.javajava
@Configuration
@EnableConfigurationProperties(MailProperties.class)
public class MailConfig {

    @Bean
    public JavaMailSender mailSender(MailProperties props) {
        JavaMailSenderImpl sender = new JavaMailSenderImpl();
        sender.setHost(props.host());
        sender.setPort(props.port());
        sender.setUsername(props.username());
        sender.setPassword(props.password());
        return sender;
    }
}

Javaのrecord(Java 16以降)は不変かつ簡潔で、@ConfigurationPropertiesと特に相性が良いです。

23. 環境ごとにSpringプロファイルを使うには?

プロファイルを使うと、実行環境に合わせて設定を切り替えられます。

yaml
# application.yml - Default configuration
spring:
  profiles:
    active: ${SPRING_PROFILES_ACTIVE:dev}
  datasource:
    url: ${DATABASE_URL:jdbc:h2:mem:testdb}

---
# application-dev.yml
spring:
  config:
    activate:
      on-profile: dev
  datasource:
    url: jdbc:postgresql://localhost:5432/myapp_dev
  jpa:
    show-sql: true
    hibernate:
      ddl-auto: update

logging:
  level:
    com.example: DEBUG

---
# application-prod.yml
spring:
  config:
    activate:
      on-profile: prod
  datasource:
    url: ${DATABASE_URL}
    hikari:
      maximum-pool-size: 20
  jpa:
    show-sql: false
    hibernate:
      ddl-auto: validate

logging:
  level:
    com.example: INFO
DevDataInitializer.javajava
@Component
@Profile("dev") // Only active in development
public class DevDataInitializer implements CommandLineRunner {

    private final UserRepository userRepository;

    @Override
    public void run(String... args) {
        // Create test data
        userRepository.save(new User("dev@example.com", "Dev User", "password"));
        userRepository.save(new User("test@example.com", "Test User", "password"));
    }
}
java
// Service with conditional behavior
@Service
public class NotificationService {

    @Value("${app.notifications.enabled:true}")
    private boolean notificationsEnabled;

    @Autowired(required = false) // Optional based on profile
    private EmailService emailService;

    public void sendNotification(String message) {
        if (notificationsEnabled && emailService != null) {
            emailService.send(message);
        } else {
            log.info("Notification (disabled): {}", message);
        }
    }
}

プロファイルは組み合わせ可能です: spring.profiles.active=prod,metricsは両方を同時に有効にします。

24. ActuatorとMicrometerで独自メトリクスを公開するには?

Micrometerはモニタリングシステムのファサードを提供します。独自メトリクスは可観測性を高めます。

OrderMetrics.javajava
@Component
public class OrderMetrics {

    private final Counter ordersCreated;
    private final Counter ordersFailed;
    private final Timer orderProcessingTime;
    private final AtomicInteger activeOrders;

    public OrderMetrics(MeterRegistry registry) {
        // Counter for orders created by status
        this.ordersCreated = Counter.builder("orders.created")
            .description("Number of orders created")
            .tag("application", "order-service")
            .register(registry);

        this.ordersFailed = Counter.builder("orders.failed")
            .description("Number of failed orders")
            .register(registry);

        // Timer to measure processing time
        this.orderProcessingTime = Timer.builder("orders.processing.time")
            .description("Order processing time")
            .publishPercentiles(0.5, 0.95, 0.99)
            .register(registry);

        // Gauge for active orders count
        this.activeOrders = new AtomicInteger(0);
        Gauge.builder("orders.active", activeOrders, AtomicInteger::get)
            .description("Number of orders being processed")
            .register(registry);
    }

    public void recordOrderCreated() {
        ordersCreated.increment();
    }

    public void recordOrderFailed() {
        ordersFailed.increment();
    }

    public Timer.Sample startProcessing() {
        activeOrders.incrementAndGet();
        return Timer.start();
    }

    public void stopProcessing(Timer.Sample sample) {
        sample.stop(orderProcessingTime);
        activeOrders.decrementAndGet();
    }
}
OrderService.javajava
@Service
public class OrderService {

    private final OrderMetrics metrics;

    public Order processOrder(CreateOrderRequest request) {
        Timer.Sample sample = metrics.startProcessing();
        try {
            Order order = createOrder(request);
            metrics.recordOrderCreated();
            return order;
        } catch (Exception e) {
            metrics.recordOrderFailed();
            throw e;
        } finally {
            metrics.stopProcessing(sample);
        }
    }
}

これらのメトリクスはPrometheus向けに/actuator/prometheus、JSON APIとしては/actuator/metricsから公開されます。

25. カスタムHealthIndicatorを作るには?

Health Indicatorは重要コンポーネントの状態を監視します。

ExternalApiHealthIndicator.javajava
@Component
public class ExternalApiHealthIndicator implements HealthIndicator {

    private final RestClient restClient;
    private final ExternalApiProperties properties;

    @Override
    public Health health() {
        try {
            long startTime = System.currentTimeMillis();

            ResponseEntity<Void> response = restClient.get()
                .uri(properties.getHealthEndpoint())
                .retrieve()
                .toBodilessEntity();

            long responseTime = System.currentTimeMillis() - startTime;

            if (response.getStatusCode().is2xxSuccessful()) {
                return Health.up()
                    .withDetail("responseTime", responseTime + "ms")
                    .withDetail("endpoint", properties.getHealthEndpoint())
                    .build();
            } else {
                return Health.down()
                    .withDetail("status", response.getStatusCode().value())
                    .build();
            }
        } catch (Exception e) {
            return Health.down()
                .withException(e)
                .withDetail("endpoint", properties.getHealthEndpoint())
                .build();
        }
    }
}
DatabaseHealthContributor.java - Composite healthjava
@Component
public class DatabaseHealthContributor implements CompositeHealthContributor {

    private final Map<String, HealthIndicator> indicators;

    public DatabaseHealthContributor(
        DataSource primaryDataSource,
        @Qualifier("replicaDataSource") DataSource replicaDataSource
    ) {
        this.indicators = Map.of(
            "primary", new DataSourceHealthIndicator(primaryDataSource),
            "replica", new DataSourceHealthIndicator(replicaDataSource)
        );
    }

    @Override
    public HealthContributor getContributor(String name) {
        return indicators.get(name);
    }

    @Override
    public Iterator<NamedContributor<HealthContributor>> iterator() {
        return indicators.entrySet().stream()
            .map(e -> NamedContributor.of(e.getKey(), e.getValue()))
            .iterator();
    }
}

/actuator/healthはすべてのIndicatorを集約します。management.endpoint.health.show-details=alwaysを設定すると詳細を表示します。

高度なトピック

26. Spring CacheとRedisでキャッシュを実装するには?

Spring Cacheの抽象化により、Redisのような分散キャッシュを容易に統合できます。

CacheConfig.javajava
@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(10))
            .serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

        // Specific configurations per cache
        Map<String, RedisCacheConfiguration> cacheConfigs = Map.of(
            "users", config.entryTtl(Duration.ofHours(1)),
            "products", config.entryTtl(Duration.ofMinutes(30)),
            "sessions", config.entryTtl(Duration.ofMinutes(5))
        );

        return RedisCacheManager.builder(connectionFactory)
            .cacheDefaults(config)
            .withInitialCacheConfigurations(cacheConfigs)
            .build();
    }
}
ProductService.javajava
@Service
public class ProductService {

    // Automatic result caching
    @Cacheable(value = "products", key = "#id")
    public Product findById(Long id) {
        log.info("Fetching product {} from database", id);
        return productRepository.findById(id)
            .orElseThrow(() -> new ProductNotFoundException(id));
    }

    // Cache update on modification
    @CachePut(value = "products", key = "#product.id")
    public Product update(Product product) {
        return productRepository.save(product);
    }

    // Cache invalidation
    @CacheEvict(value = "products", key = "#id")
    public void delete(Long id) {
        productRepository.deleteById(id);
    }

    // Clear entire cache
    @CacheEvict(value = "products", allEntries = true)
    public void clearProductCache() {
        log.info("Product cache cleared");
    }

    // Composite key for searches
    @Cacheable(value = "productSearch", key = "#category + '-' + #minPrice + '-' + #maxPrice")
    public List<Product> search(String category, BigDecimal minPrice, BigDecimal maxPrice) {
        return productRepository.findByCategoryAndPriceBetween(category, minPrice, maxPrice);
    }
}

キャッシュアノテーションは透過的で、ビジネスコードを変えずに宣言的にキャッシュを管理できます。

27. @Scheduledでスケジュールタスクを実装するには?

Springは繰り返しタスクをスケジュールする複数の方法を提供します。

SchedulerConfig.javajava
@Configuration
@EnableScheduling
public class SchedulerConfig {

    @Bean
    public TaskScheduler taskScheduler() {
        ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
        scheduler.setPoolSize(5);
        scheduler.setThreadNamePrefix("scheduled-");
        scheduler.setErrorHandler(t -> log.error("Scheduled task error", t));
        return scheduler;
    }
}
ScheduledTasks.javajava
@Component
public class ScheduledTasks {

    private static final Logger log = LoggerFactory.getLogger(ScheduledTasks.class);

    // Execute every 5 minutes
    @Scheduled(fixedRate = 5 * 60 * 1000)
    public void syncExternalData() {
        log.info("Starting external data sync");
        // Sync with external system
    }

    // Execute 10 seconds after the previous one ends
    @Scheduled(fixedDelay = 10000, initialDelay = 5000)
    public void processQueue() {
        // Queue processing with delay between executions
    }

    // Cron expression: every day at 2 AM
    @Scheduled(cron = "0 0 2 * * *", zone = "Europe/Paris")
    public void dailyCleanup() {
        log.info("Running daily cleanup");
        cleanupService.removeExpiredSessions();
        cleanupService.archiveOldData();
    }

    // Configurable cron via properties
    @Scheduled(cron = "${app.reports.cron:0 0 6 * * MON}")
    public void generateWeeklyReport() {
        reportService.generateAndSend();
    }
}
ConditionalScheduling.java - Conditional activationjava
@Component
@ConditionalOnProperty(name = "app.scheduling.enabled", havingValue = "true")
public class ConditionalScheduledTasks {

    @Scheduled(fixedRate = 60000)
    public void monitorSystem() {
        // Monitoring active only if configured
    }
}

スケジュール済みタスクは別のスレッドプールで実行され、リクエスト処理を妨げません。

28. ApplicationEventでイベントを扱うには?

イベント機構は、アプリケーションコンポーネントを疎結合にします。

UserRegisteredEvent.javajava
public class UserRegisteredEvent extends ApplicationEvent {

    private final User user;
    private final Instant registeredAt;

    public UserRegisteredEvent(Object source, User user) {
        super(source);
        this.user = user;
        this.registeredAt = Instant.now();
    }

    public User getUser() { return user; }
    public Instant getRegisteredAt() { return registeredAt; }
}
UserService.java - Event publishingjava
@Service
public class UserService {

    private final ApplicationEventPublisher eventPublisher;

    @Transactional
    public User registerUser(CreateUserRequest request) {
        User user = new User(request.email(), request.name());
        user = userRepository.save(user);

        // Publish event after transaction
        eventPublisher.publishEvent(new UserRegisteredEvent(this, user));

        return user;
    }
}
UserEventListeners.java - Event listenersjava
@Component
public class UserEventListeners {

    private static final Logger log = LoggerFactory.getLogger(UserEventListeners.class);

    // Synchronous listener
    @EventListener
    public void handleUserRegistered(UserRegisteredEvent event) {
        log.info("User registered: {}", event.getUser().getEmail());
    }

    // Asynchronous listener to avoid blocking the main thread
    @Async
    @EventListener
    public void sendWelcomeEmail(UserRegisteredEvent event) {
        emailService.sendWelcomeEmail(event.getUser());
    }

    // Transactional listener: executes after commit
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void notifyExternalSystem(UserRegisteredEvent event) {
        // External notification only if transaction succeeds
        externalApiClient.notifyNewUser(event.getUser().getId());
    }

    // Conditional listener with SpEL
    @EventListener(condition = "#event.user.role == 'PREMIUM'")
    public void handlePremiumUserRegistered(UserRegisteredEvent event) {
        premiumService.initializePremiumFeatures(event.getUser());
    }
}

トランザクショナルイベント(@TransactionalEventListener)は、DBと副作用の整合性を保ちます。

29. RestClient(Spring Boot 3+)でHTTPクライアントを実装するには?

RestClientは同期HTTP呼び出し向けの近代的で流暢なAPIで、RestTemplateを置き換えます。

ExternalApiClient.javajava
@Component
public class ExternalApiClient {

    private final RestClient restClient;

    public ExternalApiClient(RestClient.Builder builder, ExternalApiProperties props) {
        this.restClient = builder
            .baseUrl(props.getBaseUrl())
            .defaultHeader("Authorization", "Bearer " + props.getApiKey())
            .defaultHeader("Accept", "application/json")
            .requestInterceptor((request, body, execution) -> {
                log.debug("Request: {} {}", request.getMethod(), request.getURI());
                return execution.execute(request, body);
            })
            .build();
    }

    public User fetchUser(Long id) {
        return restClient.get()
            .uri("/users/{id}", id)
            .retrieve()
            .body(User.class);
    }

    public List<Product> searchProducts(String query, int page) {
        return restClient.get()
            .uri(uriBuilder -> uriBuilder
                .path("/products/search")
                .queryParam("q", query)
                .queryParam("page", page)
                .build())
            .retrieve()
            .body(new ParameterizedTypeReference<List<Product>>() {});
    }

    public Order createOrder(CreateOrderRequest request) {
        return restClient.post()
            .uri("/orders")
            .contentType(MediaType.APPLICATION_JSON)
            .body(request)
            .retrieve()
            .onStatus(HttpStatusCode::is4xxClientError, (req, res) -> {
                throw new OrderValidationException("Invalid order: " + res.getStatusCode());
            })
            .body(Order.class);
    }

    // Error handling with ResponseEntity
    public Optional<User> findUser(Long id) {
        ResponseEntity<User> response = restClient.get()
            .uri("/users/{id}", id)
            .retrieve()
            .toEntity(User.class);

        return response.getStatusCode().is2xxSuccessful()
            ? Optional.ofNullable(response.getBody())
            : Optional.empty();
    }
}

RestClientはブロッキング呼び出し向けに、WebClientに似たAPIを提供します。リアクティブを使わないアプリに最適です。

30. Flywayでデータベースマイグレーションを管理するには?

Flywayはスキーママイグレーションをバージョン管理しつつ自動化します。

properties
# application.properties
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration
spring.flyway.baseline-on-migrate=true
spring.flyway.validate-on-migrate=true
sql
-- db/migration/V1__create_users_table.sql
CREATE TABLE users (
    id BIGSERIAL PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    name VARCHAR(255) NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_users_email ON users(email);
sql
-- db/migration/V2__add_user_status.sql
ALTER TABLE users ADD COLUMN status VARCHAR(20) DEFAULT 'ACTIVE';

CREATE INDEX idx_users_status ON users(status);
sql
-- db/migration/V3__create_orders_table.sql
CREATE TABLE orders (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL REFERENCES users(id),
    total_amount DECIMAL(10, 2) NOT NULL,
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_orders_user_id ON orders(user_id);
CREATE INDEX idx_orders_status ON orders(status);
FlywayConfig.java - Advanced configurationjava
@Configuration
public class FlywayConfig {

    @Bean
    public FlywayMigrationStrategy migrationStrategy() {
        return flyway -> {
            // Validation before migration
            flyway.validate();
            // Execute migrations
            flyway.migrate();
        };
    }

    // Callback for post-migration actions
    @Bean
    public Callback flywayCallback() {
        return new BaseCallback() {
            @Override
            public void handle(Event event, Context context) {
                if (event == Event.AFTER_MIGRATE) {
                    log.info("Migrations completed successfully");
                }
            }
        };
    }
}

各マイグレーションファイルは1度だけ実行され、チェックサムが検証されることで意図せぬ変更を検出できます。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

まとめ

この30問は、技術面接で評価されるSpring Bootの中核的な領域を網羅します。これらを習得することで、フレームワークと優れたJava開発実践への深い理解を示せます。

準備チェックリスト:

  • ✅ auto-configurationの仕組みと条件を理解する
  • ✅ Spring Data JPAを使いこなす: クエリ、トランザクション、N+1問題
  • ✅ JWTとメソッドセキュリティでSpring Securityを構成できる
  • ✅ 各種テストアノテーションと用途を把握する
  • ✅ プロファイルと@ConfigurationPropertiesで設定を扱う
  • ✅ キャッシュ、スケジューリング、イベントを実装する
  • ✅ 独自のメトリクスとHealth Indicatorを公開する
  • ✅ RestClientで近代的なHTTP呼び出しを行う

実プロジェクトでの継続的な実践こそが、知識を定着させ、技術的な質問に自信を持って答えるための最良の方法です。

タグ

#spring boot
#java
#interview
#backend
#java interview

共有

関連記事