[Spring] IoC / DI

Jeon817·2023년 4월 15일

Spring

목록 보기
3/11

[IoC]

  • IoC( Inversion of Control )는 제어의 역전 이라는 뜻으로 메소드나 객체의 호출작업을 개발자가 결정하는 것이 아니라 외부에서 결정되는 것을 의미합니다.
    즉, 객체나 프로그램의 일부에 대한 제어를 컨테이너나 프레임워크로 이전하는 소프트웨어 엔지니어링의 원칙입니다. 사용자 정의 코드가 라이브러리를 호출하는 기존 프로그래밍과 달리 IoC에서는 프레임워크가 프로그램 흐름을 제어하고 사용자 정의 코드를 호출할 수 있습니다.

  • 간단한 예시를 통해 설명하면
    기존의 제어의 흐름이 A -> B 이고
    이때, B(interface)를 구현한 C 객체를 A에 주입 시킵니다.
    그렇게 된다면 제어의 B -> A 로 제어의 흐름이 역전되는 것입니다.

  • IoC는 설계 원칙에 해당 하는데, 위에서 든 예시와 같이 어떠한 요리를 만드는 레시피가 있다고 가정했을 때 IoC는 그 요리를 맛있게 만드는 원칙이라고 이해하면 쉽습니다.

[IoC 가 중요한 이유는?]

  • 가장 대표적인 이유로는 클래스간의 결합을 느슨하게 하여 유지보수 하기 쉽게 만들어 준다는 장점이 있기 때문입니다.
  • 좀 더 설명하자면, 객체를 관리해주는 스프링 프레임워크와 내가 구현 하려는 부분으로 역할을 분리해 응집도는 높이면서 결합도를 낮춰 변경에 유연한 코드를 작성할 수 있는 구조로 만들어 주기 때문입니다.

정리해보면, "DI 패턴을 사용하여 IoC 설계 원칙을 구현하고 있다" 이를 통해 객체간의 결합을 약하게 만들고 유연하면서 확장성까지 뛰어난 코드를 작성할 수 있게 Spring 에서 IoC, DI 라는 핵심 기능을 통해 도와주는 것입니다.

[DI]

  • DI(Dependency Injection)는 의존성 주입 이라는 뜻으로 객체를 직접 생성하는 것이 아닌 외부에서 생성한 객체를 주입 시켜주는 방식입니다.
  • DI는 IoC를 구현하는데 사용할 수 있는 디자인 패턴으로, DI는 어떤 요리에 대한 레시피 자체 라고 이해하면 되겠습니다.

[DI 가 중요한 이유는?]

  • 우리는 이러한 의존성 주입이 왜 필요한지를 알아야 합니다.
    객체를 직접 생성하게 되면 그것은 결합이 강한 상태라고 할 수 있습니다. 강한 결합인 경우 관리하기가 어려워지기 때문에 우리는 의존성 주입을 통해 이 문제를 해결할 수 있습니다. 의존성 주입을 받게 되면 객체 간의 결합도가 낮아지고 유연성은 높아집니다. 결국 개발을 하는데 있어 객체 간의 유연한 활요이 가능해집니다. 이 외에도 재사용성을 높여주고, 코드도 단순화 시켜주는 등 의존성 주입의 장점은 더 많기에 개발자 라면 꼭 이해해야 하는 부분이고 중요한 부분이라고 생각합니다.

  • 객체지향 관점에서 DI를 활용하여 얻을 수 있는 장점.
  1. 느슨한 결합 :
    DI를 사용하여 주문(Order) 클래스가 결제(Payment) 클래스에 직접 의존하지 않고, 의존성을 외부에서 주입받게 됩니다. 이로 인해 두 클래스 간의 결합도가 낮아지며, 변경이 발생할 때 다른 클래스에 영향을 덜 주게 됩니다.
// 주문 클래스 (Order) - 의존성을 주입받음
public class Order {
    private Payment payment;

    public Order(Payment payment) {
        this.payment = payment;
    }

    public void processOrder() {
        // 결제 로직 수행
        payment.pay();
        System.out.println("주문이 처리되었습니다.");
    }
}

// 결제 클래스 (Payment)
public class Payment {
    public void pay() {
        System.out.println("결제가 완료되었습니다.");
    }
}

// 주문을 처리하는 곳
public class Main {
    public static void main(String[] args) {
        Payment payment = new Payment();
        Order order = new Order(payment);
        order.processOrder();
    }
}
  1. 유연성과 확장성 :
    DI를 사용하면 의존성 주입을 통해 런타임에 의존 객체를 변경할 수 있습니다. 즉, 결제 시스템이 변경되더라도 주문 클래스는 변경할 필요 없이 외부에서 다른 결제 클래스를 주입받아 사용할 수 있습니다.
