dev-sohee 님의 블로그

Spring의 3대 프로그래밍 모델 : 스프링 삼각형(IoC/DI, AOP, PSA) 1탄 본문

spring

Spring의 3대 프로그래밍 모델 : 스프링 삼각형(IoC/DI, AOP, PSA) 1탄

dev-sohee 2024. 8. 31. 13:09

스프링을 이해하는 데는 POJO(Plain Old Java Object)를 기반으로 스프링 삼각형이라는 애칭을 가진 IoC/DI, AOP, PSA라고 하는 스프링의 3대 프로그래밍 모델에 대한 이해가 필수입니다. 스프링 삼각형을 이해하지 않은 상태에서 스프링 프레임워크를 학습하는 것은 알파벳을 모르고 영어를 공부하는 것과 마찬가지입니다.

출처_https://velog.io/

 

* IoC/DI (Inversion Of Control/Dependency Injection)
* AOP(Aspect-Oriented Programming)
* PSA(Portable Service Abstraction)

 

# IoC/DI

IoC/DI(Inversion Of Control/Dependency Injection)란 해석하면 제어의 역전/의존성 주입입니다. 해석을 해도 무슨 말인지 와닿지 않을 것입니다. 이 개념을 이해하려면 먼저 알아야 할 지식이 있습니다.

 

라이브러리

라이브러리는 특정 기능을 수행하기 위한 재사용 가능한 코드의 모음입니다. 개발자가 필요할 때 라이브러리의 함수나 클래스를 호출하여 사용하기 때문에 라이브러리를 사용하는 어플리케이션은 객체의 생명주기(객체의 생성, 초기화, 소멸, 메서드 호출 등)를 개발자가 직접 제어합니다.

 

프레임워크

프레임워크는 어플리케이션 개발의 기본 구조와 골격을 제공하는 재사용 가능한 소프트웨어 플랫폼입니다. 프레임워크를 사용한 어플리케이션의 경우, 개발자가 코드에 작성한 객체들이 어느 시점에 호출될 지는 신경쓰지 않습니다. 단지 프레임워크가 요구하는대로 객체를 생성하면, 프레임워크가 해당 객체들을 가져다가 생성하고, 메서드를 호출하고, 소멸시킵니다. 이를 프로그램의 제어권이 역전(IoC)됐다고 합니다.

 

의존성

스프링에서 의존성이란 한 클래스가 다른 클래스 또는 인터페이스를 필요로 하는 관계를 의미합니다. 예를 들어, Car라는 클래스에서 Tire라는 객체를 생성했다면 Car는 Tire를 필요로 하므로 Car가 Tire에 의존하는 관계가 형성됩니다. 

public Class Car {
	Tire tire;
    
    public Car() {
    	tire = new KoreaTire();    
    }
}

 

위 예제 코드에서는 Car 클래스 내에서 Tire를 생성함으로써 의존 관계를 형성, 즉 자체적으로 의존성을 해결하고 있습니다. 의존성 주입(DI)은 이러한 객체의 의존성을 외부에서 주입한다는 뜻입니다. 즉, 위의 예제처럼 자동차의 내부에서 타이어를 생산하는 것이 아니라 외부에서 생산된 타이어를 자동차에 장착하는 작업이 의존성 주입입니다. 객체 간의 의존성을 외부에서 주입함으로써 OCP의 원칙을 지킬 수 있어 코드의 유지보수성과 확장성을 높일 수 있는데 이에 대해서는 DI의 세가지 방법과 함께 설명 드리겠습니다.

 

1. 생성자 주입

: 클래스가 인스턴스화될 때 필요한 의존성을 생성자의 인자로 전달받는 방법입니다. 위의 예제를 연결하여 사용하겠습니다.

class Car {
    private final Tire tire;  // 타이어 의존성을 불변으로 설정
    
    // 생성자 주입을 통한 타이어 의존성 주입
    public Car(Tire tire) {
        this.tire = tire;
    }
}

public class Main {
    public static void main(String[] args) {
        Tire tire = new KoreaTire();  // 운전자가 타이어를 생산한다
        Car car = new Car(tire);  // 운전자가 자동차를 생성하면서 타이어를 장착한다
    }
}

 

Car 객체가 new 생성자를 통해 tire를 인자로 전달 받았습니다. 즉 Tire에 대한 의존성을 Car 생성자의 인자 주입으로 해결한 것입니다. 기존 코드에서는 Car가 KoreaTire를 생산할지 ChinaTire를 생산할지 직접 결정했는데, 이제는 운전자가 고민하고 결정하게 하는 것입니다. 이렇게 하면 새로운 타이어 브랜드가 생겨도 각 타이어 브랜드들이 타이어 인터페이스를 구현한다면(타이어의 표준 규격을 준수한다면), Car.java의 코드 변경 없이 사용할 수 있기 때문에 코드의 확장성이 좋아집니다.

 

장점

  • 객체 생성 시점에 모든 의존성이 주입되기 때문에 불변성을 보장합니다.
  • 의존성이 필수적인 경우 컴파일 시점에 확인할 수 있어, 안전성이 높아집니다.

