# 스프링

  • 스프링은 좋은 객체지향 애플리케이션을 개발할 수 있게 도와주는 프레임워크이다.
  • 객체지향의 다양한 특성들 중 다형성을 극대화해서 이용할 수 있도록 도와준다.
  • SOLID 원칙을 기억하자.
    1. SRP(Single Responsibility Principle): 단일 책임 원칙
      • 한 클래스는 하나의 책임만 갖는다.
      • 변경의 크기보다는, 변경이 있을때 파급효과가 적은 것에 집중.
    2. OCP(Open/Closed Principle): 개방-폐쇄 원칙
      • 확장에는 열려있으나 변경에는 닫혀있어야 함
      • 다형성을 활용
    3. LSP(Liskov Substitution Principle): 리스코프 치환 원칙
      • 프로그램 객체는 프로그램 정확성을 깨지 않으면서 하위타입 인스턴스로 바꿀 수 있어야 함.
      • 하위 클래스는 인터페이스 규약을 모두 지켜야한다는 의미.
      • 자동차 클래스에서 액셀은 느리더라도 앞으로 가야함. 뒤로 가게 구현하면 안됨
    4. ISP(Interface Segregation Principle): 인터페이스 분리 원칙
      • 특정 클라이언트를 위한 인터페이스 여러개가 범용 인터페이스 하나보다 낫다.
    5. DIP(Dependency Inversion Principle): 의존관계 역전 원칙
      • 프로그래머는 추상화에 의존해야 하며 구체화에 의존하면 안된다.
  • 다형성만으로는 OCP, DIP 원칙을 지킬 수 없다.
    • OCP의 경우 new 키워드로 직접 생성하면, 구현체를 바꿀 때 해당 클래스 내부 코드를 수정해야 하므로 OCP를 위반한다
    • DIP의 경우 new로 구현체를 직접 생성하는 순간, 해당 클래스는 인터페이스가 아닌 구현 클래스를 직접 알게 되므로 추상이 아닌 구체에 의존하게 된다.
  • 스프링은 DI(Dependency Injection)으로 의존성을 주입하도록 컨테이너를 제공한다.
    • 클라이언트 코드 변경이 없이 기능을 확장해준다.
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();

    @Override
    public Order createOrder(Long memberId, String itemName, int itemPrice) {
        Member member = memberRepository.findById(memberId);
        int discountPrice = discountPolicy.discount(member, itemPrice);

        return new Order(memberId, itemName, itemPrice, discountPrice);
    }
}
  • 위와 같이 주문생성 및 관리 객체가 있을때 저장소 정책과 할인정책이 변경되는 경우 구현체를 교체해줘야 하는데 이는 클라이언트 코드의 변경이 필요하다.
  • 현재 위의 서비스 객체가 구현체를 주입하는 역할까지 하고 있기 때문에, 구현 객체를 생성하고 연결하는 책임을 갖는 객체를 별도로 생성한다.
    • 이때 AppConfig가 등장한다.
public class AppConfig {
    public MemberService memberService() {
        return new MemberServiceImpl(memberRepository);
    }

    public OrderService orderService() {
        return new OrderServiceImpl(
                memberRepository(),
                discountPolicy(),
        );
    }

    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    public DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }
}
  • 위와 같이 프로그램 제어의 책임을 객체 자신이 갖지않고 외부가 갖게 되는 것을 제어의 역전이라고 한다. (IoC)
    • Impl 객체들은 자신의 로직을 실행하는 역할만 한다.
  • 프레임워크가 내가 작성한 코드를 제어하고 대신 실행하면 그것은 프레임워크이다.
  • 내가 작성한 코드가 직접 제어 흐름을 담당한다면 그것은 라이브러리이다.

의존관계 주입

  • 의존관계는 정적인 클래스 의존 관계와 실행 시점에 결정되는 동적 객체 의존관계를 구분해야 한다.
  • OrderServiceImpl 클래스는 추상화 인터페이스 MemberRepository, DiscountPolicy에 의존한다.
    • 이 의존관계만으로는 실제로 어떤 객체가 OrderServiceImpl에 주입될지 알수없다.
  • 위의 AppConfig와 같이 객체를 생성, 관리하면서 의존관계를 연결해주는 것을 IoC 컨테이너 또는 DI 컨테이너라 한다.
