Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

스프링 핵심 원리 - 기본편

[ 이 강의를 듣고 난 후 스스로에게 기대하는 점 ]

  • 스프링이 제공하는 핵심가치원리를 이해하고, 스프링 기본기를 다질 수 있다.
  • SOLID라 불리는 객체 지향 설계의 원칙과 본질을 고민하며 개발할 수 있다.
  • 실무에서 어떻게 활용하는 게 좋은 방법인지 이해할 수 있다.

스프링이란?

스프링 프레임워크 vs 스프링 부트

[ 스프링 프레임워크 ]

  • 핵심 기술: 스프링 DI 컨테이너, AOP, 이벤트, 기타
  • 웹 기술: 스프링 MVC, 스프링 WebFlux
  • 데이터 접근 기술: 트랜잭션, JDBC, ORM 지원, XML 지원
  • 기술 통합: 캐시, 이메일, 원격접근, 스케줄링
  • 테스트: 스프링 기반 테스트 지원
  • 언어: 코틀린, 그루비

[ 스프링 부트 ]

  • 스프링을 편리하게 사용할 수 있도록 지원, 최근에는 기본으로 사용
  • 단독으로 실행할 수 있는 스프링 애플리케이션을 쉽게 생성
  • Tomcat 같은 웹 서버를 내장해서 별도의 웹 서버를 설치하지 않아도 됨
  • 손쉬운 빌드 구성을 위산 starter 종속성 제공
  • 스프링과 3rd part(외부) 라이브러리 자동 구성
  • 메트릭, 상태확인, 외부 구성 같은 프로덕션 준비 기능 제공
  • 관례에 의한 간결한 설정

스프링의 핵심

  • 스프링은 자바 언어 기반의 프레임워크 -> 자바 언어의 가장 큰 특징 - 객체 지향 언어
  • 스프링은 좋은 객체 지향 애플리케이션을 개발할 수 있게 도와주는 프레임워

[ 객체 지향 특징 ]

  • 추상화
  • 캡슐화
  • 상속
  • 다형성

[ 객체 지향 프로그래밍 ]

  • 객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위, 즉 "객체"들의 모임으로 파악하고자 하는 것이다. 각각의 객체메시지를 주고받고, 데이터를 처리할 수 있다. (협력)
  • 객체 지향 프로그래밍은 프로그램을 유연하고 변경이 용이하게 만들기 때문에 대규모 소프트웨어 개발에 많이 사용된다.
    • 유연하고, 변경이 용이?
      • 레고 블럭 조립하듯이
      • 컴퓨터 부품 갈아 끼우듯이
      • 컴포넌트를 쉽고 유연하게 변경하면서 개밸할 수 있는 방법

[ 다형성 ]

  • 실세계와 객체 지향을 완벽하게 1:1로 매칭시킬 수는 없지만, 객체 지향을 쉽게 이해하기위해 실세계의 비유는 okay.
  • 역할구현으로 세상을 구분
    • < 예시 >
    • 운전자 - 자동차
    • 공연 무대
    • 키보드, 마우스, 세상의 표준 인터페이스들

역할과 구현을 분리

  • 역할구현으로 구분하면 세상이 단순해지고, 유연해지며 변경도 편리해진다.
  • 장점
    • 클라이언트는 대상의 역할(인터페이스)만 알면 된다.
    • 클라이언트는 구현 대상의 내부 구조를 몰라도 된다.
    • 클라이언트는 구현 대상의 내부 구조가 변경되어도 영향을 받지 않는다.
    • 클라이언트는 구현 대상 자체를 변경해도 영향을 받지 않는다.
  • 자바 언어의 다형성을 활용
    • 역할 = 인터페이스
    • 구현 = 인터페이스를 구현한 클래스, 구현 객체
  • 객체를 설계할 때 역할과 구현을 명확히 분리 -> 유연하고 변경이 용이
  • 객체 설계시 역할(인터페이스)을 먼저 부여하고, 그 역할을 수행하는 구현 객체 만들기
  • 인터페이스를 안정적으로 잘 설계하는 것이 중요

다형성의 본질

  • 인터페이스를 구현한 객체 인스턴스를 실행 시점에 유연하게 변경할 수 있다.
  • 다형성의 본질을 이해하려면 협력이라는 객체사이의 관계에서 시작해야함
  • 클라이언트를 변경하지 않고, 서버의 구현 기능을 유연하게 변경할 수 있다.

[ 스프링과 객체 지향 ]

  • 다형성이 가장 중요!
  • 스프링은 다형성을 극대화해서 이용할 수 있게 도와준다.
  • 스프링의 제어의 역전(IoC), 의존관계 주입(DI)은 다형성을 활용해서 역할과 구현을 편리하게 다룰 수 있도록 지원한다.

SOLID

: 좋은 객체 지향 설계의 5가지 원칙

  • SRP: 단일 책임 원칙 (Single Responsibility Principle)
  • OCP: 개방-폐쇄 원칙 (Open/Closed Principle)
  • LSP: 리스코프 치환 원칙 (Liskov Substitution Principle)
  • ISP: 인터페이스 분리 원칙 (Interface Segregation Principle)
  • DIP: 의존관계 역전 원칙 (Dependency Inversion Principle)

[ SRP 단일 책임 원칙 ]

  • 한 클래스는 하나의 책임만 가져야 한다.
  • 하나의 책임이라는 기준이 모호
    • 큰 수 있고, 작을 수 있음
    • 문맥과 상황에 따라 다름
  • 중요한 기준은 변경이다. 변경이 있을 때 파급 효과가 적으면 단일 책임 원칙을 잘 따른 것

[ OCP: 개방-폐쇄 원칙 ]

  • 소프트웨어 요소는 확장에는 열려있으나, 변경에는 닫혀있어야 한다.
  • 다형성 활용
  • 인터페이스를 구현한 새로운 클래스를 하나 만들어서 새로운 기능을 구현 -> 역할과 구현의 분리
  • 문제점
    • 클라이언트 코드에서 구현 클래스를 직접 선택한다면?(= 직접 의존하고 있다면?)
      • 예) MemberRepository m = new MemoryMemberRepository();
    • 구현 객체를 변경하려면 클라이언트 코드를 변경해야 한다.
    • 분명 다형성을 사용했지만 OCP 원칙을 지킬 수 없다.
    • 해결: 객체를 생성하고, 연관관계를 맺어주는 별도의 조립, 설정자가 필요

[ LSP: 리스코프 치환 원칙 ]

  • 프로그램의 객체는 프로그램의 정확성을 깨뜨리지 않으면서 하위 타입의 인스턴스로 바꿀 수 있어야 한다.
  • 다형성에서 하위 클래스는 인터페이스 규약을 다 지켜야 한다는 것, 다형성을 지원하기 위한 원칙.
  • 단순히 컴파일에 성공하는 것을 넘어서는 이야기

[ ISP: 인터페이스 분리 원칙 ]

  • 특정 클라이언트를 위한 인터페이스 여러 개가 범용 인터페이스 하나보다 낫다.
  • 자동차 인터페이스 -> 운전 인터페이스, 정비 인터페이스로 분리
  • 사용자 인터페이스 -> 운전자 클라이언트, 정비사 클라이언트로 분리
  • 분리하면 정비 인터페이스 자체가 변해도 운전자 클라이언트에 영향을 주지 않음
  • 인터페이스가 명확해지고, 대체 가능성이 높아진다.

[ DIP: 의존관계 역전 원칙 ]

  • 프로그래머는 "추상화에 의존해야지, 구체화에 의존하면 안된다." 의존성 주입은 이 원칙을 따르는 방법 중 하나다. -> 구현 클래스에 의존하지 말고, 인터페이스에 의존해!
  • 역할(Role)에 의존하게 해야 한다는 것과 같다. 객체 세상도 클라이언트가 인터페이스에 의존해야 유연하게 구현체를 변경할 수 있다. 구현체에 의존하게 되면 변경이 아주 어려워진다.
  • 문제점
    • 예) MemberRepository m = new MemoryMemberRepository();
      • 클라이언트가 구현 클래스 직접 선택
      • 클라이언트 코드는 인터페이스에 의존하고있지만, 구현 클래스도 동시에 의존한다. -> DIP 위반

