[Spring] 생성자 주입을 사용해야 하는 이유

kty.log·2022년 4월 3일
4

요즘 스프링 부트로 예제를 하다가 문득 내가 쓰는 코드를 설명 할 수 있나? 라는 생각이 들어 그냥 생각없이 썻던 이코드들의 이용을 알아보던중 @Autowired를 지양하며 보통 @RequiredArgsConstructor 을 사용하고 있다는 현업자의 조언에 따라 이유가 궁금해 졌다 내가 알아본 이유에 대해 공부해보자

@Autowired

국비 지원 Spring프로젝트 에서 Controller를 작성할 때, 저는 항상 @Autowired 어노테이션을 사용해 Service의 의존성을 주입하고, 하나의 객체에서 주입 한 객체를 사용하곤 했다.

코드를 분석 하는중 현업 개발자가 말을 했다 왜 이 어노테이션을 사용했나요? 라고 질문을 했다 난 그냥 벙쪄버렷다 그냥 배운대고 이 어노테이션을 씀으로 맞는 Bean을 찾아 넣어주는 용도로 밖에 생각을 안했다.

그러고 돌아오는 말은 @Autowired 어노테이션 없이 서비스 객체를 주입받아 사용 할수 있는 스프링이 권장하는 방식이 있다고

@RequiredArgsConstructor 어노테이션을 공부해 보라고 하셧다.

다양한 의존성 주입 방법

Spring Cordml 기능중 얼마전에 알아봤던 DI와 Ioc란 기능이있다.
객체를 사용자가 new 키워드를 통해 생성하고, 소멸시키는 과정이 필요없이 의존성 주입(DI)을 해 Spring 컨테이너가 Bean들이 생명 주기를 관리해 주는 기능을 (Ioc)라고 한다.

이 기능을 사용하려면 개발자가 생성한 객체를 Bean으로 등록하고 사용해야한다.
Bean 등록은 어노테이션을 사용한 자바 코드, Xml파일 등 다양한 방법이 있다.
의존성 주입을 통해, 빈 컨테이너에서 특정 빈을 필요로 하는 클래스에 빈과 클래스 간의 의존성을 주입할수있다.

필드 주입

@RestController
public class HelloController {

    @Autowired
    private HelloRepository helloRepository;
    
    @Autowired
    private HelloService helloSerivce;
}

필드 주입은 @Autowired 어노 테이션을 이용해 객체를 주입 하는 방법이다.
코드가 간경하다는 장점이 있다.

수정자 주입(메서드 주입)

@RestController
public class HelloController {

    private HelloRepository helloRepository;
    private HelloService helloService;

    @Autowired
    public void setHelloRepository(HelloRepository helloRepository) {
        this.helloRepository = helloRepository;
    }

    @Autowired
    public void setHelloService(HelloService helloService) {
        this.helloService = helloService;
    }
}

수정자 주입은 Setter, 혹은 사용자 정의 메서드를 통해 객체 의존 관계를 주입하는 방식이다.

생성자 주입

@RestController
public class HelloController {

    private HelloRepository helloRepository;
    private HelloService helloService;

    @Autowired
    public HelloController(HelloRepository helloRepository, HelloService helloService) {
        this.helloRepository = helloRepository;
        this.helloService = helloService;
    }
}

생성자 주입은 위 코드 처럼 사용할 의존 객체를 생성자를 통해 주입 받는 방식이다.
(지금 이 코드의 생성자에 선언되어있는 @Autowried 어노테이션은 생성자가 한개만 있을 경우 생략이 가능하다.)

왜 생성자 주입을 사용해야 할까?

난 여태 의존성을 주입할때 @Autowired 어노테이션을 통해 코드도 깔끔하고, 매우 편하게 의존성을 주입하고 있엇다. 하지만 이방법은 어디에서도 추천되지 않는 방법인 것을 이번 공부를 통해 배웠다
검색을 통해 본거지만 내가 사용해보진 못한 인텔리 제이에서도 이런 경고 메시지를 띄운다고 한다.

또한 Spring 프레임 워크에서도 생성자 주입 사용을 강력하게 권장하고 있다고한다.

