Spring IoC/DI

과녁스·2021년 7월 21일
0

Spring

목록 보기
2/14
post-thumbnail
post-custom-banner

개요


스프링 의존성 주입(DI)과 제어의 역전(IoC)에 대하여 학습한 내용을 정리해보았습니다.

DI(Dependency Injection)


의존성 주입이란?

객체 생성 시 특정 클래스의 객체를 생성하여 가져오는 경우 특정 클래스의 의존이 된다고 합니다.

스프링에서는 IoC 컨테이너에서 관리하는 Bean 객체를 해당 클래스에서 생성하지 않고 해당 클래스에게 주입해주는 방식을 사용하였습니다.

의존성 주입에는 아래와 같이 3가지의 방식을 사용할 수 있습니다.

필드 주입

필드 주입(Field Injection)은 의존성을 주입하고 싶은 필드에 @Autowired 어노테이션을 붙여주면 의존성이 주입된다.

@RestController
public class PostController {

    @Autowired
    private PostService postService;
    
}

Setter 주입

setter 메서드에 @Autowired 어노테이션을 붙여 의존성을 주입하는 방식입니다.

@RestController
public class PostController {

    private PostService postService;
    
    @Autowired
    public void setPostService(PostService postService){
    	this.postService = postService;
    }
    
}

생성자 주입

생성자를 사용하여 의존성을 주입하는 방식입니다.

@RestController
public class PostController {

    private final PostService postService;
    
    public PostController(PostService postService){
    	this.postService = postService;
    }
    
}

lombok의 @RequiredArgsConstructor 를 사용하면 final로 선언된 필드를 가지고 생성자를 만들어줍니다. 필드가 추가되거나 삭제되어도 @RequiredArgsConstructor에 의해 생성자 코드를 개발자가 수정할 필요가 없습니다.

@RequiredArgsConstructor
@RestController
public class PostController {

    private final PostService postService;

}

최근에는 lombok을 이용한 생성자 주입을 권장하고 많이 사용합니다.

의존성 주입 사용에 대한 이점

  • 의존성으로부터 격리시켜코드 테스트 용이
  • DI를 통하여 불가능한 상황을 Mock와 같은 기술을 통하여, 안정적인 테스트 가능
  • 코드를 확장 또는 변경할때 영향을 최소화
  • 순환참조(아래 내용 참고)를 막을 수 있음
package com.example.test;

public class Main {

    public static void main(String[] args) {
        String testUrl = "google.co.kr/spring";

		// Encoder 객체에 객체를 주입하여 다른 동작이 일어나도록 설정
        Encoder encoder = new Encoder(new Base64Encoder());
        String result = encoder.encode(url);
		System.out.println(result);
    }
}

순환참조

여러 Bean들의 서로 연결되어 있음을 의미합니다.

BeanA -> BeanB -> BeanC -> BeanA

문제

여러 Bean들이 순환참조로 인하여 앱 실행이 되지 않는 문제가 발생할 수 있습니다.

특정 클래스에서 IoC 컨테이너에 존재하는 Bean을 주입받기 위해서 필드 주입방식, Setter 주입방식, 생성자 주입방식을 사용합니다.
이 중 생성자 주입방식만 다르게 동작을 하게 됩니다.

필드 주입방식 / Setter 주입방식

필드 주입방식과 Setter 주입방식에서는 A 클래스가 B 클래스를 의존하고, B 클래스가 A 클래스를 의존하는 상황이더라도 애플리케이션 실행과정에서 예외가 발생하지 않는다.

그리고 두 개의 클래스가 순환참조하고 있다고 하더라도 당장에 문제가 발생하지 않는다. 이러한 상황에서 문제가 되는 순간은 A 클래스의 메소드와 B 클래스의 메소드가 서로 순환참조하고 있는 상황에서 해당 메소드가 호출 되었을 때이다.

@Slf4j
@Service
public class ServiceA {
    @Autowired
    private ServiceB serviceB;
    
    public void run() {
    	serviceB.run();
        log.info("Called ServiceA.run()");
    }
}

@Slf4j
@Service
public class ServiceB {
    @Autowired
    private ServiceA serviceA;
    
    public void run() {
    	serviceA.run();
        log.info("Called ServiceB.run()");
    }
}
  1. 어플리케이션 로딩단계에서는 예외가 발생하지 않습니다.
  2. 클래스 간 순환참조 되는 것이 아닌 메서드가 순환호출 되어야하고 메서드가 순환호출 되는 시점에 예외가 발생합니다.

생성자 주입

먼저 Bean이 생성되는 시점을 알아보면 아래와 같습니다.

스프링 애플리케이션이 로딩되는 시점에 A 클래스가 B 클래스를 의존하고 B 클래스가 C 클래스를 의존한다면 Spring Boot 는 A 클래스에 대한 Bean 을 만들기 위해서 B 클래스의 Bean 을 주입하는데 없으니까, B 클래스의 Bean 을 먼저 만든다. 근데 그 과정에서 또 C 클래스의 Bean 을 주입하는데 없으니까 C 클래스의 Bean 을 먼저 만든다.

  • C -> B -> A 순서로 생성

위 내용을 이해하고 두 클래스 가 서로 의존하는 상황이 발생하면 무한 반복(순환참조)이 발생합니다.

  1. 클래스가 서로 의존성 주입을 통해 순환참조하고 있을 때 발생하는 문제입니다.
  2. 어플리케이션 로딩 시점 에 예외가 발생합니다.

