UMC_Back

Week 3_(2)

oyatplum 2024. 4. 29. 17:28

 

 

1. 스프링 컨테이너와 스프링빈

2. 싱글톤 컨테이너

3. 컴포넌트 스캔

 


 

 

저번 포스팅에 이어 싱글톤에 대해 더 자세히 살펴보자.

 

2. 싱글톤 컨테이너

싱글톤 방식의 주의점

객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은 여러 클라이언트가 하나의 동일한 객체 인스턴스를 공유하기 때문에 해당 객체는 상태를 유지(stateful)하게 설계하면 안된다.

즉, 무상태(stateless)로 설계해야 한다!!

 

 

예시로 알아보자.

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를 생성하고

 

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

        statefulService1.order("userA", 10000);
        statefulService1.order("userB", 20000);

        int price = statefulService1.getPrice();
        System.out.println("price = " + price);
    }

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

}

 

이렇게 테스트 코드를 작성했다.

userA가 10000원을 주문했는데 출력 전에 userB가 20000원을 주문하면

원하는 결과 값은 10000원이지만

 

20000원이 나온다.

따라서 객체를 무상태로 설계하자~!!

 

 

 

 

@Configuration과 싱글톤

@Configuration은 사실 싱글톤을 위해 존재한다.

기존에 작성한 AppConfig를 살펴보자.

 

@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService(){
        return new MemberServiceImpl(memberRepository());
    }
    @Bean
    public MemoryMemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    @Bean
    public OrderService orderService(){
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }
    @Bean
    public DiscountPolicy discountPolicy(){
        //return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }
}

 

public MemberService memberService()를 통해 return new MemoryMemberRepository();를 하고

public OrderService orderService()를 통해 또 return new MemoryMemberRepository();를 하는 것을 보았을 때

각각 다른 MemoryMemberRepository가 생성되면서 싱글톤이 깨지는 것처럼 보인다.

 

 

실제로 깨지는 지 확인해보자.

 

//테스트 용도
    public MemberRepository getMemberRepository(){
        return memberRepository;
    }

 

MemberServiceImpl과 OrderServiceImpl에 각각 위의 코드를 작성해주었고

public class ConfigurationSingletonTest {
    @Test
    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);
			  
        MemberRepository memberRepository1 = memberservice.getMemberRepository();
        MemberRepository memberRepository2 = orderService.getMemberRepository();

        System.out.println("memberRepository1 = " + memberRepository1);
        System.out.println("memberRepository2 = " + memberRepository2);
        System.out.println("memberRepository = " + memberRepository);
    }
}

 

이렇게 MemberServiceImpl과 OrderServiceImpl에 getMemberRepository()을 통해서

생성된 MemoryMemberRepository를 확인해보면

 

 

예상과 달리 같은 객체가 반환되는 것을 확인할 수 있다.

 

@Configuration
public class AppConfig {
    @Bean
    public MemberService memberService(){
        System.out.println("AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public MemoryMemberRepository 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();
    }
}

 

이렇게 soutm으로 콜을 찍어서 확인해보면

위와 같은 결과가 나올 것 같지만

 

이렇게 나온다.

왜이럴까~? 아래서 살펴보자.

 

 

 

 

@Configuration과 바이트코드 조작의 마법

 

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

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

 

이렇게 등록된 AppConfig를 출력해보면

 

위와 같이 AppConfig$$~~~ 로 나온다.

오잉 뒤에는 왜 나올까??

 

실제로는 AppConfig가 아니라

AppConfig를 상속받은 임의의 클래스 AppConfig@CGLIB가 appConfig라는 이름으만 빈에 등록이 된다.

 

\AppConfig@CGLIB는 복잡하기 때문에

간단하게 정리하면

  • @Bean이 분튼 매서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다.
  • 덕분에 싱글톤이 보장되는 것이다.\

 

따라서 이전처럼 여러번 같은 빈이 호출되지 않는 것이다.

 

@Configuration을 사용하지 않고 @Bean만 사용하면 싱글톤은 깨진다.

 


정리하자면

 

  • @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
  • 크게 고민하지 말자! 스프링 정보는 항상 @Configuration을 사용하자!!

 

 


 

3. 컴포넌트 스캔

 

컴포넌트 스캔과 의존 관계 자동 주입

컴포넌트 스캔, 의존 관계 자동 주입

  • 지금까지 스프링 빈을 등록할 때 @Bean이나 XML의 <bean> 등을 통해 설정 정보에 직접 등록할 스프링 빈을 나열했다.
  • 등록해야 할 스프링 빈이 수백 개가 되면 못 하겠지!
  • 그래서 스프링은 설정 정보 없어도 작동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공
  • 또 의존 관계도 자동으로 주입하는 @Autowired 라는 기능 제공

위와 같은 이유로 스프링 빈을 등록할 때 컴포넌트 스캔 기능을 사용해보자.

 

@Configuration
@ComponentScan(
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Condition.class)
)
public class AutoAppConfig {
}

 

AutoAppConfig를 새롭게 생성하고 @ComponentScan을 작성해주어야 한다.

이러면 @Bean을 작성하지 않아도 된다.