생성자 주입이 권장되는 이유는 여러가지가 있다.

  • 필드 주입의 단점
  • 수정자 주입의 단점
  • 객체의 불변성 확보
  • 테스트 코드 작성의 편리함
  • 순환 참조 방지
  • 개발자의 의존성 주입 실수 방지(final이란 키워드 사용)
  • 필드 주입의 단점

    필드 주입을 사용하면 편리하고 코드가 간결하지만 필드 주입으로 객체를 주입하면 외부에서 수정이 불가능하고, 이러한 이유로 테스트 코드 작성시 객체를 수정할 수 없다.
    필드 주입은 반드시 Spring 같은 DI를 지원하는 프레임 워크가 있어야 사용할 수 있다.
    필드 주입은 @Autowried 어노테이션으로 주입된 클래스를 생성자 주입 방식으로 리팩토링 한다고 하면, 생성자의 매개벼수는 엄청나게 많아진다. 해당 객체의 생성자 매개변수가 많아진다는것은 의존성, 결함의 대한 문제가 발생할 가능성이 높아지고 , 해당객체의 역활이 많아져 단일 채임 원칙에 위배될수있다.
    필드 주입으로 의존성 주입 시 final 키워드를 통한 선언이 불가능하고 객체가 변하기 쉬워진다.

    수정자 주입의 단점

    Setter의 경우 public으로 구현하기 때문에, 관게를 주입 받는 객체의 변형 가능성을 열어둔다.
    이런 이유 때문에, 수정자 주입 방식을 주입받는 객체가 변경될 필요성이 있을때만 사용한다.
    (주입하는 객체를 변경할 경우가 굉장이 드물긴 하다고한다.)

    객체의 불변성 확보

    객체의 생성자는, 객체 생성 시 1회만 호출된다는 게 보장되는 특징이 있다.
    이 특징 덕분에 주입받은 객체가 불변 객체여야 되거나, 반드시 해당 객체의 주입이 필요한 경우에 사용한다.
    위 코드를 예로 들자면, HelloController가 사용하는 HelloRepository와 HelloService를 변경하는 코드는 HelloController 생성자 밖에 없는데 즉, 생성자로 한번 의존 관계를 주입하면, 생성자는 다시 호출될 일이 없기 때문에 불변 객체를 보장한다.
    그리고 HelloRepository와 HelloService를 생성자로 주입을 하면, HelloController가 생성되는 시점에 무조건 객체가 생성되어 주입된다는 게 보장된다.

    테스트 코드 작성의 편리함

    메인 코드가 필드 주입으로 작성된 경우, 순수 자바 코드로 단위 테스트를 실행하는 것은 불가능하다.
    메인 코드는 Spring 같은 DI 프레임워크 위에서 동작하지만, 테스트 코드는 그렇지 않아서 의존관계 주입이 null상태여서 NullPointError이 발생한다.

    순환 참조 에러방지

    순환 참조 에러는 A객체가 B객체를 참조하고, B객체가 A객체를 서로 참조하고 있을 때 발생한다.

    @Service
    public class AService {
    
        @Autowired
        private BService bService;
    
        public void hello() {
            bService.hello(); //AService 객체가 BService 메서드 호출
        }
    }
    @Service
    public class BService {
    
        @Autowired
        private AService aService;
    
        public void hello() {
            aService.hello(); //AService 객체가 BService 메서드 호출
        }
    }

    위 코드처럼 A 서비스의 hello() 메서드는 B객체를 참조하고, B 서비스의 hello() 메서드는 A객체를 참조한다.

    애플리케이션을 실행하고, AService에서나, BService에서 hello() 메서드를 호출하면, 서로 메서드를 호출하다 결국 StackOverFlow에러가 나서 애플리케이션이 다운되며 여기서 주목할 점은 이런 에러가 애플리케이션이 실행 중에 발생 한다는 점이다..
    즉, 어플리케이션이 실행되고 있을 때, AService나 BService의 hello() 메서드가 호출되지 않으면 에러는 발생하지 않고, 개발자는 틀린 부분을 찾을 수가 없다.

    정확히 말하자면, 순환 참조 에러가 아닌, 순환 참조에 의한 순환 호출 에러라고 할 수 있다.
    이 에러는 수정자 주입으로 의존성을 주입해도 동일하게 발생한다,

    @Service
    public class AService {
    	
        private BService bService;
        
        @Autowired
        public AService(BService bService) {
        	this.bService = bService;
        }
        
        public void hello() {
        	bService.hello(); //AService가 BService 메서드 호출
        }
    }
    @Service
    public class BService {
    	
        private AService aService;
        
        @Autowired
        public BService(AService aService) {
        	this.aService = aService;
        }
        
        public void hello() {
        	aService.hello(); //BService가 AService 메서드 호출
        }
    }

    이 애플리케이션을 실행하면, 실행이 되지 않고 순환 참조 문제가 있다며 예외가 발생하며,
    이렇게 애플리케이션을 실행하기 전에 발생하는 컴파일 에러는 좋은 에러이다.

    즉, 생성자 주입 방식을 사용하면 순환 참조 문제를 해결하는 것이 아니라, 순환 참조 문제를 애플리케이션 실행 시점에 알려줘서, 실제 서비스되기 전에 개발자로 하여금 순환 참조 문제를 해결할 수 있게 해 준다.

    이러한 이유로 인해, Sprin에서 객체 의존성 주입은 생성자 주입 방식을 사용하는 것이 권장된다.

    0개의 댓글