일요일

AWS S3 CORS 에러 해결 — Access-Control-Allow-Origin 없음 완전 정복

BeanCurrentlyInCreationException 해결 — Spring Boot 순환 의존성 에러 완전 정복

🔍 검색 키워드: BeanCurrentlyInCreationException 해결, Spring Boot 순환 의존성, circular dependency 에러, Spring Boot 빈 생성 실패, allow-circular-references, Spring Boot 시작 실패

Spring Boot 프로젝트를 띄우다가 서버가 뜨지도 않고 이런 에러를 만난 적 있을 것이다.

The dependencies of some of the beans in the application context form a cycle:
userService -> orderService -> userService

혹은 더 직접적으로:

org.springframework.beans.factory.BeanCurrentlyInCreationException:
Error creating bean with name 'userService':
Requested bean is currently in creation: Is there an unresolvable circular reference?

서버 자체가 뜨지 않으니 당황스럽다. 이 글에서는 원인부터 실무에서 자주 쓰는 해결책까지 정리한다.

순환 의존성이란 뭔가

ServiceA가 ServiceB를 주입받고, ServiceB가 다시 ServiceA를 주입받는 상황이다. Spring이 A를 만들려면 B가 필요하고, B를 만들려면 A가 필요하다. 닭이 먼저냐 달걀이 먼저냐 — Spring은 이 상황에서 예외를 던진다.

@Service
public class UserService {
    private final OrderService orderService;

    public UserService(OrderService orderService) { // OrderService 주입
        this.orderService = orderService;
    }
}

@Service
public class OrderService {
    private final UserService userService;

    public OrderService(UserService userService) { // UserService 주입
        this.userService = userService;
    }
}

위처럼 생성자 주입(Constructor Injection)으로 서로를 참조하면 Spring Boot 2.6 이후부터는 기본적으로 예외가 발생한다.

왜 Spring Boot 2.6부터 더 자주 보이나

Spring Boot 2.6에서 기본 순환 의존성 감지가 강화됐다. 이전 버전에서는 필드 주입(@Autowired)의 경우 특별한 설정 없이도 넘어가는 경우가 있었는데, 2.6부터는 훨씬 엄격하게 잡아낸다. 기존에 잘 돌아가던 프로젝트를 Spring Boot 버전 올리다가 갑자기 이 에러를 만나는 이유다.

에러 발생 상황 체크리스트

상황가능성
Service 간 서로 참조★★★★★
Service → Repository → Service 체인★★★★☆
@Configuration 클래스 간 참조★★★☆☆
Spring Boot 버전 업그레이드 직후★★★★☆
이벤트 리스너와 서비스 간 참조★★★☆☆

해결 방법 1 — @Lazy 어노테이션 (빠른 임시 해결)

둘 중 하나의 의존성에 @Lazy를 붙인다. Spring이 실제로 해당 빈이 필요한 시점까지 초기화를 미룬다.

@Service
public class UserService {
    private final OrderService orderService;

    public UserService(@Lazy OrderService orderService) { // @Lazy 추가
        this.orderService = orderService;
    }
}

당장 급하게 해결해야 할 때 쓰는 방법이다. 하지만 근본적인 구조 문제를 가리는 것이라 장기적으로는 좋지 않다.

해결 방법 2 — 세터/필드 주입으로 전환

생성자 주입 대신 세터 주입(Setter Injection)으로 바꾸면 Spring이 빈을 먼저 만든 후 의존성을 주입하기 때문에 순환 참조 문제가 해소된다.

@Service
public class OrderService {
    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
}

해결 방법 3 — 구조 리팩토링 (가장 올바른 해결)

순환 의존성이 생겼다는 건 클래스의 책임이 잘못 분리됐다는 신호다. 보통 아래 패턴 중 하나로 해결된다.

공통 로직을 별도 서비스로 분리:

