hobokai 님의 블로그

Spring Boot 완전 가이드 Part 2: 설정과 자동구성 본문

Backend

Spring Boot 완전 가이드 Part 2: 설정과 자동구성

hobokai 2025. 8. 6. 10:34

시리즈 소개: 실무에서 바로 써먹는 Spring Boot 완전 가이드

Part 1: Spring Boot 기초 - 시작하기
Part 2: 설정과 자동구성 ← 현재
Part 3: 웹 개발과 REST API
Part 4: 데이터 액세스와 배치
Part 5: 운영과 모니터링


🎯 Auto Configuration 이해하기

Spring Boot의 마법: Auto Configuration

Part 1에서 보신 것처럼, Spring Boot에서는 단 한 줄의 설정도 없이 웹 애플리케이션이 동작했습니다. 이것이 어떻게 가능할까요?

@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

이 간단한 코드 뒤에는 복잡한 자동 설정 메커니즘이 숨어있습니다.

Auto Configuration 동작 원리

1. @SpringBootApplication 분해

@SpringBootApplication
// 실제로는 이 3개의 조합입니다:
@SpringBootConfiguration  // = @Configuration
@EnableAutoConfiguration  // ← 자동 설정의 핵심!
@ComponentScan           // 컴포넌트 스캔
public class DemoApplication { ... }

2. @EnableAutoConfiguration 동작 과정

// Spring Boot가 내부적으로 하는 일들
@EnableAutoConfiguration
public class DemoApplication {
    // 1. 클래스패스에서 META-INF/spring.factories 파일 스캔
    // 2. AutoConfiguration 클래스들 발견
    // 3. @Conditional 조건 확인
    // 4. 조건 만족 시 자동으로 빈 등록
}

실제 Auto Configuration 예시

WebMvcAutoConfiguration 살펴보기

@Configuration
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class })
public class WebMvcAutoConfiguration {
    
    @Configuration
    @Import(EnableWebMvcConfiguration.class)
    @EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
    @Order(0)
    public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
        
        @Bean
        @ConditionalOnMissingBean
        public InternalResourceViewResolver defaultViewResolver() {
            // 뷰 리졸버 자동 설정
        }
        
        @Bean
        @ConditionalOnMissingBean
        @ConditionalOnProperty(prefix = "spring.mvc", name = "pathmatch.matching-strategy")
        public PathMatcher mvcPathMatcher() {
            // 경로 매칭 전략 설정
        }
    }
}

동작 조건 분석:

  • @ConditionalOnWebApplication: 웹 애플리케이션일 때만
  • @ConditionalOnClass: 해당 클래스가 클래스패스에 있을 때만
  • @ConditionalOnMissingBean: 사용자가 정의하지 않았을 때만

Auto Configuration 우선순위

// 사용자 설정 > Auto Configuration
@Configuration
public class MyWebConfig implements WebMvcConfigurer {
    
    @Bean
    public InternalResourceViewResolver myViewResolver() {
        // 이 빈이 있으면 Auto Configuration의 defaultViewResolver는 생성되지 않음
        return new InternalResourceViewResolver();
    }
}

⚙️ application.properties/yml 완전 정복

Properties vs YAML 비교

application.properties

# 서버 설정
server.port=8080
server.servlet.context-path=/api

# 데이터베이스 설정
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=secret

# JPA 설정
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true

# 로깅 설정
logging.level.com.example=DEBUG
logging.level.org.springframework.security=DEBUG

application.yml (권장)

server:
  port: 8080
  servlet:
    context-path: /api

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/mydb
    username: root
    password: secret
    
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true

logging:
  level:
    com.example: DEBUG
    org.springframework.security: DEBUG

환경별 설정 파일 분리

파일 구조

src/main/resources/
├── application.yml              # 공통 설정
├── application-local.yml        # 로컬 개발
├── application-dev.yml          # 개발 서버
├── application-test.yml         # 테스트 환경
└── application-prod.yml         # 운영 환경

application.yml (공통)

# 모든 환경 공통 설정
spring:
  application:
    name: demo-app
    
  jpa:
    hibernate:
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

management:
  endpoints:
    web:
      exposure:
        include: health,info,metrics

---
# 활성 프로파일 설정
spring:
  config:
    activate:
      on-profile: local
  profiles:
    include: local

application-local.yml

server:
  port: 8080

spring:
  datasource:
    url: jdbc:h2:mem:testdb
    username: sa
    password: 
    driver-class-name: org.h2.Driver
    
  h2:
    console:
      enabled: true
      path: /h2-console
      
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true
    properties:
      hibernate:
        format_sql: true

logging:
  level:
    com.example: DEBUG
    org.springframework.web: DEBUG

application-prod.yml

server:
  port: 8080
  compression:
    enabled: true
    mime-types: text/html,text/xml,text/plain,text/css,application/json,application/javascript
    min-response-size: 1024

