자주 쓰는 Config

소개

Configuration으로 자주 사용한 설정을 적어놓은 문서다.
상황에 따라 커스터마이징 필요

SecurityConfig

java
@Configuration @EnableWebSecurity @EnableMethodSecurity @RequiredArgsConstructor public class SecurityConfig { private final LoggingFilter loggingFilter; private final JwtAuthenticationFilter jwtAuthenticationFilter; @Value("${spring.profiles.active}") private String activeProfile; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { // path의 권한설정 http .headers(headers -> headers .frameOptions(frameOptions -> frameOptions.sameOrigin())) // 동일한 origin만 frameOption 허용 .csrf(AbstractHttpConfigurer::disable) // csrf 비허용 .cors(cors -> cors.configurationSource(corsConfigurationSource())) // CORS 설정 .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/**").permitAll() // 해당 URL 패턴은 허용 .anyRequest().authenticated() // 그 외 URL은 인증 필요 ) .addFilterBefore(loggingFilter, LogoutFilter.class) // Filter 설정. 적은 순서대로 Filter가 적용된다. .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } // WebMvcConfigurer가 아닌 SecurityFilter에 설정하는 이유는 // SecurityFilter에서 먼저 검사 후, WebMvcConfigurer 검사를 할 수 있기 때문 public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); // prd 프로필일 때는 CORS 비활성화 // 동일한 origin의 경우만 실행되도록 if ("prd".equals(activeProfile)) { configuration.setAllowedOrigins(List.of()); // 빈 리스트로 설정하여 모든 Origin 차단 return new UrlBasedCorsConfigurationSource(); } // 개발 환경에서 CORS 허용 configuration.setAllowedOriginPatterns(List.of("*")); configuration.setAllowedMethods(Arrays.asList( "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS" )); configuration.setAllowedHeaders(Arrays.asList( "Authorization", "Content-Type", "X-Requested-With", "Accept", "Origin", "Access-Control-Request-Method", "Access-Control-Request-Headers" )); configuration.setExposedHeaders(Arrays.asList( "Authorization", "Content-Type" )); configuration.setAllowCredentials(true); configuration.setMaxAge(3600L); // 1시간 UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } }

WebMvcConfig

  • Interceptor, ArgumentResolver
java
@Configuration @RequiredArgsConstructor public class WebMvcConfig implements WebMvcConfigurer { private final CustomInterceptor customInterceptor; private final CustomArgsResolver customArgsResolver; @Override public void addInterceptors(InterceptorRegistry registry) { // Interceptor 넣을 때 적용할 path pattern을 관리 // addPathPatterns에 들어가있지만 예외처리를 두고 싶을 때 // excludePathPatterns에 넣는다 registry.addInterceptor(customInterceptor) .addPathPatterns("/api/v1/**") .excludePathPatterns("/api/v1/status"); } @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { // 특정 클래스타입의 매개변수를 접근하고자 하는 ArgumentResolver를 추가할 때 사용 resolvers.add(customArgsResolver); } @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 서버 내 resources를 사용할 경우 추가 // URL path : /files/경로/파일명 registry.addResourceHandler("/files/**").addResourceLocations("file:///files/"); } }

AsyncConfig

  • Async 관련 설정
java
@Configuration @EnableAsync public class AsyncConfig implements AsyncConfigurer { private final int CORE_POOL_SIZE = 3; private final int MAX_POOL_SIZE = 10; private final int QUEUE_CAPACITY = 10000; @Bean(name = "asyncExecutor") public Executor threadPoolTaskExecutor(){ // @EnableAsync는 스레스 생성하는 역할만 하므로, 쓰레드 관리 코드 작성 ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); taskExecutor.setCorePoolSize(CORE_POOL_SIZE); taskExecutor.setMaxPoolSize(MAX_POOL_SIZE); taskExecutor.setQueueCapacity(QUEUE_CAPACITY); taskExecutor.setThreadNamePrefix("ASYNC_TASK-"); // 데코레이터 적용 taskExecutor.setTaskDecorator(runnable -> { // Interface TaskDecorator로 처리 //현재 요청의 RequestAttribute를 가져옴 RequestAttributes attributes = RequestContextHolder.currentRequestAttributes(); return () -> { try{ //작업 실행 전에 RequestAttributes를 설정 RequestContextHolder.setRequestAttributes(attributes); //작업 실행 runnable.run(); } finally{ //작업 실행 후에 RequestAttributes를 제거 RequestContextHolder.resetRequestAttributes(); } }; }); // 거부 작업 처리 taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); return taskExecutor; } @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler(){ // 핸들러 생성해 예외처리 // Interface AsyncUncaughtExceptionHandler 로 처리 return (ex, method, params) -> { }; } }

SchedulingConfig

