반응형

이번 시간에는 Spring Framework에서 동시성 문제를 해결해 줄 ThreadLocal에 대해서 알아보겠습니다.

스프링은 빈을 싱글톤으로 등록한다. 애플리케이션에 딱 1개만 존재한다는 뜻이다.

이렇게 하나만 있는 인스턴스에 하나의 필드에 여러 쓰레드가 동시에 접근하여 수정하게 되면 A 사용자가 B 사용자의 데이터를 보게 되는 사고가 발생될 수 있다. 아래 목차와 같이 순서대로 동시성 원인부터 해결 방안까지 알아보겠습니다. 


<목차>

1. 동시성 문제 발생하는 예시

2. ThreadLocal을 이용해서 동시성 문제 해결 방안

3. ThreadLocal의 주의사항

 


1. 동시성 문제 발생하는 예시

FieldService.java

  1. nameStore을 맴버변수로 선언

  2. nameStore 변수에 값을 저장하고 1초 뒤에 저장한 nameStore 출력

 

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

}

 

FieldServiceTest.java

  1. threadA.setName("thread-A");   -- Thread 이름 설정

  2. Thread A 시작하고 2초 잠시 멈추기 때문에 동시성 발생 안함.

  3. Thread B 시작하고 sleep(2000) 안하면 쓰레드 끝나기 전에 테스트 코드가 종료되서 모든 로그가 출력이 안되서 추가해야 한다.

 

@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"); // 1. thread 이름 설정
        Thread threadB = new Thread(userB);
        threadB.setName("thread-B");

        threadA.start();
        sleep(2000); // 2. 동시성 문제 발생x
        threadB.start();

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

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

}

 

TestCode 결과

  1. thead-A, thread-B 위에서 설정한 이름

  2. 정상적으로 thread-A에 userA thread-B에 userB 출력

 

 

동시성 문제 발생

FieldServiceTest.java

  1. ThreadA가 시작하고 0.1초만에 ThreadB 접근

 

@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();
        sleep(100); // 1. 동시성 문제 발생
        threadB.start();

        sleep(2000);
        log.info("main exit");
    }

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

}

 

TestCode 결과

  1. thread-A에 조회할 때 그 전에 thread-B가 nameStore을 수정해서 동시성 문제 발생 

 

 

 


2. ThreadLocal을 이용해서 동시성 문제 해결 방안

ThreadLocal은 해당 쓰레드만 접근할 수 있는 별도의 특별한 내부 저장소이다.

 

ThreadLocalService.java

  1. String nameStore -> ThreadLocal<String> nameStore 변경

  2. nameStore.set() - 값 저장

      nameStore.get() - 값 조회

      nameStore.remove() - 값 제거

 

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

}

 

ThreadLocalServiceTest.java

  1. FieldService만 ThreadLocalService로 수정

 

@Slf4j
public class ThreadLocalServiceTest {

    private ThreadLocalService threadLocal = new ThreadLocalService();

    @Test
    void field() {
        log.info("main start");

        Runnable userA = () -> {
            threadLocal.logic("userA");
        };
        Runnable userB = () -> {
            threadLocal.logic("userB");
        };

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

        threadA.start();
        sleep(100); // 동시성 문제 발생x
        threadB.start();

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

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

}

 

TestCode 결과

  1. ThreadLocal이라는 별도의 쓰레드 저장소를 사용함으로써 동시성 문제를 해결하였습니다.

 

 


3. ThreadLocal의 주의사항

ThreadLocal 값을 사용 후 제거하지 않고 그냥 두면 WAS에서 쓰레드 풀을 사용하는 경우 심각한 문제가 발생할 수 있다.

보통 was는 쓰레드 생성 비용이 비싸기 때문에 쓰레드를 사용하면 반납하고 재사용하게 된다. 

그러므로 thead-A를 사용하고 쓰레드 값을 제거하지 않을 상태에서 반납 후 다른 사용자 HTTP 요청이 thead-A에 할당되면 thead-A 보관소에 그대로 값이 있어서 다른 사용자에게 잘못된 정보가 노출될 수 있다.

이런 문제를 예방하기 위해서는 사용자 요청이 끝날 때 쓰레드 로컬의 값을 ThreadLocal.remove()를 통해서 꼭 제거해야 한다.

* Spring Framework 같은 경우 Filter나 Interceptor를 이용해서 깔끔하게 제거해 주는 것이 안전하다.

 

 

 

참조

  - inflearn 강의(스프링 핵심 원리 - 고급편, 김영한)

728x90
반응형
반응형

동시성을 구현할 때 명실할 것들

 

목차

1. 동시성 프로그래밍이란

2. 동시성 프로그래밍이 필요한 이유

3. 안전한 동시성 프로그래밍 규칙

4. 동시성 테스트 방법


1. 동시성 프로그래밍이란

어플리케이션을 효율적으로 실행하기 위해 멀티코어를 온전히 활용하도록 구현하는 방식

 

클라이언트가 아닌 어플리케이션 관점에서 봐야한다.

  - 내 어플리케이션의 효율성을 높여야 한다.

