싱글톤 패턴에서 필드 공유 문제

Choizz·2023년 1월 10일
1

디자인 패턴

목록 보기
8/8

스프링에 대해 공부하게 되면서 스프링 컨테이너가 싱글톤 컨테이너의 역할을 하면서 스프링 컨테이너에 저장된 bean들을 싱글톤으로 관리된다고 배웠다.

요청이 올 때마다 객체를 생성하는 것이 아니라 이미 만들어진 객체를 공유해서 사용하는 것이다. 이렇게 되면 메모리 낭비를 방지할 수 있다.

하지만, 싱글톤 방식에서 주의할 점이 존재한다.

싱글톤 패턴이나, 싱글톤 컨테이너를 사용하는 경우, 인스턴스 하나를 공유하기 때문에 객체의 상태를 stateful하게 설계를 하면 안된다. 만약 필드가 공유되게 된다면 심각한 오류가 발생할 수 있다.


필드 공유 문제 상황

  • Hello라는 클래스를 싱글톤 패턴으로 만들어 보았다.
  • Hello#print 메서드는 각 나라별 인삿말을 출력하면서 인삿말을 클래스의 필드로 설정한다.
public class Hello {

    private String hello; //공유되는 필드

    private static Hello newInstance = new Hello();

    private Hello() {
    }

    public static Hello getInstance() {
        return newInstance;
    }

    public void print(String hello) {
        this.hello = hello; // 파라미터를 필드로 저장
        System.out.println("인사말 = " + hello + " " + "[" + Thread.currentThread().getName() + "]");
    }

    public String getHello() {
        return hello;
    }
}

  • 멀티 스레드 상황을 설정한다.
  • 각각 나라별 클래스들은 모두 같은 Hello 인스턴스를 갖게 하고 각 나라별 인삿말을 100번 출력한다.
public class Dutch implements Runnable {

    @Override
    public void run() {
    	//싱글톤 객체 생성
        Hello hello4 = Hello.getInstance();
        for (int i = 0; i < 100; i++) {
            hello4.print("Hé");
        }
    }
}
public class English implements Runnable {

    @Override
    public void run() {
        Hello hello2 = Hello.getInstance();
        for (int i = 0; i < 100; i++) {
            hello2.print("hello");
        }
    }
}
public class Japanese implements Runnable {

    @Override
    public void run() {
        Hello hello3 = Hello.getInstance();
        for (int i = 0; i < 100; i++) {
            hello3.print("やあ");
        }
    }
}
public class Vietnamese implements Runnable {

    @Override
    public void run() {
        Hello hello5 = Hello.getInstance();
        for (int i = 0; i < 100; i++) {
            hello5.print("Chào");

        }
    }
}

  • 메인 스레드에서 각 스레드들을 병렬적으로 처리하고, 처음에 한국어 인삿말 "안녕"이 korean의 상태로 계속 유지가 되는지 확인해 보자!
public class Main {

    public static void main(String[] args) {
        Hello korean = Hello.getInstance();
        korean.print("안녕");
        System.out.println("처음에 저장된 korean의 hello필드 = " + korean.getHello());

        Thread english = new Thread(new English());
        english.start();

        Thread japan = new Thread(new Japanese());
        japan.start();

        Thread vietnam = new Thread(new Vietnamese());
        vietnam.start();

        Thread netherland = new Thread(new Dutch());
        netherland.start();

        for (int i = 0; i < 50; i++) {
                        System.out.println("======== korean의 필드 " + korean.getHello() + "===========");
 								//위에서 설정한 "안녕"이라는 값이 나오는지 확인
        }

    }
}
  • 결과
인사말 = 안녕 [main]
처음에 저장된 hello의 hello필드 = 안녕

인사말 = hello [Thread-0]
인사말 = hello [Thread-0]
인사말 = hello [Thread-0]
인사말 = hello [Thread-0]
인사말 = hello [Thread-0]
인사말 = hello [Thread-0]

...

인사말 = やあ [Thread-1]
인사말 = hello [Thread-0]
인사말 = やあ [Thread-1]
인사말 = Hé [Thread-3]
인사말 = Chào [Thread-2]
인사말 = hello [Thread-0]

...

======== korean의 필드 やあ===========
======== korean의 필드 Chào===========
======== korean의 필드 Chào===========
======== korean의 필드 Chào===========
======== korean의 필드 Chào===========
======== korean의 필드 Chào===========
======== korean의 필드 Chào===========

