BeanDefinition / Spring(Singleton) Container / @Configuration / @ComponentScan / DI 주입 방법 (항해일지 31일차)

김형준·2022년 6월 8일
0

TIL&WIL

목록 보기
31/45

1. 학습일지

  • 모든 출처는 인프런 김영한님의 스프링 핵심 원리 강의에 있습니다.

1) BeanDefinition

  • BeanDefinition은 빈 설정 메타정보라고 한다.

  • 위 그림과 같이 스프링 컨테이너는 자바 코드인지, xml인지 몰라도 된다. BeanDefinition만 알면 된다.
  • 즉, 이것 역시 역할과 구현으로 나눠진 것이다. (BeanDefinition은 Interface 다)

  • BeanDefinition에는 아래와 같은 정보들이 담겨있다.
    • BeanClassName
    • factoryBeanName: 팩토리 역할의 클래스 명 (AppConfig)
    • factoryMethodName: 빈을 생성할 팩토리 메서드 지정 (memberService)
    • Scope: 싱글톤 (기본 값)
    • lazyInit: 스프링 컨테이너 생성할 때 빈을 생성하지 않고, 실제 사용 시 까지 최대한 생성을 지연처리 하는지 여부
    • InitMethodName
    • DestroyMethodName
    • Constructor arguments, Properties..

2) 싱글톤 컨테이너

  • 웹 애플리케이션은 고객 요청이 많다.

    • 따라서 요청마다 객체를 생성하는 것은 극심한 메모리 소모를 초래한다.
  • 우리가 만들었던 스프링 없는 순수한 DI 컨테이너인 AppConfig는 요청을 할 때마다 객체를 새로 생성한다.

    • 고객 트래픽이 초당 100이 나오면 초당 100개의 객체가 생성되고 소멸된다. 즉, 메모리 낭비가 심하다.
    • 따라서 해당 객체가 딱 1개만 생성되고, 공유하도록 설계해야 한다. -> [싱글톤 패턴]

싱글톤 패턴

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.
    • 핵심) 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야한다.
    • (private 생성자를 사용하여 외부에서 임의로 new 키워드를 사용하지 못하도록 막아야한다.)
    • static 변수를 생성하여 초기에 한번 생성되도록 해준다.
    • 해당 static 변수를 리턴해주는 static 메서드를 정의한다.
    • 따라서 해당 싱글톤 객체가 필요하다면 정의한 static 메서드를 통해서만 가져올 수 있다.
public class SingletonService {

    // 1. 자바가 실행되며 초기에 static 변수로 딱 한번 instance를 생성한다.
    private static final SingletonService instance = new SingletonService();

    // 2. 생성한 instance를 가져올 수 있도록 instance를 리턴하는 static 메서드를 만든다.
    public static SingletonService getInstance() {
        return instance;
    }

    // 3. 외부에서 생성자에 접근하지 못하도록 private으로 접근 제어한다.
    private SingletonService(){}

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

// ////////////////////////// 외부 클래스에서 테스트 ///////////////////////////////////////

    @Test
    @DisplayName("싱글톤 패턴을 적용한 객체 사용")
    void singletonServiceTest(){
        //싱글톤 패턴으로 정의한 클래스의 객체 가져오기
        SingletonService singletonService1 = SingletonService.getInstance();
        SingletonService singletonService2 = SingletonService.getInstance();

        //참조값이 같은 것을 확인
        System.out.println("singletonService1 = " + singletonService1);
        System.out.println("singletonService2 = " + singletonService2);

        // isSameAs: == 비교 (물리적 주소)
        // isEqual: equal() 비교 (논리적 주소)
        assertThat(singletonService1).isSameAs(singletonService2);
    }
  • 싱글톤 패턴을 구현하는 방법은 여러가지가 있다. 여기서는 객체를 미리 생성해두는 가장 단순하고 안전한 방법을 선택했다.
  • 하지만 싱글톤 패턴은 수많은 문제점을 가지고 있다.
    • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
    • 의존관계상의 문제가 생긴다. 클라이언트가 구체 클래스에 의존한다 (getInstance()에 의존)
    • 따라서 OCP 원칙을 위반할 가능성이 높다.
    • 테스트하기 어렵고, 내부 속성을 변경하거나 초기화하기 어렵다.
    • 결론적으로 유연성이 떨어진다. (안티패턴으로 불리기도 한다.)

3) 스프링 컨테이너( = 싱글톤 컨테이너 ) 의 비밀

  • ✨✨✨ 스프링(싱글톤) 컨테이너는 디폴트로 객체를 싱글톤으로 만들어서 관리해주며, 위에서 언급한 문제들을 전부 제거해준다. ✨✨✨
  • 스프링 컨테이너는 객체를 하나만 생성해서 관리한다. -> (Key, Value)로 저장함
  • 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라고 한다.
  • 싱글톤 패턴의 단점 보완
    • 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
    • ✨ DIP, OCP, 테스트, private 생성자로부터 자유롭게 싱글톤을 사용할 수 있다.

  • 참고로 스프링의 기본 빈 등록 방식은 싱글톤이다. 하지만 디폴트 기능일뿐 다른 방식도 사용 가능하다.