@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(
                memberRepository()
        );
    }

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(
                memberRepository(),
                discountPolicy()
        );
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }

    @Bean
    public DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
    }
}
  • 위와 같이 스프링 기반으로 DI 컨테이너를 정의할 수 있다.
  • 메인함수에서 DI 컨테이너를 생성하고 호출하는 코드는 다음과 같다.
public class MemberApp {
    public static void main(String[] args) {
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
        MemberService memberService = applicationContext.getBean("memberService", MemberService.class);

        Member member = new Member(1L, "memberA", Grade.VIP);
        memberService.join(member);

        // ..
    }
}
  • ApplicationContext를 스프링 컨테이너라 한다.
  • 기존에는 개발자가 직접 AppConfig를 사용하여 직접 객체를 생성하고 DI를 했지만, 스프링 컨테이너를 통해 사용한다.
  • 스프링 컨테이너는 @Configuration이 붙은 AppConfig 클래스를 설정 정보로 사용한다.
    • @Bean이라 적힌 메서드를 모두 호출하여 반환된 객체를 스프링 컨테이너에 등록한다.
    • 스프링 컨테이너에 등록된 객체를 스프링 빈이라 한다.
  • 스프링 빈은 @Bean 애노테이션이 붙은 메서드명을 스프링 빈 이름으로 사용한다.

# 스프링 컨테이너와 스프링 빈

ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);
  • ApplicationContext를 스프링 컨테이너라 하며, 인터페이스에 해당한다.
  • AppConfig 방식이 애노테이션 기반 자바 설정 클래스로 스프링 컨테이너를 만든것과 같다.
  • AnnotationConfigApplicationContext는 스프링 컨테이너 인터페이스의 구현체이다.
  • 스프링 컨테이너 생성 뒤에는 스프링 빈을 등록한다.
@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(
                memberRepository()
        );
    }
    // ..
}
  • Bean의 이름은 메서드이름을 사용하고, Bean 객체는 return하는 객체를 사용한다.
  • Bean의 이름은 항상 다른 이름을 부여해야 한다.
  • 의존관계 설정은 생성자 파라미터를 활용하여 처리한다.
@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(
                memberRepository()
        );
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
}
  • 위와 같이 의존관계를 설정하면, memberService가 memberRepository에 의존하는 관계가 된다.
  • Bean을 조회하는 방법은 다음과 같다.
    • ..applicationContext.getBean(Bean이름, 타입);
    • ..applicationContext.getBean(타입)
    • 조회 대상 빈이 없으면 NoSuchBeanDefinitionException예외가 발생한다.
@Component public class MemoryMemberRepository implements MemberRepository { }
@Component public class JdbcMemberRepository implements MemberRepository { }
  • 위와 같이 같은 타입의 빈이 둘 이상이면 NoUniqueBeanDefinitionException 타입으로만 빈을 조회할때 예외가 발생한다.
    • 이때는 빈의 이름까지 지정하여 조회하면 된다.
  • getBeansOfType(타입.class) 메서드를 사용하면 파라미터에 전달된 타입을 구현한 모든 Bean들을 조회할 수 있다.

BeanFactory와 ApplicationContext

  • 빈팩토리는 스프링 컨테이너의 최상위 인터페이스이다.
    • 스프링 빈을 관리하고 조회하는 역할
    • getBean 제공
  • ApplicationContext
    • 빈 팩토리 기능을 모두 상속받아 제공한다.
    • 빈 관리기능에 더불어 여러 부가기능들을 제공한다.
    • 환경변수, 메시지 소스를 활용한 로컬라이제이션, 파일 등의 리소스 조회, 애플리케이션 이벤트 발행 및 구독하는 기능 등
  • 빈 팩토리를 직접 사용할 일은 거의 없다.
  • 스프링 컨테이너는 다양한 형식의 설정 정보를 받아들일 수 있게 설계되어 있다.
    • 자바 코드, XML, 그루비 등
    • 스프링 설정은 스프링 컨테이너에게 빈 등록과 의존관계를 알려주는 것을 의미한다.
    • AnnotationConfigApplicationContext: 자바 코드 기반
    • GenericXmlApplicationContext: XML 기반
    • 기타 등등
  • 위와 같이 다양한 설정 형식 지정이 가능한 이유는 BeanDefinition 추상화가 존재하기 때문이다.
    • 스프링 컨테이너는 BeanDefinition에 의존하고, 무엇으로 구현되어있는지는 모른다.
    • BeanDefinition은 빈 설정 메타정보라고 한다.
    • 빈 메타정보는 빈 클래스명, 싱글턴 여부, 실제 빈 사용 전까지 지연처리 여부 등에 대한 내용을 상세히 담고 있다.
  • AnnotationConfig.., GenericXml.. 등 각 형식별로 Reader가 존재하는데, 이들이 설정 정보들을 읽어들인다.
    • AnnotatedBeanDefinitionReader, XmlBeanDefinitionReader등이 존재한다.
    • 이 메타정보를 스프링 컨테이너 구현체가 읽어들인다.

