싱글톤 패턴
웹 어플리케이션 개발에 특화된 스프링은 기본적으로 싱글톤 패턴을 지원한다.
싱글톤 패턴에 대한 내용은 별도로 정리해 두었다.
https://debuggingworld.tistory.com/107
싱글톤 패턴 (Singleton Pattern)
싱글톤 패턴 클래스 인스턴스가 단 하나만 존재하는 것을 보장하는 디자인 패턴. 웹 어플리케이션과 싱글톤 웹 어플리케이션의 특성 중 하나는 다수의 클라이언트가 동시에 요청을 하는 경우가
debuggingworld.tistory.com
싱글톤 패턴은 미리 만들어진 객체를 공유하여 효율적으로 사용할 수 있다는 장점이 있지만,
코드가 길어지고 유연성이 떨어지는 등의 문제가 있다.
스프링 컨테이너는 싱글톤 패턴의 이러한 문제점을 해결하면서, 싱글톤 패턴을 사용할 수 있도록 지원한다.
싱글톤 컨테이너
스프링 컨테이너는 기본적으로 스프링 빈을 싱글톤으로 관리한다. (기본 설정)
테스트 코드로 스프링 빈의 클래스 정보를 출력해보면 확인할 수 있다.
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// 조회: 스프링 빈을 가져오면 싱글톤 객체가 리턴됨
MemberService memberService1 = ac.getBean("memberService", MemberService.class);
// 조회: 스프링 빈을 가져오면 싱글톤 객체가 리턴됨
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
// 참조값이 같은 것을 확인
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
// memberService1과 memeberService2가 서로 다른 객체인지 테스트
assertThat(memberService1).isSameAs(memberService2);
}
결과를 확인해보면 출력된 참조값이 같다. 같은 인스턴스라는 의미다.
memberServiceImpl 클래스의 코드를 보면, 싱글톤 패턴 적용을 위한 코드를 별도로 작성하지 않았다.
package hdxian.hdxianspringcore.member;
public class MemberServiceImpl implements MemberService {
// private final MemberRepository memberRepository = new MemoryMemberRepository();
private final MemberRepository memberRepository;
// memberRepository에 들어갈 구현체를 생성자를 통해 전달받는다.
public MemberServiceImpl(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Override
public void join(Member member) {
memberRepository.save(member);
}
@Override
public Member findMember(Long memberId) {
return memberRepository.findById(memberId);
}
public MemberRepository getMemberRepository() {
return this.memberRepository;
}
}
이렇듯 스프링 컨테이너는 직접 싱글톤 패턴을 적용하지 않아도, 스프링 빈을 싱글톤으로 관리한다.
따라서 개발자는 싱글톤 패턴을 위해 코드를 추가로 작성하지 않아도 되고, DIP, OCP 위반 등을 신경쓰지 않아도 된다.
+)
스프링 컨테이너와 같이 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라고 한다.
+)
스프링 빈을 싱글톤으로 관리하는 것은 기본 설정일 뿐이다. 원하면 요청마다 객체를 새로 생성하도록 설정할 수도 있다.
싱글톤 패턴의 주의점
싱글톤 패턴의 특징은 같은 객체를 여러 곳에서 공유한다는 점이다.
객체를 공유하는 과정에서 문제가 발생하지 않도록, 싱글톤 객체는 무상태(stateless)로 설계해야 한다.
즉 뭔소리냐면
- 특정 클라이언트에 의존적인 필드가 있으면 안됨.
- 특정 클라이언트가 임의로 필드 값을 변경하게 하면 안됨.
- 가급적 읽기만 가능하도록 설계해야 함
- 변수가 필요하다면 필드보다는 지역변수, 파라미터, ThreadLocal 등을 활용
싱글톤 패턴을 상태 유지(stateful)로 설계하면 발생하는 문제점을 예시 코드와 함께 살펴보자.
stateful 클래스 예시
public class StateFulService {
// 상태가 유지되는 필드.
private int price;
public void order(String name, int price) {
System.out.println("name = " + name + ", price = " + price);
// 상태가 유지되는 필드를 여러군데에서 수정할 수 있는 상황.
// 이 클래스의 인스턴스가 싱글톤으로 관리되면, 하나의 변수를 여러군데서 수정하므로 문제가 발생할 수 있다.
this.price = price;
}
public int getPrice() {
return this.price;
}
}
테스트 코드
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import static org.junit.jupiter.api.Assertions.*;
class StateFulServiceTest {
@Test
void stateFulSingletonTest() {
ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
StateFulService stateFulService1 = ac.getBean("stateFulService", StateFulService.class);
StateFulService stateFulService2 = ac.getBean("stateFulService", StateFulService.class);
// ThreadA가 10000원짜리 주문을 진행
stateFulService1.order("nameA", 10000);
// ThreadB가 20000원짜리 주문을 진행
stateFulService2.order("nameB", 20000);
// 이 때 ThreadA에서 사용자의 주문 금액을 읽으려고 하면?
int price = stateFulService1.getPrice();
System.out.println("price = " + price);
Assertions.assertThat(price).isEqualTo(20000);
}
static class TestConfig {
@Bean
public StateFulService stateFulService() {
return new StateFulService();
}
@Bean
public StatelessService statelessService() {
return new StatelessService();
}
}
}
TestConfig 클래스로 StatefulService를 빈으로 등록하고 테스트를 돌리는 코드다.
즉 클래스는 stateful로 설계되었지만, 스프링에 의해 싱글톤으로 관리되고 있는 상태.
테스트 결과를 살펴보면, 분명 10000원을 주문한 nameA가 자신의 주문 금액을 조회했지만, 20000원이 찍히고 있다.
중간에 nameB가 20000원을 주문하면서 인스턴스의 필드 값이 20000으로 변경됐기 때문이다.
결제 서비스 같이 돈과 관련된 서비스에서 이런 일이 터지면 그냥 망했다고 보면 된다.
예시 코드는 간단히 짜여있지만, 실제 복잡하게 짜여있는 코드에서 이러한 문제가 발생하면 찾기 어렵다.
따라서 싱글톤 패턴으로 클래스를 설계할 때는 무상태(stateless)로 잘 설계하도록 주의가 필요하다.
'[inflearn] 스프링 핵심 원리 - 기본편 > 섹션 5 - 싱글톤 컨테이너' 카테고리의 다른 글
@Configuration과 싱글톤 (0) | 2024.04.20 |
---|---|
싱글톤 패턴 (Singleton Pattern) (0) | 2024.04.20 |