// UserService와 OrderService 모두 참조하던 공통 로직
@Service
public class UserOrderBridgeService {
    private final UserRepository userRepository;
    private final OrderRepository orderRepository;

    public void processUserOrder(Long userId, Long orderId) {
        // 공통 로직 처리
    }
}

@Service
public class UserService {
    private final UserRepository userRepository;
    private final UserOrderBridgeService bridgeService; // 공통 서비스만 참조
}

@Service
public class OrderService {
    private final OrderRepository orderRepository;
    private final UserOrderBridgeService bridgeService;
}

이벤트 기반으로 분리:

@Service
public class UserService {
    private final ApplicationEventPublisher eventPublisher;

    public void deleteUser(Long userId) {
        eventPublisher.publishEvent(new UserDeletedEvent(userId));
    }
}

@Component
public class OrderEventListener {
    private final OrderService orderService;

    @EventListener
    public void handleUserDeleted(UserDeletedEvent event) {
        orderService.cancelOrdersByUserId(event.getUserId());
    }
}

해결 방법 4 — application.properties 설정 (비추천)

spring.main.allow-circular-references=true

이 설정은 에러만 안 보이게 하는 것이다. 실제로는 Spring Boot가 내부적으로 의존성 순서를 휴리스틱하게 결정하게 되어, 예측하기 어려운 초기화 순서 문제가 생길 수 있다. 근본 원인을 분석할 시간이 없는 긴급 상황에서 임시로만 쓰자.

정리

  • BeanCurrentlyInCreationException은 두 빈이 서로 의존하는 구조에서 발생한다
  • Spring Boot 2.6+에서 기본적으로 더 엄격하게 감지한다
  • 빠른 해결은 @Lazy, 올바른 해결은 공통 로직 분리 또는 이벤트 기반 구조 변경
  • allow-circular-references=true는 임시방편일 뿐, 운영 환경에서는 근본 해결 필요

순환 의존성 에러는 코드 냄새(Code Smell)다. 에러를 꺼주는 것보다 왜 두 서비스가 서로를 참조해야 하는지를 먼저 물어보는 게 맞다.

금요일

Nginx 502 Bad Gateway 해결 — upstream 에러 원인별 완전 정리

🔍 검색 키워드: nginx 502 bad gateway 해결, nginx upstream 에러, nginx 리버스 프록시 502, nginx proxy_read_timeout, upstream sent too big header

Nginx62�는 리버스 프록시로 쓰다 보면 피할 수 없는 에러가 502다.

502 Bad Gateway
nginx/1.x.x

사용자한테 이게 보이는 순간 장애다. 빠르게 원인 찾고 고쳐야 하는데, 502는 원인이 한두 가지가 아니다. 이 글에서 발생 빈도 높은 원인과 해결책을 순서대로 정리한다.


1. 502 Bad Gateway는 뭔가

Nginx가 업스트림 서버(백엔드 앱, WAS, API 서버)에 요청을 넘겼는데 제대로 된 응답을 못 받았을 때 클라이언트에게 돌려주는 응답이다.

즉, Nginx 자체의 문제가 아니라 Nginx 뒤에 있는 서버의 문제다.


2. 원인별 분류 및 해결

원인 1: 업스트림 서버가 꺼져 있다 (가장 흔함)

에러 로그:

2026/06/26 10:00:00 [error] 12345#0: *1 connect() failed (111: Connection refused)
while connecting to upstream, upstream: "http://127.0.0.1:8080/api/health"

Connection refused가 보이면 백엔드 앱이 죽은 거다.

# 프로세스 확인
ps aux | grep java
ps aux | grep node
ps aux | grep gunicorn

# 포트 리스닝 확인
ss -tlnp | grep 8080

프로세스가 없으면 앱을 재시작한다. 재시작 후에도 바로 죽는다면 앱 자체의 에러 로그를 봐야 한다.

원인 2: 타임아웃

에러 로그:

