[Trouble Shooting] DB 조회 시 이전 값이 조회되는 문제

kukjunLEE·2024년 4월 1일
1

Trouble Shooting

목록 보기
5/5
post-thumbnail

문제


해당 문제가 발생하는 상황을 Sequence Diagram으로 표현해보면, 다음과 같습니다.

문제 상황

해당 현상에 대해서 더 설명하면 다음과 같습니다.

  1. 서버 재부팅 이후 2~30분 동안은 나타나지 않음.
  2. 문제가 발생하고 실제 데이터베이스에서 조회 해보면, 변경되지 않은 값이 조회 됨.
  3. 업데이트 한 값이 바로 조회될 때도 있고, 바로 조회 안될 때도 있음.
    3-1. 조회 되지 않을 때, 서버를 재부팅하거나 더 시간이 지나게 되면 이전 값이 다시 조회됨.


외부의 문제 확인

DataBase
1. DB는 RDBMS, MariaDB의 latest 버전을 쓰고 있었고, 해당 버전에서 관련된 취약점을 찾을 수 없었습니다.
2. DB는 해당 Application과 DataGrip 외에는 Connection을 가지고 있지 않았습니다.


Cache
1. 이전 값 조회라는 키워드가 사실 캐시랑 연관이 될 수 있는데, 먼저 Radis와 같은 캐싱 데이터베이스를 사용하지 않았습니다.
2. 해당 문제와 관련해서 Nestjs Typeorm에서 캐시를 설정할 수 있다고 해서, 확인하고 캐싱 기능을 꺼 두었지만 문제가 해결되지 않았습니다.



해당 문제는 Application 내부에서 문제가 있다고 판단했고,
똑같은 호출에도 값이 변경되는 경우와, 변경된 것 처럼 보이지만 일정 시간 뒤에 다시 돌아오는 현상이 무분별하게 섞여 있어서 문제해결을 위해 깊게 파보기로 결정했습니다.





과정 및 해결


먼저 문제가 발생할 수 있는 부분을 생각해보면 다음과 같습니다.
1. 상품 A의 수정에 대해 DB 지연 입력 - 지연되는 동안 Connection 간 자원 공유가 되지 않음
2. 특정 Connection에 문제가 생겨 다른 Connection들과는 다른 값을 조회 함
3. Connection Pool 이상 및 TypeORM Issue (사용하는 TypeORM 버전 이상에서 해당 Issue가 있음)



과정

Application Log 추가
값을 수정하고, 조회하는 부분에 Log를 추가했습니다. Log를 살펴보니 Database에서 값을 가져올 때, 동일한 시점에 A요청은 변경된 값을 가져오고, B요청은 변경되지 않은 값을 가져오는 것을 확인할 수 있었습니다.
그리고 재부팅 하면 변경된 값이 반영이 안되고, 일정 시간이 지나도 변경된 값이 반영이 안되는 사실을 확인했습니다.

이를 Sequence Diagram으로 표현하면 다음과 같습니다.
Application Log

그리고 해당 현상이 발생한뒤 일정 시간이 지나거나 서버를 다시 띄워보면 다음과 같은 상황이 발생합니다.

Application Log 2

Application Log를 살펴보고 나면, 1번으로 말했던 상품 A의 수정에 대한 DB 지연 입력은 아니라고 판단됩니다. 판단 이유는 지연 입력이라고 하면, 기다린 후 지연 입력된 다음부터는 변경된 값으로 조회해야 합니다. 하지만 이전 값을 조회하고 있습니다.




DB SQL Log
이후에는 문제 상황을 로컬 환경에 구축하고, DB SQL Log를 활성화 시켜서 확인해보았습니다.

Log 활성화 결과, Connection 중 한개의 Connection이 RollBack 없이 StartTransaction을 수행하고 있었습니다.


해당 위치를 찾아본 결과 Cron으로 동작하는 부분에서 명시적 트랜잭션 선언을 사용하고 있었고, 중간에 return을 할때, Rollback을 해주고 있지 않았습니다. 간단하게 코드로 표현해보면 다음과 같습니다.

goodCode() {
  try {
	const connection = getConnection();
    connection.startTransaction();
    // 비지니스 로직
    connection.commitTransaction();
  } catch(e) {
  	// 예외 처리
    connection.rollback();
  } finally {
    connection.release();
  }
}

