강의/Java Spring Boot

Spring Framework 의존성 주입의 다양한 유형

studylida 2024. 7. 25. 21:00

의존성 주입에 대해 설명하기 전에, 지금까지 배운 것들을 복습해보자.

@Component 어노테이션은 스프링이 어떤 클래스를 관리해야할 대상인지 아닌지 알 수 있게 해주는 어노테이션이다.

이러한 컴포넌트들을 탐색할 범위를 정하기 위해 @ComponentScan 어노테이션을 사용했다.

이렇게 코드를 작성하고, 어플리케이션을 실행하면 Spring은 가장 먼저 컴포넌트 스캔을 수행해서 찾은 컴포넌트의 의존성을 확인하고, 이에 따라 모두 와이어링 한다. 이 과정을 의존성 주입이라고 한다.

Constructor-based: Dependencies are set by creating the Bean using its Constructor(recommended)
Setter-based: Dependencies are set by calling setter methods on your beans
Field: No setter or constructor. Depencency is injected using reflection.

의존성 주입에는 세 가지 유형이 있다

일단 이걸 배우기 위해 사용할 기반 클래스를 하나 만들어보자. 클래스의 이름은 DepInjectionLauncherApplication으로 하자.

예시를 들기 위해 하는 거니까 com.in28minutes.learnspringframework.examples.a1 패키지에 넣어두자. 이건 막 중요한 거 아니다. 구분을 위해 해주는 거니까 대충 할 거면 대충 해도 된다.

이전 시간에 ComponentScan에 대해서 배웠는데, 어디를 검사할지 지정하지 않는다면 현재 패키지에서 컴포넌트 스캔을 수행한다.

package com.in28minutes.learnspringframework.examples.a1;

import....

@Configuration
@ComponentScan
public class DepInjectionLauncherApplication {

    public static void main(String[] args) {

        try(var context = new AnnotationConfigApplicationContext(DepInjectionLauncherApplication.class)) {

        }

    }
}

이제 의존성 주입 예시로 쓰일 클래스를 만들어보자. Dependency1과 Dependency2를 YourBusinessClass에 주입할 것이다.

@Component
class YourBusinessClass {

}

@Component
class Dependency1 {

}

@Component
class Dependency2 {

}

그리고 DepInjectionLauncherApplication를 실행하면 위의 세 클래스가 생성된다.

이는 우리가 Spring Context를 실행하고 있기 때문이다.

일단 의존성을 주입하기 위해 가장 먼저 해주어야 할 것은 YourBusinessClass의 멤버변수로 Dep1, Dep2를 추가하는 것이다.

이렇게 추가한 뒤 잘 되었는지 적절히 코드를 작성해서 확인해보자.

package com.in28minutes.learn_spring_framework.example.a1;

import....

@Component
class YourBusinessClass {

    Dependency1 dependency1;
    Dependency2 dependency2;

    public String toString() {
        return "Using " + dependency1 + " and " + dependency2;
    }
}

@Component
class Dependency1 {

}

@Component
class Dependency2 {

}

@Configuration
@ComponentScan//("com.in28minutes.learn_spring_framework.example.a1") // Spring needs to know where to find the Component.
public class DepInjectionLauncherApplication {

    public static void main(String[] args) {

        try(var context =
                    new AnnotationConfigApplicationContext
                            (DepInjectionLauncherApplication.class)) {

            Arrays.stream(context.getBeanDefinitionNames())
                    .forEach(System.out::println);

            System.out.println(context.getBean(YourBusinessClass.class));
        }
    }
}

실행하면 Using null and null이 출력된다. Spring 프레임워크가 자동 와이어링을 수행하지 않은 것이다.

자동 와이어링을 위해서는 저번 시간에 보았던 @Autowired를 추가해야 한다.

@Component
class YourBusinessClass {

    @Autowired
    Dependency1 dependency1;

    @Autowired
    Dependency2 dependency2;

    public String toString() {
        return "Using " + dependency1 + " and " + dependency2;
    }
}

