Skip to main content

Spinrg DI

의존성이란

객체 지향 프로그래밍에서 클래스나 모듈 간의 관계를 의미합니다. 한 클래스가 다른 클래스에 의존한다는 것은 해당 클래스가 다른 클래스의 인스턴스나 메서드를 사용한다는 것을 의미합니다. 의존성은 클래스 간의 결합도를 나타내는 중요한 요소입니다.

의존성 주입이란

의존성 주입은 객체 지향 프로그래밍에서 의존하는 객체를 직접 생성하거나 관리하지 않고 외부에서 주입받는 것을 의미합니다. 즉, 의존성을 클래스 내부에서 생성하거나 결정하지 않고, 외부에서 주입받아 사용하는 방식을 말합니다.

// 의존성 주입을 받은 예제: 장남감 클래스는 LEGO 클래스를 주입 받음
public class ToyService {
private final LegoService legoService; // 레고 서비스에 대한 의존성이 생겼습니다.

public ToyService(LegoService legoService) {
this.legoService = legoService; // 외부에서 주입 받은 서비스
}

public void playToy() {
legoService.engraft(); // 외부에서 주입 받은 서비스 메서드를 호출하여 사용
}
}

// 외부의 클래스
public class LegoService() {
public void engraft() {
// 함치는 로직 추가
}
}

ToyService 클래스는 외부에서 주입 받은 LegoService를 사용할 수 있게 되며, 의존성을 직접 생성하거나 결정하지는 않습니다. 그렇다면 의존성을 직접 생성한다는 것은 무슨말일까요?

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

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

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

Spring Boot에서의 DI

Spring boot의 의존성 관리

Spring BootMaven 또는 Gradle을 사용하여 프로젝트의 의존성을 관리합니다. Starter 패키지는 특정한 기능 또는 모듈을 포함한 의존성 집합으로, 간단하게 필요한 의존성을 추가할 수 있도록 도와줍니다. 자동 구성은 Spring Boot가 클래스패스 상의 의존성을 검색하여 자동으로 설정하는 기능입니다.

Spring Framework의 DI 기능

Spring FrameworkDI를 지원하여 의존성을 주입하는 다양한 방법을 제공합니다. 특히 @Autowired 어노테이션을 통해 의존성을 주입받을 수 있으며 생성자 주입, 세터 주입, 필드 주입등의 방식을 사용하여 의존성을 주입할 수 있습니다.

@Component
public class ToyService {
private final LegoService legoService;

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

public void playToy() {
// 주문 처리 로직
legoService.engraft();
}
}

@Component
public class LegoService() {
public void engraft() {
// 함치는 로직 추가
}
}

위 코드에서 ToyService 클래스는 LegoService 의존성을 @Autowired 어노테이션을 통해 주입받고 있다.

Autowired란

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

  1. 자동 의존성 주입
    1. @Autowired를 사용하면 Spring은 해당 타입에 맞는 빈(Bean)을 찾아 자동으로 의존성을 주입한다. 스프링 컨테이너에서 빈으로 등록된 클래스나 인터페이스의 인스턴스를 주입할 수 있다.
  2. 타입 기반 매칭
    1. @Autowired는 주입할 의존성을 찾을 때 타입을 기반으로 매칭한다. 타입이 일치하는 빈이 여러 개인 경우, 다른 해결 방법이 필요하다.
  3. 필수 의존성
    1. 기본적으로 @Autowired는 생성자, 필드, 세터, 메서드 등 다양한 위치에서 사용할 수 있다. 생성자 주입은 불변성과 의존성의 명시적 표현을 위해 권장하는 방식입니다.

주입 방식

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

@Autowired는 다른 주입 방식과 함께 사용할 수 있습니다.

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

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

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

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

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

객체 간의 결합도를 낮춘다는 것은 한 객체가 다른 객체에 대해 의존성을 최소화하는 것을 의미합니다. 객체 간의 결합도가 높으면 한 객체의 변경이 다른 객체에 영향을 주는경우가 많아지며, 코드의 유연성과 확장서이 떨어진다. 반면에 결합도가 낮으면 한 객체의 변경이 다른 객체에 미치는 영향이 적어져 코드의 유연성과 재 사용성을 향상된다는 장점이 있습니다.

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

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

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

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

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

