들어가며

  • 이전 포스트까지 의존관계 자동 주입에 대해 스프링 프레임워크가 어떤 방식으로 수행하는지에 대해 알아보았다.
  • 이번 포스트에는 전체적인 정리와 동일한 타입의 여러 빈이 모두 필요할 때, 그리고 자동 주입과 수동 주입에 대한 기준점에 대해 알아본다.

 

 

 

의존관계 주입 방식과 자동 의존관계 주입

의존관계 주입 방식

  • 의존관계 주입 방식은 생성자 주입, 수정자 주입, 필드 주입, 일반 메서드 주입의 4가지로 나뉘었다.
  • 생성자 주입은 생성자를 이용하는 방식이었고, 수정자는 Setter를 이용하여 주입하는 자바 빈 프로퍼티 접근 방식을 차용하였다. 필드 주입은 말 그대로 변수에 그대로 주입을 수행하는 방식이었고, 메서드 방식은 생성자와 동일하나 일반 메서드라는 점이 차이점이었다.
  • 여기서 가장 안정적인 주입 방식은 생성자 주입(Constructor Injection) 이었고, 수정자, 필드 주입은 각각 문제점과 그 한계가 명확하여 현재는 특수한 경우를 제외하고는 쓰이지 않는 방식으로 알려져 있다.
    • 수정자 주입의 경우에는 public으로 열려 있어 변경에 취약하단 점이 문제였다. 그래서 런타임 도중 한번은 변경되어야 하는 상황에서 사용된다고도 했었다.
    • 필드 주입은 스프링 DI에 의존적인 방식이고, 그 이유 때문에 정상적으로 테스트 코드를 사용한 테스트가 불가능하였다. 이를 극복하기 위해서는 Setter를 사용했어야 했고 그 방식은 기존의 Setter 주입과 차이가 없다.
    • 대신 그 편리성을 이용하여 테스트 코드에서 스프링 DI가 필요할 때 간략하게 쓰이는 정도로 사용되고 있다.

자동 의존관계 주입

  • 자동 의존관계 주입을 위해서는 @Autowired라는 어노테이션이 필요하다.
  • 생성자 주입 방식을 자동화해주는 코드는 다음과 같다.
@Controller
public class MyController{
    private final MyRepository myRepository;
    
    @Autowired
    MyController(MyRepository myRepository){
        this.myRepository = myRepository;
    }
    
    ...
}
  • Setter의 경우 setMyRepository() 메서드에, 필드 주입의 경우 필드 위에 @Autowired를 사용함으로서 자동 의존관계 주입이 완료된다.
  • 이때 생성자가 단 하나인 것이 보장되어 있는 경우, @Autowired를 생략할 수 있다.
  • 추가적으로 Lombok 라이브러리를 사용하는 경우, @RequiredArgsConstructor를 사용하여 final로 무조건 초기화를 시켜야 하는 변수들만 사용한 생성자를 자동으로 구성할 수 있다.
  • 위의 두 최적화 요소를 적용한 코드는 다음과 같다.
@Controller
@RequireArgsConstructor
public class MyController{
    private final MyRepository myRepository;
    ...
}

 

 

 

조회된 스프링 빈이 2개 이상일 때의 문제점과 해결책

  • @Autowired의 기본적인 빈 탐색 방식은 타입 일치를 이용한 탐색 방식이었다. 즉 인터페이스 타입을 포함한 그 자식 타입들을 모두 조회한다.
  • 이 과정에서 조회된 스프링 빈이 2개 이상이 될 경우 찾은 스프링 빈이 유일하지 않다는 예외가 던져진다.
  • 이 문제를 해결하는 동시에 DIP, OCP를 위배하지 않도록 하기 위해 @Qualifier와 @Primary를 사용한다고 했다.

@Qualifier

  • @Qualifier는 스프링 빈 객체를 식별할 수 있도록 해주는 추가 구분자 역할을 한다. 해당 어노테이션은 @Qualifier("identifier")로 객체를 구분 가능하게 해주나 해당 스프링 빈의 이름을 저렇게 바꾸지는 않는다는것을 유의해야 한다.
  • @Qualifier를 사용한 자동 주입 코드는 다음과 같다.
@Controller
public class MyController{
    private final MyRepository myRepository;
    
    @Autowired
    MyController(
        @Qualifier("memoryRepository") MyRepository myRepository
    ){
        this.myRepository = myRepository;
    }
    
    ...
}

// MemoryRepository

@Qualifier("memoryRepository")
public class MemoryRepository implements myRepository{
    ...
}

// JdbcRepository

@Qualifier("jdbcRepository")
public class JdbcRepository implements myRepository{
    ...
}

@Primary

  • @Primary는 우선순위를 지정해줄 수 있는 어노테이션이다. 해당 어노테이션을 붙여 놓으면 조회된 빈이 2개여도 @Primary가 붙은 스프링 빈이 우선적으로 주입된다.
  • 이는 주로 사용되는 메인 스프링 빈을 @Primary로 사용하고, 덜 사용되는 서브 스프링 빈을 사용해야 할 경우 @Qualifier를 사용하여 주입하는 방식으로 구현이 가능할 것이다.

 

 

 

조회한 동일 타입의 빈이 모두 필요할 때

  • 의도적으로 해당 빈들이 모두 필요할 때가 존재할 수 있다, 예를 들어 할인 서비스 등을 제공하는데 그 할인 서비스의 종류가 여러가지라 그 중 하나를 선택해야 하는 경우가 존재할 수 있다.
  • 동일 타입의 빈이 모두 필요할 때 선택하는 코드는 다음과 같이 구현할 수 있다.
