IT/SpringFramework

ThreadLocal과 스프링의 트랜잭션 관리

주현태 2022. 2. 20. 18:45
ThreadLocal
각 스레드마다 개별적으로 변수를 저장하는 기능을 제공한다. ThreadLocal을 사용하면 특정 스레드에서만 액세스 할 수 있는 데이터를 저장할 수 있다.
즉, 쓰레드라는 scope 내에서 공유되어 사용될 수 있는 값으로 다른 쓰레드에서 공유변수를 접근할 시 발생할 수 있는 동시성 문제의 예방을 위해 만들어졌다.
 
사용법
ThreadLocal에서 제공하는 get, set 메서드를 통해 값을 읽거나 쓸 수 있다.

 

예제
public class ContextTest
{
    @Test
    void test()
    {
        Context.threadLocal.set(10);
        System.out.println(String.format("%s - %s", Thread.currentThread(), Context.threadLocal.get()));


        new Thread(() -> {
            System.out.println(String.format("%s - %s", Thread.currentThread(), Context.threadLocal.get()));
        }).start();
    }
}

class Context
{
    public static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
}
각 스레드가 서로다른 값을 사용하도록 하면서 스레드안정성을 취할수 있지만, ThreadLocal을 스레드풀과 함께 사용할때는 주의하여야 한다.
 
ThreadLocal을 잘못 사용할 경우 아래와 같은 일이 벌어질 수 있다.
  1. 먼저 애플리케이션은 풀에서 스레드를 갖고 온다.
  2. 그런 다음 현재 스레드의 ThreadLocal에 값을 할당한다.
  3. 현재 실행이 완료되어 애플리케이션은 빌린 스레드를 풀로 반환한다.
  4. 잠시 후 동일한 스레드를 갖고 와서 다른 요청을 처리합니다.
  5. 이때 지난번에 필요한 정리를 수행하지 않았기 때문에의도치 않게 ThreadLocal 데이터를 재사용할 수 있습니다.
 
이 문제를 해결하는 한 가지 방법은 사용이 끝나면 각 ThreadLocal을 수동으로 제거하는 것이다.
즉, 다음과 같은 순서로 threadLocal을 사용하도록 한다.

 

스레드 로컬 사용순서
  1. threadLocal.set(); - 스레드 로컬 변수 설정
  2. threadLocal.get(); - 스레드 로컬변수 사용
  3. threadLocal.remove(); - 스레드 로컬 변수 삭제

 

그런데, ThreadLocal을 내가 사용하는 기술 어디서 쓰고 있을까??? 인터넷을 찾아보니, Spring의 경우 SpringSecurity에서 Authentication 정보를 저장할 때와 TransactionManager에서 Connection 정보를 저장할때 사용한다고 한다.

 

나는 이 중에서 Transaction과 관련된 사항에 대해 알아보았다. 

 

 

스프링에서 트랜잭션 사용 시 ThreadLocal 사용 사례

 

TransactionManager에서 Connection관리
  • DB에 접근하기 위한 Connection 객체가 바로 ThreadLocal에 저장된다.
 
Transaction 관련 코드
 
단일실행코드
Connection conn = Driver.getConnection("url","id","pw");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery("select * from test_table");
while(rs.next()) {
  log.info("Goods: {}",  rs.getString(“name");
}
rs.close();
stmt.close();
conn.close();

 

트랜잭션 코드
Connection conn = Driver.getConnection("url","id","pw");
conn.setAutoCommit(false);
Statement stmt1 = conn.createStatement();
ResultSet rs = stmt1.executeQuery("select * from test_table");
while(rs.next()) {
  PraparedStatement stmt2 =  conn.createPreparedStatement();
  stmt2.execute("insert into test_table(name) values (?)", rs.getString("name"));
  stmt2.close();
}
rs.close();
stmt.close();
conn.commit();
conn.close();
단일실행코드와 트랜잭션코드를 보면, 작업들을 트랜잭션으로 묶어줄때 Connection의 setAutocommit을 false로 지정함으로써, autoCommit이 안되게 함을 볼 수 있다. 즉, 트랜잭션 범위를 지정하기 위해서는 Connection 객체가 필요하다.
 
 
@Transaction 내부에서 트랜잭션이 수행되는 메커니즘
  • Connection을 얻어오고 setAutoCommit(false)를 호출한 후에
  • Bean의 메서드를 호출한 후에,
  • 정상적으로 반환되면 connection.commit()을 호출하고,
  • RuntimeException 이 발생하면 connection.rollback()을 호출합니다.

그림출처 https://wjdtn7823.tistory.com/71

이러한 이유때문에, 트랜잭션이 진행중인 하나의 스레드에서 multiThread를 통해 새로운 작업을 추가시킬때, 트랜잭션이 전파되지 않는다는 사실을 유념해두고 코딩을 진행하여야 할 것이다.