Home 스프링과 싱글톤 기초
Post
Cancel

스프링과 싱글톤 기초

이 게시글은 인프런의 유료 강의 스프링 핵심 원리 - 기본편을 수강하며 공부한 내용을 담고있습니다.
저작권에 의해 강의내용에 해당하는 전체 코드는 공개할 수 없습니다.
저작권에 의해 모든 내용을 담을 수 없습니다. 중복 및 반복되는 내용은 생략했습니다.

웹 애플리케이션과 싱글톤

싱글톤 패턴은 동일한 객체는 하나만 존재하도록 한 것이다.
웹 애플리케이션 특성상 여러 사용자가 요청을 할것이다. 그때마다 새로운 객체를 만들수도 있겠지만 이미 생성했던 객체를 재사용을 해도 된다면 자원을 절약할 수 있을 것이다.
예를 들어 100명의 클라이언트가 동시에 memberSerivce 를 요청한다면 100개의 객체가 생성되는 것보다 1개의 memberService 만 생성해 메모리 낭비를 줄일 수 있다. 웹 애플리케이션 특성상 재사용이 가능한 부분도 있고 초당 몇 만번의 요청이 들어오는 것을 단 하나의 객체로 해결할 수 있기 때문에 싱글톤 패턴이 더 유용하게 작용한다.

싱글톤 예시

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class SingletonService {
    // static 과 new 를 이용해 프로그램이 실행되면 객체가 생성되도록 한다.
    // 또, private 과 final 을 이용해 외부에서 값의 변경을 불가능하도록 한다.
    private static final SingletonService instance = new SingletonService();

    // 접근제어자를 private 으로 하여 외부에서 new 를 사용해 객체를 생성할 수 없게 한다.
    private SingletonService() {
    }

    // 객체 인스턴스를 꺼내려면 이 메서드를 호출해야 하니 static 이 있어야한다.
    public static SingletonService getInstance() {
        return instance;
    }

    public void logic() {
        // 이 객체에서 사용될 로직 예를 들어, 이 객체가 memberService 였다면
        // 회원가입 같은 메서드일 것이다.
    }
}

위 코드는 싱글톤 패턴을 보여주는 예시이다.

싱글톤 예시 테스트

사진1
사진2
콘솔창에 두 객체를 출력한 결과 같은 객체임을 알 수 있고, assertThat 으로 비교한 결과도 정상으로 처리되어 녹색 체크(오류없음)으로 된 것을 볼 수 있다.
코드가 간단하므로 이해가 쉬울 것이다. 그러면 이제 원래 만들었던 MemberService, OrderService, DiscountPolicy 등을 전부 싱글톤으로 만들어야 한다.
하지만 그 전에 싱글톤 패턴의 문제점에 대해 짚고 넘어간다.

싱글톤 패턴의 문제점

앞서 장점들에 대해 알아보았으므로 단점도 알아본다.

  • 싱글톤 패턴을 구현하는 코드를 작성해야한다.
  • 의존관계상 클라이언트가 구체클래스에 의존한다. -> DIP 위반
  • DIP 를 위반하므로 OCP 를 위반할 가능성이 높다.
  • (미리 생성되어 있으므로 값을 바꿔가며 유연하게)테스트하기 어렵다.
  • 내부 속성을 변경하거나 초기화하기 어렵다.
  • private 생성자를 사용하기 때문에 자식 클래스를 만들기 어렵다.
  • 결론적으로 유연성이 떨어진다.
  • 안티패턴으로도 불린다.

싱글톤 컨테이너

스프링 컨테이너를 이용하면 싱글톤 패턴의 문제점들을 해결할 수 있다.
스프링 컨테이너는 싱글톤 컨테이너 역할을 하며, 싱글톤 객체를 생성하고 관리하는 기능을 하기때문에 싱글톤 레지스트리라고도 한다.
스프링 컨테이너 안에는 객체가 하나만 존재하므로 싱글톤으로 유지가 된다.
이러한 특성 덕분에 싱글톤 패턴의 단점을 해결하면서 싱글톤을 사용할 수 있다.

  • 싱글톤을 구현하는 코드를 작성할 필요 없음.
  • DIP, OCP 원칙 위반할 가능성 적음

싱글톤 방식의 주의점

