본문 바로가기

OOP(Object-Oriented Programming)

(OOP) - IoC와 DI란?

반응형

🍳머리말

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


📕다음글

spring에서 객체 생명주기 관리


*더 나은 내용을 위한 지적, 조언은 언제나 환영합니다.