본문 바로가기

TIL

[2022-1-12] 스프링 핵심원리 - 기본편 : 싱글톤 컨테이너

스프링 핵심원리 - 기본편 : 싱글톤 컨테이너

웹 애플리케이션과 싱글톤

스프링을 사용하지 않은 순수한 DI컨테이너 AppConfig를 만들었다.

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

    // 호출할때마다 객체 생성
    MemberService memberService1 = appConfig.memberService();
    MemberService memberService2 = appConfig.memberService();

    //참조값이 다른 것을 확인
    Assertions.assertThat(memberService1).isNotSameAs(memberService2);
}

meberService()가 호출될때마다 새로운 객체를 생성하므로 meberservice1 과 meberService2는 공유되지않는 다른 인스턴스이다.

 

싱글톤 컨테이너

순수한 자바의 컨테이너와는 다르게 스프링컨테이너는 싱글톤으로 Bean 객체를 생성하고 등록한다. 스프링은 기존 싱글톤이가지는 문제점을 해결하고 장점만을 가진 싱글톤 객체를 생성하고 관리한다.

 

- 기존 싱글톤의 문제점

  1. 싱글톤 패턴을 구현하는 코드가 많이 들어감
  2. 구현객체를 클라이언트에서 직접 사용(의존)하여 DIP 위반, OCP 위반 가능성 높음 
  3. 테스트 힘듬 
  4. 자식 클래스 만들기 힘듬

스프링이 싱글톤으로 인스턴스를 관리하기 때문에 개발자는 싱클톤 패턴으로 클래스를 구현하지 않는다. 때문에 싱글톤 패턴 구현으로 오는 문제점들이 발생하지 않는다. 사용할 때 직접적으로 구현객체를 사용하는 것이아니라 의존관계 주입을 통해 사용되기 때문에 스프링이 하나의 인스턴스를 유지시켜주면서 오는 싱글톤의 장점만을 얻을 수 있는것이다. 아래 코드는 스프링 컨테이너에 등록된 meberService 빈이 하나의 객체만 생성되기 떄문에 조회한 meberService1 과 meberService2 의 참조값이 같음을 보여주는 테스트코드다. 

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

    ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

    MemberService memberService1 = ac.getBean("memberService", MemberService.class);
    MemberService memberService2 = ac.getBean("memberService", MemberService.class);

    //참조값이 같음을 확인
    Assertions.assertThat(memberService1).isSameAs(memberService2);
}

 

싱글톤 방식의 주의점

상태를 가지면 안된다. -> 특정 클라이언트가 값을 변경할 수 있는 필드를 가지면 안된다.

싱글톤은 하나의 인스턴스를 가지기 때문에 클라이언트가 필드의 값을 변경하게 되면 다른 클라이언트는 변경된 필드값을 조회하게 된다. 다른 클라이언트들 끼리의 필드 값 변경은 각각의 클라이언트들에게 서로 예측 불가능하다. 아래는 상태를가지는 StatefulService 클래스이다.

public class StatefulService {

    private int price;

    public void order(String name,int price){
        System.out.println("name = " + name + ", price = " + price);
        this.price = price; //문제 발생
    }

    public int getPrice() {
        return price;
    }
}

StatefulService 클래스는 order() 메소드를 통해 변경가능한 멤버변수 price를 가지고 있다. 이는 값을 변경할 수 있는 필드이다. 아래 코드에서 이로인한 문제점을 확인할 수 있다.

class StatefulServiceTest {

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

        StatefulService statefulService1 = ac.getBean("statefulService", StatefulService.class);
        StatefulService statefulService2 = ac.getBean("statefulService", StatefulService.class);

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

        //Tread A: 사용자A가 주문 가격 조회
        int price = statefulService1.getPrice();
        Assertions.assertThat(price).isNotEqualTo(10000);
    }

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

order() 메소드는 멀티스레드 환경에서 호출되었다고 가정한다. 코드처럼 statefulService1 과 statefulService2는 각각 다른 지역변수에 StatefulService 객체를 담고있지만 Spring 컨테이너를 통해 같은 빈 객체를 참조한다. 떄문에 userA의 order() 호출에는 StatefulService의 price가 10000, userB의 호출에는 price가 20000으로 변한다. 

 

그 후 getPrice() 메소드를 userA 가 호출하면, 10000이 반환될 것이라고 기대했던 것에 반해 20000을 반환 받는다. userB에 의해 필드가 변경되었기 때문이다. 클라이언트가 변경 가능한 필드를 가지게 되면 이와같은 문제들이 발생할 수 있다.

 

