본문 바로가기
Java/Spring

스프링 빈(BeanFactory, ApplicationContext, ComponentScan, 의존관계 주입 방법 등)

by oneny 2023. 7. 2.

BeanFactory와 ApplicationContext

BeanFactory

  • 스프링 컨테이너의 최상위 인터페이스다.
  • 스프링 빈을 관리하고 조회하는 역할을 담당한다.
  • getBean()을 제공한다.

 

ApplicationContext

  • BeanFactory 기능을 모두 상속받아서 제공한다.
  • 빈을 관리하고 검색하는 기능을 BeanFactory가 제공해주는데, 그러면 차이는 무엇일까?
  • 애플리케이션을 개발할 때는 빈을 관리하고 조회하는 기능은 물론이고, 수 많은 부가기능이 필요하다. 따라서 ApplicationContext는 빈 관리 기능 + 편리한 부가 기능을 제공한다고 생각하면 된다.
    • BeanFactory를 직접 사용할 일은 거의 없다. 부가기능이 포함된 ApplicationContext를 사용한다.
    • BeanFactory나 ApplicationContext를 스프링 컨테이너라 한다.

 

ApplicationContext가 제공하는 부가기능

 

  • 메시지소스를 활용한 국제화 기능
    • 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력
  • 환경변수
    • 로컬, 개발, 운영 등을 구분해서 처리
  • 애플리케이션 이벤트
    • 이벤트를 발행하고 구독하는 모델을 편리하게 지원
  • 편리한 리소스 조회
    • 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회

위에서도 말했듯이 BeanFactory 보다는 추가적으로 필요한 부가기능을 제공하는 ApplicationContext를 사용한다고 생각하면 된다.

 

스프링 컨테이너 생성 과정

new AnnotationConfigApplication(AppConfig.class)를 통해 AppConfig.를 구성 정보로 지정하여 스프링 컨테이너를 생성한다. 그리고 스프링 컨테이너는 파라미터(categoryService, categoryRepository)로 넘어온 설정 클래스 정보를 사용해서 스프링 빈(CategoryService@x01, MemoryCategoryRepository@x02)을 등록한다.

이 때, 빈 이름은 메서드 이름을 사용하고, 직접 부여할 수도 있다.

 

스프링 컨테이너를 통해 빈 조회하기

  • ac.getBeanDefinitionNames(): 스프링에 등록된 모든 빈 이름을 조회한다.
  • ac.getBean(): 빈 이름으로 빈 객체(인스턴스)를 조회한다.
  • 스프링이 내부에서 사용하는 빈은 getRole()로 구분하여 내가 등록한 빈(ROLE_APPLICATION)만 출력할 수 있다.

 

출력 결과

 

스프링 빈 설정 메타 정보 - BeanDefinition

지금까지 AnnotationConfigApplicationContext 클래스를 사용하면서 자바 코드로된 설정 정보를 넘겼다. 하지만 레거시 프로젝트 같은 경우에는 설정 정보가 XML 기반으로 되어 있어 GenericXmlApplicationContext 클래스를 사용하면서 xml 설정 파일을 넘기기도 한다. 스프링은 어떻게 이런 다양한 설정 형식을 지원하는 것일까? 그 중심에는 BeanDefinition이라는 추상화가 있다.

BeanDefinition을 빈 설정 메타정보라 하고 @Bean, <bean> 당 각각 하나씩 메타 정보가 생성된다. 스프링 컨테이너는 이 메타정보를 기반으로 스프링 빈을 생성한다.

AnnotationConfigApplicationContext는 AnnotatedBeanDefinitionReader를 사용해서 AppConfig.class를 읽고 BeanDefinition을 생성한다. GenericXmlApplicationContext는 XmlBeanDefinitionReader를 사용해서 appConfig.xml 설정정보를 읽고 BeanDefinition을 생성한다. 이렇게 역할과 구현을 나눈 덕분에 스프링 컨테이너는 자바 코드인지, XML인지 모르고 오직 BeanDefinition만 알면 되기 때문에 다양한 설정 형식을 지원할 수 있는 것이다.

 

