Spring - 의존성 주입(DI - Dependency Injection)

지니·2023년 8월 20일
0

spring

목록 보기
13/13

Spring에서 사용되는 DI (Dependency Injection): 의존성 주입을 알아보자

1. 의존성, 의존성 주입이란?

의존성이란

  • 객체 지향 프로그래밍에서 클래스나 모듈 간의 관계를 의미한다.

  • 한 클래스가 다른 클래스에 의존한다는 것은 해당 클래스가 다른 클래스의 인스턴스나 메서드를 사용한다는 것을 의미한다.
    의존성은 클래스 간의 결합도를 나타내는 중요한 요소이다.

의존성 주입이란

  • 의존성 주입은 객체 지향 프로그래밍에서 의존하는 객체를 직접 생성하거나 관리하지 않고 외부에서 주입받는 것을 의미한다.

  • 즉, 의존성을 클래스 내부에서 생성하거나 결정하지 않고, 외부에서 주입받아 사용하는 방식이다.

코드 설명

// 의존성 주입을 사용한 예시: 주문 서비스 클래스가 결제 서비스 클래스를 주입받음
public class OrderService {
    private final PaymentService paymentService; // 결제 서비스에 대한 의존성을 가지고 있다.
		
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService; // 외부에서 주입받은 결제 서비스를 사용한다.
    }

    public void placeOrder() {
        // 주문 처리 로직
        paymentService.processPayment(); // 외부에서 주입받은 결제 서비스 메서드를 호출하여 사용한다.
    }
}

// 외부에 있는 하나의 클래스
public class PaymentService {
    public void processPayment() {
        // 결제 처리 로직
    }
}
  • OrderService 클래스는 외부에서 주입받은 PaymentService (결제 서비스)를 사용할 수 있게 되며, 의존성을 직접 생성하거나 결정하지 않는다.

의존성을 직접 생성한다는것(수동 의존성 설정)은 무엇일까?

  • "의존성을 직접 생성한다"는 것은 new PaymentService()와 같이 의존하는 객체를 new 키워드를 통해 직접 생성하는 것을 의미한다. 이 경우에는 의존성을 갖는 클래스가 해당 의존성 객체를 직접 생성하고 사용하게 된다.

  • 반면 "의존성 주입을 사용한다"는 것은 외부에서 의존성 객체를 생성하고, 그것을 클래스의 생성자, 세터 메서드, 또는 필드에 주입하는 방식을 의미한다. 이렇게 외부에서 의존성을 주입받는 방식을 통해 클래스는 의존성을 직접 생성하거나 결정하지 않고, 외부에서 주입받은 의존성을 사용하게 된다.

  • 의존성 주입을 사용하면 클래스는 외부에서 주입받은 의존성 객체를 사용하므로, 의존성과 클래스 간의 결합도가 낮아지며 유연성과 재사용성이 향상된다. 이는 객체 지향 설계 원칙 중 하나인 "의존성 역전 원칙(Dependency Inversion Principle)"을 따르는 것이기도 하다.


2. Spring Boot에서의 DI(Dependency Injection)

Spring Boot의 의존성 관리

  • Spring Boot는 Maven 또는 Gradle을 사용하여 프로젝트의 의존성을 관리한다.

  • Starter 패키지는 특정한 기능 또는 모듈을 포함한 의존성 집합으로, 간단하게 필요한 의존성을 추가할 수 있도록 도와준다.
    자동 구성은 Spring Boot가 클래스패스 상의 의존성을 검색하여 자동으로 설정하는 기능이다.

Spring Framework의 DI 기능

  • Spring FrameworkDI(Dependency Injection)를 지원하여 의존성을 주입하는 다양한 방법을 제공한다.

  • @Autowired 어노테이션을 통해 의존성을 주입받을 수 있으며 생성자 주입, 세터 주입, 필드 주입 등의 방식을 사용하여 의존성을 주입할 수 있다.

코드 설명

@Component
public class OrderService {
    private PaymentService paymentService;