음…운영체제나 DB 등의 강의를 수강한 적이 있다면 임계구역 또는 Lock 이라는 개념을 배웠을 것이다.
Lock 을 사용하는 이유는 자원을 공유하면서 생길 수 있는 장애를 예방하기 위해서다.
싱글톤 방식의 주의점도 동일하다.
여기서는 stateful, stateless 라는 개념으로 설명하고 있다.
state 란 뭘까?
여기서 말하는 state(상태)는 내부적으로 보관하는 데이터를 말한다. stateless 는 내부적으로 데이터를 보관하지 않는다는 의미다.
stateful 로 설계를 했다는 것은 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// MyClass 은 싱글톤이다. 싱글톤이기 위한 코드는 생략했다.
public class 주문서비스 {
  private String name; // 제품명 (state 이다)
  private int price; // 제품가격 (state 이다)

  public void 주문하기(String name, int price) {
    this.name = name;
    this.price = price; 
  }

  public int 영수증출력() {
    return price;
  }
}

위와 같은 싱글톤 객체가 있을 때, 두 명의 사용자가 동시간에 서버에 접속해서 서비스를 이용하게 되었다고 생각해보자.
첫번째 이용자는 아메리카노 1500원을 주문하고 두번째 이용자는 카푸치노 2000원을 주문했다.
그리고 첫번재 이용자가 영수증출력하게 되면 영수증에는 2000원이 찍혀있을 것이다.
이러한 문제가 발생하면 안되기 때문에 stateless 로 설계를 해야한다.

stateless 설계

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

@Configuration과 싱글톤

AppConfig 코드를 보면 이상한 점이 있다.
사진3
앞서 스프링 빈을 이 사진으로 설명했었다.
하지만 자바코드를 보면 memberRepository 는 두번 호출되기 때문에 두 개의 객체가 존재해야하고 memberService 객체와 orderService 객체는 서로 다른 memberRepository 를 참조해야한다.
물론 싱글톤이라고 설명을 하니 ‘뭐, 스프링에서 알아서 해주겠지’ 생각을 할 수도 있지만, 그래도 직접 확인해보자.

싱글톤인지 확인

첫번째 확인 방법은 memberService 에서 참조하는 memberRepository 의 값과 orderService 에서 참조하는 memberRepository 의 값, 그리고 스프링 컨테이너에 보관되어 있는 memberRepository 의 값을 대조하는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// MemberServiceImpl.java
public class MemberServiceImpl implements MemberService{
    // (코드 생략)
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}

// OrderServiceImpl.java
public class OrderServiceImpl implements OrderService{
    // (코드 생략)
    public MemberRepository getMemberRepository() {
        return memberRepository;
    }
}

// test > java > 패키지명 > ConfigurationSingletonTest.java 파일 생성
public class ConfigurationSingletonTest {

    @Test
    void configurationTest() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        // 자료형이 Impl 인 이유는 테스트용으로 작성한 getRepository 메서드가 Impl 에만 존재하니까...
        // Impl 을 빼는 것이 좋다고 한다.
        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

        System.out.println("스프링 컨테이너에 있는 memberRepository = " + memberRepository);
        System.out.println("memberService 의 memberRepository = " + memberService.getMemberRepository());
        System.out.println("orderService 의 getMemberRepository = " + orderService.getMemberRepository());

        Assertions.assertThat(memberRepository).isSameAs(memberService.getMemberRepository());
        Assertions.assertThat(memberRepository).isSameAs(orderService.getMemberRepository());
    }
}

사진4
memberRepository 를 호출하는 클래스에 getMemberRepository() 메서드를 추가하고, 테스트 코드에서 호출해서 확인한 결과를 콘솔에서 볼 수 있다.
세 번의 출력을 통해 모두 동일한 memberRepository 객체임을 확인할 수 있다.

memberRepository 호출 횟수 확인

일단 싱글톤으로 유지되는건 확인됐지만, 그래도 의문이 있다.

  1. 스프링 컨테이너가 스프링 빈에 등록하기 위해
    @Bean
    public MemberRepository memberRepository() 를 호출
  2. memberService() 에서 memberRepository() 를 호출
  3. orderService() 에서 memberRepository() 를 호출

자바 코드만 보면 new MemberRepository 를 3 번 호출할텐데, 나머지 두번은 어디가는걸까?? 아니면 스프링의 어떤 방법을 통해 한번만 호출하게 되는 것일까?? 확인해보자.

