싱글턴 패턴

yanju·2023년 1월 13일
0
post-thumbnail

싱글턴 패턴

  • 인스턴스가 오직 하나만 생성되는 것을 보장한다.
  • 어디에서든 이 인스턴스에 접근할 수 있다.

싱글턴 코드

public class Printer {
    private static Printer printer = null;

    private Printer() {
    }

    public static Printer getPrinter() {
        if (printer == null) {
            printer = new Printer();
        }

        return printer;
    }
}

public class Main {

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            Printer printer = Printer.getPrinter();
            System.out.println(printer);
        }
    }

}

실행 결과

  • 5개 모두 같은 인스턴스

문제점

멀티 스레드에서 싱글턴 클래스를 이용할 때 인스턴스가 1개 이상 생성될 수 있다.

public class Printer {
    private static Printer printer = null;

    private Printer() {
    }

    public static Printer getPrinter() {
        if (printer == null) {  // 스레드 1, printer = null 
            printer = new Printer();
        }

        return printer;
    }
}

public class Printer {
    private static Printer printer = null;

    private Printer() {
    }

    public static Printer getPrinter() {
        if (printer == null) {  // 스레드 2, printer = null
            printer = new Printer(); 	// 스레드 1, printer = null (실행예정)
        }

        return printer;
    }
}

public class Printer {
    private static Printer printer = null;

    private Printer() {
    }

    public static Printer getPrinter() {
        if (printer == null) {  
            printer = new Printer(); // 스레드 2, printer = null (실행예정)
						// 스레드 1, printer = new Printer
        }

        return printer;
    }
}

다음과 같이 멀티 스레드로 싱글턴을 여러개 만들 수 있다.

public class ThreadPrinter {
    private static ThreadPrinter printer = null;

    private ThreadPrinter() {
    }

    public static ThreadPrinter getPrinter() {
        if (printer == null) {
            try {
                Thread.sleep(1);  // 
            } catch (InterruptedException e) {
            }
            printer = new ThreadPrinter();
        }

        return printer;
    }
}

public class UserThread extends Thread {
    @Override
    public void run() {
        ThreadPrinter printer = ThreadPrinter.getPrinter();
        System.out.println(Thread.currentThread().getName() + " " + printer);
    }
}

실행결과

  • 인스턴스가 5개 생김

이와 같은 멀티 스레드 환경에서 싱글턴 클래스가 상태를 유지해야 하는 경우 문제가 발생한다.

Printer 클래스를 counter 변수 값을 유지해야 한다고 가정해보자.

public class ThreadPrinter {
    private static ThreadPrinter printer = null;
    private int counter = 0;  // 상태 변수

    private ThreadPrinter() {
    }

    public static ThreadPrinter getPrinter() {
        if (printer == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
            }
            printer = new ThreadPrinter();
        }

        return printer;
    }

    public int plusCounter() {  // 상태 변화
        counter++;
        return counter;
    }
}

실행결과

  • 스레드에서 만든 인스턴스마다 counter 변수를 관리함
  • 인스턴스가 상태값을 유지해야 한다면 문제가 생김

해결책

멀티 스레드에서 발생하는 문제를 해결하는 방법 2가지

  1. 정적 변수에 인스턴스를 만들어 바로 초기화
  2. 인스턴스를 만드는 메서드에 동기화

1. 정적 변수에 인스턴스를 만들어 바로 초기화

  • 정적 변수는 클래스가 메모리에 로딩될 때 초기화가 한 번만 실행된다.
  • 정적 변수는 프로그램이 종료될 때까지 메모리에 계속 있는다.
public class PrinterUsingInit {
    private static PrinterUsingInit printer = new PrinterUsingInit(); // 정적 변수에서 초기화
    private int counter = 0;

    private PrinterUsingInit() {
    }

    public static PrinterUsingInit getPrinter() {
        return printer; // 정적 변수 리턴
    }

    public int plusCounter() {
        counter++;
        return counter;
    }
}

실행결과

  • 하나의 인스턴스만 생성됨

2. 인스턴스를 만드는 메서드에 동기화

  • synchronized를 메서드에 사용함으로써 다중 스레드 환경에서 동시에 여러 스레드가 메서드를 접근하는 것을 방지한다.
public class PrinterSynchronized {
    private static PrinterSynchronized printer = null;
    private int counter = 0;

    private PrinterSynchronized() {
    }

    // 메서드 동기화
    public synchronized static PrinterSynchronized getPrinter() {
        if (printer == null) {
            printer = new PrinterSynchronized();
        }

        return printer;
    }

    public int plusCounter() {
        counter++;
        return counter;
    }
}

실행결과

  • 하나의 인스턴스만 생성됨
  • counter 값이 이상함

여러 개의 스레드가 하나뿐인 counter 변수 값에 동시에 접근해 갱신하기 때문이다.

다음과 같이 counter 부분도 동기화해야함

public int plusCounter() {
	  synchronized (this) { // counter 동기화
	      counter++;
	  }
	  return counter;
}

스프링과 싱글턴 패턴

웹 어플리케이션과 싱글톤의 관계

  • 서비스 운영시 동일한 요청이 위 그림처럼 서로 다른 클라이언트로부터 동시에 들어옴
  • 그 때마다 다른 memberService 를 생성한다면 메모리 공간이 남아나질 않는다.
  • 이에 대한 해결방안으로 동일한 요청들에 대해서는 싱글톤 패턴을 적용시킨다.

싱글톤 컨테이너

싱글턴은 여러가지 문제가 있다.

  • 싱글톤 패턴을 구현하기 위한 코드가 늘어남
  • 인스턴스를 반환해주는 구현 클래스를 직접 참조해야 하므로 DIP를 위반한다. (OCP 원칙)
  • 내부 속성을 변경, 초기화하기가 어렵다.
  • private 생성자로 자식 클래스를 생성하기 어렵다.
  • 유연성이 떨어진다.

스프링에서는 싱글톤 패턴의 문제점들도 보완해주면서 싱글톤 패턴으로 클래스의 인스턴스를 사용하게 해 주는데 이것을 싱글톤 컨테이너라고 한다.

스프링 컨테이너는 싱글톤 컨테이너의 역할을 하며 싱글톤 객체를 생성, 관리한다.

스프링 컨테이너의 기능으로 싱글톤 패턴을 구현하기 위한 코드를 사용하지 않아도 되고 OCP 원칙을 위배하지 않게 되었다.

@Configuration

@Bean을 붙여주면 해당 인스턴스를 Bean으로 등록해준다.

@Configuration의 유무 차이는 각각의 인스턴스 call 횟수를 통해 확인 할 수 있다.

@Configuration  // 요거의 유무차이
public class AppConfig {
    @Bean
    public MemberService memberService(){
        System.out.println("call AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository(){
        System.out.println("call AppConfig.memberRepository");
        return new MemoryMemberRepository(); //메모리 맴버리포지토리로 생성
    }
}

@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    //1. 조회: 호출할 때 마다 같은 객체를 반환
    MemberService memberService1 = ac.getBean("memberService", MemberService.class);
    //2. 조회: 호출할 때 마다 같은 객체를 반환
    MemberService memberService2 = ac.getBean("memberService", MemberService.class);
    //참조값이 같은 것을 확인
    System.out.println("memberService1 = " + memberService1);
    System.out.println("memberService2 = " + memberService2);

    assertThat(memberService1).isSameAs(memberService2);
}

스프링이 CGLIB 이라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 후 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한다.

CGLIB의 내부 기술을 이용해 무분별한 Method Call을 막고 싱글톤을 보장해준다.

0개의 댓글