위와 같은 내용으로 작성하는 것이 일반적인데, 제 경우에는 다음과 같이 작성되어 있었습니다.

badCode() {
  try {
	const connection = getConnection();
    connection.startTransaction();
    // 비지니스 로직
    if(A === true) {
      return A;
    }
    connection.commitTransaction();
  } catch(e) {
  	// 예외 처리
    connection.rollback();
  } finally {
    connection.release();
  }
}

명시적 트랜잭션 선언을 해 놓고, return 하는 시점에 Rollback, Commit을 다 하지 않아서, 그대로 ConnectionPool로 트랜잭션이 닫히지 않은 상태로 넘어가게 되었습니다.

만약 해당 커넥션이 트랜잭션 없이 생성이나, 수정을 하는 내부 로직과 결합된다면 다음과 같은 문제가 발생할 수 있습니다.



Connection Issue 1

그림은 제가 손수 그렸습니다. 잘 그렸죠?

닫히지 않은 Connection을 가지고 트랜잭션 선언 없이 수정 API를 쓰게 되면, 지금과 같이, 수정되고, 닫히지 않은 Connection이 그대로 돌아가게 됩니다.



Connection Issue 2
이 때, 닫힌 Connection과 닫히지 않은 Connection은 서로 다른 값을 조회하게 됩니다. (이 값은, 고립 정책에 따라 달라질 수 있습니다.)

그리고 만약 트랜잭션을 사용하는 로직을 만나면 다음과 같은 현상이 발생합니다.



Connection Issue 3

Start Transaction을 한번 더 하면 Rollback으로 간주한다는 것은 공식 문서에 나와 있습니다.




결론

해당 내용으로 미루어볼 때, 문제는 Connection을 닫지 않은 상태로, Pool에 돌려주는 것 때문으로 판단됩니다. (2번 Issue)



해결


해결 방법은 명시적 트랜잭션을 선언하는 부분에서, 누락되어 있는 Rollback, Commit 처리를 해주면 됩니다.

그리고 데이터 생성과 수정에 있어서 트랜잭션이 없이 동작한다면, 동시에 들어오는 요청에 대해서 문제가 발생할 확률이 높아집니다. 그러므로 다른 API에 영향을 주지 않도록 서비스 내에서 DB 변경사항이 생기는 비지니스 로직의 시작과 끝에 트랜잭션 처리를 합니다.





고찰


해당 문제를 해결하는 과정에서 느낀점은 다음과 같습니다.

1. 단일 자원을 생성, 수정하는 경우에도 트랜잭션을 필요하다.

일반적으로 트랜잭션을 사용할 때, 단일 데이터인 경우가 아니라, 여러 데이터를 한번에 수정하는 경우에만 트랜잭션을 거는 경우가 있습니다. 트랜잭션을 연관된 데이터 변경을 보장해 주는 조건으로만 사용하는 경우가 이러합니다.

만약 이런 경우에만 사용하게 된다면, 동시에 오는 여러 요청에 대해서는 문제가 발생합니다. 두개의 요청이 A라는 자원에 대해 서로 다른 수정을 한다거나, 생성시 auto increase 되는 컬럼이 있으면 두 데이터가 거의 동시에 들어가게 되어 같은 auto increase를 발생시킨 다거나 ... 많은 문제가 발생할 수 있습니다.
이러한 문제를 해결하기 위해서 혹시 단일 생성, 수정에 트랜잭션이 없다면, 무조건 걸어두어야 한다고 생각합니다.



2. 트랜잭션 사용은 AOP 방식을 이용

모든 요청마다 Entity Manager를 이용해서 Transaction을 사용하기는 힘듭니다. Spring에서는 Annotation을 이용해서 Tranasction을 할 메서드를 미리 관리할 수 있습니다.

Typeorm에서도 데코레이터를 이용해서 트랜잭션을 적용할 부분을 미리 지정할 수 있습니다. 해당 라이브러리는 다음과 같습니다.
사실 직접 구현할 수도 있습니다.^o^

이러한 방법을 사용하면 코드에서 트랜잭션을 열고 닫는 과정이 없어져 실수를 하는 경우가 크게 줄게 됩니다.

profile
Backend Developer

0개의 댓글