BeanDefinition 정보

  • BeanClassName: 생성할 빈의 클래스 명(자바설정처럼 팩토리 역할의 빈을 사용하면 없음)
  • factoryBeanName: 팩토리 역할의 빈을 사용할 경우 이름
  • factoryMethodName: 빈을 생성할 팩토리 메서드 지정
  • Scope: 싱글톤(기본값)
  • lazyInit: 스프링 컨테이너를 생성할 때 빈을 생성하는 것이 아니라, 실제 빈을 사용할 때까지 최대한 생성을 지연처리하는지 여부
  • InitMethodName: 빈을 생성하고, 의존관계를 적용한 뒤에 호출되는 초기화 메서드 명
  • DestroyMethodName: 빈의 생명주기가 끝나서 제거하기 직전에 호출되는 메서드 명
  • Constructor arguments, Properties: 의존관계 주입에서 사용한다.(자바 설정처럼 팩토리 역할의 빈을 사용하면 없음)

 

싱글톤 컨테이너

스프링의 기본 빈 등록 방식은 싱글톤 패턴을 적용하지 않아도, 객체 인스턴스를 싱글톤으로 관리한다. 즉, 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장한다. 이렇게 싱글톤 객체를 생성하고 관리하는 긴응을 싱글톤 레지스트리라 한다. 스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.

  • 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
  • DIP, OCP, 테스트, private 생성자로부터 자유롭게 싱글톤을 사용할 수 있다.

 

스프링 컨테이너가 싱글톤을 지원하지 않으면 발생하는 문제점

스프링 컨테이너가 싱글톤을 지원하지 않는다면 직접 싱글톤 패턴을 구현해야 하는데 그 때 발생하는 문제는 다음과 같다.

  • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
  • 의존관계상 클라이언트가 구체 클래스에 의존한다. -> DIP 위반
  • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
  • 테스트하기 어렵다.
  • 내부 속성을 변경하거나 초기화하기 어렵다.
  • private 생성자로부터 자식 클래스를 만들기 어렵다.
  • 결론적으로 유연성이 떨어지기 때문에 안티패턴으로 불리기도 한다.

 

싱글톤 컨테이너 확인

싱글톤 컨테이너 덕분에 매번 새로운 객체를 생성하는 것이 아니라 같은 객체를 반환하는 것을 테스트를 통해 확인할 수 있고, 이를 통해 메모리를 절약할 수 있다는 것을 알게 됐다. 물론 요청할 때마다 새로운 객체를 생성해서 반환하는 기능도 제공한다.

 

싱글톤 방식의 주의점

싱글톤 방식은 여러 클라이언트가 하나의 같은 객체 인스턴스를 공유하기 때문에 싱글톤 객체는 상태유지(stateful)하게 설계하면 안된다! 무조건 무상태(stateless)로 설계해야 한다! 무상태로 설계하기 위해서 아래와 같은 규칙을 지켜야 한다.

  • 특정 클라이언트에 의존적인 필드가 있으면 안된다.
  • 특정 클라이언트가 값을 변경할 수 있는 필드가 있으면 안된다.
  • 가급적 읽기만 가능해야 하낟.
  • 필드 대신에 자바에서 공유되지 않는, 지역변수, 파라미터, ThreadLocal 등을 사용해야 한다.

위 규칙을 지키지 않고, 스프링 빈의 필드에 공유값을 설정하면 정말 큰 장애가 발생할 수 있다!!!!!🔥🔥🔥🔥

 

@Configuration과 바이트코드 조작

스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링 빈이 싱글톤이 되도록 보장해주어야 한다. 그래서 스프링은 개발자가 작성한 코드를 어떻게 하기는 어렵기 때문에 대신 클래스의 바이트코드를 조작하는 라이브러리를 사용하고, 이때 사용되는 것이 @Configuration이다. 해당 애노테이션이 붙어있으면 스프링은 바이트코드를 조작하는 CGLIB 기술을 사용해서 싱글톤을 보장한다.

 

