스프링 핵심 원리 정리.zip

16 분 소요

📌 스프링이란?

스프링이란 어떤 특정한 하나의 기술이 아닌 여러 스프링 기술들의 모임이라고 볼 수 있다.

  • 위 목록 외에도 스프링 프레임워크를 확장하고 보완하는 여러 라이브러리들이 존재한다.

프레임워크란 ?

프레임워크는 소프트웨어 개발을 위한 구조와 규칙을 제공하는 도구나 환경을 말한다.
프레임워크를 이용하면 개발자가 애플리케이션을 빠르고 효율적으로 개발할 수 있도록 돕는 역할을 한다.

라이브러리란 ?

재사용 가능한 코드와 기능들의 집합으로 특정한 작업이나 기능을 수행하기 위해 개발된 프로그램의 부분.
일반적으로 라이브러리는 함수, 클래스, 모듈, 혹은 패키지 형태로 제공되며 개발자가 불러와 사용가능하다.
개발자가 동일한 작업을 반복해서 구현할 필요가 없으며 이미 구현된 기능을 불러와 사용가능하다.
ex) 데이터베이스 연결, 네트워크 통신, 보안 기능 등 다양한 작업을 수행하는 라이브러리가 존재한다.


프레임워크와 라이브러리의 예시 - 미니어처 키트

  • 미니어처를 만들기 위한 키트는 일종의 프레임워크로 볼 수 있다.
    • 이 키트에는 작은 가구, 소품, 재료 등이 포함되어 있어 제작자가 이를 사용하여 미니어처를 만들 수 있다.
  • 미니어처 키트를 만들때는 단계별 설명서나 가이드가 함께 제공된다.
    • 설명서와 가이드는 미니어처를 만들 때 필요한 지침이나 팁을 제공하는데 프레임워크에서 문서와 비슷하다.
  • 미니어처를 만들 때에는 보통 기본적인 구조와 틀이 제공된다.
    • 예를 들어, 집을 만든다면, 벽, 지붕, 창문, 등 기본적인 구조가 이미 준비되어있다.
    • 이는 프레임워크에서 뼈대 코드와 유사한 역할을한다.
  • 이러한 지침을 따르면서 제작자는 미니어처를 구성하고 세부사항을 완성시키게 된다. 마찬가지로 프레임워크를 사용하는 개발자도 프레임워크에서 제공하는 구조와 틀을 기반으로 코드를 작성하고, 해당 프레임워크의 가이드와 문서를 참고하여 애플리케이션을 개발한다.
  • 반대로 라이브러리는 아무런 가이드와 틀이 없고 제작자가 직접 원하는 부품을 만들거나 이미 만들어져 있는 부품을 가져와 조립하는 방식으로 사용할 수 있다.




📌 스프링의 핵심 개념


- 스프링 프레임워크는 자바 기반의 엔터프라이즈 애플리케이션을 개발하기 위한 도구이다.
- 스프링은 객체 지향 언어가 가진 강력한 특징을 살려내는 프레임워크이다.
- 스프링은 좋은 객체 지향 어플리케이션을 개발 할 수 있도록 도와준다.



📌 좋은 객체 지향 설계의 5가지 원칙 (SOLID)


SRP: 단일 책임 원칙 (Single responsibility principle)

  • 하나의 클래스는 하나의 책임을 가져야한다.
    • 여기서 말하는 ‘책임’은 하나의 기능 정도로 생각할 수 있다.
  • 클래스를 변경할 때 다른 부분에 영향을 줄 가능성을 줄여준다.
    • 클래스의 응집도를 높이며 결합도를 낮춘다.
  • 하나의 책임이라는것은 기준이 모호하다. 책임이 클 수 도 작을 수도 있다.
  • 가장 중요한 기준은 변경 이다. 변경이 있을때 파급효과가 작으면 SRP 원칙을 잘 따른것이다.


⭐ OCP: 개방 - 폐쇄 원칙 (Open - Close principle)

  • 소프트웨어의 요소는 확장에는 열려있으나 변경에는 닫혀있어야한다.
  • 새로운 기능이나 변경 사항을 추가할때 기존 코드를 수정하지 않고 확장할 수 있어야한다.
  • OCP는 추상화와 상속등으로 구현할 수 있다.
    • 자주 변화하는 부분을 추상화함으로써 기존 코드를 수정하지 않고 기능을 확장할 수 있도록 한다.


