Baeldung의 이 글을 정리 및 추가 정보를 넣은 글입니다.

1. Introduction

  • DI는 IoC 구현에 사용되고, IoC는 소프트웨어 모듈성을 구현하는데 사용된다. 즉 DI는 소프트웨어 모듈성 구현의 중요한 요소다!

  • 이 글은 Spring의 여러 DI 방식 중 하나인 생성자 기반 DI에 대해 배워볼 것이다. 어떤 class의 객체를 초기화할 때 필요로 하는 요소를 주입해서 저런 이름을 가지고 있다.

  • 사용하는 dependency는 spring-boot-starter-web

2. Annotation Based Configuration

  • 사이트에 예시로 나온 다음 코드를 보도록 하자.
@Configuration
@ComponentScan("com.baeldung.constructordi")
public class Config {

    @Bean
    public Engine engine() {
        return new Engine("v8", 5);
    }

    @Bean
    public Transmission transmission() {
        return new Transmission("sliding");
    }
}
  • DI를 하려면 주입에 사용될 bean이 먼저 필요하다. 이전 글에서 언급했지만 @Configuration을 통해 bean을 생성하는 method를 보유한 class를 명시하는게 가능하다.

@configuration이 정확히는 xml을 대신해 bean 생성과 관련된 설정을 하는 class임을 명시하는데 사용된다는 점을 유의하자.

  • 무슨 method로 bean을 만들 것인가? 그건 @Bean annotation을 활용하면 된다. 즉 위의 코드는 Config class가 bean을 생성하는 method를 보유한 class고, engine()transmission()이 bean을 생성하는 method다.

@ComponentScan, @Component

  • 그리고 새로운 annotation 확인이 가능한데, 바로 @ComponentScan이다.

  • 이 annotation은 말 그대로 'component'를 'scan'하라는 의미를 가지고 있다. bean으로 등록되는 class에만 사용이 가능하다. 괄호 안에 보면 패키지가 있는데 이 패키지에 대해 스캔하라는 뜻이다.

  • 여기서 component는 @Componet, 혹은 이를 포함한 annotation을 말한다. (@Service, @Controller 등 여러개가 여기에 해당)

  • 그러면 @Component는 무엇인가? 해당 class가 bean임을 명시하는 annotation이다. 본인이 bean이라는 것이다. bean을 생성하는 method가 있다는 @Configuration과는 다르다.

참고로 @Configuration@ComponenScan을 쓸 수 있는 이유도 @Configuration@Component를 포함하고 있기 때문이다.

@Component
public class Car {

    @Autowired
    public Car(Engine engine, Transmission transmission) {
        this.engine = engine;
        this.transmission = transmission;
    }
}
  • 글에서는 위의 코드처럼 @Component가 달린 class, Car을 만들었다. 이건 Config class에서 지정된 @ComponentScan 덕분에 인식이 되고 bean으로 등록된다. @ComponentScan이 없었으면, @Configuration에 있는 bean들만 등록이 되고 Car에 해당하는 bean은 생성되지 않는다.

  • 그리고 사실 이번 글의 메인인 생성자 기반 주입은... 사실 이전 글에서 잠깐 다뤘는데 @Autowired를 대상 생성자 위에 두면 된다. 그러면 그 생성자를 기반으로 Spring에서 주입을 한다.

  • 그러면 실제 실행은 어떻게 하는가? main method를 보유한 class를 따로 만들어서 @Configuration이 달린 class를 AnnotationConfigApplicationContext의 parameter로 집어넣으면 된다. 그러면 위에서 언급한 일들이 이루어지고, Car에 해당하는 bean을 획득하는게 가능하다.

ApplicationContext context = new AnnotationConfigApplicationContext(Config.class);
Car car = context.getBean(Car.class);

3. Implicit Constructor Injection

  • 위의 Car이라는 @Component는 생성자가 한개인데, Spring 4.3에서는 이 경우 @Autowired 생략이 가능하다. Spring에서 알아서 하나만 있는 생성자를 기반으로 주입한다.

4. XML Based Configuration

  • xml로도 생성자 기반 주입을 하는 것이 가능하다...
<bean id="toyota" class="com.baeldung.constructordi.domain.Car">
    <constructor-arg index="0" ref="engine"/>
    <constructor-arg index="1" ref="transmission"/>
</bean>

<bean id="engine" class="com.baeldung.constructordi.domain.Engine">
    <constructor-arg index="0" value="v4"/>
    <constructor-arg index="1" value="2"/>
