[Spring] 의존 관계 주입이 뭐지..?🤔

김민제·2024년 3월 27일

Spring🌱

목록 보기
5/8
post-thumbnail

의존 관계 주입이란?

  • Spring은 객체의 의존 관계를 의존 관계 주입을 통해 관리합니다.
    DI는 객체를 직접 생성하는 것이 아니라 외부에서 생성 후 주입시켜 주는 방식이라 할 수 있습니다.
    이러한 방식의 장점은 DI를 통해 객체 간의 관계를 동적으로 주입하여 유연성을 확보하고 결합도를 낮출 수 있다.

의존 관계 주입의 종류

  • 의존 관계 주입의 종류는 다음과 같습니다.
  1. 생성자 주입
  2. 세터 주입
  3. 필드 주입

(일반 메서드 주입도 있지만 세터 주입과 비슷하여 적지 않겠습니다!)

의존 관계 주입

생성자 주입(Constructor Injection)

  • 생성자 주입은 생성자를 사용하여 의존 관계를 주입해주는 방법입니다!
  • 현재 가장 권장되는 방법이며 여러 방법으로 코드를 간결하고 가독성있게 만들 수 있습니다!!
@Component
public class ConDi{

		private Service1 service1;
		private Service2 service2;
		
		@Autowired
		public ConDi(Service1 service1, Service2, service2){
				this.service1 = service1
				this.service2 = service2
		}
}
  • 생성자를 호출할 때 스프링 컨테이너에서 Service1과 Service2의 스프링 빈을 꺼내어 주입해줍니다.
  • 생성자 호출 시점에 1번만 호출되는 것을 보장합니다.
    • 초기에 할당되기 때문에 NullPointException이 발생하지 않습니다!
  • 생성자가 1개만 있다면 @Autowired를 생략해도 자동으로 의존 관계를 주입해줍니다.
  • 불변적이고 필수적인 값에 사용합니다.
  • 대부분의 의존 관계는 앱 종료 시점까지 불변해야하기 때문에 기본적으로 생성자 주입을 권장합니다.

→ 항상 생성자 주입을 사용하자

생성자 주입 + lombok

  • 생성자 주입과 여러 기능을 제공해주는 lombok의 생성자 생성 어노테이션을 사용하면 더 간결한 코드를 구현할 수 있습니다.
  • 위 생성자가 1개만 있다면 @Autowired를 생략해도 자동으로 의존 관계를 주입해준다라는 방식과 결합된 것인데요. 코드 예시는 다음과 같습니다.
@Component
@RequiredArgsConstructor
public class ConDi{
		private final Service1 service1;
		private final Service2 service2;
}
  • 우선 @RequiredArgsConstructor는 초기화 되지않은 final 필드나, @NonNull 이 붙은 필드에 대해 생성자를 생성해주는 어노테이션인데요.
  • 위 코드에서 @RequiredArgsConstructor를 보면 아래와 같은 생성자를 생성해준다고 알 수 있습니다.
public ConDi(Service1 service1, Service2, service2){
				this.service1 = service1
				this.service2 = service2
}
  • 그리고 생성자가 한 개만 있으면 @Autowired를 생략해도 자동으로 의존 관계를 주입해준다고 했으니 위 생성자에는 자동으로 @Autowired어노테이션이 붙어 의존 관계가 주입되겠네요!
  • 이렇게 코드를 간결하게 줄일 수도 있습니다!
  • 단 아래와 같이 생성자 주입 어노테이션을 한 개 더 붙이거나 생성자를 따로 만들면 아래와 같은 오류가 발생합니다!
@Component
@RequiredArgsConstructor
@AllArgsConstructor
public class ConDi{
		private final Service1 service1;
		private final Service2 service2;
}

==>Class doesn't contain matching constructor for autowiring 
  • spring이 어떤 생성자로 의존 관계 주입을 해줘야할지 몰라서 생기는 오류인데요. 이렇게 여러 개의 생성자를 생성해야하는 상황이라면 생성자를 직접 만들어주고 의존 관계 주입을 해줄 생성자 앞에 수동으로 @Autowired를 붙여줘야합니다!

→ 생성자 주입을 사용하자

세터 주입(Setter Injection)

  • 세터 주입은 세터 메소드를 통해 객체의 의존 관계를 주입해주는 방법입니다.
  • 현재는 권장하지 않는 방법입니다!!
@Component
public class ConDi{

		private Service1 service1;
		private Service2 service2;
		
		@Autowired
		public void setService1(Service1 service1){
				this.service1 = service1;
		}
		
		@Autowired
		public void setService2(Service2 service2){
				this.service2 = service2;
		}
		
		/*
		public ConDi(Service1 service1, Service2, service2){
				this.service1 = service1
				this.service2 = service2
		}
		*/
}
  • 스프링 빈은 싱글톤을 보장하기 때문에 생성자의 service1과 setter의 service1은 똑같은 객체가 주입됩니다.
  • 생성자로 객체가 초기화된 이후 세터메소드가 실행되기 때문에 final 키워드를 사용할 수 없습니다.
    • final 키워드를 사용하면 “Variable 'service1' might not have been initialized” 오류 메시지를 뱉어냅니다..!!
  • 세터 주입으로 의존관계 주입을 안해주고 객체를 사용하려 한다면 NullPointException이 발생할 수 있습니다.
    @Component
    public class ConDi{
        private MemberService service1;
    
        public void testMethod(){
            service1.findMembers();
        }
    
        @Autowired
        public void setService1(MemberService service1){
            this.service1 = service1;
        }
    }
    
    public class HelloController {
        public static void main(String[] args) {
            ConDi conDi = new ConDi();
            conDi.testMethod();
        }
    }
    
    -> Exception in thread "main" java.lang.NullPointerException
  • 선택적이고 변경 가능한 값에 사용합니다.
    • 기본적으로 생성자 주입을 사용하고 변경 가능한 값이라면 세터 주입을 열어놓는 것도 하나의 방법입니다.
  • 세터 주입은 세터 메서드를 public으로 열어두어야하기 때문에 좋은 방법이 아닙니다!!!
  • 가능한 생성자 주입을 사용합시다!!

