[스프링 기초] 컴포넌트 스캔과 의존관계 자동 주입

LTEN·2022년 8월 1일
0

※본 글은 김영한님의 '자바 스프링 완전정복 시리즈' 강의를 바탕으로 작성한 글입니다.

@Bean을 이용해서 수동으로 빈을 등록하고 조립할 수도 있지만, 관리하는 빈이 많아지면 일일이 관리하는 것이 꽤 귀찮을 것입니다.
그럴때 필요한 기능이 컴포넌트 스캔입니다.(@ComponentScan)

사용 방법

사용 방법은 간단합니다. 빈으로 등록할 클래스에는 @Component 어노테이션을 달아주고, 그것을 읽고 관리하는 클래스에는 @ComponentScan을 달아주면 됩니다.

사용법도 간단하기 때문에 간단한 코드로 사용 방법을 보겠습니다.

package hello.core.componentscan;

import org.springframework.stereotype.Component;

@Component
public class MemoryRepository implements Repository{
    void insert(){
        System.out.println("메모리에 저장");
    }
}
package hello.core.componentscan;

import org.springframework.context.annotation.ComponentScan;

@ComponentScan
public class AutoConfig {
}

MemoryRepository 클래스에 @Component 어노테이션을 추가해주고, AutoConfig 클래스에 @ComponentScan을 추가해줍니다.
AutoConfig 클래스 안에는 어떠한 코드도 없습니다.
그렇더라도 @Component가 붙은 class를 찾아 자동으로 빈으로 등록해줍니다.
(스캔하는 범위는 기본적으로 @ComponentScan이 붙은 클래스의 패키지입니다.)

의존관계 자동 주입

@Component로 등록하는 경우에 @Autowired를 이용하면 의존관계도 자동으로 주입해줍니다.

package hello.core.componentscan;

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

@Component
public class Service {
    private final Repository repository;

    @Autowired
    Service(Repository repository){
        this.repository = repository;
    }

    
}

Service 객체를 빈으로 등록하면서, @Autowired를 보고 Repository 타입이 일치하는 객체도 알아서 전달해줍니다.
이때 '타입이 일치하는 객체'는 '참조타입~참조타입을 상속한 클래스들'을 의미합니다.

참고로 생성자가 하나인 경우에는 @Autowired를 생략해도 됩니다.

생성자 주입을 이용하자

의존 관계를 자동하는 방법은 생성자 주입, 필드 주입, setter 주입, 일반 메서드 주입 총 4가지 방법이 있습니다.

결론부터 말하면 생성자 주입을 이용하면 됩니다.

필드주입의 문제점
필드 주입은 다음과 같이 필드에 @Autowired를 달아주는 방식입니다.

@Autowired
private final Repository repository;

매우 간단하지만 치명적인 문제가 있습니다.
-> DI 프레임워크가 없으면 문제가 발생합니다.
즉, 스프링없이 이 코드를 사용하면(ex) 단위 테스트)) repository를 자동 주입해주지 않아 null인 상태로 사용하게 될 것입니다.

setter 주입의 문제점
setter에 @Autowired를 추가하는 방법으로도 자동 주입이 가능합니다.
그러나 setter를 열어놔야하고, 이런 경우 수정 가능성을 열우두는 것이므로 유지보수에 좋지 않습니다.

일반 메서드 주입
아무 메서드에나 @Autowired를 붙여서 주입하도록 하는 것도 가능하지만, 일반적으로 사용하지 않습는다.

조회한 빈이 2개 이상이라면?

앞서 컴포넌트 스캔을 통해 의존 관계를 자동 주입할 때, 타입이 일치하는 범위는 '참조 타입과 참조 타입을 상속한 타입들'이라고 했습니다.

주입할 대상을 특정할 수 없으면 오류가 발생하는데,
그렇다면 다음과 같은 경우는 어떡해야 할까요?

package hello.core.componentscan;

import org.springframework.stereotype.Component;

@Component
public class MemoryRepository implements Repository{
    @Override
    public void insert(){
        System.out.println("메모리에 저장");
    }
}
package hello.core.componentscan;

import org.springframework.stereotype.Component;

@Component
public class DBRepository implements Repository{
    @Override
    public void insert() {
        System.out.println("DB에 추가");
    }
}