// AutoAppConfig.java

@Configuration
@ComponentScan
public class AutoAppConfig{

}


// RateDiscountPolicy.java

@Component
public class RateDiscountPolicy implements DiscountPolicy{
    ...
}


// FixDiscountPolicy.java

@Component
public class FixDiscountPolicy implements DiscountPolicy{
    ...
}


// DiscountService.java

@Service
public class DiscountService{
    private final Map<String, DiscountPolicy> policyMap;
    private final List<DiscountPolicy> policies;
    
    @Autowired
    DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies){
        this.policyMap = policyMap;
        this.policies = policies;
    }
    
    public int discount(Member member, int price, String discountCode){
        DiscountPolicy discountPolicy = policyMap.get(discountCode);
        
        return discountPolicy.discount(member, price); // 할인된 가격을 반환
    }
}


// Main.java
public class Main{
    public static void main(String[] args){
        ApplicationContext ac = 
            new AnnotationConfigApplicationContext(AutoAppConfig.class);
        
        DiscountService discountService = ac.getBean(DiscountService.class);
        
        int discountPrice = discountService.discount(member, 1000, "fixDiscountPolicy");
        
        System.out.println(discountPrice);
    }
}
  • 우리가 주목해야하는 코드는 DiscountService.java 부분이다.
@Service
public class DiscountService{
    private final Map<String, DiscountPolicy> policyMap;
    private final List<DiscountPolicy> policies;
    
    @Autowired
    DiscountService(Map<String, DiscountPolicy> policyMap, List<DiscountPolicy> policies){
        this.policyMap = policyMap;
        this.policies = policies;
    }
    
    public int discount(Member member, int price, String discountCode){
        DiscountPolicy discountPolicy = policyMap.get(discountCode);
        
        return discountPolicy.discount(member, price); // 할인된 가격을 반환
    }
}
  • 보면 생성자 주입으로 DiscountPolicy 타입의 모든 스프링 빈을 policyMap인 Map 형식으로 주입받고 있다. 위의 코드에서는 fixDiscountPolicy와 rateDiscountPolicy가 주입된다.
  • 이후 discount 메서드에서 입력받은 discountCode를 policyMap의 Key로 활용하여 해당 스프링 빈을 가져오고, 해당 스프링 빈의 discount 메서드를 이용하여 할인된 가격을 반환한다.
  • 이렇게 구현하면 discountPolicy의 다형성을 적극적으로 활용하면서 DIP, OCP를 만족시킬 수 있다. 만약 DiscountPolicy 타입의 새로운 구현체가 들어온다 하더라도 서비스 코드에서는 어떠한 변경도 일어나지 않는 것을 확인할 수 있다.

 

 

 

자동과 수동의 운영 기준

  • 지금까지 이전 포스트에서 배웠던 수동 빈 등록과 수동 의존관계 주입, 자동 빈 등록(컴포넌트 스캔)과 자동 의존관계 주입(@Autowired)에 대해 배웠다.
  • 그렇다면 어떨 때 수동을 사용해야 하고, 어떨 때 자동을 사용해야 하는지에 대한 기준점도 알아두는것이 좋을 것이다.
  • 스프링 프레임워크는 일반적인 어플리케이션 로직을 자동적으로 스캔하여 빈으로 등록시키도록 지원하고, 스프링 부트로 넘어오면서 컴포넌트 스캔을 기본적으로 사용한다.(@SpringBootApplication 내부에 @ComponentScan이 들어가있다.)
  • 몇몇 어노테이션들과 조건만 만족해도 스프링 프레임워크는 해당 클래스들을 자동으로 컴포넌트 스캔을 통해 빈으로 등록하고 관리해준다. 우리가 굳이 @Configuration을 통해 수동으로 빈을 등록하고, 수동으로 DI를 수행해줄 필요가 많이 없다는 이야기이다.
  • 따라서 기본적으로는 자동 기능들을 이용하나 몇몇 부분에서는 수동 방식을 사용하는 것이 더 좋을 수 있다.
    • 먼저 AOP, 또는 기술적 문제를 처리하는 스프링 빈들을 등록할 때이다. AOP의 경우 이후에 다루겠지만 조건에 맞는(관심사가 맞는) 모든 부분에 영향력을 행사하도록 설계하는 것이기 때문에 앱 전반에 걸쳐 매우 큰 영향력을 준다.
    • 또한 적용되고 있는지 아닌지조차도 명확히 파악하기가 힘들기 때문에 직접 우리가 체크해주어야 한다. 이런 경우에 수동적 방식으로 등록하여 관리해야 명확하게 관리가 가능하다.
    • 그 다음으로 우리가 위의 동일한 타입의 여러 스프링 빈이 필요한 상황처럼 비즈니스 로직 중에서 객체의 다형성을 적극적으로 사용해야 할 때이다.
      • 지금은 우리가 직접 코드를 치면서 구현했으니 상관없지만 타인이 해당 코드를 아무 정보 없이 보게 된다면 policyMap에 어떤 스프링 빈들이 등록되는지에 대해 모를 수 있다. 이 경우, @Configuration을 통한 수동 빈 등록이 있다면 해당 클래스파일을 통해 바로 알 수 있다.
      • 하지만 자동으로 등록할 시에 여러 파일들을 뒤지면서 어떤 스프링 빈들이 들어가는지를 일일히 찾아보는 번거로움이 존재한다. - 이를 막기 위해서는 수동으로 등록하거나, 자동으로 할 시 다형성을 사용하는 객체 인터페이스의 구현체들을 따로 한 패키지에 모아서 관리하는 것이 좋다.
복사했습니다!