DI(Dependency Injection)

김수민·2023년 4월 7일
0

백엔드 부트캠프

목록 보기
50/52

DI란?

DI는 IoC라는 원칙을 구현하기 위해서 사용되는 방법 중 하나. Dependency Injection 즉, 번역하면 외존성 주입이라고 표현할 수 있음.

위의 코드는 실제 MemberService라는 객체에서 MemberRepository라는 객체에 의존성을 가지고 있음. 아래 구현한 세가지 메스들은 모두 MemberRepository 객체에서 구현한 메서드를 사용하고 있음. 이러한 상황에서 MemberRepository라는 객체를 다른 MockRepository라는 객체로 교체하는 경우, 새롭게 의존관계를 설정해준 MockRepository라는 객체에 의존하지 않으면 해당 코드도 모두 변경되어야 함. 그리고 객체간의 관계가 변경될 때마다 직접 해당 코드를 찾아 수정하고 문제점이 없는지 살펴보는 과정을 거쳐야 함. 위에서 발생하는 문제점들은 의존성 주입을 통해 해결할 수 있음.

위 그림에서 작성된 코드와 같이 생성자를 통해 의존성을 주입받음으로써 객체가 생성되는 순간 의존관계를 설정할 수 있음. 생성자 뿐만 아니라 다른 방식으로도 의존성 주입이 가능하지만, 일반적으로는 생성자를 통한 의존관계 주입을 사용함. 생성자를 통해 의존성을 주입하게 되면 실제로 스프링에서 의존성 주입을 도와주게 됨. 이 방법은 스프링에서 공식적으로 추천하는 방법임.

위와 같이 코드를 수정하게 된다면 MemberTest.java 파일에서 MemberService 객체를 생성할 때 오류가 발생하게 됨. 직접 해당 파일에서 MemberRepository 객체를 생성해 생성자를 통해 넣어주면 해결이 가능하지만, 그렇게 되면 객체를 생성할 때마다 직접 주입할 객체를 직접 작성해야 함 -> 의존성 주입을 사용하는 의미 퇴색.

의존성 주입을 관리하는 설정 파일을 만들어서 관심사 분리를 통해 해결

MemberService와 같이 CoffeeService 객체도 생성자 주입을 통한 코드로 변경 후 새로운 파일을 생성함.

이 후 DependencyConfig.java 파일을 생성 후, 코드를 입력함.

해당 파일을 통해서 의존관계가 이루어지는 부분을 모두 관리할 수 있음. 하지만 현재 작성된 코드는 같은 객체를 new를 통해 두 번 생성함.
명확한 역할을 구현 부분을 나누기 위해 코드를 수정함.

package com.codestates.section2week4;

import com.codestates.section2week4.coffee.CoffeeRepository;
import com.codestates.section2week4.coffee.CoffeeService;
import com.codestates.section2week4.member.MemberRepository;
import com.codestates.section2week4.member.MemberService;

public class DependencyConfig {

  public MemberService memberService() {
    return new MemberService(memberRepository());
  }

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

  public CoffeeService coffeeService() {
    return new CoffeeService(coffeeRepository());
  }

  public CoffeeRepository coffeeRepository() {
    return new CoffeeRepository();
  }
}

이렇게 역할을 명확하게 나누어 작성해준다면 더 이상 서비스 내부에서 객체 주입 관련해서 수정 코드가 발생하는 것이 아닌 DependencyConfig.java에서 구현 부분만 수정해주면 됨.

  • MemberServiceCoffeeService 입장에서는 생성자를 통해 어떤 구현 객체가 주입될지 알 수 없으며 알 필요도 없음
    - 어떤 객체가 주입될지는 외부(DependencyConfig)에서 결정함
  • MemberServiceCoffeeService는 오로지 실행에만 집중함
  • 해당 코드의 수정이 이루어지면 MemberTest.javaCoffeeTest.java 두가지 파일에서 서비스 객체를 생성하는 부분에서 오류 발생
  • 해당 객체를 생성하는 코드의 수정이 필요

MemberTest.java  코드

package com.codestates.section2week4.member;

import com.codestates.section2week4.DependencyConfig;

public class MemberTest {
  public static void main(String[] args) {
    DependencyConfig dependencyConfig = new DependencyConfig();
    MemberService memberService = dependencyConfig.memberService();

    Member member = new Member(0L, "lucky@codestates.com", "KimLucky", "010-1234-5678");
    memberService.createMember(member);

    Member currentMember = memberService.getMember(0L);

    System.out.println("회원 가입한 유저 : " + member.getName());
    System.out.println("현재 첫번째 유저 : " + currentMember.getName());

    if(member.getName().equals(currentMember.getName())) {
      System.out.println("새롭게 가입한 사용자와 현재 사용자가 같습니다.");
    }

    memberService.deleteMember(0L);

    if(memberService.getMember(0L) == null) {
      System.out.println("회원 삭제가 정상적으로 완료되었습니다.");
    }
  }
}