LSP - 리스코프 치환 원칙 (Liskov substitution principle)

  • 상위 타입의 객체를 하위 타입의 객체로 치환해도 프로그램이 정상적으로 작동해야한다.
    • 쉽게 이야기해서 부모 객체 대신 자식 객체를 사용해도 프로그램 동작에 문제가 없어야한다는 의미이다.
    • 부모 클래스가 할 수 있는 것을 자식 클래스도 할 수 있어야 한다는 뜻이다.
  • 사각형 클래스를 상속받은 정사각형 클래스가 있다고 가정한다.
    • 사각형 클래스를 사용하는 모든 코드를 정사각형 클래스에서 문제 없이 사용해야한다.
    • 만약 사각형 클래스와 다른 동작을한다면 리스코프 치환 원칙에 위배하게 된다.


ISP - 인터페이스 분리 원칙 (Interface segregation principle)

  • 인터페이스는 클라이언트가 필요로 하는 작은 단위로 분리되어야한다.
  • 클라이언트가 필요하지 않는 기능까지 포함하는 거대 인터페이스 대신 작고 구체적인 인터페이스에 의존하게한다.
  • 거대 인터페이스를 상속받으면 자신이 사용하지 않는 인터페이스마저 구현해야한다.
  • 사용하지도 않는 인터페이스의 추상 메서드가 변경된다면 클래스에서도 수정이 필요하게된다.
  • 인터페이스를 분리하면 인터페이스의 명확도가 높아지고 대체 가능성이 높아진다.


⭐DIP : 의존관계 역전 원칙 (Dependency inversion principle)

  • 개발자는 추상화에 의존해야지 구체화에 의존하면 안된다.
  • 구체적인 구현에 의존하지 않고 추상화에 의존하도록 설계해야 된다는 의미이다.



📌 제어의 역전 IoC (Inversion of Control)

- 일반적으로 프로그램의 제어권은 개발자가 가지고 있다.
- 프로그램의 제어흐름을 개발자가 제어하는것이 아니라 외부에서 제어하는것을 제어의 역전이라고 부른다.
- 객체의 생명주기나 의존성 관리는 외부에서 담당하게된다.


기존 OrderServiceImpl.java

public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository = new MemoryMemberRepository();
    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
}
  • 객체의 생성과 의존성 주입을 개발자가 직접 수행하였다.
  • 위 코드는 MemberRepository DiscountPolicy 인터페이스의 구현체를 직접 생성하고 의존성을 주입하고 있다.


하지만 정책 변경으로 구현 객체를 변경해야 된다면?
OrderServiceImpl 클래스 내부의 소스코드를 직접 수정해야된다. -> OCP, DIP 원칙에 위배 된다.


DI 컨테이너 Appconfig.java

public class AppConfig {

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

    public DiscountPolicy discountPolicy() {
        return new RateDiscountPolicy();
    }
}
  • AppConfig 클래스는 객체의 생성과 관리를 담당하는 클래스이다.
  • MemberRepository를 구현한 MemoryMemberRepository 객체를 생성하여 반환한다.
  • DiscountPolicy 를 구현한 RateDiscountPolicy 객체를 생성하여 반환한다.


새로운 OrderServiceImpl.java

public class OrderServiceImpl implements OrderService {
    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;
    
    public OrderServiceImpl(MemberRepository memberRepository, DiscountPolicy discountPolicy) {
      this.memberRepository = memberRepository;
      this.discountPolicy = discountPolicy;
    }
}
  • OrderServiceImpl 클래스는 더 이상 객체의 생성과 설정에 대한 책임을 가지지 않게 되었다.
  • 필요한 인터페이스를 호출하지만 어떤 구현 객체들이 주입될지 모르고 묵묵히 자신의 로직에 따라 실행하게 된다.


IoC 적용