</bean>

<bean id="transmission" class="com.baeldung.constructordi.domain.Transmission">
    <constructor-arg value="sliding"/>
</bean>
  • 보면 toyota라는 Car에 해당하는 bean이 타 bean인 enginetransmission을 생성자 parameter로 받는 걸 볼 수 있다. 이 경우 ref를 사용해야 한다는 점 참고. 일반 값을 생성자가 받아야 하면 밑의 engine이랑 transmission처럼 value를 사용하면 된다.

  • index는 몇번째 parameter로 주어지는지를 나타내며, 위에 사용되진 않았자미나 Type으로 해당 parameter의 type을 표기하는 것도 가능하다. 또 debug flag를 assert 했으면 name을 사용하는 것도 가능.

  • 위의 xml을 기반으로 애플리케이션을 실행하고 싶으면 앞의 main method랑 거의 유사하나, ClassPathXmlApplicationContext를 사용해야 한다. 저 파일 이름이 baeldung.xml인 경우 밑과 같다.

ApplicationContext context = new ClassPathXmlApplicationContext("baeldung.xml");
Car car = context.getBean(Car.class);

5. Pros and Cons

  • 밑의 field 기반 injection과 비교하면서 설명하겠다.
public class UserService {
	@Autowired
    private UserRepository userRepository;
}

장점 1 : 테스트 용이성

  • UserService를 테스트하려고 한다 해보자. 그러면 관련 instance를 생성해야 한다. 그리고 본인이 의존중인 UserRepository의 instance인 userRepository 값이 뭔가라도 있어야 한다.

  • 생성자 기반 DI로 이를 구현했으면 그냥... 생성자에 mock object를 넣어서 저걸 생성하면 된다.

  • 그러나 위처럼 field로 구현한 경우 이게 많이 힘들다. 만일 setter용 method가 따로 없으면 직접 넣어야 하는데, 위처럼 private인 경우 이마저도 불가능하다. 그러면 Java reflection(...)을 써야 한다. 근데 이러면 애초에 해당 field를 encapsulate하려던 목적에 위배되고 많이 번거롭다.

장점 2 : class invariant

  • class 안의 특정 object가 어떤 메서드를 통해 특정 시기에서 특정 상태임이 보장되는게 class invariant이다.

  • 생성자 기반 DI의 경우 이게 가능하다. 생성자 호출 이후 UserServiceuserRepository에 있는 값이 null같은 쓰레기가 아닌 유효한 UserRepository임을 보장하기 때문이다.

  • 그러나 field기반 DI의 경우 이를 명확하게 보장하는 것이 불가능하다. 특히 위와 같은 경우에 말이다. class에 setter이 없으면 위와 같은 경우 그 어떤 메서드도 class invariant를 보장하지 않는다.

  • 이는 추후 프로그래머가 의도하지 않은, 예기치 않는 프로그램 동작을 일으킬 수 있다. 대표적인게 아까 말한 null값이 알고보니 저장된 상황이라 NullPointerException이 나온 경우

장점 3 : immutability와 OOP

  • 생성자 기반 DI이면 생성 이후, setter이 설정되지 않은 field가 변하는 것이 보장되지 않는다. 즉 변하도록 의도하지 않은 field가 실제로 영영 변하지 않는다는 것이다. OOP에서도 이 디자인 방식을 선호.

  • 하지만 field기반 DI이면 언제 initialize를 할지 모른다는 점 + 생성자 외의 모종의 방법으로 field 변동 방식을 제공해야 한다는 점 때문에 특정 field가 의도하지 않게 변하는걸 방지하기가 매우 힘들다.

단점 1? : verbosity

  • field기반은 field에다만 @Autowired를 넣으면 된다.

  • 그러나 생성자의 경우 field 정의 + 생성자에 @Autowired + 생성자에 parameter 넣어야 한다는 점 때문에 좀 더...복잡하다고 하나 사실 개인적으론 field에 @Autowired 넣는게 더 복잡한것 같다.

  • 설령 실제로 복잡한거더라도, 원래 의존관계가 많을수록 좋은 현상은 아니기 때문에 의존관계를 추가하는걸 고민하게 만들어서(...) 좋다고 볼 수도...?

profile
안 흔하고 싶은 개발자. 관심 분야 : 임베디드/컴퓨터 시스템 및 아키텍처/웹/AI

0개의 댓글