CoffeeTest.java 코드

package com.codestates.section2week4.coffee;

import com.codestates.section2week4.DependencyConfig;

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

    DependencyConfig dependencyConfig = new DependencyConfig();
    CoffeeService coffeeService = dependencyConfig.coffeeService();

    Coffee coffee = new Coffee(0L, "바닐라 라떼", "vanilla latte", 5000);
    coffeeService.createCoffee(coffee);

    if(coffeeService.getCoffee(0L).getKorName().equals(coffee.getKorName())) {
      System.out.println("바닐라 라떼가 등록되었습니다.");
    }

    coffeeService.editCoffee(0L, "바닐라 라떼", 3000);

    if(coffeeService.getCoffee(0L).getPrice() == 3000) {
      System.out.println("바닐라 라떼의 금액이 정상적으로 변경되었습니다.");
    }

    coffeeService.deleteCoffee(0L);

    if(coffeeService.getCoffee(0L) == null) {
      System.out.println("바닐라 라떼가 정상적으로 삭제되었습니다.");
    }
  }
}

프레임워크에서 사용하는 방식이 아닌 단순 컨테이너를 활용해 객체의 의존관계를 주입하는 방식.

스프링 컨테이너(Spring Container)

스프링 컨테이너는 스프링 프레임워크의 핵심 컴포넌트임. 스프링 컨테이너는 내부에 존재하는 애플리케이션 빈의 생명주기를 관리함.

What? (스프링 컨테이너란 무엇인가)