public class Main {
    public static void main(String[] args) {
        AppConfig appConfig = new AppConfig();
        MemberService memberService = appConfig.memberService();
        OrderService orderService = appConfig.orderService();

        Member member = new Member(1L, "john@example.com");
        memberService.join(member);

        Order order = orderService.createOrder(1L, "itemA", 10000);
        System.out.println("주문 정보: " + order);
    }
}

  • AppConfig 클래스의 등장으로 책임이 분리되었고 제어권이 외부로 넘어갔다.
  • 이렇게 프로그램의 제어 흐름을 직접 제어하는것이 아니라 외부에서 관리하는 것을 제어의 역전이라고 부른다.
  • AppConfig 클래스를 Ioc 컨테이너, DI 컨테이너라고 부른다.




📌 DI (Dependency Injection)

- 의존성 주입은 객체가 필요로 하는 의존성을 직접 생성하지 않고 외부에서 주입받는것을 의미한다.
- DI를 사용하면 클라이언트 코드를 변경하지 않고 클라이언트가 호출하는 대상의 타입 인스턴스를 변경할 수 있다.
- DI 사용하면 정적인 클래스 의존관계를 변경하지 않고, 동적인 객체 인스턴스 의존관계를 쉽게 변경할 수 있다.
  • OrderServiceImpl 클래스 내 객체 memberRepositorydiscountPolicy 는 인터페이스에게만 의존한다.
  • 실제 어떤 구현객체가 사용될지는 OrderServiceImpl 클래스 입장에서는 알 수 없다
  • 애플리케이션이 실행 시점에 AppConfig 에서 실제 구현 객체를 생성하고 생성자를 통해 구현객체가 주입된다.




📌 스프링 컨테이너

- 스프링 프레임워크에서 DI와 IoC를 담당하는 `ApplicationContext` 를 스프링 컨테이너 라고 부른다.
- 스프링 컨테이너는 객체의 생명주기를 담당하고, 빈 객체의 생성 및 의존관계를 관리한다.
- 스프링 컨테이너는 개발자가 작성한 설정 정보를 기반으로 빈 객체를 생성하고 DI를 통해 의존관계를 주입한다.
@Configuration
public class AppConfig {

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

    @Bean
    public OrderService orderService() {
        return new OrderServiceImpl(memberService()); // memberService() 메서드 호출로 의존성 주입
    }
}
  • @Configuration 어노테이션이 붙은 클래스를 설정 정보 클래스로 사용한다.
  • 설정 정보 클래스에서 @Bean 어노테이션이 붙은 메서드가 호출이 되고 반환된 객체를 스프링 빈으로 등록한다.
    • @Bean 어노테이션이 붙은 각 메서드의 이름은 해당 스프링 빈의 이름이 된다.
    • @Bean 어노테이션이 붙은 각 메서드의 반환된 객체는 스프링 빈의 객체가 된다.




📌 스프링 컨테이너 생성과정


1. 스프링 컨테이너 생성

스프링 컨테이너를 인스턴스화 해야한다.

  • 스프링 컨테이너는 ApplicationContext 인터페이스를 구현한 구현체 중 하나를 사용하여 생성한다.
  • 대표적으로 AnnotationConfigApplicationContext, ClassPathXmlApplicationContext 등이 있다.
  • 어노테이션 기반, XML 기반등을 선택하는것이다.

ApplicationContext ac = new AnnotationConfigApplicationContext(AppConfig.class);

  • AppConfig 클래스를 설정 정보로 사용하는 어노테이션 기반 스프링 컨테이너를 생성한다.

2. 스프링 빈 등록

  • 스프링 컨테이너는 파라미터로 들어온 설정 클래스 정보를 활용하여 스프링 빈을 등록한다.
  • @Bean 어노테이션이 붙은 메서드를 호출하여 반환된 객체를 스프링 빈으로 등록한다.

3. 스프링 빈 의존관계 설정 - 준비

  • 준비 단계에서는 의존성 주입이 필요한 필드를 모두 찾아낸다.

4. 스프링 빈 의존관계 설정 - 완료

  • 완료 단계에서는 실제 의존성 주입이 이루어진다.