싱글톤 방식의 주의점 (Statueful issue)

  • 싱글톤 패턴이든, 스프링 같은 싱글톤 컨테이너를 사용하든, 객체 인스턴스를 하나만 생성해서 공유하는 싱글톤 방식은
  • 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태를 유지(stateful)하게 설계하면 안된다.
  • ❗❗무상태(stateless)로 설계해야한다.
    • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
    • 가급적 읽기만 가능해야 한다.
    • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, **ThreadLocal 등을 사용해야 한다.
  • 스프링 빈의 필드에 공유 값을 설정하면 정말 큰 장애가 발생할 수 있다!!

** ThreadLocal: JDK 1.2부터 제공된 오래된 클래스, 스레드 단위로 로컬 변수를 사용할 수 있기 때문에 마치 전역변수처럼 여러 메서드에서 활용할 수 있다

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

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

        // Thread A: A사용자가 10,000원 주문
        statefulService1.order("userA", 10000);

        // Thread B: B사용자가 20,000원 주문
        statefulService2.order("userB", 20000);

        // Thread A: 사용자A가 주문 금액 조회
        // expexted = 10,000 < - > but 20,000 (자원을 공유해버림)
        int price1 = statefulService1.getPrice();
        System.out.println("price1 = " + price1);
        
        Assertions.assertThat(statefulService1.getPrice()).isEqualTo(20000);
    }
  • 무상태 (stateless)로 설계해라! ✨필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, **ThreadLocal 등을 사용해야 한다.✨

4) @Configuration과 싱글톤

  • 현재 @Configuration이 붙은 AppConfig 클래스를 보면 new MemoryMemberRepository()를 두번 호출한다.
    • 두번 생성된다면 싱글톤이 깨지는 것 아닐까??
    @Test
    void configurationTest(){
        ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

        //  ✨✨ 분명 각각 생성자를 호출하여 MemberRepository를 생성했는데.. 같은 값이 할당된다 ! ! ✨✨
        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);
    }
  • 직접 호출될 때 프린트를 찍어보며 결과를 확인해보면 딱 한번밖에 호출되지 않는다..??

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

  • @Configuration이 붙은 AppConfig의 .getClass()를 출력해보면 $$EnhancerBySpringCGLIB이 붙어있는 것을 볼 수 있다.
    • 이는 내가 만든 클래스가 아니라 스프링이 CGLIB(바이트 코드를 조작하는 라이브러리)를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다.

  • 이와 같은 임의의 클래스가 바로 싱글톤이 보장되도록 해준다.
  • 즉, @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환, 없으면 기존 로직을 실행하여 스프링 빈으로 등록하고 반환하는 동적 코드가 작성되는 것이다.

➕그렇다면 @Configuration 없이 @Bean만 붙이면 어떻게 될까??

  • @Configuration이 없다면, bean은 CGLIB 기술 없이 순수한 클래스로 스프링 빈에 등록된다.
  • 당연히 CGLIB이 관리하지 못하여 의존관계 주입이 필요해서 메서드를 직접 호출할 때 중복 호출을 허용하여 싱글톤을 보장하지 않는다.
  • 예제의 경우, new memberRepository()를 매번 호출하게 되고, 당연히 각 물리적 주소 또한 다르게 된다.
  • 즉, ❗❗@Configuration 없이 @Bean만 사용한다면! ->❗❗싱글톤을 보장할 수 없게 된다.
  • 결론: 스프링 설정 정보는 항상 @Configuration을 붙여서 사용하자!

