월요일

PostgreSQL deadlock detected 해결 — 두 트랜잭션이 서로 락을 기다리는 교착상태 원인 해결

PostgreSQL deadlock detected 에러 해결 — 교착상태 원인과 방지 전략

핵심 답변: PostgreSQL deadlock detected는 두 트랜잭션이 서로 상대방이 보유한 락을 기다리는 교착상태에서 발생한다. PostgreSQL은 이를 자동으로 감지해 하나를 강제 롤백한다. 방지법은 ① 일관된 락 순서 유지, ② SELECT FOR UPDATE 사용, ③ 배치 단위 COMMIT, ④ 외래키 인덱스 추가, ⑤ 재시도 로직 구현이다.

운영 중인 서비스 로그에 갑자기 이런 에러가 쏟아진다.

ERROR: deadlock detected

DETAIL: Process 12345 waits for ShareLock on transaction 678;

blocked by process 67890.

Process 67890 waits for ShareLock on transaction 456;

blocked by process 12345.

HINT: See server log for query details.

교착상태(Deadlock)는 두 트랜잭션이 서로 상대방이 보유한 락을 기다리는 상황이다. 양쪽 모두 진행할 수 없는 상태가 되면 PostgreSQL이 감지해 하나를 강제 롤백하고 ERROR 40P01을 반환한다.
트랜잭션 A: 행 1 잠금 → 행 2 잠금 시도 (대기)

트랜잭션 B: 행 2 잠금 → 행 1 잠금 시도 (대기)

→ 영원히 풀리지 않음 → PostgreSQL이 하나를 강제 종료


원인 1: UPDATE 순서가 트랜잭션마다 다른 경우

가장 흔한 원인이다. 같은 테이블의 여러 행을 업데이트할 때 트랜잭션마다 접근 순서가 다르면 교착상태가 생긴다.

-- 트랜잭션 A (id=1 → id=2 순서)

BEGIN;

UPDATE orders SET status = 'processing' WHERE id = 1;

UPDATE orders SET status = 'processing' WHERE id = 2;

COMMIT;

-- 트랜잭션 B (id=2 → id=1 역순 — 충돌!)

BEGIN;

UPDATE orders SET status = 'cancelled' WHERE id = 2;

UPDATE orders SET status = 'cancelled' WHERE id = 1; -- deadlock

COMMIT;

해결: 모든 트랜잭션에서 동일한 순서(id 오름차순)로 접근
-- SQL에서 ORDER BY로 순서 보장

UPDATE orders SET status = 'processing'

WHERE id IN (1, 2)

ORDER BY id;

# 애플리케이션에서 정렬 후 처리

def update_orders(session, order_ids, status):

for order_id in sorted(order_ids): # 항상 오름차순

session.query(Order).filter(Order.id == order_id).update({"status": status})

session.commit()


원인 2: SELECT FOR UPDATE 없이 나중에 UPDATE

조회 후 수정하는 패턴에서 자주 발생한다. 조회 시 락을 잡지 않으면 다른 트랜잭션이 끼어든다.

-- ❌ 락 없이 조회 후 수정 — 위험

BEGIN;

SELECT balance FROM accounts WHERE id = 100;

-- 이 사이에 다른 트랜잭션이 같은 행을 수정 가능

UPDATE accounts SET balance = balance - 1000 WHERE id = 100;

COMMIT;

-- ✅ SELECT FOR UPDATE로 조회 시 즉시 락 획득

BEGIN;

SELECT balance FROM accounts WHERE id = 100 FOR UPDATE;

UPDATE accounts SET balance = balance - 1000 WHERE id = 100;

COMMIT;

// Spring Data JPA — 비관적 락 사용

@Repository

public interface AccountRepository extends JpaRepository<Account, Long> {

@Lock(LockModeType.PESSIMISTIC_WRITE)

@Query("SELECT a FROM Account a WHERE a.id = :id")

Optional<Account> findByIdForUpdate(@Param("id") Long id);

}

@Transactional

public void transfer(Long fromId, Long toId, BigDecimal amount) {

// 작은 id부터 락 획득 — 순서 일관성 보장

Long firstId = Math.min(fromId, toId);

Long secondId = Math.max(fromId, toId);

Account first = accountRepository.findByIdForUpdate(firstId).orElseThrow();

Account second = accountRepository.findByIdForUpdate(secondId).orElseThrow();

}


원인 3: 인덱스 없이 대량 UPDATE/DELETE

인덱스 없는 컬럼으로 넓은 범위를 업데이트하면 테이블 전체에 락이 걸린다.

