Spring Framework - DI / Dependency Injection 3가지 방법

블로그를 옮겼습니당·2021년 4월 7일
0
post-thumbnail

시작

Spring에서 주입은 필수적이다. 무의식적으로 쓴다. 보통 개발하면서 필드값을 주입을 할 때, final 예약어를 쓰며 @RequiredArgsConstructor를 사용하며 주입을 한다. 그 밖에도 2가지가 더있다. DI를 공부하다보면 IOC에 대한 얘기는 뺄 수 가 없다. 그렇기에 이번 포스팅은 IoC는 안다는 전제하에 쓸 것이다. 왜 우리는 @RequiredArgsConstructor를 쓰는가?를 알아볼 예정이다.

의존 관계 (Dependency Injection)

나는 항상 생각한다. 하나의 오브젝트가 빈 공간이 있고 주사기로 그 부분을 채워주는 것. 그런식으로 이해를 한다. 너무 추상적이다. 조금 더 개발자적인 마인드로 보자면, 오브젝트는 다른 오브젝트에 주입할 수 있는게 아니고 오브젝트의 레퍼런스가 전달될 뿐이다. DI는 오브젝트 레퍼런스를 외부로부터 제공받고 이를 통해 여타 오브젝트와 다이나믹하게 의존관계를 만들어지는 것이 핵심이다.

의존이라는 것이 굉장히 모호했다. 하지만 어떠한 계기로 그 모호함이 없어졌고 명확해졌다. 물론 더 나아가 결합, 응집에 대한 모호였는데 이번 포스팅은 단순히 의존만을 얘기한다. 그렇기에 쉽다.

B는 아무것도 필요없고 자신만 있으면 온연히 능력을 펼칠 수 있다. 하지만 A 같은 경우에는 B가 있어야 자신의 능력을 펼칠 수 있다. A는 B에 의존하고 있다. B는 아무 누구에게도 의존하고 있지 않다. 즉, A는 B에 영향을 받기에 B의 변화에 민감하다고 얘기할 수 있다.

너무 의존적이지 않을까 ? 조금 더 개선해볼수는 없을까? dependency -> Realization [SOLID - DIP]

하나의 예시이다. Controller가 A 이고 ServiceImplementation이 B 이다. A는 B에 의존하고 있다. 하지만 항상 B가 바뀐다면, A는 매번 B에 따라 바껴야할 것이다. 하지만 만약 B의 껍데기는 그대로 두고 내용물만 바뀌는 것. 즉, 명세는 그대로지만 그 안에 내용만 바뀌는 경우. A는 B를 의존한다고 해서 B에 대한 구체적인 내용을 알지 못한다. 다만, 명세만 알뿐. 그렇기에 내용을 따로 빼고 껍데기만 의존하게 만드는 것이다.

그림과 같이 중간에 Interface라는 껍데기를 두고 의존. 그리고 ServiceInterface와 Impl의 Realization 관계. 의존 전체적인 부분을 개선할 수는 없겠지만 느슨하게 의존을 만들 수는 있다. 개선이다.

Realization 관계같은 경우도 '이게 또 의존이 생기네?' 라는 생각을 했지만 오히려 인터페이스와 그를 상속한 구현체의 관계 매우 명확하기 때문에 오히려 응집도가 높아진다고 생각한다. 또한 여기서 DIP에 대한 개념이 들어간다.

  1. 상위 모듈은 하위 모듈에 의존해서는 안된다.
  2. 추상화는 세부 사항에 의존해서는 안된다.

즉, 변화하지 않는 것에 의존하라는 것인데 확장성에 용이하다는 것이 장점이다.
SOLID - DIP에 대한 내용이다. GGOMJA_BLOG

오브젝트의 레퍼런스 전달

의존성 전달 방법은 3가지가 있다. 생성자를 이용한 전달, 멤버 필드로 전달, Setter 메서드 를 이용한 전달이 있다.

생성자를 이용한 전달

@RequiredArgsConstructor
@Controller
public class Controller {

    private final Service service;
}

Circular Dependency을 알아 차릴 수 있다는 것이 가장 큰 장점이며 개발을 할 때, 지향을 해야하는 이유이다. 또한, 의존성 전달이 필요한 필드를 final로 선언을 하면서 Immutable한 장점을 갖고 있다.

물론, 생성자를 이용한 전달이 순환참조를 100% 막는다는 것은 아니다. 감지를 하면서 앱구동이 실패가 되므로 방지를 할 수있다는 것이다.