  - 어플리케이션이 동작하는 머신의 환경이 효율적으로 돌아가도록 어플리케이션에 메모리 누수나 자원이 낭비되지 않도록 신경쓴다

 

 

2. 동시성 프로그래밍이 필요한 이유

동시성 프로그래밍의 미신과 오해

 

동시성은 항상 성능을 높여준다.(x)

동시성은 때로 성능을 높여준다.(o)

 

동시성을 구현해도 설계는 변하지 않는다.(x)

동시성을 구현하면 설계를 바꿔야 한다.(o)

 

Web나 EJB와 같은 컨테이너를 사용하면 동시성을 이해할 필요가 없다.(x)

컨테이너를 사용해도 동시성을 이해해야 한다.(o)

 

 

3. 안전한 동시성 프로그래밍 규칙

단일 책임 원칙(SRP) 설계

 * 동시성 관련 코드는 다른 코드와 분리하라

  - 동시성 코드는 독자적인 개발, 변경, 조율 주기가 있다.

  - 동시성 코드에는 독자적인 난관이 있다. 다른 코드에서 겪는 난관과 다르며 훨씬 어렵다.

  - 잘못 구현한 동시성 코드는 별의별 방식으로 실패한다.

  - 주변에 있는 다른 코드가 발목을 잡지 않더라도 동시성 하나만으로도 충분히 어렵다.

 

자료 범위를 제한하라

 * 공유 자료를 최대한 줄여라

  - 동시 수정 문제를 피하기 위해 객체를 사용하는 코드 내 임계영역을 synchronized 키워드로 보호하라

  - 보호할 임계영역을 빼먹거나, 모든 임계영역을 보호했는지 확인하느라 수고가 드므로 임계 영역의 수를 최소화 해야 한다.

 

자료 사본을 사용하라

 * 공유 자료를 줄이려면, 최대한 공유하지 않는 방법이 제일 좋다.

  - 객체를 복사해 읽기 전용으로 사용한다.

  - 각 스레드가 객체를 복사해 사용한 후 한 스레드가 해당 사본으로 결과를 가져온다.

  - 사본을 사용하는 방식으로 내부 잠금을 없애 수행 시간을 절약하는 것이 사본 생성과 가비지 컬렉션에 드는 부하를 상쇄할 가능성이 크다.

 

Thread는 가능한 독립적으로 구현하라

 * 다른 스레드와 자료를 공유하지 않는다.

  - 서블릿처럼 각 Thread는 클라이언트 요청 하나를 처리한다.

  - 모든 정보는 비공유 출처(client의 request)에서 가져오며 로컬 변수에 저장한다.

  - 각 서블릿은 마치 자신이 독자적인 시스템에서 동작하는 양 요청을 처리한다.

 

라이브러리를 이해하라

 * java.util.concurrent 패키지를 익혀라

  - Thread Safe한 컬렉션을 사용한다. (ConcurrentHashMap, AtomicLong)

  - 서로 무관한 작업을 수행할 때는 executor 프레임워크르르 사용한다.

  - 가능하다면 Thread가 Blocking되지 않는 방법을 사용한다.

 

동기화 하는 메서드 사이에 존재하는 의존성을 이해하라

 * 공유 객체 하나에는 메서드 하나만 사용하라

  - 클라이언트에서 잠금 - 클라이언트에서 첫 번째 메서드를 호출하기 전에 서버를 잠근다.

    마지막 메서드를 호출할 때까지 잠금을 유지한다.

  - 서버에서 잠금 - 서버에다 "서버를 잠그고 모든 메서드를 호출한 후 잠금을 해제하는" 메서드를 구현한다.

     클라이언트는 이 메서드를 호출하기만 하면 된다.

  - 연결(Adapter)서버 - 잠금을 수행하는 중간 단계를 생성한다.

     '서버에서 잠금'방식과 유사하지만 원래 서버는 변경하지 않는다.

 

4. 동시성 테스트 방법

동시성 코드를 테스트 해야 한다.

 * 테스트를 했다고 동시성 코드가 100% 올바르다고 증명하기는 불가능하다. 하지만 충분한 테스트는 위험을 낮춘다.

  - 문제를 노출하는 테스트 케이스를 작성하라

  - 프로그램의 설정과 시스템 설정과 부하를 바꿔가며 자주 돌려라

  - 테스트가 실패하면 원인을 추적하라

  - 다시 돌렸더니 통과한다는이유로 그냥 넘어가면 절대로 안된다.

 

코드에 보조 코드를 넣어 돌려라

 * 드물게 발생하는 오류를 자주 발생시키도록 보조 코드를 추가한다.

  - 코드에 wait(), sleep(), yield(), priority() 함수를 추가해 직접 구현한다.

  - 보조코드를 넣어주는 조구를 사용해 테스트한다.

     다양한 위치에 ThreadJigglePoint.jiggle()를 추가해 무작위로 sleep(), yield()가 호출되도록 한다.

  - 테스트 환경에서 보조 코드를 돌려본다.

 

동시성 코드를 실제 환경이나 테스트 환경에서 돌려본다

 * 다양한 요청과 상황에 동시성 코드가 정상적으로 동작하는지 확인한다.

  - 배포하기 전에 테스트 환경에서 충분히 오랜시간 검증한다.

  - 동시성 코드를 배포한 후에 모니터링을 통해 문제가 발생하는지 지켜본다.

728x90
반응형

'Book > Clean Code' 카테고리의 다른 글

[Clean Code] Chapter 14  (0) 2022.04.02
[Clean Code] Chapter 12  (0) 2022.03.28
[Clean Code] Chapter 11  (0) 2022.03.27
[Clean Code] Chapter 10  (0) 2022.03.26
[Clean Code] Chapter 09  (0) 2022.03.21

+ Recent posts