# 싱글톤 컨테이너

  • 일반적인 웹 애플리케이션은 여러 유저들이 동시에 요청을 하게 된다.
  • 이때 스프링없이 DI 컨테이너를 참조하게 되는 경우 컨테이너에 요청을 할때마다 새로 객체를 생성하게 된다.
    • 이로 인한 메모리 낭비가 심해지게 된다.
public class SingletonService {
    private static final SingletonService instance = new SingletonService();

    public static SingletonService getInstance() {
        return instance;
    }

    private SingletonService() { }

    private void logic() {
        System.out.println("싱글턴 호출");
    }
}
  • 생성자 함수를 private으로 만들고, 해당 객체를 생성 후 static 영역의 멤버로 할당하는 구조이다.
  • 직접 구현한 싱글턴에는 테스트의 어려움, 구현체에 의존하는 DIP 위반 등의 많은 문제들을 갖는다.
  • 반면 스프링 컨테이너를 사용하게 되면 싱글턴 패턴 적용 없이도 빈 객체를 싱글톤으로 관리한다.
ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

MemberService memberService1 = ac.getBean("memberService", MemberService.class);
MemberService memberService2 = ac.getBean("memberService", MemberService.class);
  • 위 코드에서 memberService1과 2는 같은 참조값을 갖는다.
  • 싱글턴은 항상 하나의 객체를 공유하기 때문에 stateless 하게 설계해야 한다.
    • 가급적 읽기만 가능해야하며, 공유되지 않는 지역변수 및 파라미터 등만 사용해야 한다.
  • 아래 코드를 보자.
@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        return new MemberServiceImpl(
                memberRepository()
        );
    }

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(
                memberRepository(),
                discountPolicy()
        );
    }

    @Bean
    public MemberRepository memberRepository() {
        return new MemoryMemberRepository();
    }
    // ..
}
  • 스프링 컨테이너는 싱글턴 레지스트리로서, 내부적으로는 스프링 빈이 싱글톤이 되도록 보장해준다.
  • 하지만 memberRepository를 멤버 서비스와 오더 서비스에서 각자 new 키워드와 함께 생성자를 호출하고 있기 때문에 독립적인 객체를 호출하고 있는 것으로 보인다.
  • 스프링은 내부적으로 클래스 바이트코드를 조작하는 라이브러리를 사용한다.
    • @Configuration을 적용한 AppConfig가 이를 처리해준다.
    • AnnotationConfigApplicationContext(AppConfig.class) 생성자 함수에서 파라미터로 넘긴 값 AppConfig.class도 스프링 빈으로 등록된다.
name= appConfig object=hello.core.AppConfig$$SpringCGLIB$$0@64da2a7
  • 실제로 AppConfig 스프링 빈을 출력해보면 ..CGLIB..라는 이름으로 클래스 이름에 접미사가 복잡하게 추가로 붙는 것을 볼 수 있다.
  • 스프링이 런타임에 AppConfig클래스를 직접 활용하는 것이 아닌 CGLIB라는 바이트코드 조작 라이브러리를 사용하여 AppConfig를 상속받는 임의의 클래스를 만들게 된다. 이후 해당 클래스를 스프링 빈으로 등록하게 된다.
    • 해당 클래스가 싱글톤이 되도록 보장한다.
  • @Configuration 애노테이션이 빈을 싱글턴으로 접근할 수 있게 보장해주며, @Bean 애노테이션만 사용하는 경우에는 보장하지 않는다.