-- ❌ 인덱스 없는 컬럼으로 대량 업데이트 — 테이블 전체 잠금

UPDATE orders SET processed = true WHERE created_at < '2026-01-01';

-- ✅ 배치로 나눠서 처리

DO $$

DECLARE

batch_size INT := 1000;

last_id BIGINT := 0;

BEGIN

LOOP

UPDATE orders SET processed = true

WHERE id IN (

SELECT id FROM orders

WHERE created_at < '2026-01-01'

AND processed = false

AND id > last_id

ORDER BY id

LIMIT batch_size

)

RETURNING max(id) INTO last_id;

EXIT WHEN NOT FOUND OR last_id IS NULL;

COMMIT;

PERFORM pg_sleep(0.01); -- 다른 트랜잭션에 기회 부여

END LOOP;

END $$;


원인 4: 외래키 인덱스 누락

PostgreSQL은 외래키 참조 시 부모 테이블에 ShareLock을 건다. 외래키 컬럼에 인덱스가 없으면 잠금 범위가 커진다.

-- 외래키 컬럼 인덱스 누락 여부 확인

SELECT

tc.table_name,

kcu.column_name,

(SELECT 1 FROM pg_indexes

WHERE tablename = tc.table_name

AND indexdef LIKE '%' || kcu.column_name || '%') AS has_index

FROM information_schema.table_constraints tc

JOIN information_schema.key_column_usage kcu

��N��B��6�FR7G��S�&&6�w&�V�C�6ccc�FF��s�'�g��&�&FW"�&F�W3�7��f��B�f֖Ǔ�����76S�f��B�6��S��V�#�4UB��6��F��V�WB�sW2s��6�FS������ȹΫN��B� ��Y��Zȉ��莸�B���ࠣ�7G&��s��FVF��6���B� ��9��Y���B����N�K�i�8��B�9ޫ��)�ɩC���7G&��s����7G��S�&�&v��'��Ɩ�RֆV�v�C��s�6���#�3332#���XN�����B��7Fw&U5���FVF��6�� �xȹ��Y��)��قث��������Y���BɘN�N���N� �Yθ�B�����N�K��B��ΫH�K�莸�B�8�9κ[����x�Yθ�B��N� � ����x^�x����ȹθ�N�Y���B� θ�B���ࠣƇ"7G��S�&&�&FW#����S�&�&FW"�F���6�ƖB6SSS��&v��3'�#ࠣƃ"7G��S�&f��B�6��S��VVӶ�&v��3'�'��FF��r�&�GF�ӣg��&�&FW"�&�GF�ӣ'�6�ƖB6SSS�6���#�3##"#�� ^�j����#ࠣ�7G��S�&�&v��'��Ɩ�RֆV�v�C��s�6���#�3332#��7Fw&U5�FVF��6�� ��xnɹ˙����ࠣ�7G��S�&�&v��'��Ɩ�RֆV�v�C��s�6���#�3332#���zι���h�ȉ�� Rȹ��7G&��s��Z��8�BɊN�hN� �ȉ���7G&��s�� �{�����7G��S�&�&v��'��Ɩ�RֆV�v�C��s�6���#�3332#�"��٨�ٸBȉ�� ^���7G&��s�4T�T5Bd�"UDDS��7G&��s��ȹ�������7G��S�&�&v��'��Ɩ�RֆV�v�C��s�6���#�3332#�2�������x^����Nث���B�7G&��s� ˙������C��7G&��s���)���4��ԕC����7G��S�&�&v��'��Ɩ�RֆV�v�C��s�6���#�3332#�B�ɛ����*B˺ι���y� ��9�ȹ��7G&��s��ێ��ȪC��7G&��s��9��K����7G��S�&�&v��'��Ɩ�RֆV�v�C��s�6���#�3332#�R�ث��������Y������^�Y��7G&��s��z~�(���7G&��s����x����7G��S�&�&v��'��Ɩ�RֆV�v�C��s�6���#�3332#�b����ȹθ�B���x���7G&��s�W���V�F��&6��fc��7G&��s���Z�وC��ࠣ�7G��S�&�&v��'��Ɩ�RֆV�v�C��s�6���#�3332#���Bn��x�[��x�*N��B�Y� ��8�9�� ��9��ق�^�[��x���Bȉ��莸�B���ࠣƇ"7G��S�&&�&FW#����S�&�&FW"�F���6�ƖB6SSS��&v��3'�#ࠣ�F�b7G��S�&&6�w&�V�C�6S�cFfC�&�&FW"��VgC�G�6�ƖB3s6S��&�&FW"�&F�W3�G��FF��s�G�����&v��#��f��B�6��S��VVӶ6���#�3&S�Ɩ�RֆV�v�C��r#�H� ������&Vc�&�GG3���GV�GV�6�R�&��w7�B�6��"7G��S�&6���#�3s6S�#䆖�&�56���V7F������W��W7FVB�y����[N�+�����F�c���F�c

