728x90

인프런 김영한 님의 강의를 참고했습니다.

 

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

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

www.inflearn.com

오늘은 싱글톤에 대해서 공부한다.

 

만약 스프링 없이 AppConfig를 사용한다면

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.customer.CustomerService;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;

public class SingletonTest {

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

        CustomerService customerService1 = appConfig.customerService();
        CustomerService customerService2 = appConfig.customerService();

        System.out.println("CustomerService1 = " + customerService1);
        System.out.println("CustomerService2 = " + customerService2);

        Assertions.assertNotSame(customerService1, customerService2);
    }
}

AppConfig를 요청할 때마다 객체를 새로 만들어야 한다.

그럴 때는 객체를 딱 1개만 생성하고 공유하는 싱글톤을 사용해야 한다.

 

package hello.core.singleton;

public class SingletonService {
    
    //1. static 영역에 객체를 딱 1개만 생성해둔다.
    private static final SingletonService instance = new SingletonService();
    
    
    //2. public 으로 객체가 필요하면 해당 메서드로 받아갈 수 있도록 만든다.
    public static SingletonService getInstance(){
        return instance;
    }
    
    //3. 생성자를 private 으로 만들어서 생성할 수 없도록 막는다.
    private SingletonService(){
    }
    
    public void printMethod(){
        System.out.println("싱글톤 객체 로직 호출");
    }
}

이렇게 생성자를 private으로 막고 class 내의 메서드를 이용해 객체를 받아갈 수 있도록 만드는 것을 싱글톤이라고 한다.

 

이 싱글톤을 테스트해보면

    @Test
    @DisplayName("싱글톤 패턴을 적용한 객체 사용")
    public void singletonServiceTest(){

        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

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

        Assertions.assertSame(singletonService1, singletonService2);
    }

두 객체가 같은 것을 확인할 수 있다.

하지만 싱글톤을 사용하면 싱글톤을 구현하는 코드 자체를 만들어야 하고, 클라이언트가 추상화가 아닌 싱글톤 구체 클래스에 의존하기에 DIP를 위반한다 등등 의 문제가 있기 때문에 다른 방법을 사용한다.

 

싱글톤의 문제를 해결한 방법이 스프링 빈으로 만든 빈이다.

스프링 컨테이너는 싱글톤 코드를 따로 작성하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다.

그리고 DIP, OCP 등의 원칙을 위반하지 않고 코드를 작성할 수 있다.

    @Test
    @DisplayName("스프링 컨테이너와 싱글톤")
    void springContainer(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        
        CustomerService customerService1 = applicationContext.getBean("customerService", CustomerService.class);
        CustomerService customerService2 = applicationContext.getBean("customerService", CustomerService.class);

        System.out.println("customerService1 = " + customerService1);
        System.out.println("customerService2 = " + customerService2);
        
        Assertions.assertSame(customerService1, customerService2);
    }

이렇게 싱글톤 코드를 따로 작성하지 않아도

같은 객체를 공유하는 것을 볼 수 있다.

 

이렇게 좋은 싱글톤에도 문제점이 있다.

바로 공유를 하기 때문에 생기는 문제인데, 싱글톤은 상태를 유지하게 설계하면 안된다.

특정 클라이언트에서 값을 변경할 수 있도록 만들면 안되며, 가급적 읽기만 가능해야 한다.

package hello.core.singleton;

public class StatefulService {

    private int pay;

    public void order(String name, int pay){
        System.out.println("name = " + name + " price = " + pay);
        this.pay = pay;
    }
    
    public int getPay(){
        return pay;
    }
}

이런 class를 만들고

package hello.core.singleton;

import org.junit.jupiter.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;

public class StatefulServiceTest {

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

    @Test
    void statefulServiceSingleton(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(TestConfig.class);
        StatefulService statefulService1 = applicationContext.getBean("statefulService", StatefulService.class);
        StatefulService statefulService2 = applicationContext.getBean("statefulService", StatefulService.class);

        statefulService1.order("A", 1000);
        statefulService2.order("B", 2000);

        Assertions.assertNotEquals(1000, statefulService1.getPay());
    }
}

이렇게 싱글톤으로 공유를 해보면 A에서 1000이 나와야 하지만 나오지 않는 것을 볼 수 있다.

A가 값을 얻는 중간에 B에서 값을 변경했기 때문이다.

이렇게 싱글톤은 값을 변경할 수 있도록 설정을 하면 안된다.

 

다시 AppConfig 코드를 보자.

package hello.core;

import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.FixDiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import hello.core.customer.CustomerRepository;
import hello.core.customer.CustomerService;
import hello.core.customer.CustomerServiceImpl;
import hello.core.customer.MemoryCustomerRepository;
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 CustomerService customerService(){
        return new CustomerServiceImpl(customerRepository());
    }


    @Bean
    public OrderService orderService(){
        return new OrderServiceImpl(customerRepository(), discountPolicy());
    }

    @Bean
    public CustomerRepository customerRepository(){
        return new MemoryCustomerRepository();
    }

    @Bean
    public DiscountPolicy discountPolicy(){
        //return new FixDiscountPolicy();
        return  new RateDiscountPolicy();
    }
}

CustomerService에서 customerRepository를 호출한다.

이 customerRepository는 new MemoryCustomerRepository()를 호출한다.

orderService도 customerRepository를 호출한다.