5) 컴포넌트 스캔과 의존관계 자동 주입 시작하기

  • 컴포넌트 스캔을 사용하려면 @ComponentScan을 설정 정보 클래스에 붙여주면 된다.

    • [예제에서는] @ComponentScan을 사용하면 @Configuration이 붙은 설정 정보도 자동으로 등록되기 때문에 이를 제외해준다 (excludeFilters) -> AppConfig 제외해줌
    • 원래는 제외하지 않는다. 기존 예제 코드를 공부용으로 남겨두기 위함
  • 기존의 AppConfig와 달리 @Bean으로 등록한 클래스가 하나도 없다.

  • @ComponentScan은 이름 그대로 @Component 애노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록시킨다!

  • 이 때, 생성자 파라미터는 어떻게 처리될까?? 기존 AppConfig에서는 @Bean으로 등록해서 넣어줬었는데..

    • 생성자 메서드 위에 @Autowired를 붙여주면 스프링 컨테이너가 파라미터 타입에 맞는 녀석을 찾아와서 자동으로 주입시켜준다!!
    • 이때 @Autowired 기본 조회 전략은 타입이 같은 빈을 찾아서 주입하는 것이다.
    • 이는 마치 getBean(MemberRepository.class)와 같은 기능을 하는 것

탐색 위치와 기본 스캔 대상

  • @ComponentScan에 (basePackages= "") 를 작성하여, 탐색할 패키지의 시작 위치를 지정할 수 있다.

    • 지정한 패키지를 포함하여 하위 패키지까지만 탐색한다.
  • 따로 지정하지 않는다면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.

  • 권장 방식은 패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최 상단에 두는 것. (스프링 부트도 그렇게 하고 있음)

    • 스프링 부트의 대표 시작 정보인 @SpringBootApplication 를 프로젝트 시작 위치에 두는 것이 관례이다. (@SpringBootApplication 안에 @ComponentScan이 이미 들어있다.)
  • 컴포넌트 스캔 기본 대상

    • @Component
    • 아래의 어노테이션들은 어노테이션 내부에 @Component가 이미 붙어있다.
    • @Controller: 또한 스프링 MVC 컨트롤러로 인식하게한다.
    • @Service: 특별한 처리는 없음
    • @Repository: 또한 스프링 데이터 접근 계층으로 인식하고 데이터 계층의 예외를 스프링 예외로 변환해준다.
    • @Configuration: 또한 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리한다.

6) 컴포넌트 필터 추가

  • includeFilters : 컴포넌트 스캔 대상을 추가로 지정한다
  • excludeFilters : 컴포넌트 스캔에서 제외할 대상을 지정한다.
  • 아래의 예제 코드에서는 어노테이션을 만들어서 적용시켰다. (어노테이션 만드는 방법 참고!)
// 어노테이션의 타입은 @interface
@Target(ElementType.TYPE) // TYPE -> class 레벨에 붙음
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIncludeComponent {
}

// 만들어준 어노테이션을 붙인 클래스
@MyIncludeComponent
public class BeanA {
}

// //////////////////////////////////////////////Test 파일/////////////////////////////////////////////////////////////////
    @Test
    void filterScan(){
        ApplicationContext 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, excludeFilters 사용법.
            includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
            excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
    )
    static class ComponentFilterAppConfig{

    }
  • FilterType 옵션
    • ANNOTATION: 기본값, 애노테이션을 인식해서 동작한다.
      • ex) org.example.SomeAnnotation
    • ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작한다. (클래스를 넣을 수도 있음)
      • ex) org.example.SomeClass
    • ASPECTJ: AspectJ 패턴 사용
      • ex) org.example..*Service+
    • REGEX: 정규 표현식
      • ex) org.example.Default.*
    • CUSTOM: TypeFilter 이라는 인터페이스를 구현해서 처리
      • ex) org.example.MyTypeFilter

7) 빈 중복 등록과 충돌

  • 자동 빈 등록 시 메서드명이 중복되는 일만 피해주면 된다.

  • 수동 빈 등록과 자동 빈 등록 과정에서 충돌되면 어떻게 될까?

    • 이 경우 수동 빈 등록이 우선권을 가진다. (수동 빈이 자동 빈을 오버라이딩 해버린다)
  • 단 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충돌될 경우 오류를 내기로 방식을 바꿨다.

    • application.properties에서 이를 true로 바꿔주면 오버라이딩하도록 할 수는 있다. (권장 x)
    • spring.main.allow-bean-definition-overriding=true

