수요일

Redis maxmemory 설정 — OOM 에러와 eviction policy 완벽 가이드

증상 — 에러 메시지

OOM command not allowed when used memory > 'maxmemory'.
ERR command not allowed when used memory > 'maxmemory'

캐시 서버로 Redis를 운영하다 보면 어느 날 갑자기 쓰기 명령이 전부 실패하는 상황을 만납니다. OOM(Out of Memory) 에러는 Redis가 설정된 최대 메모리 한도에 도달했을 때 발생하며, 기본 정책(noeviction)에서는 새 데이터를 추가할 수 없습니다.

원인

Redis는 기본적으로 maxmemory 제한이 없어서 서버 물리 메모리를 모두 소모할 수 있습니다. 운영 환경에서 명시적으로 제한을 걸면, 그 한도를 넘는 순간 maxmemory-policy 설정에 따라 동작이 결정됩니다. 기본값인 noeviction은 메모리가 가득 차면 쓰기 에러를 반환합니다.

해결방법

1. maxmemory 및 정책 설정

# redis.conf 파일 수정
maxmemory 2gb
maxmemory-policy allkeys-lru

# 또는 런타임에 즉시 적용 (재시작 불필요)
redis-cli CONFIG SET maxmemory 2gb
redis-cli CONFIG SET maxmemory-policy allkeys-lru

# 설정 확인
redis-cli CONFIG GET maxmemory
redis-cli CONFIG GET maxmemory-policy

2. Eviction Policy 종류와 용도

# noeviction (기본): 메모리 초과 시 쓰기 에러 반환
# allkeys-lru  : 모든 키 중 LRU 순서로 삭제 — 순수 캐시에 권장
# volatile-lru : TTL 있는 키 중 LRU 순서로 삭제 — 혼합 환경
# allkeys-lfu  : 사용 빈도 낮은 키 삭제 — Redis 4.0+
# volatile-ttl : TTL 짧은 키부터 삭제 — 세션 저장소
# allkeys-random: 무작위 삭제

3. 메모리 사용량 모니터링

redis-cli INFO memory
# 주요 항목: used_memory_human, maxmemory_human, mem_fragmentation_ratio

redis-cli DBSIZE                  # 전체 키 개수
redis-cli MEMORY USAGE mykey      # 특정 키 메모리 크기(bytes)

4. Spring Boot에서 Redis 메모리 효율화

@Bean
public RedisCacheConfiguration cacheConfiguration() {
    return RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(Duration.ofMinutes(30))   // TTL 필수 지정
        .disableCachingNullValues()          // null 캐싱 방지
        .serializeValuesWith(
            RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer())
        );
}

정책 선택 가이드

사용 목적권장 정책이유
순수 캐시 (세션 없음)allkeys-lru모든 키를 대상으로 LRU 교체, 가장 일반적
캐시 + 영구 데이터 혼합volatile-lruTTL 있는 캐시 키만 삭제, 영구 키 보존
세션 저장소volatile-ttl만료 임박 세션 먼저 삭제
쓰기 보장 필수noeviction메모리 부족 시 에러 반환, 데이터 손실 없음
접근 빈도 편중 심함allkeys-lfu자주 쓰는 키 보존, Redis 4.0 이상

핵심 요약

  • maxmemorymaxmemory-policy는 항상 함께 설정한다.
  • 캐시 전용 서버라면 allkeys-lru가 기본 선택지다.
  • Spring Boot에서는 entryTtl로 TTL을 반드시 지정해 키가 무한정 쌓이지 않게 한다.
  • mem_fragmentation_ratio가 1.5 이상이면 MEMORY PURGE 또는 재시작을 고려한다.

Spring Security CORS 완벽 해결 — @CrossOrigin 설정해도 403이 사라지지 않는 이유

🔍 검색 키워드: Spring Security CORS 에러, Spring Boot CORS 설정, Access-Control-Allow-Origin 403, CORS preflight 차단, Spring Security WebMvcConfigurer CORS

증상 — 에러 메시지

프론트엔드에서 API를 호출하면 다음과 같은 CORS 에러가 콘솔에 출력됩니다:

