[트러블슈팅 - Spring] 데이터가 있었는데요, 아니 없어요

Hocaron·2024년 6월 15일
2

트러블슈팅

목록 보기
10/12

🤷‍♀️ 데이터 있어요?

회원 데이터가 Master DB 에 INSERT 되었고 Slave DB 에서도 SELECT 하면 조회되는데, Master DB 의 다른 Session 으로 SELECT 하면 데이터가 조회되지 않는 이슈가 있었어요.

회원마케팅서비스 개발팀에서 이슈의 원인을 파악하고, 해결한 과정을 공유드리려고 합니다.

울기 시작한 페페

© 2024. Kurly. All rights reserved.

간헐적으로 존재하는 회원 데이터가 조회되지 않는 에러가 발생했어요. 이상한 점은 에러 발생 후에 다시 요청하면 데이터가 조회되고 있어요.

✔️ 사용하고 있는 데이터베이스 엔진 및 격리수준은 다음과 같아요.

  • 5.7.mysql_aurora.2.11.1
  • REPEATABLE READ

✔️ 이슈에 영향을 준 서비스의 설정은 다음과 같아요.

  • MariaDB Connector/J 2.7, aurora 모드
  • HikariCP 사용
  • hikari.auto-commit 설정: false (기본 설정은 true 이에요)
  • jpa.open-in-view 설정: true (기본 설정은 true 이에요)

✔️ 이슈가 발생한 코드는 다음과 같아요.

