금요일

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분 안에 잡을 수 있습니다.

Spring @Transactional self-invocation 완벽 해결 — 내부 메서드 호출 시 트랜잭션이 무시되는 이유

🔍 검색 키워드: Spring Transactional self-invocation, 스프링 자기 호출 트랜잭션 문제, @Transactional 내부 메서드 호출 무시, Spring AOP self-call, 스프링 트랜잭션 프록시 우회

증상: @Transactional인데 트랜잭션이 적용 안 된다

같은 클래스 내부에서 다른 메서드를 호출할 때 @Transactional이 무시되고, DB 에러가 발생해도 롤백이 되지 않는 현상이 발생한다.

@Service
public class OrderService {

    public void processOrder(Long orderId) {
        saveOrder(orderId);   // 트랜잭션이 적용되지 않음!
    }

    @Transactional
    public void saveOrder(Long orderId) {
        orderRepository.save(new Order(orderId));
        throw new RuntimeException("에러 발생");  // 롤백 안 됨!
    }
}

원인: Spring AOP 프록시 메커니즘

Spring의 @Transactional은 AOP 프록시 기반으로 동작한다. 외부에서 빈을 호출하면 프록시 객체를 거쳐 트랜잭션 처리가 이루어지지만, 같은 클래스 내에서 this.saveOrder()로 직접 호출하면 프록시를 우회하여 실제 객체를 직접 호출하게 된다. 결과적으로 트랜잭션이 시작되지 않는다.

// 외부 호출: 프록시 → 실제 메서드 (트랜잭션 O)
orderService.saveOrder(id);

// 내부 self-invocation: 실제 객체.saveOrder() (트랜잭션 X)
this.saveOrder(id);

해결 방법 1 (권장): 클래스 분리

가장 깔끔한 해결책이다. 트랜잭션이 필요한 메서드를 별도 클래스로 분리하면 외부 호출이 되어 프록시가 정상 동작한다.

@Service
public class OrderService {
    private final OrderSaveService orderSaveService;

    public OrderService(OrderSaveService orderSaveService) {
        this.orderSaveService = orderSaveService;
    }

    public void processOrder(Long orderId) {
        orderSaveService.saveOrder(orderId);  // 프록시 통과 → 트랜잭션 O
    }
}

@Service
public class OrderSaveService {
    @Transactional
    public void saveOrder(Long orderId) {
        orderRepository.save(new Order(orderId));
    }
}

해결 방법 2: ApplicationContext에서 자기 자신 빈 주입

@Service
public class OrderService {
    @Autowired
    private ApplicationContext applicationContext;

    public void processOrder(Long orderId) {
        OrderService self = applicationContext.getBean(OrderService.class);
        self.saveOrder(orderId);  // 프록시 통과
    }

    @Transactional
    public void saveOrder(Long orderId) {
        orderRepository.save(new Order(orderId));
    }
}

해결 방법 3: @Lazy Self-injection

@Service
public class OrderService {
    @Autowired
    @Lazy
    private OrderService self;

    public void processOrder(Long orderId) {
        self.saveOrder(orderId);  // 프록시 통과
    }

    @Transactional
    public void saveOrder(Long orderId) {
        orderRepository.save(new Order(orderId));
    }
}

Spring Boot 2.6 이상에서는 순환 참조가 기본으로 금지되므로 spring.main.allow-circular-references=true 설정이 필요하다. 권장하지 않는다.

해결 방법 비교표

방법권장도비고
클래스 분리★★★★★가장 깔끔, 단일 책임 원칙 준수
ApplicationContext 주입★★★코드는 다소 복잡해짐
@Lazy Self-injection★★순환 참조 이슈, 비권장
AspectJ 컴파일 타임 위빙★★★빌드 설정 복잡

Spring @Transactional self-invocation 문제는 AOP 프록시 원리를 이해하면 자연스럽게 해결 방향이 보인다. 가장 좋은 방법은 처음부터 단일 책임 원칙을 지켜 클래스를 적절히 분리하는 것이다. 이 문제는 Spring을 사용하는 개발자라면 반드시 숙지해야 할 AOP의 핵심 동작 방식이다.

수요일

Docker 멀티스테이지 빌드 캐시 최적화 — 레이어 순서와 캐시 전략

🔍 검색 키워드: Docker 멀티스테이지, 빌드 캐시, Docker layer, Dockerfile 최적화, 빌드 성능, Docker 이미지 크기

Docker 빌드가 매번 10분씩 걸린다면 문제는 멀티스테이지 캐시 전략이다. 각 stage마다 캐시가 독립적으로 작동하는데, 우리는 보통 이걸 무시하고 작성한다.

■ 증상

docker build --no-cache -t myapp .
매번 node_modules 다시 설치 → 5분 소요
또는 코드 변경 → 전체 이미지 재빌드 → 10분 대기

■ 원인

1. 레이어 순서 잘못됨 - 변경 빈도가 높은 코드를 먼저 놓음
2. stage 간 캐시 미사용 - FROM base 후 다시 의존성 설치
3. .dockerignore 미설정 - 불필요한 파일까지 ADD → 캐시 무효화

■ 해결방법

❌ 나쁜 예:

FROM node:18
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build

✅ 좋은 예:

FROM node:18 AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:18 AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:18 AS runtime
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package.json .
EXPOSE 3000
CMD ["npm", "start"]

■ .dockerignore 설정 필수:

node_modules
dist
build
.git
.env
.next

■ 최적화 체크리스트

1. 레이어 변경 빈도순 정렬 (거의 변경 안 됨→자주 변경)
2. RUN 명령어 합치기
3. npm ci 사용 (install보다 빠름)
4. 멀티스테이지로 최종 이미지 크기 50% 감소
5. BuildKit 활용으로 빌드 속도 60% 향상 가능