티스토리 뷰

동시성 문제

스프링은 기본적으로 싱글톤 빈을 등록한다.


지역변수는 쓰레드마다 각각의 다른 메모리 영역이 할당되기 때문에 동시성 문제가 발생하지 않지만, 싱글톤으로 등록된 인스턴스의 필드를 여러 쓰레드가 동시에 접근하는 경우 문제가 발생한다. 또한 동시성 문제는 값을 읽기만 해선 발생하지 않고, 어디선가 값을 변경하기 때문에 발생한다.


아래와 같은 경우는 동시성 문제가 발생하는 코드이다.


코드를 살펴보며 문제를 확인해보자.


FieldService 클래스

@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();
    }
  }

}
  • 비지니스 로직을 처리할 클래스
  • log.info("저장 name={} nameStore={}", name, nameStore);
    • logic을 호출하는 사용자의 이름과 현재 nameStore에 저장되어 있는 사용자의 이름을 출력한다.
  • nameStore = name;
    • 호출한 사용자의 이름을 nameStore에 저장한다.
  • log.info("조회 nameStore={}", nameStore);
    • 1초 후, nameStore를 다시 출력한다.

FieldServiceTest 클래스

@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(); // thread-A 비지니스 로직 실행
    sleep(2000); 
    threadB.start(); // thread-B 비지니스 로직 실행

    sleep(3000); // 메인 쓰레드 종료 대기
    log.info("main exit");
  }

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

}
  • thread-A와 thread-B는 각각 비지니스 로직을 실행한다.
  • thread-A가 logic을 실행하고, 2초 후에 thread-B가 logic을 실행한다.

결과

동시성 문제가 발생하지 않았다.



이번에는 0.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(); // thread-A 비지니스 로직 실행
    sleep(100); 
    threadB.start(); // thread-B 비지니스 로직 실행

    sleep(3000); // 메인 쓰레드 종료 대기
    log.info("main exit");
  }

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

}
  • thread-A가 logic을 실행하고, 0.1초 후에 thread-B가 logic을 실행한다.

결과

동시성 문제가 발생하였다.

FieldService.logic() 메서드 내부에는 sleep(1000)으로 1초의 지연시간이 있다. 따라서 thread-AFieldService.logic()내에서 작업이 끝나기전에 thread-B가 들어와 nameStore의 값을 바꾸었기 때문에 thread-A의 조회 결과는 userB가 되었다.




ThreadLocal 적용해보기

위와 같은 동시성 문제를 해결하기 위해서는 ThreadLocal을 이용하는 방법이 있다.


ThreadLocal은 해당 쓰레드만 접근할 수 있는 특별한 저장소를 말한다.

즉, thread-AuserA라는 값을 저장하면 ThreadLocal은 thread-A 전용 보관소에 데이터를 저장하고, thread-BuserB라는 값을 저장하면 ThreadLocal은 thread-B 전용 보관소에 데이터를 저장한다.

조회할 때도 thread-A가 조회하면 userA 데이터를 반환해주고, thread-B가 조회하면 userB 데이터를 반환해준다.


이번에는 ThreadLocal을 사용하여 동시성 문제를 해결해보자.


ThreadLocalService 클래스

@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();
    }
  }

}
  • 비지니스 로직을 처리할 클래스
  • FieldService와는 다르게 ThreadLocal를 사용하여 nameStore를 선언하였다.

ThreadLocalServiceTest 클래스

@Slf4j
public class ThreadLocalServiceTest {

  private ThreadLocalService service = new ThreadLocalService();

  @Test
  void field() {
    log.info("main start");
    Runnable userA = () -> service.logic("userA");
    Runnable userB = () -> service.logic("userB");

    Thread threadA = new Thread(userA);
    threadA.setName("thread-A");

    Thread threadB = new Thread(userB);
    threadB.setName("thread-B");

    threadA.start(); // thread-A 비지니스 로직 실행
    sleep(100); 
    threadB.start(); // thread-B 비지니스 로직 실행

    sleep(3000); // 메인 쓰레드 종료 대기
    log.info("main exit");
  }

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

}
  • thread-A가 logic을 실행하고, 0.1초 후에 thread-B가 logic을 실행한다.

결과

쓰레드 로컬 덕분에 쓰레드 마다 각각 별도의 데이터 저장소를 가지게 되었다. 결과적으로 동시성 문제도 해결되었다.



Referemce

김영한. 스프링 핵심 원리 - 고급편. 인프런.
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B3%A0%EA%B8%89%ED%8E%B8/dashboard

'Spring' 카테고리의 다른 글

[Spring] Advice 만들어보기  (0) 2022.05.17
[Spring] 리플렉션(Reflection)  (0) 2022.05.15
[Spring] 트랜잭션 동기화  (0) 2022.04.26
[Spring] 트랜잭션 AICD  (0) 2022.04.20
[Spring] DataSource  (0) 2022.04.19
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함