ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] 동시성 문제 해결하기 - ThreadLocal
    Spring 2022. 6. 14. 19:40

    이전 호출 스택에서 특정 값을 다음 호출에서도 사용할 수 있도록 하기 위해서는

    파라미터를 활용하여 값을 전달하는 방법이 있다.

     

    // 첫번째 메서드에서의 msg를 호출하는 메서드에서 유지하기 (파라미터)
    public void first(int number) {
    	number += 1;
        String msg = "메시지";
        second(msg);
    }
    
    public void second(String msg) {
    	...
    }

    하지만 파라미터로 값을 전달한다면 수정소요가 발생하였을 때

    해당 파라미터를 받는 메서드를 모두 찾아 수정/삭제 해야하고

    최초에 파라미터를 작성하는 것도 많은 노력이 필요하다.

     

    이를 해결하기 위해서 필드를 활용한 방법을 사용할 수 있다.

    필드 동기화 방법을 사용한다면 특정 클래스의 필드 변수만 관리하면 되기 때문에

    유지 보수 측면에서 큰 이득을 얻을 수 있다.

     

    public Message {
    	String msg;
        
        public startMsg(String msg) {
        	this.msg = msg;
        }
    	
        public chkMsg() {
        	if(msg == null) {
            	msg = "new Msg";
            } else {
            	msg = "old Msg";
            }
        }
    }

    하지만 해당 객체가 싱글톤으로 등록된 스프링 빈일 경우에

    여러 쓰레드가 해당 필드에 동시 접근 한다면 문제가 발생한다.

     

    여러 쓰레드가 동시에 같은 인스턴스의 필드 값을 변경하면서 발생하는 문제를

    동시성 문제라고 한다.

     

    이런 동시성 문제는 여러 쓰레드가 같은 인스턴스 필드에 접근해야하기 때문에

    트래픽이 점점 많아질수록 발생한다.

    특히 스프링 빈터럼 싱글톤 객체와 필드를 변경하며 사용 시에는 이러한 동시성 문제를 조심해야한다.

     

    - 인스턴스의 필드(주로 싱글톤) 또는 static 같은 공용 필드에 접근할 때 발생한다.

    - 값을 읽기만 한다면 문제 없지만 어디선가 값을 변경할 때 발생한다.

     

    예를 들어서

    // 이벤트 참여자를 당첨자로서 저장하는 서비스 로직
    @Slf4j
    public class ThreadProbService {
    	private String succes;
    	
    	public void startEvent(String name) {
    		log.info("이벤트 참여={} -> 당첨={}", name, succes);
    		succes = name;
            // 이벤트 참여자는 1초 이후 당첨자로서 저장된다.
    		sleep(1000);
    		log.info("당첨자={}", succes);
    	}
    	
    	private void sleep(int millis) {
    		try {
    			Thread.sleep(millis);
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    	}
    	
    }

    위와 같이 이벤트를 참여하면 1초 후에 당첨되는 서비스 로직이 있다고 하였을 때

     

    @Slf4j
    public class FieldServiceTest {
    
    	private FieldService fieldService = new FieldService();
    
    	@Test
    	void field() {
    		log.info("event 시작");
            //Runnable을 구현한 객체 2개 생성
    		Runnable userA = () -> {
    			fieldService.logic("userA");
    		};
    		Runnable userB = () -> {
    			fieldService.logic("userB");
    		};
    		
            //Thread객체를 2개 생성 후 이름을 부여
    		Thread threadA = new Thread(userA);
    		threadA.setName("thread-A");
    		Thread threadB = new Thread(userB);
    		threadB.setName("thread-B");
    
    		threadA.start();
    		sleep(100); // 0.1초만에 다음 쓰레드가 동작함
    		threadB.start();
    		
    		sleep(3000);
    		log.info("event 끝");
    	}
    
    	private void sleep(int millis) {
    		try {
    			Thread.sleep(millis);
    		} catch (InterruptedException e) {
    			e.printStackTrace();
    		}
    	}
    }

    만약 위와 같이 1초 후 당첨자로 저장되기 전 0.1초만에 다음 쓰레드가 동작되면

     

    이벤트의 참여자는 userA, userB 두명이나

    당첨자는 userB만 저장되어 있는 경우가 발생하게 된다.

     

    즉 동시성의 문제가 발생하게 된다.

     

    따라서 싱글톤 객체의 필드를 사용하면서 동시성 문제를 해결할 때 사용하는 것이 바로

    ThreadLocal이다.

     

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

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

     

    즉 쓰레드 로컬을 사용하면 각 쓰레드가 필드에 값을 저장할 때

    개별 저장을 하기 때문에 A, B라는 쓰레드가 있을 때 A가 먼저 값을 저장하고

    이어서 B가 저장한다고 해도 B의 저장소는 비어있다.

     

    // ThreadLocal 객체를 만들어 준다음
    // get()을 통해 값을 읽고
    // set(값)을 통해 값을 저장한다.
    private ThreadLocal<String> succes = new ThreadLocal<>();
    	
    	public void startEvent(String name) {
    		log.info("이벤트 참여={} -> 당첨={}", name, succes.get());
    		succes.set(name);;
    		sleep(1000);
    		log.info("당첨자={}", succes.get());
    	}

    단순히 필드로 동기화 했을 때와는 다르게 동시성 문제가 해결된 것을 볼 수 있다.

     

    만능인 것 처럼 보이는 ThreadLocal에도 주의사항이 있는데

    쓰레드 로컬의 값을 사용한 다음 제거하지 않고 그냥 두면

    WAS와 같이 쓰레드 풀(pool)을 사용하는 경우 심각한 문제가 생길 수도 있다.

    (쓰레드 풀의 경우 쓰레드 제거가 아닌 반환 형태로 이루어지며 재사용 된다.)

     

    즉 쓰레드 로컬에 개별 저장되어 있는 값이 사라지지 않기 때문에

    다른 사용자가 우연히 해당 쓰레드를 사용하여 조회 작업을 하는 경우

    이전에 쓰레드 로컬에 의해 보관되어 있던 값이 반환되어 버린다.

    따라서 remove()를 사용하여 사용이 종료되는 시점에 쓰레드 로컬의 값을 제거해야한다.

     

    [참고 : 인프런 - 스프링 핵심 원리 - 고급편]

Designed by Tistory.