Spring(3) - 싱글톤, 싱글톤 컨테이너에 대해서

김유담·2024년 6월 7일

spring

목록 보기
4/11
post-thumbnail

김영한 강사님스프링 핵심 원리의 강의 내용과 자료를 이용했음을 밝힙니다.

👨‍💻 What is 싱글톤 패턴 & Why 싱글톤 패턴? (Singleton Pattern)

스프링을 하다 보면 싱글톤을 사용한다고 한다.

그럼 검색해보면 술 이미지 밖에 안나오는 싱글톤, 즉 싱글톤 패턴이란 것은 무엇일까?

우선 간단하게 말하자면 객체를 매번 만들 지 않고 하나를 만들어서 돌려쓰는 것이다.

자 왜 그래야 할까?

간단한 예시를 한번 들어보자

AppConfig appConfig = new AppConfig()

MemberService memberService1 = appConfig.memberService();
MemberService memberService2 = appConfig.memberService();

System.out.println("memberService1 = " + memberService1);
System.out.println("memberService2 = " + memberService2);

이 코드를 실행해 본다면 memberService1memberService2는 다르게 나오는 것을 볼 수 있다.

그야 당연히 AppConfig.class 안으로 들어가서 코드를 보면

public MemberService memberService() {
	return new MemberServieceImpl(memberRepository());
}

이런 식으로 객체를 새로 생성해주니 memberService1memberService2가 다르게 나오는 것은 당연하다.

하지만 이런 생각이 들지 않는가?

"만약에 객체를 엄~~청 많이 만들면(요청하면) 메모리가 엄청 많이 쓰이겠다."

그렇다 현재 우리는 "실습"을 하고 있으니까 몇 개만 만들지만 실제로 나중에 실업에서는 수많이 만들어서 메모리를 정말 많이 사용할 수 있는 것이다.

그래서 어차피 memberService1, memberService2 둘다 memberService인데 하나를 만들고 memberService1, memberService2가 그 한개를 같이 참조하면서 쓰도록하면 메모리를 효율적으로 사용할 수 있는 것이다.

그리고 그 방식이 위에서 말한 싱글톤 패턴 되시겠다~

👨‍💻 싱글톤 패턴 구현

싱글톤 패턴을 구현하기 위해서는 몇가지 정의이자 규칙들을 알아야 한다.

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.
  • 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 한다.
  • private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 한다.

위의 것들이 지금은 잘 이해가 안될 텐데 이제 알아가보면 된다.

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("싱글톤 객체 로직 호출");
	}
}

그럼 한 줄씩 review를 해보자.

private static final SingletonService instance = new SingletonService();

싱글톤의 핵심은 같은 객체가 2개 이상 만들어지지 않게 하는 것이다.

그렇기에 클래스 인스턴스도 2개 이상 생성되어서는 안된다.
(클래스에서 객체를 하나만 만들게 하더라도 클래스 객체가 2개 이상이면 같은 객체가 2개 이상 만들어진다.)

그리고 static을 이용하면 프로그램이 시작되어 클래스가 메모리에 올라가게 되면 static이 붙은 변수나 메서드는 클래스와 함께 자동으로 메모리의 static 영역에 올라가 딱 1개만 존재하고 여러 객체가 참조할 수 있게 해준다.

그리고 final은 instance가 수정되지 않게 잡아준다.

public static SingletonService getInstance() {
	return instance;
}

이 객체의 인스턴스를 가져가기 위해 getInstance()라는 메소드를 만들어 놓은 것이다.

그러면 이런 생각도 들 것이다.

"생성자를 통해서 객체를 생성해서 가져가면 위의 2개의 코드가 의미 없는 것 아닌가?"

정확하다.

그래서 private로 외부에서 생성자를 못만들게 막는 것이다.

private SingletonService() {
}

👨‍💻 Test

자 이렇게 만들었다면 한번 test를 해보아야 한다.

//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과 singletonService2가 같은 객체임을 확인해볼 수 있다.