ApplicationContext를 스프링 컨테이너라고 하고 인터페이스로 구현되어 있음 (다형성 적용)

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, 
	HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResroucePatternResolver {

  • 스프링 컨테이는 XML, 애너테이션 기반의 자바 설정 클래스로 만들 수 있음
  • 예전에는 개발자가 xml을 통해 모두 설정해줬지만, 이러한 복잡한 부분들을 Spring Boot를 사용하면서 거의 사용하지 않게 되었음.
  • 빈의 인스턴스화, 구성, 전체 생명 주기 및 제거까지 처리함
    - 컨테이너는 개발자가 정의한 Bean을 객체로 만들어 관리하고 개발자가 필요로 할 때 제공함
  • 스프링 컨테이너를 통해 원하는만큼 많은 객체를 가질 수 있음
  • 의존성 주입을 통해 애플리케이션의 컴포넌트를 관리함
    - 스프링 컨테이너는 서로 다른 빈을 연결해 어플리케이션의 빈을 연결하는 역할을 함
    • 개발자는 모듈 간에 의존 및 결합으로 인해 발생하는 문제로부터 자유로울 수 있음
    • 메서드가 언제, 어디서 호출되어야 하는지, 메서드를 호출하기 위해 필요한 매개 변수를 준비해서 전달하지 않음

Why? (스프링 컨테이너는 왜 사용할까)

  • 객체를 사용하기 위해 new 생성자를 썼어야 함
  • 애플리케이션에서 이러한 객체가 무수히 많이 존재하고 서로 참조하게 되어있었음
    - 서로 참조가 심할수록 의존성이 높다고 표현함
    - 낮은 결합도와 높은 캡슐화가 객체지향프로그래밍의 핵심 중 하나인데 높은 의존성은 이를 지키지 못하는 방법이 될 수 밖에 없음
  • 객체 간의 의존성을 낮추기 위해 Spring 컨테이너가 사용됨

문제점 및 Spring 컨테이너가 해결볍이 되는 이유

  • 기존의 방식으론 새로운 정책이 생기게 될 경우 변경 사항들을 수작업으로 수정이 필요했음
  • 서로 의존이 많이 되어있지 않은 작업 초반부에는 하나하나 수정할 수 있겠지만 점점 서비스의 코드가 거대해질 경우 의존도는 높아져있을 것이고 그에 따른 코드의 변경도 많이 필요해질 것임
  • 스프링 컨테이너를 사용하면서 구현 클래스에 있는 의존을 제거하고 인터페이스에만 의존하도록 설계할 수 있음

How? (스프링 컨테이너의 생성과정은 어떻게 될까)

  • 스프링 컨테이너는 Configuration Metadata를 사용함
  • 스프링 컨테이너는 파라미터로 넘어온 설정 클래스 정보를 사용해서 스프링 빈을 등록함
  • new AnnotationConfigApplicationContext (구성정보.class)로 스프링에 있는 @Bean의 메서드를 등록함
  • 애너테이션 기반의 자바 설정 클래스로 Spring을 만드는 것을 의미함
// Spring Container 생성
ApplicationContext applicationContext = 
	new AnnotationConfigApplicationContext(DependencyConfig.class);
  • 스프링 컨테이너를 만드는 다양한 방법은 ApplicationContext 인터페이스의 구현체임
    - DependencyConfig.class 등의 구성정보를 지정해줘서 스프링 컨테이너를 생성해야 함
    - DependencyConfig에 있는 구성 정보를 통해서 스프링 컨테이너는 필요한 객체들을 생성하게 됨
    - 애플리케이션 클래스는 구성 메타데이터와 결합되어 ApplicationContext 생성 및 초기화된 후 완전히 구성되고 실행 가능한 시스템 또는 애플리케이션을 갖게 됨
  • 스프링 빈 조회에서 상속관계가 있을 시 부모타입으로 조회하면 자식 타입도 함께 조회함
    - 모든 자바 객체의 초고 부모인 object타입으로 조회하면 모든 스프링 빈을 조회함

스프링 컨테이너의 종류

파라미터로 넘어온 설정 클래스 정보를 참고해서 빈의 생성, 관계 설정 등의 제어작업을 총괄하는 컨테이너

Bean Factory

  • 스프링 컨테이너의 최상위 인터페이스
  • Bean Factory는 빈을 등록하고 생성하고 조회하고 돌려주는 등 빈을 관리하는 역할을 함
  • getBean() 메소드를 통해 빈을 인스턴스화 할 수 있음
  • @Bean이 붙은 메서드 명을 스프링 빈의 이름으로 사용해 빈 등록을 함

Application Context

  • Bean Factory의 기능을 상속받아 제공함
  • 빈을 관리하고 검색하는 기능을 Bean Factory가 제공하고 그 외에 부가기능을 제공함
  • 부가 기능
    - MessageSource: 메시지 다국화를 위한 인터페이스
    - EnvironmentCapable: 개발, 운영 등 환경변수 등으로 나눠 처리하고 애플리케이션 구동 시 필요한 정보들을 관리
    - ApplicationEventPublisher: 이벤트 관련 기능을 제공하는 인터페이스
    - ResourceLoader: 파일, 클래스 패스, 외부 등 리소스를 편리하게 조회

컨테이너 인스턴스화

ApplicationContext 생성자에게 제공된 위치 경로 또는 경로는 컨테이너가 로컬 파일 시스템, Java CLASSPATH 등과 같은 다양한 외부 리소스로부터 구성 메타데이터를 로드할 수 있도록 하는 리소스 문자열

// Annotation
Application context = new 
	AnnotationConfigApplicationContext(DependencyConfig.class);

빈(Bean)

스프링 컨테이너에 의해 관리되는 재사용 소프트웨어 컴포넌트
- Spring 컨테이너가 관리하는 자바 객체를 의미하며 하나 이상의 빈을 관리함

  • 빈(bean)은 인턴스화된 객체를 의미함
  • 스프링 컨테이너에 등록된 객체를 스프링 빈이라고 함
  • @Bean이 적힌 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록함
  • 빈은 클래스의 등록정보, 게터/세터 메서드를 포함함
  • 빈은 컨테이너에 사용되는 설정 메타데이터로 생성됨
  • 설정 메타디에터
    - XML 또는 자바 에너테이션, 자바 코드로 표현함
    - 컨테이너의 명령과 인스턴스화, 설정, 조립할 객체를 정의함

Bean 접근 방법

  • ApplicationContext를 사용하여 bean 정의를 읽고 액세스 할 수 있음
// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext("servfes.xml", "daos.xml");

// retrieve configured instance
PetStareService service = context.getBean("memberRepository", memberRepository.class);

//use configured instance
List<String> userList = service.getUsernameList();
  • getBean을 사용하여 bean의 인스턴스를 가져올 수 있음
  • ApplicationContext 인터페이스는 bean을 가져오는 몇 가지 방법들이 존재함
  • 실제적으로 응용 프로그램 코드에서는 getBean() 메서드로 호출하여 사용하면 안됨

Bean Definition

  • 속성에 따라 컨테이너가 Bean을 어떻게 생성하고 관리할지 결정함
  • @Bean이나 <bean>당 각 1개씩 메타 정보가 생성됨
  • Spring이 설정 메타정보를 BeanDifinition 인터페이스를 통해 관리하기 때문에 컨테이너 설정을 XMl, Java로 할 수 있음. 스프링 컨테이너는 설정 형식이 XML인지 Java 코드인지 모르고 BeanDefinition만 알면 됨.

빈 스코프(Bean Scope)

Bean definition을 만들 때 해당 bean definition에 의해 정의된 클래스의 실제 인스턴스를 만들기 위한 레시피를 만듦. (빈이 존재할 수 있는 범위를 의미)

  • 특정 bean 정의에서 생성된 개체에 연결할 다양한 의존성 및 구성 값뿐만 아니라 특정 bean 정의에서 생성된 개체의 범위도 제어할 수 있음.
  • Spring Framework는 6개의 범위를 지원하며 그 중 4개는 ApplicationContext를 사용하는 경우에만 사용할 수 있음
  • Bean은 여러 범위 중 하나에 배치되도록 정의할 수 있음
  • 구성을 통해 생성하는 개채의 범위를 선택할 수 있기 때문에 강력하고 유연함
  • 사용자 정의 범위를 생성할 수 있음
ScopeDescription
singleton(Default) 각 Srping컨테이너에 대한 단일 객체 인스턴스에 대한 단일 bean definition의 범위를 지정함
prototype스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프
request웹 요청이 들어오고 나갈 때까지 유지되는 스코프
session웹 세션이 생성되고 종료될 때까지 유지되는 스코프
application웹 서블릿 컨텍스와 같은 범위로 유지되는 스코프
websocket단일 bean definition 범위를 WebSocket의 라이프사이클까지 확장함. Spring ApplicationContext의 컨텍스트에서만 유효함.

싱긑톤(singleton) 스코프

클래스의 인스턴스가 딱 한 개만 생성되는 것을 보장하는 디자인 패턴임.

  • 스프링 컨테이너의 시작과 함께 생성되어 스프링 컨테이너가 종료될 때까지 유지됨
  • 싱글톤 빈의 하나의 공유 인스턴스만 관리함
    - private 생성자를 사용해 외부에서 임의로 new를 사용하지 못하도록 막아야 함
  • 해당 bean definition과 일치하는 ID 또는 ID를 가진 빈에 대한 모든 요청은 스프링 컨테이너에서 해당 특정 빈 인스턴스를 반환함
  • 스프링 컨테이너 종료시 소멸 메서드도 자동으로 실행됨

싱글톤 내용 정리

  • 싱글톤은 해당 빈의 인스턴스를 오직 하나만 생성해서 사용하는 것을 의미
  • 단일 인스턴스는 싱글톤 빈의 캐시에 저장됨
  • 이름이 정해진 빈에 대한 모든 요청과 참조는 캐시된 개체를 반환함
    - 싱글톤 스코프의 스프링 빈은 여러번 호출해도 모두 같은 인스턴스 참조 주소값을 가짐
  • 작동 방식

Java 기반 컨테이너(Container) 설정

@Bean and @ Configuration

자바 기반 설정의 가장 중요한 애너테이션 2가지

  • 메서드가 Spring 컨테이너에서 관리할 새 객체를 인스턴스화, 구성 및 초기화한다는 것을 나타내는데 사용됨
// DependencyConfig 클래스
컨텍스트를 인스턴스화 할 때
@Configuration
public class DependencyConfig {

	@Bean
    public MyService myService() {
    	return new MyServiceImpl();
    }
}

AnnotationConfigApplicationContext를 사용하여 스프링 컨테이너를 인스턴스화 하는 방법

  • Spring 3.0에 도입된 AnnotationConfigApplicationContext
  • ApplicationContext 구현은 아래와 같은 애너테이션이 달린 클래스로 파라미터를 전달받고 있음
    - @Configuration 클래스
    - @Component 클래스
    - JSR-330 메타데이터
  • @Configuration 클래스가 입력으로 제공되면 @Configuration 클래스 자체가 Bean 정의로 등록되고 클래스 내에서 선언된 모든 @Bean 메서드도 @Bean 정의로 등록됨
  • @Component 클래스와 JSR-330 클래스가 제공되면 빈 정의로 등록되며 필요한 경우 해당 클래스 내에서 @Autowired 또는 @Inject와 같은 DI 메타데이터가 사용되는 것으로 가정함

@Configuration 클래스를 입력으로 사용 (DependencyConfig.class)

public static void main(String[] args) {
	ApplicationContext ctx = new AnnotationConfigApplicationContext(DependencyConfig.class);
    MyService mySerivce = ctx.getBean(MyService.class);
    myService.doStuff();
}

@Component 또는 JSR-330 주석이 달린 클래스는 다음과 같이 생성자에 입력으로 사용

public static void main(String[] args) {
	ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImp.class, Dependency1.class);
    MyService myService = ctx.getBean(MySerive.class);
    myService.doStuff();
}