Spring Boot NoSuchBeanDefinitionException 해결 — Bean을 못 찾는 이유 5가지

Spring Boot NoSuchBeanDefinitionException 해결 — Bean을 못 찾는 이유 5가지

핵심 답변: NoSuchBeanDefinitionException은 Spring 컨테이너가 의존성 주입 시 해당 타입의 Bean이 찾지 못할 때 발생한다. 원인은 ① 컴포넌트 스캔 범위 밖, ② 어노테이션 누락, ③ 구현체 복수 등록, ④ @Bean 미등록, ⑤ 조건부 Bean 조건 불충족 — 이 5가지로 압축된다.

Spring Boot 프로젝트를 시작하면 이런 에러가 뜬다.

org.springframework.beans.factory.NoSuchBeanDefinitionException:

No qualifying bean of type 'com.example.service.UserService' available:

expected at least 1 bean which qualifies as autowire candidate.

또는

org.springframework.beans.factory.BeanCreationException:

Error creating bean with name 'userController':

Unsatisfied dependency expressed through field 'userService'

NoSuchBeanDefinitionException의 의미는 단 하나다: Spring IoC 컨테이너에 해당 타입의 Bean이 등록되어 있지 않다. 코드가 막아 보여도 등록이 안 되어 있으면 반드시 이 에러가 난다.

원인 1: 컴포넌트 스캔 범위 밖

@SpringBootApplication은 해당 클래스가 위치한 패키지와 그 하위 패키지만 스캔한다. 이 범위 밖의 클래스는 @Service를 붙여도 Bean으로 등록되지 않는다. 문제 구조:
com.example.app

└── Application.java ← @SpringBootApplication

com.example.service

└── UserService.java ← @Service 붙여도 스캔 안 됨

해결:
// 방법 1: Application을 최상위 패키지로 이동

// com.example → Application.java

// 방법 2: 스캔 범위 명시

@SpringBootApplication(scanBasePackages = {"com.example.app", "com.example.service"})

public class Application {}

// 방법 3: @ComponentScan 추가

@ComponentScan(basePackages = "com.example")


원인 2: @Component / @Service / @Repository 누락

어노테이션이 없으면 Spring은 그 클래스를 Bean으로 인식하지 않는다.

// ❌ 잘못된 예

public class UserService { // @Service 없음

public User findById(Long id) { ... }

}

// ✅ 올바른 예

@Service

public class UserService {

public User findById(Long id) { ... }

}

Lombok @RequiredArgsConstructor를 써도 마찬가지다. 어노테이션 없으면 Bean이 아니다.


원인 3: 인터페이스 구현체가 여러 개

PaymentService 인터페이스의 구현체가 KakaoPayService, NaverPayService 두 개라면 Spring은 어느 것을 주입할지 결정하지 못한다.
NoUniqueBeanDefinitionException:

expected single matching bean but found 2: kakaoPayService, naverPayService

해결:
// @Primary — 기본 구현체 지정

@Service

@Primary

public class KakaoPayService implements PaymentService {}

// @Qualifier — 주입 시 명시

@Autowired

@Qualifier("naverPayService")

private PaymentService paymentService;

// 변수명으로 지정 (변수명 = Bean 이름)

@Autowired

private PaymentService kakaoPayService;


원인 4: @Configuration에서 @Bean 등록 누락

외부 라이브러리 클래스는 @Configuration@Bean을 명시해야 한다. 이 과정 없이 @Autowired하면 에러가 난다.

// ❌ @Bean 미등록

@Configuration

public class AppConfig {

// RestTemplate 등록 없음

}

@Service

public class ApiService {

@Autowired

private RestTemplate restTemplate; // NoSuchBeanDefinitionException!

}

// ✅ @Bean 명시

@Configuration

public class AppConfig {

@Bean

public RestTemplate restTemplate() {

return new RestTemplate();

}

}


원인 5: 조건부 Bean 조건 불충족

@ConditionalOnProperty 등이 붙은 Bean은 조건이 맞지 않으면 컨테이너에 등록 자체가 되지 않는다.
@Bean