# 컴포넌트 스캔

  • 스프링 빈 등록시 지금까지는 @Bean 애노테이션 등을 사용하여 직접 스프링 빈들을 컨테이너 내에 나열했다.
  • 스프링에서는 설정정보 없이도 빈을 등록해주는 컴포넌트 스캔 기능을 제공한다.
    • 의존관계도 자동으로 주입하는 @Autowired 기능도 제공한다.
import static org.springframework.context.annotation.ComponentScan.*;

@Configuration
@ComponentScan(excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {
}
  • 컴포넌트 스캔 사용을 위해서는 @Component 애노테이션을 설정 정보에 붙여준다.
    • 컴포넌트 스캔 사용시 @Configuration이 붙은 다른 설정 정보들도 함께 등록되는데, 이는 @Configuration 내부에 @Component가 포함되어 있기 때문이다.
    • 위 코드는 Configuration 이름으로 애노테이션이 붙은 대상은 컴포넌트 스캔 정보에서 제외하는 코드이다.
    • 보통 설정 정보를 컴포넌트 스캔 대상에서 제외하지는 않는다.
  • 아래와 같이 클래스들이 컴포넌트 스캔 대상이 되도록 애노테이션을 붙여줘야 한다.
@Component
public class MemoryMemberRepository implements MemberRepository { }
  • @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;
    }
    // ..
}
  • 생성자에서 여러 의존관계도 한번에 주입 가능하다.

