본문 바로가기

Spring 프레임워크/이론

Spring의 의존 자동 주입(@Autowired)

이전 포스트에서는 Spring 프레임워크에서 어떤 식으로 DI가 이루어지는지 간략하게 살펴보았다. 의존성을 주입하려면 직접 Bean 객체를 생성하는 메서드를 호출하여 생성자에 전달하거나 Setter 메서드를 사용할 수도 있겠지만 스프링 프레임워크 자체에서 이 의존성을 주입해줄 수도 있다. 이를 의존 자동 주입이라 하며 @Autowired나 @Resource 어노테이션을 사용한다.

...

public class ChangePasswordService {
    @Autowired
    private MemberDAO memberDAO;
    ...

간단한 사용은 위와 같다. 그냥 의존 주입이 필요한 대상에게 @Autowired 어노테이션을 붙여주면 된다. 원래는 이 ChangePasswordService 클래스에서 MemberDAO 객체를 의존했으며 Setter 함수를 이용하여 다음처럼 의존성을 주입받았다.

        ...
        changePasswordService = new ChangePasswordService();
        changePasswordService.setMemberDAO(memberDAO);
    }
    ...

하지만 위처럼 MemberDAO 객체를 @Autowired로 자동 주입받은 이후부터는 setMemberDAO 같은 Setter 함수로 주입해줄 필요가 없다. 이런 필드뿐 아니라 메서드에도 자동 주입을 설정해줄 수 있는데 의존 객체를 주입하는 Setter 메서드가 있을 때 다음처럼 @Autowired 어노테이션을 붙이면 따로 호출하지 않아도 의존성이 주입된다.

...
    @Bean
    public MemberListPrinter memberListPrinter(){
        MemberListPrinter memberListPrinter = new MemberListPrinter();
        //memberListPrinter.setMemberDAO(memberDAO);
        //memberListPrinter.setMemberPrinter(memberPrinter());
        return memberListPrinter;
    }
...
public class MemberListPrinter {
    private MemberDAO memberDAO;
    private MemberPrinter memberPrinter;

    @Autowired
    public void setMemberDAO(MemberDAO memberDAO) {
        this.memberDAO = memberDAO;
    }

    @Autowired
    public void setMemberPrinter(MemberPrinter memberPrinter) {
        this.memberPrinter = memberPrinter;
    }

    ...

MemberListPrinter에서 MemberDAO, MemberPrinter 객체를 의존하고 있을 때 이를 주입하는 Setter 메서드를 직접 호출하지 않고 자동으로 주입하는 것을 볼 수 있다. 이는 우리가 호출하지 않고 스프링 프레임워크에서 호출해서 주입하기 때문이다. 이때 메서드의 파라미터 타입에 해당되는 Bean 객체를 컨테이너에서 찾아서 주입한다. 이렇게 자동으로 의존성을 주입해주는 어노테이션이 @Autowired다.

메서드의 파라미터 타입에 해당되는 Bean 객체라는 것은 결국 해당 Bean 객체가 @Bean 메서드든 어떻게든 스프링 컨테이너에 등록돼야 한다는 것이다. 이는 애플리케이션 콘텍스트나 다른 컴포넌트에서 등록될 수 있으며 만약 컨테이너에 등록된 객체가 없다면 스프링에서는 NoSuchBeanDefinitionException을 발생시킨다. 위의 경우 user 패키지의 MemberDAO 클래스의 객체를 컨테이너에서 찾지 못했다는 것을 의미한다. 컨테이너에 등록하려면 이전처럼 @Bean 메서드를 구현하거나 MemberDAO 클래스에 @Component 어노테이션을 붙여서 @ComponentScan에서 탐색하도록 할 수 있다.

그럼 만약 객체를 등록했는데 같은 자료형의 객체가 두 번 등록됐다면 어떨까? 이 경우 스프링 프레임워크에서 어떤 Bean 객체를 가져다가 주입해야 할지 구분할 수 없기 때문에 NoUniqueBeanDefinitionException을 발생시킨다. 그래서 스프링에서는 의존성을 자동으로 주입할 때 중복되는 Bean 객체가 있다면 어떤 객체를 주입할지 한정하는 어노테이션인 @Qualifier를 사용할 수 있다. 이는 아래와 같이 사용할 수 있다.

...
    @Bean
    @Qualifier("real")
    public MemberDAO realMemberDAO(){
        return new MemberDAO();
    }