    // @Autowired -> 생성자가 1개일때는 작성해도 괜찮고 안해도 괜찮다.
    public OrderService(PaymentService paymentService) {
        this.paymentService = paymentService;
    }

    public void placeOrder() {
        // 주문 처리 로직
        paymentService.processPayment();
    }
}
@Component
public class PaymentService {
    public void processPayment() {
        // 결제 처리 로직
    }
}
  • 위의 코드에서 OrderService 클래스는 PaymentService 의존성을 @Autowired 어노테이션을 통해 주입받고 있다. (DI)

3. Autowired란?

  • @AutowiredSpring Framework에서 사용되는 어노테이션으로, 의존성 주입(Dependency Injection)을 수행하는 데에 사용된다. 주로 생성자, 필드, 세터 메서드에 적용되어 해당 위치에 자동으로 의존성을 주입한다.

@Autowired의 주요 특징은 다음과 같다.

  1. 자동 의존성 주입:

    • @Autowired를 사용하면 Spring은 해당 타입에 맞는 빈(Bean)을 찾아 자동으로 의존성을 주입한다. 스프링 컨테이너에서 빈으로 등록된 클래스나 인터페이스의 인스턴스를 주입할 수 있다.
  2. 타입 기반 매칭:

    • @Autowired는 주입할 의존성을 찾을 때 타입을 기반으로 매칭한다. 타입이 일치하는 빈이 여러 개인 경우, 다른 해결 방법이 필요하다.
  3. 필수 의존성:

    • 기본적으로 @Autowired로 주입되는 의존성은 필수적으로 존재해야 한다. 즉, 주입할 수 있는 빈이 없으면 예외가 발생한다. 필수 의존성이 아닌 경우, required 속성을 false로 설정하여 주입할 수 없는 경우에도 예외가 발생하지 않도록 설정할 수 있다.
  4. 다양한 위치에서 사용 가능:

    • @Autowired는 생성자, 필드, 세터 메서드 등 다양한 위치에서 사용할 수 있다. 생성자 주입은 불변성과 의존성의 명시적 표현을 위해 권장되는 방식이다.

주입 방식

  • 필드 주입은 간단하고 편리하지만 테스트 등에서 유연성이 떨어질 수 있다. setter 주입은 선택적인 의존성에 사용될 수 있다.

  • 다른 주입 방식과 함께 사용 가능:

    • @Autowired는 다른 주입 방식과 함께 사용될 수 있다.

예를 들어, @Autowired와 @Qualifier를 함께 사용하여 주입할 빈을 명시적으로 선택할 수 있다.

  • 스프링의 컴포넌트 스캔과 함께 사용:

    • @Autowired는 스프링의 컴포넌트 스캔 기능과 함께 주로 사용된다. @ComponentScan을 설정하면 스프링은 해당 패키지를 스캔하여 @Autowired 어노테이션이 붙은 필드, 생성자, 세터 메서드 등을 자동으로 주입해준다.

    • @Autowired를 사용하면 의존성을 명시적으로 주입하지 않고도 스프링이 자동으로 의존성을 주입해서 해결해준다. 이를 통해 코드의 가독성과 유지보수성이 향상되며, 객체 간의 결합도를 낮출 수 있다.

객체 간의 결합도를 낮춘다는 것의 의미

  • 객체 간의 결합도를 낮춘다는 것은 한 객체가 다른 객체에 대해 의존성을 최소화하는 것을 의미한다.

  • 객체 간의 결합도가 높으면 한 객체의 변경이 다른 객체에 영향을 주는 경우가 많아지며, 코드의 유연성과 확장성이 떨어진다. 반면에 결합도가 낮으면 한 객체의 변경이 다른 객체에 미치는 영향이 적어져 코드의 유연성과 재사용성이 향상된다는 장점이 있다.