사진5
사진6
스프링 빈이 등록될 때 마다 콘솔창에 출력하도록 코드를 작성했더니, memberRepository 가 단 한번만 호출되는 것을 확인할 수 있었다.
이로써 자바코드에 적힌 그대로 동작하지 않는 것을 알 수 있다.

@Configuration 과 바이트코드 조작

AppConfig 에 작성된 자바코드가 제대로 동작하지 않는 이유를 알기 위해선 AppConfig 에 붙어있는 @Configuration 에 대해 알아야한다.

1
2
3
4
5
6
7
8
@Test
    void configurationDeep() {
        // new AnnotationConfigApplicationContext 의 매개변수로 전달되면 해당 클래스도 스프링 빈으로 등록된다.
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        AppConfig appConfig = ac.getBean(AppConfig.class);
        System.out.println(appConfig);
    }

사진7
출력결과를 보면 클래스 명이 이상하다. 여태까지 다른 객체들을 확인했을 때는 클래스명에 $$ 가 붙어있는 것을 볼 수 없었다.
그리고 여기서 알 수 있는 점은 AppConfig 클래스 로 생성된 스프링 빈 객체는 실제로 AppConfig 가 아니라 AppConfig$$SpringCGLIB$$0 클래스로 생성되었음을 알 수 있다.

그럼 AppConfig$$SpringCGLIB$$0 클래스는 어디서 튀어나온 녀석일까??
$$ 기호의 의미는 프록시 클래스라고한다. 동적으로 생성된 클래스인 것이다.
Spring 이 CGLIB 라는 바이트코드 조작 라이브러리를 이용해 AppConfig$$SpringCGLIB 라는 클래스를 만들었다고 이해하면된다.
마지막의 $$0 은 클래스를 구별하는 식별자이다(@해시코드와 같은 역할). 이름이 똑같은 프록시 클래스가 여러개 만들어질 수 있기 때문이다. 물론 AppConfig 예시만 봤을 때는 이름이 같은 동적 클래스가 어떻게 여러개 만들어질까…싶지만 말이다.

여기까지 요약하면 다음과 같다.

  1. AppConfig 클래스로 스프링 빈을 만들려고 함
  2. @Configuration 이 붙어 있으므로 AppConfig$$SpringCGLIB$$0 라는 클래스를 새로 만듬
  3. 새로 만든 클래스로 객체를 생성하고 스프링 빈에 등록
  4. 빈 이름은 AppConfig 를 그대로 사용한다.


CGLIB 은 상속기반 프록시이기 때문에 AppConfig 를 상속받은 상태이다.
AppConfig appConfig = ac.getBean(AppConfig.class); 에서 오류가 나지 않는 것을 통해 알 수 있다.

상속과 프록시 클래스의 차이점
상속은 기능을 확장하는 관점에서 바라보는 것이고 프록시는 메소드 호출을 가로채서 추가적인 로직을 수행하는 것이다.
만약 상속이었다면 AppConfig 클래스의 new MemoryMemberRepository() 가 세 번 호출되었어야한다. 하지만 memberRepository() 메서드의 로직을 가로채고 중간에 새로운 로직을 집어 넣어 중복 호출시 한 번만 호출되도록 하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 실제 AppConfig 의 코드
@Bean
public MemberRepository memberRepository() {
  System.out.println("AppConfig 의 memberRepository() 호출됨");
  // 여기만 수정하면 memberService 와 orderService 가 참조하는 DB가 바뀜
  return new MemoryMemberRepository();
}

// AppConfig$$SpringCGLIB$$0 의 코드 상상도
@Bean
public MemberRepository memberRepository() {
  if(memberRepository가 스프링 컨테이너에 등록되어 있다면) {
    return 스프링 컨테이너에서 찾아서 반환;
  }
  else {
    return 기존 로직 수행  반환;
  }
}

아무튼 이렇게 해서 스프링 내부적으로 싱글톤을 유지시켜준다는 점을 알게되었다.



이전 포스팅 : 스프링 컨테이너와 빈
다음 포스팅 : 컴포넌트 스캔

This post is licensed under CC BY 4.0 by the author.

스프링 컨테이너와 스프링 빈

태그 중복 오류 해결