본 포스팅은 김영한 님의 스프링 핵심원리 기본편 강의 섹션 5를 듣고 요약한 내용입니다.
1. 스프링 없는 순수한 DI 컨테이너
기존의 순수한 자바코드로 작성한 DI컨테이너에서는 클라이언트에서 객체를 호출할 때마다 새로운 객체를 생성해왔습니다. 다음 예제를 통해 확인해보겠습니다.
package hello.core.singleton;
import hello.core.AppConfig;
import hello.core.member.MemberService;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
public class SingletonTest {
@Test
@DisplayName("스프링 없는 순수한 DI 컨테이너")
void pureContatiner() {
AppConfig appConfig = new AppConfig();
//1. 조회 : 호출할 때 마다 새 객체를 생성
MemberService memberService1 = appConfig.memberService();
//2. 조회 : 호출할 때 마다 객체를 생성
MemberService memberService2 = appConfig.memberService();
//참고값이 다른것을 확인
System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);
//memberService1 != memberService2
assertThat(memberService1).isNotSameAs(memberService2);
}
}
참고로, Appconfig는 다음과 같습니다.
package hello.core;
import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.member.MemberRepository;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import hello.core.member.MemoryMemberRepository;
import hello.core.order.OrderService;
import hello.core.order.OrderServiceImpl;
@Configuration
public class AppConfig {
public MemberService memberService(){
return new MemberServiceImpl(memberRepository());
}
public MemberRepository memberRepository() {
return new MemoryMemberRepository();
}
public OrderService orderService(){
return new OrderServiceImpl(memberRepository(), discountPolicy());
}
public DiscountPolicy discountPolicy() {
return new FixDiscountPolicy();
}
}
결과를 확인해보면,
두가지의 객체가 다른 객체인 것을 알 수 있습니다. (다른 것을 참조함)
우리가 만들었던 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때 마다 객체를 새로 생성합니다.
따라서 메모리 낭비가 심한 단점이 있습니다. 이를 해결하기 위해서 객체를 딱 1개만 생성하고 공유되도록 설계하는 방법이 바로 싱글톤 패턴입니다.
2. 싱글톤 패턴
클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하기 위해서는 다양한 방법이 있겠지만 여기서는 private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하게 막는 방법을 사용해 보도록 하겠습니다.
package hello.core.singleton;
public class SingletonService {
//1. static 영역에 객체를 딱 1개만 생성해둔다.
private static final SingletonService instance = new SingletonService();
//2. public으로 열어서 객체 인스턴스가 필요하면 이 static 메서드를 통해서만 조회하도록 허용한다.
public static SingletonService getInstance() {
return instance;
}
//3. 생성자를 private으로 선언해서 외부에서 new키워드를 사용한 객체 생성을 못하게 막는다.
private SingletonService() {
}
public void logic() {
System.out.println("싱글톤 객체 로직 호출");
}
}
먼저 static 영역에 객체 인스턴스를 미리 하나 생성해 두고, 이 객체 인스턴스가 필요하면 getInstance() 메서드를 이용해서만 조회할 수 있도록 설계합니다. 이 메서드를 호출하면 항상 같은 인스턴스를 반환합니다.
이제 이 코드를 테스트해보겠습니다.
@Test
@DisplayName("싱글톤 패턴을 적용한 객체 사용")
public void singletonServiceTest() {
//private로 생성자를 막아두었다. 컴파일 오류가 발생한다.
//new SingletonService();
//1. 조회: 호출할 때 마다 같은 객체를 반환
SingletonService singletonService1 = SingletonService.getInstance();
//2. 조회: 호출할 때 마다 같은 객체를 반환
SingletonService singletonService2 = SingletonService.getInstance();
//참조값이 같은 것을 확인
System.out.println("singletonService1 = " + singletonService1);
System.out.println("singletonService2 = " + singletonService2);
//singletonService1 == singletonService2
assertThat(singletonService1).isSameAs(singletonService2);
singletonService1.logic();
}
싱글톤 패턴으로 객체를 호출하였고, 호출 될 때마다 같은 객체를 조회하는지 확인해 봅시다.
같은 객체 인스턴스를 반환하는 것을 확인할 수 있습니다.
하지만 싱글톤 패턴에도 문제점은 있습니다.
- 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
- 의존관계상 클라이언트가 구체 클래스에 의존합니다. -> DIP 위반 (private static final SingletonService instance = new SingletonService(); 에서 구체 클래스에 의존하고 있습니다.)
- 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높습니다.
- 유연성이 떨어진다.
다음 포스팅에선 싱글톤 패턴의 문제점을 해결하면서 객체 인스턴스를 하나만 생성하는 방법에 대해서 알아보도록 하겠습니다.