Java Spring Core

di_request

웹 어플리케이션의 경우, 여러 명의 client가 동시에 요청을 하고 server은 이에 대해 객체를 생성하고 응답해야한다.

Java Style DI Container

Test Code

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

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

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

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

    assertThat(memberService1).isNotSameAs(memberService2);
}

Results

memberService1 = hello.core.member.MemberServiceImpl@341b80b2
memberService2 = hello.core.member.MemberServiceImpl@55a1c291

위와 같이 순수 Java로 구현한 코드에서 매번 객체를 생성하게 하게 되면, 매번 새로운 객체를 만들어 내게 된다.

이런 식으로 매번 고객에 요청함에 따라 객체를 새로 만들게 되면 메모리 낭비가 심하게 된다. –> 여기서 나온 개념이 바로 Singleton Pattern이다.

Singleton

객체를 오직 1개만 생성해서 이를 client 단에서 공유한다.

Java based Singleton Pattern

순수 자바 기반으로 싱글톤 패턴을 구현하기 위해 private 생성자를 이용한다.

package hello.core.singleton;

public class SingletonService {
    private static final SingletonService instance=new SingletonService();

    public static SingletonService getInstance(){
        return instance;
    }
    //Private 생성자를 만들게 되면 클래스 외부에서 객체 생성이 불가능하다.
    private SingletonService(){

    }
    public void logic(){
        System.out.println("Logic Executed");
    }

}

이렇게 되면, 오직 객체는 1개만 존재하며, new을 통한 새로운 객체의 생성을 막을 수 있다.

Test code

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

Results

singletonService1 = hello.core.singleton.SingletonService@534df152
singletonService2 = hello.core.singleton.SingletonService@534df152

보면 동일 객체를 참조하고 있음을 알 수 있다.

하지만 위와 같은 싱글톤 패턴은 여러 단점을 가지고 있다.

Shortcomings

  • 싱글톤 패턴 구현을 위한 코드가 많이 필요하다
  • 객체 참조시, 구체 클래스에 대해서 getInstance를 해야되서 OCP,DIP 위배
  • Test 어려움.. 등 여러 가지 문제가 존재한다.

하지만, Spring Container은 싱글톤 패턴 문제가 가지는 단점들을 해결하면서, 객체를 싱글톤으로 관리한다.

Singleton Container

사실 SpringContainer는 각각의 객체들을 오직 1개씩만 가지고 있는 Singleton Container이다.

Verification

Test Code

@Test
@DisplayName("스프링 컨테이너와 싱글톤")
void springContainer(){
    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    //조회 할때 마다 객체 생성
    MemberService memberService1 = ac.getBean("memberService", MemberService.class);

    //조회 할때 마다 객체 생성
    MemberService memberService2 = ac.getBean("memberService", MemberService.class);

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

    assertThat(memberService1).isSameAs(memberService2);
}

Results

memberService1 = hello.core.member.MemberServiceImpl@71e9ebae
memberService2 = hello.core.member.MemberServiceImpl@71e9ebae

보면, spring container을 통해 가져온 Spring Bean이 모두 동일한 객체임을 확인할 수 있다.

singleton_container

spring container는 위 처럼 MemberService에 대한 객체를 오직 1개만 생성해서 관리하고 있음을 알 수 있다.

Stateless Classes

싱글톤 방식의 객체 사용을 위해서는 해당 객체는 state를 가져서는 안된다. 즉, 어떤 개인의 정보를 나타내는 변수를 가지고 있어서는 안된다. 사용자로 하여금 정보의 수정을 해서는 안되고, 오직 조회를 위해서는 사용해야 된다.

싱글톤 방식에서 Stateful 하게 설계하면 아래와 같은 문제가 발생한다.

Test Code

package hello.core.singleton;
public class StatefulService {
    //state을 저장하는 변수
    private int price;
    
    public void order(String name,int price){
        System.out.println("name = " + name + " price= " + price);
        //state에 대해 조작하는 부분
        this.price=price;
    }

    public int getPrice() {
        return price;
    }
}