@Autowired - MySerivceImpl, Dependency1, Dependency2에서 스프링 의존성 주입 애너테이션을 사용한 예제

@Bean 애너테이션 사용하기

@Bean은 메서드-레벨 애너테이션이며, 에서 제공하는 일부 속성을 지원함

  • init-method
  • detroy-method
  • autowiring
  • @Bean 애너테이션은 @Configuration-annoted 또는 @component-annoted 클래스에서 사용할 수 있음

빈 선언
@Bean 애너테이션을 메서드에 추가해서 Bean으로 정의(선언)할 수 있음

  • 애너테이션 방식의 configuration
@Configuration
public class DependenctConfig {

	@Bean
    public TransferServiceImpl transferService() {
    	return new TransferServiceImpl();
    }
}
  • 빈 정의가 있는 인터페이스를 구현하여 bean configuration을 설정할 수 있음
public interface BaseConfig{

	@Bean
    default TransferServiceImpl transferService() {
    	return new TransferSerivceImpl();
    }
}

@Configuration
public class DependencyConfig implements BaseConfig{

}

빈 의존성
@Bean 애너테이션이 추가된 (@Bean-annoted) 메서드는 빈을 구축하는데 필요한 의존성을 나타내는데 매개 변수를 사용할 수 있음

@Configuration
public class DependecyConfig{

