SSAFY11th-book-study / book-study

SSAFY 11기 6반의 '토비의 스프링 스터디'
0 stars 0 forks source link

[1.6] 전역 상태 싱글톤 #9

Closed hj-k66 closed 8 months ago

hj-k66 commented 8 months ago

일반적인 싱글톤의 구현은 아무 객체나 자유롭게 접근하고 수정하고 공유할 수 있는 전역 상태를 갖는다는 문제점이 있는데,

스프링의 빈 역시 getBean()을 사용하면 동일한 문제점을 갖고 있는 것 같습니다.

전역 상태를 갖는다는 게 어떤 치명적인 문제점인지, 스프링 빈에서는 이걸 어떻게 해결한건지가 궁금합니다.

limjongheok commented 8 months ago

우선 전역 상태를 갖는다는 것은 멀티스레드 환경상에서 동시성 문제를 가지고 있다고 보면 됩니다.

public class UserDao {

    private String userName;
    public String save(String name){
        System.out.println(userName + " " +name);
        this.userName = name;
        sleep(1000);
        System.out.println("현재 유저이름 " +  userName);
        return userName;
    }

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

}
@Configuration
public class ThreadFactory {

    @Bean
    public UserDao userName(){
        return new UserDao();
    }

}
@Configuration
public class ThreadFactory {

    @Bean
    public UserDao userName(){
        return new UserDao();
    }

}

다음과 같이 User 이름을 받아 자신에 필드에 저장하는 UserDao클래스에 빈으로 관리할 수 있도록 하는 TreadFactory 클래스가 있다면

public class TreadTest {
    AnnotationConfigApplicationContext context;
    UserDao userDao;
    @Test
    public void treadTest(){
        context = new AnnotationConfigApplicationContext(ThreadFactory.class);

        userDao = context.getBean("userName", UserDao.class);

        Runnable userA =() ->{
            userDao.save("임종혁");
        };

        Runnable userB =() ->{
            userDao.save("가나다");
        };

        // 쓰레드 지정
        Thread thread1 = new Thread(userA);
        thread1.setName("tread1");

        Thread thread2 = new Thread(userB);
        thread1.setName("tread2");

        // thread 1 시작
        thread1.start(); // userA 실행
        sleep(100);
        thread2.start(); // userB실행
        sleep(3000);
        System.out.println("종료");
    }

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

다음과 같이 실행 환경에서 userA는 임종혁 userB 는 가나다라는 값을 갖고 해당값을 return 할거라 기대를 하고 있습니다. 허나

image 다음과 같이 tread1의 userA는 "임종혁" 저장 반환 했는데 userB라는 값을 반환하는 결과를 얻게 됩니다. 이처럼 스프링 빈 뿐만 아닌 싱글톤은 동시성 문제를 갖으며 해결 하기 위한 방법으로는

  1. 쓰레드 마다 별도의 내부 저장소를 제공하는 TreadLoacal 사용
    private TreadLocal<String> userName = new TreadLocal():
  2. 책에서 처럼 무상태 방식으로 만들기 (변화할수 있는 필드 값을 주지 않기) 근데 제 생각에는 무상태 방식이 더 좋은것 같습니다.

등이 있을 거 같습니다. 혹시 추가적인 방식 있으면 comment 남겨 주세요

gmelon commented 8 months ago

저도 동일하게 이해했습니다. 스프링을 통해 싱글톤을 만들고 관리하더라도 동일한 오브젝트가 공유된다는 싱글톤의 특성은 같기 때문에 결국 공유되는 상태가 오염될 수 있다는 문제는 개발자가 신경써서 관리해야 한다고 생각합니다.

종혁님이 말씀해주신 것처럼 스프링 시큐리티 같은 경우에도 ThreadLocal을 사용해서 각 요청 별 인증 객체를 별도로 저장하고 사용하고 있다고 알고 있습니다.

다만, 전역 상태 에 대한 정의를 명확히 하면 좋을 것 같아요! static으로 선언된 변수만 전역 상태라고 볼 것인지 아니면 싱글톤이기 때문에 여러 클라이언트에서 공유되는 instance 변수도 전역 상태로 봐야하는 것인지 궁금하네요. 제 생각을 먼저 말해보면 후자의 경우 전역 상태라기보다 그냥 공유되는 상태라고 분리해야하지 않을까? 생각합니다.

limjongheok commented 8 months ago

제 생각으로는 전역 상태라는 것은 heep 메모리에 올라간 인스턴스를 모든 스레드가 동일한 인스턴스 주소를 가리키는 상태를 전역 상태라고 생각하고 있습니다. 그래서 데이터 메모리에 올라가는 전역 변수만 보고서는 전역 상태는 아니지 않나 생각 합니다.