Spring Boot

Spring Boot 프로젝트 구조와 실행 흐름: 메인 클래스부터 @SpringBootApplication까지

IT Lab 2026. 3. 5. 20:00

Spring Boot 3.x에서 메인 클래스가 실행된 뒤 어떤 순서로 컴포넌트 스캔과 자동 구성(Auto Configuration)이 적용되는지, @SpringBootApplication의 의미를 중심으로 정리합니다.

도입 (문제 상황)

Spring Initializr로 프로젝트를 만들고 main()을 실행하면 서버가 “알아서” 떠요. 그런데 막상 패키지를 조금만 옮기거나, 빈이 안 잡히는 순간부터 “대체 어디서부터 뭐가 시작되는 거지?”라는 질문이 생깁니다. 이 글에서는 메인 클래스부터 시작해 @SpringBootApplication이 실제로 해주는 일을 실행 흐름 관점에서 잡아드립니다.

핵심 개념 (Spring Boot 실행 흐름, @SpringBootApplication, 컴포넌트 스캔, 자동 구성)

1) 프로젝트 구조에서 제일 중요한 규칙: “메인 클래스 패키지 = 스캔의 기준점”

Spring Boot 프로젝트에서 가장 먼저 확인할 것은 메인 클래스의 패키지 위치입니다. 기본 설정에서 컴포넌트 스캔은 “메인 클래스가 있는 패키지부터 하위 패키지”를 훑습니다.
비유하자면, 메인 클래스 패키지는 “건물의 로비” 같은 역할이에요. 로비 아래 층(하위 패키지)으로는 자연스럽게 내려가지만, 로비 밖(상위/다른 루트 패키지)은 기본으로는 가지 않습니다.

권장 구조는 보통 아래처럼 둡니다.

  • com.example.myapp (루트)
    • MyAppApplication (메인 클래스)
    • domain, service, controller, infra 등 하위 패키지

2) @SpringBootApplication은 “3종 세트” 합성 애너테이션

@SpringBootApplication은 단순한 표식이 아니라, 실제로는 아래 3가지를 묶어둔 합성 애너테이션입니다.

구성 요소 핵심 역할 실무에서 체감되는 효과
@SpringBootConfiguration 이 클래스가 설정의 시작점임을 선언(사실상 @Configuration) “이 앱의 설정 루트는 여기”가 됩니다
@ComponentScan 컴포넌트(@Component/@Service/@Repository/@Controller 등) 자동 탐색 패키지 위치가 틀리면 빈이 아예 안 잡힙니다
@EnableAutoConfiguration 클래스패스/프로퍼티 기반으로 자동 설정을 조건부로 등록 DB 드라이버 넣으면 DataSource가 생기고, Web 의존성 넣으면 톰캣이 뜹니다

@SpringBootApplication은 “설정 시작점 + 빈 탐색 + 자동 설정”을 한 번에 켜는 스위치라고 이해하시면 됩니다.

3) 컴포넌트 스캔: “내가 만든 빈”을 등록하는 방식

컴포넌트 스캔은 애플리케이션 코드에서 클래스에 붙은 애너테이션을 보고 빈으로 등록합니다.

  • 대상: @Component, @Service, @Repository, @Controller, @RestController, @Configuration
  • 기본 스캔 범위: 메인 클래스 패키지 + 하위 패키지
  • 자주 겪는 문제:
    • 패키지를 분리했는데 빈이 안 잡힘
    • 멀티 모듈에서 다른 모듈 패키지가 스캔 범위 밖에 있음

이럴 때는 @ComponentScan(basePackages = ...)로 범위를 명시할 수 있지만, 실무에서는 가능하면 패키지 구조를 정리해서 기본 규칙을 타게 만드는 편이 유지보수에 유리합니다.

4) 자동 구성(Auto Configuration): “외부 라이브러리 기반 빈”을 등록하는 방식

자동 구성은 Spring Boot가 제공하는 강력한 편의 기능인데, 핵심은 이거예요.

  • “클래스패스에 뭐가 있지?” (예: spring-boot-starter-web, JDBC 드라이버)
  • “설정값이 뭐지?” (예: spring.datasource.*, server.port)
  • 그 조건이 맞으면 미리 준비된 설정 클래스가 빈을 등록합니다.

Spring Boot 3.x에서는 자동 구성 후보가 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports에 정의되어 있고, 조건부 애너테이션(예: @ConditionalOnClass, @ConditionalOnProperty)로 “필요할 때만” 켜집니다.
그래서 의존성을 추가하면 기능이 따라오고, 설정을 바꾸면 동작이 바뀌는 겁니다.

코드 예제 (Spring Boot 3.x에서 실행 흐름을 눈으로 확인하기)

아래 예제는 실행 시점에 어떤 빈이 등록됐는지, 그리고 컴포넌트 스캔이 어디까지 먹는지를 최소 코드로 확인할 수 있게 구성했습니다. 그대로 복붙해서 실행해 보셔도 됩니다.