의존성 주입(Dependency Injection)은 객체 간의 결합도를 낮추는 방법 중 하나이다.

  • 의존성을 주입받는 대신 의존성을 직접 생성하거나 결정하지 않고, 외부에서 주입받는 것이다. 이를 통해 객체는 외부로부터 필요한 의존성을 주입받아 사용하므로, 객체 간의 결합도가 낮아지게 된다.

  • 예를 들어, 클래스 A가 클래스 B에 직접 의존한다고 가정해보자

    • A 클래스는 B 클래스의 인스턴스를 생성하고 사용한다. 이 경우에, A 클래스와 B 클래스는 서로 강한 결합을 가지게 되며, A 클래스가 변경되면 B 클래스에 영향을 줄 수도 있다.

    • 하지만 의존성 주입을 사용한다면 A 클래스는 B 클래스를 직접 생성하지 않고 외부에서 주입받아 사용하게 된다.
      이렇게 하면 A 클래스와 B 클래스는 느슨한 결합을 갖게 되며, A 클래스의 변경이 B 클래스에 영향을 덜 주게 된다. 또한, B 클래스를 다른 구현체로 교체하거나 테스트를 용이하게 진행할 수 있다.

    • 즉, 의존성 주입을 통해 객체 간의 결합도를 낮추면 한 객체의 변경이 다른 객체에 영향을 덜 주고, 코드의 유연성과 재사용성을 향상시킬 수 있다.


4. 스프링의 DI 적용방식

생성자 주입

  • OrderService 클래스의 생성자에 PaymentService 타입의 매개변수를 추가하고, @Autowired 어노테이션을 사용하여 주입받는다.
    이렇게 함으로써 OrderService 객체가 생성될 때 Spring은 적절한 PaymentService 인스턴스를 주입하여 의존성을 해결한다.
@Autowired
public OrderService(PaymentService paymentService) {
    this.paymentService = paymentService;
}

필드 주입

  • OrderService 클래스의 멤버 변수로 PaymentService 타입의 필드를 선언하고, @Autowired 어노테이션을 사용하여 주입받는다.
    이렇게 함으로써 Spring은 OrderService 객체가 생성되면서 해당 필드에 적절한 PaymentService 인스턴스를 주입한다.
@Autowired
private PaymentService paymentService;

setter 주입

  • OrderService 클래스에 PaymentService 타입의 세터 메서드를 추가하고, @Autowired 어노테이션을 사용하여 주입받는다.
    이렇게 함으로써 Spring은 OrderService 객체가 생성된 후에 적절한 PaymentService 인스턴스를 주입한다.
@Autowired
public void setPaymentService(PaymentService paymentService) {
    this.paymentService = paymentService;
}
  • 위의 예시 코드에서 @Autowired 어노테이션을 사용하면 Spring은 해당 타입에 맞는 빈(Bean)을 찾아 의존성을 자동으로 주입한다. 이를 통해 개발자는 직접 의존성을 생성하거나 결정할 필요 없이 DI를 간편하게 구현할 수 있다.

5. DI의 다양한 활용 사례

테스트 용이성의 향상

  • 의존성 주입을 통해 모의 객체(Mock Object)를 주입하여 테스트의 의존성을 분리할 수 있다.

  • 테스트할 대상과 의존하는 객체를 분리하여 단위 테스트를 수행할 수 있다.

모듈성과 재사용성의 향상

  • 의존성 주입을 통해 모듈 간의 결합도를 낮출 수 있다.

  • 인터페이스를 통해 의존성을 주입받아 구현 세부사항에 대한 의존성을 분리할 수 있다(다형성). 이를 통해 모듈의 재사용성과 유지보수성을 향상시킬 수 있다.

AOP (Aspect-Oriented Programming)의 구현

  • AOP는 핵심 로직과 부가적인 기능을 분리하여 모듈화하는 프로그래밍 패러다임이다.

  • 의존성 주입을 통해 AOP를 구현할 수 있으며, 횡단 관심사(Cross-cutting Concerns)를 분리하여 적용할 수 있다.

  • 예를 들어, 로깅, 트랜잭션, 보안 등의 부가적인 기능을 의존성 주입을 통해 구현할 수 있다.


