본문 바로가기
JAVA & SPRING/Spring 핵심원리-기본

스프링 핵심 원리 - 4일차(싱글톤 컨테이너)

by 눈오는1월 2023. 7. 28.
728x90

스프링은 애플리케이션 지원을 한다.(백엔드 기술에 쓰이는 것처럼 태생이 그렇다) 

다수의 클라이언트가 memberService에 요청

위 그림처럼 많은 사람들이 서버에 요청을 보내면 요청보낼때마다 객체가 생성되야 한다. 이것을 직접 확인해보자

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

public class SingletonTest {
    @Test
    @DisplayName("스프링 없는 순순한 DI 컨테이너")
    void pureContainer() {
        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);

    }
}

객체 2번 호출

객체가 2번 호출되는 것을 볼 수 있다.

이런식으로 요청이 들어올때마다 객체를 생성하면 메모리낭비가 심하고 효율적이지 못한다.

이러한 문제를 해결하기 위해 싱글톤 패턴을 적용시켜서 문제를 해결할 수 있다.(싱글톤 패턴이란 객체의 인스턴스를 하나만 생성하도록 하는것)

package hello.core.singleton;

public class SingletonService {

    private static final SingletonService instance = new SingletonService(); // 자기자신을 내부에 private로 가지고 있음(static)
    // 자기자신을 생성한 후 인스턴스의 참조를 넣어놓는다.

    public static SingletonService getInstance() {
        return instance;  // 참조를 꺼낼 수 있는 놈은 얘밖에없음 더 이상 생성도 못함
    }
    // 생성자를 private으로 선언해서 외부에서 new 키워드를 사용한 객체 생성을 못하게 막는다.
    private SingletonService() {
    }

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

이런식으로 싱글톤 패턴으로 만들면 해당 클래스 내부에서 인스턴스를 만들고 밖에서는 못만들게 만든다. private으로 차단함

객체를 생성하는데 비용이 1000 이면 가져오는 비용은 1이라고 생각하면된다. 그만큼 생성되는 것을 가져오는것이 비용 절감에 좋음

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

public class SingletonTest {
    @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);
        // sameAs와 Equalto차이 -> same은 == 비교, Equal는 자바의 Equals 비교 메서드 오버라이드

    }
}

호출된 객체가 같음

위 결과 사진을 보면 호출된 객체가 동일하다는 것을 알 수 있다.

 

싱글톤 패턴에는 여러 방식이 있지만 로딩할때 싱글톤 패턴을 적용하는게 제일 낫다(위에서 했던 방식)

 

그럼 이런 생각을 할 수 있다. 우리가 AppConfig에 있는 것들을 다 싱글톤패턴을 적용을 해야하나? 

답은 x이다. 스프링이 알아서 다 해준다.

 

또한 이런식으로 싱글톤패턴을 적용시켰을때의 문제점으로는

1. 코드가 매우 복잡해진다

2. 클라이언트가 구체 클래스에 의존하게 된다 -> DIP위반하게됨

3. 구체 클래스에 의존하게 돼서 OCP를 위반할 확률이 매우매우매우 높음

등등 여러 문제가 있다

 

스프링이 제공하는 싱글톤 패턴을 이용하면 위 단점들을 다 해결할 수 있고 동시에 인스턴스를 하나만 생성할 수 있게된다.->스프링 컨테이너의 사용!!

 

스프링 컨테이너에 스프링 빈이 싱글톤으로 관리가 된다.

(@Bean어노테이션을 쓰면 스프링 컨테이너에 빈이 등록되는 것을 말함)

 

싱글톤 컨테이너는 싱글턴 패턴을 적용안해도 객체 인스턴스를 싱글톤으로 알아서 관리를 해줌-> 싱글톤 단점도 다 해결해주고 ㅎㅎ

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberService;
import hello.core.member.MemberServiceImpl;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

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

public class SingletonTest {
    @Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void springContainer() {

        //AppConfig appConfig = new AppConfig();

        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);

    }
}

위 사진처럼 테스트 후 결과가 객체의 인스턴스가 동일하다는 것을 알 수 있다.

즉, 객체를 공유해서 효율적으로 재사용 한다는 것을 알 수 있음(인스턴스가 한번만 만들어지고 해당 인스턴스를 사용)

이렇게 싱글톤 방식은 효율적으로 매우 좋다 또한 싱글톤 컨테이너를 사용하면, 싱글톤의 단점도 없어진다.

다만 이러한 방식에 엄청 주의해야할점이 있다!(실무에서 몇년마다 한번씩 본다고하는데 걸리면 오류 잡기 쉽지 않다고 한다)

 

싱글톤 패턴(싱글톤 컨테이너) 방식을 사용할대 상태를 유지하게 설계 해서는 안된다. 즉 무상태로 설계를 해야함

 

package hello.core.singleton;

public class StatefulService {