    @Bean
    public MemberDAO fakeMemberDAO(){
        return new MemberDAO();
    }
...

@Qualifier 어노테이션을 사용하면 자동 주입할 빈을 한정할 수 있는데 이는 마치 Bean 객체에 태그를 붙이는 느낌이라 생각하면 된다. 위의 realMemberDAO, fakeMemberDAO 메서드에서는 같은 MemberDAO 객체를 반환하고 있다. 여기서 realMemberDAO에서 등록한 객체를 자동 주입 대상으로 사용하기 위해 위의 코드처럼 @Qualifier("real") 어노테이션을 붙여주자.

public class MemberListPrinter {
    @Autowired @Qualifier("real")
    private MemberDAO memberDAO;
    
    @Autowired
    private MemberPrinter memberPrinter;
    ...

그리고 이 Bean 객체를 자동 주입받을 필드나 메서드에 똑같이 @Qualifier("real") 어노테이션을 붙이면 해당 필드나 메서드에서는 realMemberDAO 메서드에서 생성한 Bean 객체를 주입받게 된다. 이를 확인해보기 위해 해당 객체를 사용할 때 realMemberDAO는 "for real"이라는 문자열을, fakeMemberDAO는 "it's fake"라는 문자열을 출력하도록 수정해보았다.

실행 결과 위처럼 "Data inserted by for real"라는 문자열이 출력된 것을 볼 수 있다. 이때 @Qualifier 어노테이션이 지정되지 않은 @Autowired 항목의 경우 여전히 에러가 발생하기 때문에 모든 자동 주입 의존성을 가지는 필드나 메서드에 @Qualifier("real") 어노테이션을 부착해야 한다. 위의 realMemberDAO 메서드에 @Qualifier를 부착했으니 아무런 @Qualifier도 부착되지 않은 fakeMemberDAO 객체는 @Autowired만 써도 자동으로 주입되는 게 아닐까? 싶지만 있고 없고 차이로 구분해서 주입해주지는 않는다.

확인을 위해 realMemberDAO 대신 fakeMemberDAO로 @Qualifier를 옮기면 위처럼 "it's fake"라는 문자열이 출력되는 것을 볼 수 있다.

 

재밌는 점은 중복되는 Bean 객체를 반환하는 메서드일지라도 이름에 따라 알아서 선택하는 경우도 있다.

...
    @Bean
    public MemberDAO realMemberDAO(){
        return new MemberDAO();
    }

    @Bean
    public MemberDAO fakeMemberDAO(){
        return new MemberDAO();
    }
...
// Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException:
// No qualifying bean of type 'user.MemberDAO' available: 
// expected single matching bean but found 2: realMemberDAO,fakeMemberDAO
...
    @Bean
    public MemberDAO memberDAO(){
        return new MemberDAO();
    }

    @Bean
    public MemberDAO memberDAONEW(){
        return new MemberDAO();
    }
...

// 아무 문제 없이 잘 실행됨.

위의 메서드들이 반환하는 Bean 객체는 스프링에서 구분하지 못하고 에러가 발생하지만 아래의 메서드들은 memberDAO 메서드를 선택하여 주입하기 때문에 아무런 에러가 발생하지 않는다. 왜 그럴까? 스프링에서는 주입할 Bean 객체를 탐색할 때 @Qualifier 어노테이션이 없다면 Bean 객체의 이름을 자동으로 한정자로 사용하기 때문이다.

public class MemberRegisterService {
    @Autowired
    // @Qualifier("real")
    private MemberDAO memberDAO;
    ...
    ...
    @Bean
    public MemberDAO realMemberDAO(){
        return new MemberDAO("for real");
    }

