코드를 구현한 후 로그를 확인하기 위해 새로고침을 했다. 실수로 연속 두 번을 눌러버렸다.
그랬더니 예상치 못한 결과가 출력되었다.
예상한 결과와 달리 순서가 뒤죽박죽 섞인 로그가 출력되었다.
심지어 다른 스레드인데 트랜잭션 ID가 일치한다. 대체 무슨 일이 생긴 걸까?
동시성 문제
동시성 문제가 일어나는 과정은 다음과 같다.
1. 스레드 A가 서비스 객체에 접근하여 데이터를 저장하고 조회하려고 한다.
2. 스레드 A가 서비스 객체에 접근하여 데이터를 저장했다.
3. 스레드 A가 저장한 데이터를 조회하기 전에 스레드 B가 서비스 객체에 접근하여 데이터를 저장했다.
4. 스레드 A는 본인이 저장한 데이터가 아닌, 스레드 B가 저장한 데이터를 조회한다.
5. 스레드 B는 본인이 저장한 데이터를 조회한다.
🙋🏻♀️ 이런 동시성 문제는 왜 발생하나요?
A. 여러 스레드가 같은 인스턴스의 필드에 접근하기 때문이다.
사실 트래픽이 적거나 데이터 읽기가 들어오면 동시성 문제가 발생하지 않는다(트래픽이 적은 경우에도 가끔은 발생함). 하지만 사용자가 증가하여 트래픽이 많아지거나 데이터 변경이 일어나면 동시성 문제가 빈번하게 발생한다. 스프링 같은 경우 빈을 싱글톤 객체로 관리하기 때문에 동시성 문제를 특히 조심해야 한다.
🙋🏻♀️ 싱글톤 객체를 사용하면 동시성 문제를 해결할 수 없나요?
A. 스레드 로컬을 사용하면 된다.
✅ 스레드 로컬이란?
각 스레드마다 별도의 내부 저장소를 제공한다. 따라서 같은 인스턴스의 스레드 로컬 필드에 접근해도 문제없다. 위에 작성한 예시 상황을 스레드 로컬로 해결하면 다음과 같다.
1. 스레드 A가 데이터를 저장하려고 하면 스레드 로컬은 스레드 A 전용 저장소에 데이터를 보관한다.
2. 바로 스레드 B가 데이터를 저장하려고 하면 스레드 로컬은 스레드 B 전용 저장소에 데이터를 보관한다.
3. 스레드 A가 데이터를 읽으려고 하면 스레드 A 전용 저장소에서 데이터를 반환한다.
4. 스레드 B가 데이터를 읽으려고 하면 스레드 B 전용 저장소에서 데이터를 반환한다.
스레드 로컬은 요청이 동시다발적으로 들어와도 동시성 문제 없이 처리한다.
💻 코드에 스레드 로컬 적용하기
동시성 문제가 발생하는 코드 (String 필드)
private String nameStore;
public String logic(String name) {
log.info("저장 name={} --> nameStore={}", name, nameStore);
nameStore = name;
sleep(1000);
log.info("조회 nameStore={}", nameStore);
return nameStore;
}
만약 요청이 1밀리 초 사이에 동시다발적으로 들어올 경우 동시성 문제가 발생하는 코드이다. 이 코드를 어떻게 바꿔야 스레드 로컬을 적용하면서 동시성 문제를 해결할 수 있을까?
딱 3개만 변경하면 된다.
- nameStore의 타입 ➡️ ThreadLocal<String>
- nameStore setter ➡️ nameStore.set(name)
- nameStore getter ➡️ nameStore.get()
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();
}
🙋🏻♀️ userA가 먼저 값을 저장했는데, 왜 userB를 저장할 때 nameStore의 값이 null인가요?
A. 각자 다른 저장소를 호출하여 저장하기 때문이다. 스레드 B 입장에서는 보관소에 저장한 것이 없기 때문에 null로 출력된다.
위처럼 ThreadLocal 클래스를 사용하면 동시성 문제를 해결할 수 있다. 기존 코드와 달리 setter, getter 메서드를 사용한다. 그리고 반드시 스레드 로컬을 사용하고 나면 ThreadLocal.remove()를 호출하여 저장된 값을 제거해 주어야 한다.
결과적으로 스레드마다 고유한 트랜잭션 ID를 가지며 동시성 문제가 해결된 것을 알 수 있다.
'Dev > Java' 카테고리의 다른 글
[Java] Random 클래스를 사용하면 안 되는 이유 (0) | 2024.05.17 |
---|---|
[Java] 값 객체(Value Object)란 도대체 뭘까? (0) | 2024.05.12 |
[Java] Web server failed to start. Port 5000 was already in use. (MacOS) (1) | 2024.01.24 |
[Java] Checked Exception vs. UnChecked Exception (1) | 2023.09.12 |
[Java] 얕은 복사 vs. 깊은 복사 (0) | 2023.09.10 |