📌 스프링 컨테이너에 등록된 빈 조회 방법


  • 부모 타입으로 조회할 경우 자식 타입의 스프링 빈도 모두 조회가 된다.
    • Object 타입으로 조회시 모든 스프링 빈이 조회된다.


  • 빈 이름으로 조회 ac.getBean("memberService", MemberService.class);
    • 빈의 이름이 컨테이너에 등록되지 않은 경우 NoSuchBeanDefinitionException 발생한다.


  • 타입으로 조회 ac.getBean(MemberService.class);
    • 타입으로 조회시 같은 타입이 둘 이상 있을 경우 NoUniqueBeanDefinitionException 발생한다.
    • 동일한 타입이 둘 이상일 경우 빈 이름을 지정해서 조회해야한다.
    • getBeansOfType() 사용하면 해당 타입의 모든 빈 조회가능.


  • 부모 타입으로 조회 ac.getBean(DiscountPolicy.class);
    • 부모 타입으로 조회시, 자식 타입이 둘 이상 있을 경우 NoUniqueBeanDefinitionException 발생한다.
    • 자식 타입이 둘 이상일 경우 빈 이름을 지정해서 조회해야한다.
    • 또는 특정 구체타입을 지정하여 조회한다. - 지향하기




📌 BeanFactory, ApplicationContext, BeanDefinition

BeanFacotory

  • 스프링의 가장 기본적인 IoC 컨테이너
  • 빈의 생성, 조회, DI등의 기본적인 기능을 제공한다.


ApplicationContext

  • BeanFactory의 확장된 기능을 제공하는 인터페이스.
  • BeanFactory가 제공하는 기능 외에도 이벤트 처리, 국제화 지원, 메시지 리소스 핸들링 등 부가 기능 지원


BeanDefinition

  • 스프링 컨테이너에 등록될 빈의 메타정보를 담고 있는 인터페이스이다.
  • 빈의 클래스 이름, 스코프, 생성 방법, 의존성 등의 정보를 담고있다.
  • XML, Annotation, JavaConfig 등 다양한 설정 형식에서 빈의 메타정보를 표현할 수 있도록 설계되어 있다.
  • 각 설정 형식마다 BeanDefinition을 생성하여 컨테이너에 등록한다.
  • 이렇게 등록된 BeanDefinition을 통해 컨테이너는 빈을 생성하고 관리하며 의존성 주입을 할 수 있다.
    • 다양한 설정 형식을 지원할 수 있게 한다.




📌 웹 어플리케이션과 싱글톤

웹 어플리케이션은 여러 고객들의 동시에 요청하는 경우가 있다.

이전에 만들었던 순수한 DI 컨테이너인 AppConfig를 사용하면 발생되는 문제점을 확인해보자.


PureContainerTest.java

  @Test
    @DisplayName("스프링 없는 순수한 DI 컨테이너")
    void pureContainer() {
        AppConfig appConfig = new AppConfig();

        //조회1 : 호출할 때마다 객체 생성
        MemberService memberService1 = appConfig.memberService();

        //조회2 : 호출할 때마다 객체 생성
        MemberService memberService2 = appConfig.memberService();

        //참조값이 다른것을 확인
        System.out.println("memberService1 = " + memberService1);
        System.out.println("memberService2 = " + memberService2);
    }
  • 순수한 DI 컨테이너인 AppConfig를 사용하게 된다면 고객이 요청할 때 마다 새로운 객체가 생성되고 소멸된다.

  • 고객이 요청할 때마다 JVM 메모리에 객체가 계속 생성되고 소멸 될것이다. (메모리 낭비)

  • 고객이 요청시 할 때 마다 객체가 매번 생성되는것이 아닌 하나의 객체를 생성하여 공유하도록 설계하면 될것이다.


싱글톤 패턴

 - 싱글톤 패턴은 쉽게말하여 객체 인스턴스가 하나만 생성되는것을 보장하는 패턴이다.
 - 싱글톤 패터을 적용하면 객체를 여러 번 생성하지 않고 하나의 인스턴스를 공유할 수 있다.


싱글톤 패턴을 구현하는 방법으로는 여러가지 방법이 존재한다.

이른 초기화 방법 : 클래스 로딩 시점에 인스턴스를 미리 생성하는 방식

public class SingletonService {
    private static final SingletonService instance = new SingletonService();

    private SingletonService() {
    }

    public static SingletonService getInstance() {
        return instance;
    }

    public void logic() {
        System.out.println("singleton instance logic call");
    }

}


