본문 바로가기

Spring 프레임워크/이론

Spring의 의존성 주입(Dependency Injection)

의존성 주입

스프링의 큰 특징 중 하나는 Dependency Injection(DI, 의존성 주입)을 지원한다는 것이다. 의존성 주입 자체는 스프링에 한정된 개념이 아니고 객체 간 관계를 다루는 방법 중 하나인데 일단 의존성이라는 것은 다음과 같다.

public class MemberRegisterService {
    private MemberDAO memberDAO = new MemberDAO();
    
    public long regist(...){ 
        memberDAO.requestRegiest(...);
        ...
    }
    ...
}

위의 MemberRegisterService 클래스에서는 MemberDAO 클래스의 객체를 직접 new 연산자로 생성하여 그의 메서드를 사용하고 있다. 이 경우 MemberRegisterService 클래스는 MemberDAO 클래스를 의존하고 있다고 표현할 수 있다. 

만약 MemberDAO 클래스의 메서드가 변경되면 이는 MemberRegisterService에도 영향을 끼친다. 이렇게 변경에 대한 영향이 전파되는 의존 관계를 생성하는 방법 중 하나는 위의 예처럼 클래스 내부에서 의존 대상 객체를 직접 생성해서 변수로 저장하는 것이다. 하지만 이런 깊은 의존 관계는 유지보수를 까다롭게 만들 수 있으며 이에 대해서 객체지향 개발론에서는 설계 원칙 중 하나로 추상적인 것에 의존하라는 Dependency Inversion Principle(DIP, 의존관계 역전 원칙)이라는 것을 제시하고 있기도 하다. 아무튼 스프링에서 제공하는 Dependency Injection은 이런 의존성을 활용하는 기능이다.

 

의존성 주입은 쉽게 말해서 한 객체가 다른 객체에게 의존 대상을 주입해주는 것을 말한다. 즉 말 그대로 의존성을 주입해주는 것이다. 위키피디아의 정의에 따르면 의존 대상(MemberDAO)을 서비스, 의존하는 객체(MemberRegisterService)를 클라이언트라고 할 때 의존성을 주입하면 다음과 같은 특징이 있다.

  • 클라이언트는 서비스를 찾거나 구축하지 않고 외부로부터 주입받는다.

  • 클라이언트는 주입된 서비스가 어떻게 구성되었는지 알지 못하며 이에 대한 책임은 외부에 위임한다.

  • 클라이언트는 외부(주입자) 코드를 호출할 수 없다.

  • 클라이언트는 서비스의 사용 방식을 정의하고 있는 인터페이스에 대해서만 인지한다.

즉 객체의 생성(주입자)과 사용(클라이언트)을 분리하여 결과적으로 느슨한(유연한) 프로그램을 디자인하는 것이다.

의존성 주입 방법

그렇다면 어떻게 이 의존성을 주입할 수 있을까? 생각할 수 있는 간단한 방법은 다음과 같다.

  • 객체 생성 시 생성자의 매개변수로 의존성 주입

  • 객체 생성 후 메서드(setter 등)로 의존성 주입

  • 클래스 생성 시 @Autowired 어노테이션으로 의존성 주입

이를 코드로 나타내면 다음과 같다.

public class MemberRegisterService {
    private MemberDAO memberDAO;
    
