Spring Framework

[Spring Core] Spring DI의 세 가지 방식(setter injection, constructor injection, field injection)

kgvovc 2021. 4. 3. 15:03
반응형

의존성 주입

다음은 의존성 주입에 대해서 알아보자. 총 세 가지 의존성 주입 방법을 사용할 수 있다.

 

  • 설정자 기반 의존성 주입 방식(setter-based dependency injection)
  • 생성자 기반 의존성 주입 방식(constructor-based dependency injection)
  • 필드 기반 의존성 주입 방식(field-based injection)

 

 

 

[설정자 기반 의존성 주입 방식]

설정자 기반의 의존성 주입 방식은 설정자 메서드의 인수를 통해 의존성을 주입하는 방식이다. 편의상 설정자 기반 의존성 주입 방식을 세터 인젝션이라고 부르자.

 

  • UserServiceImpl에 설정자 메서드 구현
public class UserServiceImpl implements UserService {
	private UserRepository userRepository;
    private PasswordEncoder passwordEncoder;
    
    // 기본 생성자(생략 가능)
    public UserServiceImpl() {
        
    }
    
    public void setUserRepository(UserRepository userRepository) {
       this.userRepository = userRepository;
    }
    
    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }
    
    // 생략
}

 

 

이와 같이 설정자 메서드가 만들어졌다면 이제 의존성 주입을 해보자. 우선 세터 인젝션자바 기반 설정 방식으로 표현한 예다.

 

  • 세터 인젝션을 자바 기반 설정 방식으로 표현한 예
@Bean
UserService userService() {
    UserServiceImpl userService = new UserServiceImpl();
    userService.setUserRepository(userRepository());
    userService.setPasswordEncoder(passwordEncoder());
    return userService;
}

 

설정한 내용을 살펴보면 설정자 메서드에 다른 컴포넌트의 참조 결과를 설정했을 뿐이다. 이와 조금 다른 방식으로 다음과 같이 @Bean 애너테이션을 붙인 메서드에 매개변수 형태로 의존 컴포넌트를 받게한 후, 그 값을 설정자 메서드를 통해 주입시켜도 된다.

 

 

  • 매개변수를 활용해 세터 인젝션을 자바 기반 설정 방식으로 표현한 예
@Bean
UserService userService(UserRepository userRepository, PasswordEncoder passwordEncoder) {
    UserServiceImpl userService = new UserServiceImpl();
    userService.setUserRepository(userRepository);
    userService.setPasswordEncoder(passwordEncoder);
    return userService;
}

 

이처럼 자바 기반 설정 방식으로 세터 인젝션을 하는 경우, 마치 프로그램에서 인스턴스를 직접 생성하는 코드처럼 보이기 때문에 과연 이것이 빈을 정의한 설정인지 체감이 안될 수 있다.

 

 

 

 

 

다음은 세터 인젝션XML 기반 설정 방식으로 표현한 예다.

 

  • 세터 인젝션을 XML 기반 설정 방식으로 표현한 예
<bean id="userService" class="com.example.demo.UserServiceImpl">
	<property name="userRepository" ref="userRepository" />
    <property name="passwordEncoder" ref="passwordEncoder" />
</bean>

 

XML 기반 설정 방식에서 세터 인젝션을 할 때는 주입할 대상을 <property> 요소에 기술하는데, <property> 요소의 name 속성에 주입할 대상의 이름을 지정하면 된다. 여기서 말하는 프로퍼티는 자바빈즈(JavaBeans)의 프로퍼티에 해당하기 때문에 자바빈즈의 관례에 따라 프로퍼티의 이름과 메서드의 이름을 정하게 된다. 예를 들어, 프로퍼티의 이름이 xyz라고 한다면 설정자와 접근자 메서드의 이름은 setXyz, getXyz가 된다.

 

 

 

 

 

 

다음은 세터 인젝션애너테이션 기반 설정 방식으로 표현한 예다.

 

  • 세터 인젝션을 애너테이션 기반 설정 방식으로 표현한 예
@Component
public class UserServiceImpl implements UserService {
	private UserRepository userRepository;
	private PasswordEncoder passwordEncoder;
    
    @Autowired
    public void setUserRepository(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
    
    @Autowired
    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }
}

 

이처럼 설정자 메서드@Autowired 애너테이션을 달아주기만 하면 된다. 애너테이션 기반의 설정 방식을 이용하면 자바 기반 설정 방식이나 XML 기반 설정 방식과 같이 별도의 설정 파일을 둘 필요가 없다.

 

 

 

[생성자 기반 의존성 주입 방식]

생성자 기반 의존성 주입 방식생성자의 인수를 사용해 의존성을 주입하는 방식이다. 편의상 생성자 기반 의존성 주입 방식을 컨스트럭터 인젝션이라고 부르기로 한다.

 

