글 작성자: Key Ryung

개요

Scale-Out을 진행하기 전 추가적으로 진행하는 성능 개선 포인트로 Connection의 점유 시간을 단축하는 방향을 적용해 보았습니다. Connection을 갖고 오거나 반환하는 부분을 Code를 통해서 제어하는 방향보다는 Spring, Hibernate, HikariCP의 설정을 조정하여 비효율적으로 동작하는 부분을 개선하는 방향으로 진행했습니다.

 

비즈니스 로직에 집중하여 개발할 수 있도록 Spring 프레임워크의 많은 부분이 추상화되어 있습니다. 이런 Spring 프레임워크의 철학을 적용하여 사용되는 라이브러리도 추상화가 잘 되어 있습니다. 이번 개선 과정에서는 무엇보다 Spring의 추상화된 부분부터 구체화된 부분으로 동작을 따라가는 과정이 필요했습니다. 특히 Spring과 Database 간의 연결 사이에 있는 HikariCP와 JPA 모두 들여다볼 수 있는 계기가 되어 인상적인 개선 과정이었습니다.

 

처음 Connection의 점유 시간을 단축 시키는 부분에 대한 힌트를 얻은 것은 'Spring의 Service 레이어에서 항상 영속성 컨텍스트를 사용할 수 있을까?'라는 질문을 던지며 찾아본 결과였습니다. 이 질문에서 시작돼서 Spring의 Transaction은 어떻게 수행될까, Transaction이 실제로 어떻게 수행될까라는 질문까지 이어지면서 Transaction과 Connection의 동작까지 살펴보았습니다.

 

개선 과정에서는 Top-Down으로 조사했지만 설명에서는 Bottom-Up으로 개선 과정을 묘사하겠습니다.

 

언제 Connection이 필요할까?

Connection은 쿼리가 수행될 때

먼저 Connection 시간을 단축시키려면 Connection이 언제 획득되고 반환되는지 알아야 합니다. Spring, JPA, HikariCP, MySQL을 쓰는 YOUSINSA에서는 언제 Connection이 필요할지 생각해보면 당연히 Database와의 통신이 필요할 경우 Connection이 필요합니다. 그렇다면 언제 Database와의 통신이 필요할지도 명확합니다. 쿼리가 수행될 때!

 

쿼리가 수행되는 것을 Code 레벨로 연결시키는 과정이 추상화에서 구체화로 나아가는 첫걸음이었습니다. 작성한 로직에서는 Connection을 하는 과정이 포함되어 있지 않기 때문에 어디에 있는지 찾아야 합니다. 처음 저의 가정은 쿼리가 수행될 때 필요하므로 Repository Layer의 코드가 실행될 때 Connection이 획득되고 쿼리 결과를 반환받으면 Connection이 반납될 것이라 생각했습니다.

 

Database Connection Pool을 사용하므로 획득/반환이라는 표현을 사용했습니다.

 

Transaction과 Connection

Transaction으로 수행되는 쿼리는 Connection을 어느 시점에 갖고 오는가도 알아야 합니다. 확실하게 기억해두고 갈 것은 Transaction은 Web, Application, Database 크게 3가지 영역 중 어디에서 동작하는가입니다. Database에서 지원하는 것이 Transaction이므로 Database에서만 동작한다고 생각할 수 있습니다. 하지만 Spring은 Transaction을 Application과 Database에 걸쳐 적용하고 동기화시켜 Application의 Transaction과 Database의 Transaction이 서로의 상태를 알도록 설계되었습니다.

 

먼저 Database에 Transaction이 동작하는 부분을 알아보겠습니다. 다음과 같이 AutoCommit을 False로 두고 쿼리들을 수행한 뒤 Commit을 수행하고 AutoCommit을 다시 True로 설정해줌으로써 Transaction을 수행할 수 있습니다.

setAutoCommit(false);
쿼리1 수행
쿼리2 수행
setAutoCommit(true);


Spring은 앞서 언급했듯이 OOP를 충실하게 반영하여 확장에 유연하여 조립과 분리가 쉬운 프레임워크입니다. 그러므로 당연히 어딘가에 이런 쿼리를 보낼 수 있는 영역을 갖고 있을 것입니다. 이 부분에서 눈여겨볼 점은 이전과 다르게 AutoCommit의 설정 부분입니다. 그렇다면 AutoCommit을 설정하는 것은 쿼리일까요? '네!'

 

Spring Transaction?

Spring에서는 Transaction을 @Transactional 어노테이션을 통해 수행할 수 있도록 추상화되어 있습니다. 다음은 YOUSINSA 회원가입 기능에 사용되는 비즈니스 로직입니다. 회원 가입하는 User의 정보가 올바르지 않거나 비밀번호를 Hashing 하는 과정에서 오류가 발생한다면 해당 Transaction은 실패 처리됩니다.

 

@Service
@Slf4j
public class UserSignUpServiceImpl implements UserSignUpService {

	private final UserRepository userRepository;
	private final SignUpDtoConverter signUpDtoConverter;
	private final PasswordEncoder passwordEncoder;

	// ... Constructor

