Spring에서 중요한 의미를 가지고 있는 IoC 와 DI를 쉽게 이해해보자.
직역하면 "제어의 역전" 이다.
일단 나같은 대한외국놈은 제어의역전? 듣자마자 머라는겨 소리가 바로 나옴
대체 이놈이 무엇인지 함께 이해해보자
우리가 자바를 사용해서 객체를 생성할때 아래와 같이 객체가 필요한 곳에서 객체를 직접 생성하는 방법을 사용했을 것이다.
Public class A {
b = new B(); // new 키워드로 B 객체 생성
}
스프링에서 말하는 제어의 역전이란, 위처럼 객체를 직접 생성하거나 제어하는 게 아니라 외부에서 관리하는 객체를 가져와 사용하는 것 을 말한다.
오.. 내가 직접 안하고 다른데서 가져와 쓴다? 아직은 이해가 잘안감.
코드로 이해해보자.
Public class A {
private B b; // 어디선가 받아온 객체B를 b에 할당한다.
}
이렇게 IoC를 사용하게 되면 객체를 외부에서 관리하고 있고, 실제로 사용할 때에는 외부에서 제공해주는 객체를 받아오는 거구나 !!! 라고 이해하면 된다.
🤷♂️ 그럼 여기서 계속 언급되는 외부는 어디일까.
👉 우리는 이놈을 스프링 컨테이너 라고 한다.
🤷♂️그럼 스프링 컨테이너에서 해주는 "관리"는 뭐여?
👉 2가지로 나눠봤음.
1. 의존성 주입(DI & DL)
2. 빈의 라이프사이클(생명주기) - 생성, 소멸 관리
아항 그럼 IoC는 스프링 컨테이너에서 관리하는 객체를 가져와 사용하는 개념이구낭! 라고 이해하고 DI 로 넘어가보자.
앞서서 스프링은 IoC 를 사용한다고 했다. DI는 이 IoC를 구현하기 위해 사용하는 방법이다.
자 이놈도 직역해보면 "의존성 주입" 이다.
✔️ 의존성
: 이놈부터 정의해보자면 객체간에 레퍼런스 참조가 되어있다는 말이다.
A가 B에 의존관계에 있을때 B객체에 변경사항이 생기면 A객체가 영향을 받게 된다는 것!!
아니 근데 의존성을 주입한다고? 어떻게???? 라고 생각한다면 다음 코드를 보면서 이해해보자.
Public class A {
@Autowired // A에서 B를 주입받는다
B b;
}
우리가(아니 필자가) 무심코 지나쳤던 @Autowired
이 어노테이션... 이놈은 스프링 컨테이너에 있는 "빈"을 주입하는 역할을 한다.
위에서 말한 IoC 는 외부에서 관리하는 객체를 가져와 사용한다고 했다.
여기서 외부는 스프링 컨테이너고, 스프링 컨테이너에서 관리하는 객체를 빈이라고 한다.
위 코드에서 클래스A 에서 B객체 쓰고싶어~ 하면 스프링 컨테이너가 짠하고 객체 B를 만들어서 준거라고 생각하면 된다.
각 클래스들간 연관 관계를 클래스 자체 내에서 맺어주게 되면, 직접적인 연관관계가 발생하기 때문에 변경에 용이하지 않은(연속적으로 다른 클래스에 영향을 주는), 한마디로 유지보수가 어렵게 된다.
하지만 스프링 자체에서 설정을 통해 연관관계를 맺어주어 의존 관계를 최소화 하게 되면, 각 클래스들의 변경이 자유로워 지게 되어 유지보수가 용이해지는 것!
이걸 느슨한 결합이라고 한다.
의존관계 주입 방법에는 총 3가지가 있다.
Spring 3.x 버전까지만 해도 Setter 주입을 권장하였으나, 최근에는 순환 참조 등의 문제로 인해 Spring 4.3 이후 버전부터는 생성자 주입(Construct Injection) 방법을 권장하고 있다. 이건 마지막에 설명함
일단 요렇게 3가지 방법이 있다.
1. Field Injection(필드 주입)
2. Setter Injection(Setter 주입)
3. Construct Injection(생성자 주입)
스프링에서 빈으로 등록된 객체를 사용할 클래스에 필드로 선언하고 @Autowired
어노테이션을 붙여주어 자동으로 의존관게를 주입하는 방식이다.
@Service
public class MemberService{
@Autowired
private MemberRepository memberRepository;
}
딱봐도 코드가 간결해져서 남발하고 싶은 욕구가 차오른다.
하지만 클래스 외부에서 접근이 불가능해서 테스트하기 어렵다는 치명적인 단점을 가지고 있다. 요즘같이 테스트 코드의 중요성이 강조되는 흐름에는 맞지 않는 방식.
또한, DI 로 작동하니 테스트는 단독적으로 실행되기 때문에 의존관계 주입이 null상태여서 NullPointerException이 발생하게 된다.
님들이 생각하는 그 getter/setter 의 그 setter 맞음.
필드 값을 변경하는 Setter를 통해서 의존 관계를 주입하는 방법이다.
다음과 같이 메서드에 @Autowired
로 빈을 가져와 의존성을 주입한다.
@Service
public class MemberService{
private MemberRepository memberRepository;
@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
Setter 주입은 주입받는 객체가 변경될 가능성이 있는 경우에 사용한다.
=> 변경이 일어날 수 있으니 final 제어자를 붙일 수 없다.
이 방식은 외부에서 손쉽게 접근하여 값을 변경할 수 있는 위험성이 있어서, 권장하지 않는다.
생성자를 통해 의존 관계를 주입하는 방법이다.
@Service
public class MemberService{
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
위의 두 주입 방법은 빈이 만들어지고 객체를 가져와 주입하는데, 생성자 주입은 빈이 만들어지는 시점에서 모든 의존관계를 후에 가져와야한다.
이렇게 생성자의 호출 시점에 한번 할당이 된 객체는 외부에서 변경할 수 없다. => final 키워드 사용 가능!! => 불변성을 확보할 수 있다!!
아까 위에서 스프링에서는 생성자 주입을 권장하고 있다고 했다.
지금까지 보았듯이,
✅ 필드 주입에서는 외부 접근이 어려워 테스트 코드 작성시에 어려움이 있고, 필드 주입으로 의존성을 주입하면 final 키워드 사용이 불가능하니 객체가 변경될 수 있는 가능성이 있다.
✅ setter 주입에서도 변경의 가능성이 있다.
✅ 생성자 주입은 불변성이 확보된다. 또한 순환 참조 문제를 애플리케이션 실행 시점(컴파일)에 알려주기 때문에 순환 참조 문제를 방지할 수 있다.
✔️ 순환참조 문제 간단하게 짚고가기
순환 참조 에러는 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 메서드 호출 } }
위와 같은 코드에서 hello()메서드를 호출하면 서로 호출호출호출! 되면서 StackOverFlow에러가 나서 애플리케이션이 다운되는 참사가일어난다.
이런 순환 참조 문제는 에러가 애플리케이션 실행 중에 발생하는 점인데, 생성자 주입은 이 문제를 컴파일 시점에 알려주어 미리 방지할 수 있게 하는 것이다.