[스프링] DI(Dependency Injection)

손성우·2022년 1월 11일
0

스프링

목록 보기
5/9
**스프링의 주요 특징**
의존관계를 주입하는 기능으로, 객체를 직접 생성하지 않고 외부에서 생성한 후 주입시키는 방식
결합도가 낮아지고 유연성이 높아진다.

IoC

위에 설명한 스프링의 주요 특징을 IoC(Inversion of Control)이라고 한다.

  1. 일반적인 의존성 제어권.(개발자가 직접 객체를 생성하고 주입)
public class MemberController {
	private MemberService memberService = new MemberService();
}
  1. 스프링 컨테이너가 관리(IoC)
public class MemberController{
	private MemberService memberService;
    public MemberController(MemberService memberService){
    	this.memberService = memberservice;
    }
}

위 처럼 객체를 직접 생성하지 않고, 외부(스프링 컨테이너)에서 의존성을 가져오는 경우인데. 이렇게 의존성을 주입하는 것을 DI(Dependency Injection)이라고 한다. DI가 IoC의 일부라고 생각하면 된다.

그리고 의존성 주입은 반드시 Bean으로 등록된 객체만 가능하다.

Bean

스프링 컨테이너가 관리하는 객체들을 Bean이라고 한다. 자바 빈과 똑같다.

  • 자바빈 프로퍼티 규약의 수정자 메서드 방식을 사용하는 방법
    필드의 값을 직접 변경하지 않고, setXxx, getXxx 라는 메서드를 통해서 값을 읽거나 수정하는 규칙을 만들었는데, 그것이 자바빈 프로퍼티 규약
  • 자바빈 프로퍼티 예시
class Data {

 private int age;
 public void setAge(int age) {
 	this.age = age;
 }
 public int getAge() {
 	return age;
 }
}

Bean 등록 방법

  1. @Component 어노테이션(자동)
    등록하고 싶은 클래스에 선언하면된다. 외부 라이브러리나 설정정보들을 제외한 대부분의 클래스들은 위와같은 자동등록을 사용한다.
@Component
public class OrderServiceImpl implements OrderService{
	...
}

이렇게 선언 하면 @SpringBootApplication이 선언된 main클래스에서 실행시
@ComponentScan이 동작되어 @ComponentScan이 선언된 클래스의 동일 및 하위 패키지에 선언된 @Component들을 모두 스프링 빈으로 등록해준다.

자세한 내용은 https://velog.io/@tjddnths0223/ComponentScan

@SpringBootApplication
public class Week01Application {

    public static void main(String[] args) {
        SpringApplication.run(Week01Application.class, args);
    }

}

@Component대신 @Service, @Controller, @Repository를 선언했을 경우도 자동으로 빈으로 등록이 되는 것을 볼 수 있는데 그 이유는 위 어노테이션 안에 @Component가 선언되었기 때문이다.

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Service {

	/**
	 * The value may indicate a suggestion for a logical component name,
	 * to be turned into a Spring bean in case of an autodetected component.
	 * @return the suggested component name, if any (or empty String otherwise)
	 */
	@AliasFor(annotation = Component.class)
	String value() default "";

}
  1. @Bean
    외부 라이브러리나 설정 정보들을 빈으로 등록할 떄 많이 사용된다.
@Configuration
public class AppConfig {

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
      
}

DI(Dependency Injection)

1. 생성자 주입(강추!)

  • 불변, 필수 의존관계에 사용
    -> final을 붙임으로써 필수적으로 값을 넣어야 한다.
  • 생성자 호출시점에 딱 1번만 호출된다.
  • 생성자가 1개만 존재하면 @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(required = false) 로 지정하면 된다.

생성자 주입을 강추하는 이유

  1. 대부분의 의존관계 주입은 애플리케이션 종료시점가지 변경할 일이 거의 없다. 변경해서는 안된다.
  2. 수정자 주입을 사용하면, setXXX메서드를 public으로 열어두어야 하는데 누군가 실수로 변경할 수도 있다.
  3. 의존관계가 어떤지 눈에 보인다.(수정자 메서드는 코드에 들어가봐야 알 수 있다.)
  4. 생성자에서 값이 누락된 경우 컴파일 오류가 발생해서 바로 확인할 수 있다.(컴파일 오류는 가장 직관적이고 해결하기 쉬운 오류다!)