👨‍💻 싱글톤 패턴의 문제점

자 다 좋은 것 같은 싱글톤 패턴도 사실 문제가 있다.

우선 개인적으로 싱글톤 패턴 보면서 바로 생각한 것은 바로 공유의 문제점이다.

공유하다 보니 혹시 한 쪽에서 임의로 변수 이런거 바꿔놓으면 다른 쪽에서 영향을 받으니 이런 것이 추상화가 잘 안되는 것이 아닌가? 라는 생각을 바로 했다.

일종의 공유지 비극...

이것 외에도

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다. DIP를 위반한다.
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  • 테스트하기 어렵다.
  • 내부 속성을 변경하거나 초기화 하기 어렵다.
  • private 생성자로 자식 클래스를 만들기 어렵다.
  • 결론적으로 유연성이 떨어진다.
  • 안티패턴으로 불리기도 한다.

이라고 한다.. 예..

근데

의존관계상 클라이언트가 구체 클래스에 의존한다. DIP를 위반한다.

사실 이거 이해가 되지 않아서 stackoverflow에 물어봤었다.

I can't understand that "Singleton pattern violates DIP."

답변을 내가 이해한대로 생각을 해보자면

singletonService 클래스에서 미리 클래스 인스턴스를 만들어 놓기 때문에 그 인스턴스에 의존할 수 밖에 없다.

즉 같은 interface를 사용하는 다른 객체로 쉽게 갈아끼울 수 없기에 DIP 위반을 한다고 볼 수 있는 것 같다.

👨‍💻 싱글톤 컨테이너

싱글톤 컨테이너는 싱글톤의 장점인 객체 인스턴스를 1개만 생성하게 하면서 앞에서 본 싱글톤의 문제점들은 해결한 컨테이너이다.

그리고 우리가 지금까지 배운 스프링 빈이 이 싱글톤 컨테이너에 의해서 관리된다.

그렇기에

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다

=> 코드 구현 필요 X

  • OCP, DIP 위반

=> OCP, DIP를 준수

이런 장점들이 있다.

그럼 스프링 빈이 정말 싱글톤 컨테이너에 의해서 관리되는지 확인을 해보자.

@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer() {
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    
    //1. 조회: 호출할 때 마다 같은 객체를 반환
    MemberService memberService1 = ac.getBean("memberService", MemberService.class);
    
    //2. 조회: 호출할 때 마다 같은 객체를 반환
    MemberService memberService2 = ac.getBean("memberService",  MemberService.class);
    
    //참조값이 같은 것을 확인
    System.out.println("memberService1 = " + memberService1);
    System.out.println("memberService2 = " + memberService2);
    
    //memberService1 == memberService2
    assertThat(memberService1).isSameAs(memberService2);
}

이 test code를 이용해서 확인하면 같게 나오는 것을 확인할 수 있다.

👨‍💻 싱글톤 컨테이너의 주의할 점

그래 이렇게 보니 싱글톤 컨테이너는 싱글톤의 문제점은 해결하고 메모리 장점은 가져가는 완벽한 존재로 보인다.

하지만! 모든 문제점이 해결되었다는 말은 완전히 맞는 말은 아니다.

위에서 내가 말한 공유지의 비극을 기억하는가?

바로 공유하는 객체의 변수를 한 클라이언트가 변경해서 다른 클라이언트에서 영향을 받는 경우 말이다.

사실 공유를 하는 싱글톤의 정의상 이 문제는 필연적이다.

그렇기에 객체 인스턴스를 하나 생성해서 공유하는 싱글톤 방식은 무상태(stateless)로 설계해야 한다.

무상태란 무엇일까?

무상태(stateless)란

  • 특정 클라이언트가 의존적인 필드가 있으면 안된다.
  • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다
  • 가급적 읽기만 가능해야 한다
  • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다

코드로 예시를 들어보자

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 price;
    }
}

