[Spring] DI & IoC

Donghoon Jeong·2023년 4월 4일
0

Spring

목록 보기
1/15
post-thumbnail

이번 포스팅에서는 자바 코드로 작성된 예시의 문제점을 파악하고 Spring 프레임워크의 강력한 장점 중 하나인 의존성 주입(Dependency Injection, DI)을 활용하여 문제점을 해결하여 Spring이 이를 지원하는 이유에 대해서 알아보겠습니다.


DI (Dependency Injection)

Spring 프레임워크는 3가지 핵심 프로그래밍 모델을 지원하고 있는데, 그중 하나가 의존성 주입(Dependency Injection, DI)입니다. DI란 외부에서 두 객체 간의 관계를 결정해 주는 디자인 패턴으로, 인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않도록 하고 런타임 시에 관계를 동적으로 주입하여 유연성을 확보하고 결합도를 낮출 수 있게 해줍니다.

public class Store {

    private Pencil pencil;
}


의존성이란 한 객체가 다른 객체를 사용할 때 의존성이 있다고 합니다. 예를 들어 다음과 같이 Store 객체가 Pencil 객체를 사용하고 있는 경우에 우리는 Store 객체가 Pencil 객체에 의존성이 있다고 표현합니다.


DI의 필요성

DI의 필요성을 설명하기 위해 상품을 주문할 수 있는 OrderService 인터페이스를 구현한 OrderServiceImpl 클래스와 상품을 할인하는 DiscountPolicy 인터페이스를 구현한 FixDiscountPolicy 클래스가 있다고 하겠습니다.

public class FixDiscountPolicy implements DiscountPolicy {

    private int discountFixAmount = 1000; // 고정적으로 1000원 할인

    @Override
    public int discount() {  
            return discountFixAmount;
    }
}
public class OrderServiceImpl implements OrderService {

    private final DiscountPolicy discountPolicy = new FixDiscountPolicy();

    @Override
    public Order createOrder(String itemName, int itemPrice) {
        int discountPrice = discountPolicy.discount(itemPrice);
        return new Order(itemName, itemPrice, discountPrice);
    }
}

위와 같은 예시 클래스에서 OrderServiceImpl 클래스가 DiscountPolicy 인터페이스에 의존하여 객체지향적으로 코드가 잘 설계된 것처럼 보이지만 위 코드는 객체지향 5원칙(SOLID)의 OCPDIP를 준수하고 있지 않습니다.

  • DIP 위반 (Dependency Inversion Principle)
    OrderServiceImpl 클래스가 DiscountPolicy 인터페이스뿐만 아니라 구현 클래스인 FixDiscountPolicy 클래스도 함께 의존하고 있기 때문에 DIP를 위반하게 됩니다.


  • OCP 위반 (Open-Closed Principle)
    OrderServiceImpl 클래스는 구현 클래스인 FixDiscountPolicy 클래스를 직접적으로 의존하고 있기 때문에, 만약 할인 정책이 고정 할인 정책 → FixDiscountPolicy에서 정률 할인 정책 → RateDiscountPolicy으로 변경된다면 클라이언트 코드인 OrderServiceImpl 클래스를 변경해야 하기 때문에 OCP를 위반하게 됩니다.


public class OrderServiceImpl implements OrderService {
 
//  private final DiscountPolicy discountPolicy = new FixDiscountPolicy();
    private DiscountPolicy discountPolicy;
}

위 코드를 인터페이스에만 의존하게 위와 같이 코드를 변경하면 당연히 Null Pointer Exception이 발생하게 됩니다.

이 문제를 해결하려면 누군가가 클라이언트인 OrderServiceImpl 클래스에 DiscountPolicy 인터페이스의 구현 객체를 대신 생성하고 주입해 주어야 한다.

이 과정을 의존성 주입(Dependency Injection, DI)이라고 하고, 위 예시 코드를 통해 DI의 필요성에 대해 알게 되었으니 이를 활용하여 위 코드의 문제점을 해결해 보겠습니다.


DI를 통한 문제 해결

애플리케이션의 전체 동작 방식을 구성(config) 하기 위해, 구현 객체를 생성하고, 연결하는 책임을 가지는 별도의 설정 클래스를 생성하겠습니다.

public class AppConfig {
   
    public OrderService orderService() {
        return new OrderServiceImpl(discountPolicy());
    }
    
    public DiscountPolicy discountPolicy() {
//  	return new RateDiscountPolicy();
        return new FixDiscountPolicy();
    }
    
}
  • AppConfig는 애플리케이션의 실제 동작에 필요한 구현 객체를 생성합니다.

    • OrderServiceImpl

    • FixDiscountPolicy

  • AppConfig는 생성한 객체 인스턴스의 참조(레퍼런스)를 생성자를 통해서 주입(연결) 해줍니다.

    • OrderServiceImplFixDiscountPolicy