    public MemberRegisterService(MemberDAO memberDAO) {
        this.memberDAO = memberDAO;
    }

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

각각 클래스의 생성자, Setter 메서드, @Autowired 어노테이션을 이용하여 외부에서 의존성을 주입받고 있다. 그런데 이렇게 의존성을 주입하는 이유는 무엇일까? 이는 언급했듯이 유지보수의 편리성을 위한 것이다.

만약 이 MemberDAO 클래스를 의존하는 클래스가 여러 개 있다고 할 때 MemberDAO 클래스에서 기능이 추가된 하위 클래스를 하나 만들어서 의존 대상을 변경하고자 한다면 MemberDAO를 의존하는 모든 소스코드에서 이를 찾아서 바꿔야 한다. 하지만 위의 그림처럼 파일에서 의존 객체를 생성하고 주입해주기만 한다면 의존 대상이 변경되어도 이 한 파일만 수정하면 되기 때문에 느슨한 결합을 유지할 수 있는 것이다. 자바에서는 주로 같은 인터페이스를 구현한 서로 다른 클래스를 의존할 때 더 유용하게 적용될 수 있다.

@Configuration
public class AppContext {
    @Bean
    public MemberDAO memberDAO(){
        return new MemberDAO();
    }

    @Bean
    public MemberRegisterService memberRegisterService(){
        return new MemberRegisterService(memberDAO());
    }
    ...
}

스프링에서는 여러가지 어노테이션을 활용하여 의존성을 주입할 수 있다. 위의 코드에서는 MemberDAO를 의존하는 MemberRegisterService 객체를 생성하는 @Bean 메서드를 정의했는데 이는 @Configuration 어노테이션에 의해 컨테이너에 Bean 객체로 등록된다. 그래서 memberDAO 메서드가 반환하는 MemberDAO 객체를 memberRegisterService 메서드 내부에서 얻어와서 의존성을 주입할 수 있다.

 

스프링의 Bean 객체는 기본적으로 싱글턴 패턴이 적용된다. 그렇기 때문에 여러 Bean 객체가 memberDAO() 메서드로 받은 MemberDAO 객체를 의존하고 있을 때 이들은 다들 같은 객체를 의존하게 된다.

@Autowired 어노테이션

@Configuration
public class AppContextPrinter {

    @Autowired
    private MemberDAO memberDAO;

    @Bean
    public MemberListPrinter memberListPrinter(){
        return new MemberListPrinter(memberDAO, memberPrinter());
    }
    
    ...
}

스프링의 컨테이너에서는 Bean 객체가 너무 많아지면 설정 파일을 분리해서 관리할 수 있다. 이때 다른 파일에 있는 Bean 객체를 의존하는 경우 스프링 컨테이너에서 자동으로 의존성을 주입해주는 @Autowired라는 어노테이션을 사용할 수 있다. 예를 들어 위의 AppContextPrinter라는 설정 파일의 memberListPrinter 메서드에서는 MemberDAO 객체를 의존하고 있는데 의존 객체를 직접 생성하지 않고 @Autowired 어노테이션으로 AppContext에 있는 메서드에서 객체를 받아 주입하는 것을 볼 수 있다.

 

이 어노테이션은 다른 설정 파일에 정의된 해당 타입의 Bean 객체를 어노테이션이 설정된 필드에 주입해준다. 그런데 어떤 Bean 객체가 어떤 건지 알고 주입해주는 것일까? 이것에는 몇 가지 기준과 순서가 있다.

  • 자료형

  • @Qualifier

  • @Component

첫 번째로 자료형의 경우 말 그대로 @Autowired된 변수의 자료형(클래스)에 맞춰서 탐색하는 것이다. 

@Configuration
public class AppContextMember {

    @Bean
    public MemberDAO memberDAO(){
        return new MemberDAO();
    }

