Spring Framework - DI

jungseo·2023년 6월 1일
0

Spring

목록 보기
2/23
post-thumbnail

DI

1) 스프링 전환


(1) 스프링 컨테이너 구성 설정

  • 순수 자바 코드
public class AppConfigurer {

// 싱글톤
    private Cart cart = new Cart(productRepository(), menu());

    public Menu menu() {
        return new Menu(productRepository());
    }

    public ProductRepository productRepository() {
        return new ProductRepository();
    }

    public Cart cart() {
        return cart;
    }

    public Order order() {
        return new Order(cart(), discount());
    }
}

AppConfigurer 클래스는 각 역할과 책임에 따라 필요한 객체를 생성한 후에 생성한 객체의 참조값을 생성자를 통해 주입

  • 스프링 코드(@Configuration과 @Bean을 사용한 수동 주입 방식)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class AppConfigurer {

//    private Cart cart = new Cart(productRepository(), menu());

    @Bean
    public Menu menu() {
        return new Menu(productRepository());
    }

    @Bean
    public ProductRepository productRepository() {
        return new ProductRepository();
    }

    @Bean
    public Cart cart() {
        return new Cart(productRepository(), menu());
    }

    @Bean
    public Order order() {
        return new Order(cart(), discount());
    }
}

@Configuration, @Bean 애너테이션 사용

  • @Configuration : 해당 클래스를 스프링 컨테이너의 구성 정보로 사용
  • @Bean : 스프링이 실행될때 @Bean으로 등록된 메서드들을 모두 호출하여 반환된 객체를 스프링 컨테이너에 등록하고 관리
  • 스프링 컨테이너는 기본적으로 싱글톤으로 빈 객체들을 관리

(2) 스프링 컨테이너 생성과 사용

    public static void main(String[] args) {

//        AppConfigurer appConfigurer = new AppConfigurer();
//
//        OrderApp orderApp = new OrderApp(
//                appConfigurer.productRepository(),
//                appConfigurer.menu(),
//                appConfigurer.cart(),
//                appConfigurer.order()
//        );

//        스프링 컨테이너 생성
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfigurer.class);

//        스프링 빈 조회
        ProductRepository productRepository = applicationContext.getBean("productRepository", ProductRepository.class);
        Menu menu = applicationContext.getBean("menu", Menu.class);
        Cart cart = applicationContext.getBean("cart", Cart.class);
        Order order = applicationContext.getBean("order", Order.class);

//        불러온 빈 사용
        OrderApp orderApp = new OrderApp(
                productRepository,
                menu,
                cart,
                order
        );

AppConfigurer 클래스를 사용하여 직접 객체를 생성하고 DI를 통해 객체를 연결해 주었던 역할을 이제 스프링 컨테이너가 대신함


2) DI

  • setter 주입
  • 필드 주입
  • 생성자 주입(권장)
    • 외부로부터 특정 객체를 주입받아 this 키워드로 내부 필드에 할당
    • 생성자를 통해 객체를 주입
    • @Configuration 애너테이션이 붙은 클래스에서 객체를 생성하고 의존 관계를 연결
    • 원문 설명
// 생성자 주입 예시
public class Menu {
    private Product[] products;

    public Menu(ProductRepository products) {
        this.products = products.getAllProducts();
    }
}

3) 스프링 컨테이너 / 빈

(1) 스프링 컨테이너

: 애플리케이션의 구성 요소를 관리하고 제어

  • 빈 생성 / 관리(의존성 주입, 생명주기 관리)
  • 애플리케이션 설정 관리
  • AOP
    public static void main(String[] args) {

//        스프링 컨테이너 생성
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfigurer.class);

//        스프링 빈 조회
        ProductRepository productRepository = applicationContext.getBean("productRepository", ProductRepository.class);
        Menu menu = applicationContext.getBean("menu", Menu.class);
        Cart cart = applicationContext.getBean("cart", Cart.class);
        Order order = applicationContext.getBean("order", Order.class);

