본문 바로가기

공부 정리/Java

싱글톤(Singleton)

728x90
반응형

singleton


 싱글톤은 유일한 1개의 공유 인스턴스만 관리하며, 해당 빈 정의와 일치하는 ID를 가진 모든 요청은 스프링 컨테이너에 의해 유일하게 "하나의 빈"만 반환합니다.

 만약 싱글톤으로 1개의 빈을 정의했다면, 스프링 IoC 컨테이너는 해당 빈 정의로 유일한 1개의 객체 인스턴스를 생성합니다. 유일한 객체 인스턴스들은 캐시에 저장되고 해당 빈이 필요하면 저장된 캐시 객체를 반환합니다. 

 

 

 스프링의 싱글톤 개념과 디자인 패턴의 싱글톤 개념 다릅니다. 디자인패턴 싱글톤은 '클래스 로더'마다 특정 클래스의 유일한 1개의 인스턴스만 생성합니다. 하지만 스프링 싱글톤은 '스프링 컨테이너'마다 1개의 빈을 생성합니다. 즉, 하나의 스프링 컨테이너에 특정 클래스를 빈으로 정의했다면, 스프링 컨테이너는 해당 클래스로 유일한 하나의 인스턴스만 생성합니다. 싱글톤 스코프는 스프링에서 기본 스코프입니다. XML에서 싱글톤으로 빈을 설정해보겠습니다.

 

<bean id="accountService" class="com.something.DefaultAccountService"/>

<!-- the following is equivalent, though redundant (singleton scope is the default) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>

 

 싱글톤이므로 Spring IoC 컨테이너마다 유일하게 하나의 빈 인스턴스만 초기화가 되고 같은 인스턴스는 매 요청마다 공유됩니다. 새로운 요청이 올때마다, 스프링 IoC 컨테이너는 이미 인스턴스가 생성되었는지 확인합니다. 이미 생성되어 있다면, IoC 컨테이너는 같은 인스턴스를 반환하며, 그렇지 않다면 첫 요청에만 새로운 인스턴스를 만듭니다.

 

 

Spring 싱글톤 퀴즈 - 2개 빈은 동일할까요? (==)


 Scope 클래스로 생성한 scopeTest, scopeTestDuplicate 2개의 빈이 있습니다.

 

//xml 정의 버전
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean id="scopeTest" class="com.example.scope.Scope" scope="singleton">
        <property name="name" value="Shamik Mitra"/>    
    </bean>    
    <bean id="scopeTestDuplicate" class="com.example.scope.Scope" scope="singleton">
        <property name="name" value="Samir Mitra"/>    
    </bean>
</beans>

//어노테이션 기반 버전
@Bean
public Scope scopeTest() {
	return new Scope("name", "Shamik Mitra");
}

@Bean
public Scope scopeTestDuplicate() {
	return new Scope("name", "Shamik Mitra");
}

 

 

public class Scope {
    private String name;
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Scope [name=" + name + "]";
    }
}

 

 

package com.example.scope;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {

    public static void main(String[] args) {
        ApplicationContext ctx = new ClassPathXmlApplicationContext(
            "configFiles/Scope.xml");
        Scope scope = (Scope) ctx.getBean("scopeTest");
        Scope scopeDuplicate = (Scope) ctx.getBean("scopeTestDuplicate");
        System.out.println(scope == scopeDuplicate);
        System.out.println(scope + "::" + scopeDuplicate);
    }
}

 

  •  만약, ScopeTest와 ScopeTestDuplicate 2개의 bean을 생성한다면, 출력문에서 2개의  결과는 어떻게 될까요?

 2개 모두 싱글톤으로 정의가 되어 있는데, 같은 class를 참조하고 있으면 2개의 빈은 만들 수 없을까요? 아니면, 같은 클래스를 대상으로 빈을 만들었기 때문에, 빈이 1개의 공유자원으로 인식되어 같은 scope와 scopeDuplicate는 같은 주소를 가리킬까요? 답은, "class가 같아도 고유한 id를 가지면 독립적인 빈으로 생성되고, 다른 주소를 참조한다" 입니다.

 

Reference Check ::false
Scope [name=Shamik Mitra]::Scope [name=Samir Mitra]

 

 스프링에서 싱글톤이란, id마다 1개의 공유 인스턴스를 만드는 것입니다. id가 다르다면, 클래스가 같더라도 각각 독립적인 인스턴스를 생성합니다.

 

 

Spring이 정말 싱글톤인지 객체의 주소값을 확인해보자


 2개의 서비스 OrderService와 MemberSvice를 정의하고 MemberRepoitory를 의존하는 코드입니다. OrderService와 MemberService 2개의 객체를 스프링 빈으로 등록하며 MemberRepository를 가집니다. 각 빈은 생성자를 통해 객체를 생성할 때 MemberRepository를 호출합니다.

 