위에서 출력된 결과를 보면 순수한 클래스라면 class oneny.admin.AppConfig로 출력되어야 하지만 예상과 다르게 클래스 명에 xxxCGLIB가 붙으면서 상당히 복잡해진 것을 볼 수 있다. 이것은 내가 만든 클래스가 아니라 스프링이 CGLIB라는 바이트코드 조작 라이브러리를 사용해서 AppConfig 클래스를 상속받은 임의의 다른 클래스를 만들고, 그 다른 클래스를 스프링 빈으로 등록한 것이다!

그 임의의 다른 클래스가 바로 싱글톤이 보장되도록 해주고 바이트 코드를 조작해서 작성되어 있는데 실제 CGLIB 내부 기술은 매우 복잡하기 때문에 간단히 설명하자면 @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다. 이 덕분에 싱글톤이 보장되는 것이다!

 

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

지금까지 스프링 빈을 등록할 때는 자바 코드의 @Bean을 통해서 설정 정보에 직접 등록할 스프링 빈을 나열했다. 이렇게 하면 등록해야 할 스프링 빈이 수십, 수백개가 되면 일일이 등록하기 귀찮고, 설정 정보도 커지고, 누락하는 문제도 발생할 수 있다. 그래서 스프링은 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공한다.

 

스프링 부트에서 @SpringBootApplication 애노테이션을 들어가보면 @ComponentScan 애노테이션이 있는 것을 확인할 수 있다. 컴포넌트 스캔은 이름 그래도 @Component 애노테이션이 붙은 모든 클래스를 스캔해서 스프링 빈으로 등록한다. 이때 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자를 사용한다.

  • 빈 이름 기본 전략: CategoryController 클래스 -> categoryController
  • 빈 이름 직접 지정: 만약 스프링 빈의 이름을 직접 지정하고 싶으면 @Component("categoryController2")처럼 이름을 부여

탐색할 패캐지의 시작 위치는 basePackages 옵션을 통해 시작 위치를 지정할 수 있지만 패키지 위치를 지정하지 않으면 설정 정보 클래스의 패키지가 시작 위치가 되기 때문에 프로젝트 메인 설정 정보는 프로젝트 최상단인 시작 루트 위치에 두는 것을 권장한다.

 

참고: 컴포넌트 스캐에 의해 자동으로 스프링 빈이 등록되는데 수동으로 설정 정보 클래스에도 등록하면 최근 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충돌나서 예외가 발생한다.
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
참고
  - includeFilters:
컴포넌트 스캔 대상을 추가로 지정한다.
  - excludeFilters:
컴포넌트 스캔에서 제외할 대상을 지정한다.

 

CategoryController에서 컴포넌트 스캔과 자동 의존관계 설정

CategoryController 클래스에는 @Controller가 붙어있고 그 안을 확인하면 @Component가 있다. 따라서 @Bean으로 직접 설정 정보를 하는 것 없고, 컴포넌트 스캔을 통해 자동으로 스프링 빈으로 등록된다. @Autowired는 의존관계를 자동으로 주입해준다. 즉, applicationContext.getBean(CategoryService.class)와 동일하다고 이해하면 된다.

 

컴포넌트 스캔 기본 대상

컴포넌트 스캔은 @Component 뿐만 아니라 다음 애노테이션도 추가로 대상에 포함하는데 모두 안을 확인하면 @Component가 있다.

  • @Component: 컴포넌트 스캔에서 사용
  • @Controller: 스프링 MVC 컨트롤러에서 사용
  • @Service: 스프링 비즈니스 로직에서 사용
  • @Repository: 스프링 데이터 접근 계층에서 사용
  • @Configuration: 스프링 설정 정보에서 사용

컴포넌트 스캔의 용도 뿐만 아니라 위 애노테이션은 부가 기능을 수행한다.

  • @Controller: 스프링 MVC 컨트롤러로 인식
  • @Repository: 스프링 데이터 접근 계층으로 인식하고, 데이터 계층의 예외를 스프링 예외로 변환해준다.
  • @Configuration: 스프링 설정 정보로 인식하고, 스프링 빈이 싱글톤을 유지하도록 추가 처리를 한다.
  • @Service: @Service는 특별한 처리를 하지 않지만, 핵심 비즈니스 로직이 여기에 있다고 인식하는데 도움이 된다.

 