필드 주입(Field Injection)

  • 필드 주입은 필드를 통해 의존 관계를 주입하는 방법입니다.
  • 가장 간단하지만 참조 관계를 눈으로 확인하기 어렵기 때문에 사용이 권장되지 않습니다!!
  • 사용하려하면 “Field injection is not recommended “같은 메시지를 띄워줍니다!
@Component
public class ConDi{

		@Autowired
		private Service1 service1;
		
		@Autowired
		private Service2 service2;
		
}
  • 외부에서 변경이 불가능하기 때문에 테스트하기가 매우 어렵다는 단점이 있다.
    • 테스트를 실행할 때는 순수한 자바 코드로 실행되어 필드가 주입되지 않기 때문에 NullPointException이 발생한다.
  • 생성자 초기화 후 실행되므로 final 키워드를 사용할 수 없습니다!
    • final 키워드를 사용하면 “Variable 'service1' might not have been initialized” 오류 메시지를 뱉어냅니다..!!
  • 저는 테스트 코드를 작성할 때 가끔 사용합니다!!
  • 가능한 생성자 주입을 사용합시다!!

순환 참조 문제

  • 순환 참조 문제는 두 클래스에서 각자에 대한 의존관계 주입을 필요로 하여 스프링 컨테이너가 빈을 찾는 과정을 무한히 반복되게 하는 문제입니다.
  • 세터 주입은 순환 참조에 대한 방어가 되지 않는다는 단점이 있었지만 스프링 3.x 버전에서는 컴파일 시 오류를 잡아주어 방어가 됩니다!!
  • 예를 들어 아래와 같은 코드가 있다고 생각해봅시다.
    @Service
    public class Service1 {
    
        @Autowired
        private Service2 service2;
    }
    
    @Service
    public class Service2 {
    
        @Autowired
        private Service1 service1;
    }
    • Service1의 의존관계 주입이 필요할 때 스프링 컨테이너는 Service2의 Bean을 찾게 될 것입니다. 하지만 Service2의 Bean을 생성하려면 또 Service1이 필요한 문제가 있습니다.
  • 스프링 3.x 버전 이상부터는 세터 주입과 생성자 주입에서도 앱 구동 시점에서 순환 참조 문제를 알려주지만 위에서 말했듯 생성자 주입을 사용하는 이점이 많기 때문에 최대한 생성자 주입을 사용합시다!!
    • 세터 주입 순환 참조 문제 컴파일 시 에러 발생
      @Service
      @RequiredArgsConstructor
      public class Service1 {
          private Service2 service2;
      
          @Autowired
          public void setService2(Service2 service2){
              this.service2 = service2;
          }
      
          public void runService1(){
              service2.runService2();
          }
      }
      
      @Service
      public class Service2 {
          private Service1 service1;
      
          @Autowired
          public void setService1(Service1 service1){
              this.service1 = service1;
          }
      
          public void runService2(){
              service1.runService1();
          }
      }
      
      ***************************
      APPLICATION FAILED TO START
      ***************************
      
      Description:
      
      The dependencies of some of the beans in the application context form a cycle:
      
         conDi defined in file [/Users/minje/jpashop/build/classes/java/main/com/jpabook/jpashop/ConDi.class]
      ┌─────┐
      |  service1
      ↑     ↓
      |  service2 (field private com.jpabook.jpashop.service.Service1 com.jpabook.jpashop.service.Service2.service1)
      └─────┘

의존 관계 주입 옵션

  • 스프링 컨테이너에 주입할 스프링 빈이 없이도 코드가 동작해야 할 때가 있을 수 있습니다. 하지만 기본적인 Autowired의 required 옵션은 true이기 때문에 주입할 스프링 빈이 없으면 에러가 발생합니다.
  • 자동 주입 대상이 없어도 문제없이 작동하기 위한 방법은 다음과 같습니다!!

@Autowired(required = false)

@Autowired(required = false)
public void setNoBean(@Nullable Member member){}
  • 의존 관계가 없다면 메소드 자체를 호출하지 않습니다.

@Nullable

@Autowired
public void setNoBean(@Nullable Member member){}
  • 메소드가 호출은 되지만 member가 null로 들어오게 됩니다.

Optional

@Autowired
public void setNoBean(Optional<Member> member){}
  • member가 없다면 Optional.empty로 감싸서 member에 넣어줍니다.

결론

  • 오늘은 의존관계 주입에 대하여 알아보았습니다!!
  • 스프링 가이드에서도 세터 주입과 필드 주입은 지양하고 생성자 주입을 사용한 개발을 권장하고 있으니 생성자 주입을 사용한 개발을 하도록 합시다!!!
profile
블로그 이전했습니다!! 👉 https://alswp006.github.io/

0개의 댓글