[Spring] 의존관계 자동 주입

imcool2551·2022년 1월 27일
0

Spring

목록 보기
5/15
post-thumbnail

본 글은 인프런 김영한님의 스프링 완전 정복 로드맵을 기반으로 정리했습니다.

의존관계 자동 주입


컴포넌트 스캔을 통해 빈을 등록하면 설정파일에서 의존관계를 주입할 수 없기 때문에 의존 관계를 주입해줄 다른 방법이 필요하다. 이 때 사용되는 것이 의존관계 자동 주입 @Autowired 이다.

@Autowired 의존 관계 자동 주입은 꼭 컴포넌트 스캔과 함께 사용해야만 하는 것은 아니다. 수동으로 빈을 등록할 때도 의존관계 자동 주입을 사용할 수 있다.

과거에는 의존관계 자동 주입에대한 호불호가 있었지만, 최근에는 스프링 부트와 함께 컴포넌트 스캔과 의존관계 자동 주입을 적극 사용하는 추세다.

의존관계 자동 주입의 종류


의존관계 자동 주입을 사용하는 방법은 간단하다. 의존을 주입할 대상에 @Autowired 애노테이션을 붙이면 된다. @Autowired를 통해 의존관계 자동 주입을 적용할 수 있는 방법은 4개다.

1. 생성자 주입

@Service
public class OrderService {
    private final MemberRepository memberRepository;

    @Autowired
    public OrderService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

생성자는 객체 생성 시점에 단 한번만 호출된다. 자바에서 생성자에서 초기화 하는 필드에는 final 키워드를 붙일 수 있다. 아래에서 다룰 다른 방법들은 생성자 호출 이후에 실행되므로 final 키워드를 붙일 수 없다.

final 키워드를 붙임으로써 얻을 수 있는 이점이 두 가지 있다.

  • final 키워드를 붙이고 초기화해주지 않으면 컴파일 에러가 난다. 컴파일 단계에서 의존객체를 초기화하지 않는 실수를 감지해서 런타임에서 NPE가 터지는 일을 방지할 수 있다. 즉, 필수 의존 관계임이 보장된다.

  • final 변수는 한 번만 초기화 할 수 있다. 즉, 불변성이 보장된다.

대부분의 의존 객체는 필수값이고 바꿀 일이 없기 때문에 가장 많이 쓰이는 방법이다.

스프링 4.3버전부터 생성자가 1개면 @Autowired 키워드를 생략해도 자동으로 의존관계를 주입해준다. 생성자가 2개 이상이면 생략하면 안 된다. 컨테이너가 어떤 생성자를 통해 의존 관계를 주입해야할 지 모르기 때문이다.

롬복 라이브러리를 쓰면 코드를 더 줄일 수 있다. @RequiredArgsConstructorfinal이붙은 필드를 매개변수로 받는 생성자를 자동으로 만들어준다. 아래 코드는 위와 동일하다.

@Service
@RequiredArgsConstructor
public class OrderService {
    private final MemberRepository memberRepository;
}
  • 정리: 불변, 필수 의존관계에 사용하자

2. 세터(setter) 주입

@Service
public class OrderService {
    private MemberRepository memberRepository;

    @Autowired
    public setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

세터를 통해 의존관계를 주입받는 방법이다. 빈이 컨테이너에 등록될 때 세터가 호출되서 의존관계가 주입된다. 세터를 정의 했기 때문에 의존 객체를 변경할 수 있다. 의존 객체를 변경할 일이 있다면 세터를 통해 주입받도록 하자.

@Autowired를 사용하면 주입할 타입의 빈이 컨테이너에 없다면 오류가 난다. 주입할 대상이 필수 의존관계가 아니라면 @Autowired(required=false)로 지정하면 된다. 즉, 선택적 의존관계에 사용할 수 있다. 선택적 의존관계를 처리하는 방법은 두 가지가 더 있는데 이는 밑에서 다루도록 하겠다.

주의할 점이 있다. 컴포넌트 스캔을 사용하지 않고 수동으로 빈을 등록하면서 세터를 사용할 수 있다. 이 때, 세터로 주입한 객체와 @Autowired가 주입해준 객체가 다르면 세터를 통해 수동으로 주입한 객체가 무시된다. 이는 찾기 어려운 버그가 될 가능성이 있다. 의존관계 자동 주입을 사용했다면 자동 주입이 어려운 경우를 제외하고 일관되게 자동 주입을 사용하도록 하자.