# 컴포넌트 스캔 동작 과정

  • @ComponentScan@Component가 붙은 모든 클래스들을 스프링 빈으로 등록한다.
    • 빈 이름: 클래스명을 그대로 사용하되 첫글자만 소문자로 사용한다.
    • 빈 이름 직접 지정: `@Component("myComponent")``
  • 생성자에 @Autowired를 지정하면 스프링 컨테이너가 해당 스프링 빈을 찾아 주입해준다.
    • 위 예시 코드에서 MemberRepository 스프링 빈을 찾아서 주입
    • 기본적으로는 타입이 같은 빈을 찾아 주입한다.
  • 여러 파라미터가 있더라도 직접 찾아 주입한다.

컴포넌트 탐색 위치 지정

@ComponentScan(
    basePackages = "hello.core"
    // basePackages = {"hello.core", "hello.service"}
)
  • 모든 자바 클래스에 대해 컴포넌트 스캔을 시도하면 시간이 오래 걸리기 때문에 필요한 위치부터 탐색하도록 위치 지정이 가능하다.
    • 위 코드의 경우 hello.core패키지를 비롯하여 하위 패키지 전체를 스캔한다.
    • 중괄호와 함께 여러 패키지 위치 지정도 가능하다.
  • 실제로는 basePackages 지정 없이 설정정보 클래스를 프로젝트 최상단에 두는 방법이 일반적으로 사용된다.

# 컴포넌트 스캔 대상

  • 컴포넌트 스캔은 컴포넌트 애노테이션 외에 다른 내용들도 스캔 대상에 포함한다.
    • @Component: 컴포넌트 스캔에서 사용
    • @Controller: 스프링 MVC 컨트롤러에서 사용
    • @Service: 스프링 비즈니스 로직에서 사용
      • 별도의 처리는 없고, 핵심 비즈니스 로직이 여기 있음을 개발자들이 알기 쉽게 해줌.
    • @Repository: 스프링 데이터 접근 계층에서 사용
      • 데이터 계층 예외를 스프링 예외로 변환도 해줌
    • @Configuration: 스프링 설정 정보에서 사용
      • 스프링 빈이 싱글턴을 유지하도록 추가 처리해줌
  • 위 대상들은 내부적으로 @Component를 이미 포함하고 있다.
  • 애노테이션 자체로는 상속기능이랄게 없기 때문에 위와 같은 기능은 자바가 아닌 스프링이 자체적으로 지원하는 기능이다.
  • 필터를 통해 컴포넌트 스캔 대상 필터링 조건을 추가할 수 있다.
    • includeFilters: 스캔 대상을 추가로 지정
    • excludeFilters: 스캔 제외 대상을 지정
    • @ComponentScan(includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class))
  • 필터타입 옵션은 5가지가 존재한다.
    • ANNOTATION: 기본값, 애노테이션 인식 후 동작
    • ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식하여 동작
    • ASPECTJ: AspectJ 패턴 사용
    • REGEX: 정규 표현식
    • CUSTOM: TypeFilter 인터페이스 구현 후 처리
  • 자동 빈 등록과 수동 빈 등록의 이름 충돌 시 최근 버전 스프링에서는 에러가 나도록 개선되었다.
  • 자동 빈 등록끼리는 이름 충돌 시 항상 예외가 발생한다.

# 의존관계 자동 주입

  • 의존관계 주입 방법은 네가지가 있다.
    1. 생성자를 통한 주입
    2. 수정자(setter)를 통한 주입
    3. 필드 주입
    4. 일반 메서드 주입

# 생성자 주입

  1. 생성자 호출 시점에 한번만 호출되는 것이 보장
  2. 불변 및 필수 의존관계 사용
@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를 생략해도 자동으로 주입된다.

# 수정자(setter) 주입

  • 필드값을 변경하는 수정자 메서드를 통해 의존관계를 주입한다.
  • 선택 / 변경 가능성이 있을때 사용한다.
@Component
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

    @Autowired
    public void setDiscountPolicy(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
  • Autowired는 주입할 대상 빈이 컨테이너에 등록되어있지 않은 경우 오류가 발생한다.
  • 자바빈 프로퍼티 규약은 setXX, getXX라는 메서드를 통해 값 읽고 쓰기 작업을 처리하는 규칙을 만들었는데, 이를 자바빈 프로퍼티 규약이라 한다.
    • 함수명 작성시 접두사를 위와같이 붙여야한다.
    • 위 규약을 지키지 않는 경우 스프링에서 수정자 주입으로 인식하지 못한다.

필드 주입, 일반 메서드 주입

  • 필드 주입
    • 필드에 바로 주입하는 방식이다.
    • 사용하지 않는 것이 좋다.
  • 일반 메서드 주입
    • 일반 메서드를 통해 주입받는다.
    • 여러 필드를 주입받을 수 있지만, 잘 사용하지 않는다.

롬복 라이브러리

  • @RequiredArgsConstructor 기능을 사용하면 final이 붙은 필드를 모아서 생성자를 자동으로 만들어준다.
/// 생성자 정의 없이도 호출 가능
@Component
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
}
  • 최근에는 생성자 하나만 두고 @Autowired를 생략하고 롬복 라이브러리를 적용하는 패턴을 자주 사용한다.

# 조회된 빈이 2개 이상일때

  • 같은 타입의 @Component로 선언한 두 빈이 있고, @Autowired로 해당 타입을 주입받으려 하면 NoUniqueBeanDefinitionException 오류가 발생한다.
  • 같은 타입의 빈이 2개 이상일때의 문제를 해결하는 방법들이다.
    1. @Autowired 필드명 매칭
    2. @Qualifier끼리 매칭하여 빈 이름 매칭
    3. @Primary
  • @Autowired 필드명 매칭
    • 타입매칭을 시도하고, 여러 빈이 있으면 필드 이름, 파라미터 이름으로 빈 이름을 추가로 매칭한다.
    • 빈이 컨테이너에 등록될때 첫글자를 소문자로 하여 등록하기 때문에 가능한 동작이다.
// 빈 이름: memoryMemberRepository
@Component
public class MemoryMemberRepository implements MemberRepository { }

// 파라미터명이 빈 이름과 일치 → 매칭 성공
@Autowired
public OrderService(MemberRepository memoryMemberRepository) { }
  • @Qualifier
    • 추가 구분자를 붙여주는 방식이다.
    • 생성자 주입 시 지정한 구분자를 파라터에 명시해야 한다.
@Component
@Qualifier("mainDiscountPolicy")
public class RateDiscountPolicy implements DiscountPolicy { }

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

//..

@Autowired
public OrderServiceImpl(
    MemberRepository memberRepository,
    @Qualifier("mainDiscountPolicy") DiscountPolicy discountPolicy
) {
    //..
}
  • @Primary
    • 우선순위를 정하는 방법이다.
    • 여러 빈이 매칭되면 @Primary가 우선권을 갖는다.
    • 테스트용 DB 환경을 주입한다거나 할때 사용한다.
@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy { }

@Component
public class FixDiscountPolicy implements DiscountPolicy { }

# 빈 생명주기 콜백

  • DB 커넥션 풀, 네트워크 소켓처럼 애플리케이션 시작 시점에 연결을 해두고 종료 시점에 해제하는 작업들이 필요하다.
  • 스프링에서는 빈 객체를 생성하고 의존관계 주입까지 완료되어야 해당 객체의 데이터들을 활용할 준비가 된다.
    • 의존관계 주입까지 완료된 시점을 스프링에서 콜백 메서드를 통해 제공해준다.
  • 초기화 시점을 알려주기도, 스프링 컨테이너가 종료되기 직전 소멸 콜백을 주기도 한다.

객체 생성과 초기화의 분리

  • 생성자는 필수 정보(파라미터)를 받고 메모리 할당 후 객체를 생성하는 책임을 갖는다.
  • 초기화는 생성된 값들을 활용하여 외부 커넥션을 연결하는 등의 무거운 작업들을 수행한다.
  • 초기화 작업이 무겁다면 둘의 동작을 분리하는 것이 유지보수 관점에서 더 좋다.
  • 스프링에서는 3가지 방법으로 Bean 생명주기 콜백을 지원한다.
    1. 인터페이스(initializingBean, DisposableBean)
    2. 설정 정보에 초기화 메서드 / 종료 메서드 지정
    3. @PostConstruct, @PreDestroy 애노테이션

# 인터페이스(InitializingBean, DisposableBean)

  • InitializingBean: afterPropertiesSet(), 초기화 지원
    • 빈 생성(생성자 호출) 이후, 의존관계 주입이 완료된 후에 호출됨
  • DisposableBean: destroy()
    • 빈 소멸 직전 콜백으로 호출
  • 스프링 전용 인터페이스이므로, 초기화 및 소멸 메서드 이름을 변경할 수 없다.
  • 최근에는 거의 사용하지 않음.

# 빈 등록 초기화 / 소멸 메서드 지정

  • 설정 정보에 @Bean(initMethod = "init", destroyMethod = "close")와 같이 초기화 및 소멸 메서드를 직접 지정할 수 있다.
public class NetworkClient {

    private String url;

    public NetworkClient() {
        // ..
    }

    public void init() {
        // 직접 정의한 초기화 함수
    }

    public void close() {
        // 직접 정의한 소멸 함수
    }
}
//..

@Configuration
static class LifeCycleConfig {

    @Bean(initMethod = "init", destroyMethod = "close")
    public NetworkClient networkClient() {
        NetworkClient networkClient = new NetworkClient();
        // ..
        return networkClient;
    }
}
  • 메서드 이름을 자유롭게 지정할 수 있고, 스프링 빈이 스프링 코드에 의존하지 않는 방식이다.
  • 설정 정보를 사용하기 때문에 외부 라이브러리에도 초기화 및 종료 메서드를 적용할 수 있다.
    • InitializingBean, DisposableBean을 사용하는 경우 래퍼 클래스를 중간 계층에 하나 두어 연결해줘야 함.
    • @Bean(initMethod, destroyMethod)을 사용하면 래퍼 클래스 없이 외부 라이브러리의 메서드를 직접 초기화/소멸 시점에 연결할 수 있다.

destroyMethod 추론

  • 대부분의 라이브러리는 close, shutdown등의 이름으로 종료 메서드를 사용한다.
  • @Bean의 destroyMethod는 기본값이 추론으로 등록되어, close, shutdown과 같은 이름의 메서드를 자동으로 호출해준다.
  • 스프링 빈으로 등록된 객체는 destroyMethod 지정 없이도 잘 동작한다.

# 애노테이션 @PostConstruct, @PreDestroy

public class NetworkClient {

    public NetworkClient() {
        System.out.println("생성자");
    }

    @PostConstruct
    public void init() {
        // ..
    }

    @PreDestroy
    public void close() {
        // ..
    }
}

// ..

@Configuration
static class LifeCycleConfig {

    @Bean
    public NetworkClient networkClient() {
        NetworkClient networkClient = new NetworkClient();
        // ..
        return networkClient;
    }
}
  • 위와 같이 사용하는 방식으로, 가장 간단하고 최신 스프링에서 권장되는 방식이다.
  • 스프링 종속적 기술이 아닌 자바 표준으로 동작한다.
  • @Component를 붙여 직접 빈으로 자동등록이 가능한 커스텀 클래스의 경우 위의 두 애노테이션을 사용하면 된다.

# Bean 스코프

  • 스프링은 다양한 스코프를 지원한다.
    • 싱글톤: 기본 스코프로, 스프링 컨테이너 시작과 종료까지 유지되는 스코프
    • 프로토타입: 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 스코프
    • 웹 관련 스코프
      • request: 웹 요청이 들어온 뒤 나갈때까지 유지되는 스코프
      • session: 웹 세션이 생성되고 종료시까지 유지되는 스코프
      • application: 웹 서블릿 컨텍스트와 같은 범위로 유지되는 스코프
  • 스코프는 @Scope를 통해 지정한다.
// 컴포넌트 스코프
@Scope("prototype")
@Component
public class HelloBean { }

// 빈 직접 지정 스코프
@Scope("prototype")
@Bean
PrototypeBean HelloBean() { }

# 프로토타입 스코프

  • 싱글톤 스코프는 빈 조회시 컨테이너가 항상 같은 인스턴스의 빈을 반환해준다.
  • 프로토타입 스코프는 컨테이너에 빈 조회 시 항상 새로운 인스턴스를 생성하여 반환해준다.
    • 새로운 빈을 생성하고 DI까지 새로 처리한다.
  • 프로토타입 빈은 컨테이너가 생성과 주입까지만 책임지고 이후 관리는 하지 않는다.
    • 따라서 컨테이너 종료 시 @PreDestroy 같은 소멸 메서드는 호출되지 않는다.
    • 초기화까지는 책임이 있기 때문에 @PostConstruct 메서드는 호출된다.

싱글톤 빈이 프로토타입 빈을 참조할때

  • DI 컨테이너 내에서 싱글톤 빈이 프로토타입 빈을 참조하는 구조를 생각해보자.
  • 클라이언트가 싱글톤 빈을 통해서만 프로토타입 빈의 로직을 수행하는 상황이다.
  • 이때 싱글톤 빈을 여러번 참조한다고 프로토타입 빈이 새로 인스턴스를 매번 생성하는 것이 아니다.
  • 컨테이너는 프로토타입 빈의 생성, 초기화, 의존관계 주입까지만 책임지고 이후 라이프사이클은 관리하지 않는다.
  • 따라서 클라이언트가 싱글톤 빈을 여러번 조회한다고 하여 프로토타입 빈이 매번 생성되는 것이 아니라 첫 초기화 시 생성된 참조를 계속해서 보유하고 있게 된다.
  • 의존관계를 주입받는 것을 DI, 직접 필요한 의존관계를 찾는 것을 Dependency Lookup(DL)이라고 한다.
  • 지정한 프로토타입 빈을 컨테이너에서 찾아주는 DL 기능을 ObjectProvider, ObjectFactory가 지원한다.

# ObjectFactory, ObjectProvider

@Autowired
private ObjectProvider<PrototypeBean> prototypeBean;

public int logic() {
    PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
    prototypeBean.addCount(); // 자체 로직
    // ..
}
  • ObjectProvider의 getObject 메서드 호출시 항상 새로운 프로토타입 빈이 생성된다.
    • 사실 getObject 메서드는 컨테이너에 빈을 새로 요청하는 함수로서, 싱글톤 스코프 빈을 요청하는 경우 기존 참조값을 그대로 반환해준다.
  • 또한 ObjectProvider는 호출시점에 빈이 생성되므로 빈을 지연생성하고싶은 경우 사용할 수 있다.
// 1. 프로토타입 or 무거운 빈
@Component
public class HeavyBean {
    public HeavyBean() {
        System.out.println("HeavyBean 생성"); // 생성 시점 확인용
    }
    public void doSomething() { }
}

// 2. ObjectProvider로 지연생성
@Component
public class OrderService {
    private final ObjectProvider<HeavyBean> heavyBeanProvider;

    @Autowired
    public OrderService(ObjectProvider<HeavyBean> heavyBeanProvider) {
        this.heavyBeanProvider = heavyBeanProvider;
    }

    public void logic() {
        HeavyBean bean = heavyBeanProvider.getObject(); // 여기서 HeavyBean 생성
        bean.doSomething();
    }
}

// 3. OrderService를 사용하는 클라이언트
@Component
public class ClientService {
    private final OrderService orderService;

    @Autowired
    public ClientService(OrderService orderService) {
        this.orderService = orderService;
    }

    public void doSomething() {
        orderService.logic();
    }
}

// 4. 컨테이너 실행
@SpringBootApplication
public class Main {
    public static void main(String[] args) {
        ApplicationContext ac = SpringApplication.run(Main.class, args);

        // 이 시점엔 HeavyBean 아직 생성 안됨
        ClientService clientService = ac.getBean(ClientService.class);

        // 이 호출 시점에 HeavyBean 생성됨
        clientService.doSomething();
    }
}
  1. 스프링 애플리케이션 실행
    1. 컴포넌트 스캔
    2. 빈 생성 및 의존관계 주입
      1. OrderService 싱글톤 빈 생성
        1. ObjectProvider<HeavyBean> 생성 (빈 아님, 스프링 내장 객체)
        2. OrderService에 주입
      2. ClientService 싱글톤 빈 생성
        1. OrderService 주입
    3. 컨테이너 완전히 뜸 (HeavyBean 아직 생성 안됨)
  2. getBean(ClientService.class) 호출
  3. doSomething() 호출
    1. OrderService.logic() 호출
      1. heavyBeanProvider.getObject() 호출
      2. 컨테이너에 HeavyBean 요청
      3. 프로토타입 HeavyBean 새로 생성 후 반환

# 웹 스코프

  • 웹 환경에서만 동작하는 스코프이다.
  • 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다.
    • 종료 메서드가 호출된다.
  • 다음과 같은 종류가 있다.
    1. request: HTTP 요청이 들어오고 나갈때까지 유지되는 스코프. 각 요청마다 Bean 인스턴스가 생성되고 관리됨
    2. session: HTTP 세션과 동일한 생명주기를 갖는 스코프
    3. application: 서블릿 컨텍스트와 동일한 생명주기를 갖는 스코프
    4. websocket: 웹소켓과 동일한 생명주기를 갖는 스코프
@Component
@Scope(value = "request")
public class MyLogger {
    // ..
}
  • @Scope에 value값을 지정해주면 된다.
    • 빈 소멸시점에 @PreDestroy 등의 방법을 통해 종료시 동작들을 정의할 수 있다.
  • 각 스코프의 생명주기내에서 빈을 여러번 조회해도 같은 빈이 반환된다.

스코프 프록시

@Component
@RequestScope
public class MyLogger { } // HTTP 요청 있어야 생성 가능

@Service
public class OrderService {
    private final MyLogger myLogger; // 컨테이너 시작 시점엔 MyLogger 없음 → 문제!
}
  • 위 코드에서 myLogger는 request 스코프이므로 HTTP 요청이 실제로 와야지 빈 생성이 이루어진다.
  • 반면 OrderService는 싱글톤 스코프이므로 스프링 애플리케이션 실행과 동시에 생성된다.
  • OrderService 생성 시점에 MyLogger를 주입하려 하지만 아직 HTTP 요청이 없어 MyLogger 빈이 존재하지 않으므로 오류가 발생한다. (BeanCreationException)
  • 이때 스코프 프록시를 활용하면 HTTP request와 상관없이 가짜 프록시 클레스를 다른 빈에 미리 주입할 수 있다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger { }
  • 적용대상이 클래스이면 TARGET_CLASS, 인터페이스이면 INTERFACES를 선택하면 된다.
  • CGLIB 라이브러리가 MyLogger를 상속한 가짜 프록시 객체를 생성하며, 클래스명에 $$EnhancerBySpringCGLIB$$ 가 붙는다.
  • 실제 요청이 오면 프록시 객체가 내부적으로 진짜 빈을 찾아 실제 로직을 위임한다.
  • 프록시 객체는 request scope와는 관계없이 위임로직만 가지고 싱글톤처럼 동작한다.
    • request 스코프 빈은 HTTP request 생명주기에 따라 생성되고 소멸한다.