public class OrderServiceImpl implements OrderService {

    private final DiscountPolicy discountPolicy;

    public OrderServiceImpl(DiscountPolicy discountPolicy) {
        this.discountPolicy = discountPolicy;
    }

    @Override
    public Order createOrder(String itemName, int itemPrice) { 
        int discountPrice = discountPolicy.discount(itemPrice);
        return new Order(itemName, itemPrice, discountPrice);
    }
    
}

위와 같은 변경으로 OrderServiceImpl 클래스는 DiscountPolicy 인터페이스만 의존하고 구현 클래스인 FixDiscountPolicy 클래스에는 의존하지 않게 됩니다.

OrderServiceImpl 클래스 입장에서 생성자를 통해 어떤 구현 객체가 주입될지는 알 수 없고 OrderServiceImpl의 생성자를 통해서 어떤 구현 객체를 주입할지는 오직 외부 → AppConfig에서 결정하게 됩니다.

그리고 이러한 개념은 제어의 역전(Inversion of Control, IoC



다시 한번 정리하자면, 할인 정책을 변경할 경우에 클라이언트 코드인 OrderServiceImpl 클래스는 변경하지 않고 변경할 수 있기 때문에 OCP를 준수하게 됩니다.

또한 직접 구현 클래스인 FixDiscountPolicy 클래스를 의존하는 대신에 인터페이스에만 의존하도록 코드를 수정하였기 때문에 DIP를 준수하게 됩니다.


Spring DI

지금까지는 순수한 자바 코드를 사용하여 DI를 구현하였습니다. 스프링에서 제공하는 DI를 사용하는 코드로 리팩토링을 해보겠습니다.

@Configuration
public class AppConfig {

    @Bean
    public OrderServiceImpl orderService() {
        return new OrderServiceImpl(discountPolicy());
    }

    @Bean
    public DiscountPolicy discountPolicy() {
        return new FixDiscountPolicy();
//      return new RateDiscountPolicy();
    }

}

AppConfig에 설정을 구성한다는 뜻의 @Configuration 어노테이션을 붙여주고 각 메서드에 @Bean 어노테이션을 붙여줍니다. 이렇게 하면 스프링 컨테이너에 스프링 빈으로 등록됩니다.

public class OrderApp {

    public static void main(String[] args) {
// 		기존 자바코드 DI 방법
//      AppConfig appConfig = new AppConfig();
//      OrderService orderService = appConfig.orderService();

//      스프링 컨테이너 생성
        ApplicationContext applicationContext = new AnnotationConfigApplicationContext(AppConfig.class);

        OrderService orderService = applicationContext.getBean("orderService", OrderService.class);

        Order order = orderService.createOrder("itemA", 20000);
        System.out.println(order);
    }
}

위 코드의 ApplicationContext스프링 컨테이너라 합니다. 기존에는 개발자가 AppConfig를 사용해서 직접 객체를 생성하고 DI 했지만, 이제부터는 스프링 컨테이너를 통해서 사용합니다.

스프링 컨테이너는 @Configuration 어 로테이션이 붙은 AppConfig를 설정(구성) 정보로 사용합니다. 여기서 @Bean 어 로테이션이 붙은 메서드를 모두 호출해서 반환된 객체를 스프링 컨테이너에 등록합니다. 이렇게 스프링 컨테이너에 등록된 객체를 스프링 빈이라 합니다.

이전에는 개발자가 필요한 객체를 AppConfig를 사용해서 직접 조회했지만, 이제부터는 스프링 컨테이너를 통해서 필요한 스프링 빈(객체)를 찾아야 합니다.

스프링 빈은 applicationContext.getBean() 메서드를 사용해서 찾을 수 있습니다.
기존에는 개발자가 직접 자바 코드로 모든 것을 했다면 이제부터는 스프링 컨테이너에 객체를 스프링 빈으로 등록하고, 스프링 컨테이너에서 스프링 빈을 찾아서 사용하도록 변경되었습니다.

단순히 자바 코드를 실행하는 것 같고 코드가 약간 더 복잡해진 것 같은데, 위와 같이 스프링 컨테이너를 사용하면 어떤 장점이 있는지 다음 포스팅에서 알아보도록 하겠습니다.

DI & IoC

IoC는 객체의 흐름, 생명주기 관리 등 독립적인 제3자에게 역할과 책임을 위임하는 개념, 즉 원칙 중 하나이고, DI는 IoC를 달성하는 디자인 패턴 중 하나로 좀 더 구체적인 행위라고 할 수 있습니다.


Reference

스프링 핵심 원리 - 기본편

profile
정신 🍒 !

0개의 댓글