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);
}
}
}
실행 결과
멀티 스레드에서 싱글턴 클래스를 이용할 때 인스턴스가 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);
}
}
실행결과
이와 같은 멀티 스레드 환경에서 싱글턴 클래스가 상태를 유지해야 하는 경우 문제가 발생한다.
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;
}
}
실행결과
멀티 스레드에서 발생하는 문제를 해결하는 방법 2가지
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;
}
}
실행결과
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 부분도 동기화해야함
public int plusCounter() {
synchronized (this) { // counter 동기화
counter++;
}
return counter;
}
싱글턴은 여러가지 문제가 있다.
스프링에서는 싱글톤 패턴의 문제점들도 보완해주면서 싱글톤 패턴으로 클래스의 인스턴스를 사용하게 해 주는데 이것을 싱글톤 컨테이너라고 한다.
스프링 컨테이너는 싱글톤 컨테이너의 역할을 하며 싱글톤 객체를 생성, 관리한다.
스프링 컨테이너의 기능으로 싱글톤 패턴을 구현하기 위한 코드를 사용하지 않아도 되고 OCP 원칙을 위배하지 않게 되었다.
@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을 막고 싱글톤을 보장해준다.