멤버 필드로 전달

@Controller
public class Controller {

    @Autowired
    private Service service;
}

@Autowired를 쓰면서 전달을 받는 방식이다. 우선 @Autowired가 deprecated으로 아예 고려를 안하는 것이 좋다. 그리고 또한 SOLID의 SRP에 어긋나는 안티 패턴이다. GGOMJA_BLOG

Setter 메서드를 이용한 전달

@Controller
public class Controller {

    private Service service;

    public void setService(Service service) {
        this.service = service;
    }

}

치명적인 단점이라고 한다면 순환참조를 방지하지 못한다는 것이다. 그러므로 A, B 서로가 참조하고 있다는 경우에 서로를 호출하면서 결국에는 StackOverflowError를 일으키면서 작동이 중지된다.

생성자를 이용한 전달의 이점

final 같은 경우, 한번 DI가 형성되면 의존성이 변경되지 않기 때문에(상수라서) 불변성의 장점이 있다. 하지만, setter, autowired 와 같은 메서드, 필드로 부터의 전달은 변경할 수 있기에 단점이 있다.

프로젝트의 성질마다 다르다. 의존성이 변해야하는 프로젝트의 경우에는 setter, autowired를 이용하여 전달을 해야한다면, 그렇지 않은 경우에는 final을 이용한 생성자를 이용한 전달을 써야한다.

직접 해보기


CycleTest1,2를 만들어서 @RequiredArgsConstructor를 이용하여 각 필드를 전달받았다.
실행시켜보자.


실행이 되지않는다. 필드 전달과 Setter 메서드를 이용한 전달 같은 경우에는 실행이 되며 그 메소드를 실행시켰을 때 그제서야 비로소 에러를 내뱉는다.

순환 참조가 왜 생성자로 인한 전달에만 ??

멤버 필드, Setter 메서드로 인한 전달은 먼저 bean을 생성 후 어노테이션이 붙은 필드에 해당하는 bean을 찾아 주입하는 방법이다.

반면,

생성자를 이용한 전달은 생성자로 객체를 생성하는 시점에 필요한 bean을 주입한다.

생성자의 인자에 사용되는 bean을 찾거나 beanFactory에 만든다. 그 후 찾은 인자 bean으로 주입하려는 bean의 생성자를 호출한다.

즉, 먼저 빈을 생성하지 않는다.

더 나아가 SRP 안티패턴에 대한 나의 생각

찾아보면 결국에는 필드를 이용한 전달은 SRP를 위반하는 안티패턴이라는 것이 보편적인 의견이다. 근데 과연 생성자를 이용한 전달 방법은 SRP를 위반하는 안티패턴이 아닌가? 아는 똑같다고 본다. 이유는 @RequiredArgsConstructor를 이용하여 final로 전달받을 객체를 여러개. 아주 여러개를 전달 받으면 그 또한 여러 책임을 껴안게 되는 SRP를 위반하는 안티패턴이라 생각한다.

그리고 또 하나의 의견으로는 아예 생성자를 만들어서 전달받는 분도 계신데 이것 또한 인지만 하고 사용하는 것일 뿐 딱히 안티패턴이 아니다 라고 말할 수는 없는 것같다.

좀 더 공부해야할 것

생성자를 이용하여 순환되는 것을 감지하는 것은 Compile Time에서 체크한다는 것.
최대한 compile time에서 필수적인 체크를 끝내야한다. ( generic을 공부하다보면 알 수 있음 )
Compile Time과 Runtime에서의 각각의 장단점을 좀 더 공부해보자.

끝내며

매우 좋은 시간이었다. 물론, 처음에 정리하고자 토비의 스프링책을 꺼내고 여러 구글링하는 것이 귀찮았다. 하지만 정리를 하면 할수록 의문점이 있었으며 그 부분을 생각을 하면서 조금씩 풀고 나의 생각이 정해지면서 나름 재미가 있었다. 요즘 느끼는 것은 알고 쓰는 것모르고 쓰는 것은 굉장히 큰 차이라 생각한다. 여기서 내가 말하는 차이'코딩을 하는 즐거움' 이다. 모르면서 '어? 되네 그냥 이렇게 하자.' 하며 그냥 코딩하는 것, 머리에 논리적인 그림이 그려지면서 수학처럼 풀어가는 그 과정을 거치면서 코딩하는 것. 재밌겠다.

profile
https://github.com/minyul

0개의 댓글