프로필

데브고래밥

@devgoraebap

스택오버플로우의 단골손님이였던 Claude를 채찍질하는 개발자

Album Art

0:00 0:00
방문자 정보

요즘 관심있는

60
0
#java #spring

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 접근 제어 방법

  1. HTTP Basic Auth - 브라우저 로그인 팝업 (현재 적용)
  2. API Key - Header/Query로 키 전달
  3. JWT Token - Swagger "Authorize" 버튼으로 토큰 입력
  4. OAuth2 - 소셜 로그인, SSO
  5. IP Whitelist - 특정 IP만 허용
  6. 환경별 비활성화 - @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 Auth
  • LoggingFilter.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 예제값 추가