Springboot

JDBCTemplate는 동시성문제를 개발자가 직접 해결해 줘야하나?

25G 2022. 10. 21. 14:00

동시성 문제?

동시성 문제란 여러 스레드가 동시에 같은 인스턴스의 필드 값을 변경하면서 발생하는 문제를 동시성 문제라 한다. 이런 동시성 문제는 여러 쓰레드가 같은 인스턴스의 필드에 접근해야 하기 때문에 트래픽이 적은 상황에서는 확률상 잘 나타나지 않고, 트래픽이 점점 많아질 수 록 자주 발생한다.

특히 스프링 빈처럼 싱글톤 객체의 필드를 변경하며 사용할 때 이러한 동시성 문제를 조심해야 한다.

예를 들어 1명의 유저가 2개의 카드만 생성할 수 있다고 시스템에서 정의를 해놓고 테스트도 다 해봤을 때 2개 이상 절대 들어갈 일이 없을 것이라 생각했다. 그리고 서비스 첫날 서비스가 대박 나서 트래픽이 막막 몰려왔고 서버에서 스레드가 꼬여서 어떠한 유저는 카드를 2개를 초과해서 받는 일이 생길 수도 있다. 그렇기 때문에 이 동시성 문제는 운영단계에서 만은 개발자의 퇴근을 늦추게 된다.

 

Jpa는 어떻게 이를 해결했을까

Spring에서는 ThreadLocal이라는 클래스를 제공합니다. 이는 목욕탕에서 내 옷을 아무나 가져가지 못하게 옷을 개인 락카? 에 넣어놨다가 목욕(로직 수행)을 하고 다시 이 락카에서 내 옷을 찾아내는? 비유가 올바른지는 모르겠지만 쓰래드끼리 자원을 공유하지 않고 쓰래드로컬이라는 락카에 자원을 넣어놓고 해당 스래드만 이 락카의 자원을 이용할 수 있도록 하는 것입니다. 

Jpa interface의 구현채인 EntityManager는 이 ThreadLocal을 기반으로 움직이기 때문에 동시성 문제가 발생할 확률이 굉장히 낮다고 합니다. 즉 Thread-safe 한 것이죠

 

Jpa가 있는데 왜 JDBCTemplate를 사용했나?

mysql에서는 PK채번 방식을 Sequence를 지원하지 않아 Table이나 AutoIncrement에 의존해 Identyti방식으로 Pk를 생성하는 것이 일반적이고 저희 프로젝트 또한 그렇게 pk를 생성하고 있습니다. 문제는 JPA에서 bulkInsert/update를 하기 위해서는 PK채번 방식이 Sequence를 사용해야 한다는 점입니다.

identity방식을 사용하여 흔히 사용하는 saveAll()을 사용하면 list의 length만큼 insert/update query가 나가게 됩니다. 단순히 100개 내외의 그런 서비스 로직이라면 그래도 성능 감소를 어느 정도 감안하고 사용할지 모르겠지만 만약 1000건이 한 번에 저장/수정돼야 한다면? 엄청난 성능 저하를 불러올 것입니다. 

- db서버 부하 app서버 부하를 동시에 주게 된다.

- 불필요한 I/O가 발생한다. 

 

그렇다면 이를 해결방법은 다음과 같이 있을 수 있을 것 같습니다.

1. PK채번 방식을 바꾼다.

- 이는 절대 좋은 방식이 될 수 없습니다. 기존에 데이터가 쌓여있다면? 그리고 PK채번 방식에 서비스 로직이 의존돼 있다면? mysql처럼 Sequence방식을 지원하지 않는다면? 등등 고려해야 할 사항과 리스크가 너무 크기 때문입니다.

2. bulkInsert/update를 할 수 있는 방법을 찾아본다.

- 가장 합리적인 방법이 아닐까 생각합니다.

 

JDBCTemplate는 동시성 문제를 내부적으로 해결해 놨을까? 아니면 개발자가 직접 해 주어야 하는 것인가?

결론은 JDBCTemplate는 Thread-Safe 하기 때문에 빈으로 등록해서 사용할 수 있습니다.

JdbcTemplate의 query, update, execute 등 대부분의 메서드들은 살펴보면 메소드 내에서

DataSourceUtils.getConnection(ds)를 통해 Connection을 얻어오고 있으며

메서드에서 생성된 ps나 rs 그리고 connection(물론 connection은 무조건 정리하지는 않습니다.) 정리 하고 있습니다. 즉, JdbcTemplate 객체에 상태 변화 없이 메소드 내에서 모든 리소스가 정리됨

 

그렇다면 @Transaction이 있어서 객체 내에서 리소스가 정리되지 않고 영속화돼서 commit 되기 전까지 보전해야 하는 상황이라면?!

그 경우를 위해 DataSourceUtils.getConnection(ds) 코드를 따라가 보면

TransactionSynchronizationManager.getResource(ds)를 통해 ConnectionHolder를 가져오는데

TransactionSynchronizationManager.getResource(ds)를 더 들어가 보면

ThreadLocal <Map <Object, Object>>로 되어있는 resources를 가져와 ds를 키로 ConnectionHolder를 가져오는 것을 볼 수 있었습니다.

 

결론

1. JdbcTemplate 인스턴스는 상태를 갖지 않고 메서드 내에서 생성된 리소스(rs, ps, connection) 들을 정리하기 때문에 Thread-Safe 하다
2. Transaction이 필요한 경우 정리하면 안 되는 Connection의 경우 JdbcTemplate이 상태를 가지지 않고 TransactionSynchronizationManager에 ThreadLocal로 보관하기 때문에 JdbcTemplate를 Thread-Safe 하며,
Connection 객체 전달 및 전파 필요 없이 트랜잭션 제어를 하기 용이하다.

 

실제 jdbcTamplate가 ThreadLocal을 사용하는 lib코드