현재 Repository의 구현체가 2개 존재하고, 대부분 DBRepsitory를 쓰지만 특수한 경우에는 MemoryRepsitory도 사용하는 경우가 있어, 둘 모두 빈으로 등록해야 하는 상황입니다.

package hello.core.componentscan;

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

@Component
public class Service {
    private final Repository repository;

    @Autowired
    Service(Repository repository){
        this.repository = repository;
    }


}

서비스에서는 DBRepository를 주입 받으면 되지만, 그렇다고 위 코드를 구체 클래스에 의존하도록 수정하게 되면 DIP를 위반하게 됩니다.

이러한 문제를 해결하는 방법은 크게 3가지가 존재합니다.

1. 필드이름을 통한 선택

@Component
public class Service {
    private final Repository repository;

    @Autowired
    Service(Repository dBrepository){
        this.repository = dBrepository;
    }


}

스프링은 처음으로 타입을 통해 타입이 맞는 빈을 조회하고, 이때 하나를 특정할 수 없다면, 필드 이름, 파라미터 이름을 통해 매칭을 시도합니다.
따라서 위와 같은 방식으로 DBMemoryRepository가 주입되도록 할 수 있습니다.

※ 단순히 변수명을 설정하는 수준이므로 DIP를 위반한다고 보기는 어렵다고 합니다. '변수명을 통해 우선순위를 설정하는 정도.'라고 생각하면 될 것 같습니다.

2. @Qualifier
말그대로 'Qualifier' 통해 이름 외에 구분할 수 있는 새로운 표시를 해두는 것입니다.
코드를 보면 어렵지 않게 이해할 수 있습니다.

package hello.core.componentscan;

import org.springframework.stereotype.Component;

@Component
@Qualifier("sub")
public class MemoryRepository implements Repository{
    @Override
    public void insert(){
        System.out.println("메모리에 저장");
    }
}
package hello.core.componentscan;

import org.springframework.stereotype.Component;

@Component
@Qualifier("main) 
public class DBRepository implements Repository{
    @Override
    public void insert() {
        System.out.println("DB에 추가");
    }
}
@Component
public class Service {
    private final Repository repository;

    @Autowired
    Service(@Qualifier("main") Repository repository){
        this.repository = repository;
    }
}

Qualifer 어노테이션을 통해 등록한 일종의 표시로, 중복 시 구분할 수 있는 새로운 방법을 추가하는 것입니다.
빈의 이름을 바꾸는 등의 기능은 아닙니다.

3. @Primary
가장 간편한 방법입니다.

여러 개가 조회되더라도 @Primary가 붙은 쪽을 우선으로 매칭하게 됩니다.

정리
세부적으로 설정이 필요한 경우 Qualifier를 이용하고, 간단하게 설정이 가능한 경우 @Primary만으로 처리하는 방법을 권장한다고 합니다.

자동 등록, 주입 vs 수동 등록, 주입

자동 등록, 주입과 수동 등록, 주입이 존재하는데, 그렇다면 어떤 것을 쓰는게 맞을까요?
결론적으로 다음과 같은 가이드라인을 따르면 됩니다.

(1) 기본적으로는 자동 등록을 이용
편리한 기능은 적극 활용하는 것이 좋습니다.
편리하고 기술적으로도 별다른 차이가 없습니다.

(2) 기술 지원, 다형성 적극 활용 - 두 경우에만 수동 등록

  • 기술 지원이란 AOP 등을 처리할 떄 주로 사용되는 기술로, 그 수가 적고 앱 전반에 걸쳐 공통으로 적용되는 것이므로 가장 상위 패키지에서 수동으로 등록해주면 확인하는 입장에서 편리합니다.
  • 다형성을 적극 활용하는 예시로 앞서 다룬 DBRepository와 MemoryRepsitory를 들 수 있습니다.
    Repository 종류들을 따로 수동 등록해주면, 'Repository로 현재 이런 옵션들이 존재하는구나' 같은 내용을 한눈에 파악하기 좋기 때문에 이런 경우에는 별도의 Config 파일을 생성해서 수동 등록 해주는 것이 더 좋은 방법일 수 있습니다.
profile
백엔드

0개의 댓글