spring:
  datasource:
    url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:myapp}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    hikari:
      maximum-pool-size: 20
      minimum-idle: 5
      
  jpa:
    hibernate:
      ddl-auto: validate
    show-sql: false
    open-in-view: false

logging:
  level:
    com.example: INFO
    org.springframework.security: WARN
  file:
    name: logs/application.log
  logback:
    rollingpolicy:
      max-file-size: 100MB
      max-history: 30

프로파일 활성화 방법들

# 1. JAR 실행 시
java -jar app.jar --spring.profiles.active=prod

# 2. JVM 시스템 프로퍼티
java -Dspring.profiles.active=prod -jar app.jar

# 3. 환경 변수
export SPRING_PROFILES_ACTIVE=prod
java -jar app.jar

# 4. application.yml에서 설정
spring:
  profiles:
    active: dev

# 5. IDE에서 설정 (IntelliJ)
# Run Configuration → Environment Variables → SPRING_PROFILES_ACTIVE=local

설정값 우선순위

Spring Boot는 다음 순서로 설정값을 적용합니다:

1. 커맨드 라인 아규먼트
2. 시스템 프로퍼티 (System.getProperty())  
3. 환경 변수
4. application-{profile}.yml
5. application.yml
6. @PropertySource
7. 기본값

예시:

# 최우선 순위: 커맨드 라인
java -jar app.jar --server.port=9090 --spring.profiles.active=prod

# 두 번째 우선순위: 시스템 프로퍼티  
java -Dserver.port=8090 -jar app.jar

# 환경 변수로도 설정 가능
export SERVER_PORT=7090

🏗️ 커스텀 Configuration 클래스

기본 Configuration 클래스

@Configuration
@EnableConfigurationProperties
public class AppConfig {
    
    @Bean
    @Primary
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        mapper.configure(PropertyNamingStrategies.SNAKE_CASE);
        mapper.registerModule(new JavaTimeModule());
        return mapper;
    }
    
    @Bean
    public RestTemplate restTemplate() {
        RestTemplate restTemplate = new RestTemplate();
        restTemplate.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
        
        // 타임아웃 설정
        HttpComponentsClientHttpRequestFactory factory = 
            (HttpComponentsClientHttpRequestFactory) restTemplate.getRequestFactory();
        factory.setConnectTimeout(5000);
        factory.setReadTimeout(10000);
        
        return restTemplate;
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

환경별 Configuration

@Configuration
@Profile("prod")  // 운영환경에서만 활성화
public class ProductionConfig {
    
    @Bean
    @Primary
    public DataSource dataSource() {
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl(System.getenv("DB_URL"));
        config.setUsername(System.getenv("DB_USERNAME"));
        config.setPassword(System.getenv("DB_PASSWORD"));
        config.setMaximumPoolSize(20);
        config.setMinimumIdle(5);
        config.setConnectionTimeout(30000);
        return new HikariDataSource(config);
    }
}

@Configuration
@Profile("local")  // 로컬 개발환경에서만
public class LocalConfig {
    
    @Bean
    @Primary
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .build();
    }
    
    @Bean
    public CommandLineRunner initData(UserRepository userRepository) {
        return args -> {
            // 테스트 데이터 초기화
            userRepository.save(new User("admin", "admin@test.com"));
            userRepository.save(new User("user", "user@test.com"));
        };
    }
}

🔀 조건부 빈 등록 (@Conditional)

주요 @Conditional 어노테이션들

@Configuration
public class ConditionalConfig {
    
    // 1. 클래스 존재 여부 확인
    @Bean
    @ConditionalOnClass(RedisTemplate.class)
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(jedisConnectionFactory());
        return template;
    }
    
    // 2. 프로퍼티 값 확인
    @Bean
    @ConditionalOnProperty(name = "app.cache.type", havingValue = "redis")
    public CacheManager redisCacheManager() {
        RedisCacheManager.Builder builder = RedisCacheManager
                .RedisCacheManagerBuilder
                .fromConnectionFactory(jedisConnectionFactory());
        return builder.build();
    }
    
    // 3. 빈이 없을 때만 생성
    @Bean
    @ConditionalOnMissingBean(CacheManager.class)
    public CacheManager defaultCacheManager() {
        return new ConcurrentMapCacheManager();
    }
    
    // 4. 웹 환경일 때만
    @Bean
    @ConditionalOnWebApplication
    public FilterRegistrationBean<CorsFilter> corsFilter() {
        FilterRegistrationBean<CorsFilter> registration = new FilterRegistrationBean<>();
        registration.setFilter(new CorsFilter());
        registration.addUrlPatterns("/*");
        return registration;
    }
    
    // 5. 특정 프로파일에서만
    @Bean
    @ConditionalOnProfile("!prod")  // 운영 환경이 아닐 때만
    public MockExternalApiService mockApiService() {
        return new MockExternalApiService();
    }
}

