본문 바로가기

Programming/Java & Spring 관련 내용 정리

[Spring] Thread-safe 에 대해서 고찰해 보다.

부제 : 스프링 빈은  멀티쓰레드 환경에서 Thread-safe 할까?

 

 

개발 실장님께서

아래 코드에 버그가 있는데, 무엇이 버그인지 알겠냐고 물어보셨다.

 

( 회사코드를 그대로 가져다 쓸 수 없어

예시를 들기 위해 클래스명, 변수명 등을 조금 변경했다. )

 

 

 

아래 코드를 보면,

for문을 돌면서 getBean() 메소드를 통해

생성되어 있는 스프링 빈(bean)을 찾아와
characterService에 할당하고 있다.                                       - (1)번 파트 

 

그다음 할당된 스프링 빈(bean)의 init() 메소드를 실행한다.   - (2)번 파트

 

 

public class Service {
    // .... 생략
    private CharacterService characterService;

    public Response request(RequestDto requestDto) {
        Response response = new Response();

        // .... 생략
        for (CharacterGroup character : characterMap.keySet()) {
            try {
            
            	// (1)번 파트
                characterService = (CharacterService) ApplicationContextUtil.getApplicationContext().getBean(character.getName() + "service");
                
                // (2)번 파트
                characterService.init(character, characterMap.get(character));
                
            } catch (NoSuchBeanDefinitionException e) {
                log.info("characterService {}");
            }
        }

        return response;
    }
}

 

 

 

ChoonSikService, RyanService, ApeachService, MuziService 등 

'캐릭터이름 + service' 형식으로 네이밍된 여러 스프링 빈이 존재한다.

 

@Service("ChoonSikService")
@Transactional
public class ChoonSikService extends CharacterService {
	
    // 생략 ..
    public void init(CharacterGroup charaterGroup, ... ) {
    // 생략..
    }
}

 

@Service("RyanService")
@Transactional
public class RyanService extends CharacterService {
	
    // 생략 ..
    public void init(CharacterGroup charaterGroup, ... ) {
    // 생략..
    }
}

 

@Service("ApeachService")
@Transactional
public class ApeachService extends CharacterService {
	
    // 생략 ..
    public void init(CharacterGroup charaterGroup, ... ) {
    // 생략..
    }
}

 

 

 

위 코드에서 무엇이 문제일까?

 

몇 번 답을 틀리고 나서 

드디어 내가 맞춘 답은.....

'스프링 빈(bean)은 thread safe 하지 않아서 CharacterService를 지역변수로 해야한다.' 이다.

 

 

그럼 Thread-safe 하다는 건 뭘까?


Thread-safe는 멀티 스레드 프로그래밍에서

어떤 함수나 변수나 객체가

여러 스레드로부터 동시에 접근이 이루어져도

프로그램의 실행에 문제가 없음을 뜻한다.

 

하나의 함수가 한 스레드로부터 호출되어 실행 중일 때,

다른 스레드가 그 함수를 호출하여 동시에 함께 실행되더라도

각 스레드에서의 함수의 수행 결과가 올바로 나오는 것을 말한다.

 

 

 

 

Thread-safe 하기 위해서 왜 지역변수로 선언해야 한다고 했을까?

 

위 (에러가 생길수도 있는) 코드에서 CharacterService는

멤버변수로 선언되어 있다.

 

 

 

 

 

위 이미지를 참고하여 아래 내용을 정리할 수 있다.

 

  •  지역 변수는 '스택' 영역에 저장
    • '스택'은 각 쓰레드별로 할당되기 때문에 자원이 공유되지 않음

 

  • 멤버 변수는 '힙' 영역에 저장
    • '힙'에 저장 시 자원이 공유될 수 있음

 

  • characterService를 멤버변수로 선언 시
    • 힙 영역에 저장되어 자원이 공유되므로 -> 의도치 않게 데이터가 변할 수 있음

 

 

 

 

그럼 여기서 말하는 의도치 않은 변경이란 무엇일까?

 

아래와 같이 생각해보았다.

 

 

1. (1) 메소드와 (2) 메소드 호출이 동시에 요청 되었다.


- 동시에 호출되는 경우, 미리 정해놓은 어떠한 기준에 따라 최종 호출 순서가 정해지는데

결론적으로 --->  (1) 메소드가 먼저 수행되었다.

 

- 이에 따라 characterService는 (1) 메소드에 맞춰 코드 내용이 수행된다.

 

 


2. 그 다음 바로 이어서 대기하고 있던 (2) 메소드가 수행된다.


- 이에 따라 characterService는 (2) 메소드에 맞춰 코드 내용이 수행된다.


- 멤버 변수라 힙에 저장되므로,

예를 들면 (1) 메소드에 맞춰 세팅된 choonSikService의 코드가

👉  (2) 메소드 내용의 RyanService 코드로 변경되는 상황이 발생할 수 있는 것이다.

 

 


3. 사용자는 (1) 메소드에 맞게 세팅된 choonSikService에 있는 자원 A에 접근하려고 했는데
바로 이어서 호출된 (2) 메소드에 맞춰 RyanService가 세팅되어서 

자원 A가 아니라 자원 B에 접근된다.

 

 


4. 결론적으로 원하는 데이터를 얻어오지 못하는 상황이 발생할 수 있다.

 

 


 

그럼 어떻게 코드를 고쳤을까?

 

 

아래와 같이 멤버 변수가 아닌, 

지역변수로 선언함으로써 

자원이 공유되지 않도록 변경했다.

 

 

 


🎈 Spring bean은 Thread-safe 하지 않다!

 

빈(bean)은 개발자 입장에서 편리하다.

스프링이 생성하고 관리하고 제어를 해주기 때문이다.

하지만 스프링 빈(bean)은 근본적으로 thread-safe 하지 않다.

 

 

 

그럼 왜 스프링 빈이 Thread-safe 하다고  생각했을까?

 

첫번째 이유는 멀티 쓰레드 환경을 많이 겪어보지 않아서

막연히 'Thread-safe 할 것이다.' 라고 생각했기 때문이다. (근본없는 믿음)

 

두번째 이유는 (본인은 깨닫지 못했어도) Thread-safe 하게 빈 클래스를 만들고 있었기 때문이다.

스프링 개발자들이 배우고 학습한 자료 및 기존 코드는 Thread-safe한 예제가 많았기 때문이다.

 

 

 

🎈 그래서 결론은!

 

- 스프링 빈(bean)은 기본적으로 Thread-safe 하지 않을 수 있다.

싱글톤 빈은 여러 스레드가 동시에 접근할 수 있기 때문에

상태를 갖는 빈은 Thread-safe 하도록 만들어줘야 한다.

 

- 스프링 빈(bean) 클래스에 멤버 변수가 있는지 확인하자.

  값이 변하는 변수인 경우는 지역 변수로 선언하자.