의존관계 자동 주입

의존관계 주입은 크게 4가지 방법이 있다.

  • 생성자 주입
  • 수정자 주입(setter 주입)
  • 필드 주입
  • 일반 메서드 주입

 

생성자 주입

이전에서 본대로 생성자를 통해서 의존관계를 주입 받는 방법이다. 생성자 호출시점 딱 1번만 호출되는 것을 보장한다. 불변, 필수 의존관계에 사용된다. 그리고 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입된다. 최근 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장한다. 그 이유에 대해서 알아보자.

 

불변

대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없고, 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안된다(불변해야 한다). 수정자 주입을 사용하면 수정자 메서드를 public으로 열어두어야 하는데 누군가 실수로 변경할 수 있는 가능성을 열어두는 것은 좋은 설계 방법이 아니다. 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 불변하게 설계할 수 있다.

 

final 키워드

생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있다. 그래서 생성자에서 혹시라도 값이 설정되지 않는 예외를 컴파일 시점에 막아주어 사전에 방지할 수 있다. 수정자 주입을 포함한 나머지 주입 방식은 모두 생성자 이후에 호출되므로, 필드에 final 키워드를 사용할 수 없어 혹시나 의존관계 주입을 누락하여 런타임에 NullPointerException와 같은 예외가 발생할 가능성이 있다.

 

수정자 주입(setter 주입)

setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해서 의존관계를 주입하는 방법이다. 선택, 변경 가능성이 있는 의존관계에 사용하며 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법이다.

 

참고: 자바에서는 과거부터 필드의 값을 직접 변경하지 않고, setXxx, getXxx 라는 메서드를 통해서 값을 읽거나 수정하는 규칙을 만들었는데, 그것이 자바빈 프로퍼티 규약이다.

 

필드 주입

이름 그대로 필드에 바로 주입하는 방법이다. 하지만 외부에서 변경이 불가능해서 테스트하기 힘들다는 치명적인 단점이 있다. DI 프레임워크가 없으면 아무것도 할 수 없다. 애플리케이션의 실제 코드와 관계 없는 테스트 코드 및 스프링 설정을 목적으로 하는 @Configuration 같은 곳에서만 특별한 용도로 사용하고 그 외에는 사용하지 말자❗️

 

일반 메서드 주입

일반 메서드를 통해서 주입 받는 방법인데 일반적으로 잘 사용하지 않는다.

 

옵션 처리

주입할 스프링 빈이 없어도 동작해야 할 때가 있다. 하지만 아래 결과처럼 @Autowired의 required 옵션의 기본값은 true이기 때문에 기본 동작은 주입할 대상이 없으면 예외가 발생한다. 주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false)로 지정하면 된다.

 

자동 주입 대상을 옵션으로 처리하는 방법은 다음과 같다.

  • @Autowired(required=false): 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨
  • org.springframework.lang.@Nullable: 자동 주입할 대상이 없으면 null이 입력된다.
  • Optional<>: 자동 주입할 대상이 없으면 Optional.empty가 입력된다.

 

조회 대상 빈이 2개 이상일 때 해결 방법

조회 대상 빈이 2개 이상일 때 스프링은 어떤 빈을 주입해야할지 모른다. 만약 JdbcCategoryRepository와 JdbcTemplateRepository에 @Repository 애노테이션을 붙이고 실행을 하면 위 결과처럼 NoUniqueBeanDefinitionException 에러가 발생하게 된다. 이처럼 조회 대상 빈이 2개 이상일 때는 다음과 같은 방법들로 해결할 수 있다.

  • @Autowired 필드 명 매칭
  • @Qualifier -> @Qualifier끼리 매칭 -> 빈 이름 매칭
  • @Primary 사용

 

@Autowired 필드 명 매칭

@Autowired는 타입 매칭을 시도하고, 이때 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가 매칭한다. 따라서 구현체를 주입한다면 그 구현체 타입 매칭을 먼저 시도하기 때문에 정상적으로 동작하게 된다.

 

