싱글톤 패턴
싱글톤 컨테이너를 살펴보기 전에 싱글톤 패턴에 대해 알아보도록 하겠습니다.
싱글톤 패턴이란 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴 입니다.
singletonService (싱글톤 패턴을 사용한 클래스)
package hello.core.singleton;
public class SingletonService {
//1. static 영역에 객체 하나 생성.
private static final SingletonService instance = new SingletonService();
//2. 접근제한자를 public으로 하여 객체 인스턴스가 필요하면 조회가능하도록 한다.
public static SingletonService getInstance() {
return instance;
}
//3. 생성자를 private로 하여 외부에서 new 키워드를 사용하지 못하게 막는다.
private SingletonService() {
}
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
}
예제코드로 살펴보면 static 영역에 객체를 하나 생성해놓고, 생성자를 private로 하여 외부에서 해당 클래스 객체를 생성하지 못하도록 막아 놓습니다. 이때 외부에서 해당 객체를 사용할 수 있게 getInstance 메소드를 public으로 열어놓습니다.
test code (싱글톤 패턴을 사용한 클래스의 테스트코드)
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
public void singletonServiceTest() {
// 1. 조회: 호출할 때 마다 같은 객체를 반환
SingletonService singletonService1 = SingletonService.getInstance();
SingletonService singletonService2 = SingletonService.getInstance();
// 참조값이 같은지 확인
System.out.println("singletonService1 = " + singletonService1);
System.out.println("singletonService2 = " + singletonService2);
// singletonService1 == singletonService2 인지 확인
Assertions.assertThat(singletonService1).isSameAs(singletonService2);
singletonService1.logic();
}
테스트 코드를 살펴보면 싱글톤 패턴을 적용한 클래스의 객체는 여러번 조회해도 같은 객체를 반환하는것을 알 수 있습니다.
싱글톤 패턴은 이렇게 클래스의 인스턴스가 하나만 생성되도록 보장하므로써 불필요한 객체 생성을 줄여 메모리를 낭비하는것을 방지해줍니다.
참고: 싱글톤 패턴을 구현하는 방법은 여러개가 있고, 위의 예제코드는 그중 가장 단순한 방식입니다. 다른 방식은 (https://readystory.tistory.com/116) 해당 블로그에 참고하시면 좋을듯합니다.
위에서 싱글톤 패턴의 예제코드를 살펴보았습니다. 이런 싱글톤 패턴은 웹 애플리케이션에서 사용할 때, 고객의 요청이 올 때 마다 객체를 생성하지않고, 이미 만들어진 객체를 공유하여 효율적으로 사용할 수 있습니다. 하지만 싱글톤 패턴은 아래와 같은 문제점들을 가지고 있습니다.
싱글톤 패턴의 문제점
- 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
- 의존관계상 클라이언트가 구체 클래스에 의존한다 → DIP를 위반한다.
- 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
- 테스트하기 어렵다.
- 내부 속성을 변경하거나 초기화 하기 어렵다.
등등 다수의 문제점을 가지고 있습니다. 스프링에서 제공하는 싱글톤 컨테이너를 사용하면 싱글톤 패턴의 장점은 유지하면서 위와같은 단점들을 해결할 수 있습니다.
싱글톤 컨테이너
스프링 컨테이너는 기본적으로 객체 인스턴스를 싱글톤(1개만 생성)으로 관리합니다.(기본 빈 등록 방식은 싱글톤이지만, 프로토타입방식으로도 생성 가능합니다)
한마디로 스프링 컨테이너는 싱글톤 컨테이너의 역할을 한다고 할 수 있습니다.
이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리 라고 합니다.
스프링 컨테이너를 통해 싱글톤 패턴의 단점들을 해결하면서 객체를 싱글톤으로 유지할 수 있습니다.
스프링 컨테이너로 해결
- 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 됨
- DIP,OCP,Test,private 생성자로 부터 자유롭게 싱글톤 생성 가능
AppConfig
package hello.core;
@Configuration
public class AppConfig {
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
여기서는 @Bean을 사용하여 직접 스프링 컨테이너에 등록하는 방식을 사용했습니다.
스프링 컨테이너를 사용하는 테스트 코드
package hello.core.singleton;
public class SingletonContainerTest {
@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
// 1. 조회 : 호출할 때 마다 같은 객체를 반환
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 == memberService2 인지 확인
Assertions.assertThat(memberService1).isSameAs(memberService2);
}
}
테스트 실행 결과를 확인해보면 싱글톤 패턴을 적용했을때와 마찬가지로 하나의 객체를 공유해서 사용한다는 것을 알 수 있습니다. 하지만 단순히 예제코드를 살펴봤을때는 스프링 컨테이너를 사용하면 왜 DIP, OCP를 지킬 수 있는지, 왜 private 생성자로부터 자유롭게 싱글톤을 생성 가능하다는 것인지 잘 와닿지 않습니다. 아래에서 하나씩 살펴보겠습니다.
스프링 컨테이너는 어떻게 DIP, OCP를 지키는가
DIP란 Dependency Inversion Principle 으로 아래와 같은 원칙을 가집니다.
- 상위 모듈(클래스)은 하위 모듈(클래스)에 의존해서는 안된다. 둘 다 추상화에 의존해야 한다.
- 추상화는 구체적인 것에 의존해서는 안된다. 구체적인 것이 추상화에 의존해야 한다.
스프링 컨테이너는 DI(Dependency Injection) 즉 의존성 주입을 통하여 위와같은 원칙들을 지킵니다.
스프링에서 싱글톤 빈을 사용할 때, 의존성을 주입받는 클래스는 구체적인 빈의 인스턴스에 의존하지 않고, 그 빈의 인터페이스나 추상화된 타입에 의존합니다.
OCP란 Open/Closed Principle 으로 확장에는 열려있고, 수정에는 닫혀있어야 한다는 원칙입니다.
스프링 컨테이너는 DI(의존성 주입)을 통해 OCP원칙을 준수합니다.
스프링 DI 예제코드
public interface UserService {
void performAction();
}
@Service
public class UserServiceImpl implements UserService {
@Override
public void performAction() {
}
}
@Component
public class UserClient {
private final UserService userService;
@Autowired
public UserClient(UserService userService) {
this.userService = userService;
}
}
예제코드를 보면 UserService라는 인터페이스를 구현하는 UserServiceImpl클래스가 @Service 를 이용하여 스프링 컨테이너에 등록됩니다. UserClient클래스는 @Component 를 통해 스프링 컨테이너에 등록됩니다. 이때 UserClient의 생성자에서 @Autowired 를 통해 스프링 컨테이너에 등록되어있는 빈들중 UserService 타입의 빈을 조회하여 매개변수로 넣어주게 됩니다. 이렇듯 스프링 컨테이너를 사용하면 DI를 통해 DIP 원칙을 지킬 수 있습니다.
만약 UserServiceImpl이 아닌 UserService를 구현하는 새로운 구현체(NewUserService)를 추가한다고 해도 UserClient 클래스는 UserService에 의존하고 있기 때문에 코드를 수정할 필요가없습니다. 이렇게 OCP 원칙또한 지킬 수 있게 됩니다.
스프링 컨테이너는 어떻게 private 생성자로 부터 자유롭게 싱글톤을 사용할 수 있게 하는가
사실 이전 AppConfig 코드를 살펴보면 약간 의아한 부분이 있습니다.
package hello.core;
@Configuration
public class AppConfig {
// 1번
@Bean
public MemberService memberService() {
return new MemberServiceImpl(memberRepository());
}
// 2번
@Bean
public OrderService orderService() {
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
@Bean
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
@Bean
public DiscountPolicy discountPolicy() {
return new RateDiscountPolicy();
}
}
1번,2번 주석처리한 부분을 살펴보면 모두 생성자의 매개변수로 memberRepository() 를 호출하는것을 볼 수 있습니다. memberRepository()는 반환값으로 new MemoryMemberRepository()를 호출합니다. new 키워드를 통해 객체를 생성하는데 어떻게 싱글톤을 보장할 수 있을까요? 이부분은 @Configuration 과 관련이 있고 해당내용이 길기 때문에 싱글톤(싱글톤 패턴, 싱글톤 컨테이너 모두)의 주의사항과 함께 다음에 알아보도록 하겠습니다.
참고
https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1/dashboard
'Back-end > Spring' 카테고리의 다른 글
[Spring] Dispatcher-Servlet 알아보기 (0) | 2025.02.26 |
---|---|
[Spring MVC] Bean Validation(검증) (1) | 2024.09.05 |
[Spring] 빈 생명주기 콜백 (2) | 2024.08.18 |