이렇게 멤버변수에 @Autowired 어노테이션을 사용하는 것을 필드 주입이라고 한다. 이 방식은 생성자나 세터를 사용하지 않고 리플렉션을 사용하여 의존성을 주입한다.

다음은 세터를 사용한 방식이다.

    // Setter Injection
    Dependency1 dependency1;
    Dependency2 dependency2;

    @Autowired
    public void setDependency1(Dependency1 dependency1) {
        System.out.println("Setter Injection - setDependency1");
        this.dependency1 = dependency1;
    }

    @Autowired
    public void setDependency2(Dependency2 dependency2) {
        System.out.println("Setter Injection - setDependency2");
        this.dependency2 = dependency2;
    }

다음은 생성자를 사용한 방식이다.

    // Constructor Injection
    // Spring team recommends Constructor-based injection as dependencies are automatically set when an object is created!
    Dependency1 dependency1;
    Dependency2 dependency2;

    // @Autowired // Autowired is not required
    public YourBusinessClass(Dependency1 dependency1, Dependency2 dependency2) {
        System.out.println("Constructor Injection - YourBusinessClass");
        this.dependency1 = dependency1;
        this.dependency2 = dependency2;
    }

    public String toString() {
        return "Using " + dependency1 + " and " + dependency2;
    }

보통은 생성자를 사용하여 의존성 주입을 하는 것이 추천된다. 이유는, 생성자 주입을 할 때는 Autowired가 의무가 아니라는 점이다. @Autowired를 추가하지 않아도 Spring이 자동으로 생성자를 사용해서 객체를 만든다.

또한 생성자 주입을 사용하면 모든 필수 의존성이 생성자를 통해 주입되므로, 객체 생성과 동시에 초기화가 완료된다. 반면에 필드 주입이나 세터 주입은 의존성 주입이 분산되어있어 초기화 시점이 명확하지 않다는 단점이 있다.

마지막으로는 생성자 주입을 사용할 때는 런타임에 객체 불변성을 보장한다는 장점이 있다. 다른 주입 방법들은 객체의 생성 이후에 호출되므로 final 키워드를 사용할 수 없다.

이렇게 의존성 주입을 하는 세 가지 방법에 대해 알아보았다. 이와 관련하여 IOC(Inversion Of Control), 제어반전이라 부르기도 한다. 이전에는 프로그래머가 객체를 생성, 관리했지만, 스프링을 이용함에 따라 프로그래머, 또는 명시적인 코드에서 Spring으로 제어권이 넘어갔다고 볼 수 있기 때문이다.

아래는 의존성 주입을 이용한 코드 작성의 예시다.

package com.in28minutes.learn_spring_framework.exercise.section3;

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

import java.util.Arrays;

@Configuration
@ComponentScan
public class ExerciseLauncherApplication {
    public static void main(String[] args) {
        try (var context =
                     new AnnotationConfigApplicationContext
                             (ExerciseLauncherApplication.class)) {

            Arrays.stream(context.getBeanDefinitionNames())
                    .forEach(System.out::println);

            System.out.println(context.getBean(BusinessCalculationService.class));

            System.out.println(context.getBean(BusinessCalculationService.class).findMax());
        }
    }
}
package com.in28minutes.learn_spring_framework.exercise.section3;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import java.util.Arrays;

@Component
public class BusinessCalculationService {
    private DataService dataService;

    public BusinessCalculationService(@Qualifier("mySQLDataService") DataService dataService) {
        //super
        this.dataService = dataService;
    }

    public int findMax() {
        return Arrays.stream(dataService.retrieveData()).max().orElse(0);
    }
}
package com.in28minutes.learn_spring_framework.exercise.section3;

import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Component
@Primary
public class MongoDBDataService implements DataService {
    public int[] retrieveData() {
        return new int[] {11,22,33,44,55};
    }
}
package com.in28minutes.learn_spring_framework.exercise.section3;

import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;

@Component
@Qualifier("mySQLDataService")
public class MySQLDataService implements DataService {
    public int[] retrieveData() {
        return new int[] {1,2,3,4,5};
    }
}
package com.in28minutes.learn_spring_framework.exercise.section3;

public interface DataService {
    int[] retrieveData();
}

finish.