    @Transactional // (1)
	@Override
	public SignUpResponseDto trySignUpUser(SignUpRequestDto signUpRequest) {
		Assert.notNull(signUpRequest, "SignUpRequest must be not null"); // (2)
		validateSignUpUser(signUpRequest);

		String hashedPassword = passwordEncoder.hashPassword(signUpRequest.getUserPassword());

		UserEntity newUser = signUpDtoConverter.convertSignUpRequestToUser(signUpRequest, hashedPassword);

		UserEntity savedUser = userRepository.save(newUser); // (3)

		return signUpDtoConverter.convertUserToSignUpResponse(savedUser); // (4)
	}
}

 

그렇다면 위의 코드에서 어느 시점에 Connection을 갖고 올까요?

 

실제 Connection이 필요한 시점은 쿼리가 실행되는 시점이니 (3) 번일까요? 혹은 AOP로 실행되는 @Transactional의 동작을 생각해보았을 때 (2) 번 전에 실행될까요? 위에서 살펴보았던 Database의 Transaction 동작을 생각해보면 AutoCommit의 설정이 수행되는 시점에서 Connection이 필요할 것이니 설정 시점을 찾아야 합니다.

Connection을 얻어오는 과정을 Bean과 함께 나타낸 Diagram

다이어그램을 보면 `LogicalConnectionManagedImpl` Bean이 Connection과 연관 있는 것을 확인할 수 있습니다. LogicalConnectionManagedImpl에서 AutoCommit과 관련된 2가지의 동작이 수행됩니다.

  • getAutoCommit
  • setAutoCommit
// LogicalConnectionManagedImpl.java
@Override
public void begin() {
    initiallyAutoCommit = !doConnectionsFromProviderHaveAutoCommitDisabled() 
			&& determineInitialAutoCommitMode(getConnectionForTransactionManagement() );
    super.begin(); // AbstractLogicalConnectionImplementator.java
}

doConnectionsFromProviderHaveAutoCommitDisabled()는 설정 값을 읽어와 판단하는 부분이고 Short-Circuit을 사용하는 것을 볼 수 있습니다. 처음 메서드의 호출에서 False가 반환되었다면 뒤이어 determineInitialAutoCommitMode 메서드가 실행될 것입니다. 그리고 determineInitialAutoCommitMode에서는 Connection의 AutoCommit 설정을 읽습니다. 따라서 이 부분부터 Connection이 필요하므로 Connection이 사용된다는 것을 알 수 있습니다.

// AbstractLogicalConnectionImplementator.java
protected static boolean determineInitialAutoCommitMode(Connection providedConnection) {
    try {
        return providedConnection.getAutoCommit();
    }
    catch (SQLException e) {
        log.debug( "Unable to ascertain initial auto-commit state of provided connection; assuming auto-commit" );
        return true;
    }
}

// AbtractLogicalConnectionImplementator.java
@Override
public void begin() {
	try {
		if (!doConnectionsFromProviderHaveAutoCommitDisabled()) {
			log.trace( "Preparing to begin transaction via JDBC Connection.setAutoCommit(false)" );
			getConnectionForTransactionManagement().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 );
	}
}

여기서 드는 의문점이 있습니다. AutoCommit 설정을 갑자기 왜 읽을까요? Transaction이 시작할 때 AutoCommit 설정을 동적으로 connection의 설정을 읽는다는 것인데 기본 설정에서는 항상 읽도록 설정되어 있습니다.

 

풀어 설명하면 기본적으로 Hibernate는 Database Connection Pool의 AutoCommit 설정을 신뢰하지 않습니다. 따라서 DBCP의 AutoCommit 설정을 읽고 난 뒤에 적용합니다. 이런 동작 때문에 실제 쿼리가 수행되는 시점이 아닌 AutoCommit의 설정을 확인하는 시점에 Connection이 획득됩니다.

 

따라서 앞서 말했던 Hibernate가 Database Connection Pool의 AutoCommit 설정을 신뢰하도록 설정한다면 실제 쿼리가 수행되는 시점에 Connection을 가지고 오도록 하여 Connection을 획득을 지연시켜 점유 시간을 줄일 수 있을 것이라 예상했습니다. 추가적으로 setAutoCommit과 getAutoCommit의 호출을 줄일 수 있기 때문에 불필요한 메서드 호출을 줄인다는 점에서도 성능 상 이득일 것이라 예상했습니다.

// application.yml
datasource:
    ...
    hikari:
      auto-commit: false

jpa:
    ...
    properties:
      hibernate:
        ...
        connection:
          provider_disables_autocommit: true

HikariCP를 사용하고 있기 때문에 HikariCP 설정에서 Auto-Commit을 False로 설정하고 Hibernate의 설정에서 HikariCP의 AutoCommit 설정을 확인하지 않겠다는 설정을 True로 두는 설정을 적용했습니다.

 

Conclusion

이번 포스팅에서는 AutoCommit 설정을 통해 성능 개선 포인트를 적용했습니다. Connection 점유 시간 최적화와 관련하여 한 가지의 설정을 추가적으로 적용한 뒤에 실험을 진행했기 때문에 다음 포스팅에서 함께 적용한 결과를 담을 예정입니다.