Access to XMLHttpRequest at 'https://api.example.com/api/v1/users' from origin
'https://app.example.com' has been blocked by CORS policy:
Response to preflight request doesn't pass access control check:
No 'Access-Control-Allow-Origin' header is present on the requested resource.

특히 @CrossOrigin 어노테이션이나 WebMvcConfigurer로 CORS를 설정했는데도 에러가 사라지지 않는 경우, Spring Security가 원인일 가능성이 높습니다.

원인

Spring Boot 프로젝트에 Spring Security가 추가되면 CORS 처리 순서가 달라집니다. Spring Security의 필터 체인이 Spring MVC의 CORS 처리보다 먼저 실행되기 때문에, @CrossOrigin 이나 WebMvcConfigurer를 통한 CORS 설정이 Security 레이어에서 막혀버립니다.

Preflight 요청(OPTIONS)은 인증 헤더가 없기 때문에 Spring Security가 401/403으로 거부하고, 브라우저는 CORS 에러로 인식합니다. 즉, CORS 설정이 잘못된 게 아니라 Security 필터가 CORS 처리 전에 먼저 요청을 차단하는 것이 핵심 원인입니다.

해결방법

방법 1: SecurityFilterChain에 CORS 직접 등록 (Spring Boot 3.x 권장)

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
        public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
                http
                            .cors(cors -> cors.configurationSource(corsConfigurationSource()))
                                        .csrf(csrf -> csrf.disable())
                                                    .authorizeHttpRequests(auth -> auth
                                                                    .requestMatchers("/api/public/**").permitAll()
                                                                                    .anyRequest().authenticated()
                                                                                                );
                                                                                                        return http.build();
                                                                                                            }
                                                                                                            
                                                                                                                @Bean
                                                                                                                    public CorsConfigurationSource corsConfigurationSource() {
                                                                                                                            CorsConfiguration config = new CorsConfiguration();
                                                                                                                                    config.setAllowedOrigins(List.of("https://app.example.com"));
                                                                                                                                            config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
                                                                                                                                                    config.setAllowedHeaders(List.of("*"));
                                                                                                                                                            config.setAllowCredentials(true);
                                                                                                                                                                    config.setMaxAge(3600L);
                                                                                                                                                                    
                                                                                                                                                                            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
                                                                                                                                                                                    source.registerCorsConfiguration("/**", config);
                                                                                                                                                                                            return source;
                                                                                                                                                                                                }
                                                                                                                                                                                                }

방법 2: WebMvcConfigurer와 Security 통합

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
        public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                            .allowedOrigins("https://app.example.com")
                                        .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                                                    .allowedHeaders("*")
                                                                .allowCredentials(true)
                                                                            .maxAge(3600);
                                                                                }
                                                                                }
                                                                                
                                                                                // Security 설정에서 반드시 cors() 활성화
                                                                                @Bean
                                                                                public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
                                                                                    http
                                                                                            .cors(Customizer.withDefaults())  // WebMvcConfigurer 설정 사용
                                                                                                    .csrf(csrf -> csrf.disable())
                                                                                                            // ...
                                                                                                                return http.build();
                                                                                                                }

자주 하는 실수 3가지

1. Security 설정에 .cors() 없이 WebMvcConfigurer만 설정 — Security 필터가 OPTIONS 요청을 차단함

2. allowedOrigins에 trailing slash 포함 — "https://app.example.com/" 처럼 끝에 슬래시 금지

3. allowedMethods에 OPTIONS 누락 — Preflight 통과를 위해 OPTIONS 반드시 포함

원인별 정리표

증상원인해결
OPTIONS 요청 401/403Security가 Preflight 차단cors() 활성화 + permitAll
@CrossOrigin 무시됨Security 필터 우선 실행SecurityFilterChain에 cors() 추가
쿠키/세션 전송 안 됨allowCredentials 미설정setAllowCredentials(true)
특정 헤더만 에러allowedHeaders 미포함setAllowedHeaders(List.of("*"))
로컬에서만 동작origin 목록에 배포 URL 없음allowedOrigins에 배포 도메인 추가

핵심 정리

