본문 바로가기
  • 개발공부 및 일상적인 내용을 작성하는 블로그 입니다.
Spring basic

스프링 핵심원리 : 기본편 - 웹 어플리케이션과 싱글톤 패턴

by 방구석 대학생 2022. 2. 14.

"인프런의 스프링 핵심원리 - 기본편 강의를 듣고 작성한 글 입니다."

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard

 

스프링 핵심 원리 - 기본편 - 인프런 | 강의

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

웹 어플리케이션과 싱글톤 패턴

- 스프링은 태생이 기업용 온라인 서비스 기술을 지원하기 위해 탄생했다.

- 대부분의 스프링 어플리케이션은 웹 어플리케이션이다. 물론 웹이 아닌 어플리케이션 개발도 얼마든지 개발할 수 있다.

- 웹 어플리케이션은 보통 여러 고객으로부터 동시에 요청이 들어온다.

 

AppConfig DI 컨테이너

스프링이 없는 순수한 자바 코드 AppConfig 방식의 문제점 :

여러 고객이 동시에 요청을 하는 동안 계속해서 요구되는 객체를 만들어 줘야 한다.

 

- 스프링 없는 순수한 DI 컨테이너 테스트

test 폴더 아래 hello.core.singleton 패키지에 SingletonTest 클래스를 만들고 아래와 같이 코드를 작성하자.

- SingletonTest.java

import static org.assertj.core.api.Assertions.*;

public class SingletonTest {

    @Test
    @DisplayName("스프링 없는 순수한 DI 컨테이너")
    void pureContainer(){
        AppConfig appConfig = new AppConfig();

        // 1. 조회 : 호출할 때마다 객체를 생성
        MemberService memberService1 = appConfig.memberService();

        // 1. 조회 : 호출할 때마다 객체를 생성
        MemberService memberService2 = appConfig.memberService();

        // 참조값이 다른 것을 확인
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

        assertThat(memberService1).isNotSameAs(memberService2);
    }
}

결과 : 테스트 통과

memberService1 = hello.core.member.MemberServiceImpl@15bb6bea
memberService2 = hello.core.member.MemberServiceImpl@8b96fde

 

위와 같이 외부로부터 요청이 들어올 때마다 새롭게 MemberService 객체를 만들게 되면 참조값이 서로 다른 객체들이 JVM 에 계속해서 쌓이게 되어 결국 메모리에 부하가 발생하게 된다.

- 기존에 만들었던 스프링 없는 순수한 DI 컨테이너인 AppConfig 는 요청을 할 때 마다 객체를 새로 생성한다.

- 고객 트래픽이 초당 100 이 나오면 초당 100 개 객체가 생성되고 소멸된다. -> 메모리 낭비가 심하다.

- 해결방안은 해당 객체가 딱 1개만 생성되고, 공유하도록 설계하면 된다. -> 싱글톤 패턴

 

 

싱글톤 패턴

- 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.

- 그래서 객체 인스턴스를 2개 잇강 생성하지 못하도록 막아야 한다.

    -  private 생성자를 사용해서 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야 한다.

 

test 폴더 아래 hello.core.singleton 패키지에 SingletonService 클래스를 만들고 아래와 같이 코드를 작성해보자.

- SingletonService.java

public class SingletonService {

    // static 키워드를 통해 자기 자신의 객체를 생성한다.
    // 이렇게 해두면 해당 객체는 클래스 레벨로 올라가기 때문에 유일하게 하나만 존재할 수 있다.
    private static final SingletonService instance = new SingletonService();

    // public 으로 열어서 객체 인스턴스가 필요하면 이 static 메소드를 통해서만 조회하도록 허용한다.
    public static SingletonService getInstance(){
        return instance;
    }

    // 생성자를 private 으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다.
    private SingletonService(){

    }

    public void logic(){
        System.out.println("싱글톤 객체 로직 호출");
    }
}

1. static 영역에 instance 를 미리 하나 생성해서 올려둔다.

2. 이 객체 인스턴스가 필요하면 오직 getInstance() 메소드를 통해서만 조회할 수 있다. 이 메소드를 호출하면 항상 같은 인스턴스를 반환한다.

3. 딱 1개의 객체 인스턴스만 존재해야 하므로, 생성자를 private 으로 막아서 혹시라도 외부에서 new 키워드로 객체 인스턴스가 생성되는 것을 막는다.

 

그럼 이제 싱글톤 패턴을 사용하는 테스트 코드를 추가해보자.

SingletonTest 클래스에 다음과 같은 코드를 추가한다.

- SingletonTest.java

public class SingletonTest {

    @Test
    @DisplayName("스프링 없는 순수한 DI 컨테이너")
    void pureContainer(){
        AppConfig appConfig = new AppConfig();

        // 1. 조회 : 호출할 때마다 객체를 생성
        MemberService memberService1 = appConfig.memberService();

        // 1. 조회 : 호출할 때마다 객체를 생성
        MemberService memberService2 = appConfig.memberService();

        // 참조값이 다른 것을 확인
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);

        assertThat(memberService1).isNotSameAs(memberService2);
    }

    
    // 싱글톤 패턴을 적용시킨 객체 생성 테스트
    @Test
    @DisplayName("싱글톤 패턴을 적용한 객체 사용")
    void singletonServiceTest(){
        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        System.out.println("singletonService1 = " + singletonService1);
        System.out.println("singletonService2 = " + singletonService2);

        assertThat(singletonService1).isSameAs(singletonService2);
        singletonService1.logic();
    }
}

singletonServiceTest() 실행 결과 :
singletonService1 = hello.core.singleton.SingletonService@44a664f2
singletonService2 = hello.core.singleton.SingletonService@44a664f2
싱글톤 객체 로직 호출

 

결과를 보면 서로 같은 객체 참조값을 반환하는 것을 확인할 수 있다.

 

- private 으로 new 키워드를 막아두었다.

- 호출할 때마다 같은 객체 인스턴스를 반환하는 것을 확인할 수 있다.

* 참고 : 싱글톤 패턴을 구현하는 방법은 여러가지가 있다. 여기서는 객체를 미리 생성해두는 가장 단순하고 안전한 방법을 선택했다.

 

* 테스트 코드에서 isEqualTo 와 isSameAs 의 차이점

- isSameAS : == 연산(동등 연산자 - 변수에 적재되어 있는 값 자체를 비교)

- isEqualTo : 자바의 equal() 메소드 연산(메모리 참조값안에 있는 value 값 비교)

 

그러면 혹시 AppConfig 에 작성해둔 스프링 빈 객체들을 이와 같이 싱글톤 패턴 방식으로 다시 작성해주어야 할까?

: 그럴 필요 없다. 스프링 컨테이너를 쓰면 스프링 컨테이너가 기본적으로 객체들을 싱글톤으로 만들어서 관리해주기 때문이다.

 

싱글톤 패턴을 적용하면 고객의 요청이 올 때마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 사용할 수 있다.

하지만 싱글톤 패턴은 다음과 같은 수 많은 문제점들을 가지고 있다.

 

싱글톤 패턴 문제점

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

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

- 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.

- 테스트 하기 어렵다.

- 내부 속성을 변경하거나 초기화 하기 어렵다.

- private 생성자로 자식 클래스를 만들기 어렵다.

- 결론적으로 코드의 유연성이 떨어진다.

- 안티패턴으로 불리기도 한다.

 

그런데 스프링 프레임워크는 싱글톤 패턴의 문제점을 전부 다 해결 하면서도 객체들을 모두 싱글톤으로 관리해준다.

다음번에는 스프링 컨테이너에서의 싱글톤 컨테이너 역할에 대해 알아보자.