단점

  •  final 키워드를 사용해 의존성을 불변으로 설정하기 때문에 유연성이 부족합니다. 예를 들어, 자동차를 생산할 때 타이어를 한번 장착하면, 교체할 수 없음을 의미합니다. 

 

2. 필드(속성) 주입

: 의존성을 클래스의 필드(속성)에 직접 주입하는 방식입니다. 보통 @Autowired 어노테이션을 사용하여 의존성을 자동으로 주입합니다. 이해를 위해 스프링을 사용하지 않고 의존성을 주입한 예제 코드를 먼저 보여드리겠습니다. 

Tire tire = new KoreaTire(); // 운전자가 타이어를 생산한다
Car car = new Car(); // 운전자가 자동차를 생산한다
car.setTire(tire); // 운전자가 자동차에 타이어를 장착한다

 

생성자 주입이 한번 장착된 타이어를 바꿀 수 없다면, 필드 주입은 운전자가 원할 때 바꿀 수 있습니다. 현실세계라면 필드 주입이 더 유용할 것 같지만, 프로그래밍 세계에서는 한번 주입된 의존성을 계속 사용하는 경우가 많기 때문에 생성자를 통한 의존성 주입이 더 선호받는 추세입니다. 아래는 스프링에서 필드를 통해 의존성을 주입한 예제입니다.

// 운전자는 종합 쇼핑몰에서 자동차와 타이어를 구매하고, 타이어를 자동차에 장착합니다
public class Driver {
    private String name;

    public Driver(String name) {
        this.name = name;
    }

    public void buyAndMountTires(Shop shop) {
        Car car = shop.sellCar(); // 자동차 구매
        Tire tire = shop.sellTire(); // 타이어 구매

        car.mountTire(tire); // 타이어를 자동차에 장착
    }
}

// 자동차 클래스는 타이어를 장착할 수 있도록 합니다
public class Car {
    private Tire tire; // 장착될 타이어

    // 타이어를 자동차에 장착하는 메서드
    public void mountTire(Tire tire) {
        this.tire = tire;
    }
}

// 자동차에 장착될 타이어
public class Tire {
    private String type;

    public Tire(String type) {
        this.type = type;
    }
}

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

@Component
public class Shop {

    @Autowired
    private Car car;  // 필드 주입을 사용하여 Car 의존성 주입

    @Autowired
    private Tire tire;  // 필드 주입을 사용하여 Tire 의존성 주입

}

 

장점

  • 구현이 가장 간단하고 코드가 짧습니다.

단점

  • 필드 주입 방식으로 주입된 필드는 객체 생성 이후에도 외부에서 변경할 수 있기 때문에 불변성을 보장할 수 없습니다. 예를 들어, 다른 클래스나 테스트 코드에서 Shop 객체의 car 필드를 변경할 수 있습니다. 
  • 주입된 필드가 외부에서 변경될 수 있어 유지보수성이 떨어집니다.

 

3. Setter 주입

: 의존성을 세터 메서드를 통해 주입하는 방식입니다. 클래스가 생성된 후에 필요한 의존성을 설정할 수 있습니다.

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

@Component
public class Shop {
    private Car car;  // 자동차 객체
    private Tire tire;  // 타이어 객체

    // 자동차 의존성을 주입하는 세터 메서드
    @Autowired
    public void setCar(Car car) {
        this.car = car;
        System.out.println("Shop에 Car가 주입되었습니다.");
    }

    // 타이어 의존성을 주입하는 세터 메서드
    @Autowired
    public void setTire(Tire tire) {
        this.tire = tire;
        System.out.println("Shop에 Tire가 주입되었습니다.");
    }

    // 자동차를 판매하는 메서드
    public Car sellCar() {
        System.out.println("종합 쇼핑몰에서 자동차를 판매합니다.");
        return car;
    }

    // 타이어를 판매하는 메서드
    public Tire sellTire() {
        System.out.println("종합 쇼핑몰에서 타이어를 판매합니다.");
        return tire;
    }
}

 

장점

  • 의존성을 선택적으로 주입하거나, 런타임에 의존성을 변경할 수 있습니다.

단점

  • 선택적인 의존성 주입 방식이기 때문에 필수적인 의존성도 주입하지 않을 수 있습니다.
  • 필드 주입과 마찬가지로, 객체의 상태가 변할 수 있어 불변성을 보장하기 어렵습니다.


여기까지 이해했더니 한 가지 가벼운 의문이 생겼습니다. IoC와 DI는 완전히 다른 개념인 것 같은데 왜 IoC/DI 이렇게 같이 쓰는거지? 라는 의문입니다. IoC와 DI가 자주 같이 사용되는 이유는 IoC는 개념적 원칙이고, DI는 IoC를 구현하는 방법이기 때문입니다. 즉, IoC는 "누가 객체를 제어할 것인가"에 대한 것이고, DI는 "그 객체의 의존성을 어떻게 제공(주입)할 것인가"에 대한 것입니다. 스프링 삼각형의 나머지 모델인 AOP와 PSA에 대해서는 'Spring의 3대 프로그래밍 모델 : 스프링 삼각형(IoC/DI, AOP, PSA) 2탄'에서 이어서 다루도록 하겠습니다.