//        의존성 주입
        OrderApp orderApp = new OrderApp(
                productRepository,
                menu,
                cart,
                order
        );
    }
  • ApplicationContext 인터페이스를 일반적으로 스프링 컨테이너라 부름

    • 빈을 생성하고 관리하는 BeanFactory 인터페이스를 상속받은 ApplicationContext 인터페이스를 구현한 AnnotationConfigApplicationContext
      • 실질적으로 스프링 컨테이너의 역할은 BeanFactory가 함
    • ApplicationContext가 상속받는 인터페이스들
      - 빈을 관리하고 조회하는 BeanFactory외에 환경 변수 설정, 메세지 국제화, 이벤트 발생, 리소스 조회 등 다양한 기능 지원
  • AnnotationConfigApplicationContext는 ApplicationContext의 구현 객체이며 매개변수로 구성 정보를 넘겨줌

  • 스프링 컨테이너는 넘겨받은 구성 정보를 가지고 메서드들을 호출해 빈을 생성하고, 빈들 간 의존관계를 설정

  • 호출하는 메서드의 이름을 기준으로 빈의 이름을 등록

    @Bean(name="menu2") // 빈의 이름을 다르게 설정 가능
    public Menu menu() {
        return new Menu(productRepository());
    }

(2) 빈(Bean)

: 스프링 컨테이너가 관리하는 자바 객체

  • 클래스의 등록 정보, getter/setter 메서드를 포함하며, 구성 정보(설정 메타 정보)를 통해 생성

  • 스프링 컨테이너가 빈을 생성하고 의존 관계를 연결해 주면 이제 스프링 컨테이너의 관리 하에 있는 객체 빈들을 getBean() 메서드로 조회 가능

  • 공식문서

    • getBean(빈 이름, 타입)
      -> applicationContext.getBean("menu", Menu.class);
    • getBean(타입)
      -> applicationContext.getBean(Menu.class);
  • 빈 조회 단위 테스트 예시

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class MainTest {
//    스프링 컨테이너 생성
    AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfigurer.class);

//    빈 조회 테스트 케이스
    @Test
//    @DisplayName("")
    void findBean() {
//        given
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfigurer.class);

//        when
        Menu menu = applicationContext.getBean("menu", Menu.class);

//        then
        Assertions.assertThat(menu).isInstanceOf(Menu.class);
    }
}

(3) 싱글톤 레지스트리

싱글톤으로 객체를 생성하고 관리하는 기능

  • 해당 객체가 이미 스프링 컨테이너에 있는 경우 이미 있는 객체를 반환
  • 새로운 객체를 생성하고 스프링 컨테이너에 등록 후 해당 객체를 반환

4) 빈 생명주기

  1. 빈 객체 생성
    • 기본 생성자를 통해 생성
  2. 빈 객체 간 의존 관계 설정
  3. 내부적으로 지정한 메서드를 호출하여 빈 객체 초기화
  4. 스프링 컨테이너가 종료되면 지정한 메서드를 호출해 빈 객체들이 소멸

인터페이스를 구현하여 메서드 사용

// 초기화 단계에서 실행되는 메서드
public interface InitializingBean {
	void afterPropertiesSet() throws Exception;
}

// 소멸 단계에서 실행되는 메서드 
public interface DisposableBean {
	void destroy() throws Exception;
}
  • 위 인터페이스들을 구현해 메서드를 오버라이딩 해서 특정 기능 구현 가능

@PostConstruct / @PreDestroy 애너테이션 사용

  • 메서드 구현 후 해당 애너테이션 사용
    @PostConstruct
    public void init() {
        System.out.println("초기화 메서드 실행");
    }
    @PreDestroy
    public void close() {
        System.out.println("종료 메서드 실행");
    }

  • 사용 예시
// ClientCongif.java
import org.springframework.context.annotation.Bean;

public class ClientConfig {