참고:

  • 수정자 주입을 포함한 나머지 주입 방식은 모두 생성자 이후에 호출되므로, 필드에 final 키워드를 사용할 수 없다. 오직 생성자 주입 방식만 final 키워드를 사용할 수 있다.
  • 프레임워크없이 순수 자바 코드로 단위 테스트를 진행할 때, 수정자 메소드인 경우 @Autowired가 작동하지 않아 의존관계 주입 누락으로 실행은 되나 널NPE(NullPointException)이 발생한다. 그러나 생성자 주입일 경우 컴파일 오류가 발생하고 IDE에서 어디에서 문제가 생겼는지 알 수 있다.

2. 수정자 주입(setter 주입)

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

 private MemberRepository memberRepository;
 private DiscountPolicy discountPolicy;
 
 @Autowired
 public void setMemberRepository(MemberRepository memberRepository) {
 		this.memberRepository = memberRepository;
 }
 
 @Autowired
 public void setDiscountPolicy(DiscountPolicy discountPolicy) {
 		this.discountPolicy = discountPolicy;
 }
}

public으로 열려있어서 잘못 건드리면 에러가 날 수 있다.

3. 필드 주입(비추)

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

 @Autowired
 private MemberRepository memberRepository;
 
 @Autowired
 private DiscountPolicy discountPolicy;
 
}

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

4. 일반 메서드 주입

  • 한 번에 여러 필드를 주입 받을 수 있다.
  • 사용 잘 안한다.
@Component
public class OrderServiceImpl implements OrderService {

 private MemberRepository memberRepository;
 private DiscountPolicy discountPolicy;
 
 @Autowired
 public void init(MemberRepository memberRepository, DiscountPolicy 
discountPolicy) {

   this.memberRepository = memberRepository;
   this.discountPolicy = discountPolicy;
 
 }
}

조회할 빈의 타입 매칭이 2개 이상일 때

@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) {
 this.memberRepository = memberRepository;
 this.discountPolicy = discountPolicy;
}

만약 @Qualifier("mainDiscountPolicy")을 못찾으면 mainDiscountPolicy라는 스프링 빈을 추가로 찾는다. 하지만, @Qualifier는 찾는 용도로만 사용하는 것이 좋다.

@Primary

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

활용방법

코드에서 자주 사용하는 스프링 빈에 @Primary를 선언하고, 특별한 곳에서 가끔 사용하는 곳은 @Qualifier를 선언해서 필요한 곳에 직접 지정해서 사용한다.

  • 스프링은 자동보다는 수동이, 넒은 범위의 선택권 보다는 좁은 범위의 선택권이 우선
    순위가 높다. 따라서 여기서도 @Qualifier 가 우선권이 높다.

주의

@Qulifier("") 이렇게 적으면 문자라서 컴파일시 타입 체크가 안되는데 이는 애노테이션을 직접 만들면 해결 할 수 있다.

  1. 애너테이션
@Target({ElementType.FIELD, ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@Qualifier("mainDiscountPolicy")
public @interface MainDiscountPolicy {
}
  1. Qualifier 지정할 클래스
@MainDiscountPolicy
public class RateDiscountPolicy implements DiscountPolicy {}
  1. 의존관계 주입할 클래스
@Autowired
public OrderServiceImpl(MemberRepository memberRepository,
 @MainDiscountPolicy DiscountPolicy discountPolicy) {
 this.memberRepository = memberRepository;
 this.discountPolicy = discountPolicy;
}

애노테이션에는 상속이라는 개념이 없다. 이렇게 여러 애노테이션을 모아서 사용하는 기능은 스프링에서 지원하는 기능이다.

주의 @Autowired를 통한 DI는 스프링이 관리하는 객체에서만 동작한다.
스프링 빈으로 등록하지 않고 내가 직접 생성한 객체에서는 동작하지 않는다.

출처 : 김영한의 스프링 핵심 원리 - 기본편

profile
백엔드 개발자를 꿈꾸며 공부한 내용을 기록하고 있습니다.

0개의 댓글