upstream timed out (110: Operation timed out) while reading response header from upstream

백엔드가 살아있긴 한데 응답이 너무 느린 경우다. Nginx의 기본 타임아웃값(60초)을 넘기면 이 에러가 발생한다.

# nginx.conf 또는 서버 블록
location / {
    proxy_pass http://localhost:8080;

    proxy_connect_timeout 60s;   # 연결 타임아웃
    proxy_send_timeout    60s;   # 요청 전송 타임아웃
    proxy_read_timeout    120s;  # 응답 수신 타임아웃 (이걸 늘리면 됨)
}

배치 작업이나 파일 업로드처럼 처리 시간이 긴 요청은 proxy_read_timeout을 요청 성격에 맞게 올린다. 무작정 크게 올리는 건 좋지 않고, 실제 처리 시간 + 여유분 정도로 설정한다.

원인 3: 업스트림 헤더가 너무 크다

에러 로그:

upstream sent too big header while reading response header from upstream

백엔드 응답의 HTTP 헤더가 Nginx 버퍼 크기를 초과한 경우다. 쿠키가 많거나 커스텀 헤더를 대량으로 쓰는 앱에서 발생한다.

location / {
    proxy_pass http://localhost:8080;

    proxy_buffer_size          128k;
    proxy_buffers              4 256k;
    proxy_busy_buffers_size    256k;
}

헤더 크기를 줄이는 게 근본 해결이지만, 당장 급하면 버퍼를 올린다.

원인 4: Unix 소켓 권한 문제

PHP-FPM이나 Gunicorn을 소켓으로 연결할 때 발생한다.

connect() to unix:/var/run/php-fpm.sock failed (13: Permission denied)
# 소켓 파일 권한 확인
ls -la /var/run/php-fpm.sock

# 소켓 파일 소유권 변경
chown www-data:www-data /var/run/php-fpm.sock
chmod 660 /var/run/php-fpm.sock
; /etc/php-fpm.d/www.conf
listen.owner = www-data
listen.group = www-data
listen.mode = 0660

3. 빠른 진단 순서

# 1. Nginx 에러 로그 확인 (제일 먼저)
tail -f /var/log/nginx/error.log

# 2. 업스트림 서버 상태 확인
curl -I http://127.0.0.1:8080/

# 3. 포트 리스닝 확인
ss -tlnp | grep 8080

# 4. Nginx 설정 문법 검사
nginx -t

# 5. Nginx 재시작 (설정 변경 후)
systemctl reload nginx

에러 로그에 Connection refused, timed out, Permission denied, too big header 중 뭐가 찍혀 있는지 먼저 보는 게 핵심이다.


4. 원인별 체크리스트

에러 로그 키워드 원인 해결 방법
Connection refused 업스트림 프로세스 다운 앱 재시작
timed out 응답 지연 proxy_read_timeout 증가
too big header 헤더 버퍼 부족 proxy_buffer_size 증가
Permission denied 소켓 권한 문제 소켓 파일 권한 수정
no live upstreams 업스트림 그룹 전체 다운 업스트림 서버 상태 확인

5. 실무 팁 — health check 설정

업스트림이 다운됐을 때 Nginx가 알아서 fallback하도록 설정할 수 있다.

upstream backend {
    server 127.0.0.1:8080 max_fails=3 fail_timeout=30s;
    server 127.0.0.1:8081 backup;  # 메인이 죽으면 이걸로
}

max_fails는 해당 서버를 비활성화하기 전 실패 허용 횟수, fail_timeout은 비활성화 유지 시간이다.


정리

Nginx 502는 반드시 /var/log/nginx/error.log를 먼저 봐야 한다. 에러 메시지가 원인을 정확히 알려준다. Connection refused면 앱 프로세스 확인, timed out이면 타임아웃 설정, too big header면 버퍼 설정 — 이 세 가지가 전체 502의 대부분을 차지한다.

작성일: 2026-06-26