	@Bean
    public TransferSerivce transferService(AccountRepository accountRepositry) {
    	return new TransferServiceImpl(accountRepository);
    }
}

@Configuration 애너테이션 사용하기

  • @Configuration은 해당 객체가 bean definition의 소스임을 나타내는 애너테이션
  • @Configuration는 @Bean-annoted 메서드를 통해 bean을 선언함
  • @Configuration 클래스의 @Bean 메서드에 대한 호출을 사용하여 bean 사이의 의존성을 정의할 수 있음

Bean 사이에 의존성 주입
빈이 서로 의존성을 가질 때, 의존성을 표현하는 것은 다른 bean 메서드를 호출하는 것처럼 간단함

  • beanOne은 생성자 주입을 통해 beanTwo에 대한 참조를 받음
@Configuration
public class DependencyConfig {

	@Bean
    public BeanOne beanOne() {
    	return new BeanOne(beanTwo());
    }
    
    @Bean
    public BeanTwo beanTwo() {
    	return new BeanTwo();
    }
}

Java를 기반으로 설정되어있는 환경에서 내부적으로 작동하는 방식에 대한 정보

@Configuration
public class DependencyConfig {

	@Bean
    public ClientService clientService1() {
    	ClientServiceImpl clientService = new ClientSerivceImpl();
        clientService.setClientDao(clientDao());
        return clientService;
    }
    
    @Bean
    public ClientSerivce clientService2() {
    	ClientSerivceImpl clientSerivce = new ClientSerivceImpl();
        clientService.setClientDao(clientDao());
        return clientSevice;
    }
    
    @Bean
    public ClientDao clientDao() {
    	return new ClientDaoImpl();
    }
}
  • clientDao() 메서드는 clientSerivce1()clientService2() 메서드에서 한 번씩 호출됨
  • 이 메서드는 clientDaoImpl의 새 인스턴스를 만들고 이를 반환하므로 일반적으로 두 개의 인스턴스(각 서비스마다 하나씩)이 있어야 함
    - 문제점: Spring에 인스턴스화된 빈은 기본적으로 싱글톤 범위를 갖게 됨
  • 하위 클래스의 하위 메서드는 상위 메서드를 호출하고 새 인스턴스를 만들기 전에 먼저 컨테이너에 캐시된(범위 지정) bean이 있는지 확인함

Java 코드에서 애너테이션을 사용해 Spring 컨테이너를 구성하는 방법

Spring의 자바 기반 구성 기능 특징인 애너테이션을 사용해 구성의 복잡성을 줄일 수 있음

  • @Import 애너테이션
    - XML 파일 내에서 요소가 사용되는 것처럼 구성을 모듈화하는데 사용됨
    - 다른 구성 클래스에서 @Bean definitions를 가져올 수 있음
@Configuration
public class DependencyConfigA {

	@Bean
    public A a() {
    	return new A();
    }
}

@Configuration
@Import(DependencyConfigA.class)
public class DependencyConfigB {

	@Bean
    public B b() {
    	return new B();
    }
}