싱글톤 패턴의 문제점

  • 싱글톤 구현 코드 -> 매번 작성하기 번거로움
  • DIP 위반
  • OCP 위반
  • 테스트 하기 어렵다
  • 내부 속성을 변경하거나 초기화 하기 어렵다.
  • private로 자식 클래스를 만들기 어렵다.
  • 유연성이 떨어짐


스프링 컨테이너는 위에 말했던 문제점을 모두 해결하고 등록된 빈들을 싱글톤으로 관리되는것을 보장해준다.


스프링 컨테이너가 싱글톤으로 관리할 수 있는 이유는 @Configuration 어노테이션에 있다.

  • @Configuration 은 CGLIB 이라는 바이트 코드 조작 라이브러리를 사용한다.
  • CGLIB 라이브러리를 사용하여 설정 정보 클래스를 상속받은 임의의 다른 클래스를 생성한다.
  • CGLIB을 통해 만들어진 AppConfig@CGLIB 이라는 클래스를 스프링 빈으로 등록하게된다.
  • AppConfig@CGLIB 클래스가 싱글톤이 보장되도록 한다.


AppConfig@CGLIB 예상 코드

@Bean
public MemberRepository memberRepository() {
	if (memberRepositry가 이미 스프링 컨테이너에 등록되 있으면?) {
		return 스프링 컨테이너에서 찾아서 반환;
	} else {
		기존 로직을 호출하여 반환 객체를 스프링 컨테이너에 등록
		reteurn 반환
	}
}
  • @Bean 어노테이션이 붙은 메서드를 호출하며 이미 스프링 컨테이너 등록된 빈이 있으면 등록된 빈을 반환.
  • 등록된 빈이 없다면 스프링 빈으로 등록하고 반환할 것이다.




📌 컴포넌트 스캔

스프링이 자동으로 지정된 패키지 이하에서 애노테이션을 스캔하여 스프링 빈으로 등록하는 기능
  • 이전에는 AppConfig 클래스에 직접 @bean 으로 클래스를 등록하고 의존관계를 명시적으로 등록하였다.
  • 등록해야할 클래스가 많아진다면 매우 번거로운 작업이 될것이다.
  • 스프링에서는 설정 정보가 없어도 자동으로 스프링 빈을 등록해주는 컴포넌트 스캔 이라는 기능을 지원한다.
  • 컴포넌트 스캔은 @Component 어노테이션이 붙은 모든 클래스를 스프링 빈으로 등록한다.
  • 스프링 빈의 기본 이름은 클래스명을 사용하되 맨 앞글자는 소문자를 사용한다.
    • 스프링 빈의 이름은 개발자가 직접 지정도 가능하다. @Component("hello")


컴포넌트 스캔은 패키지를 검색하며 @Component 어노테이션이 붙은 클래스를 스프링 컨테이너에 등록하는 기능이다.


AutoAppConfig.java

@Configuration
@ComponentScan
public class AutoAppConfig {}
  • 설정 클래스로 사용할 클래스에 @ComponentScan 어노테이션을 사용하면 컴포넌트 스캔 기능을 사용할 수 있다.
  • 기존 AppConfig 클래스와 달리 @Bean 으로 등록한 클래스가 하나도 없는것을 확인할 수 있다.
  • @ComponentScan@Component 어노테이션이 붙은 클래스를 스캔해서 스프링 빈으로 등록한다.
  • @Component, @Controller, @Service, @Repository 에도 내부에 @Component가 달려있다.


@Component 어노테이션

@Component
public class MemberServiceImpl implements MemberService

@Component
public class MemoryMemberRepository implements MemberRepository

@Component
public class OrderServiceImpl implements OrderService
...
  • @ComponentScan의 대상이 되기 위해서는 등록하려는 클래스에 @Component 어노테이션을 붙여준다.


그렇다면 컴포넌스 스캔은 모든 패키지를 검색하며 @Component 어노테이션이 붙은 클래스를 검색하는 것일까?

  • 개발자가 따로 지정하지 않은 경우 @ComponentScan 이 붙은 설정 정보 클래스부터 하위로 검색한다.
    • ex) AutoAppConfig 클래스의 위치가 package com.hello.core 이므로 core 부터 하위로 검색하게 된다.
  • 컴포넌트 스캔을 임의로 필요한 위치부터 검색할 수 있도록 변경할 수 있다.
    • basePackages : 탐색할 패키지의 시작 위치를 지정한다. 패키지를 포함하여 하위로 검색한다.
    • basePackageClasses : 지정한 클래스의 패키지를 탐색 시작 위치로 지정한다