[ 한계 ]

  • 객체 지향의 핵심은 다형성
  • 다형성만으로는 쉽게 부품을 갈아 끼우듯이 개발할 수 없다.
  • 다형성만으로는 구현 객체를 변경할 때 클라이언트 코드도 함께 변경된다.
  • 다형성만으로는 OCP, DIP를 지킬 수 없다.
  • 뭔가 더 필요하다. -> 스프링 DI 컨테이너

객체 지향 설계와 스프링

  • 스프링은 다음 기술로 다형성 + OCP, DIP를 가능하게 지원
    • DI(Dependency Injection): 의존관계, 의존성 주입
    • DI 컨테이너 제공
  • 클라이언트 코드의 변경 없이 기능 확장 -> 쉽게 부품을 교체하듯이 개발
  • 이상적으로는 모든 설계에 인터페이스를 부여하자!
    • 한계
      • 인터페이스를 도입하면 추상화라는 비용이 발생한다.
      • 기능을 확장할 가능성이 없다면, 구체 클래스를 직접 사용하고 향후 꼭 필요할 때 리팩터링해서 인터페이스를 도입하는 것도 방법

IoC, DI 그리고 컨테이너

[ 제어의 역전 IoC (Inversion of Control) ]

  • 클라이언트 구현 객체가 스스로 필요한 서버 구현 객체를 생성하고, 연결하고, 실행함. -> 구현 객체가 프로그램의 제어 흐름을 스스로 조종
  • 반면, DI 컨테이너(객체를 생성하고, 관리하면서 의존관계를 연결해주는 것) 등장 이후에 구현 객체는 자신의 로직을 실행하는 역할만 담당한다. -> 프로그램의 제어 흐름은 DI 컨테이너 즉, 스프링 프레임워크에게 있다.
  • 프로그램의 제어 흐름을 직접 제어하는 것이 아니라, 외부에서 관리하는 것을 제어의 역전(IoC)이라 한다.

[ 프레임워크 vs 라이브러리 ]

  • 프레임워크: 내가 작성한 코드를 제어하고, 대신 실행하면 그것은 프레임워크! (ex - JUnit)
  • 라이브러리: 내가 작성한 코드가 직접 제어의 흐름을 담당한다면 그것은 프레임워크가 아니라 라이브러리!

[ 의존관계 주입 DI (Dependency Injection)]

  • 클라이언트 구현 객체는 추상화(인터페이스)에 의존한다. 실제 어떤 구현 객체가 사용될지는 모름. 알 필요도 없음.
  • 의존관계는 정적인 클래스 의존 관계와, 실행 시점에 결정되는 동적인 객체(인스턴스) 의존 관계 둘을 분리해서 생각해야 한다.
  1. 정적인 클래스 의존관계: 클래스가 사용하는 import 코드만 보고 의존관계를 쉽게 판단할 수 있으며, 정적인 의존관계는 애플리케이션을 실행하지 않아도 분석할 수 있다. 클래스 다이어그램을 통해서도 확인 가능하지만, 이런한 클래스 의존관계 만으로는 실제 어떤 객체가 주입될지 알 수 없다.
  2. 동적인 객체 인스턴스 의존 관계: 애플리케이션 실행 시점에 실제 생성된 객체 인스턴스의 참조가 연결된 의존 관계
    • 애플리케이션 실행 시점(런타임) 에 외부에서 실제 구현 객체를 생성하고, 클라이언트에 전달해서 클라이언트와 서버의 실제 의존관계가 연결 되는 것을 의존관계 주입이라 한다.
    • 객체 인스턴스를 생성하고, 그 참조값을 전달해서 연결된다.
    • 의존관계 주입을 사용하면 클라이언트 코드를 변경하지 않고, 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.
    • 의존관계 주입을 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.

스프링 컨테이너

  • ApplicationContext를 스프링 컨테이너라 한다.
  • ApplicationContext는 인터페이스이며, XML을 기반으로 만들 수 있고, 애노테이션 기반의 자바 설정 클래스로 만들 수 있다.
  • 스프링 컨테이너는 @Configuration이 붙은 클래스를 설정(구성) 정보로 사용한다. 여기서 @Bean이라 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록한다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.
  • 스프링 빈은 @Bean이 붙은 메서드의 명을 스프링 빈의 이름으로 사용한다.
  • 스프링 컨테이너를 통해서 스프링 빈(객체)을 찾는 방법은 applicationContext.getBean()이다.

[ 스프링 컨테이너 생성 과정 ]

  1. 스프링 컨테이너 생성
    • new AnnotationConfigApplicationContext(AppConfig.class)
    • 스프링 컨테이너를 생성할 때는 구성 정보를 지정해주어야 한다.
    • 여기서는 AppConfig.class를 구선 정보로 지정했다.
  2. 스프링 빈 등록
    • 스프링 컨테이너는 파라미터로 넘어온 설정 클래스 정보를 사용해서 스프링 빈을 등록
      • 빈 이름
        • 빈 이름은 메서드 이름을 사용
        • 빈 이름을 직접 부여할 수도 있다.
          • @Bean(name="memberService2")
        • 주의: 빈 이름은 항상 다른 이름을 부여해야 한다. 같은 이름을 부여하면 설정에 따라 다른 빈이 무시되거나, 기존 빈을 덮어버리거나 오류가 발생한다.
  3. 스프링 빈 의존관계 설정 - 준비
  4. 스프링 빈 의존관계 설정 - 완료
    • 스프링 컨테이너는 설정 정보를 참고해서 의존관계를 주입(DI)한다.
    • 단순히 자바 코드를 호출하는 것 같지만 차이가 있다. -> 싱글톤으로 생성

참고 - 스프링은 빈을 생성하고, 의존관계를 주입하는 단계가 나누어져 있다. 그런데 자바 코드로 스프링 빈을 등록하면 생성자를 호출하면서 의존관계 주입도 한번에 처리된다.

[ 컨테이너에 등록된 빈 조회 ]

  1. 모든 빈 출력하기
    • 스프링에 등록된 모든 빈 정보를 출력할 수 있다.
    • ac.getBeanDefinitioNames(): 스프링에 등록된 모든 빈 이름을 조회
    • ac.getBean(): 빈 이름으로 빈 객체(인스턴스)를 조회한다.
  2. 애플리케이션 빈 출력하기
    • 스프링이 내부에서 사용하는 빈은 제외하고, 내가 등록한 빈만 출력하고 싶은 경우
    • 스프링이 내부에서 사용하는 빈은 getRole()로 구분할 수 있다.
      • ROLE_APPLICATION: 일반적으로 사용자가 정의한 빈
      • ROLE_INFRASTRUCTURE: 스프링이 내부에서 사용하는 빈

[ 스프링 빈 조회 - 기본 ]

: 스프링 컨테이너에서 스프링 빈을 찾는 가장 기본적인 방법

  • ac.getBean(빈이름, 타입)
  • ac.getBean(타입)
  • 조회 대상 스프링 빈이 없으면 예외 발생
    • NoSuchBeanDefinitionException: No bean named 'xxxxx' available

[ 스프링 빈 조회 - 동일한 타입이 둘 이상 ]

  • 타입으로 조회 시 같은 타입의 스프링 빈이 둘 이상이면 오류가 발생한다. 이때는 빈 이름을 지정하다.
  • ac.getBeansOfType()을 사용하면 해당 타입의 모든 빈을 조회할 수 있다.