컨텍스르트를 인스턴스화할 때 DependencyConfigA.class와 DependencyConfigB.class 모두 지정하는 대신 DepencyConfigB만 제공하면 됨

public static void main(String[] args) {
	ApplicationContext ctx = new AnnotationConfigApplicationContext(DependencyConfigB.class);
    
    // now both beans A and B will be available
    A a = ctx.getBean(A.class);
    B b = ctx.getBean(B.calss);
}
  • ctx에 Import(DependencyConfigA.class) 받은 DependencyConfigB.class 사용으로 인해 ctx.getBean(A.class)가 가능해짐.
  • 컨테이너 인스턴스화를 단수화할 수 있음
    - 많은 @Configuration 클래스를 기억할 필요 없이 하나의 클래스만 처리하면 됨

문제점

  • 실제로 사용될 때 빈은 한 개의 구성파일만 @Import 받지 않고 여러 구성 클래스 간에 걸쳐 서로 의존성을 가짐
  • XML을 사용할 때는 컴파일러가 관여하지 않아 컨테이너 초기화 중에 ref="some Bean"을 선언하여 스프링으로 해결할 수 있어 문제가 되지 않음
  • @Configuration 클래스를 사용할 때 자바 컴파일러는 구성 모델에 제약을 두며 다른 빈에 대한 참조는 유효한 자바 구문이어야 함
@Configuration
public class ServiceConfig {

	@Bean
    public TransferService transferService(AccountRepository accountRepository) {
    	return new TransferServiceImp(accountRepository);
    }
}

@Configuration
public class RepositoryConfig {

	@Bean
    public AccountRepository accountRepository(DataSource dataSource) {
    	return new JdbcAccountRepository(dataSource);
    }
}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {

	@Bean
    public DataSource dataSource() {
    	// return new DataSource
    }
}

public static void main(String[] args) {
	ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
    // everthirng wires up across configuration classes
    TransferSerivce transferService = ctx.getBean(TransferSeriver.class);
    transferService.transfer(100.00, "A123", "C456");
}

해결방법

  • @Bean 메서드는 빈 의존성을 설명하는 임의 개수 파라미터를 가질 수 있음
  • @Autowired 및 @Value 주입 및 다른 bean과 동일한 기능을 사용할 수 있음
  • 단, @Configuration의 생성자 주입은 스프링 프레임워크 4.3에서만 지원됨
  • 대상 빈이 하나의 생성자만 정의하는 경우 @Autowired를 지정할 필요가 없음
@Configuration
public class ServiceConfig {

	@Autowired
    private AccountRespository accountRepository;
    
    @Bean
    public TransferService transferService() {
    	return new TransferServiceImpl(accountRespository);
    }
}
 
@Configuration
public class RespositoryConfig {
	
    private final DataSource dataSource;
    
    public RepositoryConfig(DataSource dataSource) {
    	this.dataSource = dataSource;
    }
    
    @Bean
    public AccountRepository accountRepository() {
    	return new JdbcAccountRepository(dataSoruce);
    }
}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
	
    @Bean
    public DataSource dataSource() {
    	// return new dataSource
	}
}

public static void main(String[] args) {
	ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
    // everyhing wires up acrros configuration classes
    TransferService transferService = ctx.getBean(TransferService.class);
    trasferService.trasfer(100.00, "A123", "C456");
}

Component Scan

스프링은 설정 정보 없이 자동으로 스프링 빈을 등록하는 컴포넌트 스캔이라는 기능을 제공함

지금까지는 스프링 빈을 등록할 때 자바 코드의 @Bean or XML의 등의 설정 정보에 등록한 스프링 빈들을 직접 작성하였음. 이렇게 수작업으로 등록하게 되면 설정 정보가 커지고 누락하는 등 다양한 문제가 발생할 수 있음. @ComponentScan은 @Component가 붙은 모든 클래스를 스프링 빈으로 등록해주기 때문에 설정 정보에 붙여주면 됨

의존관계도 자동으로 주입하는 @Autowired 기능도 제공함.

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;

@Configuration
@ComponentScan
public class AutoDepdencyConfig {

}

기존에 작성하던 DependencyConfig와 비교한다면 @Bean으로 등록한 클래스를 볼 수 없음.

  • 주의: 컴퐅넌트 스캔을 사용하면 @Configuration이 붙은 설정 정보도 자동으로 등록됨
    - @Configuration이 붙은 설정 정보가 자동 등록되는 이유는 @Configuration 코드에 @Component 애너테이션이 붙어있기 때문.