  • Schedule 관련 설정
java
@Configuration @EnableScheduling public class SchedulingConfig implements SchedulingConfigurer { @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { // @EnableScheduling 설정시 기본 사이즈가 1로 설정 되어있으므로, 별도의 풀사이즈 지정할 때 사용 ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(5); scheduler.initialize(); taskRegistrar.setTaskScheduler(scheduler); } }

RedisConfig

  • Redis 설정
java
@Configuration public class RedisConfig { // Spring boot 실행시 자동으로 spring.data.redis 정보를 토대로 RedisConnectionFactory Bean이 설정된다 // Custom RedisConnectionFactory를 만들때 new LettuceConnectionFactory를 사용하면 된다. @Value("${spring.data.redis.host:localhost}") private String host; @Value("${spring.data.redis.port:6379}") private int port; @Value("${spring.data.redis.password:}") private String password; // Redis template @Bean public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<?, ?> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(connectionFactory); //connection ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL); objectMapper.registerModule(new JavaTimeModule()); // LocalDateTime 등 지원 // JSON 직렬화 설정 RedisSerializer<Object> jsonSerializer = RedisSerializer.json(); // Key는 String으로 직렬화 redisTemplate.setKeySerializer(RedisSerializer.string()); redisTemplate.setHashKeySerializer(RedisSerializer.string()); // Value는 JSON으로 직렬화 redisTemplate.setValueSerializer(jsonSerializer); redisTemplate.setHashValueSerializer(jsonSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } // pub/sub message listener @Bean public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); MessageListenerAdapter adapter = new MessageListenerAdapter(new RedisPubSubService.RedisListenerService(), "onMessage"); container.addMessageListener(adapter, new ChannelTopic("public")); return container; } // Distributed Lock(분산락)을 위한 Redisson Client @Bean public RedissonClient redissonClient() { Config config = new Config(); // 가성비 포인트: 단일 서버 모드 사용 (2~4대 WAS 환경에 적합) String address = "redis://" + host + ":" + port; config.useSingleServer() .setAddress(address) .setConnectionMinimumIdleSize(5) // 최소 유휴 커넥션 .setConnectionPoolSize(20); // 최대 커넥션 if (password != null && !password.isEmpty()) { config.useSingleServer().setPassword(password); } return Redisson.create(config); } }

AWS Config

  • AWS에 필요한 Config
java
@Configuration public class AwsConfig { @Value("${cloud.aws.credentials.accessKey") private String accessKey; @Value("${cloud.aws.credentials.secretKey}") private String secretKey; @Value("${cloud.aws.region.static}") private String region; @Bean public S3Client amazonS3Client() { AwsBasicCredentials credentials = AwsBasicCredentials.create(accessKey, secretKey); return S3Client.builder() .region(Region.of(region)) .credentialsProvider(StaticCredentialsProvider.create(credentials)) .build(); } }

BaseConfig

  • 그 외 Bean 함수 모음
java
@Configuration @EnableJpaAuditing public class BaseConfig { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public ObjectMapper objectMapper() { return new ObjectMapper(); } @Bean public RestTemplate restTemplate() { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); factory.setConnectTimeout(5000); // 연결 타임아웃 5초 factory.setReadTimeout(10000); // 읽기 타임아웃 10초 RestTemplate restTemplate = new RestTemplate(factory; return restTemplate; } }

Filter & JWT

JwtTokenProvider

  • JWT토큰 생성, 검증, Claim 정보를 가져올 수 있는 유틸성 클래스
groovy
// 필수 라이브러리 implementation 'io.jsonwebtoken:jjwt-api:0.12.5' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'
java
@Component public class JwtTokenProvider { private final SecretKey secretKey; public JwtTokenProvider( @Value("${jwt.secret}") String secret) { this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); } /** * 토큰 생성 */ public String createToken(String username, String role, Long milliseconds) { Date now = new Date(); Date validity = new Date(now.getTime() + milliseconds); return Jwts.builder() .subject(username) .claim("role", role) .issuedAt(now) .expiration(validity) .signWith(secretKey) .compact(); } /** * 토큰에서 사용자명 추출 */ public String getUsername(String token) { return getClaims(token).getSubject(); } /** * 토큰에서 역할 추출 */ public String getRole(String token) { return getClaims(token).get("role", String.class); } /** * 토큰에서 역할 추출 */ public Long getExpiration(String token) { return getClaims(token).getExpiration().getTime(); } /** * 토큰 유효성 검증 */ public boolean validateToken(String token) { try { Claims claims = getClaims(token); return !claims.getExpiration().before(new Date()); } catch (Exception e) { return false; } } private Claims getClaims(String token) { return Jwts.parser() .verifyWith(secretKey) .build() .parseSignedClaims(token) .getPayload(); } }

JwtAuthenticationFilter