public Membership outerMethod(long memberNo) {

        var member = memberRepository.findByMemberNo(memberNo)
                .orElseThrow(() -> new RuntimeException("요청한 리소스를 찾을 수 없습니다.");

        var innerResult = service.innerMethod(memberNo);
        return ...;
    }
  • outerMethod() 에는 @Transactional 설정이 없어요.
  • innerMethod()@Transactional(readOnly = true) 설정이 적용되어있어요.

이슈 상황을 정리해볼게요.

  1. DB 에 회원 데이터 INSERT 완료
  2. DB 에서 회원 데이터 SELECT 시, 정상 조회
  3. DB 에서 회원 데이터 SELECT 시, 조회 되지 않음

🤔 이슈 원인 파악

❎ Slave DB 에서 발생할 수 있는 Replica Lag 때문이지 않을까요?

데이터 생성 시기와 조회 시기의 차이가 최대 1분이내였기때문에, Master DB 에 데이터가 적재 되었지만 Replica Lag 으로 인해 Slave DB 에 데이터가 늦게 복제되는 경우를 의심했어요.

멤버십 서비스에서 Slave DB 를 통해 데이터를 조회한다면 Replica Lag 현상으로 데이터가 조회되지 않을 수 있거든요. 다시 요청하면 데이터가 조회되는 이유도 Master DB 에서 Slave DB 로 데이터 복제가 완료되어 데이터가 조회될 수 있는 거죠!

하지만 Replica Lag 이 원인은 아니었어요. 멤버십 서비스에서 조회 쿼리가 실행되는 Connection 디버깅 시에 Slave DB 가 아닌 Master DB 에서 실행되고 있었기 때문이에요.

Replica Lag
Slave DB 가 Master DB 에서 발생하는 업데이트를 따라잡을 수 없을 때 발생해요. 적용되지 않은 변경 사항은 Slave DB 의 릴레이 로그에 누적되며, Slave DB 와 Master DB 의 데이터베이스 버전 차이는 점점 커져요.
즉, Slave DB 의 Master DB 복제가 지연되는 것이에요.

혼란스럽지만 이슈 상황을 다시 정리해볼게요.

  1. Master DB 에 회원 데이터 INSERT 완료
  2. Master DB 에서 Slave DB 로 데이터 복제 완료
  3. Slave DB 에서 데이터 SELECT 시, 정상 조회
  4. Master DB 에서 회원 데이터 SELECT 시, 조회 되지 않음

✅ 원인은 COMMIT 이 실행되지 않았기 때문이에요.

public Membership outerMethod(long memberNo) {

        var member = memberRepository.findByMemberNo(memberNo)
                .orElseThrow(() -> new RuntimeException("요청한 리소스를 찾을 수 없습니다.");

        var innerResult = service.innerMethod(memberNo);
        return ...;
    }

autocommit 설정이 false 이기 때문에 쿼리 종료시점에 수동으로 COMMIT 이 실행되어야해요. 개발자가 명시적으로 실행하거나, @Transactional 을 사용할 수도 있어요. @Transactional 은 메서드 종료 시점에 정상 종료 시 COMMIT, 예외 발생 시 ROLLBACK 을 실행시켜요.

그럼 COMMIT 이 실행되지 않으면 어떤 일이 벌어지나요?

COMMIT 이 실행되지 않으면, Consistent Nonlocking Reads 로 인해 Snapshot 이 갱신되지 않아 최초 쿼리 실행 시점 이후에 실행된 트랜잭션의 변경사항을 보지 못해요.

Consistent Nonlocking Reads 는 무엇인가요?

InnoDB 가 특정 시점의 데이터베이스 Snapshot 을 쿼리에 제공하기 위해 MVCC(다중 버전 동시성 제어)를 사용하는 것을 의미해요. 쿼리는 그 시점 이전에 커밋된 트랜잭션의 변경 사항은 보지만, 그 이후의 변경 사항이나 커밋되지 않은 트랜잭션의 변경 사항은 보지 않아요. 단, 같은 트랜잭션 내에서 이전에 실행된 명령문에 의해 변경된 사항은 예외에요.

트랜잭션 격리 수준이 기본값인 REPEATABLE READ 일 경우, 같은 트랜잭션 내의 모든 일관된 읽기는 그 트랜잭션에서 처음 읽기를 수행한 시점의 Snapshot 을 읽어요. 현재 트랜잭션을 커밋한 후 새로운 쿼리를 실행하면 더 최신의 Snapshot 을 얻을 수 있어요.

fyi; innodb-consistent-read

용어도 어렵고, 설명도 어려우니 코드로 보기로 해요.

time         Session A                               Session B
  |
  |        SET autocommit=0;                       SET autocommit=0;
  |
  |        SELECT * FROM t; -- Snapshot #1
  |        데이터 없음
  |                                                INSERT INTO t VALUES (1, 2);
  |
  |        SELECT * FROM t; -- Snapshot #1
  |        데이터 없음
  |                                                COMMIT;
  |
  |        // ✨ Session B 추가한 데이터를 보지 못해요.
  |        SELECT * FROM t; -- Snapshot #1
  |        데이터 없음
  |
  |        // ✨ 다음 쿼리 실행시점에 Snapshot #2 생성
  |        COMMIT;          
  |
  |        SELECT * FROM t; -- Snapshot #2
  |        ---------------------
  |        |    1    |    2    |
  v        ---------------------

SET autocommit=0; 설정이 된 경우, Session B 에서 데이터를 INSERTCOMMIT 이 실행되어도 Session A 에서 COMMIT 이 실행되기 전까지 Snapshot #1 을 가지고 있어 Session B 에서 추가한 데이터를 보지 못하는 현상이 발생해요.

COMMIT , ROLLBACK , BEGIN 모두 동일하게 Snapshot 을 갱신 해주는 역할을 해요.

실마리가 보여요! 이슈 원인을 정리해 볼게요.

  1. (Session A) 기존 회원 데이터 조회 요청 시에 Snapshot #1 이 생성
  2. (Session B) 신규 회원 가입
  3. (Session B) 신규 회원 데이터 조회 성공
  4. (Session A) Snapshot #1 에서는 신규 회원 데이터를 볼 수 없으므로 ROLLBACK 실행
  5. (Session A) ROLLBACK 으로 인해 신규 회원 데이터 INSERT 변경내역이 반영된 Snapshot #4 가 생성되고, 신규 회원 데이터 조회 성공

기존 회원 데이터 조회는 왜 일어나는 걸까요?
멤버십 서비스의 회원정보 조회 API 는 회원가입 직후 외에도 회원의 멤버십 정보가 필요한 시점마다 호출되는 API 에요. 그래서 기존 회원 데이터 조회도 발생하고 있어요.

📄 발생했던 이슈를 정리하고, 해결해볼게요.

간헐적으로 존재하는 회원 데이터가 조회되지 않는 에러가 발생했어요. 이상한 점은 에러 발생 후에 다시 요청하면 데이터가 조회되고 있어요.

간헐적으로 신규 데이터가 적재되기 전 Snapshot 을 가지고 있던 Session 에서 데이터가 조회되지 않았어요. 데이터가 조회되지 않으면 ROLLBACK 이 발생하고, 그 다음 데이터 요청에서 Snapshot 이 갱신되기 때문에 다시 요청하면 데이터가 조회되고 있어요.

❎ [해결방법1] READ COMMITTED 격리수준으로 낮추기

READ COMMITTED 격리 수준에서는 각 SELECT 명령어가 별도의 Snapshot 을 사용해요.

저희 팀에서는 MySQL 의 기본 트랜잭션 격리 수준과 동일한 REPEATABLE READ 를 사용하고 있는데, 격리수준을 낮추는 경우에 Phantom Read 이슈가 발생하여 기존 비즈니스 로직에 영향을 주는 등의 사이드 이펙트를 고려하여 다른 해결방법을 찾아보기로 해요.

Phantom Read
한 트랜잭션에서 두 번의 읽기 작업을 수행할 때, 서로 다른 결과를 반환하는 현상을 말해요. 이는 다른 트랜잭션이 새로운 행을 삽입하거나 기존 행을 삭제하여 발생해요.
fyi; phantom rows

❎ [해결방법2] 잠금 읽기를 사용하기

SELECT * FROM t LOCK IN SHARE MODE;

잠금 읽기(locking read)를 사용하면 InnoDB 는 Snapshot 이 아니라 테이블의 최신 데이터를 읽어요. 이는 데이터의 일관성을 유지하기 위함이에요.

하지만 잠금 읽기를 사용하면 SELECT 명령어가 실행될 때 해당 행을 잠그고, 다른 트랜잭션이 그 행을 수정하거나 삭제하지 못하게 되어 락 경합 및 데드락 발생 가능성 증가 이슈가 발생할 수 있어요. 다른 해결방법을 찾아보기로 해요.

✅ [해결방법3] @Transactional(readOnly = true) 설정 추가하기

저희 팀은 @Transactional(readOnly = true) 설정을 추가하여, 메서드가 종료될 때 실행되는 COMMIT 을 통해 Snapshot 을 갱신하는 방법을 사용하기로 했어요.

@Transactional(readOnly = true)
public Membership outerMethod(long memberNo) {

        var member = memberRepository.findByMemberNo(memberNo)
                .orElseThrow(() -> new RuntimeException("요청한 리소스를 찾을 수 없습니다.");

        var innerResult = service.innerMethod(memberNo);
        return ...;
    }

이제 매 요청마다 테이블의 최신 데이터가 포함된 Snapshot 기준으로 데이터를 조회하여, 존재하는 데이터를 정상적으로 조회할 수 있을거에요.

➕ 함께 살펴보면 좋은 내용들이에요.

개발자가 정의한 쿼리메서드는 @Transactional 적용이 되어있지 않아요.

SimpleJpaRepository 정의된 Spring Data JPA의 기본 메서드에는 @Transactional 이 적용됩니다.

  • 읽기 전용 메서드 (findById, findAll 등): @Transactional(readOnly = true)
  • 쓰기 메서드 (save, delete 등): @Transactional

하지만 findByMemberNo 처럼 JpaRepository 인터페이스에 정의한 쿼리 메서드는 @Transactional 이 적용되지 않아 트랜잭션에 의한 COMMIT 이 실행되지 않아요.

그래서 트랜잭션을 적용하려면 개발자가 명시적으로 @Transactional 을 추가해야 합니다.

사용자가 정의한 쿼리 메서드인지 확인하는 코드는 여기있어요.
[QueryExecutorMethodInterceptor.doInvoke()](https://github.com/spring-projects/spring-data-commons/blob/19e2a1c925c34bb195fbdd4778d8e89b0a3a37a6/src/main/java/org/springframework/data/repository/core/support/QueryExecutorMethodInterceptor.java#L150-L179) 에서 쿼리 메서드인지 확인하여 CrudRepoisotory 에게 위임 여부를 결정해요.
    @Nullable
	private Object doInvoke(MethodInvocation invocation) throws Throwable {

		Method method = invocation.getMethod();

                // 🌟🌟🌟 쿼리 메서드인지 확인
		if (hasQueryFor(method)) {

			RepositoryMethodInvoker invocationMetadata = invocationMetadataCache.get(method);

			if (invocationMetadata == null) {
				invocationMetadata = RepositoryMethodInvoker.forRepositoryQuery(method, queries.get(method));
				invocationMetadataCache.put(method, invocationMetadata);
			}

			return invocationMetadata.invoke(repositoryInformation.getRepositoryInterface(), invocationMulticaster,
					invocation.getArguments());
		}
                // 쿼리 메서드가 아니라면 기본 CRUD 메서드로 위임
		return invocation.proceed();
	}

	private boolean hasQueryFor(Method method) {
		return queries.containsKey(method);
	}
Log 를 통해서 @Transactional 설정 적용 여부 확인이 가능해요!
// application.yml 형식을 사용한다면 아래와 같이 로깅 레벨을 설정해주세요.
logging:
  level:
    org.springframework.orm.jpa: TRACE
    org.springframework.transaction: TRACE

// application.properties 형식을 사용한다면 아래와 같이 로깅 레벨을 설정해주세요.
logging.level.org.springframework.orm.jpa=TRACE
logging.level.org.springframework.transaction=TRACE
o.s.t.i.TransactionInterceptor           : No need to create transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findByMemberNo]: This method is not transactional.          // @Transactional 적용되지 않아요
...
o.s.orm.jpa.JpaTransactionManager        : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findById]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly // @Transactional 이 적용되어요.
o.s.t.i.TransactionInterceptor           : Getting transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findById]
o.s.t.i.TransactionInterceptor           : Completing transaction for [org.springframework.data.jpa.repository.support.SimpleJpaRepository.findById]

HikariCP 는 Connection 종료 시에 COMMIT 여부를 체크해요.

HikariCP 는 JDBC Connection 관리를 위해 ProxyConnection 클래스를 사용하여 실제 JDBC Connection 객체를 관리하고 래핑합니다. Connection 종료 시점에 COMMIT 여부를 확인하여, COMMIT 이 실행되지 않았다면 ROLLBACK 을 실행시켜요.

Connection 종료시점에 COMMIT 여부를 확인하는 코드는 여기있어요.

ProxyConnection.close() 에서 COMMIT 이 실행되지 않았고(isCommitStateDirty = true) autocommit 설정이 false 라면 ROLLBACK 을 실행시켜요.

    public final void close() throws SQLException {
            try {
                // ✨✨✨ COMMIT이 없고, autocommit 설정이 false 라면 ROLLBACK
                if (this.isCommitStateDirty && !this.isAutoCommit) {
                    this.delegate.rollback();
                    LOGGER.debug("{} - Executed rollback on connection {} due to dirty commit state on close().", this.poolEntry.getPoolName(), this.delegate);
                }
...
}

이는 트랜잭션의 일관성을 유지하기 위함으로 트랜잭션이 `COMMIT` 되어야 할 상태인데도 불구하고, 커밋하지 않은 변경 사항이 존재할 가능성이 있어 Connection 을 닫을 때 `ROLLBACK` 을 실행하여 변경 사항을 취소해요

위에서 언급한 JPA 쿼리 메서드나 QueryDSL@Transactional 설정이 적용되어있지 않아 COMMIT 이 실행되지 않기 때문에 autocommit 설정이 false 라면, 쿼리가 정상적으로 실행되었더라도 ROLLBACK 이 실행됩니다. 물론 ROLLBACK 이 실행된다고 해서 서비스에서 예외가 반환되는 것은 아니에요.

ROLLBACK 이 발생하는 예시코드에요.
public void rollbackTest(long memberNo) {
    var member = memberRepository.findByMemberNo(memberNo);
    memberRepisitory.findByName(member.getIdentification());
}

서비스는 정상 응답을 하지만, JPA 쿼리 메서드가 종료되는 시점마다 ROLLBACK 이 발생하는 것을 알 수 있어요.

Log 를 통해서 ROLLBACK 실행 여부 확인이 가능해요!
// application.yml 형식을 사용한다면 아래와 같이 로깅 레벨을 설정해주세요.
logging:
  level:
    com.zaxxer.hikari.pool.ProxyConnection: TRACE

// application.properties 형식을 사용한다면 아래와 같이 로깅 레벨을 설정해주세요.
  logging.level.com.zaxxer.hikari.pool.ProxyConnection=TRACE
com.zaxxer.hikari.pool.ProxyConnection   : HikariPool-1 - Executed rollback on connection org.mariadb.jdbc.MariaDbConnection@321302bc due to dirty commit state on close().

이번 이슈에서 데이터가 조회되지 않으면 ROLLBACK 이 발생하는 이유도 JPA 쿼리 메서드로 조회 후에 데이터가 없으면 Exception 을 반환하는데, ProxyConnection 종료 시점에서 해당 Connection 에 COMMIT 이 실행되지 않아 ROLLBACK 을 시켜 버린 것이었습니다.

MariaDB Connector/J 의 aurora 모드는 읽기 전용(readOnly=true)으로 설정하면 Slave DB로 로드 밸런싱을 지원해요.

@Transactional 설정 또는 설정이 없는 경우는 Master DB 로 요청이 가고, @Transactional(readOnly=true) 설정인 경우 Slave DB 로 요청이 갑니다.

MariaDB Connector/J 드라이버는 v3.0.3 부터 aurora 모드를 지원하지 않으니 주의해주세요!
관련 내용이 궁금하다면 aurora 모드를 지원하지 않는다고? 블록을 참고해주세요.

fyi; about-mariadb-connector-j

🙋‍♀️ 위 내용과 함께 이슈가 발생했던 코드를 보면 궁금증이 생길 수 있어 자체 Q & A 세션을 준비했어요.

public Membership outerMethod(long memberNo) {

        var member = memberRepository.findByMemberNo(memberNo)
                .orElseThrow(() -> new RuntimeException("요청한 리소스를 찾을 수 없습니다.");

        var innerResult = service.innerMethod(memberNo);
        return ...;
    }
  • outerMethod() 에는 @Transactional 설정이 없어요.
  • innerMethod()@Transactional(readOnly = true) 설정이 적용되어있어요.

🆀 findByMemberNo() 메서드 실행 후 데이터가 조회되더라도 ROLLBACK 이 실행되어 Snapshot 이 갱신되었을텐데, 왜 과거 Snapshot 을 가지고 있었나요?

🅰 예리하시군요. 사용자가 정의한 쿼리메서드는 @Transactional 이 적용되지 않아요. 그래서 Connection 을 닫을 때, COMMIT 이 없어 ROLLBACK 이 실행되고 Snapshot 이 갱신되는게 맞아요.

그렇다면 매 요청마다 최신 스냅샷으로 갱신되어 데이터를 조회할 수 있어야 하는데, 왜 조회되지않았을까요? 이건 open-in-view 설정과 관련이 있어요.

open-in-view 설정이 false 라면 트랜잭션 단위로 Connection 이 시작 및 종료돼요. 그래서 findByMemberNo 실행 후 Connection 종료되고, 이때 ROLLBACK 이 실행되어 매 요청에서 최신 Snapshot 을 가지고 있었을 거에요. 최신 Snapshot 을 가지고 있었다면 존재하는 회원 데이터는 조회돼요.

하지만, 위 프로젝트 설정은 open-in-view 설정이 true 였어요. Connection 시작 시점부터 API 종료시점까지 Connection 이 유지되고, API 종료시점에 Connection 의 COMMIT 여부를 체크하게 되는데 하위에 존재하는 @Transactional(readOnly = true) 설정으로 인해 Connection 에 COMMIT 이 실행되어 ROLLBACK 이 실행되지 않았고, Snapshot도 갱신되지 않았어요😢

🆀 innerMethod()@Transactional(readOnly = true) 가 있는데, Slave Session 의 Snapshot 은 매번 갱신되나요?

🅰 맞아요. Slave Session 은 매 요청마다 Snapshot 이 갱신돼요.

public Membership outerMethod(long memberNo) {

        var member = memberRepository.findByMemberNo(memberNo); // 🙅‍♀️ 데이터 없음

        var innerResult = service.innerMethod(memberNo);
        return ...;
    }

@Transactional(readOnly = true)
public InnerResult innerMethod(long memberNo) {

        var member = memberRepository.findByMemberNo(memberNo); // 🙆‍♀️ 데이터 있음
...
}
  • outerMethod() 에는 @Transactional 이 적용되어있지 않아 Master Session 에서 실행돼요.
  • outerMethod() 에서 회원의 데이터가 없으면 에러를 반환하지만, innerMethod() 메서드 호출을 위해 제거해볼게요.
  • innerMethod()@Transactional(readOnly = true) 이 적용되어있기 때문에 Slave Session 에서 실행돼요.

저희 팀은 MariaDB Connector/J 의 aurora 모드를 사용하고 있어요. 따라서 @Transactional(readOnly = true) 가 존재하는 innerMethod() 는 Slave Session 에서 실행되고, 메서드 종료시점에 COMMIT 이 발생하여, 매 요청마다 Snapshot 이 갱신됩니다. Master Session 은 Snapshot 이 갱신되지 않았는데 말이죠.

그래서 Master Session 에서 조회되지 않던 데이터가 Slave Session 에서 조회되는 이상한 현상이 발생해요.

🆀 그럼 innerMethod() 에 (readOnly=true) 옵션이 없었다면, Snapshot 이 매번 갱신되어 데이터가 정상 조회되었을까요?

🅰 맞아요. innerMethod() 에 (readOnly=true) 옵션을 제외한 @Transactional 설정이 적용되었다면 데이터는 정상 조회돼요.

Master Session 에서 COMMIT 이 발생하여 Snapshot 이 갱신되었을 것이기 때문에 위 이슈는 발생하지 않았어요.

하지만 조회 쿼리이기때문에 (readOnly=true) 옵션을 제외하여 Master DB 에서 조회 쿼리가 실행되는 것보다는 outerMethod()@Transactional(readOnly = true) 설정을 추가해서 전체 쿼리가 Slave DB 에서 실행되도록 변경하는게 더 좋을 것 같아요.

🆀 잠시만요, hikari.auto-commit 설정이 기본값인 true 로 되어있으면 개발자가 COMMIT 을 신경쓰지 않아도 되는거 아닌가요? false 로 설정한 이유가 있나요?

🅰 맞아요. hikari.auto-commit 설정이 기본값인 true 라면 각 SQL 문은 자체적으로 단일 트랜잭션을 형성하여 오류가 발생하지 않은 쿼리에 대해 COMMIT 을 수행하기 때문에 COMMIT 여부를 신경쓰지 않아도 돼요.

하지만, @Transactional 전후로 추가적인 쿼리가 발생하게 돼요. autocommit 설정이 true 라면 트랜잭션으로 묶어서 작업을 수행해도 즉시 반영 되기때문에, 이를 방지하기 위해 Hibernate 는 트랜잭션 전후로 setAutoCommit(false), setAutoCommit(true) 를 수행하여 트랜잭션 관리와 데이터 일관성을 보장해요.

fyi; innodb-autocommit-commit-rollback

setAutoCommit(false), setAutoCommit(true) 쿼리를 수행하는 코드는 여기있어요.

AbstractLogicalConnectionImplementor.begin & resetConnection 에서 수행돼요.

	@Override
	public void begin() {
		try {
			if ( !doConnectionsFromProviderHaveAutoCommitDisabled() ) {
				log.trace( "Preparing to begin transaction via JDBC Connection.setAutoCommit(false)" );
				getConnectionForTransactionManagement().setAutoCommit( false ); // 🌟🌟🌟 트랜잭션 시작 전에 setAutoCommit( false )
				log.trace( "Transaction begun via JDBC Connection.setAutoCommit(false)" );
			}
			status = TransactionStatus.ACTIVE;
		}
		catch( SQLException e ) {
			throw new TransactionException( "JDBC begin transaction failed: ", e );
		}
	}

	protected void resetConnection(boolean initiallyAutoCommit) {
		try {
			if ( initiallyAutoCommit ) {
				log.trace( "re-enabling auto-commit on JDBC Connection after completion of JDBC-based transaction" );
				getConnectionForTransactionManagement().setAutoCommit( true );  // 🌟🌟🌟 트랜잭션 종료 후에 setAutoCommit( true )
				status = TransactionStatus.NOT_ACTIVE;
			}
		}
		catch ( Exception e ) {
			log.debug(
					"Could not re-enable auto-commit on JDBC Connection after completion of JDBC-based transaction : " + e
			);
		}
	}
autocommit 설정을 false 로 하여 성능 개선을 할 수 있었어요.

@Transactional 설정이 있는 쿼리 전후로 set autocommit = 0, set autocommit = 1 이 수행되는 것을 확인할 수 있어요.

Propagation 이 REQUIRES_NEW 인 경우는 부모 트랜잭션이 있더라도 set autocommit 쿼리가 전후로 발생하고, REQUIRED 는 부모 트랜잭션이 없어 새로운 트랜잭션을 생성할 때만 쿼리가 전후로 발생해요. 부모 트랜잭션이 있다면 쿼리는 추가로 발생하지 않아요.

autocommit 설정을 false 로 변경하여 트랜잭션 전후로 발생하는 set autocommit 쿼리를 호출하지 않도록 한 결과, API 응답시간을 1.5 ms 줄여, 응답속도를 40프로 향상시킬 수 있었어요.

© 2024. Kurly. All rights reserved.
set autocommit 설정은 어떻게 하는거죠?
// application.yml 형식을 사용한다면 아래와 같이 Hikari autocommit 설정을 해주세요.
spring:
  datasource:
    hikari:
      auto-commit: false

// application.properties 형식을 사용한다면 아래와 같이 Hikari autocommit 설정을 해주세요.
spring.datasource.hikari.auto-commit: false

만약 이 설정을 적용한다면 쿼리 종료시점에 COMMIT 이 실행되고 있는지에 대한 확인이 필요합니다.

마무리

이번 이슈의 주요 원인은 hikari.auto-commit: false 설정으로 인해 메서드 종료시점에 COMMIT 이 발생하지 않았기 때문이에요. 거기에 더불어 jpa.open-in-view 설정과 하위 메서드에서 실행되는 COMMIT 으로 인해 ROLLBACK 되어야하는 쿼리가 실행되지 않아 스냅샷 갱신이 이루어지지 않는 결과를 만들었어요.

복합적인 원인으로 인해 전달에 필요한 내용이 많아서, 준비한 자체 Q & A 세션에서 공유된 부분 외에도 궁금한 부분이 생길 수 있을 것 같아요. 이 부분은 댓글을 통해 함께 고민할 수 있는 시간을 가지면 좋을 것 같아요. 🤗

SHOUT OUT TO

함께 이 문제를 고민하고 해결해주신 도성혁님, 이민수님

set autocomit 설정을 통해 성능을 개선해주신 김광용님

References

profile
기록을 통한 성장을

5개의 댓글

comment-user-thumbnail
2024년 6월 21일

디테일한 이슈 원인과 😳 해결해 나아가는 과정이 너무 재밌게 보았습니다. (+@ 의 내용까지도!!!)
많이 배웁니다. 🙇‍♂️ (디버깅은 @Hocaron 님 처럼..)

1개의 답글
comment-user-thumbnail
2024년 7월 26일

혹시 hikari.auto-commit을 false로 설정하였기 때문에 @Transactional을 주의하여 사용하실텐데, open-in-view 설정은 true로 설정하신 이유가 있나요?? @Transactional 밖에서 open-in-view를 사용하는 로직이 있을 수 있기 때문..?

1개의 답글