이 코드가 있다고 하자.
클라이언트 A가 1000원 값의 음식을 주문하면 price = 1000
클라이언트 B가 2000원 값의 음식을 주문하면 price = 2000
클라이언트 A가 계산하려고 getPrice를 하면 분명 1000원이 나와야 하는데 B가 그 사이에 price 값을 수정했기에 2000원을 내야하는 참사가 발생하는 것이다.

그래서 price와 같이 공유필드가 있어서는 절대!!! 안된다.

👨‍💻 @Configuration과 싱글톤

자 singleton 방식을 다시 한번 되돌아보자.

생성을 한번만 하고 그걸 공유해서 사용한다.

근데 AppConfig의 코드를 한번 보자.

@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();
    }
}

memberService()를 호출해도 MemoryMemberRepository() 호출
orderService()를 호출해도 MemoryMemberRepository() 호출한다.

띠용? 생성을 두 번 해버리면 객체가 2개가 생성되버리니 싱글톤이 깨진다!

"근데 이렇게 쉽게 깨지게 만들었으면 현업에서 사용되지 않았겠지."

맞다.
memberService를 통한 MemoryMemberRepository와
orderService를 통한 MemoryMemberRepository를 비교해보면 같은 객체임을 확인할 수 있다.

잠깐! 여기서 예외가 있다. (혹시 test 코드로 확인하는데 다르게 나온다?)

그런데 내가 처음에 테스트를 했을 때는 다르게 나왔다.
많이 당황...

그러면 혹시 AppConfig 클래스에 Bean에 static을 붙이지 않았는지 확인을 해보아라.

@Bean
public static MemberRepository memberRepository() {
    System.out.println("call AppConfig.memberRepository");
    return new MemoryMemberRepositroy();
}

이런 식으로 static이 붙어있다면 클래스 로더가 해당 클래스를 메모리에 로드할 때 메모리에 함께 배치되서 언제든지 불러낼 수 있게 설정되기에 Bean에 의해 관리되지 않게 된다.

자세한 내용은 inflearn 질문 게시판을 참고하면 되겠다:)

그러고 테스트를 돌리면 같은 instance임 즉 싱글톤임을 확인할 수 있다.

왜 그럴까? 생성을 두 번을 했는데 왜 그런 것일까?

👨‍💻 @Configuration과 바이트코드 조작

사실 생성 코드가 두 번이 있기는 하지만 실제로는 첫 번째 호출문만 객체를 생성하고 두 번째 호출문 때는 만들어진 객체를 공유한다.

@Test
void configurationDeep() {
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
    
    //AppConfig도 스프링 빈으로 등록된다.
    AppConfig bean = ac.getBean(AppConfig.class);

    System.out.println("bean = " + bean.getClass());
}

자 여기서 우리의 예상대로라면

class hllo.core.AppConfig

이렇게 순수한 클래스처럼 출력이 되어야 하는데 실제로는

class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70

이런 식으로 나오는데

이것은 스프링에서 CGLIB 바이트코드 조작 라이브러리 AppConfig를 상속받은 임의의 다른 클래스이다.

이 임의의 클래스가 싱글톤을 보장해준다.

스프링 빈이 존재하면 존재하는 빈을 반환하고 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 이 AppConfig@CGLIB에서 실행된다.

그래서 싱글톤이 유지되는 것.

참고로 @Configuration을 사용하지 않으면 CGLIB 바이트코드 조작 기술이 사용되지 않아서 호출할 때마다 새로 생성해서 싱글톤이 깨진다.

👨‍💻 후기

싱글톤이란 개념이 처음이어서 매우 신선했고 이것이 수많은 단점 또한 존재하는데 이를 스프링이 다 보완한 기술을 만들어 놓은 것도 대단하다고 느껴졌다.

이제야 스프링이 그냥 자바 코드보다 장점을 지닌다는 것이 느껴지는 것 같다.

profile
잘하진 못할지언정 꾸준히 하는 개발자:)

0개의 댓글