[ BeanFactory와 ApplicationContext ]

  1. BeanFactory
    • 스프링 컨테이너 최상위 인터페이스
    • getBean()을 제공
    • 스프링 빈을 관리하고 조회하는 역할을 담당한다.
    • 지금까지 사용했던 대부분의 기능은 BeanFactory가 제공하는 기능이다.
  2. ApplicationContext
    • BeanFactory 기능을 모두 상속받아서 제공한다.
    • 그 외 ApplicationContext의 역할
      • 메시지소스를 활용한 국제화 기능
        • 예를 들어서 한국에서 들어오면 한국어로, 영어권에서 들어오면 영어로 출력
      • 환경변수
        • 로컬, 개발, 운영등을 구분해서 처리
      • 애플리케이션 이벤트
        • 이벤트를 발행하고 구독하는 모델을 편리하게 지원
      • 편리한 리소스 조회
        • 파일, 클래스패스, 외부 등에서 리소스를 편리하게 조회
    • ApplicationContext는 빈 관리기능 + 편리한 부가 기능을 제공한다.
    • BeanFactory를 직접 사용할 일은 거의 없다. 부가기능이 포함된 ApplicationContext를 사용한다.
    • BeanFactory나 ApplicationContext를 스프링 컨테이너라 한다.

[ 다양한 설정 형식 지원 - 자바코드, XML ]

: 스프링 컨테이너는 다양한 형식의 설정 정보를 받아드릴 수 있게 유연하게 설계되어 있다.

  1. 애노테이션 기반 자바 코드 설정 사용
    • new AnnotationConfigApplicationContext(AppConfig.class)
    • AnnotationConfigApplicationContext 클래스를 사용하면서 자바 코드로된 설정 정보를 넘기면 된다.
  2. XML 설정 사용
    • GenericXmlApplicationContext를 사용하면서 xml 설정 파일을 넘기면 된다.

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

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

싱글톤 컨테이너

  • 스프링 없는 순수한 DI 컨테이너는 요청을 할 때 마다 객체를 새로 생성한다.
  • 고객 트래픽이 초당 100이 나오면 초당 100개 객체가 생성되고 소멸된다! -> 메모리 낭비가 심함
  • 해결방안은 해당 객체가 딱 1개만 생성되고, 공유하도록 설계하면 된다. -> 싱글톤 패턴

[ 싱글톤 패턴 ]

  • 클래스의 인스턴스가 딱 1개만 생성되는 것을 보장하는 디자인 패턴이다.
  • 객체 인스턴스를 2개 이상 생성하지 못하도록 막아야 한다.
    1. static 영역에 객체 instance를 미리 하나 생성해서 올려둔다.
    2. 이 객체 인스턴스가 필요하면 오직 스태틱 게터 메서드를 통해서만 조회할 수 있다. 이 메서드를 호출하면 항상 같은 인스턴스를 반환한다.
    3. 딱 1개의 객체 인스턴스만 존재해야 하므로, 생성자를 private으로 막아서 혹시라도 외부에서 new 키워드로 객체 인스턴스가 생성되는 것을 막는다.
  • 싱글톤 패턴을 적용하면 고객의 요청이 올 때 마다 객체를 생성하는 것이 아니라, 이미 만들어진 객체를 공유해서 효율적으로 사용할 수 있다. 하지만 싱글톤 패턴은 다음과 같은 수 많은 문제점들을 가지고 있다.
  • 문제점
    • 싱글톤 패턴을 구현하는 코드 자체가 많이 들어간다.
    • 의존관계상 클라이언트가 구체 클래스에 의존한다. -> DIP 위반
    • 클라이언트가 구체 클래스에 의존해서 OCP 원칙을 위반할 가능성이 높다.
    • 테스트하기 어렵다.
    • 내부 속성을 변경하거나 초기화하기 어렵다.
    • private 생성자로 자식 클래스를 만들기 어렵다.
    • 결론적으로 유연성이 떨어진다.
    • 안티패턴으로 불리기도 한다.

[ 싱글톤 컨테이너 ]

  • 스프링 컨테이너는 싱글톤 패턴의 문제점들을 해결하면서, 싱글톤 패턴을 적용하지 않아도 객체 인스턴스를 싱글톤으로 관리한다.
  • 스프링 컨테이너는 싱글톤 컨테이너 역할을 한다. 이렇게 싱글톤 객체를 생성하고 관리하는 기능을 싱글톤 레지스트리라 한다.
  • 스프링 컨테이너의 이런 기능 덕분에 싱글턴 패턴의 모든 단점을 해결하면서 객체를 싱글톤으로 유지할 수 있다.
    • 싱글톤 패턴을 위한 지저분한 코드가 들어가지 않아도 된다.
    • DIP, OCP, 테스트, private 생성자로부터 자유롭게 싱글톤을 사용할 수 있다.

참고: 스프링의 기본 빈 등록 방식은 싱글톤이지만, 싱글톤 방식만 지원하는 것은 아니다. 요청할 때마다 새로운 객체를 생성해서 반환하는 기능도 제공한다. -> 빈 스코프 참고

[ 싱글톤 방식의 주의점 ]

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

[ @Configuration과 바이트코드 조작 ]

스프링 컨테이너는 싱글톤 레지스트리다. 따라서 스프링 빈이 싱글톤이 되도록 보장해주어야 한다. 하지만 설정 클래스에 new 키워드로 같은 클래스의 인스턴스를 생성하는 코드가 있다면 싱글톤이 깨지는 것 처럼 보일 수 있다. 스프링이 자바 코드까지 조작할 수는 없는데.. 그렇다면 어떻게 싱글톤을 보장해줄 수 있는 것일까?

스프링은 클래스의 바이트코드를 조작하는 라이브러리를 사용한다. 모든 비밀은 @Configuration 을 적용한 설정 클래스에 있다. AnnotationConfigApplicationContext에 파라미터로 넘긴 값도 스프링 빈으로 등록된다. 그래서 설정 클래스도 스프링 빈이 된다. AppConfig(설정 클래스) 스프링 빈을 조회해서 클래스 정보를 출력해보자.

bean = class hello.core.AppConfig$$EnhancerBySpringCGLIB$$bd479d70

순수한 클래스라면 다음과 같이 출력되어야 한다.

class hello.core.AppConfig

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

이 임의의 다른 클래스가 바로 싱글톤이 보장되도록 해준다. 아마도 다음과 같이 바이트 코드를 조작해서 작성되어 있을 것이다. (실제로는 CGLIB의 내부 기술을 사용하는데 매우 복잡하다.)


AppConfig@CGLIB 예상 코드

@Bean 
public MemberRepository memberRepository() {
    if (memoryMemberRepository가 이미 스프링 컨테이너에 등록되어 있으면?) {
        return 스프링 컨테이너에서 찾아서 반환;
    } else { //스프링 컨테이너에 없으면
        기존 로직을 호출해서 MemoryMemberRepository를 생성하고 스프링 컨테이너에 등록 
        return 반환
    } 
}
  • @Bean이 붙은 메서드마다 이미 스프링 빈이 존재하면 존재하는 빈을 반환하고, 스프링 빈이 없으면 생성해서 스프링 빈으로 등록하고 반환하는 코드가 동적으로 만들어진다.
  • 덕분에 싱글톤이 보장되는 것이다.

< @Configuration을 적용하지 않고, @Bean만 적용하면 어떻게 될까? >

@Configuration을 붙이면 바이트코드를 조작하는 CGLIB 기술을 사용해서 싱글톤을 보장하지만 @Bean만 적용하면 AppConfig가 CGLIB 기술 없이 순수한 AppConfig로 스프링 빈에 등록된다.

  • @Bean만 사용해도 스프링 빈으로 등록되지만, 싱글톤을 보장하지 않는다.
    • 의존관계 주입이 필요해서 메서드를 직접 호출할 때 싱글톤을 보장하지 않는 것
  • 크게 고민할 것 없이 스프링 설정 정보는 항상 @Configuration을 사용하자!

컴포넌트 스캔

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

