Django ORM으로 MSSQL 사용하기

코인원

쿼리 성능과 씨름하며 ORM에 대해 생각하기

들어가며

안녕하세요, 코인원 Web Backend Engineer 김종헌입니다. 코인원에서는 웹 API를 개발할 때 python 기반 웹 프레임워크인 Django를 사용합니다. Django API 서버에서는 ORM을 적극적으로 활용하여 서비스의 다양한 데이터베이스와 상호작용하고 있습니다. 그중 MSSQL 환경에서 성능 개선 작업을 하면서 겪었던 어려움에 대해 이야기해보려고 합니다.

문제의 발견

제가 코인원에서 개발을 담당하고 있는 웹 API에서는 사용자의 인증 요청을 처리하거나 사용자 정보를 제공합니다. 또한 정보를 업데이트하는 등 사용자 정보와 관련된 일을 전담하고 있어 사용자 데이터 접근이 빈번하게 이루어집니다. 그런데 서비스 이용자가 증가하고 서비스의 규모가 확장될수록, 사용자 데이터의 접근 성능이 저하되는 것을 발견했습니다.

문제는 Django API 서버에서 MSSQL에 접근할 때 서비스 레벨의 격리 수준을 read uncommited로 사용하며 비즈니스 로직이나 미들웨어 레벨에서 select 쿼리를 실행할 때마다 공유 잠금을 걸며 업데이트 쿼리 성능의 저하를 유발하는 데 있었습니다. 저희는 API 서비스에서 사용자 데이터를 조회할 때 발생하는 shared lock을 해소함으로써 성능 저하를 해결하려고 했습니다.

해결 방법

공유 잠금에 대한 문제에 대해 발견한 해결 방법은 크게 두 가지 방식입니다. 첫 번째는 데이터베이스 세션에 대해 isolation level 을 read uncommited로 설정하는 것이고 두 번째는 MSSQL의 nolock을 이용하는 것입니다.

첫 번째 방법을 사용할 경우 데이터베이스 설정에서 쉽게 설정이 가능하지만 API 서비스에서 사용하는 모든 MSSQL의 테이블에 대해 동일한 isolation level이 적용되어 데이터 일관성의 보장이 필요한 경우에도 dirty reading이 발생할 우려가 있습니다.

저는 shared lock을 해소하기 위해 두 번째 방법인 nolock을 사용하기로 하였습니다.

MSSQL의 nolock

MSSQL에서의 기본 transaction isolation level은 read commited입니다. 데이터를 조회할 때 커밋되지 않은 내용을 읽을 수 없도록 하여 일관성을 유지하지만 이 과정에서 조회되는 데이터에 공유 잠금이 유지됩니다. 따라서 데이터를 조회하는 중에는 해당 데이터를 갱신할 수 없어 공유 잠금을 필요로 하는 읽기 옵션이 빈번하게 일어날 경우 업데이트와 같은 작업에 영향을 미칠 수 있습니다.

MSSQL에는 이와 같은 상황에 활용 가능한 nolock이라는 힌트가 존재합니다. Select 쿼리를 수행할 때 with (nolock) 힌트를 추가하게 되면 세션 레벨에서 Isolation Level을 조정하지 않고도 단일 쿼리에 대해 공유 잠금을 걸지 않고 데이터를 조회할 수 있습니다.

문제는 django ORM에서는 nolock 옵션을 지원하지 않는다는 것이었습니다.

MSSQL의 뷰 활용하기

Django ORM에서 자체적으로 쿼리 수행 시 힌트를 추가해줄 방법이 없어 MSSQL의 뷰를 활용하기로 하였습니다. with (nolock) 힌트를 사용하여 원본 테이블의 모든 칼럼 데이터를 조회하는 뷰를 생성해준 후 이를 서비스에서 같이 사용하기로 하였습니다.

Django는 모델의 상속을 지원하여 공통되는 정보를 가진 다수의 모델의 정보를 Abstract model에서 관리할 수 있도록 합니다. abstract로 정의된 모델은 독립적으로 테이블을 생성하거나 매니저를 가지지 않고 이를 상속받는 모델에서 실제 데이터베이스 테이블 생성과 데이터 조작이 이루어집니다. 원본 테이블과 nolock 뷰는 같은 칼럼 이름으로 데이터에 접근이 가능하므로 이 테이블의 칼럼들을 필드로 가지는 Abstract model을 정의해줍니다.

다음은 원본 테이블과 뷰에 대해 Abstract model를 상속받는 모델로 정의해주고 Meta 클래스를 override 해 테이블 이름을 설정해줍니다.

결과적으로 한 개의 테이블에 대해 두 개의 모델을 선택적으로 사용하며 단순 조회 쿼리의 수행이 필요한 경우 nolock 힌트를 사용하는 뷰 모델을, 데이터의 업데이트나 생성이 필요한 경우 원본 테이블을 이용할 수 있도록 하였습니다.

결과

읽기 전용 모델과 업데이트용 모델을 함께 사용하니 단순 조회 쿼리 수행 시에는 shared lock을 걸지 않아 퍼포먼스 저하가 눈에 띄게 감소하였습니다. 다만 이와 같은 방식을 적용할 경우 아쉬운 점도 있습니다. 읽기 전용 모델에서 얻어낸 쿼리 세트에 대해 바로 업데이트가 어려우며 Django DB 마이그레이션의 사용이 어렵습니다.

마무리하며

ORM을 사용하는 것은 범용적인 객체 모델 내에서 데이터를 정의하고 조작하는데 큰 편의성이 있습니다. 쿼리를 직접 작성하지 않고 객체로서 데이터를 다룰 수 있다는 것은 분명한 장점입니다.

하지만 ORM 역시 Silver bullet이 아닙니다. sql 레벨에서의 튜닝이 필요하거나 특정한 데이터베이스 엔진에 결부된 문제를 해결하고자 할 때에는 ORM에서 제공하는 기능에는 분명 한계가 존재합니다.

ORM을 적극적으로 활용하는 Django 프레임워크는 코인원이 빠르게 비즈니스 로직을 구축하는데 큰 도움이 되었지만 퍼포먼스의 개선에 대해 고민할 때 ORM의 틀 안에서 골머리를 앓기도 했습니다.

따라서 ORM의 사용을 고려하시거나 프레임워크를 선택하실 때 서비스의 목적과 사용되는 데이터의 특성을 고려하여 도입하시는 것을 권해드립니다.

김종헌, Backend Engineer

기업문화 엿볼 때, 더팀스

로그인

/