@Target(ElementType.TYPE)
@Retention(RententionPolicy.RUNTIME)
@Documented
@Component
public @interface Configuration {

기존에 작성한 AppConfig가 있다면 정상적인 작동이 되지 않음.
새 프로그젝트로 진행할 경우엔 문제가 되지 않음
DependenctConfig등 @Configuration 설정이 된 파일이 있을 시 아래 코드 추가: @ComponentScan(excludeFilters = @Filter(type = FilterType.ANNOTATION, classes = Configuration.class))

basePakcages

탐색할 패키지의 시작 위치를 지정하고, 해당 피키지부터 하위 패키지 모두 탐색함.

  • @ComponentScan()의 매개변수로 basePackages = ""를 줄 수 있음
  • 지정하지 않으면 @ComponentScan이 붙은 설정 정보 클래스의 패키지가 시작 위치가 됨
    - 설정 정보 클래스의 위치를 프로젝트 최상단에 두고 패키지 위치를 지정하지 않는 방법이 가장 편함
  • 스프링 부트를 사용하면 @SpringBootApplication을 이 프로젝트 시작 루트 위치에 두는 것을 추천함
    - @SpringBootApplication에 @ComponentScan이 들어있음

컴포넌트 스캔 기본 대상

  • @Component: 컴포넌트 스캔에서 사용됨
  • @Controller & RestController: 스프링 MVC 및 REST 전용 컨트롤러에서 사용됨
  • @Service: 스프링 비즈니스 로직에서 사용됨
    - 특별한 처리를 하지 않음
    - 개발자들이 핵심 비즈니스 로직이 여기 있다는 비즈니스 계층을 인식하는데 도움이 됨
  • @Repository: 스프링 데이터 접근 계층에서 사용됨
    - 스프링 데이터 접근 계층으로 인식하고 데이터 계층의 예외를 스프링 예의로 변환해줌
  • @Configuration: 스프링 설정 정보에서 사용됨
    - 스프링 설정 정보를 인식하고 스프링 빈이 싱글톤을 유지하도록 추가 처리를 함
  • 해당 클래스의 소스 코드에는 @Component를 포함하고 있음
@Target(ElementType.TYPE)
@Retention(RententionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {
@Target(ElementType.TYPE)
@Retention(RententionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
@Target(ElementType.TYPE)
@Retention(RententionPolicy.RUNTIME)
@Documented
@Component
public @interface Repository {

필터

  • includeFilters: 컴포넌트 스캔 대상을 추가로 지정함
  • excludeFilters: 컴포넌트 스캔에서 제외할 대상을 지정함
  • FilterType 옵션
    - ANNOTATION: 기본값, 애너테이션으로 인식해서 동작함
    - ASSIGNABLE_TYPE: 지정한 타입과 자식 타입을 인식해서 동작함
    - ASPECTJ: AspectJ 패턴을 사용함
    - REGEX: 정규 표현식을 나타냄
    - CUSTOM: TypeFilter라는 인터페이스를 구현해서 처리함

다양한 의존관계 주입 방법

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

생성자 주입

생성자를 통해서 의존 관계를 주입받는 방법임. 생성자에 @Autowired를 하면 스프링 컨테이너에 @Component로 등록된 빈에서 생성자에 필요한 빈들을 주입함.

특징

  • 생성자 호출 시점에서 딱 한 번만 호출되는 것이 보장됨
  • 불변과 필수 의존 관계에 사용됨
  • 생성자가 한 개만 존재하는 경우에는 @Autowired를 생략해도 자동 주입됨
  • NullPointerException을 방지할 수 있음
  • 주입받을 필드를 final로 선언 가능함

예제

@Component
public class CoffeeService {
	private final MemberRespository memberRepository;
    private final CoffeeRepository coffeeRepository;
    
    @Autrowired
    public CoffeeService(MemberRepository memberRepository, CoffeeRespository coffeeRespository) {
    	this.memberRespository = memberRepository;
        this.coffeeRepository = coffeeRespository;
    }
}

수정자 주입 (setter 주입)

setter라 불리는 필드의 값을 변경하는 수정자 메서드를 통해 의존 관계를 주입하는 방법

특징

  • 선택과 변경 가능성이 있는 의존 관계에서 사용됨
  • 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방식

예제

@Component
public class CoffeeService {
	private MemberRepository memberRepository;
    private CoffeeRepository coffeeRepository;
    
    @Autowired
    public void SetMemberRepository(MemberRepository memberRepository) {
    	this.memberRepository = memberRepository;
    }
    
    @Autowired
    public void setCoffeeRepository(CoffeeRepository coffeeRepository) {
    	this.coffeeRepository = coffeeRepository;
    }
}
  • 생성자 주입과 차이점은 생성자 대신 set필드명 메서드를 생성하여 의존 관계를 주입하게 됨
  • 수정자의 경우 @Autowired를 입력하지 않으면 실행이 되지 않음
    - @Component가 실행하는 클래스를 스프링 빈으로 등록함
    - 스프링 빈으로 등록한 다음 의존관계를 주입하게 되는데 @Autowired 있는 것들을 자동으로 주입하게 됨
  • 생성자는 한 개일 때 @Autowired가 없어도 작동되는 이유
    - 스프링이 해당 클래스 객체를 생성하여 빈에 넣어야하는데 생성할 때 생성자를 부를 수 밖에 없게 됨. 그렇기 때문에 빈을 등록하면서 의존 관계 주입도 같이 발생하게 됨.

필드 주입

필드에 @Autowired 붙여서 바로 주입하는 방법임

특징

  • 코드가 간결해서 예전에 많이 사용된 방식이지만 외부에서 변경이 불가능하여 테스트하기 힘들다는 단점이 있음
  • DI 프레임워크가 없으면 아무것도 할 수 없음
  • 실제 코드와 상관없는 특정 테스트를 하고 싶을 때 사용할 수 있음
  • 정상적으로 작동하게 하려면 결국 setter가 필요하게 되어서 수정자 주입을 사용하는게 더 편리함

예제

@Component
public class CoffeeService {
	@Autowired
    private MemberRepository memberRepository;
    @Autowired
    private CoffeeRepository coffeeRepository;
}

일반 메서드 주입

일반 메서드를 사용해 주입하는 방법

특징

  • 한번에 여러 필드를 주입할 수 있음
  • 일반적으로 사용되지 않음

옵션 처리

주입할 스프링 빈이 없을 때 동작해야 하는 경우가 있음

  • @Autowired만 사용하는 경우 required 옵션 기본값인 true가 사용되어 자동 주입 대상이 없으면 오류가 발생하는 경우가 있을 수 있음
  • 스프링 빈을 옵셔널하게 해둔 상태에서 등록이 되지 않고 기본 로직으로 동작하게 하는 경우
  • 자동 주입 대상 옵션 처리 방법
    -@Autowired(required=false): 자동 주입할 대상이 없으면 수정자 메서드 자체가 호출되지 않게 함
    - org.springwork.lang.@Nullable: 자동 주입할 대상이 없으면 null이 입력됨
    - Optional<>: 자동 주입 대상이 없으면Optional.empty가 입력됨

생성자 주입을 사용해야하는 이유

과거에는 수정자, 필드 주입을 많이 사용했지만 최근에는 대부분 생성자 주입 사용을 권장함

  • 불변
    - 의존관계 주입은 처음 애플리케이션이 실행될 때 대부분 정해지고 종료전까지 변경되지 않고 변경되어서는 안됨
    - 수정자 주입같은 경우에는 이름 메스드를 public으로 열어두어 변경이 가능하기 때문에 적합하지 않음
    - 누군가 실수로 변경할 수도 있고, 애초에 변경하면 안되는 메서드가 변경될 수 있게 설계되는 것은 좋은 방법이 아님
    - 생성자 주입은 객체를 생성할 때 최초로 한 번만 호출되고 그 이후에는 다시 호출되는 일이 없기 때문에 불변하게 설계할 수 있음
  • 누락
    - 호출했을 때 NPE(Null Point Exception)이 발생하는데 의존관계 주입이 누락되었기 때문에 발생함
    - 생성자 주입을 사용하면 주입 데이터 누락 시 컴파일 오류가 발생함
  • final 키워드 사용 가능
    - 생성자 주입을 사용하면 필드에 final 키워드를 사용할 수 있음
    - 생성자에서 값이 설정되지 않으면 컴파일 시점에서 오류를 확인할 수 있음
    - java: variable (데이터 이름) might not have been initialized
    - 생성자 주입을 제외한 나머지 주입 방식은 생성자 이후에 호출되는 형태이므로 final 키워드를 사용할 수 없음
  • 순환 참조
    - 순환 참조를 방지할 수 있음
    - 생성자를 통해 주입하게 되면 BeanCurrentlyInCreationException이 발생함
    - 개발하다보면 여러 컴포넌트 간에 의존성이 생기게 됨 (A -> B 참조, B -> A 참조)
    - 필드 주입과 수정자 주입은 빈이 생성된 이후에 참조를 하기 때문에 애플리케이션이 어떠한 오류와 경고 없이 구동됨
    • 실제 코드가 호출 될 때까지 문제를 알 수 없음

0개의 댓글