스프링은 자바 코드의 @Bean이나 XML의 <bean> 등의 설정 정보가 없어도 자동으로 스프링 빈을 등록하는 '컴포넌트 스캔' 이라는 기능을 제공한다. 또한, 의존관계도 자동으로 주입하는 @Autowired 라는 기능도 제공한다.

  • 컴포넌트 스캔을 사용하려면 먼저 ComponentScan을 설정 정보에 붙여주면 된다.
  • 기존의 @Configuration을 적용한 설정 클래스와는 다르게 @Bean으로 등록한 클래스가 하나도 없다.
  • 컴포넌트 스캔의 이름 그대로 @Component 애노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록한다. 스프링 빈으로 등록할 클래스에 @Component를 붙여주자!

참고: Configuration이 컴포넌트 스캔의 대상이 된 이유도 @Configuration 소스코드를 열어보면 @Component 애노테이션이 붙어있기 때문이다.

< 예시 - OrderServiceImpl @Component, @Autowired 추가 >

@Component
  public class OrderServiceImpl implements OrderService {
      private final MemberRepository memberRepository;
      private final DiscountPolicy discountPolicy;
      
 @Autowired
      public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
          this.memberRepository = memberRepository;
          this.discountPolicy = discountPolicy;
      }
}
  • @Autowired는 의존관계를 자동으로 주입해준다.
  • 상단의 코드처럼 생성자 주입 방식으로 @Autowired를 사용하면 생성자에서 여러 의존관계도 한번에 주입받을 수 있다.

[ 동작 방식 ]

  1. @ComponentScan
    • @ComponentScan@Component가 붙은 모든 클래스를 스프링 빈으로 등록한다.
    • 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자만 소문자를 사용한다.
      • 빈 이름 기본 전략: MemberServiceImpl 클래스 -> memberServiceImpl
      • 빈 이름 직접 지정: 스프링 빈의 이름을 직접 지정하고 싶으면 Component("memberService2") 이런식으로 이름을 부여하면 된다.
  2. @Autowired 의존관계 자동 주입
    • 생성자에 @Autowired를 지정하면, 스프링 컨테이너가 자동으로 해당 스프링 빈을 찾아서 주입한다.
    • 이때 기본 조회 전략은 타입이 같은 빈을 찾아서 주입한다.
      • getBean(MemberRepository.class)와 동일하다고 이해하면 된다.

[ 탐색 위치 ]

탐색할 패키지의 시작 위치 지정: 모든 자바 클래스를 모두 컴포넌트 스캔하면 시간이 오래 걸려, 꼭 필요한 위치부터 탐색하도록 시작 위치를 지정할 수 있다.

@ComponentScan(
          basePackages = "hello.core",
}
  • basePackages: 탐색할 패키지의 시작 위치를 지정한다. 이 패키지를 포함해서 하위 패키지를 모두 탐색한다.
    • basePackages = {"hello.core", "hello.service"} 이렇게 여러 시작 위치를 지정할 수도 있다.
  • basePackageClasses: 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다.
    • 따로 지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 된다.

< 영한님께서 권장하시는 방법! >

개인적으로 즐겨 사용하는 방법은 패키지 위치를 지정하지 않고, 설정 정보 클래스의 위치를 프로젝트 최상단에 두는 것이다. 최근 스프링 부트도 이 방법을 기본으로 제공한다.

예를 들어서 프로젝트가 다음과 같이 구조가 되어 있으면

  • com.hello
  • com.hello.service
  • com.hello.repository

com.hello -> 프로젝트 시작 루트, 여기에 AppConfig 같은 메인 설정 정보를 두고, @ComponentScan 애노테이션을 붙이고, basePackages 지정은 생략한다.

이렇게 하면 com.hello를 포함한 하위는 모두 자동으로 컴포넌트 스캔의 대상이 된다. 그리고 프로젝트 메인 설정 정보는 프로젝트를 대표하는 정보이기 때문에 프로젝트 시작 루트 위치에 두는 것이 좋다고 생각한다.

참고로 스프링 부트를 사용하면 스프링 부트의 대표 시작 정보인 @SpringBootApplication을 이 프로젝트 시작 루트 위치에 두는 것이 관례이다. 그리고 이 설정안에 바로 @ComponentScan이 들어있다!


[ 컴포넌트 스캔 기본 대상 ]

컴포넌트 스캔은 @Component뿐만 아니라 다음과 같은 내용도 추가로 대상에 포함하며, 컴포넌트 스캔의 용도 뿐만 아니라 다음 애노테이션이 있으면 스프링은 부가 기능을 수행한다.

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

[ 필터 ]

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

< FilterType 옵션 >

: FilterType은 5가지 옵션이 있다.

  • 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

참고: Component면 충분하기 때문에, includeFilters를 사용할 일은 거의 없다. excludeFilters는 여러가지 이유로 간혹 사용할 때가 있지만 많지는 않다.

특히 최근 스프링 부트는 컴포넌트 스캔을 기본으로 제공하는데, 개인적으로는 옵션을 변경하면서 사용하기 보다는 스프링의 기본 설정에 최대한 맞추어 사용하는 것을 권장하고, 선호하는 편이다.

by. 영한님

[ 중복 등록과 충돌 ]

컴포넌트 스캔에서 같은 빈 이름을 등록하면 어떻게 될까?
다음 두 가지 상황이 있다.

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

< 자동 빈 등록 vs 자동 빈 등록 >

  • 컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데, 그 이름이 같은 경우 스프링은 오류를 발생시킨다.
    • ConflictingBeanDefinitionException 예외 발생

< 수동 빈 등록 vs 자동 빈 등록 >

  • 이 경우 수동 빈 등록이 우선권을 가진다.
    • 수동 빈이 자동 빈을 오버라이딩 해버린다.

수동 빈 등록시 남는 로그

Overriding bean definition for bean 'memoryMemberRepository' with a different 
definition: replacing

물론 개발자가 의도적으로 이런 결과를 기대했다면, 자동 보다는 수동이 우선권을 가지는 것이 좋다. 하지만 현실은 개발자가 의도적으로 설정해서 이런 결과가 만들어지기 보다는 여러 설정들이 꼬여서 이런 결과가 만들어지는 경우가 대부분이다.

그러면 정말 잡기 어려운 버그가 만들어진다. 항상 잡기 어려운 버그는 애매한 버그다.

그래서 최근 스프링 부트에서는 수동 빈 등록과 자동 빈 등록이 충돌나면 오류가 발생하도록 기본 값을 바꾸었다.


수동 빈 등록, 자동 빈 등록 오류 시 스프링 부트 에러

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

스프링 부트인 CoreApplication을 실행해보면 오류를 볼 수 있다.


의존관계 자동 주입

[ 다양한 의존관계 주입 방법 ]

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

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

< 생성자 주입 >

  • 이름 그대로 생성자를 통해서 의존 관계를 주입 받는 방법
  • 특징
    • 생성자 호출 시점에 딱 1번만 호출되는 것이 보장된다.
    • 불변, 필수 의존관계의 사용
  • 중요! 생성자가 딱 1개만 있으면 @Autowired를 생략해도 자동 주입 된다. 물론 스프링 빈에만 해당한다.

< 수정자 주입 (setter 주입) >

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

참고: Autowired의 기본 동작은 주입할 대상이 없으면 오류가 발생한다. 주입할 대상이 없어도 동작하게 하려면 @Autowired(required = false)로 지정하면 된다.

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


< 필드 주입 >

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

참고: 순수한 자바 테스트 코드에는 당연히 @Autowired가 동작하지 않는다. @springBootTest처럼 스프링 컨테이너를 테스트에 통합한 경우에만 가능하다.


< 일반 메서드 주입 >

  • 일반 메서드를 통해서 주입받을 수 있다.
  • 특징
    • 한번에 여러 필드를 주입 받을 수 있다.
    • 일반적으로 잘 사용하지 않는다.

참고: 의존관계 자동 주입은 스프링 컨테이너가 관리하는 스프링 빈이어야 동작한다. 스프링 빈이 아닌 Member같은 클래스에서 @Autowired 코드를 적용해도 아무 기능도 동작하지 않는다.