    @Bean
    public MemberDAO memberDAO(){
        return new MemberDAO("it's fake");
    }
    ...

아까 Bean 객체를 반환하는 메서드의 이름을 memberDAO로 바꾸고 어떤 @Qualifier도 지정하지 않아 보자. 그럼 이 경우 이 메서드는 memberDAO라는 한정자를 가진다. 즉 @Qualifier("memberDAO")처럼 생각할 수 있는 것이다. 그리고 다른 곳(MemberRegisterService)에서 MemberDAO 객체를 memberDAO라는 이름으로 자동 주입받고 있을 때 따로 한정자를 적용하지 않는다면 이 필드 역시 필드의 이름(memberDAO)을 한정자로 사용한다. 즉 @Qualifier("memberDAO")처럼 취급하는 것이다. 그래서 자동적으로 이 두 @Qualifier("memberDAO")가 매칭 되기 때문에 중복된 Bean 객체임에도 불구하고 특수한 작업 없이 객체를 알아서 선택하여 주입하는 것이다.

중복된 Bean 객체는 단순히 같은 클래스뿐 아니라 상속관계의 Bean 객체에도 해당된다. Bean 객체로 등록되는 MemberPrinter라는 클래스와 이를 상속한 MemberSummaryPrinter 클래스가 있을 때 스프링에서는 두 객체를 구분하지 못한다. 왜냐면 MemberSummaryPrinter가 MemberPrinter의 자식 클래스기 때문에 MemberPrinter 객체를 주입할 공간에 MemberSummaryPrinter 객체를 대신 주입할 수 있기 때문이다. 즉 스프링이 MemberPrinter 객체를 의존하는 필드에 자동으로 의존성을 주입하려고 할 때 MemberPrinter 객체를 주입할지 MemberSummaryPrinter 객체를 주입할지 결정할 수 없다.

    ...
    @Autowired
    private MemberSummaryPrinter memberPrinter;
    ...

그래서 이 경우 이전처럼 @Qualifier 어노테이션을 사용하거나 아니면 하위 클래스( MemberSummaryPrinter)를 의존하도록 할 수 있다. 이 경우 MemberPrinter는 MemberSummaryPrinter 대신 사용할 수 없기 때문에 스프링에서는 Bean 객체를 헷갈리지 않는다.

 

이렇게 @Autowired로 자동 주입할 때 상황에 따라 빈이 꼭 주입되지 않아도 괜찮을 수 있다. 그러나 이전에 보았듯이 스프링의 @Autowired 어노테이션은 주입할 빈이 없다면 예외를 발생시키기 때문에 추가적인 설정이 필요하다.

    ...
    @Autowired(required = false)
    private String unique;
    ...

첫 번째 방법은 @Autowired 어노테이션의 required 속성을 false로 지정하는 것이다. 이 속성은 주입할 Bean 객체가 컨테이너에 등록되지 않았더라도 예외를 발생시키지 않는다. 대신 아무런 객체도 주입하지 않기 때문에 해당 객체는 null이 된다.

    ...
    @Autowired
    private Optional<String> unique;
    ...

두 번째 방법은 Optional 객체를 사용하는 것이다. 의존 주입 대상이 Optional인 경우 해당 타입의 Bean 객체(헷갈릴 수 있겠지만 String 객체)가 있다면 해당 값을 포함한 Optional 객체를, 그렇지 않다면 빈 Optional 객체를 삽입한다. 어찌 됐든 null이 삽입되지 않기 때문에 좀 더 안전한 방법이라 할 수 있다.