필터 기능을 사용하면 컴포넌트 스캔 대상을 추가하거나 제외할 수 도 있다.

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


AutoAppConfig.java

@Configuration
    @ComponentScan(
            includeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyIncludeComponent.class),
            excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = MyExcludeComponent.class)
    )
    static class ComponentFilterAppConfig{
}


컴포넌트 스캔에서 같은 빈 이름을 등록하면?

  • 컴포넌트 스캔에 의해 자동으로 스프링 빈이 등록되는데 이름이 같을 경우 스프링은 오류 발생
    • ConflictingBeanDefinitionException 발생
  • 기본적으로 수동빈이 우선권을 갖지만 최근에는 스프링부트가 오류가 발생하도록 변경하였다.




📌 의존관계 자동 주입


의존관계 자동주입

  • 기존에는 설정 클래스에 @Bean 으로 클래스를 등록하면서 의존관계도 명시해주었다.

  • 하지만 컴포넌트 스캔을 사용하면서 명시적으로 의존관계를 주입할 방법이 없다.

  • @AutoWired 어노테이션을 사용하여 의존관계 자동 주입을 사용하자.


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

  • 생성자 주입
  • 수정자 주입
  • 메서드 주입
  • 필드 주입


생성자 주입

  • 객체가 생성될 때 해당 클래스의 생성자를 호출하여 의존 관계를 주입하는 방식이다.
  • 생성자를 통해 의존관계가 주입되므로 딱 1번만 호출되는것을 보장한다.
  • 불변, 필수 의존관계에 사용된다.


수정자 주입

  • 객체를 먼저 생성후 스프링 컨테이너가 해당 객체의 Setter 메서드를 호출하여 의존관계를 주입하는 방식이다.
  • 선택, 변경 가능성이 있는 의존관계에서 사용된다.
    • Setter 메서드를 통해 의존관계를 변경 할 수 있기 때문.


메서드 주입

  • 객체 생성후 스프링 컨테이너가 @Autowired 붙은 메서드를 실행하여 의존관계를 주입하는 방식이다.
  • 한번에 여러 필드를 주입받을 수 있다는 장점이 있다.


필드 주입

  • 객체 생성후 해당 객체의 필드에 직접 주입하는 방식이다.
  • 코드가 간결하게보일 수 있지만 외부에서 변경할 방법이 없어 테스트가 힘들다는 단점이있다.
  • DI 프레임 워크가 없으면 아무것도 할 수 없다.


옵션처리

@Autowired 만 사용하면 required 의 값이 ture 이기 때문에 자동 주입 대상이 없으면 오류가 발생


자동 주입할 대상을 옵션으로 처리하는 방법

  • @Autowired(required=false) : 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출되지 않는다.
  • @Nullable : 자동 주입할 대상이 없으면 null 값
  • Optional<> : 자동 주입할 대상이 없으면 Optional.empty


TeseBean.java

     @Autowired(required = false) // 호출 X
        public void setNoBean1(Member noBean1) {
            System.out.println("noBean1 = " + noBean1);
        }

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

        @Autowired // noBean3 = Optional.empty
        public void setNoBean3(Optional<Member> noBean3) {
            System.out.println("noBean3 = " + noBean3);
        }


생성자 주입 권장

  • 대부분 의존관계는 한 번 주입하면 애플리케이션 종료 시점까지 의존관계를 변경할 일이 없다.
  • 대부분 의존관계는 애플리케이션 종료 시점까지 변하면 안된다 → 불변
  • 수정자 주입을 사용하면 Setter 메서드를 public으로 열어두어야한다 → 실수 할 수 있다.
  • 생성자 주입은 객체를 생성할 때 딱 1번만 실행되므로 이후 호출 X → 불변하다.
  • 프레임워크에 의존하지 않고 순수한 자바 언어 특징을 잘살리는 방법이다.




