의존성 주입의 종류와 필드 주입의 문제점

굿거리·2023년 5월 3일
0

의존성 주입은 클래스 간 강한 결합을 풀어주어 조금 더 OOP스러운 개발을 할 수 있도록 만들어준다. 이는 Spring뿐만 아닌 객체지향적인 개발을 할 때 두루 사용된다.

의존성 주입 방식에는 다음의 3가지가 있다.

  • 필드 주입(Field Injection)
  • 수정자 주입(Setter Injection)
  • 생성자 주입(Constructor Injection)

다음과 같은 Service 클래스가 있다. 이를 컨트롤러에서 이용하려고 한다.

@Service
public Class MemberService {
	...
}

스프링에서 의존성 주입을 설정하는 방법에는 xml을 이용한 방식과 @Autowired 애노테이션을 사용하는 방식이 있다. 이번 포스팅에서는 직관성을 올리기 위해 @Autowired 애노테이션을 사용하려고 한다.

필드 주입(Field Injection)

@Controller
public Class MemberController {
	
    @Autowired
    MemberService service;
}

필드 주입은 Service를 사용하려는 Controller의 필드에 직접 @Autowired를 부여하는 방식이다. 가장 코드가 짧고 주입하기도 간편하고 보기에도 직관적이지만, 현재 가장 경계시되는 방식이다. 실제로 IntelliJ에서는 필드 주입을 사용하면 "Field Injection is not recommended"라는 경고문을 띄우고, 스프링 공식 레퍼런스에는 소개조차 되지 않는다. 그 이유는 무엇일까?

  • Hiding Dependencies

의존성이란 뒤에서는 체결되어있어도 직접 눈으로 보이지 않으면 문제가 있다. 강한 결합이 객체 지향적 개발에서는 좋지 않지만 드러나지 않는 것과는 다르다.
Controller에 Service를 넣는다는 어떠한 코드도 없지만 실행하는 데에 문제는 없다. 결국 이 책임 소재가 다른 곳에 있다는 것이다. 실제 시행 시에는 컨테이너가, 테스트 코드를 짤 때는 개발자가 될 것이다. 이런 필드 주입이 하나 둘 쌓이게 되면 나의 어플리케이션 전체의 책임 소재가 명시적으로 드러나지 않아 디자인을 해치게 된다.

  • final을 사용하지 못해 불변성을 보장받지 못함

Field Injection을 사용하면 final 제어자를 사용하지 못한다. 따라서 중간에 service 객체가 변경될 수도 있다는 뜻이다. 이는 런타임 중에 service의 불변성을 보장받을 수 없게 만든다.

  • 프레임워크와의 결합성

필드 주입은 정확히 어떤 방식으로 결합되어있는지 자바로 명시되어있지 않다. 따라서 프레임워크 혹은 컨테이너 자체에 생성과 소멸을 의존하게 된다. 느슨한 결합을 위해 사용한 의존성 주입이 또다른 강한 결합을 만드는 아이러니한 상황이 발생하게 된다.

  • 테스트 환경에서의 문제점

테스트 코드를 짜는 데에 있어서도 불편함이 있다. 필드 주입은 달랑 Service 선언 위에 @Autowired 애노테이션이 달려있는 구조이기 때문에, 스프링의 도움을 받지 않으면 객체를 생성하기 어렵다. 따라서 테스트를 하려면 프로젝트 전체를 돌려야 한다. 이는 무거운 프레임워크를 피해 자바 코드만으로 가동하려는 테스트의 의미를 무색하게 만든다.

  • 순환 의존성 관련

순환 의존이란 서로 다른 클래스가 서로를 참조하게 되는 상황이다. Class A가 Class B의 객체를 이용하고 Class B가 Class A의 객체를 이용하게 되면, 서로의 객체가 생성되지 않아 콜스택이 쌓이다 결국 오버플로우를 발생시킨다.
이는 다른 의존 주입을 통해서도 발생할 수 있지만 Field Injection은 의존하고 있는 객체가 생성되지 않아도 의존받는 객체가 생성될 수 있어 컴파일 단계에서는 문제가 없다. 결국 런타임 단계까지 가서 순환 참조를 잡아낼 수 있기 때문에 매우 곤란해질 수 있다.

수정자 주입(Setter Injection)

@Controller
public Class MemberController {
	
    MemberService service;
    
    @Autowired
    public void setMemberService(MemberService service) {
    	this.service = service;
    }
}

데이터 구조에서의 Setter처럼 외부의 서비스를 가져와 세팅하는 형식의 주입 방법이다. 과거에는 Setter Injection을 많이 추천하고 사용했지만, 현재는 잘 사용하지 않는다.
Setter Injection은 Field Injection에 비해 컨테이너와의 결합도는 낮아져 테스트 코드를 짤 수 있다는 등의 장점은 있지만, 결국 근본적으로 이 set 메서드를 거치지 않고도 의존당하는 객체가 생성될 수 있다는 점은 극복하지 못한다. 따라서 상기한 Field Injection의 final을 사용하지 못하는 문제, 순환 의존성에 관한 문제 등은 여전히 남아있다.

생성자 주입(Constructor Injection)

@Controller
public Class MemberController {
	
    final MemberService service;
    
    @Autowired
    public MemberController(MemberService service) {
    	this.service = service;
    }
}

생성자 주입은 의존당하는 클래스(Controller)의 생성자에 의존되는 객체(service)를 인자로 넣는 방식이다. 최근 스프링 공식 레퍼런스에서는 Constructor Injection을 추천하고 있다.
Constructor Injection의 핵심이자 다른 주입 방식과의 가장 큰 차이점은 바로 의존중인 객체가 하나라도 없으면 만들어지지 않는다는 점이다. 이는 NullPointerException을 막아주고 컴파일 단계에서 미리 캐치할 수 있게 해준다. 순환 참조도 컴파일 단계에서 알 수 있다.
final 지시자를 사용해 불변성을 보장받을 수 있고, Lombok의 @RequiredArgsConstructor 애노테이션도 사용할 수 있다. 또한 테스트 코드도 개발자가 자바로 작성이 가능하다.

profile
개발자를 향해

0개의 댓글