꾸물꾸물 졔의 개발공부
[Spring] 스프링 DI (Dependency Injection) 본문
DI (Dependency Injection)
DI란 스프링뿐만 아니라 객체지향 프로그래밍에서는 통용되는 개념의 의존 관계 주입 기능으로, 객체를 직접 생성하는 것이 아니라 외부에서 생성한 후 주입 시켜주는 방식이다. 여기서 외부는 스프링 컨테이너를 의미한다.
의존이란,
예를 들어 A클래스 내부에서 B 클래스의 메소드를 활용한다고 가정해보자.
만약 B 클래스의 return 타입이 변경되거나, 매개변수가 추가되는 것과 같이 B내부의 변경이 발생한다면 A 클래스도 변경되어야 한다.
이러한 경우를 "A가 B에 의존한다" 라고 표현한다.
Public class A {
private B b = new B();
}
기존에는 의존하는 객체를 new 생성자를 통해 직접 생성했다면, 이는 강한 결합도를 가지는 구조이다.
의존성 주입을 통해 외부 컨테이너에서 관리하고 생성한 객체를 인터페이스를 통해서 넘겨받아 주입시켜 사용하게 된다. 이렇게 하면 결합도를 낮출 수 있고, 런타임시에 의존관계가 결정되기 때문에 유연한 구조를 가진다.
즉 ! DI(의존성 주입)을 통해서 모듈 간의 결합도를 낮추고, 유연성을 높일 수 있다.
DI를 주입하는 방식은 총 3가지가 있다.
- Field Injection (필드 주입)
- Setter Injection (수정자 주입)
- Constructor Injection (생성자 주입)
1. Field Injection (필드 주입)
Field Injection은 의존성을 주입하고자 하는 필드에 @Autowired 어노테이션을 붙여주면 의존성이 주입된다.
@RestController
public class TestController {
@Autowired
private TestService testService;
}
2. Setter Injection (수정 주입)
setter 메서드에 @Autowired 어노테이션을 붙여 의존성을 주입한다.
@RestController
public class TestController {
private TestService testService;
@Autowired
public void setTestService(TestService testService){
this.testService = testService;
}
}
위의 두 방식은 런타임시에 의존성을 주입하기 때문에 의존성을 주입하지 않아도 객체가 생성될 수 있다.
3. Constructor Injection (생성자 주입) (*추천)
생성자를 사용하여 의존성을 주입한다.
@RestController
public class TestController{
private final TestService testService;
public TestController(TestService testService){
this.testService = testService;
}
}
생성자 주입의 장점은
- NullPointerException 을 방지할 수 있다.
- 주입받을 필드를 final 로 선언이 가능하다.
[ 생성자 주입을 사용하는 것이 좋은 이유 ]
1. NullPointerException 방지
A와 B 클래스가 아래와 같이 존재한다고 가정하자.
@Component
public class A {
@Autowired
private B b;
public void print(){
b.print();
}
}
@Component
public class B {
public void print(){
System.out.println("Hello");
}
}
new 를 통해 A 클래스에 대한 객체를 만들고 메소드를 실행시켰다.
@SpringBootTest
public class Test{
@Test
void test(){
A a = new A();
a.print();
}
}
→ NullPointerException이 발생한다.
A클래스에서 필드주입을 통해 의존성을 주입했기 때문에 컴파일 시에 A, B 객체가 각각 빈으로 등록되어 생성되고, A에서 B의 의존성은 주입되지 않는다. 이후에 test() 함수에서, 기껏 bean으로 A객체를 등록해놓고 new 생성자를 통해 새로운 a객체를 만들었다. a는 빈으로 등록하지 않은 새로운 객체로 Spring에서 관리하지 않는다. 스프링은 a 를 관리하지 않고 a에 대해 알지 못하므로 b의 의존성을 주입해야 한다는 사실도 모른다. A에서 B는 null인 상태로 유지, null 인 객체 b의 메서드 print()를 호출하려고 하니 NullPointerException 이 발생하게 된다.
if 생성자 주입을 사용했다면, 생성과 동시에 B를 주입시켜주니 위 에러가 발생하지 않을 것이다.
2. 순환참조 방지 ( 순환참조시, 앱 구동 자체가 안됨 )
생성자 주입을 사용하면 순환참조를 방지 할 수 있다.
예를 들어 다음과 같이 필드 주입을 사용해 서로 호출하는 코드가 있다고 하자.
@Service
public class UserService {
@Autowired
private MemberService memberService;
@Override
public void register(String name) {
memberService.add(name);
}
}
@Service
public class MemberService{
@Autowired
private UserService userService;
public void add(String name){
userService.register(name);
}
}
UserService가 이미 MemberService에 의존하고 있는데, MemberService 역시 UserService 에 의존해 있다.
위의 두 메소드는 서로를 계속 호출할 것이고, 끊임없이 호출하다가 결국 StackOverflowError를 발생시키고 죽는다.
필드 주입(field injection)이나 수정자 주입(setter injection) 에서의 순환참조의 문제점은, 실제 코드가 호출되기 전까지 아무것도 알지 못한다. 애플리케이션도 아무 문제없이 구동된다. 그 이유는 bean의 생성과 @Autowired(의존성 주입) 시점이 분리되어 있기 때문이다. @Autowired는 모든 객체가 생성이 완료된 후에 의존관계 주입이 처리됨으로, 객체 생성 시점에는 순환참조가 일어나는지 아닌지 발견할 수 있는 방법이 없다.
반면에, 생성자 주입은 객체의 생성과 의존관계 주입이 동시에 실행되기 때문에 위와 같은 에러를 사전에 잡을 수 있다.
'SPRING' 카테고리의 다른 글
[Spring JPA] JPA란 (0) | 2023.03.23 |
---|---|
[Spring] 스프링 IOC (Inversion of Control) (0) | 2023.03.14 |
[Spring] 스프링 컨테이너란 (0) | 2023.03.09 |
[Springboot] JWT(6) _ 로그아웃 구현/AccessToken 블랙리스트 + RefreshToken 삭제 + Redis (0) | 2022.08.24 |
[Springboot] JWT(5)_ WebClient 통신 (서버 투 서버)에서 JWT 필터 예외 ( 유효 기간 만료 / 올바르지 않은 값 ) 처리하기 (0) | 2022.08.23 |