    @Bean
    public TestClient testClient() {
        TestClient testClient = new TestClient();
        return testClient;
    }
}

// ClientMain.java
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

public class ClientMain {
    public static void main(String[] args) {

//        컨테이너 생성
        AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(ClientConfig.class);

//        컨테이너 사용(빈 초기화)
        TestClient testClient = applicationContext.getBean("testClient", TestClient.class);
        testClient.use();

//        컨테이너 종료
        applicationContext.close();
    }
}

// TestClient.java
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

public class TestClient {

    public TestClient() {
        System.out.println("생성자 호출.");
    }

    @PostConstruct
    public void init() {
        System.out.println("초기화 메서드 실행");
    }

    public void use() {
        System.out.println("빈 사용");
    }

    @PreDestroy
    public void close() {
        System.out.println("종료 메서드 실행");
    }
}
  • main 메서드 실행시

@Configuration 클래스의 @Bean 태그에서 메서드 지정

@Bean(initMethod = "init", destroyMethod = "close")

5) 빈 객체의 관리 범위

싱글톤 이외에 프로토타입(prototype), 세션(session), 리퀘스트(request) 등의 범위가 존재

  • prototype 예시
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

public class ScopeTest {

    @Test
    public void scopeTest() {
        AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(TestBean.class);

        TestBean bean1 = annotationConfigApplicationContext.getBean(TestBean.class);
        TestBean bean2 = annotationConfigApplicationContext.getBean(TestBean.class);

        Assertions.assertThat(bean1).isNotEqualTo(bean2); // 테스트 통과

        annotationConfigApplicationContext.close();
    }

    @Scope("prototype")
    static class TestBean {

        @PostConstruct
        public void init() {
            System.out.println("init() 초기화 메서드 실행");
        }

        @PreDestroy
        public void close() {
            System.out.println("close() 종료 메서드 실행");
        }
    }
}
// 출력
// init() 초기화 메서드 실행
// init() 초기화 메서드 실행
  • getBean()이 호출 될 때마다 새로운 빈 객체를 생성
  • 빈의 범위가 프로토타입이기 때문에 빈을 조회했을 때 서로 다른 객체가 반환
  • 프로토타입 범위는 싱글톤 범위와 다르게 빈의 생성, 의존성 주입, 초기화까지만 스프링 컨테이너가 관여하여 초기화 메서드만 두 번 호출

6) 의존성 자동 주입

@ComponentScan과 @Autowired

  • 빈을 자동으로 등록하고 동시에 의존관계 설정 가능
@Configuration
@ComponentScan
public class AppConfigurer {

//    private Cart cart = new Cart(productRepository(), menu());
//
//    @Bean
//    public Menu menu() {
//        return new Menu(productRepository());
//    }
//
//    @Bean
//    public ProductRepository productRepository() {
//        return new ProductRepository();
//    }
//
//    @Bean
//    public Cart cart() {
//        return new Cart(productRepository(), menu());
//    }
//
//    @Bean
//    public Order order() {
//        return new Order(cart(), discount());
//    }
//
//    @Bean
//    public Discount discount() {
//        return new Discount(new DiscountCondition[] {
//                new CozDiscountCondition(new FixedRateDiscountPolicy()),
//                new KidDiscountCondition(new FixedAmountDiscountPolicy())
//        });
//    }
}
  1. 기존 Configure 클래스에 @ComponentScan 애너테이션을 추가
  2. 빈을 생성하는 클래스들에 @Component 애너테이션 추가
  3. 해당 클래스의 생성자에 @Autowired 애너테이션 추가
  • @ComponentScan
    • @Component 애너테이션이 붙은 클래스들을 스캔하여 자동으로 스프링 빈으로 등록
    • @Configuration에도 @Component 애너테이션이 붙어 있어 스캔 대상에 포함
    • 해당 구성 정보 클래스의 패키지가 스캔의 시작 위치
    • @ComponentScan(basePackages = “”) "" 사이에 패키지 이름을 넣어 스캔 범위 변경 가능
  • @Component
    • 스프링 컨테이너가 해당 클래스의 인스턴스를 생성해 스프링 빈으로 관리