@ConditionalOnProperty(name = "feature.payment.enabled", havingValue = "true")

public PaymentService paymentService() {

return new PaymentService();

}

# application.yml — 이 설정이 없으면 Bean 등록 안 됨

feature:

payment:

enabled: true


빠른 디버깅: 등록된 Bean 목록 확인

management:

endpoints:

web:

exposure:

include: beans

/actuator/beans에서 현재 등록된 모든 Bean을 확인할 수 있다. 문제의 Bean 이름이 목록에 없으면 등록이 안 된 것이다.

체크리스트

증상원인해결
어노테이션 있는데 에러패키지 스캔 범위 밖scanBasePackages 추가
인터페이스 주입 에러구현체 복수 등록@Primary 또는 @Qualifier
외부 라이브러리 주입 에러@Bean 미등록@Configuration에 @Bean 추가
환경별로 다르게 동작조건부 Bean 조건 불충족프로퍼티 값 확인

자주 묻는 질문 (FAQ)

Q. @Autowired 없이 생성자 주입해도 같은 에러가 나나요?

A. 네. 에러 원인은 Bean 등록 여부이지 주입 방식이 아니다. @RequiredArgsConstructor, 생성자 직접 선언, @Autowired 모두 동일하게 발생한다.

Q. 로컬에선 되는데 운영에서만 에러가 납니다.

A. @ConditionalOnProperty@Profile로 환경별 Bean 등록을 제어하는 코드가 있을 가능성이 높다. application-prod.yml에 해당 프로퍼티가 누락됐는지 확인한다.

Q. BeanCreationExceptionNoSuchBeanDefinitionException은 다른 에러인가요?

A. BeanCreationException은 Bean 생성 도중 발생하는 상위 예외다. caused by 라인을 찾으면 내부 원인으로 NoSuchBeanDefinitionException이 포함된 경우가 많다.


정리

NoSuchBeanDefinitionException 진단 순서:

1. 클래스에 @Service / @Component / @Repository가 있는가?

2. 컴포넌트 스캔 패키지 범위 안에 있는가?

3. 구현체가 여러 개라면 @Primary 또는 @Qualifier로 지정했는가?

4. 외부 클래스라면 @Configuration@Bean으로 등록했는가?

5. 조건부 Bean이라면 조건이 충족되는가?

이 5단계를 순서대로 확인하면 원인이 반드시 나온다.


일요일

🔍 검색 키워드: Python asyncio RuntimeError 해결, event loop is closed 에러, asyncio 이벤트 루프 닫힘, Python 비동기 에러, asyncio.run() RuntimeError, httpx asyncio event loop closed

Python 비동기 코드를 짜다 보면 이런 에러가 나온다.

RuntimeError: Event loop is closed

또는:

RuntimeError: This event loop is already running.

FastAPI, SQLAlchemy async, httpx, aiohttp 같은 비동기 라이브러리를 쓰다 보면 특히 자주 마주친다. 에러 메시지는 짧은데 원인은 여러 가지라 헷갈린다. 이 글에서 상황별로 원인과 해결책을 정리한다.

asyncio 이벤트 루프 기초

Python asyncio는 단일 이벤트 루프(Event Loop)를 기반으로 돌아간다. 이 루프가 열려 있어야 async/await 코드가 실행되고, 루프가 닫히면 그 위에서 뭔가를 실행하려 할 때 RuntimeError: Event loop is closed가 발생한다.

문제가 생기는 대표 상황은 루프가 이미 닫혔는데 비동기 작업을 추가 실행하려 할 때, asyncio.get_event_loop()를 잘못 쓸 때, Windows 환경에서 asyncio.run() 이후 후처리 과정에서, pytest 등 테스트 프레임워크와 충돌할 때다.

원인 1 — asyncio.run() 이후 루프에 접근

import asyncio

async def main():
    await asyncio.sleep(1)
    print("done")

asyncio.run(main())  # 루프가 생성되고 실행 후 닫힘

# 여기서 다시 루프에 접근하면 에러
loop = asyncio.get_event_loop()
loop.run_until_complete(some_coroutine())  # RuntimeError: Event loop is closed

asyncio.run()은 새 이벤트 루프를 만들고, 코루틴 실행 후 루프를 닫는다. 이후 get_event_loop()로 루프를 가져오면 이미 닫힌 루프다.

해결: 모든 비동기 로직을 main() 코루틴 안으로 옮긴다.

import asyncio