[ 옵션 처리 ]

주입할 스프링 빈이 없어도 동작해야 할 때가 있다.
그런데 @Autowired만 사용하면 required 옵션의 기본값이 true로 되어 있어서 자동 주입 대상이 없으면 오류가 발생한다.


옵션으로 처리하는 방법

  • @Autowired(required=false): 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출 안됨
  • org.springframework.lang.@Nullable: 자동 주입할 대상이 없으면 null이 입력된다.
  • Optional<>: 자동 주입할 대상이 없으면 Optional.empty가 입력된다.
// 호출 안됨
@Autowired(required = false)
public void setNoBean1(Member noBean1){
        System.out.println("noBean1 = " + noBean1);
}

// null 호출
@Autowired
public void setNoBean2(@Nullable Member noBean2){
        System.out.println("noBean2 = " + noBean2);
}

// Optional.empty 호출
@Autowired
public void setNoBean3(Optional<Member> noBean3){
        System.out.println("noBean3 = " + noBean3);
}
  • Member는 스프링 빈이 아니다
    • setNoBean1()@Autowired(required=false)이므로 호출 자체가 안된다.

출력 결과

setNoBean2 = null
setNoBean3 = Optional.empty
  • @Nullable, Optional은 스프링 전반에 걸쳐서 지원된다. 예를 들어 생성자 자동 주입에서 특정 필드에만 사용해도 된다.

[ 생성자 주입을 선택해라! ]

과거에는 수정자 주입과 필드 주입을 많이 사용했지만, 최근에는 스프링을 포함한 DI 프레임워크 대부분이 생성자 주입을 권장한다.

  • 불변
    • 대부분의 의존관계 주입은 한번 일어나면 애플리케이션 종료시점까지 의존관계를 변경할 일이 없다. 오히려 대부분의 의존관계는 애플리케이션 종료 전까지 변하면 안된다. (불변해야 한다.)
    • 수정자 주입을 사용하면 setXxx 메서드를 public으로 열어두어야 한다.
    • 누군가 실수로 변경할 수도 있고, 변경하면 안 되는 메서드를 열어두는 것은 좋은 설계 방법이 아니다.
    • 생성자 주입은 객체를 생성할 때 딱 1번만 호출되므로 이후에 호출되는 일이 없다. 따라서 불변하게 설계할 수 있다.
  • 누락
    • 수정자 의존관계인 경우 @Autowired가 프레임워크 안에서 동작할 때는 의존관계가 없으면 오류가 발생하지만, 프레임워크 없이 순수한 자바 코드로만 단위 테스트를 수행하면 NPE가 발생한다.
    • 생성자 주입을 사용하면 주입 데이터를 누락했을 때 컴파일 오류가 발생한다. 그리고 IDE에서 바로 어떤 값을 필수로 주입해야 하는지 알 수 있다.
  • final 키워드
    • 생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있어, 생성자에서 혹시라도 값이 설정되지 않으면 오류를 컴파일 시점에 막아준다.
    • 기억하자! 컴파일 오류는 세상에서 가장 빠르고, 좋은 오류다!
    • 수정자 주입을 포함한 나머지 주입 방식은 모두 생성자 이후에 호출되므로, 필드에 final 키워드를 사용할 수 없다.
  • 생성자 주입 방식을 선택하는 이유는 여러가지 있지만, 프레임워크에 의존하지 않고, 순수한 자바 언어의 특징을 잘 살리는 방법이기도 하다.
  • 기본적으로 생성자 주입을 사용하고, 필수 값이 아닌 경우에는 수정자 주입 방식을 옵션으로 부여하면 된다. 생성자 주입과 수정자 주입을 동시에 사용할 수 있다.
  • 항상 생성자 주입을 선택해라! 그리고 가끔 옵션이 필요하면 수정자 주입을 선택해라. 필드 주입은 사용하지 않는 게 좋다.

[ 조회 빈이 2개 이상 - 문제 ]

@Autowired는 타입(Type)으로 조회한다. 타입으로 조회하기 때문에 마치, 다음 코드와 유사하게 동작한다. (실제로는 더 많은 기능을 제공)

ac.getBean(DiscountPolicy.class)

타입으로 조회했을 경우 선택된 빈이 2개 이상이면 문제가 발생한다. 이때 하위 타입으로 지정할 수도 있지만, 하위 타입으로 지정하는 것은 DIP를 위배하고, 유연성이 떨어진다. 그리고 이름만 다르고, 완전히 똑같은 타입의 스프링 빈이 2개 있을 때 해결이 안된다.

스프링 빈을 수동 등록해서 문제를 해결해도 되지만, 의존 관계 자동 주입에서 해결하는 여러 방법이 있다.

[ @Autowired 필드 명, @Qualifier, @Primary ]

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

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

< @Autowired 필드 명 매칭 >

@Autowired는 타입 매칭을 시도하고, 이때 여러 빈이 있으면 필드명, 파라미터명으로 빈 이름을 추가 매칭한다.

기존 코드

@Autowired
private DiscountPolicy discountPolicy

필드 명을 빈 이름으로 변경

@Autowired
private DiscountPolicy rateDiscountPolicy

필드 명이 rateDiscountPolicy 이므로 정상 주입된다.
필드 명 매칭은 먼저 타입 매칭을 시도하고, 그 결과에 여러 빈이 있을 때 추가로 동작하는 기능이다.


< @Qualifier 사용 >

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

빈 등록 시 @Qualifier를 붙여준다.

@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
@Qualifier("fixDiscountPolicy")
public class FixDiscountPolicy implements DiscountPolicy {}

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

@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
    this.memberRepository = memberRepository;
    this.discountPolicy = discountPolicy;
}

수정자 자동 주입 예시

@Autowired
public DiscountPolicy setDiscountPolicy(@Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy) {
    return discountPolicy;
}

@Qualifier로 주입할 때 @Qualifier("mainDiscountPolicy")를 못 찾으면 mainDiscountPolicy라는 이름의 스프링 빈을 추가로 찾는다. 하지만 @Qualifier@Qualifier를 찾는 용도로만 사용하는 게 명확하고 좋다.

최종적으로 찾지 못한다면 NoSuchBeanDefinitionException예외 발생

참고: 직접 빈 등록시에도 @Qualifier를 동일하게 사용할 수 있다.


< @Primary 사용 >

@primary는 우선 순위를 정하는 방법이다. @Autowired 시에 여러 빈이 매칭되면 @Primary가 우선권을 가진다.


rateDiscountPolicy가 우선권을 가지도록 하자.

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}

@Component
public class FixDiscountPolicy implements DiscountPolicy {}

코드를 실행해보면 문제 없이 @Primary가 잘 동작하는 것을 확인 할 수 있다.


< @Primary VS @Qualifier >
@Qualifier의 단점은 주입 받을 때 모든 코드에 @Qualifier를 붙여주어야 한다는 점이다. 반면에 @Primary를 사용하면 @Qualifier를 붙일 필요가 없다.

코드에서 자주 사용하는 메인 데이터베이스의 커넥션을 획득하는 스프링 빈이 있고, 코드에서 특별한 기능으로 가끔 사용하는 서브 데이터베이스의 커넥션을 획득하는 스프링 빈이 있다고 생각해보자.

메인 데이터베이스의 커넥션을 획득하는 스프링 빈은 @Primary를 적용해서 조회하는 곳에서 @Qualifier 지정 없이 편리하게 조회하고, 서브 데이터베이스 커넥션 빈을 획득할 때는 @Qualifier를 지정해서 명시적으로 획득하는 방식으로 사용하면 코드를 깔끔하게 유지할 수 있다. 물론 이때 메인 데이터베이스의 스프링 빈을 등록할 때 @Qualifier를 지정해두는 것은 상관없다.

