-
[SQLAlchemy] Session vs scoped_session💻 프로그래밍/Python 2022. 10. 1. 02:23
안녕하세요! 개발자 Jay입니다!
블로그에 말하지는 않았지만 퇴사한지 거진 한 달쯤 다되어가네요 ㅋㅋㅋ 입사 전 이것저것 해볼 계획은 많았는데... 생각보다 많이 못해서 아쉽지만... 여하튼ㅋㅋㅋ
토이 프로젝트 프레임워크로 FastAPI를 사용하면서 SQLAlchemy를 사용했었습니다. 그때는 막상 구현하는데 급급해서 자세히 알아보지 못했던 부분들이 있었습니다.
이번에 repository 쪽 코드를 리팩토링 하면서 Session과 scoped_session에 대한 내용을 제대로 파보았습니다.
Session이란?
SQLAlchemy에서 말하는 Session은 DB Session과는 전혀 다릅니다.(혹시나 헷갈릴 수도 있으니)
SQLAlchmey의 Session은 일종에 ORM 버퍼라고 보면 되며 ORM 매핑 객체(object)에 대한 지속성 작업을 관리합니다. DB connection의 트랜잭션을 설정하고 관리합니다. 이 트랜잭션은 commit 하거나 rollback 지시가 있기 전까지 유효합니다.
그림에서 볼 수 있다시피 Session은 Unit-of-work(UoW) 패턴으로 되어있습니다. 공식문서에도 나오는 내용인데 UoW에 대해서는 여기를 참고하길 바랍니다. 간단히 말하자면 비즈니스 레이어에서의 트랜잭션 개념입니다. UoW 패턴을 사용하면 데이터 무결성을 보장할 수 있습니다.
Session은 ORM 혹은 model 객체(object) 들을 이용하여 DB에 CRUD 작업들을 실행시킬 수 있습니다.
Session은 thread-safe를 보장하지 않습니다.
(이게 진짜 오늘의 핵심 포인트)
https://docs.sqlalchemy.org/en/14/orm/session_basics.html#is-the-session-thread-safe
scoped_session이란?
여기서부터가 진짜 중요한 내용입니다. 위에서 Session은 thread-safe하지 않다고 했습니다. 공식문서를 보면 Session은 일반적으로 하나의 스레드에서 비동기 방식으로 사용하기 위한 것이라고 나와있습니다.
두 번째 문단의 내용을 간단히 말하면 아래와 같습니다.
다중 스레드(multi-thread)간에 Session이 공유되는 경우 thread-safe하지 못하다.
이것을 막기 위해서는 하나의 스레드당 하나의 Session을 연결하면 된다.(=Thread-local).
아니면 session이 함수 간에 전달되는 과정에서 다른 스레드와 공유하지 않는 패턴을 이용하는 것이다.근데 위와 같은 것을 자동으로 해주는 게 scoped_session입니다!
scoped_session은 thread.local()을 사용하여 스레드 고유의 데이터 영역에 Session을 저장함으로서 thread-safe를 보장합니다. 이 말은 즉슨, 스레드끼리 Session을 공유하지 않습니다.
네 문제가 있죠. 위에서 계속 언급한 thread-safe와 관계있습니다. thread-safe는 다중 스레드 환경에서 여러 스레드가 공유자원에 접근해도 프로그램 실행에 문제가 없음을 뜻합니다. Session을 여러 스레드가 공유하게 되면 Session을 제어하면서 잘못된 동작 혹은 오류를 유발할 수 있습니다.
요약하자면...
thread-local을 사용하는 scoped_session은 하나의 thread당 하나의 Session을 가지기 때문에
thread-safe 하게 Session을 다룰 수 있다!코드로 알아보자!
그럼 scoped_session을 사용하면 정말 thread-safe 한 지? scpoed_session에서는 어떻게 thread-local을 구현해서 session을 매니징 하는지 알아보겠습니다.
1. session을 다중 스레드에서 공유하는 형태
Session은 sessionmaker로 생성합니다. 여기서 사용되는 engine은 데이터베이스 연결과 동작의 소스를 제공하는 객체입니다.
위 코드는 UserRepository의 메서드에서 thread에 session이 공유되는 형태입니다. API 호출을 통해 get_detail_info() 호출되면 어떻게 로그가 찍히는지 확인해 봅시다!
결과는 역시 동일한 Session을 스레드가 공유하는 걸 확인할 수 있습니다. 그렇다면 이렇게 Session을 여러 스레드에서 공유하게 되면 어떤 문제가 있을 수 있을까요?
UserRepository에 위와 같은 insert 메소드를 만들어 보았습니다. 예제가 다소 극단적이긴 하지만 만약에 두 개의 스레드가 동시에 같은 데이터를 가진 User객체를 add하고 main 스레드에서 commit하면 어떻게 될까요?
에러를 마주하게 됩니다. 두개의 스레드가 동일한 session을 사용함으로써 같은 트랜잭션 내에서 오류가 생겼습니다(Duplicate entry). 뭐 위처럼 코드를 작성하지는 않겠지만 이런 식으로 오류를 일으킬 수 있다고 생각하시면 됩니다. 그리고 같은 session내에서 에러가 생겼기 때문에 User는 DB에 추가되지 않았음을 확인할 수 있습니다.
이것이 SQLAlchemy 공식문서에서 말하는 Session이 thread-safe하지 않다는 말입니다.
그렇다면 scoped_session을 사용하면 어떨까요?
2. scoped_session에서 다중 스레드를 사용하는 경우
코드가 바뀌는 부분은 scoped_session을 사용하는 것뿐입니다. 먼저 위에서 만든 get_detail_info()를 호출해보겠습니다.
오! 정말 하나의 thread당 하나의 Session사용이 가능해졌습니다.
이제 insert()를 호출해볼 건데 약간의 코드 변경이 있습니다.
scoped_session은 thread당 다른 Session을 사용하기 때문에 각자 commit()을 호출하도록 했습니다. 결과는 어떨까요?
첫 번째 데이터를 추가할 때는 성공, 두번째 데이터를 추가할때는 오류가 났습니다. 어떻게 보면 당연한 거죠?! 다만 session을 서로 공유하지 않기 때문에 적어도 먼저 실행한 트랜잭션은 제대로 실행되었습니다.
지금 와서 보니 약간 예제가 다소 적절하지는 않은 것 같기도 하고... 좀 더 적절한 예제를 생각해본다면... Session을 여러 스레드에서 사용하는데 중간에 flush 혹은 commit을 어떤 스레드가 해버리면 다른 스레드에서는 잘못된 동작이 되겠... 죠?!
여튼 "여러 스레드에서 동일한 session을 공유하게 되면 thread-safe하지 않게 되어 의도하지 않은 동작이 일어날 수 있다" 정도로 이해하면 좋을 것 같습니다.
그럼 여기서 궁금증! scoped_session은 어떻게 thread-safe 하게 Session을 사용할 수 있는 것일까? 앞서 말했던 thread-local data를 사용하기 때문인데요.
scopre_session에서는 Session을 생성할 때 threading.local()을 사용합니다. self.registry.value에 값이 있으면 그대로 리턴, 값이 없으면 새로운 Session을 만들어서 리턴합니다.
디버깅을 찍어보면 이렇게 thread-local data로 Session을 가지고 있는 것을 볼 수 있습니다.
번외 (Thread가 아닌 Process를 사용하는 경우)
자 그럼 이 경우는 어떨까요? scoped_session을 사용할 때 Thread가 아닌 Process를 사용한다면?! ㅋㅋㅋ 한번 고민해보시고 아래를 봐주시겠어요?
결과는 동일한 Session을 사용합니다! 이유는 scoped_session은 하나의 스레드당 하나의 Session을 사용한다고 했습니다. 위 코드에서는 메인 스레드에서 두 개의 Process를 실행했습니다. 결국 두 프로세스가 같은 메인 스레드에서 실행되었기 때문에 같은 Session을 사용한 것입니다.
처음에 Thread 말고 Process로 쓰다가 예상했던 동작이 아니어서 삽질하다가 발견한 내용입니다;; ㅋㅋㅋ
마치며
사실 이전에 SQLAlchemy를 사용할 때 토이 프로젝트에서 사용하기도 했고.... 그렇기에 구현 자체에만 집중했던 것 같습니다. 이번에 공식문서와 다른 블로그들을 참고하면서 Session 하나에서도 thread-safe, thread-local, unit-of-work 등 다양한 개념들을 배울 수 있었습니다.
이번에 글을 쓰면서 공식문서를 잘 읽기 위해 영어를 공부해야겠다는 생각을 정말 많이 한 것 같네요 ㅋㅋㅋ 물론 구글 번역과 일부 문장은 해석이 되지만... 문법이 좀만 복잡해지거나 모르는 단어가 많으면 헷갈리더라고요 ㅋㅋㅋ 영어를 잘했으면 삽질을 덜 했을 것 같네요.
여튼 글을 쓰면서도 재밌었고 다음은 async_scope_session에 대한 내용을 정리해보도록 하겠습니다! 그런 오늘도 즐거운 코딩 하세요~
참고자료
https://jybaek.tistory.com/914
https://yujuwon.tistory.com/349
https://velog.io/@khh180cm/6.-%EC%9E%91%EC%97%85-%EB%8B%A8%EC%9C%84-%ED%8C%A8%ED%84%B4
https://soundprovider.tistory.com/entry/python-Thread-Local-Data
https://www.hides.kr/1103?category=666044
'💻 프로그래밍 > Python' 카테고리의 다른 글
백엔드 개발자가 만들어 본 App 테스트 자동화 (feat. Appium, Jenkins, AWS DeviceFarm) (1) 2024.01.07 [SQLAlchemy] async_scoped_session과 context-local (0) 2022.10.01 RabbitMQ 톺아보기 2부 (feat.pika) (0) 2022.09.17 RabbitMQ 톺아보기 1부 (0) 2022.09.12 어느날 신입 개발자가 나에게 물었다..."python에서 staticmethod를 사용하는 것에 있어서 메모리 이슈가 없는 것 일까요?" (feat. java) (4) 2022.07.17