    private int price; // 상태를 유지하는 필드

    public void order(String name , int price) {
        System.out.println("name = " + name + "price = " + price);
        this.price = price; // 여기가 문제!
        //return price; // -> 이런식으로 처리르 해야하암
    }

    public int getPrice() {
        return price;
    }


}

위 코드처럼 하나의 인스턴스가 생성될때 this.price = price를 해버리면 다음 객체를 호출할때도 같은 인스턴스에 값이 덧붙여지는 상황이 발생한다.

package hello.core.singleton;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;

import static org.junit.jupiter.api.Assertions.*;

class StatefulServiceTest {

    @Test
    void statefulServiceSingleton() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = ac.getBean(StatefulService.class);
        StatefulService statefulService2 = ac.getBean(StatefulService.class);

        //ThreadA : A사용자가 10000원 주문
        statefulService1.order("userA",10000);
        //ThreadB: B사용자가 20000원 주문
        statefulService2.order("userB",20000);

        //ThreadA : 사용자A 주문 금액 조회
        int price = statefulService1.getPrice();
        System.out.println("price = " + price);

        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
    }

    static class TestConfig {
        @Bean
        public StatefulService statefulService() {
            return new StatefulService();
        }
    }

}

위 테스트를 실행했을때 statefulService1 에는 10000이 들어갔지만 2에서 20000으로 덧붙여서 써져서 20000과 같은지 테스트를하면 같다고 나온다(틀렸다고 발생을 해야 정상이다)

즉 공유필드는 조심하게 사용을 해야한다. 

 

그런데 이제 이상한 점 하나가 있는데 우리 AppConfig 코드를 보면 이 자바코드가 싱글톤으로되어있나를 생각해봐야한다.

package hello.core;

import hello.core.discount.DiscountPolicy;
import hello.core.discount.FixDiscountPolicy;
import hello.core.discount.RateDiscountPolicy;
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;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration  // 구성정보라는 의미
public class AppConfig {
    // 펙토리 메서드 라는 방식임 (직접 등록이 아닌) -> 뭔가 외부에서 메서드를 호출해서 하는 방식


    @Bean //스프링컨테이너에 등록
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    @Bean
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    @Bean
    public DiscountPolicy discountPolicy() {
        //return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }


}

위 코드는 에전에 만들었언 AppConfig 클래스 코드이다. 지금 되게 이상한 상태이다. 코드만 보면 memberRepository 메소드가 여러번 호출을 하기때문에 싱글톤이 적용하지 않는다.

그러나 실제로 테스트를 해보면

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.member.MemberRepository;
import hello.core.member.MemberServiceImpl;
import hello.core.order.OrderServiceImpl;
import org.assertj.core.api.Assert;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import java.lang.annotation.Annotation;

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

public class ConfigurationSingletonTest {

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

        // Test용도 메소드 꺼낼려고 구체타입클래스 사용 -> 원래는 이러면 안좋음
        MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);
        OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);
        MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

        MemberRepository memberRepository1 = memberService.getMemberRepository();
        MemberRepository memberRepository2 = orderService.getMemberRepository();

        System.out.println("memberService -> memberRepository = " + memberRepository1);
        System.out.println("orderService -> memberRepository = " + memberRepository2);
        System.out.println("memberRepository = " + memberRepository);

        assertThat(memberService.getMemberRepository()).isSameAs(memberRepository);
        assertThat(orderService.getMemberRepository()).isSameAs(memberRepository);
    }
        @Test
    void configurationDeep() {
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);
        AppConfig bean = ac.getBean(AppConfig.class);

        System.out.println("bean = " + bean.getClass()); // .getClass 를 호출해야지 클래스 타입을 확인 할 수 있음

    }

}

테스트 결과 동일한 인스턴스만 사용된다.

 

이러한 이유는 바로 @Configuration 어노테이션 때문이다. 이 어노테이션이 붙은 AppConfig는 사실 우리가 실제로 사용하고 있지않고 AppConfig를 상속받는 임의의 다른 클래스를 빈으로 등록하고 사용하는 컷이다.

위 아래 테스트를 실행했을때 이런식으로 AppConfig.만 나오는것이 아닌 뒤에 따라붙는 글자들이 그것을 의미한다.

@Configuration 에 다른 클래스가 이런식으로 우리는 알지 못하게 싱글톤 처럼 되게끔 해주는 것!

아마 예상하자면 그 클래스는 스프링 컨테이너에 등록되어있으면 해당값을 반환하고 없으면 생성하는 역할을 하는 것이다. 그덕에 싱글톤이 보장된다.

 

그래서 @Configuration을 사용안하고 @Bean만 사용하게되면 스프링 빈에는 등록되지만 싱글톤이 적용되지 않는 것을 확인할 수 있다.

 

결론! 스프링 설정 정보에는 항상 @Configuration 어노테이션을 사용해야한다.

728x90