컴포넌트 스캔을 하게 되면 @Configuration이 붙었던 AppConfig도 함께 등록되기 때문에 지금은 AutoAppConfig가 작동되는 지만 확인해야하므로 위의 코드와 같이 제외시켰다.

 

 

@Component
public class MemoryMemberRepository implements MemberRepository{
...
}
@Component
public class RateDiscountPolicy implements DiscountPolicy{
...
}
@Component
public class MemberServiceImpl implements MemberService{
...
    @Autowired
    public MemberServiceImpl(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
...
}
@Component
public class OrderServiceImpl implements OrderService{
...
    @Autowired
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }
...
}

 

AutoAppConfig에는 AppConfig에서처럼 의존 관계를 주입하지 않고 있으므로 각 클래스의 생성자 위에 @Autowired 어노테이션을 붙여주면 자동으로 의존 관계가 주입된다.

 

 

 

이제 테스트 코드를 작성해보자.

public class AutoAppConfigTest {
    @Test
    void basicScan(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(AutoAppConfig.class);

        MemberService memberService = ac.getBean(MemberService.class);
        assertThat(memberService).isInstanceOf(MemberService.class);
    }
}

 

위와 같이 작성하고 실행하면 정상적으로 AutoAppConfig 클래스가 작동한다.

 

 

  1. @ComponentScan
    1. @ComponentScan 은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록한다.
  2. @Autowired 의존관계 자동 주입
    1. 생성자에 @Autowired를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.

 

 

탐색 위치와 기본 스캔 대상

 

@Configuration
@ComponentScan(
        basePackages = "umc_6th.spring_principles.member",
        excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class
        )
)
public class AutoAppConfig {
}

 

이렇게 basePackages = "umc_6th.spring_principles.member"를 작성해주면 부수적인 라이브러리들까지 찾지 않고 지정해 준 탐색 위치부터 탐색한다.

 

조금 더 복잡한 방법들이 있지만 권장되는 방법은

관례에 따라 프로젝트 루트에 컴포넌트 스캔을 지정하자!!

 

  • @Component
  • @Controller
  • @Service
  • @Repository
  • @Configuration

 

위의 대상들은 모두 컴포넌트 스캐너를 통해 스프링 빈으로 등록된다.

 

 

 

필터

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {

}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyExcludeComponent {
    
}

@MyIncludeComponent
public class BeanA {
}
@MyExcludeComponent
public class BeanB {
}

 

이렇게 MyIncludeComponent와 MyExcludeComponent를 생성하고

include하는 beanA와 exclude하는 beanB를 생성했다.

 

public class ComponentFilterAppConfigTest {

    @Test
    void filterScan() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ComponentFilterAppConfig.class);
        BeanA beanA = ac.getBean("beanA", BeanA.class);
        assertThat(beanA).isNotNull();

        assertThrows(
                NoSuchBeanDefinitionException.class,
                () -> ac.getBean("beanB", BeanB.class)
        );
    }


    @Configuration
    @ComponentScan(
            includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
            excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
    )
    static class ComponentFilterAppConfig{
    }

}

 

그리고 위와 같이 테스트 코드를 작성했다.

includeFilters에 MyIncludeComponent 어노테이션을 추가해서 BeanA가 스프링 빈에 등록된다.

동일하게 excludeFilters에 MyExcludeComponent 어노테이션을 추가해서 BeanB은 스프링 빈에 등록되지 않는다.

 

 

필터에는 다음과 같은 다양한 타입들이 있다.

  • ANNOTATION : 기본값, 애노테이션을 인식해서 동작한다.
  • ASSIGNABLE_TYPE : 지정한 타입과 자식 타입을 인식해서 동작한다.
  • ASPECTJ : AspectJ 패턴 사용
  • REGEX : 정규 표현식
  • CUSTOM : TypeFilter 라는 인터페이스 구현해서 처리

 

 

 

중복 등록과 충돌

1. 자동 빈 등록 vs 자동 빈 등록

2. 수동 빈 등록 vs 자동 빈 등록

 

컴포넌트 스캔에서 같은 빈 이름을 등록하면 어떻게 될까?

 

 

 

1. 자동 빈 등록 vs 자동 빈 등록의 경우

ConflictingBeanDefinitionException 예외가 발생한다.

 

 

 

2. 수동 빈 등록 vs 자동 빈 등록의 경우

public class AutoAppConfig {
        @Bean(name = "memoryMemberRepository")
        MemberRepository memberRepository() {
                return new MemoryMemberRepository();
        }
}

 

기존에 있던 memoryMemberRepository 빈을 그대로 두고 이렇게 수동으로 같은 이름을 가지고 있는 memoryMemberRepository 빈을 생성해서 등록한 뒤 테스트를 돌려보면 오류가 발생하지 않는다.

 

이런 경우, 수동 빈 등록이 우선권을 가지게 된다.

(수동 빈이 자동 빈을 오버라이딩 해버린다.)

 

 

 

 

'UMC_Back' 카테고리의 다른 글

여행기 - 도시, 국가 검색 정리 (전반적인 스프링 코드 이해)  (0) 2024.08.18
Week 3_(1)  (0) 2024.04.28
Week 2  (0) 2024.04.06
Week 1  (0) 2024.03.30