public class OrderService {
    private final MemberRepository memberRepository;
    private final OrderRepository orderRepository;
    ...
    
    public MemberRepository memberRepository() {
    	return this.memberRepository;
    }
}

public class MemberService {
    private final MemberRepository memberRepository;
    ...
    
    public MemberRepository memberRepository() {
    	return this.memberRepository;
    }
}

public class MemberRepository {
	...
}

@Configuration
public class AppConfig {
    @Bean
    public OrderService orderService() {
    	return new OrderService(memberRepository(), orderRepository());
    }
    
    @Bean
    public MemberService memberService() {
    	return new MemberService(memberRepository());
    }
    
    @Bean
    public MemberRepository memberRepository() {
    	return new MemoryMemberRepository();
    }
}

 

 각각 다른 빈에서 MemberRepository을 호출할 때 같은 빈을 호출하는지 테스트를 통해 비교해보겠습니다.

 

public class Test {
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    MemberService memberService = ac.getBean(MemberService.class);
    OrderService orderService = ac.getBean(OrderService.class);
    
    MemberReopository memberRepository1 = memberService.memberRepository();
    MemberReopository memberRepository2 = orderService.memberRepository();
    
    Assertions.assertSame(memberRepository1, memberRepository2); //true!
    
    System.out.println("memberRepository1 = " + memberRepository1);
    System.out.println("memberRepository2 = " + memberRepository2);
}

 

 OrderSevice와 MemberService는 서로 아무런 관련이 없지만, 2개의 빈이 동일하게 의존하는 MemberRepository는 스프링 컨테이너에 등록된 하나의 빈을 공유합니다. @Configuration + @Bean으로 빈을 등록하면, 스프링 컨테이너는 바이트 조작으로 등록된 빈들의 싱글톤을 보장합니다. 따라서 MemberRepository가 각종 빈들에 의존성 주입에 사용이 될 때 모든 빈들은 스프링 컨테이너에 등록된 MemberRepository 빈을 공유합니다.

 

 

Spring 싱글턴 주의사항 - 무상태로 설계할 것


 싱글턴 빈은 어떠한 상태정보도 저장하면 안됩니다. 빈으로 등록된 인스턴스는 해당 빈으로 들어오는 모든 요청 쓰레드에 공유됩니다. 만약 스프링 싱글턴 빈에 상태를 저장한다면, 전혀 연관이 없는 다른 쓰레드에서 영향을 받을 수 있습니다.

 

 주문 시, 가격을 상태로 저장하고 getter 함수로 꺼내는 서비스가 있다고 가정하겠습니다.

 

//절대 이렇게 짜면 안된다!
public class OrderService() {
	private int price;
    
	public void order(String itemName, int price) {
    	...
    	this.price = price;	
    }
    
    public int getPrice() {
    	return this.price;
    }
}

//나와 상관없는 주문가격이 반환된다!
public class OrderServiceTest {
    ApplicationContext ac = new AnnotationConfigApplicationContext(Test.class);
    
    OrderService orderService1 = ac.getBean(OrderService.class);
    OrderService orderService2 = ac.getBean(OrderService.class);
    orderService1.order("itemA", 10000);
    orderService2.order("itemB", 20000);
    
    Assertions.assertEquals(orderService1.getPrice(), 20000); //true!
    
    static class Test {
    	@Bean
        OrderService orderService() {
        	return new OrderService();
        }
    }
}

 

 itemA를 10000원 주고 샀음에도 불구하고, itemB를 20000원으로 샀던 다른 쓰레드 작업에 의해 itemA의 price가 20000으로 변경됩니다. 따라서 OrderService는 다음과 같이 상태를 가지지 않도록 개선해야 합니다.

 

//상태를 가져서는 안된다!
public class OrderService() {
	//private int price;
    
	public int order(String itemName, int price) {
    	...
    	return price;
    }
    
    //public int getPrice() {
    //	return this.price;
    //}
}

//올바른 테스트 결과
int price1 = orderService1.order("itemA", 10000);
int price2 = orderService2.order("itemB", 20000);
Assertions.assertEquals(price1, 10000); //true!

 

상태 값을 저장하고 getter로 price를 조회하는 것이 아닌, order() 자체에서 price를 반환하는 무상태 설계로 개선합니다.

 

* 참고

https://dzone.com/articles/an-interview-question-on-spring-singletons

728x90
반응형

'공부 정리 > Java' 카테고리의 다른 글

OOD, OOP(객체 지향 개발)의 원칙 - SOLID  (1) 2024.03.15
Generics의 Type Erasure(타입 소거)  (0) 2024.03.08
Generics의 Wildcards(와일드카드)  (0) 2024.03.07
Value Object 패턴  (0) 2023.07.07
Entity vs Value Object 차이점  (0) 2023.06.30