우선 순위
@Primary는 기본값처럼 동작하는 것이고, @Qualifier는 매우 상세하게 동작한다. 이런 경우 어떤 것이 우선권을 가져갈까? 스프링은 자동보다는 수동이, 넓은 범위의 선택권 보다는 좁은 범위의 선택권이 우선 순위가 높다. 따라서 여기서도 Qualifier가 우선권이 높다.


[ 애노테이션 직접 만들기 ]

@Qualifier("mainDiscountPolicy") 이렇게 문자를 적으면 컴파일 시 타입 체크가 안된다. 다음과 같은 애노테이션을 만들어서 문제를 해결할 수 있다.

package hello.core.annotataion;

import org.springframework.beans.factory.annotation.Qualifier;

import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER,
ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
@Component
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {}
//생성자 자동 주입
@Autowired
public OrderServiceImpl(MemberRepository memberRepository, @MainDiscountPolicy DiscountPolicy discountPolicy) {
      this.memberRepository = memberRepository;
      this.discountPolicy = discountPolicy;
}
  
//수정자 자동 주입
@Autowired
public DiscountPolicy setDiscountPolicy(@MainDiscountPolicy DiscountPolicy discountPolicy) {
      return discountPolicy;
}

[ 조회한 빈이 모두 필요할 때 - List, Map ]

의도적으로 해당 타입의 스프링 빈이 모두 필요한 경우도 있다. 이럴 경우 List 또는 Map을 이용하여 주입받으면 된다.

  • Map<String, 빈 타입>
    • Map<String, DiscountPolicy>: map의 키에 스프링 이름을 넣어주고, 그 값으로 DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
  • List<빈 타입>
    • List<DiscountPolicy>: DiscountPolicy 타입으로 조회한 모든 스프링 빈을 담아준다.
  • 만약, 해당하는 타입의 스프링 빈이 없으면, 빈 컬렉션이나 Map을 주입한다.

[ 자동, 수동 올바른 실무 운영 기준 ]

  • 편리한 자동 기능을 기본으로 사용하자
    • 설정 정보를 기반으로 애플리케이션을 구성하는 부분과 실제 동작하는 부분을 명확하게 나누는 것이 이상적이지만, 개발자 입장에서 스프링 빈을 하나 등록할 때 @Component만 넣어주면 끝나는 일을 @Configuration 설정 정보에 가서 @Bean을 적고, 객체를 생성하고, 주입할 대상을 일일이 적어주는 과정은 상당히 번거롭다.
    • 관리할 빈이 많아서 설정 정보가 커지면 설정 정보를 관리하는 것 자체가 부담이 된다.
    • 결정적으로 자동 빈 등록을 사용해도 OCP, DIP를 지킬 수 있다.
  • 수동 빈 등록은 언제?
    • 애플리케이션은 크게 업무 로직과 기술 지원 로직으로 나눌 수 있다.
      • 업무 로직 빈: 웹을 지원하는 컨트롤러, 핵심 비즈니스 로직이 있는 서비스, 데이터 계층의 로직을 처리하는 리포지토리 등이 모두 업무 로직이다. 보통 비즈니스 요구사항을 개발할 때 추가되거나 변경된다.
      • 기술 지원 빈: 기술적인 문제나 공통 관심사(AOP)를 처리할 때 주로 사용된다. 데이터베이스 연결이나, 공통 로그 처리 처럼 업무 로직을 지원하기 위한 하부 기술이나 공통 기술들이다.
    • 업무 로직은 숫자도 매우 많고, 한번 개발해야 하면 컨트롤러, 서비스, 리포지토리 처럼 어느정도 유사한 패턴이 있다. 이런 경우 자동 기능을 적극 사용하는 것이 좋다. 보통 문제가 발생해도 어떤 곳에서 문제가 발생했는지 명확하게 파악하기 쉽다.
    • 기술 지원 로직은 업무 로직과 비교해서 그 수가 매우 적고, 보통 애플리케이션 전반에 걸쳐서 광범위하게 영향을 미친다. 업무 로직은 문제가 발생했을 때 어디가 문제인지 명확하게 잘 들어나지만, 기술 지원 로직은 적용이 잘 되고 있는지 아닌지 조차 파악하기 어려운 경우가 많다. 그래서 이런 기술 지원 로직들은 가급적 수동 빈 등록을 사용해서 명확하게 들어내는 것이 좋다.
  • 비즈니스 로직 중에서 다형성을 적극 활용할 때
    • 의존관계 자동 주입
      • 조회한 빈이 모두 필요할 때 List 또는 Map을 사용한다. 이런 경우 여기에 어떤 빈들이 주입될 지, 각 빈들의 이름은 무엇일지 코드만 보고 한번에 파악하기 힘들다. 자동 등록을 사용하고 있다면 여러 코드를 찾아봐야 한다. 이런 경우 수동 빈으로 등록하거나 또는 자동으로하면 특정 패키지에 같이 묶어 두는 게 좋다!

빈 생명주기 콜백

: 데이터베이스 커넥션 풀이나, 네트워크 소켓처럼 애플리케이션 시작 시점에 필요한 연결을 미리 해두고, 애플리케이션 종료 시점에 연결을 모두 종료하는 작업을 진행하려면 객체의 초기화와 종료 작업이 필요하다.

스프링 빈은 간단하게 다음과 같은 라이프 사이클을 가진다.

객체 생성 -> 의존관계 주입

스프링 빈은 객체를 생성하고, 의존관계 주입이 다 끝난 다음에야 필요한 데이터를 사용할 수 있는 준비가 완료된다. 따라서 초기화 작업은 의존관계 주입이 모두 완료되고 난 다음에 호출해야 한다. 그런데 개발자가 의존관계 주입이 모두 완료된 시점을 어떻게 알 수 있을까?
스프링은 의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 통해서 초기화 시점을 알려주는 다양한 기능을 제공한다. 또한 스프링은 스프링 컨테이너가 종료되기 직전에 소멸 콜백을 준다. 따라서 안전하게 종료 작업을 진행할 수 있다.


스프링 빈의 이벤트 라이프사이클
스프링 컨테이너 생성 -> 스프링 빈 생성 -> 의존관계 주입 -> 초기화 콜백 -> 사용 -> 소멸전 콜백 -> 스프링 종료


참고: 객체의 생성과 초기화를 분리하자

생성자는 필수 정보(파라미터)를 받고, 메모리를 할당해서 객체를 생성하는 책임을 가진다. 반면에 초기화는 이렇게 생성된 값들을 활용해서 외부 커넥션을 연결하는 등 무거운 동작을 수행한다. 따라서 생성자 안에서 무거운 초기화 작업을 함께 하는 것 보다는 객체를 생성하는 부분과 초기화 하는 부분을 명확하게 나누는 것이 유지보수 관점에서 좋다. 물론 초기화 작업이 내부 값들만 약간 변경하는 정도로 단순한 경우에는 생성자에서 한 번에 다 처리하는 게 더 나을 수 있다.

참고: 싱글톤 빈들은 스프링 컨테이너가 종료될 때 싱글톤 빈들도 함께 종료되기 때문에 스프링 컨테이너가 종료되기 직전에 소멸전 콜백이 일어난다. 싱글톤처럼 컨테이너의 시작과 종료까지 생존하는 빈도 있지만, 생명주기가 짧은 빈들도 있는데, 이 빈들은 컨테이너와 무관하게 해당 빈이 종료되기 직전에 소멸 전 콜백이 일어난다. -> 자세한 내용은 빈 스코프 참고

스프링은 크게 3가지 방법으로 빈 생명주기 콜백을 지원한다.

  • 인터페이스 (InitializingBean, DisposableBean)
  • 설정 정보에 초기화 메서드, 종료 메서드 지정
  • @PostConstruct, @PreDestroy 애노테이션 지원