자바 기반 설정 방식에서는 생성자에 의존 컴포넌트를 직접 설정하고, XML 기반 설정 방식에서는 <constructor-arg> 요소에서 참조하는 컴포넌트를 설정한다. 그리고 애너테이션 기반 설정 방식에서는 생성자에 @Autowired를 부여한다.

 

 

  • 컨스트럭터 인젝션을 자바 기반 설정 방식으로 표현한 예
@Configuration
public class AppConfig {
    @Bean  
    UserRepository userRepository() {
        return new UserRepositoryImpl();
    }
    
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    UserService userService() {
        return new UserServiceImpl(userRepository(), passwordEncoder()); 
    }
}

 

 

 

 

  • 컨스트럭터 인젝션을 XML 기반 설정 방식으로 표현한 예(인덱스 사용)

생성자가 여러 개의 인자를 필요로 하는 경우 <constructor-arg>를 여러 번 정의하면 된다. 단, 이때 인수의 순서에 주의해야 하는데, 다음과 같이 index 속성을 활용하면 생성자 인수의 순서를 명시적으로 지정할 수 있다.

 

<bean id="userService" class="com.example.demo.UserServiceImpl">
	<constructor-arg index="0" ref="userRepository" />
    <constructor-arg index="1" ref="passwordEncoder" />
</bean>

 

이렇게 index 속성을 사용하면 가독성이 좋아지고 생성자의 개수가 늘거나 줄어들 때 발생할 수 있는 실수를 쉽게 발견할 수 있다는 장점이 있다.

 

 

다른 방법으로는 name 속성에 인수명을 지정할 수도 있다.

 

  • 컨스트럭터 인젝션을 XML 기반 설정 방식으로 표현한 예(인수명을 사용)
<bean id="userService" class="com.example.demo.UserServiceImpl">
	<constructor-arg name="userRepository" ref="userRepository" />
    <constructor-arg name="passwordEncoder" ref="passwordEncoder" />
</bean>

 

name 속성을 활용하면 인수의 순서가 바뀌거나 추가될 때도 인덱스 순서를 매번 변경하지 않아도 되는 장점이 있다. 다만 인수명 정보는 소스코드가 컴파일되는 과정에서 없어지기 때문에 컴파일할 때 javac 명령과 함께 디버깅 정보를 전달할 수 있는 -g 옵션을 사용하거나, JDK 8 이후부터는 메서드 매개변수의 메타 정보를 생성할 수 있는 -parameters 옵션을 사용해야 한다. 만약 이렇게 별도의 컴파일 옵션을 주는 것이 번거롭다면 다음과 같이 @ConstructorProperties 애너테이션(java.beans.ConstructorProperties)을 달아주는 방법도 있다.

 

 

 

 

  • 컨스트럭터 인젝션을 애너테이션 기반 설정으로 표현한 예
@ConstructorProperties({"userRepository", "passwordEncoder"})
public UserServiceImpl(UserRepository userRepository, PasswordEncoder passwordEncoder) {
    // 생략
}

 

컨스트럭터 인젝션을 사용하면 필드를 final로 선언해서 생성 후에 변경되지 않게 만들 수 있다. 이렇게 필드를 변경하지 못하도록 엄격하게 제한을 거는 것은 다른 의존성 주입 방법으로는 처리하지 못하고 오직 컨스트럭터 인젝션에서만 할 수 있다.

 

 

 

 

[필드 기반 의존성 주입 방식]

필드 기반 의존성 주입 방식은 생성자나 설정자 메서드를 쓰지 않고 DI 컨테이너의 힘을 빌려 의존성을 주입하는 방식이다. 편의상 필드 기반 의존성 주입 방식을 필드 인젝션이라고 부른다.

 

필드 인젝션을 할 때는 의존성을 주입하고 싶은 필드에 @Autowired 애너테이션을 달아주면 된다. Autowiring에 대해서는 뒤에서 자세히 설명한다. 이처럼 필드 기반 의존성 주입 방식을 사용하면 생성자나 설정자 메서드를 굳이 만들 필요가 없어지기 때문에 생성자와 설정자 메서드 작성을 생략해서 소스코드가 비교적 간결해 보이는 장점이 있다.

 

  • 필드 인젝션을 애너테이션 기반 설정으로 표현한 예
@Component
public class UserServiceImpl implements UserService {
    @Autowired
    UserRepository userRepository;
    
    @Autowired
    PasswordEncoder passwordEncoder;
    
    // 생략
}

 

필드 인젝션을 사용할 때는 한 가지 주의할 점이 있다. 그것은 소스코드의 양을 줄이기 위해 생성자나 설정자 메서드를 생략하고 싶다면 반드시 DI 컨테이너를 사용해야 한다는 것을 전제해야 한다는 것이다. 예를 들어, DI 컨테이너 없이 사용되는 독립형 라이브러리로 사용될 소스코드에서 필드 인젝션을 사용하고 있다면 잘못된 것이라고 판단해야 한다.

반응형