@Autowired

  • 필요한 클래스의 인스턴스를 생성하고, 해당 클래스가 필요로 하는 의존성을 자동으로 찾아서 주입

  • 생성자, 필드, 메서드에 적용 가능

  • 타입으로 빈을 조회

  • 애너테이션이 붙은 곳에 들어 올 수 있는 타입의 객체가 여러개면 필드명이나 매개변수명으로 매칭을 시도

  • @Autowired 필드명 매칭

    • @Autowired는 먼저 타입으로 빈을 조회하고, 여러 개의 빈이 있는 경우에 필드명 또는 매개변수명으로 빈을 매칭
    • 필요로하는 객체의 필드명으로 매칭
@Component
public class UserDiscountCondition implements DiscountCondition {

    @Autowired
    private DiscountPolicy fixedRateDiscountPolicy;
@Component
public class FixedRateDiscountPolicy implements DiscountPolicy {

    private int discountRate = 10;

    public int calculateDiscountedPrice(int price) {
        return price - (price * discountRate / 100);
    }
}
@Component
public class KidDiscountCondition implements DiscountCondition {

    @Autowired
    private DiscountPolicy fixedAmountDiscountPolicy;
@Component
public class FixedAmountDiscountPolicy implements DiscountPolicy {

    private int discountAmount = 500;

    public int calculateDiscountedPrice(int price) {
        return price - discountAmount;
    }
}

  • @Qualifier 사용
    • @Qualifier이 붙여진 추가 구분자를 통해 매칭되는 빈이 있는지 탐색
    • 매칭되는 빈이 없다면 빈의 이름으로 조회
@Component
public class UserDiscountCondition implements DiscountCondition {

    
    private DiscountPolicy discountPolicy;
    
        public UserDiscountCondition(@Qualifier("fixedRate") DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
@Component
@Qualifier("fixedRate")
public class FixedRateDiscountPolicy implements DiscountPolicy {

    private int discountRate = 10;

    public int calculateDiscountedPrice(int price) {
        return price - (price * discountRate / 100);
    }
}
@Component
public class KidDiscountCondition implements DiscountCondition {

    private DiscountPolicy discountPolicy;
    
    public KidDiscountCondition(@Qualifier("fixedAmount") DiscountPolicy discountPolicy) {
    	this.discountPolicy = discountPolicy;
    }
@Component
@Qualifier("fixedAmount")
public class FixedAmountDiscountPolicy implements DiscountPolicy {

    private int discountAmount = 500;

    public int calculateDiscountedPrice(int price) {
        return price - discountAmount;
    }
}

  • @Primary 사용
    • 빈 객체들 간 우선순위를 설정
    • 상대적으로 사용 빈도가 적은 인스턴스를 @Qualifier로 지정하여 상황에 맞게 사용
@Component
public class UserDiscountCondition implements DiscountCondition {

    
    private DiscountPolicy discountPolicy;
    
        public UserDiscountCondition(@Qualifier("fixedRate") DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }
@Component
@Qualifier("fixedRate")
public class FixedRateDiscountPolicy implements DiscountPolicy {

    private int discountRate = 10;

    public int calculateDiscountedPrice(int price) {
        return price - (price * discountRate / 100);
    }
}
@Component
public class KidDiscountCondition implements DiscountCondition {

    private DiscountPolicy discountPolicy;
    
    public KidDiscountCondition(DiscountPolicy discountPolicy) {
    	this.discountPolicy = discountPolicy;
    }
@Component
@Primary
public class FixedAmountDiscountPolicy implements DiscountPolicy {

    private int discountAmount = 500;

    public int calculateDiscountedPrice(int price) {
        return price - discountAmount;
    }
}

0개의 댓글