  • 정리: 가변, 선택적 의존관계에 사용하자

3. 필드 주입

@Service
public class OrderService {
    @Autowired
    private MemberRepository memberRepository;
}

필드에 바로 주입받는 방식이다. 코드는 간단하지만 의존 객체를 변경할 수 없다는 치명적인 단점이 있다. OrderService를 유닛 테스트 하기위해 MemberRepository를 모의 객체로 주입하고 싶을 때 순수한 자바 코드로 MemberRepository를 설정할 방법이 없다. 테스트하기가 어려워진다는 단점은 치명적이다.

필드 주입은 @SpringBootTest로 스프링 컨테이너를 띄워서 테스트하는 테스트 코드나 @Configuration이 붙은 스프링 설정 클래스와 같은 제한적인 경우에만 사용하자.

  • 정리: 실제 애플리케이션 코드에 사용하지 말자

4. 일반 메서드 주입

@Service
public class OrderService {
    private MemberRepository memberRepository;
    private OrderRepostiroy orderRepostiroy;

    @Autowired
    public void init(MemberRepository memberRepository,
                     OrderRepository orderRepository) {
        this.memberRepository = memberRepository;
        this.orderRepostiroy = orderRepostiroy;
    }
}

일반 메서드를 통해 한 번에 여러 객체를 주입 받을 수 있다. 거의 사용되지 않는다.

선택적 의존관계 처리 방법


자동 주입할 빈이 없어도 오류를 내지 않고 동작하게 하고 싶을 수 있다. 이를 처리하는 방법은 3개다.

1. Autowired(required=false)

@Service
public class OrderService {
    private MemberRepository memberRepository;

    @Autowired(required=false)
    public setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

@Autowired에 아무런 옵션도 주지 않았을 때 자동 주입할 객체가 컨테이너에 없으면 오류가 발생한다. required의 기본 옵션이 true이기 때문인데 이 옵션을 false로 설정하면 객체가 컨테이너에 없을 때 객체를 호출하지 않기 때문에 오류가 발생하지 않는다.

세터에서 사용할 수 있다.

2. org.springframework.lang.@Nullabe

@Service
public class OrderService {
    private MemberRepository memberRepository;