[ 인터페이스 - InitializingBean, DisposableBean ]

  • InitializingBeanafterPropertiesSet()메서드로 초기화를 지원한다.
  • DisposableBeandestroy()메서드로 소멸을 지원한다.
  • 초기화, 소멸 인터페이스 단점
    • 이 인터페이스는 스프링 전용 인터페이스다. 해당 코드가 스프링 전용 인터페이스에 의존한다.
    • 초기화, 소멸 메서드의 이름을 변경할 수 없다.
    • 코드를 고칠 수 없는 외부 라이브러리에 적용할 수 없다.

참고: 이 인터페이스를 사용하는 초기화, 종료 방법은 스프링 초창기에 나온 방법들이고, 지금은 다음의 더 나은 방법들이 있어서 거의 사용하지 않는다.

[ 빈 등록 초기화, 소멸 메서드 지정 ]

  • 설정 정보에 @Bean(initMethod = "init", destroyMethod = "close") 처럼 초기화, 소멸 메서드를 지정할 수 있다.
  • 설정 정보 사용 특징
    • 메서드 이름을 자유롭게 줄 수 있다.
    • 스프링 빈이 스프링 코드에 의존하지 않는다.
    • 코드가 아니라 설정 정보를 사용하기 때문에 코드를 고칠 수 없는 외부 라이브러리에도 초기화, 종료 베서드를 적용할 수 있다.
  • 종료 메서드 추론
    • @Bean의 destroymethod 속성에는 아주 특별한 기능이 있다.
    • 라이브러리 대부분 close, shutdown 이라는 이름의 종료 메서드를 사용한다.
    • @Bean의 destroyMethod는 기본값이 (inferred)(추론)으로 등록되어 있다.
    • 이 추론 기능은 close, shutdown이라는 이름의 메서드를 자동으로 호출해준다. 이름 그대로 종료 메서드를 추론해서 호출해준다.
    • 따라서 직접 스프링 빈으로 등록하면 종료 메서드는 따로 적어주지 않아도 잘 작동한다.
    • 추론 기능을 사용하기 싫으면 destroyMethod=""처럼 빈 공백을 지정하면 된다.

[ 애노테이션 - @PostConstruct, @PreDestroy ]

  • @PostConstruct, @PreDestroy 이 두 애노테이션을 사용하면 가장 편리하게 초기화와 종료를 실행할 수 있다.
  • 특징
    • 최신 스프링에서 가장 권장하는 방법이다.
    • 애노테이션 하나만 붙이면 되므로 매우 편리하다.
    • 패키지를 잘 보면 javax.annotation.PostConstruct이다. 스프링에 종속적인 기술이 아니라 JSP-250라는 자바 표준이다. 따라서 스프링이 아닌 다른 컨테이너에서도 동작한다.
    • 컴포넌트 스캔과 잘 어울린다.
    • 유일한 단점은 외부 라이브러리에는 적용하지 못한다는 것이다. 외부 라이브러리를 초기화, 종료해야 한다면 @Bean의 기능을 사용하다.

빈 스코프

[ 빈 스코프란? ]

: 스프링 빈은 스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가 종료될 때까지 유지된다. 이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다. 스코프는 번역 그대로 빈이 존재할 수 있는 범위를 의미한다.

스프링은 다음과 같은 다양한 스코프를 지원한다.

  • 싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.
  • 프로토타입: 스프링 컨테이너는 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.
  • 웹 관련 스코프
    • request: 웹 요청이 들어오고 나갈 때까지 유지되는 스코프이다.
    • session: 웹 세션이 생성되고 종료될 때까지 유지되는 스코프이다.
    • application: 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프이다.

빈 스코프는 다음과 같이 지정할 수 있다.

컴포넌트 스캔 자동 등록

@Scope("prototype")
@Component
public class HelloBean {}

수동 등록

@Scope("prototype")
@Bean
PrototypeBean HelloBean() {
  return new HelloBean();
}

[ 프로토타입 스코프 ]

: 싱글톤 스코프의 빈 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다. 반면에 프로토타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다.

  • 프로토타입 빈 요청
    1. 프로토타입 스코프의 빈을 스프링 컨테이너에 요청한다.
    2. 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다.
    3. 스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환한다.
    4. 이후에 스프링 컨테이너에 같은 요청이 오면 항상 새로운 프로토타입 빈을 생성해서 반환한다.

여기서 핵심은 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리한다는 것이다. 클라이언트에 빈을 반환하고, 이후 스프링 컨테이너는 생성된 프로토타입 빈을 관리하지 않는다. 프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에 있다. 그래서 @PreDstroy같은 종료 메서드가 호출되지 않는다. 종료 메서드에 대한 호출도 클라이언트가 직접 해야한다.


[ 프로토타입 스코프 - 싱글톤 빈과 함께 사용 시 문제점 ]

스프링 컨테이너에 프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해서 반환한다. 하지만 싱글톤 빈과 함께 사용할 때는 의도한 대로 잘 동작하지 않으므로 주의해야 한다.

< 프로토타입 빈 직접 요청 >

  1. 클라이언트A와 B는 스프링 컨테이너에 프로토타입 빈을 요청한다.
  2. 스프링 컨테이너는 프로토타입 빈을 각각 새로 생성해서 반환한다.
  3. 클라이언트A의 프로토타입 빈(x01)과 B의 프로토타입 빈(x02)은 다른 인스턴스다.

< 싱글톤 빈에서 프로토타입 빈 사용 >

  1. 싱글톤 빈은 의존관계 자동 주입을 사용한다. 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청한다.
  2. 스프링 컨테이너는 프로토타입 빈을 생성해서 싱글톤 빈에 반환한다.
  3. 이제 싱글톤 빈은 주입받은 프로토타입 빈을 내부 필드에 보관한다. (정확히는 참조값을 보관한다.)
  4. 클라이언트 A와 B는 싱글톤 빈을 스프링 컨테이너에 요청해서 받는다. 싱글톤이므로 항상 같은 스프링 빈이 반환된다.
  5. 클라이언트A와 B는 반환받은 싱글톤 빈을 통해 프로토타입 빈을 사용한다.
  6. 여기서 클라이언트A의 프로토타입 빈(x01)과 B의 프로토타입 빈(x01)은 같은 인스턴스다.
    • 이 싱글톤이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다. 주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성된 것이지, 사용할 때마다 새로 생성되는 것이 아니다.

문제 - 스프링은 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 된다. 그런데 싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에 프로토타입 빈이 새로 생성되기는 하지만, 싱글톤 빈과 함께 계속 유지되는 것이 문제다.

프로토타입 빈을 사용하면서 이런 결과를 원했던 것은 아닐 것이다. 프로토타입 빈을 주입 시점에만 새로 생성하는 게 아니라, 사용할 때마다 새로 생성해서 사용하는 것을 원할 것이다.

참고: 여러 빈에서 같은 프로토타입 빈을 주입 받으면, 주입 받는 시점에 각각 새로운 프로토타입 빈이 생성된다. 예를 들어서 클라이언트A, 클라이언트B가 각각 의존관계 주입을 받으면 각각 다른 인스턴스의 프로토타입 빈을 주입 받는다.

클라이언트A -> prototypeBean@x01
클라이언트B -> prototypeBean@x02

물론 사용할 때마다 새로 생성되는 것은 아니다.


[ 프로토타입 스코프 - 싱글톤 빈과 함께 사용 시 Provider로 문제 해결 ]

싱글톤 빈에서 프로토타입 빈을 사용할 때, 사용할 때마다 항상 새로운 프로토타입 빈을 생성하고 싶다면?

  1. 가장 간단한 방법은 싱글톤 빈이 프로토타입을 사용할 때마다 스프링 컨테이너에 새로 요청하는 것이다.

핵심코드

@Autowired
private ApplicationContext ac;

public int logic(){
    PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
    prototypeBean.addCount();
    int count = prototypeBean.getCount();
    return count;
}
  • 실행해보면 ac.getBean()을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
  • 의존관계를 외부에서 주입(DI)받는 게 아니라 이렇게 직접 필요한 의존관계를 찾는 것을 Dependency Lookup(DL) 의존관계 조회(탐색) 라고한다.
  • 이렇게 스프링 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워진다.
  • 필요한 기능은 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 딱! DL정도의 기능만 제공하는 무언가가 있으면 된다.

  1. ObjectFactory, ObjectProvider