📌 의존관계 주입 - 조회 빈이 2개 이상 일때

  • 의존관계 주입시 타입으로 빈을 조회하기 때문에 하위 타입이 2개 이상 조회되면 예외가 발생한다.
  • 의존관계 주입시 타입을 하위타입으로 지정하여 해결할 수 있지만 DIP 위배하고 유연성이 떨어진다.


해결방법

  1. @Autowired 필드 명 매칭
  2. @Qualifier 끼리 매칭
  3. @Primary 사용


@Autowird

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


필드 명을 빈 이름으로 변경

  private final DiscountPolicy rateDiscountPolicy;


파라미터 이름을 빈이름으로 변경

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


@Qualifier

  • 추가 구분자를 붙여주는 방법이다.
  • @Qualifier 의 설정된 이름을 못찾을 경우 Qualifier 의 해당하는 이름의 스프링 빈을 추가로 찾는다.
    • @Qualifier(“testRepository”) -> testRepository 스프링 빈 검색
  • 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;
}


@Primary

  • 우선 순위를 정하는 방법이다.
  • @Autuwired 시 여러 빈이 매칭되면 @Primary 어노테이션이 붙은 빈이 우선권을 가진다.


rateDiscountPolicy 가 우선권을 가지는 소스코드

@Component
@Primary
public class RateDiscountPolicy implements DiscountPolicy {}
@Component
public class FixDiscountPolicy implements DiscountPolicy {}


우선 순위

스프링은 자동보다는 수동이, 넓은 범위의 선택권보다는 좁은 범위의 선택권이 우선순위가 높다.

@Primary < @Qualifier




📌 빈 생명주기 콜백

의존관계 주입이 완료되면 스프링 빈에게 콜백 메서드를 호출하여 초기화나 종료와 같은 작업을 수행할 수 있다.
  • 초기화 콜백 : 빈이 생성되고 빈의 의존관계의 주입이 완료되면 호출한다.
  • 소멸전 콜백 : 빈이 소멸되기 전에 호출한다.


스프링 빈의 이벤트 사이클

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


스프링 빈은 3가지 방법으로 빈 생명주기 콜백을 제공한다

  1. 인터페이스 제공
  2. 설정 정보에 초기화 메서드, 종료 메서드 지정
  3. 어노테이션 제공


인터페이스 - InitializingBean, DisposableBean

@Override
  public void afterPropertiesSet() throws Exception {}
@Override
  public void destroy() throws Exception {}
  • 초기화 메서드는 InitializingBean 인터페이스를 상속받아 메서드를 오버라이딩 한다.
  • 종료 메서드는 DisposableBean 인터페이스를 상속받아 메서드를 오버라이딩 한다.
  • 초기화 종료 메서드의 이름을 변경할 수 없다는 단점이 존재한다.
  • 위 인터페이스는 스프링 전용 인터페이스이다. 스프링에 의존하게된다.


설정 정보에 초기화 메서드, 종료 메서드 지정

설정 정보에 명시적으로 초기화 메서드, 종료 메서드를 지정함으로 콜백 메서드를 사용할 수 있다.

@Configuration
static class LifeCycleConfig {
    @Bean(initMethod = "init", destroyMethod = "close")
    public NetworkClient networkClient() {
      NetworkClient networkClient = new NetworkClient();
      networkClient.setUrl("http://hello-spring.dev");
      return networkClient;
  }
}
  • 인터페이스 상속과 달리 메서드의 이름을 변경할 수 있다.
  • 스프링 빈이 스프링 코드에 의존하지 않는다.
  • 외부 라이브러리에도 적용이 가능하다.


어노테이션 활용 - @PostConstruct, @PreDestroy

@PostConstruct
  public void init() {}

@PreDestroy
  public void close() {}
  • 스프링에서 권장하는 방법
  • 어노테이션 하나만 붙이면 되므로 매우 편리하다.
  • 자바 표준기술로 스프링이 아닌 다른 컨테이너에서도 사용이 가능하다.
  • 외부라이브러리에 적용하지 못한다.




📌 빈 스코프

빈 스코프란 빈이 존재할 수 있는 범위를 뜻한다.