@Configuration과 싱글톤과 바이트코드 조작 마법

Spring@Configuration 바이트코드 조작을 통해 스프링 빈이 싱글톤이 되도록 보장해준다. 아래는 AppConfig 클래스의 코드이다.

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

메소드들을 보면 memberService(), orderService() 에서 각각 memberRepository()를 호출한다. 만약 이코드가 그대로 실행되면 인스턴스는 하나로 유지되지 못하고 여러개의 빈이 등록되게 될 것이다. @Configuration은 인스턴스가 하나만 생성될 수 있도록 바이트코드 조작을 통해 새로운 클래스를 만든다. 이클래스는 내가 짠 클래스에서 만약 이미 생성된 동일한 클래스의 빈이 있다면 새로운 빈을 생성하지 않도록하는 코드를 가진 새로운 AppConfig 클래스를 만들고 빈에 등록한다. 즉, 빈에 등록되는 AppConfig는 순수하게 내가 짠 코드가 아니라 스프링에 의해 조작되어 새로 만들어진 클래스이다. 이는 아래 코드를 통해서 확인할 수 있다.

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

    //멤버서비스의 클래스
    Assertions.assertThat(MemberServiceImpl.class).isEqualTo(ac.getBean(MemberService.class).getClass());
    
    //AppConfig 의 클래스
    Assertions.assertThat(AppConfig.class).isNotEqualTo(ac.getBean(AppConfig.class).getClass());
}

위의 테스트 코드처럼 AppConfig아닌 다른 빈의 클래스는 구현클래스와 같다. 하지만 AppConfig는 내가 구현한 클래스와 빈에 등록된 빈의 클래스와 다르다. 이는 스프링이 싱글톤을 가능케하는 AppConfig 클래스를 새로만들어 빈에 등록했기 때문이다. 

그 외 : AssertJ에서 isEqualTo 와 isSameAs 차이

AssertJ에 isEqualTo 와 isSameAs 의 차이를 조사했다. 

isSameAs : == 을 통해 참조값 조사

isEqualTo : 객체의 값이 같은지 조사

 

이를 아래와 같은 테스트코드를 작성해 테스트 해보았다.

@Test
@DisplayName("isEqualTo vs isSameAs")
void compareAssertMethod(){
    String str1 = "abc";
    String str2 = new String("abc");

    Assertions.assertThat(str1).isEqualTo(str2);
    Assertions.assertThat(str1).isNotSameAs(str2);
}

str1 과 str2 는 둘다 "abc" 라는 같은 값을 가지지만 str2는 생성자를 통해 생성되어 str1가 별개의 메모리 영역에 존재하는 다른 객체이다. 때문에 isEqualTo() 로 이 둘을 비교하면 통과하지만 둘의 참조값은 다름으로 isNotSameAs()를 사용해야지 통과된다.(isSameAs는 통과X)

 

프로그래머스 코딩테스트 연습문제 - 최대공약수와 최소공약수

최대공약수를 구하는 재귀코드와 이를통해 최소공약수를 구하는 공식을 알고있으면 아주 빠르게 풀 수 있는 문제였다. 직접 손으로 적어가면서 흐름을 적어보긴 했는데 이정도는 그냥외우는게 좋을 것 같다 코드는 다음과같다.

class Solution {
    public int[] solution(int n, int m) {
        int gcd = gcd(n, m);
        return new int[]{gcd, n * m / gcd};
    }

    private int gcd(int a, int b){
        if (b == 0) {
            return a;
        }
        else{
            return gcd(b, a % b);
        }
    }
}

최대공약수를 반환하는 메소드 gcd는 재귀함수로 구현되어 있다. a 와 b의 대소는 상관없다. b로 들어가는 매개변수는 다음번엔 a로 들어간다. 최소공배수는 원래 최초 두 수의 곱 나누기 최대공약수이다.

결론 및 느낀 점

스프링에등록되는 빈의 주소값들을 직접 찍어보면서 어떻게 Spring이 빈을 관리하는지 이해할 수 있게되어 앞으로 어떻게 코드를 짜야하는지에 대한 방향성을 조금더 잡을 수 있었던거 같다. 이제 조만간 개인프로젝트를 시작하여 이렇게 공부한 내용들을 바탕으로 코드를 작성하고, 계속 공부하면서 리팩토링 해 나갈 예정인데 재밌을 것 같다 얼른 하고싶다!!