// 새로운 결제 클래스 추가 (신용카드 결제)
public class CreditCardPayment extends Payment {
    @Override
    public void pay() {
        System.out.println("신용카드로 결제가 완료되었습니다.");
    }
}

// 주문을 처리하는 곳
public class Main {
    public static void main(String[] args) {
        // 기존 결제 방식
        Payment payment = new Payment();
        Order order = new Order(payment);
        order.processOrder();

        // 새로운 결제 방식
        CreditCardPayment creditCardPayment = new CreditCardPayment();
        Order orderWithCreditCard = new Order(creditCardPayment);
        orderWithCreditCard.processOrder();
    }
}

[주입의 종류]

1) 필드 주입

  • 굉장히 편하게 주입할 수 있으나 순환 참조(무한 루프)시 오류가 발생하지 않기 때문에 StackOverFlow 발생 합니다.
  • final을 붙일 수 없기 때문에 다른 곳에서 변형이 가능합니다.
// 의존성을 주입받을 클래스
public class Car {
    @Autowired // 필드 주입
    private Engine engine;

    public void start() {
        engine.start();
        System.out.println("Car ON");
    }
}

// 의존성을 주입할 클래스
public class Engine {
    public void start() {
        System.out.println("Engine ON");
    }
}

// 주입하는 곳
public class Main {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
        Car car = context.getBean(Car.class);
        car.start();
    }
}

2) 생성자 주입

  • 순환 참조 시 컴파일러 인지 가능, 오류 발생! -> 메모리에 할당되면서 초기값으로 주입되므로 초기값에 문제 발생 시 할당도 되지 않기 때문입니다.
  • 초기화 생성자를 사용하면 해당 객체에 final을 사용할 수 있습니다(다른 곳에서 변형 x).
// 의존성을 주입받을 클래스
public class Car {
    private Engine engine;

    // 생성자 주입
    public Car(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start();
        System.out.println("Car ON");
    }
}

// 의존성을 주입할 클래스
public class Engine {
    public void start() {
        System.out.println("Engine ON");
    }
}

// 주입하는 곳
public class Main {
    public static void main(String[] args) {
        Engine engine = new Engine();
        Car car = new Car(engine);
        car.start();
    }
}

3) Setter 주입

  • Setter 메소드에 @Autowired 어노테이션을 붙이는 방법입니다. Setter 주입을 사용하면 setXXX 메서드를 public으로 열어두어야 하기 때문에 언제 어디서든 변경이 가능하다는 단점이 있습니다.
// 의존성을 주입받을 클래스
public class Car {
    private Engine engine;

    // 세터 주입
    public void setEngine(Engine engine) {
        this.engine = engine;
    }

    public void start() {
        engine.start();
        System.out.println("Car ON");
    }
}

// 의존성을 주입할 클래스
public class Engine {
    public void start() {
        System.out.println("Engine ON");
    }
}

// 주입하는 곳
public class Main {
    public static void main(String[] args) {
        Engine engine = new Engine();
        Car car = new Car();
        car.setEngine(engine);
        car.start();
    }
}

정리해보면,

  • Bean 이란 스프링이 관리하는 클래스다.
  • @Autowired는 우리가 주입을 하기 위해서 주입해달라고 스프링에게 알려주는 어노테이션이다.
  • @Autowired를 통해 주입을 할 수 있는 객체는 꼭 Bean 이어야 한다.
  • Bean 으로 등록하고 위해서는 @Controller, @Service와 같은 어노테이션을 클래스 위에 등록해야한다.
    ( Bean을 등록하는 방법 중 하나는 @Component 어노테이션을 사용하는 것인데, @Controller, @Service, @Repository는 @Component를 포함하고 있기 때문에 가능 )

결론, 필드 주입은 테스트 시 활용하기 좋고 대부분의 경우에는 생성자 주입을 사용하자!

위 예제 코드에서 Car 클래스는 생성자 주입을 사용하여 Engine 클래스를 의존성으로 주입받고 있습니다. 이렇게 하면 필수적으로 필요한 의존성을 생성자 매개변수로 명시하고, 불변성을 보장하며, 순환 의존성을 방지할 수 있습니다. 또한 단위 테스트에서도 쉽게 의존성을 Mock 객체로 대체하여 테스트할 수 있기에 스프링 공식 문서에서도 생성자 주입을 권고하고 있습니다.



[참고]https://beststar-1.tistory.com/33
[참고]https://dev-coco.tistory.com/70
[참고]https://sabarada.tistory.com/67

0개의 댓글