스프링은 다양한 스코프 지원

  • singleton : 기본 스코프, 스프링 컨테이터 시작과 종료까지 유지되는 스코프
  • prototype : 스프링 컨테이너는 빈 생성 의존관계 주입까지만 관여하고 더 이상 관여하지 않는 스코프
  • request : 웹 요청이 들어오고 나갈때까지 유지되는 스코프
  • session : 웹 세션이 생성되고 종료될때까지 유지되는 스코프
  • application : 웹 서블릿 컨텍스트와 같은 범위로 유지되는 스코프


@Scope("singleton")
@Component

@Scope("prototype")
@Component

@Scope("request")
@Component

...
  • 위와 같은 방법으로 빈의 스코프를 지정할 수 있다.


프로토타입 스코프

  • 프로토타입 스코프는 스프링 컨테이너에 조회하면 매번 새로운 인스턴스를 생성해서 반환한다.
    1. 사용자가 프로토타입 스코프의 빈을 스프링 컨테이너에 요청한다.
    2. 스프링 컨테이너는 이 시점에 스프링 빈을 생성하고 의존관계를 주입한다.
    3. 스프링 컨테이너는 생성한 프로토타입 빈을 사용자에게 반환한다.
    4. 이후에도 위같은 과정을 반복하여 매번 새로운 빈을 생성해서 반환한다.
  • 초기화 콜백은 호출되지만 종료 콜백은 호출되지 않는다. -> 스프링 컨테이너에서 관리하지 않기 때문이다.


Dependency Lookup

의존관계를 주입받는 것이아니라 직접 필요한 의존관계를 찾는것을 말한다.


ObjectProvider

@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
 PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
 prototypeBean.addCount();
 int count = prototypeBean.getCount();
 return count;
}
  • prototypeBeanProvider.getObject(); 메서드를 호출하면 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.


JSR-330 Provider

  • JSR 이라는 자바 표준을 사용하는방법이다
  • 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() 을 통해 항상 새로운 타입의 프로토 타입 빈이 생성되는것을 확인할 수 있다.
  • provider의 get() 을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)
  • 자바 표준이고 기능이 단순하여 단위테스트를 만들거나 mock 코드를 만들기도 쉬워진다.
  • 자바 표준이므로 스프링 컨테이너가 아닌 다른 컨테이너에서도 사용 가능하다.



📌 웹 스코프


웹 스코프의 특징

  • 웹 스코프는 웹 환경에서만 동작한다.
  • 프로토타입과 다르게 스코프의 종료 시점까지 관리한다.
    • 종료메서드 호출된다.


웹 스코프 종류

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


MyLogger.java

package com.hello.core.common;

import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import java.util.UUID;
import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {

    private String uuid;
    private String requestURL;

    public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
    }

    public void log(String message) {
        System.out.println("[" + uuid + "]" + "[" + requestURL + "]" + message);
    }

    @PostConstruct
    public void init() {
        uuid = UUID.randomUUID().toString();
        System.out.println("[" + uuid + "] request scope bean create : " + this);
    }

    @PreDestroy
    public void close() {
        System.out.println("[" + uuid + "] request scope bean close : " + this);
    }
}
  • @scope("request") 를 사용하여 reqeust 스코프로 지정 할 수 있다.
    • HTTP 요청 당 하나씩 생성되고 HTTP 요청이 끝나는 시점에 종료된다.
  • request 스코프는 실제 HTTP 요청이 와야 스프링 빈이 생성된다.
    • Provider를 사용하여 request scope의 빈의 생성을 지연 시킬 수 있다.


스코프와 프록시

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {}
  • proxyMode = ScopedProxyMode.TARGET_CLASS
    • 클래스면 TARGET_CLASS 선택
    • 인터페이스면 INTERFACE 선택
  • MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 가짜 프록시 클래스를 스프링 빈에 등록한다.


동작정리

  • proxyMode = ScopedProxyMode.TARGET_CLASS 를 설정하면 스프링은 CGLIB 라이브러리를 사용한다.
    • 바이트코드 조작 라이브러리를 사용하여 MyLogger 를 상속받은 가짜 프록시 객체를 생성.
  • 스프링 컨테이너에 가짜 프록시 객체를 등록하고 실제 요청이 들어오면 실제 빈을 호출한다.
  • Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다.
  • 가짜로 때우다가 실제 실행시점에 진짜 실제 빈을 호출하는것.

태그:

카테고리:

업데이트:

댓글남기기