자바 - 쓰레드 로컬(ThreadLocal)

SeungTaek·2021년 10월 29일
1

자바(Java)

목록 보기
7/8
post-thumbnail

본 게시물은 스스로의 공부를 위한 글입니다.
잘못된 내용이 있으면 댓글로 알려주세요!


📒 동시성 문제

  • 스프링에서 빈(bean)은 싱글톤으로 등록된다. 이 객체의 이느턴스가 애플리케이션에 딱 1개만 존재하는다는 뜻이다.
  • 이렇게 하나만 있는 인스턴스의 필드에 여러 쓰레드가 동시에 접근하면 동시성 문제가 발생 할 수 있다.

예제

  1. 🎈 스프링 프로젝트 생성 - gradle, lombok 디펜던시 추가

  2. 🎈 build.gradle 수정. 테스트에서도 lombok을 사용하기 위해서이다.

dependencies {
	...
	//테스트에서 lombok 사용
	testCompileOnly 'org.projectlombok:lombok'
	testAnnotationProcessor 'org.projectlombok:lombok'
}

  1. 🎈 Service 코드 작성
@Slf4j
public class FieldService {
	private String nameStore;
    
	public String logic(String name) {
		log.info("저장 name={} -> nameStore={}", name, nameStore);
		nameStore = name;
		sleep(1000);
		log.info("조회 nameStore={}",nameStore);
		return nameStore;
	}
    
	private void sleep(int millis) {
		try {
			Thread.sleep(millis);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
  • 매개변수를 통해 넘어온 name을 전역 변수인 nameStore에 저장 후, 1초 뒤 조회 로그를 찍어주는 서비스이다.

  1. 🎈 테스트 코드 작성
@Slf4j
public class FieldServiceTest {
	private FieldService fieldService = new FieldService();
    
	@Test
	void field() {
		log.info("main start");
		Runnable userA = () -> {
			fieldService.logic("userA");
		};
		Runnable userB = () -> {
			fieldService.logic("userB");
		};
        
		Thread threadA = new Thread(userA);
		threadA.setName("thread-A");
		Thread threadB = new Thread(userB);
		threadB.setName("thread-B");
		threadA.start(); //A실행
        
		sleep(2000); //동시성 문제 발생X
		// sleep(100); //동시성 문제 발생O
        
		threadB.start(); //B실행
		sleep(3000); //쓰레드B가 끝날 때까지 메인 쓰레드 종료 대기
		log.info("main exit");
	}
	private void sleep(int millis) {
		try {
			Thread.sleep(millis);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
  • 쓰레드 2개를 생성 후 2초 간격, 혹은 0.1초 간격으로 두 쓰레드를 실행시킨다.
  • 메인 쓰레드 종료를 대기하는 sleep(3000);이 있는 이유는 쓰레드 B가 실행을 모두 마치기 전에 메인 쓰레드가 종료하면, 쓰레드 B도 함께 강제 종료되기 때문이다.

  1. 🎈 실행 결과 확인
  • FieldServiceTest에서 중간에 sleep(2000);을 사용한 경우 동시성 문제가 없다.
[Test worker] main start
[Thread-A] 저장 name=userA -> nameStore=null
[Thread-A] 조회 nameStore=userA
[Thread-B] 저장 name=userB -> nameStore=userA
[Thread-B] 조회 nameStore=userB
[Test worker] main exit

  • FieldServiceTest에서 중간에 sleep(100);을 사용한 경우 동시성 문제가 있다.
[Test worker] main start
[Thread-A] 저장 name=userA -> nameStore=null
[Thread-B] 저장 name=userB -> nameStore=userA
[Thread-A] 조회 nameStore=userB
[Thread-B] 조회 nameStore=userB
[Test worker] main exit

  1. 🎈 왜 동시성 문제가 발생하는가?
    • 스프링은 빈을 싱글톤으로 생성한다. 즉, 인스턴스 1개를 공유해서 함께 사용한다.
    • 쓰레드A가 nameStore에 저장 후 조회를 하는데는 1초의 딜레이 시간이 있다.
    • 그 딜레이 시간에 쓰레드B가 nameStore에 접근해서 값을 바꿔버린다면?? 쓰레드A는 바뀐 값을 얻게 된다.
    • 어쩌면 문제가 발생하는게 당연하다.

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

동시성 문제는 지역 변수에서는 발생하지 않는다. 동시성 문제는 인스턴스의 필드(주로 싱글톤), 또는 static 같은 공용 필드에 접근할 때 발생한다.

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




📒 ThreadLocal

  • 쓰레드 로컬은 해당 쓰레드만 접근할 수 있는 특별한 저장소를 말한다.
  • 쓰레드 로컬을 사용하면 각 쓰레드마다 별도의 내부 저장소를 제공한다.
  • 예를 들면, thread-Athread-A 전용 보관소에서 필드 데이터를 반환해준다.
  • 따라서 같은 인스턴스의 쓰레드 로컬 필드에 접근해도 문제가 생기지 않는다.

예제 수정

  1. 🎈 서비스 수정
@Slf4j
public class ThreadLocalService {
    private ThreadLocal<String> nameStore = new ThreadLocal<>();

    public String logic(String name) {
        log.info("저장 name={} -> nameStore={}", name, nameStore.get());
        nameStore.set(name);
        sleep(1000);
        log.info("조회 nameStore={}", nameStore.get());
        return nameStore.get();
    }

    private void sleep(int millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

  1. 🎈 테스트 코드 수정
    • private FieldService fieldService = new FieldService();부분을 private ThreadLocalService service = new ThreadLocalService();로 수정

  1. 🎈 결과 확인
    • 동시성 문제가 발생하지 않는걸 확인할 수 있다.



📒 ThreadLocal 사용법

값 저장: ThreadLocal.set(xxx)
값 조회: ThreadLocal.get()
값 제거: ThreadLocal.remove()


주의

  • 쓰레드 로컬의 값을 사용 후 제거하지 않고 그냥 두면 메모리 누수가 발생할 수 있다.

  • 또한 WAS(톰캣)처럼 쓰레드 풀을 사용하는 경우에는 해당 쓰레드를 제거하지 않고 나중에 재사용하게 된다.

    • 이때, ThreadLoal에 값도 그 전 값이 그대로 저장되어있다. 따라서 다른 데이터가 반환되는 심각한 문제가 발생한다.
  • 따라서 반드시 ThreadLocal 사용 후에는 .remove()를 해주자!!


인프런의 '스프링 핵심 원리 고급편(김영한)'을 스스로 정리한 글입니다.
자세한 내용은 해당 강의를 참고해주세요.

profile
I Think So!

0개의 댓글