Coding History/Team Project

팀플) SQLAlchemy 세션 트랜잭션 관리 오류로 인해 다중 요청 환경에서 발생하는 충돌 문제 (오라클 서버 재배포)

BlackBirdIT 2024. 11. 22. 09:19

나는 몰랐는데 팀원들이 내 코드를 받아서 요청보낼 때 행성 하나씩 오류가 나는걸 발견함.

지금 문제는 ->

동시에 다수의 행성 데이터를 요청할 때, SQLAlchemy 세션이 동일 트랜잭션 상태를 공유하거나 적절히 초기화되지 않아 Can't reconnect until invalid transaction is rolled back 에러가 발생함. 이로 인해 트랜잭션 충돌이나 데이터베이스 연결 문제가 간헐적으로 나타남.

으로 정리할 수 있음.


그래서 이걸 근본적으로 해결하기 위해서는

  1. SQLAlchemy 세션 관리 최적화
  2. retry_query 개선
  3. 데이터베이스 연결 풀 확인 및 최적화
  4. 캐싱 도입

이정도?

순차적으로 해보자.


우선은 app/__init__.py

# scoped_session 설정
Session = scoped_session(sessionmaker())

scoped_session설정부터 해줬음. 그리고 기존 DB로직들을 전부 아래와 같이 변경하는 작업을 함.

dbSession으로 대체:

  • 기존 db.engineSession.bind
  • 기존 db.session.addsession.add
  • 기존 db.session.commitsession.commit
  • 기존 db.session.rollbacksession.rollback

이런식으로 작업중임.

는 오류가 나서

db_utils.pyget_session()함수 도입함.

초기화 이슈때문에 실행이 안되서 기존 app.py에서 초기화 하던 것을

session_manager.py도입해서 초기화 관리만 따로 하게끔 설계함.

이걸로 초기화를 제대로 진행하게끔 해서 문제 수정한 로직에 도입후에 문제 없이 조회 되는 것을 확인함.


그럼 이제 DB를 쓰는 모든 곳에 이 객체를 사용하게끔 바꿔줘야함. ㅎㅎ..

다 수정함.

문제없이 다 돌아감.


그럼 이제 3단계로 넘어가자. (데이터베이스 연결 풀 확인 및 최적화)

  • 이 작업을 왜 해야 하나?
    • 데이터베이스에 동시에 많은 요청이 들어오면 기본 설정으로는 Connection Pool이 부족하거나 끊어진 연결로 인해 문제가 발생할 수 있음. (실제로 여러번 경험함)
    • 이를 방지하려면 SQLAlchemy의 연결 풀 설정을 최적화하고, 연결 상태를 사전에 확인하도록 설정해야 함.

그래서 뭐.. 해보자고?

이건 좀 간단한데 app/__init__.py에서 SQLAlchemy 엔진 최적화 설정을 넣어주면 됨.
pool_size, max_overflow, pool_pre_ping에 대한 설정을 해줬음.

    # SQLAlchemy 엔진 최적화 설정
    db_engine = create_engine(
        app.config['SQLALCHEMY_DATABASE_URI'],
        pool_size=10,            # 기본 연결 수
        max_overflow=20,         # 최대 초과 연결 수
        pool_pre_ping=True       # 끊어진 연결 확인
    )

이제 또 잘 되는지 테스트 해보자.

DB 접근이 필요한 데이터 요청 반환 잘 된다. 그냥 넘어가자 동시 요청을 해보고 싶은데 귀찮다.


이제 마지막으로 4번인 캐싱 도입.

Redis는 이미 spring에서 기본 설정으로 사용중이라. 포트 번호는 동일하게 가되, DB 번호를 바꿔주는식으로 진행하자. (redis는 DB 인덱스로 구분함. 그래서 1번 쓰면 될듯.)

일단 라이브러리 설치부터.

pip install flask-caching && pip freeze > requirements.txt

진행하고.

cache = Cache() # 캐시 초기화

    # Flask-Caching 설정
    app.config['CACHE_TYPE'] = 'RedisCache'
    app.config['CACHE_REDIS_HOST'] = 'localhost'  # Redis 서버 호스트
    app.config['CACHE_REDIS_PORT'] = 6379         # Redis 서버 포트
    app.config['CACHE_REDIS_DB'] = 1              # Redis DB 인덱스 (기본: 0)
    app.config['CACHE_DEFAULT_TIMEOUT'] = 1000     # 캐싱 데이터의 기본 유효 기간 (초)

기본 설정해주고..

@cache.memoize(timeout=3600) 캐싱이 필요한 곳에 선언해줌.


기존에 캐싱을 사용하던 곳들도 새로 다 이걸로 바꿔줌.

생각해보니까 얘 도커 이미지로 돌리고 있어서.

  redis_container:
    image: redis:latest
    container_name: redis_container
    ports:
      - "6379:6379"
    networks:
      - my_network
    healthcheck:
      test: [ "CMD", "redis-cli", "ping" ]
      interval: 10s
      timeout: 5s
      retries: 3
    restart: always

설정 추가해주고 기존에 돌아가던 로컬 Redis꺼줌. 그리고

    app.config['CACHE_REDIS_HOST'] = 'redis_container'  # Redis 서버 호스트

여기도 컨테이너 이름과 맞춰주고 빌드해서 테스트해봤는데 한번 요청한 건에 대해서는 빠르게 처리하는걸 보니 캐싱 잘 적용되는듯. 혹시나 다른 요청들도 다 잘되나 싹 다 해봤는데 잘 됨.

(달의 위상에는 굳이 캐싱 넣지 않음.)

굳 4단계 작업 모두 완료 했고, 이제 배포만 하면 됨.


이미지 푸쉬하는동안 서버 컴퓨터에 접근해서 기존 서버 다운 시키고..

이미지 삭제함.

그리고 redis설정 추가후에 재빌드!

성공~

서버 오류 없이 정상 작동한다!

굳 재배포까지 끝.