async def main():
    await step_one()
    await step_two()  # 모든 비동기 작업을 하나의 main 안에서 처리
    await step_three()

asyncio.run(main())

원인 2 — Windows에서 발생하늕 Event loop is closed

Windows에서 asyncio.run() 이후 ProactorEventLoop의 종료 과정에서 내부적으로 에러가 발생한다. Python 3.10 이전 버전에서 특히 자주 보인다.

Exception ignored in: <function _ProactorBasePipeTransport.__del__>
RuntimeError: Event loop is closed

해결: 스크립트 시작 부분에 이벤트 루프 정책을 변경한다.

import asyncio
import sys

if sys.platform == "win32":
    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())

async def main():
    pass

asyncio.run(main())

원인 3 — SQLAlchemy async 사용 시 엔진 미처리

SQLAlchemy의 AsyncEngine을 쓸 때 프로그램이 종료되면서 루프가 닫히기 전에 엔진을 제대로 dispose하지 않으면 에러가 난다.

from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession

engine = create_async_engine("postgresql+asyncpg://user:pass@localhost/db")

async def main():
    async with AsyncSession(engine) as session:
        result = await session.execute(...)

    # 반드시 엔진 정리
    await engine.dispose()

asyncio.run(main())

engine.dispose()를 빠뜨리면 프로그램 종료 시 asyncpg 내부 커넥션 정리 과정에서 이벤트 루프가 이미 닫힌 상태라 에러가 발생한다.

원인 4 — httpx/aiohttp AsyncClient를 재사용할 때

httpx나 aiohttp의 AsyncClient는 이벤트 루프에 바인딩된다. 루프가 바뀌면 기존 클라이언트를 쓸 수 없다.

import asyncio
import httpx

# 잘못된 패턴 — 전역 클라이언트
client = httpx.AsyncClient()  # 이 시점에 루프에 바인딩

async def fetch(url):
    return await client.get(url)

asyncio.run(fetch("https://example.com"))
asyncio.run(fetch("https://example.com"))  # 두 번째 실행에서 문제

해결: async with로 컨텍스트 매니저를 활용한다.

import asyncio
import httpx

async def fetch(url):
    async with httpx.AsyncClient() as client:  # 루프마다 새 클라이언트
        response = await client.get(url)
        return response.json()

asyncio.run(fetch("https://example.com"))
asyncio.run(fetch("https://example.com"))  # 정상 작동

원인 5 — pytest에서 asyncio 테스트 충돌

pytest로 비동기 테스트를 작성할 때 이벤트 루프 설정이 잘못되면 에러가 난다. pytest-asyncio를 설치하고 사용한다.

pip install pytest-asyncio
# 올바른 패턴
import pytest

@pytest.mark.asyncio
async def test_async_func():
    result = await some_async_func()
    assert result == expect%d
# pytest.ini
[pytest]
asyncio_mode = auto

원인별 에러 상황 체크리스트

에러 상황원인해결
asyncio.run() 이후 다시 루프 사용닫힌 루프에 접근모든 로직을 main 코루틴 안으로
Windows에서 종료 시 에러 출력ProactorEventLoop 종료 버그WindowsSelectorEventLoopPolicy 설정
SQLAlchemy async 종료 시 에러엔진 dispose 누락await engine.dispose() 명시적 호출
httpx/aiohttp 두 번째 실행 시 에러클라이언트 루프 바인딩async with 패턴으로 클라이언트 관리
pytest 비동기 테스트 에러이벤트 루프 충돌pytest-asyncio 사용

디버깅 팁

에러 나왔는 짧아서 어디서 왔는지 파악하기 어려울 때, asyncio 디버그 모드를 켜면 상세 정보가 나온다.

import asyncio
import logging

logging.basicConfig(level=logging.DEBUG)

async def main():
    pass

asyncio.run(main(), debug=True)  # debug=True 추가

또는 환경변수로:

PYTHONASYNCIODEBUG=1 python main.py

정리

  • RuntimeError: Event loop is closed는 닫힌 루프에 접근하거나, 루프 생명주기를 제대로 관리하지 않아서 발생한다
  • asyncio.run()을 쓴다면 모든 비동기 로직은 그 안에서 시작하고 끝내야 한다
  • Windows는 WindowsSelectorEventLoopPolicy로 별도 처리 필요
  • httpx/aiohttp 끴라 �언트는 async with라 관리
  • 테스트는 pytest-asyncio라 통일

비동기 코드는 루프의 생릅하가 관리하면 대부분의 문제가 해결된다.