Spring Security + CORS 문제의 90%는 SecurityFilterChain에 .cors()를 추가하지 않아서 발생합니다. @CrossOrigin이나 WebMvcConfigurer만으로는 Security 레이어를 통과할 수 없습니다. Spring Boot 3.x 환경에서는 corsConfigurationSource Bean을 별도로 등록하는 방법 1이 가장 명확하고 안정적입니다.

금요일

Nginx 502 Bad Gateway 완벽 해결 가이드 — upstream 연결 실패 원인과 해결

🔍 검색 키워드: Nginx 502 Bad Gateway 해결, Nginx upstream 502 에러, upstream connect() failed, Nginx 리버스 프록시 502, upstream timed out 해결

증상: 502 Bad Gateway 에러 발생

Nginx를 리버스 프록시로 사용할 때 클라이언트가 갑자기 502 Bad Gateway 에러를 마주치는 경우가 있습니다. 브라우저에는 아무 설명도 없고, Nginx 에러 로그를 보면 이런 메시지가 남습니다:

2026/09/18 09:12:34 [error] 12345#12345: *1 connect() failed (111: Connection refused) while connecting to upstream, client: 1.2.3.4, server: example.com, request: "GET / HTTP/1.1", upstream: "http://127.0.0.1:8080/", host: "example.com"

2026/09/18 09:13:01 [error] 12345#12345: *2 upstream timed out (110: Connection timed out) while reading response header from upstream, client: 1.2.3.4, upstream: "http://127.0.0.1:8080/"

원인: upstream 서버 연결 실패

Nginx 502 에러의 원인은 크게 세 가지입니다:

  • upstream 서버가 죽어 있음 — WAS(Spring Boot, Node.js 등)가 다운됨
  • upstream 응답 시간 초과 — 서버는 살아있지만 너무 느리게 응답
  • SELinux/방화벽 차단 — 네트워크 정책이 연결을 막음

먼저 어떤 케이스인지 확인해야 합니다:

# upstream 서버 포트 확인
ss -tlnp | grep 8080

# 서비스 상태 확인
systemctl status myapp

# Nginx 설정 문법 확인
nginx -t

해결 방법

케이스 1: upstream 서버가 꺼진 경우 → 재시작

# systemd 서비스라면
sudo systemctl restart myapp

# nohup으로 띄운 Spring Boot라면
nohup java -jar /opt/myapp/app.jar > /var/log/myapp.log 2>&1 &

케이스 2: 응답 시간 초과 → 타임아웃 설정 조정

Nginx의 기본 proxy_read_timeout은 60초입니다. 배치 처리나 파일 업로드처럼 오래 걸리는 요청은 이 값을 늘려야 합니다:

server {
    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_connect_timeout 10s;
        proxy_send_timeout 300s;
        proxy_read_timeout 300s;
    }
}

설정 변경 후 반드시 reload:

sudo nginx -s reload

케이스 3: SELinux가 연결 차단 → 정책 허용

CentOS/RHEL 계열에서 SELinux가 활성화된 경우 Nginx가 upstream에 연결하지 못할 수 있습니다:

# SELinux 상태 확인
getenforce

# Nginx → upstream 네트워크 연결 허용
sudo setsebool -P httpd_can_network_connect on

케이스 4: upstream 이중화로 장애 대응

단일 upstream이 죽으면 502가 납니다. 백업 서버를 설정해두면 자동 전환됩니다:

upstream backend {
    server 127.0.0.1:8080;
    server 127.0.0.1:8081 backup;
}

server {
    location / {
        proxy_pass http://backend;
        proxy_next_upstream error timeout http_502;
    }
}

에러 유형별 빠른 참고표

에러 로그 키워드 원인 해결
Connection refused (111) upstream 프로세스 다운 서비스 재시작
upstream timed out (110) 응답 지연 / 타임아웃 proxy_read_timeout 증가
Permission denied (13) SELinux/방화벽 차단 setsebool 또는 방화벽 해제
no live upstreams while connecting upstream 전체 다운 백업 서버 추가

Nginx 502는 대부분 upstream 프로세스 상태 확인 → 타임아웃 튜닝 → SELinux 정책 순서로 해결됩니다. 에러 로그의 키워드를 보고 케이스를 특정하면 대부분 10분 안에 잡을 수 있습니다.