// build.gradle (Gradle Groovy)
plugins {
    id 'java'
    id 'org.springframework.boot' version '3.3.2'
    id 'io.spring.dependency-management' version '1.1.6'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(17)
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}
# src/main/resources/application.yml
server:
  port: 8080

logging:
  level:
    org.springframework.boot.autoconfigure: INFO
// src/main/java/com/example/demo/DemoApplication.java
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
// src/main/java/com/example/demo/hello/HelloController.java
package com.example.demo.hello;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "ok";
    }
}
// src/main/java/com/example/demo/inspect/StartupInspector.java
package com.example.demo.inspect;

import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;

import java.util.Map;

@Component
public class StartupInspector implements ApplicationRunner {

    private final ApplicationContext context;

    public StartupInspector(ApplicationContext context) {
        this.context = context;
    }

    @Override
    public void run(ApplicationArguments args) {
        // 1) 컴포넌트 스캔으로 등록된 컨트롤러가 실제로 빈으로 있는지 확인
        Map<String, Object> controllers = context.getBeansWithAnnotation(
                org.springframework.web.bind.annotation.RestController.class
        );
        System.out.println("=== @RestController beans ===");
        controllers.forEach((name, bean) -> System.out.println(name + " -> " + bean.getClass().getName()));

        // 2) 자동 구성으로 흔히 생기는 빈 중 하나 확인: RequestMappingHandlerMapping (Spring MVC)
        boolean hasHandlerMapping = context.containsBean("requestMappingHandlerMapping");
        System.out.println("Has requestMappingHandlerMapping? " + hasHandlerMapping);

        // 3) 내장 서버/웹 관련 자동 구성이 작동했는지 간접 확인 (웹 앱이면 Environment에 웹 관련 프로퍼티가 많아집니다)
        System.out.println("Active profiles: " + String.join(",", context.getEnvironment().getActiveProfiles()));
    }
}

실행해 볼 것

  • GET http://localhost:8080/hello가 동작하면
    • @ComponentScan으로 HelloController가 잡혔고
    • spring-boot-starter-web 기반 자동 구성으로 Spring MVC/내장 톰캣 구성이 올라온 것입니다.
  • 콘솔에서 requestMappingHandlerMapping 존재 여부가 true면 Spring MVC 자동 구성이 정상적으로 적용된 흐름을 확인할 수 있습니다.
flowchart TD
  A["main()"] --> B["SpringApplication.run()"]
  B --> C["ApplicationContext 생성"]
  C --> D["@ComponentScan: 애플리케이션 빈 탐색"]
  C --> E["AutoConfiguration: 조건 만족 시 외부 기반 빈 등록"]
  D --> F["내가 만든 Controller/Service 빈 등록"]
  E --> G["웹/DB 등 인프라 빈 등록"]
  F --> H["서버 기동 및 요청 처리 준비 완료"]
  G --> H

Spring Boot 실행 흐름 요약 다이어그램

메인 클래스 실행 이후, “내 코드 빈(스캔)”과 “외부 기반 빈(자동 구성)”이 합쳐져 컨텍스트가 완성됩니다.

실무 팁

💡 실무에서는: “빈이 안 잡힌다”의 80%는 패키지/스캔 문제예요

  • 컨트롤러/서비스를 만들었는데 주입이 안 되면, 먼저 클래스가 메인 클래스 패키지 하위에 있는지부터 확인해 보세요.
  • 멀티 모듈에서 특히 많이 터집니다. api 모듈의 메인 클래스가 com.company.api인데, core 모듈이 com.company.core로 분리되어 있으면 기본 스캔으로는 core가 안 잡힐 수 있어요. 이 경우는 패키지 정렬(공통 루트 패키지)로 해결하는 편이 깔끔합니다.

💡 실무에서는: 자동 구성은 “편리하지만 투명하게” 다뤄야 합니다

  • 원인을 모르고 “그냥 된다”에 기대면, 운영 이슈에서 디버깅이 어려워집니다.
  • 최소한 아래 2가지는 습관처럼 확인해 보세요.
    1. 어떤 스타터를 추가했는지(= 어떤 자동 구성이 열릴지)
    2. 어떤 프로퍼티를 줬는지(= 자동 구성 조건이 바뀌는지)
  • 필요하면 --debug 옵션으로 실행해 “어떤 자동 구성이 적용/미적용됐는지(Condition Evaluation)”를 확인할 수 있습니다. (로그가 많으니 문제 상황에서만 쓰는 게 좋아요.)

핵심 요약

  • @SpringBootApplication은 설정 시작점 + 컴포넌트 스캔 + 자동 구성을 한 번에 켭니다.
  • 컴포넌트 스캔 범위는 “메인 클래스 패키지 하위”가 기본 규칙입니다.
  • 자동 구성은 클래스패스/설정값 조건에 따라 인프라 빈을 알아서 등록합니다.

다음 글: [#04 application.yml 설정 기본기]