8) 의존관계 자동 주입

방식

  • 생성자 주입

    • 생성자 호출 시점에 딱 1번만 호출되는 것이 보장된다. -> 객체 불변성이 보장된다.
    • 불변, 필수 의존 관계에 사용한다. (주로)
    • 클래스 멤버변수에 final을 붙이면 해당 변수들은 꼭 값을 가져야만 한다. 따라서 보통 이를 통해 생성자의 파라미터를 필수 값으로 만들어버린다.
    • 가급적이면 수정자 (setter)를 정의하지 않는 것이 좋다.
    • 생성자가 딱 1개만 있다면 @Autowired를 생략해줘도 된다.
    • 순수 자바 테스트의 경우에도 임의로 구현체를 생성하여 넣어줄 수 있다.
    • 또한, 수정자, 필드 주입과 달리 객체 생성 시점에 주입을 시도하기 때문에 오류를 바로 뱉어내고, 이를 통해 순환참조 되고 있음을 바로 알 수 있다.
    • 반면 수정자와 필드 주입의 경우 객체 생성시 빈으로 정상 등록되고 메서드가 실행될 때 까지 오류가 없어 문제를 바로 발견하기 어렵다.
  • 수정자 주입 (setter 주입)

    • 선택, 변경 가능성이 있는 의존관계에 사용
    • 수정자 파라미터 값이 빈으로 등록 안되어있을 경우 (주입할 대상이 없으면) @Autowired(required = false)를 붙여주며 오류 발생을 막을 수 있다.
    • 스프링 컨테이너가 스프링 빈을 모두 등록하고 -> 스프링 빈의 의존관계를 자동으로 주입할 때 넣어진다
    • 수정자 주입 시 @Autowired는 필수로 붙여줘야 DI된다.
  • 필드 주입

    • 필드 자체에 @Autowired를 붙여주는 방식이다.
    • ❗❗❗ 외부에서 해당 필드가 변경이 불가능해서 테스트하기 굉장히 힘들다는 단점을 지닌다
    • 순수한 자바 코드로 테스트하기 어렵다. 테스트하려면 NullPointerException을 방지하기 위해 Setter를 만들어줘야 하는데, 그럴거면 그냥 Setter에서 @Autowired하는게 낫다..
    • DI 프레임워크가 없으면 아무것도 할 수 없다.
    • 애플리케이션의 실제 코드와는 관계없는 테스트 코드에서나 사용한다.
    • 혹은 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용
  • 일반 메서드 주입

    • 아무 메서드에서나 @Autowired를 붙여서 사용 가능하다.
    • 수정자 주입할 경우와 비슷한 시점에 주입 받는다.
    • 일반적으로 사용하지 않는다.
  • ❗❗ 의존 관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다!

  • ✨✨✨ 결론: 생성자를 통한 의존관계 주입이 가장 좋은 방식임을 알 수 있다. (실제로 스프링 진영에서도 생성자 주입 방식을 권장한다.)


2. 코멘트

  • 오늘은 김영한님의 스프링 핵심원리 강의를 2/3 까지 수강했다.
  • 사실 오늘까지 공부하면 완강할 수 있을 줄 알았는데, 핵심만을 다루기도하고 쉽게쉽게 넘길 내용들이 아니어서, 머릿속에 정리하다 보니 꽤나 오래 걸렸던 것 같다.
  • 그래도 핵심 개념을 정립하는 과정에서 이해도 못한채 스킵하는 것 보다는 시간이 조금 걸리더라도 적어도 6~70 정도의 이해는 가지고 가는게 낫다고 생각한다
  • 슬슬 체력에 한계가 오는 것 같다.. 아무래도 다음 주 부터는 프로젝트 주간이다보니 개인 공부 시간이 줄어들 것 같아서 요새 무리 좀 했더니 바로 몸에 이상신호들이 나타난다 😂
  • 너무 조급해 하지 말고 천천히 계획대로 움직이자
  • 이번 주차의 목표는 김영한님의 핵심원리 강의를 완강하고 남은 기간동안의 로드맵을 그려보는 것이다.
  • 충분히 열심히 하고 있으니 걱정말고 페이스 조절이나 잘하자!
  • 오늘도 고생했고 내일도 힘냅시다 화이팅!👍
profile
BackEnd Developer

0개의 댓글