```java
@Test
void statefuleServiceSingleton(){
    ApplicationContext ac = new AnnotationConfigApplicationContext(TestConfig.class);

    StatefulService statefulService1 = ac.getBean(StatefulService.class);
    StatefulService statefulService2 = ac.getBean(StatefulService.class);

    //A 10000원 주문
    statefulService1.order("userA",10000);
    //B 20000원 주문
    statefulService2.order("userB",20000);

    //A 주문 조회
    int price=statefulService1.getPrice();
    System.out.println("price = " + price);

    assertThat(price).isEqualTo(20000);
}

static class TestConfig{

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

위의 statefulService1, statefuleService2는 StatefuleService 객체를 참조하고 있다.

우선 userA가 먼저 10000을 주문을 하고, 그 다음 userB가 20000원을 주문한다.

예상한대로라면, staefulService1에 저장되어 있는 값은 10000이어야 한다. 하지만, 실제로 저장되어있는 값은 20000원이다.

이는 userA, userB가 참조하고 있는 StatefulService 객체가 동일한 객체이여서 발생하는 문제이다. userA가 주문 하면서 10000이 state에 기록되지만, userB가 주문함에 따라 20000으로 대체된다.

따라서 stateful하게 짜게되면 원치 않는 문제가 발생할 수 있기 때문에 항상

singleton pattern에서는 stateless 한 클래스를 설계해야한다.

@Configuration

Suspects

우선 AppConfig을 살펴보자

@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService(){
        System.out.println("AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository() {
        System.out.println("AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }
    @Bean
    public OrderService orderService(){
        System.out.println("AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    @Bean
    public DiscountPolicy discountPolicy() {
        /*return new FixDiscountPolicy();*/
        return new RateDiscountPolicy();

    }
}

MemberService 객체를 생성하기 위해서는 memberRepository()를 호출해야하고 이에 따라 MemoryMemberRepository 객체가 생성되고 하지만, OrderService 객체를 생성하는 과정에서 또다시, memberRepositry()를 호출하고, 또 새로운 MemoryMemberRespository 객체를 생성하게 된다.

자바 코드로만 봤을때는 MemoryMemberRepository 객체가 2개 생기는 것 처럼 보인다. 실제로도 그럴까?

Test Code

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

      MemberServiceImpl memberService = ac.getBean("memberService", MemberServiceImpl.class);

      OrderServiceImpl orderService = ac.getBean("orderService", OrderServiceImpl.class);

      MemberRepository memberRepository = ac.getBean("memberRepository", MemberRepository.class);

      System.out.println("MemberService -> MemberRepository = " + memberService.getMemberRepository());
      System.out.println("OrderService -> MemberRepository = " + orderService.getMemberRepository());
      System.out.println("memberRepository = " + memberRepository);

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

  }

MemoryRepository 객체와, MemberService가 가지고 있는 MemberRepository객체와, OrderService 객체가 가지고 있는 MemberRepository 객체를 확인해본다.

Results

MemberService -> MemberRepository = hello.core.member.MemoryMemberRepository@51549490
OrderService -> MemberRepository = hello.core.member.MemoryMemberRepository@51549490
memberRepository = hello.core.member.MemoryMemberRepository@51549490

확인해보니 모두 동일한 객체를 생성하는 것을 알 수 있다.

실제 호출도 한번씩만 되는 지 확인해보기 위해 AppConfig 파일에 객체 생성을 위한 메소드를 실행시 출력되는 로그를 확인해보자

Results

AppConfig.memberService
AppConfig.memberRepository
AppConfig.orderService

각각 1번씩 호출되는 것을 확인할 수 있다.

여기서 알 수 있는 것은 Spring 에서 AppConfig 파일을 그대로 실행하지 않고 따로 조작을 한다는 것을 알수 있다.

AppConfig Test

Spring Container에서는 AppConfig 또한 Spring Bean으로 관리한다.

Test Code

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

    AppConfig bean = ac.getBean(AppConfig.class);
    System.out.println("bean.getClass() = " + bean.getClass());

}

원래 대로라면 AppConfig는 hello.core.AppConfig class이다.

Results

hello.core.AppConfig$$EnhancerBySpringCGLIB$$ecabb706

하지만 위와 같은 클래스가 나온다.

CGLIB Class

사실 Spring에서는 Spring Bean으로 등록할 때, 클래스를 바로 등록하지 않는다. 위 처럼 특정 조작을 한후 새로운 형태의 class로 등록한다.

cglib_class cglib_class_logic

CGLIB class의 내부 로직을 살펴보면 singleton 객체를 유지하기 위한 로직이 구현되어있음을 예상할 수 있다.

Non @Configuration

@Configuration annotation을 제거하면 Spring Container은 어떤 식으로 구성되었을까?

AppConfig

//@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService(){
        System.out.println("AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public MemberRepository memberRepository() {
        System.out.println("AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }
    @Bean
    public OrderService orderService(){
        System.out.println("AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    @Bean
    public DiscountPolicy discountPolicy() {
        /*return new FixDiscountPolicy();*/
        return new RateDiscountPolicy();

    }
}

Results

AppConfig.memberService
AppConfig.memberRepository
AppConfig.memberRepository
AppConfig.orderService
AppConfig.memberRepository

호출 스택을 확인해보면 memberRepository()가 중복으로 실행된 것을 확인할 수 있고

MemberService -> MemberRepository = hello.core.member.MemoryMemberRepository@18317edc
OrderService -> MemberRepository = hello.core.member.MemoryMemberRepository@4e0ae11f
memberRepository = hello.core.member.MemoryMemberRepository@238d68ff

MemberRepository 객체 모두 다른 인스턴스임을 확인할 수 있다.

이를 통해 @Configurataion이 싱글톤 패턴을 보장해주는 것을 알 수 있다.

References

link: inflearn

link:springcore

link:spring_framework

댓글남기기