의존성 주입(Dependency Injection)

이영섭·2024년 11월 17일

Dependency Injection

DI란 외부에서 두 객체 간의 관계를 결정해주는 디자인 패턴으로,

인터페이스를 사이에 두어 클래스 레벨에서는 의존관계가 고정되지 않도록 하고 런타임 시에 관계를 동적으로 주입하여 유연성을 확보하고 결합도를 낮출 수 있게 해준다.

의존성이란 한 객체가 다른 객체를 사용할 때 의존성이 있다고 한다.


의존성 주입의 3가지 방법

1. 생성자 주입

  • 장점
    • Bean 객체를 생성하는 시점에 필요한 모든 의존 객체를 주입받기 때문에 사용할 때 완전한 상태로 사용할 수 있음.
  • 단점
    • 생성자의 파라미터 개수가 많을 경우 각 인자가 어떤 의존 객체를 설정하는지 알아내려면 생성자의 코드를 확인해야 함.

2. 세터 주입

  • 장점
    • 메서드 이름만으로도 어떤 의존 객체를 설정하는지 쉽게 유추할 수 있음.
  • 단점
    • 필요한 의존 객체를 전달하지 않아도 빈 객체가 생성되기 때문에 객체를 사용하는 시점에 NullPointerException이 발생할 수도 있음.

3. 필드 주입

@Autowired - 스프링의 자동 주입 기능을 위한 어노테이션. 해당 타입의 빈을 찾아서 필드에 할당.

⚠️ 주의사항 ⚠️

  • 빈 설정을 안해줬을 경우 - NoSuchBeanDefinitionException
  • 해당 타입이 인터페이스이면서, 인터페이스를 상속받아 만들어진 구체 클래스가 2개 이상일 때, @Qualifier(”빈이름”) 어노테이션을 이용해 꼭 사용할 빈을 지정해줘야 한다. 그렇지 않을 경우, NoUniqueBeanDefinitionException
  • 빈 객체가 필수가 아닌 경우 처리 방법
    1. @Autowired(required = false)
    2. Optional타입
    3. @Nullable
  • 필드 주입이 지양되는 이유
    1. 순환 참조 문제

      • 컴파일 타임에 순환 참조를 감지할 수 없음
      • 런타임에 에러 발생 가능
      @Service
      public class ServiceA {
          @Autowired
          private ServiceB serviceB;  // ServiceB를 주입받음
      }
      
      @Service
      public class ServiceB {
          @Autowired
          private ServiceA serviceA;  // ServiceA를 주입받음
      }
    2. 불변성(final) 보장 불가

      • final 선언 불가능
    3. 테스트 어려움 - 테스트 코드 의존성 주입이 어려움

    4. 의존성 관계가 불명확

    5. 단일 책임 원칙 위반 가능성

      • 필드 주입은 쉽게 의존성을 추가할 수 있어 클래스가 점점 비대해질 수 있음
      • 생성자 주입은 파라미터가 많아질수록 리팩토링의 신호가 됨

4. 정리

@Autowired 책에서는 이 방법을 많이 사용하지만,

  • @Autowired 필드 주입은 점점 사용을 지양하는 추세
  • 생성자 주입이 권장됨
  • @Service + Component Scan 조합이 더 일반적

@Import

두 개 이상의 설정 파일을 사용하는 또 다른 방법의 어노테이션

@Configuration
@Import(AppConfig2.class) // 1개
@Import({AppConfig1.class, AppConfig2.class}) // 2개 이상
class AppConfImport {...}

@Import로 가져오는 클래스는 어떠한 스프링 관련 어노테이션이 없더라도 스프링 컨테이너가 관리하는 빈으로 등록된다.

하지만, 코드의 의도와 유지보수성을 위해서 명시적으로 기입해주는 것이 좋다.

다중 @Import 사용시 최상위 설정 클래스 한 개만 사용하면 된다.


@Qualifier

자동 주입 가능한 빈이 두 개 이상이면 자동 주입할 빈을 지정할 수 있는 방법이 필요하다. 이때 해당 어노테이션을 사용한다.

사용 방법

@Bean 어노테이션을 붙인 빈 설정 메서드

// 1. 해당 빈의 한정 값으로 "printer"를 지정한다.
@Bean
@Qualifier("printer")
public MemberPrinter memberPrinter1() {
		return new MemberPrinter();
}

// 2. @Autowired 어노테이션에서 자동 주입할 빈을 한정
@Autowired
@Qualifier("printer")
public void setMemberPrinter(MemberPrinter printer) {
		this.printer = printer;
}

빈 이름과 기본 한정자

빈 설정에 @Qualifier 이 없으면 빈의 이름을 한정자로 지정한다.

@Configuration
public class AppCtx2 {

    @Bean
    public MemberPrinter printer() {
        return new MemberPrinter();
    }

    @Bean
    @Qualifier("mprinter")
    public MemberPrinter printer2() {
        return new MemberPrinter();
    }

    @Bean
    public MemberInfoPrinter infoPrinter() {
        MemberInfoPrinter infoPrinter = new MemberInfoPrinter();
        return infoPrinter;
    }
}
빈 이름@Qualifier한정자
printerprinter
printer2mprintermprinter
infoPrinterintoPrinter

기타

Gradle이나 Maven을 이용하여 외부 라이브러리를 프로젝트에 추가하는 행위도 의존성 주입이라고 부른다.

프로젝트 레벨의 의존성이고, 빌드시점에 의존성을 해결한다.

위 설명은 스프링 컴포넌트에 대한 의존성 주입에 대한 이야기이다.

애플리케이션 내부의 객체 간 의존성이고, IoC컨테이너가 관리하며, 런타임시에 스프링 의존성이 해결된다.


참고

책 - 초보 웹 개발자를 위한 스프링 5 프로그래밍 입문
Claude.ai

profile
성장과 발전에는 열려있고, 안주와 정체에는 닫혀있는 개발자 이영섭입니다.

0개의 댓글