• [인프런 김영한 로드맵2]스프링 핵심 원리 정리(4)

    2022. 2. 26.

    by. 웰시코더

    반응형

    -인프런 김영한 강사님의 스프링 핵심 원리를 정리한다.

    -싱글톤 패턴의 개념과 한계를 살펴본다.

    -싱글톤 패턴의 한계를 해결하는 스프링 컨테이너를 살펴본다.

    -기존 @Bean을 통한 설정방식에서 @ComponentScan을 통한 컴포넌트스캔 방식을 살펴본다.

    -모든 소스는 깃허브에서 관리한다.(https://github.com/coderahn/Spring-Lecture2)


    5.싱글톤 컨테이너

     

    1.웹 애플리케이션과 싱글톤

     

    스프링은 대부분 기업용 웹 애플리케이션을 지원하기 위해 탄생했다. 웹 애플리케이션은 동시에 여러 고객의 요청이 들어오게 된다. 그렇기 때문에 요청에 따라 객체가 새로 생성되면 메모리낭비가 심하다. 싱글톤 패턴으로 공유하도록 하면 메모리 낭비 등을 막을 수 있다.

     

    2.싱글톤 패턴

     

    자바코드로 싱글톤 패턴은 다음과 같이 구현할 수 있다.

     

    싱글톤 패턴

     

    사실 다른 방식으로도 구현할 수 있다. 예를 들어 new 인스턴스 초기화를 getInstance() 호출시마다 instance 전역변수가 null인지 체크 후, null이면 new SingletonService()를 호출하고 null이 아니면 instance를 호출하는 방식도 있다.

     

    싱글톤패턴으로 구현하면 고객 요청마다 인스턴스 한 개만을 공유하지만 몇 가지 한계가 있다.

     

    • DIP를 위반한다. 위에 보듯이 new SingletonService()를 클라이언트에서 생성한다.
    • OCP를 위반하게 된다. DIP를 위반했기 때문에 OCP도 위반할 확률이 크다.
    • 테스트 하기 어렵다.
    • private 생성자로 자식 클래스를 만들기 어렵다.

     

    3.싱글톤 컨테이너

     

    스프링 컨테이너는 객체를 싱글톤으로 관리하면서 싱글톤패턴의 문제점을 해결해준다. 기본적으로 컨테이너는 객체를 하나만 생성해서 관리한다. DIP, OCP, 테스트 등에서 모두 자유로워진다. 그러나 원한다면 빈 스코프 설정을 통해 싱글톤 이외의 방식으로도 관리가 가능하다.

     

    4.싱글톤 방식의 주의점

     

    싱글톤 방식은 객체를 공유하기 때문에 객체를 stateful(상태유지)하게 설계하면 안 된다. stateless(무상태)로 설계해야 한다. 

    • 특정 클라이언트에 의존적인 필드가 있으면 안 된다.
    • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안 된다.
    • 가급적 readonly 유지
    • 필드 대신 지역변수, 파라미터, ThreadLocal 등을 사용한다.

    ThreadLocal는 다음 블로그를 참고하면 된다.

    https://javacan.tistory.com/entry/ThreadLocalUsage

     

    ThreadLocal 사용법과 활용

    자바 1.2 버전부터 제공되고 있지만 아직 다수의 개발자들이 잘 몰라서 활용을 잘 못하는 기능이 하나 있는데, 그 기능이 바로 쓰레드 단위로 로컬 변수를 할당하는 기능이다. 이 기능은 ThreadLocal

    javacan.tistory.com

     

    객체에 상태유지 필드가 있다면 문제가 발생하는데 다음 예제를 참고하자.

     

    Stateful한 클래스

     

    다음과 같이 테스트한다.

     

    A사용자의 주문 조회 테스트 -> 10000원이 아니라 20000원이 나온다.

    테스트해보면 A사용자의 주문 금액 10000원이 아니라 B사용자의 주문금액 20000원이 나온다.

     

    5.Configuration과 싱글톤

     

    AppConfig 설정파일을 살펴보면 memberService(), orderService()에서 @Bean생성시 memberRepository()를 호출하여 new MemoryMemberRepository()를 반환한다. 다음과 같다.

     

     

    코드가 위와 같다면 memberRepository는 호출시마다 새로운 객체를 생성해서 싱글톤을 보장하지 않을 것이다. 그러나 테스트를 해보면 memberService, orderService 모두 같은 MemoryMemberRepository 주소를 참조하고 있다.

     

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

     

    @Configuration이 붙은 설정파일은 CGLIB라는 라이브러리를 통해 바이트코드의 조작을 받는다. 그래서 실제로 AppConfig를 getBean으로 조회해보면 다음과 같다.

     

    @Configuration을 붙인 설정파일의 주소를 조회했을 때 CGLIB주소가 붙어 있다.

     

    만약 @Configuration을 붙이지 않으면 @Bean은 등록되지만 싱글톤 보장은 하지 않는다.

     

     

    6.컴포넌트 스캔

     

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

     

    기존 @Bean, <bean>을 사용하여 빈을 등록하는 방식은 등록할 빈 개수가 많아지면 귀찮아진다. 그래서 스프링은 컴포넌트 스캔이라는 기능을 제공한다. @ComponentScan을 설정파일에 붙여주면 @Component가 붙은 클래스를 모두 스프링이 관리하는 빈으로 스캔하여 등록한다.

     

    기존에 @Bean으로 등록한 객체들의 의존관계 설정은 @Autowired기능을 통해 제공한다.

     

    다음과 같이 설정파일에 @Configuration, @ComponentScan을 붙여준다.

     

    컴포넌스 스캔을 사용하기 위한 설정파일

     

    그리고 빈으로 등록할 클래스에게 @Component를 붙여준다. 그리고 의존할 클래스 인스턴스가 주입될 수 있게 @Autowired를 붙여준다.

    컴포넌트 스캔 대상이 되었다.

     

    2.탐색 위치와 기본 스캔 대상

     

    컴포넌트 스캔의 스캔 시작위치는 해당 설정파일(@ComponentScan 붙은 클래스)의 소속 패키지이다. 해당 설정파일이 hello.core라는 패키지에 있다면 hello.core 하위를 모두 스캔한다. 그러나 위치를 지정하고 싶으면 basePackages, basePackageClasses 등의 옵션을 사용한다. 아래와 같다.

    스캔 시작 위치를 hello.core로 지정하였다.

     

    기본적으로 설정정보는 프로젝트 최상단에 둔다. 그러면 알아서 하위 패키지가 모두 스캔된다. 스프링부트 사용시 시작 정보인 @SpringBootApplication가 붙은 설정파일이 프로젝트 최상단에 위치해 있다. 그리고 @SpringBootApplication 내부에는 @ComponentScan을 상속하고 있기 때문에 부트를 사용하면 자동으로 빈 스캔이 된다.

     

    컴포넌스 스캔은 @Component뿐만 아니라 다음 대상들도 스캔 대상에 포함된다.

     

    • @Component : 컴포넌스 스캔 기본 대상
    • @Controller : 스프링 MVC 컨트롤러에서 사용
    • @Service : 스프링 비지니스 로직에서 사용(명시적)
    • @Repository : 스프링 데이터 접근 계층에서 사용(DB예외 발생시 스프링 추상화를 하여 예외전달을 해준다. DB가 변경되어도 일관된 예외에 접근 가능)
    • @Configuration : 스프링 설정 정보에서 사용. 스프링 빈이 싱글톤을 유지하도록 추가 기능을 제공.

     

    3.필터

     

    컴포넌스스캔 어노테이션에는 다음과 같은 옵션을 제공한다.

    • includeFilters : 컴포넌트 스캔 대상을 추가로 지정
    • excludeFilters : 컴포넌스 스캔에서 제외할 대상을 지정

     

    @MyIncludeComponent, @MyExcludeComponent는 만든 어노테이션이다.

     

    BeanA는 @MyIncludeComponent를 적용했고 BeanB는 @MyExcludeComponent를 적용했다. 그래서 테스트하면 BeanA만 빈으로 등록된 것을 확인할 수 있다.

     

    필터타입은 5가지 옵션이 있다.

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

     

    4.중복 등록과 충돌

     

    2가지 경우가 있다.

     

    첫번째로는 자동빈등록vs자동빈등록이다. 컴포넌트 스캔을 하는데 빈 이름이 같은 경우 충돌이 발생하고 ConfilctingBeanDefinitionException이 발생한다.

     

    두번째로 수동빈등록vs자동빈등록이다. 이 경우 수동빈등록이 우선권을 가지며 자동빈등록을 오버라이딩한다. 최근 스프링부트는 다음과 같은 오류를 발생시킨다. 보통 개발과정에서 꼬여서 나는 오류가 많기 때문이다.

     

    Consider renaming one of the beans or enabling overriding by setting 
    spring.main.allow-bean-definition-overriding=true

     

    반응형

    댓글