Spring Boot 개발기록 #3 [claude 요약]
로깅 관련 설정, 스프링부트 실행 컨택스트, 스프링 시큐리티, 애플리케이션 코드에서 application.properies의 값에 접근하는 방법등을 요약한 내용이다.
1. 로깅 관련 설정
Spring에서 로깅은 어떤 라이브러리나 프레임워크 자체적으로 지원해주는가?
답변:
- Spring Boot는 기본적으로 SLF4J + Logback 내장
- 별도 설정 없이 바로 사용 가능
- Lombok
@Slf4j어노테이션으로 간편하게 사용
간단한 사용법:
@Slf4j
@Service
public class TodoService {
public void someMethod() {
log.info("정보 메시지");
log.debug("디버그 메시지");
log.warn("경고 메시지");
log.error("에러 메시지", exception);
}
}
로깅 커스터마이징 (application.properties)
# 로그 레벨 설정
logging.level.root=INFO
logging.level.xyz.goraebap.spring_progressive_demo=DEBUG
logging.level.xyz.goraebap.spring_progressive_demo.app.todo.TodoQueryMapper=DEBUG
logging.level.org.hibernate.SQL=DEBUG
# 콘솔 출력 패턴
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
# 파일 저장 설정
logging.file.name=/app/logs/application.log
logging.pattern.file=%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
# 파일 로테이션 (날짜별 + 용량별)
logging.logback.rollingpolicy.file-name-pattern=/app/logs/application-%d{yyyy-MM-dd}.%i.log.gz
logging.logback.rollingpolicy.max-file-size=50MB
logging.logback.rollingpolicy.max-history=30
로그 파일 구조:
/app/logs/
├── application.log ← 현재 활성 로그
├── application-2025-01-22.0.log.gz ← 오늘 과거 로그 (압축)
├── application-2025-01-21.0.log.gz ← 어제
├── application-2025-01-21.1.log.gz ← 어제 (50MB 초과 분할)
└── application-2025-01-20.0.log.gz ← 그저께
로테이션 정책:
- 매일 자정에 새 파일 생성
- 50MB 초과 시
.0,.1,.2로 분할 - 30일 이전 파일 자동 삭제
- 압축된 파일은
vi,vim,zcat,zgrep으로 조회 가능
로깅을 출력하는 일반적인 사례들
1. Filter에서 HTTP 요청/응답 로깅
@Slf4j
@Component
@Order(1)
public class LoggingFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String method = httpRequest.getMethod();
String uri = httpRequest.getRequestURI();
String queryString = httpRequest.getQueryString();
String ip = httpRequest.getRemoteAddr();
log.info(">>> [HTTP Request] {} {} {} from {}", method, uri,
queryString != null ? "?" + queryString : "", ip);
long startTime = System.currentTimeMillis();
try {
chain.doFilter(request, response);
} finally {
int status = httpResponse.getStatus();
long executionTime = System.currentTimeMillis() - startTime;
log.info("<<< [HTTP Response] {} {} - Status: {} - {}ms",
method, uri, status, executionTime);
}
}
}
2. AOP를 활용한 Controller/Service 공통 로깅
@Slf4j
@Aspect
@Component
public class LoggingAspect {
// Controller 메서드 자동 로깅
@Around("execution(* xyz.goraebap.spring_progressive_demo.app..*Controller.*(..))")
public Object logController(ProceedingJoinPoint joinPoint) throws Throwable {
String className = joinPoint.getSignature().getDeclaringTypeName();
String methodName = joinPoint.getSignature().getName();
log.info("[Controller] {}.{} called", className, methodName);
long startTime = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long executionTime = System.currentTimeMillis() - startTime;
log.info("[Controller] {}.{} completed - {}ms", className, methodName, executionTime);
return result;
} catch (Exception e) {
log.error("[Controller] {}.{} failed - {}: {}",
className, methodName, e.getClass().getSimpleName(), e.getMessage());
throw e;
}
}
// Service 메서드 자동 로깅
@Around("execution(* xyz.goraebap.spring_progressive_demo.app..*Service.*(..))")
public Object logService(ProceedingJoinPoint joinPoint) throws Throwable {
String className = joinPoint.getSignature().getDeclaringTypeName();
String methodName = joinPoint.getSignature().getName();
log.debug("[Service] {}.{} started", className, methodName);
try {
Object result = joinPoint.proceed();
log.debug("[Service] {}.{} completed", className, methodName);
return result;
} catch (Exception e) {
log.error("[Service] {}.{} failed - {}: {}",
className, methodName, e.getClass().getSimpleName(), e.getMessage());
throw e;
}
}
}
AOP 의존성:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-aop'
}
3. Exception Handler 로깅
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, Object>> handleValidationException(...) {
log.warn("Validation failed - path: {}, errors: {}", ...);
// ...
}
@ExceptionHandler(NotFoundException.class)
public ResponseEntity<Map<String, Object>> handleNotFoundException(...) {
log.warn("Resource not found - path: {}, message: {}", ...);
// ...
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, Object>> handleGenericException(...) {
log.error("Unexpected error occurred - path: {}", ..., ex);
// ...
}
}
4. 서버 배포 시 Docker 볼륨과 연결
docker-compose.yml:
services:
spring-progressive-demo:
volumes:
- ./logs:/app/logs # 로그 디렉토리 마운트
로그 확인:
# 호스트에서 바로 확인 (Docker 접속 불필요)
tail -f ./logs/application.log
grep "ERROR" ./logs/application.log
tail -n 100 ./logs/application.log
2. Spring Framework 실행 컨텍스트 (NestJS 비교)
NestJS와 Spring 개념 매핑
| NestJS | Spring | 역할 |
|---|---|---|
| Middleware | Filter | HTTP 요청/응답 전처리, 로깅, 인증 |
| Guard | HandlerInterceptor | 인가, 권한 체크 |
| Interceptor | AOP | 메서드 실행 전/후 처리 |
| Pipe | Validator + ArgumentResolver | 데이터 검증, 변환 |
| Exception Filter | ExceptionHandler | 예외 처리 |
Spring 실행 컨텍스트 구현
| 레이어 | 구현 방식 | 동작 레벨 | 주요 역할 |
|---|---|---|---|
| Filter | Filter 인터페이스 구현 |
HTTP 요청/응답 | IP, URI, 상태코드, 실행시간 |
| HandlerInterceptor | HandlerInterceptor 구현 |
Controller 진입 전/후 | 인증/인가, 로깅, 공통 처리 |
| AOP | @Around, @Before, @After |
메서드 실행 전/후 | 트랜잭션, 로깅, 보안, 캐싱 |
| ArgumentResolver | HandlerMethodArgumentResolver |
파라미터 바인딩 | 커스텀 파라미터 처리 |
| ExceptionHandler | @ExceptionHandler |
예외 발생 시 | 전역 예외 처리 |
실행 순서
1. Filter (HTTP 레벨)
↓
2. HandlerInterceptor.preHandle()
↓
3. ArgumentResolver (파라미터 바인딩)
↓
4. AOP @Before
↓
5. Controller 메서드 실행
↓
6. AOP @After
↓
7. HandlerInterceptor.postHandle()
↓
8. Filter 응답 처리
↓
9. (예외 발생 시) ExceptionHandler
Filter vs Interceptor vs AOP
| 구분 | Filter | HandlerInterceptor | AOP |
|---|---|---|---|
| 동작 시점 | Servlet 진입 전/후 | DispatcherServlet → Controller | 메서드 실행 전/후 |
| 처리 범위 | 모든 HTTP 요청 | Spring MVC 요청만 | 모든 Spring Bean |
| 접근 가능 | HttpServletRequest/Response | ModelAndView, Handler | 메서드 파라미터, 리턴값 |
| 사용 목적 | 인증, 로깅, 인코딩 | 권한 체크, 로깅 | 트랜잭션, 로깅, 캐싱 |
3. Spring Security
Spring Security를 기본으로 설정하면 뭐가 달라지는가?
- 모든 경로가 기본적으로 보호됨 (인증 필요)
- 정적 리소스(
/css/**,/js/**)도 보호 대상 .permitAll()로 명시적으로 공개 처리 필요- 기본 로그인 페이지 자동 생성
Spring Security가 처리하는 일들
- 인증 (Authentication): 사용자 신원 확인
- 인가 (Authorization): 권한 체크
- CSRF 방어: 크로스 사이트 요청 위조 방지
- 세션 관리: 세션 고정 공격 방지, 동시 세션 제어
- CORS 처리: 교차 출처 리소스 공유
- HTTP 보안 헤더: X-Frame-Options, X-XSS-Protection 등
정적 리소스 접근과의 관계
Spring Security는 정적 리소스도 기본적으로 보호함!
SecurityConfig에서 명시적으로 .permitAll() 필요:
.authorizeHttpRequests(auth -> auth
.requestMatchers("/css/**", "/js/**", "/images/**", "/favicon.ico").permitAll()
// ...
)
안 그러면 CSS, JS 파일도 로그인 페이지로 리다이렉트되거나 403 에러 발생
로그인 페이지 설정
Spring Security 의존성 추가 시 자동으로 기본 로그인 페이지 생성
1. HTTP Basic Auth (현재 설정)
- 브라우저 기본 팝업으로 ID/PW 입력
- 별도 HTML 페이지 없음
.httpBasic(Customizer.withDefaults())
2. Form Login 추가 시
- Spring이
/login경로에 기본 로그인 페이지 자동 생성 - HTML 파일 없이 런타임에 동적 생성
.formLogin(form -> form
.defaultSuccessUrl("/swagger-ui/index.html", true)
.permitAll()
)
주의: Form Login 사용 시 세션 정책 변경 필요
// Stateless → 세션 사용 안 함 (HTTP Basic, JWT 용)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// Form Login 사용 시 변경
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
CORS 설정 방법
CorsConfig.java:
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("http://localhost:3000", "http://localhost:5173"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
SecurityConfig에서 CORS 적용:
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CorsConfigurationSource corsConfigurationSource;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource))
.csrf(csrf -> csrf.disable())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/css/**", "/js/**", "/images/**", "/favicon.ico").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/api/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}
의존성:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
}
4. Swagger 구체적인 설정
Swagger 기본 설정 (SwaggerConfig.java)
@Configuration
public class SwaggerConfig {
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.info(new Info()
.title("Spring Progressive Demo API")
.description("API Documentation")
.version("1.0.0")
.contact(new Contact()
.name("Goraebap")
.url("https://goraebap.xyz")
.email("contact@goraebap.xyz"))
.license(new License()
.name("MIT License")
.url("https://opensource.org/licenses/MIT"))
);
}
}
Swagger PathVariable 예제값 추가
import io.swagger.v3.oas.annotations.Parameter;
@PutMapping("/{id}")
public void update(
@Parameter(description = "할일 ID", example = "1") @PathVariable Long id,
@Valid @RequestBody TodoUpdateDto dto) {
todoService.update(id, dto);
}
Swagger에 HTTP Basic Auth 추가
application.properties:
# Swagger Basic Auth
swagger.username=admin
swagger.password=admin123
SecurityConfig 업데이트:
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final CorsConfigurationSource corsConfigurationSource;
@Value("${swagger.username}")
private String swaggerUsername;
@Value("${swagger.password}")
private String swaggerPassword;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource))
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/api-docs.html").authenticated()
.requestMatchers("/api/**").permitAll()
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user = User.builder()
.username(swaggerUsername)
.password(passwordEncoder().encode(swaggerPassword))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Swagger 접근 제어 방법
- HTTP Basic Auth - 브라우저 로그인 팝업 (현재 적용)
- API Key - Header/Query로 키 전달
- JWT Token - Swagger "Authorize" 버튼으로 토큰 입력
- OAuth2 - 소셜 로그인, SSO
- IP Whitelist - 특정 IP만 허용
- 환경별 비활성화 -
@Profile("!prod")
실무 조합:
- 개발: Basic Auth
- 스테이징/프로덕션: IP Whitelist + Basic Auth
- 공개 API: JWT Token
5. application.properties 값을 코드에서 사용하기
@Value 어노테이션 사용법
@Configuration
public class SomeConfig {
@Value("${swagger.username}")
private String swaggerUsername;
@Value("${swagger.password}")
private String swaggerPassword;
@Value("${app.feature.enabled:false}") // 기본값 설정
private boolean featureEnabled;
}
설정값 사용 모범 사례
1. 환경별 분리:
application-local.properties- 개발 환경application-prod.properties- 운영 환경
2. 민감정보는 환경변수로 관리:
# application.properties
swagger.username=${SWAGGER_USERNAME:admin}
swagger.password=${SWAGGER_PASSWORD:admin123}
3. @ConfigurationProperties 사용 (타입 안전):
@ConfigurationProperties(prefix = "app.swagger")
@Component
public class SwaggerProperties {
private String username;
private String password;
// getter, setter
}
4. 설정값 검증:
@ConfigurationProperties(prefix = "app")
@Validated
public class AppProperties {
@NotBlank
private String name;
@Min(1)
@Max(100)
private int maxRetry;
}
파일 변경 이력
신규 생성
CorsConfig.java- CORS 설정SecurityConfig.java- Spring Security + HTTP Basic AuthLoggingFilter.java- HTTP 요청/응답 로깅LoggingAspect.java- Controller/Service 메서드 로깅
수정
build.gradle- Spring Security, AOP 의존성 추가application.properties- 로깅 설정application-local.properties- Swagger 인증 계정, 로깅 설정application-prod.properties- Swagger 인증 계정, 로깅 + 로테이션docker-compose.yml- 로그 볼륨 마운트GlobalExceptionHandler.java- 로깅 추가TodoController.java- PathVariable @Parameter 예제값 추가