지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 바로 ObjectProvider이다. 과거에는 ObjectFactory가 있었는데, 여기에 편의 기능을 추가해서 ObjectProvider가 만들어졌다.

핵심코드

@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;

public int logic() {
  PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
  prototypeBean.addCount();
  int count = prototypeBean.getCount();
  return count;
}
  • 실행해보면 prototypeBeanProvider.getObject()를 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인한 수 있다.
  • ObjectProvidergetObject()를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)
  • 스프링이 제공하는 기능을 사용하지만, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.
  • ObjectProvider는 지금 딱! 필요한 DL 정도의 기능만 제공한다.
  • 특징
    • ObjectFactory: 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존
    • ObjectProvider: ObjectFactory 상속, 옵션, 스트림 처리 등 편의 기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존

  1. JSP-330 Provider

마지막 방법은 javax.inject.Provider라는 JSR-330 자바 표준을 사용하는 방법이다. 이 방법을 사용하려면 javax.inject:javax.inject:1 라이브러리를 gradle에 추가해야 한다.

javax.inject.Provider 참고용 코드

package javax.inject;
public interface Provider<T> {
    T get(); 
}

핵심 코드

// implementation 'javax.inject:javax.inject:1' gradle 추가 필수
@Autowired
private Provider<PrototypeBean> provider;

public int logic() {
    PrototypeBean prototypeBean = provider.get();
    prototypeBean.addCount();
    int count = prototypeBean.getCount();
    return count;
}
  • 실행해보면 provider.get()을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
  • poviderget()을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)
  • 자바 표준이고, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.
  • Povider는 지금 딱! 필요한 DL 정도의 기능만 제공한다.
  • 특징
    • get()메서드 하나로 기능이 매우 단순하다.
    • 별도의 라이브러리가 필요하다.
    • 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.

< 정리 >
프로토타입 빈은 언제 사용할까? 매번 사용할 때마다 새로운 객체가 필요할 때 사용하면 된다. 하지만 실무에서 웹 애플리케이션을 개발하다보면, 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매운 드물다고 함.

ObjectProvider, JSR330 Provider등은 프로토타입 뿐만 아니라 DL이 필요한 경우는 언제든지 사용할 수 있다.

참고: 자바 표준인 JSR-330 Provider를 사용할 것인지, 스프링이 제공하는 ObjectProvider를 사용할 것인지 고민이 될 것이다. ObjectProvider는 DL을 위한 편의 기능을 많이 제공해주고 스프링 외에 별도의 의존관계 추가가 필요 없기 때문에 편리하다. 그럴 일은 거의 없겠지만, 코드를 스프링이 아닌 다른 컨테이너에서도 사용할 수 있어야 한다면 JSR-330 Provider를 사용해야 한다.

스프링을 사용하다 보면 이 기능 뿐만 아니라 다른 기능들도 자바 표준과 스프링이 제공하는 기능이 겹칠때가 많이 있다. 대부분 스프링이 더 다양하고 편리한 기능을 제공해주기 때문에 특별히 다른 컨테이너를 사용할 일이 없다면, 스프링이 제공하는 기능을 사용하면 된다.

참고: 스프링이 제공하는 메서드에 @Lookup애노테이션을 사용하는 방법도 있지만 이전 방법들로 충분하고, 고려해야할 내용도 많아서 생략하였음.


[ 웹 스코프 ]

: 웹 환경에서만 동작하는 스코프로, 웹스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다. 따라서 종료 메서드가 호출된다.

  • 웹 스코프의 종류
    • request: HTTP 요청 하나가 들어오고 나갈 때까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고 관리된다.
    • session: HTTP Session과 동일한 생명주기를 가지는 스코프
    • application: 서블릿 컨텍스트(ServletContext)와 동일한 생명주기를 가지는 스코프
    • websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프

참고: 강의에선 request 스코프로 설명 진행했음. 나머지도 범위만 다르지 동작 방식은 비슷하다고 함.

< request 스코프 >
Povider를 사용하지 않고 request 스코프 빈을 싱글톤 필드에 두고 스프링 애플리케이션을 실행 시키면 오류가 발생한다. 스프링 애플리케이션 실행하는 시점에 싱글톤 빈은 생성해서 주입이 가능하지만, request 스코프 빈은 그렇지 않다. 실제 고객의 요청이 와야 생성할 수 있다.

  • ObjectProvider 또는 JSR330 Provider 덕분에 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있다.
  • Provider로 컨트롤러와 서비스에서 각각 한번씩 따로 호출해도 같은 HTTP 요청이면 같은 스프링 빈이 반환된다.

[ 스코프와 프록시 ]

프록시 방식 (전체코드 바로가기)

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
  • proxyMode = scopedProxyMode.TARGET_CLASS 여기가 핵심이다.
    • 적용 대상이 인터페이스가 아닌 클래스면 TARGET_CLASS를 선택
    • 적용 대상이 인터페이스면 INTERFACE를 선택
  • 이렇게 하면 MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있다.
  • Provider를 사용하지 않고 코드 구현이 가능하다.

< 웹 스코프와 프록시 동작 원리 >

  1. 먼저 주입된 myLogger를 확인해보자.

출력결과

myLogger = class hello.core.common.MyLogger$$EnhancerBySpringCGLIB$$b68b726d
  • CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.
    • @ScopeproxyMode = ScopedProxyMode.TARGET_CALSS를 설정하면 스프링 컨테이너는 CGLIB라는 바이트코드를 조작하는 라이브러리를 사용해서, MyLogger를 상속받은 가짜 프록시 객체를 생성한다.
    • 결과를 확인해보면 우리가 등록한 순수한 MyLogger 클래스가 아니라 MyLogger$$EnhancerBySpringCGLIB이라는 클래스로 만들어진 객체가 대신 등록된 것을 확인할 수 있다.
    • 그리고 스프링 컨테이너에 "myLogger"라는 이름으로 진짜 대신에 이 가짜 프록시 객체를 등록한다.
    • ac.getBean("myLogger", MyLogger.class)로 조회해도 프록시 객체가 조회되는 것을 확인할 수 있다.
    • 의존관계 주입도 이 가짜 프록시 객체가 주입된다.

  • 가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.
    • 가짜 프록시 객체는 내부에 진짜 myLogger를 찾는 방법을 알고 있다.
    • 클라이언트가 myLogger.logic()을 호출하면 사실은 가짜 프록시 객체의 메서드를 호출한 것이다.
    • 가짜 프록시 객체는 request 스코프의 진짜 myLogger.logic()을 호출한다.
    • 가짜 프록시 객체는 원본 클래스를 상속 받아서 만들어졌기 때문에 이 객체를 사용하는 클라이언트 입장에서는 사실 원본인지 아닌지도 모르게 동일하게 사용할 수 있다. (다형성)
    • 가짜 프록시 객체는 실제 request scope과는 관계가 없다. 그냥 가짜이고, 내부에 단순한 위임 로직만 있고, 싱글톤처럼 동작한다.

  • 특징
    • 프록시 객체 덕분에 클라이언트는 마치 싱글촌 빈을 사용하듯이 편리하게 request scope을 사용할 수 있다.
    • Provider를 사용하든 프록시를 사용하든 핵심은 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 것이다.
    • 단지 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있다. 이것이 바로 다형성과 DI 컨테이너가 가진 큰 강점이다.
    • 꼭 웹 스코프가 아니어도 프록시는 사용할 수 있다.

  • 주의점
    • 마치 싱글톤을 사용하는 것 같지만 다르게 동작하기 때문에 결국 주의해서 사용해야 한다.
    • 이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하자. 무분별하게 사용하면 유지모수하기 어려워진다.