...
  • 처음에 저장된 "안녕"이라는 상태 값이 네덜란드어와 영어 등으로 바뀐 것을 확인할 수 있다. 즉, 스레드가 print 메서드에 접근할 때마다 싱글톤 객체의 상태 값이 변해버린다.
  • Hello의 hello 필드는 공유되는 필드인데, 멀티 스레딩 상황에서 스레드들이 이 값을 변경해버린다.
  • main 스레드(korean)에서 요청이 들어와서 "안녕"을 출력해야하는데 다른 나라의 언어가 출력되는 현상이 발생한 것이다.

원인

  • 싱글톤으로 같은 인스턴스를 공유해서 쓰는 경우 여러 클라이언트가 동시에 같은 코드를 실행하는 경우가 발생한다.

  • 처음에 main 스레드에서 print("안녕")를 실행하여 "안녕"을 Hello의 필드로 할당한다.

  • 그 후 여러 다른 언어를 사용하는 스레드가 print()를 실행하면서 공유되는 인스턴스 필드를 다른 언어로 바꿔버린다.

  • JVM Stack

    • 각 메서드가 실행될 때 메서드의 인자, 로컬 변수 등을 관리하는 메모리 영역
    • 스레드마다 각각의 스택 영역을 가진다.
  • JVM Heap

    • 클래스의 인스턴스가 생성되고, 인스턴스의 상태 데이터를 관리하는 메모리 영역
    • 각각의 스레드가 서로 공유할 수 있다.

해결

  • 무상태(Stateless)로 설계한다.
    • 특정 클라이언트에 의존적인 필드가 있어선 안된다.
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
    • 가급적 읽기만 허용한다.
    • 공유되지 않는 지역변수, 파라미터, ThreadLocal 등을 활용한다. 즉, 스레드끼리 공유하지 않도록 한다.

ThreadLocal 사용

  • Hello 클래스에 스레드끼리 공유되지 않는 ThreadLocal을 사용해서 데이터를 저장한다.
public class Hello {

    ThreadLocal<String> local = new ThreadLocal<>(); //스레드 로컬을 사용한다.

    private static Hello newInstance = new Hello();

    private Hello() {
    }

    public static Hello getInstance() {
        return newInstance;
    }

    public void print(String hello) {
        local.set(hello); //스레드 로컬에 파라미터를 저장한다.
        System.out.println("인사말 = " + hello + " " + "[" + Thread.currentThread().getName());
    }
}
class Main {

    public static void main(String[] args) {
        Hello korean = Hello.getInstance();
        korean.print("안녕");

        Thread english = new Thread(new English());
        english.start();

        Thread japan = new Thread(new Japanese());
        japan.start();

        Thread vietnam = new Thread(new Vietnamese());
        vietnam.start();

        Thread netherland = new Thread(new Dutch());
        netherland.start();

        for (int i = 0; i < 50; i++) {
            System.out.println(
                "========" + korean.local.get() + "==========="); //위에서 설정한 "안녕"이라는 값이 나오는지 확인
        }

    }
}
  • 결과
    • 처음에 설정한 "안녕" 이라는 값이 나오는 것을 확인할 수 있다.
...

======== 안녕===========
======== 안녕===========
인사말 = Chào [Thread-2]
인사말 = Chào [Thread-2]
인사말 = Chào [Thread-2]
인사말 = Chào [Thread-2]
인사말 = Chào [Thread-2]
인사말 = Chào [Thread-2]
인사말 = Chào [Thread-2]
========안녕===========
========안녕===========
========안녕===========
========안녕===========

ThreadLocal 사용시 주의 사항

  • 만약 WAS는 스레드 풀이 존재하여 스레드가 사용되고 다시 스레드 풀로 반납하게 되는데 만약 ThreadLocal이 제거되지 않고 존재하면, 다시 그 스레드가 사용될 때 ThreadLocal의 그대로 남아있게 되어 오류가 발생할 수 있다. 따라서, 스레드가 할 일이 모두 끝난다면 ThreadLocal을 제거해야 한다.

로컬 변수 사용

  • 로컬 변수를 사용하여 스레드 끼리 공유되지 않게 한다.
public class Hello {

    private static Hello newInstance = new Hello();

    private Hello() {
    }

    public static Hello getInstance() {
        return newInstance;
    }

    public String print(String hello) {
        String localHello = hello; //파라미터로 온 값을 로컬 변수에 저장하고 그것을 리턴한다.
        System.out.println("인사말 = " + hello + " " + "[" + Thread.currentThread().getName());
        return localHello;
    }
}
  • 로컬 변수는 공유되지 않고 스레드마다 할당되기 때문에 thread-safe하다.

Refernce

profile
집중

0개의 댓글