[LG CNS AM Inspire Camp 1기] Spring (3) - 의존성 주입 (DI) & @Autowired 그리고 @Qulifier

정성엽·2025년 1월 21일
2

LG CNS AM Inspire 1기

목록 보기
32/53
post-thumbnail

INTRO

이전 포스팅에서는 스프링 컨테이너가 빈을 관리하는 방법들을 살펴봤다.

라이프 사이클에서도 잠깐 언급했지만, 의존성 주입이라는 단계가 있는데 이번에 이에 대해서 조금 자세히 정리해보고자 한다 👀


1. 의존성 주입(DI)의 필요성

의존성 주입은 스프링의 핵심 기능 중 하나로, 객체 간의 결합도를 낮추고 유연한 프로그래밍을 가능하게 하는 방법이다.

그렇다면 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도 같이 오류가 발생한다. (코드의 수정이 제한적이다!)

또한, 구현체를 직접 생성하므로 테스트가 어렵다는 문제가 있다.


2. 의존성 주입(DI)

한 클래스가 다른 클래스의 메서드를 사용할 때 의존한다라고 표현한다.

즉, 의존은 클래스 사이에서 변경에 의해 영향을 받는 관계를 의미한다.

스프링에서는 의존성 주입을 통해 이러한 문제를 해결한다.

우선 의존성을 외부에서 주입하는 코드를 한번 직접 구현해보자!

💡 생성자를 이용한 의존성 주입

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를 활용한 의존성 주입

위 코드를 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로 옮긴 것과 동일하다.

이처럼 외부에서 의존성을 주입받도록 코드를 작성할 수 있다.


3. 스프링의 의존성 주입(DI) & @Autowired

스프링에서는 @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와 일치하는 빈을 스프링 컨테이너에서 찾아서 주입해준다.

💡 DI 주의사항

의존성 주입을 위한 @Autowired 어노테이션을 사용할 때, 주의 사항을 조금 정리해보려고 한다.

1. 빈이 컨테이너에 등록되지 않은 경우

사진과 같이 NoSuchBeanDefinitionException 이라는 예외가 발생한다.

@Autowired 는 스프링 컨테이너에 등록된 빈들 중에서 의존성 주입 대상을 찾기 때문에 대상이 컨테이너에 없다면 해당 예외가 발생하는 것이다.

2. 동일한 타입의 빈이 2개 이상인 경우

우선 동일한 타입의 빈을 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개 이상이라면 의존성 주입에 사용할 빈을 지정할 방법이 필요하다!


4. @Qulifier

동일한 타입의 빈이 여러 개 등록되어 있을 때, 스프링은 어떤 빈을 주입해야 할지 판단하기 어렵다.

이런 경우 @Qualifier 어노테이션을 사용하여 주입할 빈을 명확하게 지정할 수 있다.

💡 @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 어노테이션을 사용해야 할 것이다.

즉, 부모 타입을 사용해야 하는 경우에는 다음 사진과 같이 반드시 한정자를 사용하여 혼란을 방지하자!


OUTRO

이번 포스팅에서는 스프링의 핵심 개념인 의존성 주입(DI)에 대해 자세히 알아보았다.

기존 자바 코드의 문제점부터 시작해서, 의존성 주입의 필요성과 구현 방법, 그리고 스프링에서 제공하는 @Autowired@Qualifier 어노테이션의 사용법까지 살펴보았다.

이제는 @Autowired 를 사용할 때 단순히 의존성을 주입하는 것을 넘어서, 상황에 따른 적절한 처리 방법과 주의사항까지 고려할 수 있길 바란다.. 👊

profile
코린이

0개의 댓글

관련 채용 정보