Spring

IoC / DI

후후후하하하 2024. 5. 9. 17:06

DI (Dependency Injection)

  • 외부에서 두 객체 간의 관계를 결정해주는 디자인 패턴
  • 인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않도록 하고 런타임 시에 관계를 동적으로 주입하여 유연성을 확보하고 결합도를 낮춘다.
  • 스프링은 특정 위치부터 클래스를 탐색하고, 객체를 만들며 객체들의 관계까지 설정해준다. = 제어의 역전(IoC)
  • 어떠한 객체를 사용할지에 대한 책임은 프레임워크에게 넘어갔고, 자신은 수동적으로 주입받는 객체를 사용.
  • 한 객체가 어떤 객체에 의존할 것인지는 별도의 관심사이다.
  • 스프링은 의존성 주입을 도와주는 DI 컨테이너로써, 강하게 결합된 클래스들을 분리하고, 애플리케이션 실행 시점에 객체 간의 관계를 결정해줌으로써 결합도를 낮추고 유연성을 확보해준다. = 상속보다 훨씬 유연하다. 단 한 객체가 다른 객체를 주입받으려면 반드시 DI 컨테이너에 의해 관리되어야 한다.
  • 두 객체 간의 관계라는 관심사의 분리
  • 두 객체 간의 결합도를 낮춤
  • 객체의 유연성을 높임
  • 테스트 작성 용이

DI안하면,

  1. 두 클래스가 강하게 결합되는 문제
  2. 객체들 간의 관계가 아니라 클래스 간의 관계가 맺어짐
    1. 객체들 간 관계가 맺어졌다면 다른 객체의 구체 클래스를 전혀 알지 못하더라도 해당 클래스가 인터페이스를 구현했다면 인터페이스의 타입으로 사용할 수 있다.

즉, 관심사의 분리가 중요하다. = DI 적용해야함.

 

 

다양한 의존성 주입 방법

  • 생성자 주입
    • 생성자의 호출 시점에 1회 호출 되는 것이 보장된다.
    • 주입받은 객체가 변하지 않거나, 반드시 객체의 주입이 필요한 경우에 강제하기 위해 사용할 수 있다.
  • 수정자 주입
    • 주입받는 객체가 변경될 가능성이 있는 경우에 사용된다. (실제로 변경이 필요한 경우는 극히 드물다.)
  • 필드 주입
    • 필드에 바로 의존 관계를 주입하는 방법
    • 외부에서 접근이 불가능하다는 단점 존재. 테스트 코드의 중요성이 부각됨에 따라, 객체를 수정할 수 없는 필드 주입은 거의 사용되지 않는다.
    • 또한 필드 주입은 반드시 DI 프레임워크가 존재해야 하므로 사용을 지양해야 한다.

 

 

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

  1. 객체의 불변성 확보
    1. 개발 중 의존 관계의 변경이 필요한 상황은 거의 없다. 하지만 수정자 주입이나 일반 메서드 주입을 이용하면 불필요하게 수정의 가능성을 열어두어 유지보수성을 떨어뜨린다. 그러므로 생성자 주입을 통해 변경의 가능성을 배제하고 불변성을 보장하는 것이 좋다.
  2. 테스트 코드의 작성
    1. 테스트가 특정 프레임워크에 의존하는 것은 침투적이므로 좋지 못하다. 순수 자바로 테스트를 작성하는 것이 가장 좋은데, 생성자 주입이 아닌 다른 주입으로 작성된 코드는 순수한 자바 코드로 단위 테스트를 작성하는 것이 어렵다.
    2. 테스트 코드에서 @Autowired를 사용하기 위해 스프링을 사용하면 단위테스트가 아닐 뿐만 아니라, 컴포넌트들을 등록하고 초기화하는 시간 때문에 테스트 비용이 증가한다. 그렇다고 대안으로 리플렉션을 사용하면 깨지기 쉬운 테스트가 된다.
    3. 반면 생성자 주입을 사용하면 컴파일 시점에 객체를 주입받아 테스트 코드를 작성할 수 있으며, 주입하는 객체가 누락된 경우 컴파일 시점에 오류를 발견할 수 있다.
  3. final 키워드 작성 및 Lombok과의 결합
    1. 생성자 주입을 사용하면 필드 객체에 final 키워드를 사용할 수 있으며, 컴파일 시점에 누락된 의존성을 확인할 수 있다. 반면에 다른 주입 방법들은 객체의 생성 이후에 호출되므로 final 키워드를 사용할 수 없다.
    2. 또한 final 키워드를 붙이면 Lombok과 결합되어 코드를 간결하게 작성할 수 있다.
  4. 스프링에 비침투적인 코드 작성
    1. 필드주입 하려고 @Autowired를 사용하면 클래스 import에 스프링 의존성이 침투한다.
    2. 우리가 사용하는 프레임워크는 언제 바뀔지도 모를 뿐만 아니라, 사용자와 관련된 책임을 지는 UserService에 스프링 코드가 박히는것은 바람직하지 않다. 프레임워크는 서비스 계층에서 알아야 할 대상이 아니다. 생성자 주입을 그래서 사용하자. @Autowired 안써도 되니까.
  5. 순환 참조 에러 방지
    1. 생성자 주입을 사용하면 애플리케이션 구동 시점(객체의 생성 시점)에 순환 참조 에러를 예방할 수 있다.
    2. 생성자 주입을 이용하면 애플리케이션 구동 시점(객체의 생성 시점)에 에러가 발생한다. 이유는 Bean에 등록하기 위해 객체를 생성하는 과정에서 다음과 같은 순환 참조가 발생하기 때문이다.
    3.  
    4. new UserService(new MemberService(new UserService(new MemberService()...)))
    5.  필드주입에서 이러한 문제가 구동 시점에 에러가 발생하지 않는 이유는 빈의 생성과 조립 시점이 분리되어 있기    때문이다. 생성자 주입은 객체의 생성과 조립이 동시에 실행되다 보니 위와 같은 에러를 사전에 잡는다. 하지만 필드 주입은 모든 객체의 생성이 완료된 후에 조립이 처리된다. 그러다보니 위와 같이 호출이 되고 나서야 순환 이슈를 확인할 수 있는 것이다.

스프링부트 2.6부터는 순환 참조가 기본적으로 허용되지 않도록 변경되었다. 

 

참조 : https://mangkyu.tistory.com/125

https://mangkyu.tistory.com/150

https://mangkyu.tistory.com/151

https://mangkyu.tistory.com/210

https://mangkyu.tistory.com/155