이 customerRepository도 new MemoryCustomerRepository()를 호출한다.

 

이렇게 MemoryCustomerRepository를 계속 호출하면 싱글톤이 깨지는 것처럼 보인다.

 

한번 테스트를 해보자.

CustomerServiceImpl과 orderServiceImpl에 가지고 있는 CustomerRepository를 반환하는 메서드를 추가한다.

package hello.core.customer;

public class CustomerServiceImpl implements CustomerService{

    private final CustomerRepository customerRepository;

    public CustomerServiceImpl(CustomerRepository customerRepository) {
        this.customerRepository = customerRepository;
    }

    @Override
    public void join(Customer customer) {
        customerRepository.save(customer);
    }

    @Override
    public Customer findCustomer(Long id) {
        return customerRepository.findById(id);
    }

    public CustomerRepository customerRepository(){
        return customerRepository;
    }
}
package hello.core.order;

import hello.core.Discount.DiscountPolicy;
import hello.core.customer.Customer;
import hello.core.customer.CustomerRepository;

public class OrderServiceImpl implements OrderService{

    private final CustomerRepository customerRepository;
    //private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(CustomerRepository customerRepository, DiscountPolicy discountPolicy) {
        this.customerRepository = customerRepository;
        this.discountPolicy = discountPolicy;
    }

    @Override
    public Order createOrder(Long id, String itemName, int itemPrice) {
        Customer customer = customerRepository.findById(id);
        int discountPrice = discountPolicy.discount(customer, itemPrice);

        return new Order(id, itemName, itemPrice, discountPrice);
    }

    public CustomerRepository customerRepository(){
        return customerRepository;
    }
}

 

그리고 이 class 들이 가지고 있는 customeRepository가 동일한지 확인해보자.

package hello.core.singleton;

import hello.core.AppConfig;
import hello.core.customer.CustomerRepository;
import hello.core.customer.CustomerServiceImpl;
import hello.core.order.OrderServiceImpl;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ConfigurationSingletonTest {

    @Test
    void ConfigurationTest(){
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

        CustomerServiceImpl customerService = applicationContext.getBean("customerService", CustomerServiceImpl.class);
        OrderServiceImpl orderService = applicationContext.getBean("orderService", OrderServiceImpl.class);
        CustomerRepository customerRepository = applicationContext.getBean("customerRepository", CustomerRepository.class);

        Assertions.assertSame(customerService.customerRepository(), customerRepository);
        Assertions.assertSame(orderService.customerRepository(), customerRepository);
    }
}

이 테스트 코드를 실행해보면 모두 동일한 인스턴스를 반환하는 것을 볼 수 있다.

 

이번엔 AppConfig에 코드를 추가해 생성자가 호출될 때마다 로그를 남기도록 해보자.

package hello.core;

import hello.core.Discount.DiscountPolicy;
import hello.core.Discount.FixDiscountPolicy;
import hello.core.Discount.RateDiscountPolicy;
import hello.core.customer.CustomerRepository;
import hello.core.customer.CustomerService;
import hello.core.customer.CustomerServiceImpl;
import hello.core.customer.MemoryCustomerRepository;
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 CustomerService customerService(){
        System.out.println("call AppConfig.customerService");
        return new CustomerServiceImpl(customerRepository());
    }


    @Bean
    public OrderService orderService(){
        System.out.println("call AppConfig.orderService");
        return new OrderServiceImpl(customerRepository(), discountPolicy());
    }

    @Bean
    public CustomerRepository customerRepository(){
        System.out.println("call AppConfig.customerRepository");
        return new MemoryCustomerRepository();
    }

    @Bean
    public DiscountPolicy discountPolicy(){
        //return new FixDiscountPolicy();
        return  new RateDiscountPolicy();
    }
}

이렇게 작성을 하고 테스트를 실행하면

 

call AppConfig.customerService
20:09:15.392 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'customerRepository'
call AppConfig.customerRepository
20:09:15.393 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'orderService'
call AppConfig.orderService
20:09:15.394 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'discountPolicy'

 

이렇게 call은 customerService, orderService, customerRepository 각각 한번씩, 총 3번 실행되는 것을 볼 수 있다.

 

자바 코드 상으로는 customerRepository가 3번 실행되어야 하지만 스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다.

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

        AppConfig bean = applicationContext.getBean(AppConfig.class);

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

        AppConfig appConfig = new AppConfig();

        System.out.println("AppConfig = " + appConfig.getClass());

    }

이렇게 스프링 빈을 조회해보면

 

bean = class hello.core.AppConfig$$SpringCGLIB$$0
AppConfig = class hello.core.AppConfig

 

빈으로는 AppConfig가 아니라 SpringCGLIB가 더 붙어 컨테이너에 등록이 되게 된다.

 

스프링이 해당 class를 상속받는 다른 클래스를 만들고 다른 클래스를 스프링 빈으로 등록한 것이다.

그렇기에 스프링이 싱글톤이 보장되도록 해준다.

@Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 업승면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다.

'백엔드 > 스프링' 카테고리의 다른 글

스프링 10일차  (0) 2023.02.11
스프링 9일차  (0) 2023.02.10
스프링 7일차  (0) 2023.02.07
스프링 6일차  (0) 2023.02.06
스프링 5일차  (0) 2023.02.04

+ Recent posts