@Qualifier 사용

@Qualifier는 추가 구분자를 붙여주는 방법이다. 주입시 추가적인 방법을 제공하는 것이지 빈 이름을 변경하는 것은 아니다.

주입 시에 @Qualifier를 붙여주고 등록한 이름을 적어준다.

 

그리고 생성자 자동 주입 시에 @Qualifier("jdbcTemplateRepository")를 주입하다록 애노테이션을 적어 해결할 수 있다.

@Qualifier로 주입할 때 @Qualifier("jdbcTemplateRepository")를 못찾는 경우에는 jdbcTemplateRepository라는 이름의 스프링 빈을 추가로 찾는다. 하지만 @Qualifier는 @Qualifier를 찾는 용도로만 사용하는게 명확하고 좋다. 빈 이름까지도 매칭되지 않는 경우에는 NoSuchBeanDefinitionException 예외가 발생한다.

 

@Primary 사용

@Primary는 우선순위를 정하는 방법이다. @Autowired 시에 여러 빈이 매칭되면 @Primary가 우선권을 가진다. 위처럼 @Primary 애노테이션을 붙여주면 정상적으로 동작한 것을 확인할 수 있다.

 

@Primary와 @Qualifier 활용

코드에서 자주 사용하는 메인 데이터베이스의 커넥션을 획득하는 스프링 빈이 있고, 코드에서 특별한 기능으로 가끔 사용하는 서브 데이터베이스의 커넥션을 획득하는 스프링 빈이 있다고 생각해보자. 메인 데이터베이스의 커넥션을 획득하는 스프링 빈은 @Primary를 적용해서 조회하는 곳에서 @Qualifier 지정 없이 편리하게  조회하고, 서브 데이터베이스 커넥션 빈을 획득할 때는 @Qualifier를 지정해서 명시적으로 획득하는 방식으로 사용하면 코드를 깔끔하게 유지할 수 있다.

 

우선순위

@Primary는 기본값처럼 동작하는 것이고, @Qualifier는 매우 상세하게 동작한다. 스프링은 자동보다는 수동이, 넓은 범위의 선택권 보다는 좁은 범위의 선택권이 우선순위가 높다. 따라서 @Qualifier가 우선권이 높아 위 예시처럼 서브 데이터베이스에서는 @Qualifier를 사용한 곳에서는 @Qualifier가 주입되는 것을 알 수 있다.

 

자동, 수입의 올바른 기준

애플리케이션을 크게 업무 로직과 기술 지원 로직으로 나눌 수 있다.

  • 업무 로직 빈: 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등이 모두 업무 로직이다. 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.
  • 기술 지원 빈: 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다. 데이터베이스 연결이나, 공통 로그 처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.

설정 정보를 기반으로 애플리케이션을 구성하는 부분과 실제 동작하는 부분을 명확하게 나누는 것이 이상적이지만, @Configuration 설정 정보에 가서 @Bean을 적고, 객체를 생성하고, 주입할 대상을 일일이 적어주는 과정을 상당히 번거롭고 실수로 누락할 수 있는 가능성이 있다. 그리고 업무 로직은 숫자도 매우 많고, 한번 개발해야 하면 컨트롤러, 서비스, 리포지터리처럼 어느정도 유사한 패턴이 있다. 따라서 기본적으로 편리한 컴포넌트 스캔과 자동 주입을 사용하는 것이 좋다. 문제가 발생해도 어떤 곳에서 문제가 발생했는지 명확하게 파악하기 쉽다.

기술 지원 로직은 업무 로직과 비교해서 그 수가 매우 적고, 보통 애플리케이션 전반에 걸쳐서 광범위하게 영향을 미친다. 그리고 업무 로직과 달리 기술 지원 로직은 적용이 잘 되고 있는지 아닌지 조차 파악하기 어려운 경우가 많다. 그래서 이런 기술 지원 로직들은 가급적 수동 빈 등록을 사용해서 명확하게 드러내는 것이 유지보수하기 좋다.