스프링의 DI 방식

생성자 주입

ToyService 클래스의 생성자는 LegoService 타입의 매개변수를 추가하고, @Autowired 어노테이션을 사용하여 주입받는다. 이렇게 함으로써 ToyService 객체가 생성될 때 Spring은 적절한 LegoService 인스턴스를 주입하여 의존성을 해결한다.

@Autowired
public ToyService(LegoService legoService) {
this.legoService = legoService;
}

필드 주입

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

@Autowired
private LegoService legoService;

setter 주입

ToyService 클래스에 LegoService 타입의 세터 메서드를 추가하고, @Autowired 어노테이션을 사용하여 주입받는다. 이렇게 함으로써 SpringToyService 객체가 생성된 후에 적절한 LegoService 인스턴스를 주입한다.

@Autowired
public void setLegoService(LegoService legoService) {
this.legoService = legoService;
}

위의 예시 코드에서 @Autowired 어노테이션을 사용하면 Spring은 해당 타입에 맞는 빈(Bean)을 찾아 의존성을 자동으로 주입한다. 이를 통해 개발자는 직접 의존성을 생성하거나 결정할 필요 없이 DI를 간편하게 구현할 수 있습니다.

DI의 다양한 활용 사례

테스트 용이성의 향상

의존성 주입을 통해 모의 객체를 주입하여 테스트의 의존성을 분리할 수 있다. 또한 테스트할 대상과 의존하는 객체를 분리하여 단위 테스트를 수행할 수 있다.

모듈성과 재사용성의 향상

의존성 주입을 통해 모듈 간의 결합도를 낮출 수 있다. 또한 인터페이스를 통해 의존성을 주입받아 구현 세부사항에 대한 의존성을 분리할 수 있다. 이를 통해 모듈의 재사용성과 유지보수성을 향상할 수 있다.

AOP (Aspect-Oriented Programming) 구현

AOP는 핵심 로직과 부가적인 개능을 분리하여 모듈화하는 프로그래밍 패러다임이다. 의존성 주입을 통해 AOP를 구현할 수 있으며, 횡단 관심사(Cross-cutting Concerns)를 분리하여 적용할 수 있다. 예를 들어, 로깅, 트랜잭션, 보안 등의 부가적인 기능을 의존성에 주입할 수 있다.

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

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

@Component // Spring Bean으로 등록해 준다.
public class TestClass {
private final int test1;
private int test2;

@Autowired
public TestM(int test1) {
this.test1 = test1;
}

public TestM(int test1, int test2) {
this.test1 = test1;
this.test2 = test2;
}
}

설명

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

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

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

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

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

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

  1. 상태의 일관성
    1. 싱글톤 객체가 불변하면 객체의 상태가 변경되지 않으므로 일관된 상태를 유지할 수 있다. 다중 스레드 환경에서도 안전하게 사용할 수 있다.
  2. 병행성
    1. 싱글톤 객체의 불변성은 다중 스레드 환경에서 병행성 문제를 해결하는데 도움을 준다.
    2. 여러 스레드에서 동시에 접근해도 객체의 상태가 변경되지 않아 동기화나 락을 사용하지 않고도 안전하게 사용이 가능하다.
  3. 캐싱
    1. 싱글톤 객체가 불변하면 한 번 생성하고 재사용할 수 있다. 이는 리소스 사용량을 줄이고 성능을 향상시킨다.
    2. 불변셩을 가진 싱글톤 객체는 일반적으로 상태 변경이 필요하지 않은 공유 리소스에 대해 효과적이다. 이는 스레드 안전성과 예측 가능한 동작을 제공하며, 객체의 일관된 상태를 유지할 수 있도록 한다.
    3. 또한, private final 필드는 객체 생성 시점에 반드시 초기화되어야 한다는 규칙을 강제한다. 이는 객체를 사용하기 전에 필요한 의존성이 모두 주입되었는지 확인할 수 있고, 의존성이 누락되는 상황을 방지할 수 있다.