[1/3] Spring과 싱글톤 패턴
개요
싱글톤 패턴이란 클래스의 인스턴스가 1개만 생성되는 것을 보장하는 디자인 패턴이다. 그렇기에 객체 인스턴스를 2개 이상 생성할 수 없도록 막아야 한다.
private생성자를 사용해서 외부에서 임의로 new키워드를 사용하지 못하도록 막는 것이다.
싱글톤 패턴의 필요성
웹 서비스에 접속한 사용자가 특정 요청을 하면 서버에서는 요청을 처리하기 위한 객체가 생성된다. 비즈니스 로직을 처리하기 위한 객체가 있다면 이 객체가 생성될 것이다. 그런데 사용자 100명이 동시에 호출한다면? 1000명이 동시에 호출한다면? 해당 객체는 100개씩, 1000개씩 동시에 생성되고 삭제될 것이며 그에 따른 GC도 계속해서 동작하게 될 것이다.
싱글톤은 필요한 객체를 미리 내부에서 생성한 후 해당 객체를 외부에서 생성할 수 없도록 한다. 이렇게 하면 외부에서는 새로운 객체를 생성할 수 없고 내부에 미리 생성된 객체를 호출해서 사용하기 때문에 같은 객체가 계속해서 호출되더라도 매번 새로운 객체가 생성되는 것이 아니라 미리 생성된 하나의 객체로 모든 작업을 처리할 수 있다.
테스트를 진행하기 전에 몇 가지 클래스를 만들어주자.
MemberService라는 인터페이스를 만들고 그에 맞는 구현체들을 싱글톤과 싱글톤이 아닌 상태로 만들어서 테스트를 할 예정이다.
DI를 적용하여 구현체들은 AppConfig를 만들어서 AppConfig를 통해서 주입받아 사용할 수 있도록 만들 것이다.
먼저 MemberService를 만들어주자.
public interface MemberService {
public String singletonStatus();
}
테스트용이기 때문에 어떤 상태인지를 출력해 줄 singletonStatus만 만들었다.
이제 구현체를 만들어주자. 먼저 싱글톤이 적용되지 않은 구현체를 만들 것이다.
public class MemberServiceImpl implements MemberService {
public String singletonStatus() {
return "싱글톤 적용안됨";
}
}
마지막으로 AppConfig를 만들어서 구현체를 주입할 수 있게 해주자.
public class AppConfig {
public MemberServiceImpl memberService() {
return new MemberServiceImpl();
}
}
그리고 테스트코드에서 2개의 MemberService를 생성해서 같은 객체인지를 확인해보자
public class SingletonTest {
@Test
void 두_개의_객체_생성_싱글톤_적용_안함() throws Exception {
// given
AppConfig appConfig = new AppConfig();
MemberServiceImpl memberService1 = appConfig.memberService();
MemberServiceImpl memberService2 = appConfig.memberService();
// when, then
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
assertThat(memberService1).isNotSameAs(memberService2);
}
}
결과는 당연하게도 2개의 객체는 다른 주소 값을 가진다.
이렇게 되면 이제 클라이언트가 MemberService를 호출할 때마다 계속해서 객체가 생성되고 삭제되는 일이 반복되게 된다.
이 문제를 싱글톤 패턴으로 만들어서 해결할 수 있다.
MemberServiceImplSingleton이라는 싱글톤 형태의 클래스를 새로 만들어보자.
public class MemberServiceImplSingleton implements MemberService{
private static final MemberServiceImplSingleton instance = new MemberServiceImplSingleton(); // 1
private MemberServiceImplSingleton() {} // 2
public static MemberServiceImplSingleton getInstance() { // 3
return instance;
}
public String singletonStatus() {
return "싱글톤 적용 완료";
}
}
- 먼저 내부에서 인스턴스를 생성한 뒤 접근, 변경을 할 수 없도록 만들어준다.
- 생성자는 private으로 선언해서 외부에서는 new를 사용해서 생성자를 만들 수 없도록 한다.
- static메서드로 내부에서 미리 만들어놓은 인스턴스를 사용할 수 있도록 한다.
이렇게 하면 더이상더 이상 외부에서 마음대로 인스턴스를 생성할 수 없고 미리 생성된 하나의 인스턴스를 가져와서 사용하기 때문에 더 이상 무분별하게 인스턴스가 생성되는 것을 막을 수 있다.
AppConfig에 추가해주자.
public class AppConfig {
public MemberServiceImpl memberService() {
return new MemberServiceImpl();
}
public MemberServiceImplSingleton memberServiceSingleton() {
return MemberServiceImplSingleton.getInstance();
}
}
테스트 코드를 만들어서 확인해보도록 하자.
@Test
void Spring을_사용한_싱글톤_적용() throws Exception {
// given
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
MemberServiceImpl memberService1 = ac.getBean("memberService", MemberServiceImpl.class);
MemberServiceImpl memberService2 = ac.getBean("memberService", MemberServiceImpl.class);
// when, then
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
assertThat(memberService1).isSameAs(memberService2);
}
생성한 2개의 인스턴스가 서로 같은 인스턴스임을 확인할 수 있다.
싱글톤 패턴을 만드는 방법은 다양하게 있지만 여기서는 가장 안전하고 확실한 방법을 사용했다. 하지만 이렇게 만들더라도 몇 가지 문제가 발생하게 된다.
- 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
- 의존관계상 클라이언트가 구체 클래스에 의존한다. -> DIP를 위반한다.
- 클라이언트가 구체 클래스에 의존해서 OCP원칙을 위반할 가능성이 높다.
- 테스트하기 어렵다.
- 내부 속성을 변경하거나 초기화 하기 어렵다.
- private생성자로 자식 클래스를 만들기 어렵다.
- 유연성이 떨어진다.
분명 하나의 인스턴스로 모든 호출을 처리할 수 있는 장점이 있지만 많은 단점들도 존재하기에 안티 패턴으로 불리기도 한다.
Spring은 이러한 문제점들을 모두 해결하면서 싱글톤 패턴의 장점만을 가지고 사용할 수 있도록 만들어주고 있다.
Spring이 제공하는 싱글톤 패턴
스프링은 인스턴스를 빈이라는 것으로 관리한다. 처음 스프링이 실행될 때 @Bean으로 등록된 인스턴스들을 모두 스프링 컨테이너에 key, value의 형태로 저장한다.
스프링 빈으로 등록하는 방법은 여러가지가 있지만 여기서는 가장 많이 사용하는 어노테이션을 이용해서 등록해보도록 하자.
AppConfig를 다음과 같이 수정해주어야 한다.
@Configuration
public class AppConfig {
@Bean
public MemberServiceImpl memberService() {
return new MemberServiceImpl();
}
@Bean
public MemberServiceImplSingleton memberServiceSingleton() {
return MemberServiceImplSingleton.getInstance();
}
}
이렇게 등록하고 실행시키면 아래 사진과 같이 스프링 컨테이너에 등록될 것이다.
AppConfig에 등록한 메소드이름이 key로 들어가고 실행되서 반환된 인스턴스의 주소 값이 value가 등록되어 스프링 컨테이너에서 관리된다. 객체가 호출되면 스프링이 관리하는 컨테이너에서 해당 객체를 주입해주는 방식으로 동작되는 것이다.
이제 스프링을 적용시킨 MemberServiceImpl이 싱글톤 형태로 동작하는지 테스트해보도록 하자.
@Test
void Spring을_사용한_싱글톤_적용() throws Exception {
// given
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class); // 1
MemberServiceImpl memberService1 = ac.getBean("memberService", MemberServiceImpl.class); // 2
MemberServiceImpl memberService2 = ac.getBean("memberService", MemberServiceImpl.class);
// when, then
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
assertThat(memberService1).isSameAs(memberService2);
}
- AnnotationConfigApplicationContext은 ApplicationContext를 상속받고 있으며 어노테이션을 이용해서 스프링 컨테이너에 등록시키는 방식이다. 다른 방식으로 등록하고 싶다면 다른 구현체를 사용해야 한다.
- 등록된 빈을 가져오는 방식이다. 빈의 이름, 클래스를 넘겨주면서 가져올 수 있다.
테스트를 실행시키면 위의 테스트에서는 서로 다른 주소를 가져오던 MemberServiceImpl이 같은 주소 값을 가지는 것을 알 수 있다.
스프링을 실행시켰기 때문에 로그도 스프링이 띄어주는 로그들이 더 나타나는 것을 볼 수 있다.
About
해당 게시글은 김영한님의 '스프링 핵심 원리 - 기본편'을 공부하고 작성한 내용입니다.
해당 게시글의 소스코드는 https://github.com/ssung0810/spring_study/tree/main/singleton에서 볼 수 있습니다.
'Spring > Core' 카테고리의 다른 글
스프링과 데이터베이스[1] - JDBC와 커넥션 풀 (0) | 2022.11.08 |
---|---|
스프링 주입의 다양한 방법들 (0) | 2022.06.13 |
스프링 빈 등록의 다양한 방법들 (0) | 2022.06.09 |
[3/3] Spring과 싱글톤 패턴 - @Configuration의 비밀 (0) | 2022.06.07 |
[2/3] Spring과 싱글톤 패턴 - 싱글톤 패턴의 주의점 (0) | 2022.06.06 |
댓글
이 글 공유하기
다른 글
-
스프링 주입의 다양한 방법들
스프링 주입의 다양한 방법들
2022.06.13 -
스프링 빈 등록의 다양한 방법들
스프링 빈 등록의 다양한 방법들
2022.06.09 -
[3/3] Spring과 싱글톤 패턴 - @Configuration의 비밀
[3/3] Spring과 싱글톤 패턴 - @Configuration의 비밀
2022.06.07 -
[2/3] Spring과 싱글톤 패턴 - 싱글톤 패턴의 주의점
[2/3] Spring과 싱글톤 패턴 - 싱글톤 패턴의 주의점
2022.06.06