    ...
}

위에서 사용했던 코드에서는 MemberDAO 객체를 클래스의 멤버 변수로 의존하고 있었다. 해당 멤버 변수는 MemberDAO 클래스기 때문에 스프링 컨테이너는 MemberDAO 객체를 반환하는 @Bean 메서드를 탐색하고 AppContextMember에 있는 memberDAO 메서드를 참조, 의존 객체를 생성하게 된다. 그렇다면 하나의 메서드 대신 아래처럼 MemberDAO 객체를 반환하는 @Bean 메서드가 여러 개 있다면 어떨까?

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

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

이 경우 NoUniqueBeanDefinitionException이라는 예외가 발생한다. 말 그대로 @Autowired된 memberDAO 변수에 어떤 @Bean 메서드의 값을 넣어야 할지 모르겠다는 것이다. 그렇기 때문에 @Autowired를 사용할 때는 @Autowired Bean 객체와 @Bean 메서드가 매칭 될 수 있는지 유의해야 한다. 재밌는 것은 변수의 이름과 메서드의 이름이 일치할 경우 문제없이 실행되는 것을 볼 수 있었는데 아마 이름에 우선순위가 좀 있는 것 같다.

 

두 번째로 @Qualifier의 경우 위처럼 중복된 자료형의 @Bean 메서드가 있을 때 어떤 @Autowired Bean 객체가 어떤 메서드에 해당될지 지정할 수 있는 어노테이션이다.

    ...
    @Autowired
    @Qualifier("memberDAO1")
    private MemberDAO memberDAO1;

    @Autowired
    @Qualifier("memberDAO2")
    private MemberDAO memberDAO2;
    ...

어노테이션 내부에 지정할 메서드의 이름을 적어주면 해당 Bean 객체는 지정된 @Bean 메서드를 활용하여 자동으로 주입된다. 확인을 위해 MemberDAO 객체에 id 필드를 지정해두고 memberDAO1, memberDAO2 메서드에서 아래처럼 각기 다른 id를 부여하였다.

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

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

그리고 초기화 시 자신의 id를 출력하게 하여 확인한 결과 아래처럼 memberDAO1, memberDAO2 객체가 각기 다른 메서드로 생성된 것을 볼 수 있었다.

...
> Task :SpringMain.main()
MemberDAO1's ID: 1
MemberDAO2's ID: 2
...

 

세 번째로 @Component 방식의 경우 Bean 객체를 스캔해서 등록하면 이를 의존 대상으로 활용하는 방식이다. 이게 무슨 말이냐면 위의 예제에서는 @Bean 메서드에서 MemberDAO 객체를 생성해서 반환하고 있었고 이를 다른 클래스에서 찾아서 의존하는 방식이었는데 @Component 어노테이션의 경우 자동적으로 Bean 객체로 등록되기 때문에 이를 생성하는 메서드가 필요 없는 것이다.

@Configuration
@ComponentScan(basePackages = "user")
public class AppContextMember {

    @Autowired
    private MemberDAO memberDAO;

    ...
@Configuration
@ComponentScan(basePackages = "user")
public class AppContextPrinter {

    @Autowired
    private MemberDAO memberDAO;
    
    ...

이 컴포넌트를 찾으려면 @Autowired 변수를 유지하는 클래스에서 @ComponentScan 어노테이션을 이용하여 컴포넌트를 탐색할 패키지를 지정해야 한다. 여기서 컴포넌트는 @Component 어노테이션이 붙은 클래스로 현재 MemberDAO 자료형의 변수를 의존하고 있기 때문에 MemberDAO 클래스가 이에 해당한다.

@Component
public class MemberDAO {
    private static long nextId = 0;
    
    ...

이를 실행시키면 어디에서도 MemberDAO 객체를 생성하지 않지만 AppContextPrinter와 AppContextMember에서 MemberDAO 객체(Bean 객체)를 성공적으로 의존하는 것을 볼 수 있다.

 

 

어려운 개념이었지만 조금씩 이해가 되는 것 같다. 검색하니 좋은 자료도 많이 있어서 얼른 더 학습해야겠다. @Autowired는 생각보다 내용이 많기 때문에 다음 포스트에서도 이어서 다루겠다.

 

[참고 | www.baeldung.com/spring-annotations-resource-inject-autowire]

[참고 | blog.naver.com/PostView.nhn?blogId=kbh3983&logNo=220876402144]

[참고 | ko.wikipedia.org/wiki/의존성_주입]

[참고 | stackoverflow.com/questions/19368378/what-is-the-point-of-dependency-injection-as-used-in-spring]