  • Filter 에서 JWT토큰을 검증한다.
  • 검증되면 Authentication에 값 저장
java
@Component public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenProvider jwtTokenProvider; public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) { this.jwtTokenProvider = jwtTokenProvider; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = extractToken(request); if (token != null && jwtTokenProvider.validateToken(token)) { String username = jwtTokenProvider.getUsername(token); String role = jwtTokenProvider.getRole(token); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( username, null, Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + role)) ); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); } private String extractToken(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (bearerToken != null && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } // 아래는 필요할 경우에 사용 // Resilience4j 사용 // Redis가 죽으면 fallbackMethod인 checkRedisFallback이 실행됩니다. @CircuitBreaker(name = "redisCheck", fallbackMethod = "checkRedisFallback") private boolean isBlacklisted(String token) { // 캐시(Redis)에 'logout' 상태로 등록된 토큰인지 확인 // 존재하지 않으면(null) 로그아웃되지 않은 정상 토큰임 return Boolean.TRUE.equals(redisTemplate.hasKey("BL:" + token)); } // Redis 장애 시 실행될 로직: 무조건 "블랙리스트 아님"을 반환하여 통과시킴 private boolean checkRedisFallback(String token, Throwable t) { log.error("Redis 장애 발생! JWT 검증만으로 인증을 진행합니다. 원인: {}", t.getMessage()); return false; } }

LoggingFilter

java
@Order(2) // traceIdFilter를 사용한다면 추가 @Slf4j @Component public class LoggingFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // mutipart 요청은 래핑하지 않도록 변경 String contentType = request.getContentType(); if(contentType != null && contentType.startsWith("multipart/")) { filterChain.doFilter(request, response); return; } // 여기서 딱 한 번만 래핑합니다. MultiReadRequestWrapper requestWrapper = new MultiReadRequestWrapper(request); MultiReadResponseWrapper responseWrapper = new MultiReadResponseWrapper(response); try { filterChain.doFilter(requestWrapper, responseWrapper); responseWrapper.copyBodyToResponse(); } finally { logRequestAndResponse(requestWrapper, responseWrapper); } } private void logRequestAndResponse(MultiReadRequestWrapper request, MultiReadResponseWrapper response) throws IOException { String requestBody = new String(request.getContentAsByteArray(), StandardCharsets.UTF_8); String responseBody = new String(response.getContentAsByteArray(), StandardCharsets.UTF_8); log.info("Request: [{} {}] | Body: {}", request.getMethod(), request.getRequestURI(), requestBody); log.info("Response: [Status {}] | Body: {}", response.getStatus(), responseBody); } }

resources 설정

application.yaml

yaml
spring: application: name: backend # 1. 데이터베이스: MariaDB & HikariCP 최적화 datasource: # useSSL=false: 내부망 통신 시 불필요한 핸드쉐이크 비용 절감 (가성비) # serverTimezone=UTC: 글로벌 서비스 및 데이터 일관성을 위한 표준 시간대 설정 url: jdbc:mariadb://localhost:3306/mydb?useSSL=false&characterEncoding=UTF-8&serverTimezone=UTC driver-class-name: org.mariadb.jdbc.Driver username: admin password: password hikari: pool-name: CommonHikariCP # maximum-pool-size: 동시 접속자 대응력. 너무 크면 DB 부하, 작으면 대기 발생 (보통 CPU 코어수와 연동) maximum-pool-size: 20 # minimum-idle: 유휴 커넥션 유지 수. 0보다 크게 설정하여 첫 요청 응답 속도 최적화 minimum-idle: 10 # idle-timeout: 사용되지 않는 커넥션 폐기 시간 (300초). 리소스 반환을 통한 가성비 확보 idle-timeout: 300000 # connection-timeout: 풀에서 커넥션을 받기 위해 기다리는 최대 시간 (20초). 장애 전파 방지 connection-timeout: 20000 # max-lifetime: 커넥션 최대 유지 시간 (10분). DB 측의 강제 연결 끊김 방지를 위해 DB 설정보다 짧게 설정 max-lifetime: 600000 # 2. Redis & NoSQL (MongoDB) data: redis: host: localhost port: 6379 lettuce: # shutdown-timeout: 앱 종료 시 Redis 연결을 정리하는 대기 시간. 우아한 종료(Graceful) 지원 shutdown-timeout: 100ms mongodb: # authSource=admin: 보안을 위해 관리자 계정 정보를 별도 DB에서 관리하는 모범 사례 적용 uri: mongodb://admin::password@localhost:27017/mydb?authSource=admin # 3. JPA/Hibernate: 성능과 디버깅의 균형 jpa: # open-in-view: false. 뷰 렌더링 시점까지 커넥션을 잡고 있지 않아 DB 커넥션 효율 극대화 (시니어 권장) open-in-view: false hibernate: # ddl-auto: 운영 환경 데이터 유실 방지를 위해 반드시 'none' 또는 'validate' 사용 (보안) ddl-auto: none properties: hibernate: # default_batch_fetch_size: 연관 엔티티 조회 시 IN 절을 사용하여 N+1 문제를 원천 차단 (성능) default_batch_fetch_size: 100 # format_sql: false. 운영 로그 부하 및 가독성을 위해 해제. 필요 시 개발 프로파일에서 true format_sql: false # jdbc.batch_size: 여러 INSERT/UPDATE 문을 묶어서 실행하여 DB 네트워크 IO 횟수 감소 (성능) jdbc.batch_size: 30 # 4. 보안 및 웹 설정 servlet: multipart: # 파일 업로드 제한: 서버 자원 고갈(DoS) 공격 방지 (보안 및 가성비) max-file-size: 10MB max-request-size: 10MB jackson: # default-property-inclusion: non_null. JSON 응답에서 null 필드를 제거하여 전송량 감소 및 구조 보안 확보 default-property-inclusion: non_null serialization: # fail-on-empty-beans: 비어있는 객체 직렬화 시 에러 방지 (예외 처리 유연성) fail-on-empty-beans: false # write-dates-as-timestamps: false. 날짜를 [2026, 3, 13] 배열 대신 "2026-03-13..." 표준 규격으로 출력 write-dates-as-timestamps: false server: port: 8080 # shutdown: graceful. 진행 중인 요청을 안전하게 처리한 뒤 종료 (무중단 배포 시 필수) shutdown: graceful servlet: encoding: charset: UTF-8 enabled: true force: true # 5. 모니터링: 액추에이터 보안 강화 management: endpoints: web: exposure: # prometheus: 모니터링(Grafana 등) 연동용. loggers: 런타임에 로그 레벨 동적 변경용 include: health, info, prometheus, loggers endpoint: health: # show-details: 인증된 사용자(또는 특정 조건)에게만 DB 상태 등 상세 정보 노출 (보안) show-details: when_authorized probes: # enabled: true. K8S의 Liveness/Readiness 체크에 대응하여 인프라 자동 복구 지원 enabled: true

logback-spring.xml

xml
<?xml version="1.0" encoding="UTF-8"?> <configuration> <property name="LOG_PATH" value="${LOG_PATH:-/home/ec2-user/project/logs}"/> <property name="LOG_FILE" value="server"/> <property name="LOG_PATTERN" value="[%d{yyyy-MM-dd HH:mm:ss.SSS}] [%thread] %-5level traceId=%X{traceId} %logger{36}.%M\(%line\) - %msg%n"/> <springProfile name="prd"> <property name="LOG_LEVEL" value="INFO"/> </springProfile> <springProfile name="!prd"> <property name="LOG_LEVEL" value="DEBUG"/> </springProfile> <!-- 콘솔 출력 설정 --> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>${LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> </appender> <!-- 파일 출력 설정 --> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_PATH}/${LOG_FILE}.log</file> <encoder> <pattern>${LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>${LOG_PATH}/${LOG_FILE}.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <maxHistory>90</maxHistory> <maxFileSize>500MB</maxFileSize> </rollingPolicy> </appender> <!-- 에러 로그 설정 --> <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>${LOG_PATH}/${LOG_FILE}-error.log</file> <filter class="ch.qos.logback.classic.filter.ThresholdFilter"> <level>ERROR</level> </filter> <encoder> <pattern>${LOG_PATTERN}</pattern> <charset>UTF-8</charset> </encoder> <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy"> <fileNamePattern>${LOG_PATH}/${LOG_FILE}-error.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <maxHistory>90</maxHistory> <maxFileSize>500MB</maxFileSize> </rollingPolicy> </appender> <root level="${LOG_LEVEL}"> <appender-ref ref="CONSOLE"/> <appender-ref ref="FILE"/> <appender-ref ref="ERROR_FILE"/> </root> </configuration>

로그 traceId 설정

java
@Order(1) @Component public class TraceIdFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String traceId = UUID.randomUUID().toString().substring(0, 8); MDC.put("traceId", traceId); try { filterChain.doFilter(request, response); } finally { MDC.remove("traceId"); } } }