소프트웨어를 개발할 때, 객체 간의 의존성은 코드를 확장하거나 수정할 때 많은 영향을 끼친다. IoC와 DI는 이런 부분을 좀 더 효율적으로 관리하고 코드의 유연성을 높여주는 설계 패턴이라고 할 수 있다.
1. IoC(Inversion of Control)란?
IoC란 단어 뜻 그대로 제어의 역전이라고 부른다. 객체의 생성이나 의존성 관리의 책임을 기존에는 개발자가 담당했었다면, 그걸 프레임워크나 컨테이너가 담당하도록 하는 개념을 말한다. 자바를 기준으로 하면 개발자가 아닌 Spring, 좀 더 깊게 들어가면 IoC 컨테이너에서 그 부분을 담당해서 객체의 생성과 관리를 외부에서 제어하게 된다.
구체적으로 살펴보면 스프링에서 IoC 컨테이너라는 개념을 구현하기 위해, BeanFactory와 ApplicationContext라는 두가지 주요 인터페이스가 존재한다. 다만 일반적으로는 ApplicationContext를 많이 사용하는데, 이는 BeanFactory의 기능(객체 생성 및 DI)을 포함하면서도 추가적인 기능을 제공하기 때문이다.(이벤트 처리, AOP, 메시지 리소스 관리 등)
따라서 Spring에서 IoC 컨테이너라고 하면 ApplicationContext를 떠올리면 된다. 그럼 이제 IoC 컨테이너가 어떻게 객체를 관리하는지를 보면 다음과 같은 일련의 과정을 거친다.
- 객체의 생성 및 관리 : 빈(Bean)을 생성하고 관리한다.
- 의존성 관리 : 객체 간의 의존성을 Spring이 주입(DI)한다.
- 제어 흐름의 역전 : 개발자가 아닌 스프링 프레임워크가 객체의 라이프사이클 및 실행 흐름을 제어한다.
Bean은 IoC 컨테이너를 통해 생성, 관리되는 객체로 이해하면 된다. Bean이 위와 같은 과정을 통해서 생성되고 관리되며, 추후에 소멸까지도 Spring 컨테이너에서 관여한다.
2. DI(Dependency Injection)란?
이름처럼 DI는 객체 간의 의존성을 주입하는 방식을 말하며, 객체가 필요한 의존성을 스스로 생성하거나 하지 않고 외부에서 주입 받아 사용하는 것을 의미한다. 이를 통해 객체 간의 결합도를 낮추고 코드의 재사용성을 높인다.
DI의 방식으로는 크게 3가지가 있는데 다음과 같다.
- 생성자 주입 : 객체 생성 시점에 필요한 의존성을 주입
- Setter 주입 : 객체 생성 후 setter 메서드를 통해 주입
- 필드 주입 : 필드에 직접 주입하는 방식
이 중 가장 권장되는 방식은 생성자 주입이며, 왜 그런지는 각각의 특징에 대해서 알아보며 확인하자.
2-1 생성자 주입
이름 그대로 생성자를 통해 객체를 주입하는 방식이며, 코드는 다음과 같다.
import org.springframework.stereotype.Service;
@Service
public class UserService {
public String getUserName() {
return "John Doe";
}
}
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
private final UserService userService;
// 생성자 주입
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/user")
public String getUser() {
return userService.getUserName();
}
}
과거 Spring 4.3 이하 버전에서는 생성자에 @Autowired 키워드를 붙였어야했지만 지금은 생략해도 주입이 된다. 또한 주입하려는 객체에 대해서 final 키워드를 통해 불변성을 보장하는 것이 권장된다. 장점은 다음과 같다.
- 불변성 : 의존성이 한 번만 주입되고 변경되지 않아 불변성이 보장됨.
- 테스트 용이성 : 의존성이 명확히 보이므로 테스트 시 Mock 객체를 쉽게 주입 가능
- 필수 의존성 보장 : 객체 생성시 모든 필수 의존성이 주입되었는지 컴파일 타임에 확인가능.
- 순환 참조 감지 가능 : 객체가 서로 참조하는 순환 참조 문제에 대해서 에러를 확인 가능하다.
2-2 Setter 주입
setter를 사용해 의존관계를 주입하는 방식으로 코드는 다음과 같다.
import org.springframework.stereotype.Service;
@Service
public class UserService {
public String getUserName() {
return "John Doe";
}
}
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
private UserService userService;
// Setter 주입
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
@GetMapping("/user")
public String getUser() {
return userService.getUserName();
}
}
Setter 주입은 수정할 가능성이 있는 의존관계일 때 사용 가능하다. 또한 생성자 주입과 달리 final 키워드 선언이 불가능하며 반드시 Setter 메서드에 @Autowired 키워드를 붙여 사용한다.
장점
- 선택적 의존성 : 의존성이 필수적이지 않은 경우에 유용
- 유연성 : 객체 생성 후에도 수정이 가능함.
단점
- 불변성 상실 : 객체 생성 후 의존성이 변경될 가능성이 있음.
- 필수 의존성 확인 어려움 : 런타임까지 확인할 수 없음.
2-3 필드 주입
가장 권장되지 않는 의존성 주입 방식으로 예시 코드는 다음과 같다.
import org.springframework.stereotype.Service;
@Service
public class UserService {
public String getUserName() {
return "John Doe";
}
}
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Autowired;
@RestController
public class UserController {
// 필드 주입
@Autowired
private UserService userService;
@GetMapping("/user")
public String getUser() {
return userService.getUserName();
}
}
@Autowired 키워드를 붙임으로써 사용 가능하며 장단점은 다음과 같다.
장점
- 코드가 매우 간단해진다.
단점
- 테스트가 어려움 : 의존성을 주입하기 어려워 테스트가 복잡해짐. Mockito와 같은 도구 사용시 까다로워짐.
- 의존성 주입 시점이 불명확 : 객체 생성 후 어느 시점에 의존성이 주입될지 명확하지 않음.
- 불변성이 상실 : 필드가 외부에서 바로 접근 가능하므로 불변성이 깨진다.
2-4 생성자 주입을 권장하는 이유
위에서 설명했던 생성자 주입의 장점과 더불어 추가적인 장점은 다음과 같다.
- 불변성 : 객체의 상태가 불변 상태가 유지됨.
- 테스트 용이성 : 생성자를 통해 의존성을 명확히 드러내므로 테스트 환경에서 쉽게 DI를 구성 가능함.
- 필수 의존성 보장 : 의존성이 필수라면 객체 생성시에 주입받도록 강제할 수 있음.
- DI 프레임워크에 의존하지 않음 : @Autowired 같은 어노테이션 없이도 구현 가능하므로 프레임워크에 대한 의존성을 줄일 수 있음.
- 롬복의 활용 : 롬복에서 @RequiredArgsConstructor 를 통해서 간단하게 구현이 가능함.
정리
스프링을 사용할 때 핵심적인 기능이고 일상적으로 사용하는 기능임에도 왜 그렇게 사용하는지 어떻게 구현되는지에 대한 이해를 할 수 있어서 좋았던 것 같다.
참고 자료
https://steady-coding.tistory.com/600
'Web Programming > Spring Boot' 카테고리의 다른 글
[Spring Boot] Spring MVC의 DispatcherServlet (0) | 2024.08.28 |
---|---|
[Spring Boot] 유효성 검사(Validation) (0) | 2024.07.04 |