Typeorm 에 기여해보기

TypeORM 사용 중 발생한 데드락 문제를 해결하기 위해 직접 Pull Request를 보내고 merge된 과정을 공유합니다.

Background


업무에서 1년 넘게 typeorm 을 사용해 오고 있는데, 가끔식 이유를 알 수 없는 에러가 Datadog 에 잡히는 이슈가 있었다. 가끔씩 트래픽이 몰리게 되는 날, MSSQL 에서 다음과 같은 에러 메세지를 발견하게 되었다.

transaction was deadlocked on lock resources with another process and has been chosen as the deadlock victim. rerun the transaction. ...

로드밸런싱 되어있는 서버 컨테이너들의 DB Connection 들에서 SELECT 문을 DB 에 날렸을 때, 다른 컨테이너가 Transaction 을 잡고 있다면 deadlock 상태에 빠져서 나타나게 되는 에러였다. 코드를 이상하게 짰다면 충분히 발생할 수 있는 문제이지만, SELECT 쿼리에 WITH (NOLOCK) Hint 를 사용했음에도 위와 같은 에러가 발생하게 되었다.

typeorm 으로 작성했던 쿼리는 다음과 비슷한 쿼리였다.

await getConnection()
        .createQueryBuilder()
        .select("order")
        .setLock("dirty_read")
        .from(Order, "order")
        .innerJoinAndSelect("order", "order.OrderItems")
        .where("1=1")
        .getMany()


위의 쿼리는 다음과 같은 SQL 로 실행될 것을 예상하고 있었다.

SELECT      *
FROM        Order O       WITH (NOLOCK)
LEFT JOIN   OrderItems OI WITH (NOLOCK)
    ON      O.OrderID=OI.OrderID
WHERE       1=1


하지만 실제로 실행되는 쿼리는 다음과 같은 쿼리였다.

SELECT      *
FROM        Order O     WITH (NOLOCK)
LEFT JOIN   OrderItems  
    ON      O.OrderID=OI.OrderID
WHERE       1=1


위와 같은 쿼리가 실행되는 경우, OrderItems 에는 NOLOCK 힌트가 걸리지 않아, deadlock 상태에 빠질 수 있었다. 구글링을 통해 다양한 시도를 해 보았지만, 현재 typeorm 의 기능으로는 위의 문제를 해결할 수 없었다. 결국 typeorm 에 Pull Request 를 보내서, 문제상황을 해결해 보기로 했다.




Pull Request

https://github.com/typeorm/typeorm/pull/8507

typeorm 의 내부 코드를 까서, 실제로 SELECT 문이 어떻게 생성되는지를 살펴보았다. 생각보다 코드가 직관적이고 원시적(?) 인 형태로 구성되어 있어서, 문제가 되는 코드가 어디인지 파악하는 데에는 오래 걸리지 않았다. SELECTJOIN 쿼리를 생성하는 부분이 명확히 분리되어 있어서, 어떻게 코드를 수정해야 될지도 비교적 명확하게 보였다. https://github.com/typeorm/typeorm/blob/master/src/query-builder/SelectQueryBuilder.ts

/**
  * Creates "LOCK" part of SELECT Query after table Clause
  * ex.
  *  SELECT 1
  *  FROM USER U WITH (NOLOCK)
  *  JOIN ORDER O WITH (NOLOCK)
  *      ON U.ID=O.USER_ID
  */
private createTableLockExpression(): string {
    if(this.connection.driver instanceof SqlServerDriver) {
        switch (this.expressionMap.lockMode) {
            case "pessimistic_read":
                return " WITH (HOLDLOCK, ROWLOCK)";
            case "pessimistic_write":
                return " WITH (UPDLOCK, ROWLOCK)";
            case "dirty_read":
                return " WITH (NOLOCK)";
        }
    }

    return "";
}


먼저, 위와 같이 LOCK HINT 를 생성해주는 부분을 인라인 코드에서 별도의 함수로 분리하였다. SELECTJOIN 문에서 공통으로 사용되는 기능이기 때문이다. 그리고, 실질적으로 SELECT 쿼리를 생성해 주는 부분들에서 위의 함수를 찾아서 호출하는 방식으로 기능을 변경하였다.

return select + selection + " FROM " + froms.join(", ") + this.createTableLockExpression() + useIndex;
...
return " " + joinAttr.direction + " JOIN " + destinationJoin + " " + this.escape(destinationTableAlias) + this.createTableLockExpression() +
                    (joinAttr.condition ? " ON " + this.replacePropertyNames(joinAttr.condition) : "");


위와 같이 코드를 고친 후 로컬에서 SELECT 쿼리를 날려 보았더니, 의도대로 코드가 동작함을 확인할 수 있었다.


typeorm 에는 지켜야 할 Contribution Rule 이 있었는데(https://github.com/typeorm/typeorm/blob/master/CONTRIBUTING.md), 이번 코드 변경의 경우 fix 로 분류되어, 필수적으로 테스트 코드들을 작성해야 Pull Request 로 받아들여질 수 있었다. typeorm 에서는 chai 라는 테스트 라이브러리를 이용하고 있었는데, 사용법은 jest 와 크게 다르지 않아, 쉽게 적응해서 테스트 코드를 작성할 수 있었다.
Image
Pull Request 를 요청하자, Github Action과 CircleCI 를 통해서 내가 작성한 테스트 코드와, 기본적으로 충족해야 할 테스트 코드들이 돌기 시작했다. 각각의 Nodejs 버전, 그리고 각각의 DB 밴더사별로 테스트 코드들이 돌기 시작했다. 약 30분이 경과했을 때, 모든 테스트들이 성공적으로 통과했음을 확인할 수 있었다.




Merge

typeorm 의 운영자들과 몇 번의 소통을 한 후에, 본격적인 Code Review 가 진행되게 되었다. Alex 라는 개발자가 나의 코드를 약 3주 정도 리뷰하였다. 나의 코드의 영향도 파악을 진행했고, 별다른 코드 수정 요청은 들어오지 않았다. 약 5주 후에, 나의 Pull Request 가 master 브랜치에 병합되었다.
Image

유명한 오픈소스 라이브러리에 기여하는 것이 버킷리스트 중 하나였는데, 평소에 자주 쓰는 typeorm 에 코드로 기여할 수 있어서 기뻤다. 망상일수도 있지만, 개발자들이 typeorm 을 통해 SELECT 문을 실행할 때마다 나의 코드가 사용되게 된다는 점이 제일 기뻤다. 앞으로도 자주 사용하는 라이브러리들을 유심히 지켜봐서, 오픈소스 활동을 계속 해보고 싶다는 마음이 생겼다.

Contents

Background Pull Request Merge

이것도 읽어보세요