    @Autowired
    public setMemberRepository(@Nullable MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

자동 주입할 객체가 없으면 null이 입력된다.

생성자에서도 사용할 수 있다.

3. Optional<>

@Service
public class OrderService {
    private MemberRepository memberRepository;

    @Autowired
    public setMemberRepository(Optional<MemberRepository> memberRepository) {
        if (memberRepository.isPresent()) {
            this.memberRepository = memberRepository.get();
        } else {
            this.memberRepository = null;
        }
    }
}

자동 주입할 객체가 존재하지 않으면 Optional.empty 가 입력된다. Optional은 자바8에 추가된 문법으로 null 처리를 편리하게 만들어준다.

생성자에서도 사용할 수 있다.

충돌 (조회 대상이 2개 이상인 경우)


@Autowired 는 타입을 기반으로 빈을 조회해서 주입해준다. 그런데 조회된 빈이 2개 이상이면 컨테이너는 어떤 빈을 주입해야할지 모르기 때문에 NoUniqueBeanDefinition 오류를 발생시킨다.

@Component
public class MemoryMemberRepository implements MemberRepository {}
@Component
public class JpaMemberRepository implements MemberRepository {}
@Service
public class OrderService {
    private MemberRepository memberRepository;

    @Autowired
    public OrderService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

MemberRepository 타입의 빈이 2개다. 주입받을 객체의 타입을 구체 타입으로 바꿔주면 충돌이 안나지만 객체를 구체 타입으로 다루면 유연성이 떨어진다.

추상 타입으로 주입받으면서 충돌을 해결하는 방법은 다음과 같다.

1. 주입받는 객체의 필드 명, 파라미터 명 변경

@Service
public class OrderService {
    @Autowired
    private MemberRepository memoryMemberRepository;
}
@Service
public class OrderService {
    private MemberRepository memberRepository;

    @Autowired
    public OrderService(MemberRepository memoryMemberRepository) {
        this.memberRepository = memoryMemberRepository;
    }
}

주입받는 필드명, 혹은 파라미터 명을 주입받는 빈의 이름으로 변경하면 충돌이 나지 않는다. @Autowired는 타입으로 매칭해서 빈이 2개이상 조회되면 빈의 이름으로 추가 매칭한다.

2. @Qualifier

@Qualifier는 자동주입 대상인 빈을 구분하는 추가적인 방법이다. 빈의 이름을 변경하는 것이 아니라는 것에 주의하자.

@Component
@Qualifier("memoryMemberRepository")
public class MemoryMemberRepository implements MemberRepository {}
@Component
@Qualifier("mainMemberRepository")
public class JpaMemberRepository implements MemberRepository {}
@Service
public class OrderService {
    private MemberRepository memberRepository;

    @Autowired
    public OrderService(
            @Qualifier("mainMemberRepository") MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

@Qualifer 애노테이션을 붙여주면 해당 애노테이션이 붙은 빈을 주입받는다. 만약 매칭되는 빈이 없으면 @Qualifier의 이름을 가진 빈을 추가적으로 찾는다. 그래도 없으면 NoSuchBeanDefinition 예외가 발생한다.

@Qualifier로 빈의 이름을 찾으려 하지말고 매칭되는 @Qualifier를 찾는 용도로만 즉, 쌍으로 사용하는게 의도가 명확하고 유지보수 하기에 좋다. @Qualifier는 빈을 구분하는 방법이므로 @Autowired가 붙은 곳에 모두 적용할 수 있다.

3. @Primary

@Component
public class MemoryMemberRepository implements MemberRepository {}
@Component
@Primary
public class JpaMemberRepository implements MemberRepository {}
@Service
public class OrderService {
    private MemberRepository memberRepository;

    @Autowired
    public OrderService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
}

여러개의 빈이 조회될 경우 예외를 발생시키기보다 @Primary를 통해 우선권을 가지는 빈을 지정해줄 수 있다.

팁으로 주로 사용하는 빈을 @Primary로 등록하고 덜 사용되는 빈만 @Qualifier로 명시적으로 사용하면 유지보수 하기에 좋다.


컴포넌트 스캔과 의존관계 자동 주입은 매우 편리하다.@Component 애노테이션과 @Autowired 두 애노테이션 만으로 @Configuration이 붙은 설정 클래스에 @Bean 애노테이션이 붙은 메서드를 정의하고 의존관계를 생성자 혹은 수정자로 일일이 넣어줘야 하는 수고를 줄일 수 있다.

하지만 수동으로 빈을 등록하는 것이 더 좋을때가 있다. 바로 애플리케이션에 광범위하게 영향을 미치는 AOP 처리다. 데이터베이스 연결, 공통 로그 처리와 같은 애플리케이션 전반에 걸친 관심사는 보통 AOP로 처리한다. 이런 큰 영향을 끼치는 빈은 자동으로 등록했다가 오작동하면 그 이유를 알기가 쉽지 않다. 자동 등록은 @Controller, @Service, @Repository처럼 정형화된 패턴이 있는 빈에 사용하고 AOP처럼 애플리케이션 전반에 영향을 끼치는 빈은 수동으로 등록해서 명시적으로 들어내는 것이 유지보수 하기에 좋다.

profile
아임쿨

0개의 댓글