커스텀 Conditional 만들기

// 커스텀 Condition 클래스
public class LinuxCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        return context.getEnvironment()
                .getProperty("os.name", "")
                .toLowerCase()
                .contains("linux");
    }
}

// 커스텀 어노테이션
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Conditional(LinuxCondition.class)
public @interface ConditionalOnLinux {
}

// 사용법
@Configuration
public class OsSpecificConfig {
    
    @Bean
    @ConditionalOnLinux
    public LinuxSpecificService linuxService() {
        return new LinuxSpecificService();
    }
    
    @Bean
    @ConditionalOnProperty(name = "os.name", havingValue = "Windows")
    public WindowsSpecificService windowsService() {
        return new WindowsSpecificService();
    }
}

📋 외부 설정 바인딩 (@ConfigurationProperties)

기본 사용법

Properties 정의

# application.yml
app:
  name: Demo Application
  version: 1.0.0
  api:
    timeout: 5000
    retries: 3
    base-url: https://api.example.com
  features:
    - user-management
    - notification
    - analytics
  database:
    max-connections: 20
    idle-timeout: 300

Configuration Properties 클래스

@ConfigurationProperties(prefix = "app")
@Data
@Component
public class AppProperties {
    
    private String name;
    private String version;
    private Api api = new Api();
    private List<String> features = new ArrayList<>();
    private Database database = new Database();
    
    @Data
    public static class Api {
        private int timeout = 5000;
        private int retries = 3;
        private String baseUrl;
    }
    
    @Data
    public static class Database {
        private int maxConnections = 10;
        private int idleTimeout = 300;
    }
}

검증(Validation) 추가

@ConfigurationProperties(prefix = "app")
@Validated  // 검증 활성화
@Data
@Component
public class ValidatedAppProperties {
    
    @NotBlank(message = "애플리케이션 이름은 필수입니다")
    private String name;
    
    @Pattern(regexp = "^\\d+\\.\\d+\\.\\d+$", message = "버전 형식이 올바르지 않습니다")
    private String version;
    
    @Valid  // 중첩 객체 검증
    private Api api = new Api();
    
    @Data
    public static class Api {
        @Min(value = 1000, message = "타임아웃은 최소 1000ms 이상이어야 합니다")
        @Max(value = 30000, message = "타임아웃은 최대 30000ms 이하여야 합니다")
        private int timeout = 5000;
        
        @Min(value = 1, message = "재시도 횟수는 최소 1회 이상이어야 합니다")
        @Max(value = 10, message = "재시도 횟수는 최대 10회 이하여야 합니다")
        private int retries = 3;
        
        @URL(message = "올바른 URL 형식이어야 합니다")
        @NotBlank(message = "API Base URL은 필수입니다")
        private String baseUrl;
    }
}

Properties 사용하기

@Service
@RequiredArgsConstructor
public class ExternalApiService {
    
    private final AppProperties appProperties;
    private final RestTemplate restTemplate;
    
    public ApiResponse callApi(String endpoint) {
        String url = appProperties.getApi().getBaseUrl() + endpoint;
        
        // 타임아웃 설정
        HttpComponentsClientHttpRequestFactory factory = 
            new HttpComponentsClientHttpRequestFactory();
        factory.setConnectTimeout(appProperties.getApi().getTimeout());
        factory.setReadTimeout(appProperties.getApi().getTimeout());
        
        RestTemplate customRestTemplate = new RestTemplate(factory);
        
        // 재시도 로직
        int retries = appProperties.getApi().getRetries();
        for (int i = 0; i < retries; i++) {
            try {
                return customRestTemplate.getForObject(url, ApiResponse.class);
            } catch (Exception e) {
                if (i == retries - 1) {
                    throw new ExternalApiException("API 호출 실패: " + e.getMessage(), e);
                }
                try {
                    Thread.sleep(1000 * (i + 1)); // 지수 백오프
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException(ie);
                }
            }
        }
        return null;
    }
}

복잡한 설정 매핑

# application.yml
app:
  security:
    jwt:
      secret: mySecretKey
      expiration: 86400
      refresh-expiration: 604800
    cors:
      allowed-origins:
        - http://localhost:3000
        - https://mydomain.com
      allowed-methods:
        - GET
        - POST
        - PUT
        - DELETE
      allowed-headers:
        - "*"
    rate-limit:
      enabled: true
      requests-per-minute: 100
      burst-capacity: 20
@ConfigurationProperties(prefix = "app.security")
@Validated
@Data
@Component
public class SecurityProperties {
    
    @Valid
    private Jwt jwt = new Jwt();
    
    @Valid
    private Cors cors = new Cors();
    
    @Valid
    private RateLimit rateLimit = new RateLimit();
    
