반응형
🍳머리말
IoC(Inversion of Control) 과 DI(Dependency Injection)에 대해 알아봅니다
📕 정의
📔 IoC
객체의 생성과 라이프사이클 관리를 개발자가 아닌 프레임워크 또는 컨테이너가 하도록 하는 설계 원칙
- 객제 간 제어 권한을 개발자가 아닌 프레임워크로 역전
- 대표적 구현 방식: DI
📔 DI
객체가 의존성을 외부에서 주입받는 설계 패턴으로 IoC를 구현하는 주요 방법
- 객체는 자신이 필요한 의존성을 컨테이너에서 주입받아 사용
📕 주요 특징
📔 IoC
📑 제어권 역전
객체 생성 및 의존성 관리를 프레임워크 또는 컨테이너에서 담당
📑 프레임 워크 중심 설계
객체간 상호작용이 프레임워크에 의해 관리
📑 유연성
객체 간 결합도를 낮춰 다양한 구성, 확장 가능. 유지보수성 향상
📔 DI
📑 의존성 관리
객체가 필요한 의존성을 외부에서 주입
📑 코드 간결화
의존성 주입을 통해 객체 생성 로직과 비즈니스 로직이 분리
📕 실무에서 어떻게 사용되며 그 이유
📔 스프링 프레임워크
스프링의 IoC 컨테이너가 객체를 생성하고 DI를 통해 의존성을 주입
📑 생성자 주입
📘 예시
@Component
public class UserRepository {
public void save(User user) {
System.out.println("User saved!");
}
}
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired // 생성자에 의존성 주입
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void saveUser(User user) {
userRepository.save(user);
}
}
📑 필드 주입
📘 예시
@Component
public class UserRepository {
public void save(User user) {
System.out.println("User saved!");
}
}
@Service
public class UserService {
@Autowired // 필드에 직접 의존성 주입
private UserRepository userRepository;
public void saveUser(User user) {
userRepository.save(user);
}
}
📑 Setter 주입
📘 예시
@Component
public class UserRepository {
public void save(User user) {
System.out.println("User saved!");
}
}
@Service
public class UserService {
private UserRepository userRepository;
@Autowired // Setter 메서드를 통한 의존성 주입
public void setUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
public void saveUser(User user) {
userRepository.save(user);
}
}
항목 | 생성자 주입 | 필드 주입 | Setter 주입 |
불변성 지원 | O | X | X |
코드 간결성 | 약간 복잡 | 매우 간결 | 중간 |
유연성 | 중간 | 낮음 | 높음 |
테스트 용이성 | 매우 용이 | 어려움 | 용이 |
의존성 명확성 | 명확 | 낮음 | 중간 |
주입 시점 | 객체 생성 시점 | 객체 생성 이후 | 객체 생성 이후 |
📑 테스트
📘 예시
@ExtendWith(MockitoExtension.class) // Mockito 확장을 사용
public class UserServiceTest {
@Mock // UserRepository를 Mock 객체로 선언
private UserRepository userRepository;
@InjectMocks // UserRepository의 Mock 객체를 UserService에 주입
private UserService userService;
@Test
public void testGetUserName() {
// Given: Mock 객체의 동작 설정
User mockUser = new User();
mockUser.setId(1L);
mockUser.setName("John Doe");
Mockito.when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser));
// When: UserService의 메서드 호출
String userName = userService.getUserName(1L);
// Then: 결과 검증
Assertions.assertEquals("John Doe", userName);
}
@Test
public void testGetUserName_UserNotFound() {
// Given: Mock 객체 동작 설정 (ID에 해당하는 User 없음)
Mockito.when(userRepository.findById(2L)).thenReturn(Optional.empty());
// When & Then: 예외 발생 여부 확인
Assertions.assertThrows(RuntimeException.class, () -> userService.getUserName(2L));
}
}
실제 UserRepository가 아니라 Mock객체로 동작하므로 실제 DB에 접근하지 않음
📔 플러그인
📘 장점
플러그인 추가 용이. 유연한 의존성 교체, 확장성
📘 예시
- 앱에서 알림 기능을 제공한다고 가정하면
- 이메일 알람을 기존적으로 사용하지만 나중에 SMS 알림이나 푸시 알림으로 교체 가능해야함
...
@Service
public class NotificationManager {
private final NotificationService notificationService;
@Autowired
public NotificationManager(NotificationService notificationService) {
this.notificationService = notificationService;
}
public void notifyUser(String message) {
notificationService.sendNotification(message);
}
}
...
@Service
@Primary // 기본 플러그인으로 설정
public class EmailNotificationService implements NotificationService {
@Override
public void sendNotification(String message) {
System.out.println("Email Notification: " + message);
}
}
...
public interface NotificationService {
void sendNotification(String message);
}
...
@Service
public class EmailNotificationService implements NotificationService {
@Override
public void sendNotification(String message) {
System.out.println("Email Notification: " + message);
}
}
...
@RestController
@RequestMapping("/notifications")
public class NotificationController {
private final NotificationManager notificationManager;
@Autowired
public NotificationController(NotificationManager notificationManager) {
this.notificationManager = notificationManager;
}
@PostMapping
public ResponseEntity<String> sendNotification(@RequestBody String message) {
notificationManager.notifyUser(message);
return ResponseEntity.ok("Notification sent!");
}
}
...
//새로운 플러그인 추가
@Service
public class SmsNotificationService implements NotificationService {
@Override
public void sendNotification(String message) {
System.out.println("SMS Notification: " + message);
}
}
여기서 sms를 기본 알림 서비스로 설정하려면 @Primary를 추가하거나 application.properties에 프로파일 설정을 추가
장점: 플러그인 추가 용이
📕 예제
📔 기존 방식 - IoC와 DI 적용 없는 코드
public class UserService {
private UserRepository userRepository;
public UserService() {
this.userRepository = new UserRepository(); // 직접 생성
}
public void saveUser(User user) {
userRepository.save(user);
}
}
📔 IoC와 DI 적용 코드
@Component
public class UserRepository {
public void save(User user) {
System.out.println("User saved!");
}
}
@Service
public class UserService {
private final UserRepository userRepository;
@Autowired
public UserService(UserRepository userRepository) {
this.userRepository = userRepository; // 외부에서 주입
}
public void saveUser(User user) {
userRepository.save(user);
}
}
@Configuration
@ComponentScan("com.example")
public class AppConfig {}
public class Main {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService userService = context.getBean(UserService.class);
userService.saveUser(new User());
}
}
📕 장단점
항목 | IoC/DI 사용전 | IoC/DI 사용 후 |
결합도 | 강한 결합(객체 직접 생성) | 낮은 결합(외부에서 의존성 주입) |
테스트 용이성 | Mock 객체 사용 어려움 | Mock 객체로 단위 테스트 용이 |
유지보수성 | 코드 수정 시 의존성 수정 필요 | 설정 변경만으로 의존성 교체 가능 |
코드 복잡도 | 간단하지만 비효율적 | 복잡하나 효율적 |
확장성 | 객체 변경시 전반적 코드 수정 필요 | 객체 간 결합 최소화로 확장 용이 |
📕 새로운 트렌드
📔 DI의 확장
📑 클래스 뿐 아니라 모듈 단위로 확장
📘 Micronaut: 경량 프레임워크
- 컴파일 타임 DI: 런타임 오버헤드가 감소
- 경량 컨테이너로 빠른 실행 속도
- 클라우드 네이티브 앱에 적합
@Factory
public class AppModule {
@Singleton
public UserService userService(UserRepository userRepository) {
return new UserService(userRepository);
}
}
📘 Ktor: 경량 프레임워크에서
- Kotlin 기반 비동기 웹 프레임워크
- DI를 통한 모듈간 결합도 감소
- 경량 서버 앱 개발에 적합
val myModule = DI.Module("myModule") {
bind<UserService>() with singleton { UserService(instance()) }
bind<UserRepository>() with singleton { UserRepository() }
}
fun main() {
val di = DI {
import(myModule)
}
val userService: UserService by di.instance()
userService.doSomething()
}
- Ktor는 Kodein 같은 DI 라이브러리와 함께 사용
특징 | Micronaut | Ktor |
DI 방식 | 어노테이션 기반 | Kotlin DSL 기반 |
주입 시점 | 컴파일 타임 (런타임 성능 최적화) | 런타임 |
유연성 | Spring-like 어노테이션 제공 | 설정이 명시적이고 코드 간결 |
적합한 환경 | 클라우드 네이티브 앱 | 경량 서버 앱 |
DI 라이브러리 | 자체 DI 컨테이너 | Kodein, Koin 등과 통합 가능 |
📔 DI와 클라우드 네이티브
📑 클라우드서 DI 컨테이너 이용한 동적 의존성 관리
Spring Cloud에서 설정 및 서비스 등록
📔 DI와 Functional Programming
📑 함수형 프로그래밍과의 결합으로 더 가벼운 DI 구현
Kotlin DSL 및 람다를 활용한 DI 설정
📔 Zero-config DI
📑 설정 없이 자동 구성
@ComponentScan, @AutoWiring
📔 Serverless와 DI
📑 서버리스 환경에서 객체를 직접 생성하지 않고 DI 컨테이너로 관리
AWS Lambda
📕다음글
*더 나은 내용을 위한 지적, 조언은 언제나 환영합니다.