[스프링 핵심원리 - 고급편 ] 필드 동기화 - 동시성 문제

JEONG SUJIN·2023년 2월 12일
0

스프링부트 기본

목록 보기
13/15

필드 동기화 - 동시성 문제

잘 만든 로그추적기를 실제 서비스에 배포했다 가정.
테스트할 때는 문제가 없는 것 처럼 보인다. 사실 직전에 만든 FieldLogTrace 는 심각한 동시성 문제를 가지고 있다.
동시성 문제를 확인하려면 다음과 같이 동시에 여러번 호출해보면 된다.

동시성 문제확인

다음 로직을 1초안에 2번 실행하면
traceId를 동일한데, 쓰레드가번호가 다른 문제가 발생한다.

  • 동시성 문제
    FieldLogTrace는 싱글톤으로 등록된 스프링 빈이다.
    이 객체의 인스턴스가 애플리케이션에 딱 1개 존재한다는 뜻.
    이렇게 하나만 있는 인스턴스의 FieldLogTrace.traceHolder 필드를 여러 쓰레드가 동시에 접근하기 때문에 문제가 발생한다.

실무에서 한번 나타나면 개발자를 가장 괴롭히는 문제도 바로 이러한 동시성 문제.

동시성 문제 - 예제코드

동시성 문제가 어떻게 발생하는지 단순화해서 알아보자

테스트에서도 lombok을 사용하기 위해 다음 코드를 추가

build.gradle

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	// 테스트에서 lombok사용
	testCompileOnly 'org.projectlombok:lombok'
	testAnnotationProcessor 'org.projectlombok:lombok'
}

FieldService.java

package study.advanced.trace.threadlocal.code;

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class FieldService {
	
	private String nameStore; //여기에 저장할 변수
	
	public String logic(String name) {
		log.info("저장 name={} -> nameStore={}", name, nameStore);
		nameStore = name;
		sleep(1000); //1초 쉬고
		log.info("조회 nameStore={}", nameStore);
		return nameStore;
	}

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

FieldServiceTest.java

package study.advanced.trace.threadlocal;

import org.junit.jupiter.api.Test;

import lombok.extern.slf4j.Slf4j;
import study.advanced.trace.threadlocal.code.FieldService;

@Slf4j
public class FieldServiceTest {
	
	private FieldService fieldService  = new FieldService();
	
	@Test
	void field() {
		log.info("main start");
		
		//쓰레드1 
		Runnable userA = () -> {
			fieldService.logic("userA");
		};
		
		//쓰레드2
		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); //2초동안 쉼, 동시성 문제 발생X
		
		threadB.start(); //쓰레드 B시작
		sleep(2000); //메인 쓰레드 종료대기
		log.info("main exit");
		
		
	}

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

쓰레드A가 저장하고, 쓰레드A 조회하고, 쓰레드B가 저장하고, 쓰레드B조회하고 main exit 된다.

sleep(2000)을 설정해서 thread-A의 실행이 끝나고 나서 thread-B가 실행.
참고로 FieldService.logic() 메서드는 내부에 sleep(1000)으로 1초의 지연이 있다.
1초 이후에 호출하면 순서대로 실행 할 수 있다. 여기서는 넉넉하게 2초(2000)를 설정.


여기서 만약 sleep(2000)이 아니라 sleep(100)으로 준다면 동시성문제가 발생한다.

//sleep(2000); //2초동안 쉼, 동시성 문제 발생X
		sleep(100); //동시성 문제발생

실행결과를 보면 저장하는 부분에서는 문제가 없다. 문제는 조회하는 부분에서 발생한다.

  1. 먼저 thread-AuserA값을 nameStore에 보관한다.
  2. 0.1초 후에 thread-Buser-B의 값을 nameStore에 보관한다. 기존에 nameStore에 보관되어있던 userA값은 제거되고 userB값이 저장된다.
  3. threadA의 호출이 끝나면서 nameStore의 결과를 반환받는데, 이때 nameStore는 앞의 2번에서 userB의 값으로 대체되었다. 생각했던 userA의 값이 아니라 userB의 값이 반환된다.

📍정리하면 Thread-A ThreadB userA userB nameStore

    1. Thread-A 는 userA 를 nameStore 에 저장했다.
    1. ThreadB 는 userB 를 nameStore에 저장했다.
    1. Thread-A 는 userB 를 nameStore 에서 조회했다.
    1. ThreadB 는 userB 를 nameStore 에서 조회했다.

동시성 문제

결과적으로 Thread-A입장에서는 저장한 데이터와 조회한 데이터가 다른 문제가 발생한다.
이처럼 여러 쓰레드가 동시에 같은 인스턴스의 필드 값을 변경하면서 발생하는 문제를 동시성 문제라 한다.
이런 동시성 문제는 여러 쓰레드가 같은 인스턴스의 필드에 접근해야 하기 때문에 트래픽이 적은 상황에서는 확률상 잘 나타타지 않고, 트래픽이 점점 많아질 수록 자주 발생한다.
특히 스프링 빈 처럼 싱글톤 객체의 필드를 변경하며 사용할 때 이러한 동시성 문제를 조심해야한다.

❗️참고

이런 동시성 문제는 지역변수에서는 발생하지 않는다. 지역변수는 쓰레드마다 각각 다른 메모리 영역이 할당된다.
동시성 문제가 발생하는 곳은 같은 인스턴스의 필드(주로 싱글톤에서 자주 발생), 또는 static같은 공용 필드에 접근할 때 발생한다.
동시성 문제는 값을 읽기만 하면 발생하지 않는다. 어디선가 값을 변경하기 때문에 발생한다.

싱글톤 객체의 필드를 사용하면서 동시성 문제를 해결하려면 어떻게 해야할까? 이럴 때 사용하는 것이 바로 쓰레드 로컬 !

profile
기록하기

0개의 댓글