    @Data
    public static class Jwt {
        @NotBlank
        private String secret;
        
        @Min(3600)  // 최소 1시간
        private long expiration = 86400;
        
        @Min(86400)  // 최소 1일
        private long refreshExpiration = 604800;
    }
    
    @Data
    public static class Cors {
        @NotEmpty
        private List<String> allowedOrigins = new ArrayList<>();
        
        @NotEmpty
        private List<String> allowedMethods = Arrays.asList("GET", "POST");
        
        private List<String> allowedHeaders = Arrays.asList("*");
    }
    
    @Data
    public static class RateLimit {
        private boolean enabled = false;
        
        @Min(1)
        private int requestsPerMinute = 100;
        
        @Min(1)
        private int burstCapacity = 20;
    }
}

🔧 Configuration 실전 팁

1. 환경 변수 활용

# application-prod.yml
spring:
  datasource:
    url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    
server:
  port: ${SERVER_PORT:8080}
  
logging:
  level:
    com.example: ${LOG_LEVEL:INFO}
# Docker 환경에서
docker run -e DB_HOST=prod-db-server \
          -e DB_USERNAME=app_user \
          -e DB_PASSWORD=secret123 \
          -e LOG_LEVEL=DEBUG \
          myapp:latest

2. 설정 암호화

# application.yml (Jasypt 사용)
spring:
  datasource:
    password: ENC(encrypted_password_here)
    
app:
  api:
    secret-key: ENC(encrypted_api_key_here)
// JasyptConfig.java
@Configuration
public class JasyptConfig {
    
    @Bean("jasyptStringEncryptor")
    public StringEncryptor stringEncryptor() {
        PooledPBEStringEncryptor encryptor = new PooledPBEStringEncryptor();
        SimpleStringPBEConfig config = new SimpleStringPBEConfig();
        config.setPassword(System.getenv("JASYPT_PASSWORD"));
        config.setAlgorithm("PBEWithMD5AndDES");
        config.setKeyObtentionIterations("1000");
        config.setPoolSize("1");
        config.setProviderName("SunJCE");
        config.setSaltGeneratorClassName("org.jasypt.salt.RandomSaltGenerator");
        config.setStringOutputType("base64");
        encryptor.setConfig(config);
        return encryptor;
    }
}

3. 설정 테스트

@SpringBootTest
@TestPropertySource(properties = {
    "app.name=Test Application",
    "app.api.timeout=1000",
    "app.features[0]=test-feature"
})
class AppPropertiesTest {
    
    @Autowired
    private AppProperties appProperties;
    
    @Test
    void should_LoadConfigurationProperties() {
        assertThat(appProperties.getName()).isEqualTo("Test Application");
        assertThat(appProperties.getApi().getTimeout()).isEqualTo(1000);
        assertThat(appProperties.getFeatures()).contains("test-feature");
    }
}

@TestConfiguration
public class TestConfig {
    
    @Bean
    @Primary
    public AppProperties testAppProperties() {
        AppProperties properties = new AppProperties();
        properties.setName("Test App");
        return properties;
    }
}

🎯 Part 2 정리

핵심 포인트 요약

  1. Auto Configuration:
    • @ConditionalOnXxx 어노테이션으로 조건부 빈 등록
    • 사용자 설정이 Auto Configuration보다 우선
    • spring.factories 파일로 자동 설정 클래스 등록
  2. 설정 관리:
    • YAML 형식 권장 (가독성, 계층 구조)
    • 프로파일별 설정 파일 분리
    • 설정값 우선순위 이해
  3. Configuration Properties:
    • @ConfigurationProperties로 타입 안전한 설정
    • @Validated로 설정값 검증
    • 환경 변수와 연동
  4. 조건부 설정:
    • @ConditionalOnXxx로 상황별 빈 등록
    • 커스텀 Condition 구현 가능
    • 프로파일별 다른 설정 적용

실습 체크리스트

  •  Auto Configuration 동작 원리 이해
  •  환경별 설정 파일 분리 (local, dev, prod)
  •  @ConfigurationProperties 클래스 작성
  •  설정값 검증 (@Validated) 적용
  •  조건부 빈 등록 실습
  •  환경 변수 활용한 설정 외부화
  •  설정 암호화 적용
  •  설정 테스트 작성

다음 편 예고

Part 3: 웹 개발과 REST API에서는:

  • Spring MVC vs WebFlux 선택 가이드
  • REST API 설계 Best Practices
  • 예외 처리와 검증 전략
  • Spring Security 보안 설정
  • API 문서화와 테스트 자동화

실무에서 바로 사용할 수 있는 웹 API 개발 노하우를 공유합니다! 🌐


📚 참고 자료:

🏷️ 태그: #SpringBoot #Configuration #AutoConfiguration #Properties #실무가이드