6. 가장 권장되는 DI방식인 생성자 주입에 대한 설명

생성자 주입 방식으로 DI를 할때의 예시

@Component // Spring Bean으로 등록해 준다.
public class JinanNoJam{
    private final int field1;
    private int field2;

    @Autowired
    public JinanNoJAM(int field1) {
        this.field1 = field1;
    }

    public JinanNoJAM(int field1, int field2) {
        this.field1 = field1;
        this.field2 = field2;
    }
}

예시코드 설명

  • JinanNoJam 클래스는 final로 선언된 필드를 하나 가졌다. 여기서 자바의 기초가 나온다.
    클래스를 외부에서 생성(new)해서 만들어서 사용할때 생성하고자 하는 클래스의 내부 필드에 final로 선언된 값이 있다면 그것은 무조건 값을 외부에서 할당(주입)시켜줘야 한다.

  • 즉, 아래 코드에선 private final int field1; private int field2; 이렇게 2개의 값이 필드에 선언되어있는데 이중 filed1만 final로 선언되었다. 그렇기 때문에 이 JinanNoJam클래스를 외부에서 생성하려면 무조건 filed1의 값을 생성할때 주입시켜줘야 한다.

  • 이 원리로 스프링은 의존성을 주입받을 객체를 필드에 선언할때 앞에 private final을 선언해 주는것이다. → 꼭 외부에서 주입을 받아야 하기 때문이다.

Spring에서 private final로 필드를 선언하고 주입받는 주요 이유는 안정성과 불변성을 보장하기 위함이다.

  1. 안정성(Safety):
    • 필드를 private으로 선언하면 외부에서 직접 접근할 수 없으므로 캡슐화를 통한 안정성을 제공한다. 외부에서 필드에 직접 접근하지 않고, 주입된 값을 통해 필드에 접근하도록 제한함으로써 의도치 않은 변경이나 오용을 방지할 수 있다.
  1. 불변성(Immutability):
    • 필드를 final로 선언하면 해당 필드는 객체가 생성된 이후에는 값을 변경할 수 없다. 이는 불변 객체(Immutable Object)를 유지할 수 있게 해준다. 불변 객체는 객체의 상태가 변경되지 않으므로 스레드 안정성 및 예측 가능한 동작을 보장하고, 병행성(Concurrency) 문제를 해결하는 데 도움을 준다.

불변성과 싱글톤은 보통 함께 사용될 때 많은 이점을 제공한다. 싱글톤 객체가 불변하다면 다음과 같은 장점이 있다.

  1. 상태의 일관성:

    • 싱글톤 객체가 불변하면 객체의 상태가 변경되지 않으므로 일관된 상태를 유지할 수 있다. 다중 스레드 환경에서도 안전하게 사용할 수 있다.
  2. 병행성(Concurrency):

    • 싱글톤 객체의 불변성은 다중 스레드 환경에서 병행성 문제를 해결하는 데 도움을 준다.

    • 여러 스레드에서 동시에 접근해도 객체의 상태가 변경되지 않아 동기화나 락을 사용하지 않고도 안전하게 사용이 가능하다.

  3. 캐싱:

    • 싱글톤 객체가 불변하면 객체를 한 번 생성하고 재사용할 수 있다. 이는 리소스 사용량을 줄이고 성능을 향상시킨다.

    • 불변성을 가진 싱글톤 객체는 일반적으로 상태 변경이 필요하지 않은 공유 리소스에 대해 효과적이다. 이는 스레드 안전성과 예측 가능한 동작을 제공하며, 객체의 일관된 상태를 유지할 수 있도록 한다.

    • 또한, private final 필드는 객체 생성 시점에 반드시 초기화되어야 한다는 규칙을 강제한다. 이는 객체를 사용하기 전에 필요한 의존성이 모두 주입되었는지 확인할 수 있고, 의존성이 누락되는 상황을 방지할 수 있다.

profile
탐구하는 Backend 개발자

0개의 댓글