본문 바로가기

개발중/Spring

[스프링] 동시성에 대해 공부 좀 했습니다.

728x90
반응형

🌱동시성

동시성 문제는 멀티 쓰레드 환경에서 발생하는 문제다.

 

그림처럼 여러 쓰레드가 동시에 동일한 자원에

접근해서 수정을 하는 경우 발생한다.

 

왜냐하면 동시에 값을 수정했을 때,

각 쓰레드가 기대하던 값과는

다른 형태의 값이 들어올 가능성이 있기 때문이다.

 

멀티 쓰레드 환경이라고 하더라도 싱글톤 객체에 단순히 '조회'만 하는 것이라면 동시성 문제가 발생하지 않는다. 

문제는 동시에 동일한 자원에 접근해서 수정을 했을 때 발생한다. 


이런 동시성 문제는 지역 변수에서 발생하지 않는다. 

지역 변수는 쓰레드마다 각각 다른 메모리 영역이 할당되기 때문이다.

 

동시성 문제가 발생하는 곳은 같은 인스턴스의 필드 또는 static 같은 공용 필드에 접근할 때 발생한다.

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


동시성 문제는 결국 같은 공간에 여러 쓰레드가 동시에 수정을 하면서 일어나는 일이기 때문에 

수정을 하는 공간을 분리해주면 된다. 


🌱동시성 예제

 

FieldService 는 nameStore 를 전역적으로 사용한다.

 

FieldService.logic

  • name 을 파라메터로 전달받아 저장하겠다고 log 를 nameSpace 와 함께 로그를 찍는다.
  • name 을 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;
    }
}

 

 

🌱동시성 문제 발생 없음 ( 테스트 코드를 작성해보기 1 ) 

@SpringBootTest
class FieldServiceTest {

    private FieldService FieldService = new FieldService();

    @Test
    void field(){

        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(2000);
        
        threadB.start();
        sleep(3000);

    } 
}

 

결과

2022-11-01 20:10:03.544  INFO 18604 --- [       thread A] hello.advanced.threadlocal.FieldService  : 저장 name=userA >> nameStore=null
2022-11-01 20:10:04.568  INFO 18604 --- [       thread A] hello.advanced.threadlocal.FieldService  : 조회 nameStore=userA
2022-11-01 20:10:05.553  INFO 18604 --- [       thread B] hello.advanced.threadlocal.FieldService  : 저장 name=userB >> nameStore=userA
2022-11-01 20:10:06.559  INFO 18604 --- [       thread B] hello.advanced.threadlocal.FieldService  : 조회 nameStore=userB

 

🌱동시성 문제 발생 있음 ( 테스트 코드를 작성해보기 2 ) 

@SpringBootTest
class FieldServiceTest {

    private FieldService FieldService = new FieldService();

    @Test
    void field(){

        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(500);
        
        threadB.start();
        sleep(3000);

    } 
}

 

결과

2022-11-01 20:12:29.818  INFO 4612 --- [       thread A] hello.advanced.threadlocal.FieldService  : 저장 name=userA >> nameStore=null
2022-11-01 20:12:30.330  INFO 4612 --- [       thread B] hello.advanced.threadlocal.FieldService  : 저장 name=userB >> nameStore=userA
2022-11-01 20:12:30.831  INFO 4612 --- [       thread A] hello.advanced.threadlocal.FieldService  : 조회 nameStore=userB
2022-11-01 20:12:31.340  INFO 4612 --- [       thread B] hello.advanced.threadlocal.FieldService  : 조회 nameStore=userB

🌱 ThreadLocal

ThreadLocal은 JDK 1.2부터 제공된 오래된 클래스다. 

 

java.lang.ThreadLocal

 

이 클래스를 활용하면 스레드 단위로 로컬 변수를 사용할 수 있기 때문에 

마치 전역변수처럼 여러 메서드에서 활용할 수 있다. 

다만 잘못 사용하는 경우 큰 부작용(side-effect)이 발생할 수 있기 때문에 

다른 스레드와 변수가 공유되지 않도록 주의해야 한다.

 

쓰레드 로컬을 사용하면 각 쓰레드마다 별도의 내부 저장소를 제공한다.

따라서 같은 인스턴스의 쓰레드 로컬 필드에 접근해도 문제없다.


🌱 ThreadLocal 예제

좌 : 쓰레드 문제 발생 / 우 : ThreadLocal 사용으로 인해 동시성문제 해결

 

동시성 문제를 ThreadLocal 적용으로 인해서 해결했다.


🌱 ThreadLocal 의 사용법

값 저장

ThreadLocal.set(xxx);

 

값 조회

ThreadLocal.get();

 

값 제거

ThreadLocal.remove();

🌱 ThreadLocal 사용시 주의 사항

ThreadLocal 을 사용한 후에는 꼭 remove 를 사용해서 쓰레드 로컬에서 사용한 값을 제거해줘야 한다.


🌱 ThreadLocal 테스트

아래와 같이 테스트를 진행했을 때 Thread A 와 Thread B 는 각 Thread 별로 저장소를 사용하기 때문에

전역적으로 사용하는 변수를 수정하였을 때 Thread 간의 결과에 영향을 미치지 않았다.

 

Thread A 와 Thread B 의 예상하는 결과가 나와 동시성 문제를 해결할 수 있었다.

 

- 테스트 코드

private ThreadLocalService fieldService = new ThreadLocalService();

@Test
void field(){

    System.out.println("main start");

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

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

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

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

    treadA.start();
    sleep(2000);
    
    treadB.start();
    sleep(2000);
}

 

- 테스트 결과

2022-11-04 14:39:42.865  INFO 11208 --- [       thread-A] h.a.threadlocal.ThreadLocalService       : 저장 name=userA >> nameStore=null
2022-11-04 14:39:43.881  INFO 11208 --- [       thread-A] h.a.threadlocal.ThreadLocalService       : 조회 nameStore=userA
2022-11-04 14:39:44.878  INFO 11208 --- [       thread-B] h.a.threadlocal.ThreadLocalService       : 저장 name=userB >> nameStore=null
2022-11-04 14:39:45.888  INFO 11208 --- [       thread-B] h.a.threadlocal.ThreadLocalService       : 조회 nameStore=userB

728x90
반응형