해결 방법

설계상으로 순환참조 문제가 발생할 수 있는 구조를 만들지 않는 것이 가장 좋습니다.

만약 구조상 설계가 어려운 경우에는 @Lazy 어노테이션을 사용하는 방법이 있습니다.


@Service
public class ServiceA {
    private final ServiceB serviceB;
    
    @Autowired
    public ServiceA(ServiceB serviceB) {
    	this.serviceB = serviceB;
    }
}

@Service
public class ServiceB {
    private final ServiceA serviceA;
    
    @Autowired
    public ServiceB(@Lazy ServiceA serviceA) {
    	this.serviceA = serviceA;
    }
}

그러나 위 방법은 스프링에서 권장하지 않습니다.

IoC(Inversion of Control)


스프링에서는 대부분 개발자가 객체를 생성하여 관리하는 것이 아닌 Spring Container에 맡기게 된다.
개발자에서 프레임워크로 제어의 객체관리의 권한이 넘어 갔음으로 '제어의 역전'이라고 한다.

축약해서 정리해보면 스프링에서 객체를 직접 관리하는 것을 Bean이고, Bean을 관리하는 것이 스프링 컨테이너고 스프링 컨테이너가 제어하는 권한을 가져갔기 때문에 제어의 역전, IoC라고 한다.

역할과 개념

IoC를 적용하면 객체가 자신의 라이프사이클(생성, 소멸 등)과 의존 관계에 대한 책임을 IoC 컨테이너에 위임하게 됩니다.

즉, 개발자가 직접 필요한 객체를 생성하고 연결하는 것이 아니라, 스프링이 이 객체들을 관리하고, 필요한 객체를 주입해 줍니다. 이렇게 하면 개발자는 비즈니스 로직 구현에만 집중할 수 있습니다.

IoC와 DI의 관계

IoC는 넓은 개념으로 DI(Dependency Injection, 의존성 주입)를 포함합니다. IoC는 제어를 넘긴다는 큰 개념을 의미하고, DI는 IoC의 일종으로, 외부에서 객체의 의존성을 주입해주는 구체적인 방법입니다. 스프링에서는 IoC를 DI로 구현하여 객체 간의 결합을 느슨하게 합니다.

Spring IoC 컨테이너

Spring IoC는 ApplicationContext와 BeanFactory라는 두 가지 주요 인터페이스로 구현됩니다. 이 컨테이너들은 스프링 빈(bean)을 관리하는 역할을 하며, 빈 설정과 의존성 주입을 담당합니다.

  • BeanFactory: 기본적인 IoC 컨테이너로, 빈 생성과 의존성 주입을 담당합니다. 경량 컨테이너로 리소스를 많이 사용하지 않으며, 지연 초기화(lazy loading)를 지원합니다.
  • ApplicationContext: BeanFactory를 확장한 IoC 컨테이너로, 애플리케이션 전반에 필요한 여러 기능을 추가로 제공합니다. 이벤트 기능, 메시지 리소스 처리, 다양한 컨텍스트 타입 등을 지원합니다.

동작 방식

@Component 어노테이션을 사용하여 Spring Bean에 등록이 가능하고,

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class PaymentService {
    public void processPayment() {
        System.out.println("Payment processed.");
    }
}

@Service
public class OrderService {
    private final PaymentService paymentService;

    // 생성자 주입 방식으로 PaymentService를 외부에서 주입받음
    @Autowired
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public void placeOrder() {
        System.out.println("Placing order...");
        paymentService.processPayment();
    }
}
  • 의존성 주입(DI): OrderService는 PaymentService를 생성하는 것이 아니라, 외부에서 주입받습니다. 여기서는 생성자 주입 방식을 사용했습니다.
  • IoC 컨테이너 관리: Spring IoC 컨테이너는 @Service 어노테이션을 통해 OrderService와 PaymentService 객체를 생성하고, OrderService 생성 시 PaymentService를 자동으로 주입합니다.

스프링 애플리케이션을 실행하는 Application 클래스를 만들어서 실행해보겠습니다.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        ApplicationContext context = SpringApplication.run(Application.class, args);

        // IoC 컨테이너에서 OrderService 빈을 가져와서 사용
        OrderService orderService = context.getBean(OrderService.class);
        orderService.placeOrder();
    }
}

이제 OrderService는 IoC 컨테이너로부터 PaymentService를 주입받기 때문에, OrderService와 PaymentService는 서로 강하게 결합되지 않으며, PaymentService가 변경되더라도 OrderService의 코드를 수정할 필요가 없습니다. 또한, 테스트 시에는 PaymentService 대신 다른 Mock 객체를 주입할 수 있어 테스트도 용이해집니다.

여러 개를 등록하고 싶을 때는 @Configuration 어노테이션을 사용하여 설정하고, @Bean 어노테이션에 이름을 지정하여 bean에 등록할 수 있다.

IoC의 이점

  • 결합도 감소: 객체 간 결합도가 낮아져서 코드의 유지보수와 확장성이 높아집니다.
  • 재사용성 증가: 다양한 환경에서 객체를 쉽게 주입할 수 있어 코드 재사용성이 증가합니다.
  • 테스트 용이성: 모듈화된 코드로 인해 의존성 주입 시 Mock 객체를 사용하여 유닛 테스트가 용이해집니다.

출처 및 참고🙏

profile
ㅎㅅㅎ
post-custom-banner

0개의 댓글