이전 포스팅에서는 스프링 컨테이너가 빈을 관리하는 방법들을 살펴봤다.
라이프 사이클에서도 잠깐 언급했지만, 의존성 주입이라는 단계가 있는데 이번에 이에 대해서 조금 자세히 정리해보고자 한다 👀
의존성 주입은 스프링의 핵심 기능 중 하나로, 객체 간의 결합도를 낮추고 유연한 프로그래밍을 가능하게 하는 방법이다.
그렇다면 DI가 되지 않은 코드는 어떤걸까?
우선 의존성 주입이 왜 필요한지 기존 자바 코드의 문제점을 통해 살펴보자
Sample Code
public class OrderService { private final PaymentService paymentService; public OrderService() { // 직접 구현 클래스를 생성하여 할당 this.paymentService = new PaymentServiceImpl(); } public void processOrder(Order order) { paymentService.processPayment(order); } }
위 코드는 new PaymentServiceImpl()
를 사용하여 생성자 내부에서 직접 인스턴스를 생성하고 있다.
processOrder 메서드를 살펴보면 paymentService의 메서드를 호출하고 있으므로, PaymentServiceImpl의 구현에 직접적으로 의존하고 있다고 볼 수 있다.
즉, PaymentServiceImple에서 오류가 발생하면 OrderService도 같이 오류가 발생한다. (코드의 수정이 제한적이다!)
또한, 구현체를 직접 생성하므로 테스트가 어렵다는 문제가 있다.
한 클래스가 다른 클래스의 메서드를 사용할 때 의존한다라고 표현한다.
즉, 의존은 클래스 사이에서 변경에 의해 영향을 받는 관계를 의미한다.
스프링에서는 의존성 주입을 통해 이러한 문제를 해결한다.
우선 의존성을 외부에서 주입하는 코드를 한번 직접 구현해보자!
Sample Code
public class OrderService { private final PaymentService paymentService; public OrderService(PaymentService paymentService) { this.paymentService = paymentService; } public void processOrder(Order order) { paymentService.processPayment(order); } }
간단하게 생성자의 매개변수로 외부에서 생성한 PaymentService의 구현체를 전달받아서 필드에 대입한다.
이처럼 외부에서 생성된 무언가를 전달받는 것을 의존성 주입
이라고 한다.
의존성 주입에는 여러 방법이 존재하지만, 기본적으로 외부에서 생성된 인스턴스를 매개변수를 통해 받는다는 아이디어를 근간으로 구현한다.
위 코드는 생성자를 통해 받았지만, setter 메서드를 통해서도 의존성 주입을 구현할 수 있다.
위 코드를 setter를 사용해서 의존성 주입을 받으면 다음과 같다.
Sample Code
public class OrderService { private final PaymentService paymentService; public void setOrderService(PaymentService paymentService){ this.paymentService = paymentService; } public void processOrder(Order order) { paymentService.processPayment(order); } }
단순하게 생성자에서 정의된 코드를 setter로 옮긴 것과 동일하다.
이처럼 외부에서 의존성을 주입받도록 코드를 작성할 수 있다.
스프링에서는 @Autowired
어노테이션을 사용하여 의존성 주입을 수행한다.
@Autowired
는 해당 어노테이션이 붙은 필드나 생성자에 대해 스프링 컨테이너에 등록된 빈 중에서 타입이 일치하는 빈을 찾아 자동으로 주입해주는 기능을 제공한다.
이를 통해 개발자는 직접 객체를 생성하고 할당하는 과정 없이, 스프링의 IoC 컨테이너에 위임하여 의존성을 관리할 수 있다!
클래스의 필드에서 직접 @Autowired
어노테이션을 추가하여 사용할 수 있다.
간단한 코드를 살펴보자
Sample Code
public class ChangePasswordService { @Autowired private MemberDAO memberDAO; public void changePassword(String email, String currentPassword, String newPassword){ Member member = memberDAO.selectByEmail(email); if (member == null){ throw new RuntimeException("등록된 회원이 없습니다."); } member.changePassword(currentPassword, newPassword); memberDAO.update(member); } }
(여기서 MemberDAO
는 스프링 컨테이너에 이미 등록되어있다고 가정하자)
위 코드에서 memberDAO 필드에 @Autowired
어노테이션을 붙여줌으로써, 스프링은 컨테이너에 등록된 MemberDAO 타입의 빈을 찾아 자동으로 주입해준다.
이러한 방식을 사용하면 생성자나 setter 메서드 없이도 의존성 주입이 가능하다는 장점이 있다.
Sample Code
public class ChangePasswordService { private MemberDAO memberDAO; @Autowired public void setMemberDAO(MemberDAO memberDAO) { this.memberDAO = memberDAO; } public void changePassword(String email, String currentPassword, String newPassword){ Member member = memberDAO.selectByEmail(email); if (member == null){ throw new RuntimeException("등록된 회원이 없습니다."); } member.changePassword(currentPassword, newPassword); memberDAO.update(member); } }
필드에서 사용한 코드와는 다르게 메서드에서도 @Autowired
어노테이션을 사용할 수 있다.
이처럼 메서드에서 해당 어노테이션을 사용하면, 매개변수로 넘어오는 MemberDAO와 일치하는 빈을 스프링 컨테이너에서 찾아서 주입해준다.
의존성 주입을 위한 @Autowired
어노테이션을 사용할 때, 주의 사항을 조금 정리해보려고 한다.
사진과 같이 NoSuchBeanDefinitionException
이라는 예외가 발생한다.
@Autowired
는 스프링 컨테이너에 등록된 빈들 중에서 의존성 주입 대상을 찾기 때문에 대상이 컨테이너에 없다면 해당 예외가 발생하는 것이다.
우선 동일한 타입의 빈을 2개 등록해보자
Sample Code 1
@Bean public MemberPrinter memberPrinter() { return new MemberPrinter(); } @Bean public MemberPrinter memberPrinter2() { return new MemberPrinter(); }
MemberPrinter 타입의 인스턴스를 반환하는 빈이 컨테이너에 2개가 등록되어있다.
이제 @Autowired
어노테이션으로 의존성을 주입하는 코드를 작성해보자
Sample Code 2
public class MemberListPrinter { @Autowired private MemberDAO memberDAO; @Autowired private MemberPrinter printer; public void printAll(){ Collection<Member> members = memberDAO.selectAll(); members.forEach(printer::print); } }
Result View
MemberListPrinter에서는 MemberPrinter 타입의 printer 변수에 의존성 주입을 수행한다.
하지만, 이전 코드와 마찬가지로 컨테이너에서 동일한 타입의 인스턴스를 반환하는 빈이 2개이므로 Result View에 첨부된 사진과 같이 예외가 발생한다.
따라서, 동일한 타입의 빈이 2개 이상이라면 의존성 주입에 사용할 빈을 지정할 방법이 필요하다!
동일한 타입의 빈이 여러 개 등록되어 있을 때, 스프링은 어떤 빈을 주입해야 할지 판단하기 어렵다.
이런 경우 @Qualifier
어노테이션을 사용하여 주입할 빈을 명확하게 지정할 수 있다.
우선 설정 파일에서 빈을 등록할 때 한정자를 지정해보자
Sample Code 1
@Bean @Qualifier("printer") public MemberPrinter memberPrinter() { return new MemberPrinter(); } @Bean public MemberPrinter memberPrinter2() { return new MemberPrinter(); }
이제 의존성 주입이 필요한 곳에서 한정자를 통해 특정 빈을 지정할 수 있다
Sample Code 2
public class MemberListPrinter { @Autowired private MemberDAO memberDAO; @Autowired @Qualifier("printer") private MemberPrinter printer; public void printAll() { Collection<Member> members = memberDAO.selectAll(); members.forEach(printer::print); } }
이처럼 사용하는 곳에서도 @Qualifier
를 사용하면 등록된 빈 중에서 동일한 한정자를 가진 빈을 가져와서 의존성 주입을 진행할 수 있다.
@Qualifier
관련 규칙을 정리하면 다음과 같다.
@Qualifier 관련 규칙
- 설정 파일에서
@Qualifier
생략 시
-> 빈의 이름이 자동으로 기본 한정자로 지정된다
- 필드에서
@Qualifier
생략 시
-> 필드 이름이나 파라미터 이름을 한정자로 사용한다
타입이 다른 경우에도 주입 대상을 명확히 해야 할 때가 있다.
예를 들어 MemberSummaryPrinter가 MemberPrinter를 상속받은 경우를 살펴보자
Sample Code 1
@Bean public MemberPrinter memberPrinter() { return new MemberPrinter(); } @Bean public MemberSummaryPrinter memberSummaryPrinter() { return new MemberSummaryPrinter(); }
우선 빈을 다음과 같이 등록할 수 있다.
이전과는 다르게 MemberPrinter를 상속받아서 구현된 MemberSummaryPrinter를 빈으로 등록했다.
마찬가지로 @Autowired
어노테이션으로 의존성을 주입하는 코드를 작성해보자
Sample Code
public class MemberInfoPrinter { @Autowired private MemberDAO memberDAO; @Autowired private MemberPrinter printer; public void printMemberInfo(String email) { Member member = memberDAO.selectByEmail(email); if (member == null) { System.out.println("일치하는 데이터가 없습니다."); return; } printer.print(member); System.out.println(); } }
Result View
마찬가지로 빈을 특정할 수 없다는 오류가 발생하고 있다.
물론 상속 관계에 존재하는 타입의 빈도 @Qualifier
어노테이션을 사용해서 분리할 수 있다.
하지만, 명시적으로 자식 클래스의 타입을 사용하는 방법도 존재한다.
코드는 다음과 같이 수정할 수 있다.
Sample Code
public class MemberInfoPrinter { @Autowired private MemberDAO memberDAO; @Autowired // 수정된 부분 private MemberSummaryPrinter printer; public void printMemberInfo(String email) { Member member = memberDAO.selectByEmail(email); if (member == null) { System.out.println("일치하는 데이터가 없습니다."); return; } printer.print(member); System.out.println(); } }
의존성 주입이 필요한 곳에서 명시적으로 자식 클래스인 MemberSummaryPrinter 타입으로 선언하여 의존받을 빈을 특정할 수 있다.
하지만, MemberSummaryPrinter가 아닌 MemberPrinter를 사용하고자 한다면 결국 @Qulifier
어노테이션을 사용해야 할 것이다.
즉, 부모 타입을 사용해야 하는 경우에는 다음 사진과 같이 반드시 한정자를 사용하여 혼란을 방지하자!
이번 포스팅에서는 스프링의 핵심 개념인 의존성 주입(DI)에 대해 자세히 알아보았다.
기존 자바 코드의 문제점부터 시작해서, 의존성 주입의 필요성과 구현 방법, 그리고 스프링에서 제공하는 @Autowired
와 @Qualifier
어노테이션의 사용법까지 살펴보았다.
이제는 @Autowired
를 사용할 때 단순히 의존성을 주입하는 것을 넘어서, 상황에 따른 적절한 처리 방법과 주의사항까지 고려할 수 있길 바란다.. 👊