    ...
    @Autowired
    @Nullable
    private String unique;
    ...

세 번째 방법은 @Nullable 어노테이션을 사용하는 것이다. 이 방법은 비교적 간단하다. 이런 방법들은 지금처럼 @Autowired 필드 자체뿐 아니라 Setter 메서드나 생성자 함수에도 붙일 수 있다. 그냥 보기에는 세 방법 다 비슷해 보이는데 무슨 차이가 있는 걸까? 이는 해당하는 빈이 없을 때 어떤 값이 전달되는지 좀 더 자세히 살펴볼 필요가 있다.

먼저 첫 번째 방법인 @Autowired 어노테이션의 required 속성을 false 값으로 지정하는 경우를 살펴보자. 이 경우 Bean 객체가 존재하지 않으면 생성자에서 지정되거나 변수 선언 시 초기화된 값이 유지된다. 즉 Bean 객체가 없다고 해서 null을 집어넣거나 하지는 않는다는 얘기다. 그 예로 위의 unique 변수를 출력시켜본 결과 "DEFAULT"라는 문자열이 출력되는 것을 볼 수 있다.

다음 방법으로 Optional 객체는 어떨까? 언급했듯이 Bean 객체가 없으면 빈 Optional 객체를 전달하기 때문에 값이 존재하지 않으면 대체 값을 출력하는 orElse 메서드에 의해 "Not Existing"이라는 문자열이 출력된 것을 볼 수 있다.

@Nullable 예제는 필드 말고 메서드에 적용해야 원하는 결과가 나타났기 때문에 코드를 조금 수정하였다. 이 @Nullable 어노테이션은 해당 Bean 객체가 존재하지 않으면 null을 전달한다. 그렇기 때문에 처음에 required 옵션을 false로 설정했던 경우와 달리 변수 생성 시 초기화했던 "DEFAULT"라는 문자열 대신 null이 전달되어 출력된 것을 볼 수 있다.

 

@Autowired 어노테이션의 required 속성을 제외한 두 방법들은 꼭 스프링이 아니어도 자바에서 기본적으로 쓰이는 개념들인데 스프링 프레임워크에서 이들을 추가적으로 활용하기 때문에 주입할 Bean 객체가 존재하지 않아도 별 문제가 없는 것이다. 물론 이 세 가지 방법의 차이점을 구분해서 필요한 곳에 적절한 방법을 사용해야 할 것이다.

 

특이하게 마지막 방법은 첫 번째, 두 번째처럼 필드 변수에 직접 @Autowired와 @Nullable을 설정해줬을 경우 제대로 동작하지 않았는데 아마 뭔가 놓친 게 있거나 메서드에서만 동작하는 것 같다.

 

마지막으로 자동 주입과 수동 주입을 모두 사용한 경우 어떻게 될까? 이때는 자동 주입이 우선이 된다. 위의 그림에서는 콘솔에 출력될 문자열인 unique 변수를 세 가지 방법으로 초기화 또는 주입했다. 변수 생성 시 초기화에는 "DEFAULT"를, Bean 객체로 등록해 자동 주입하는 방법에는 "Auto-injected dependencies"를, Setter 메서드로 수동 주입하는 방법에는 "Manual-injected dependencies"라는 문자열을 등록했다. 그리고 실행한 결과 "Auto-injected dependencies"라는 문자열이 출력된 것을 볼 수 있다.

 

즉 설정 클래스에서 직접 의존성을 주입해도 @Autowired 어노테이션이 붙어있다면 스프링 프레임워크에서 일치하는 Bean 객체를 찾아 자동으로 주입한다. 물론 Bean 객체가 없다고 해서 수동으로 주입한 "Manual-injected dependencies"가 출력되진 않는데 위에서 언급한 required 옵션이나 Optional, @Nullable 등을 활용하지 않는 이상 @Autowired 어노테이션에는 Bean 객체가 필요하기 때문이다.

 

그렇기 때문에 자동 주입과 수동 주입을 섞어서 사용하는 것보다는 의존 자동 주입을 최대한 사용하고 자동 주입이 어려운 경우 수동으로 주입하는 것이 좋을 것이다.