Spring | DI(Dependency Injection, 의존성 주입)

바다·2023년 6월 20일
0

Spring

목록 보기
3/13
post-thumbnail

Dependency Injection(의존성 주입)

의존성 주입이란?

Spring 프레임워크가 지원하는 핵심 프로그래밍 모델 중 하나로, 객체 간의 "의존 관계"를 빈 설정 정보를 바탕으로 컨테이너가 자동으로 연결해주는 것을 말한다.

  • 강하게 결합된 클래스들을 분리
  • 외부에서 두 객체 간의 관계를 결정해주는 디자인 패턴
  • 인터페이스를 사이에 둬서 클래스 레벨에서는 의존관계가 고정되지 않도록 하고, 런타임 시에 관계를 동적으로 주입

"의존성"이란?

A가 B를 의존하고 있다.
라고 하였을 때, 의존 대상인 B가 변화하면 그것이 A에 영향을 미친다면 그것을 "의존관계"라고 한다.

코드 "의존성" 확인하기

public class Cafe {
	private Coffee coffee()
    
    public Cafe() {
    	coffee = new Coffee();
}

public class Coffee {}

class Cafe가 class Coffee를 필드로 가질 때, Cafe는 Coffee에 의존하게 된다.

public class Cafe {
	//컴파일 에러 발생
	private Coffee coffee()
    
    public Cafe() {
    	coffee = new Coffee();
}

public class Coffee2 {}

기존 Coffee 클래스의 이름이 Coffee2로 변경된다면, Coffee 클래스를 사용하던 다른 객체들도 함께 변경되어야 한다.

문제점

  • 두 클래스가 강하게 결합되어 있다. (유연성이 떨어진다.)
  • 객체들 간의 관계가 아니라 클래스 간의 관계가 맺어진다.

의존성을 "인터페이스"를 사용해 추상화하기

public class Cafe {
	//상위 타입을 필드로 설정
	private Coffee coffee()
    
    //직접 객체를 생성하지 않고 생성자를 통해 전달 받음
    public Cafe(Coffee coffee) {
    	this.coffee = coffee;
}

//상위 타입으로 사용할 인터페이스
public interface Coffee {}

//인터페이스 구현 클래스
public class Americano implements Coffee {}
  • Americano라는 구체적인 구현체의 타입을 사용하는 대신 Coffee라는 상위 인터페이스 타입으로 필드를 선언하고, 직접 객체를 생성하는 구문이 사라졌다.
  • 실제로 사용하는 구현체가 Americano에서 다른 타입으로 변경 되더라도 A의 코드는 변경될 필요가 없다.

그러면 DI는 왜 쓰나요?

앞의 방식들은 Cafe 클래스가 내부적으로 의존관계인 Coffee가 어떤 값을 가질지 "직접" 정하고 있다. 하지만, Spring의 DI는 그것을 외부에서 결정하고 자동으로 주입해준다.

DI 장점

1. 결합도가 줄어든다.
어떤 객체가 다른 객체에 의존한다는 것은, 그 의존 대상의 변화에 취약하다는 것이다. DI를 이용하면 주입받는 대상이 바뀔지 몰라도 해당 객체의 구현 자체를 수정할 일은 없어진다.

2. 재사용성이 높은 코드가 된다.
기존 Cafe 내부에서만 사용되었던 Coffee를 별도로 구분하여 구현하면, 다른 클래스에서 재사용할 수가 있다.

3. 테스트하기 좋은 코드가 된다.
DI를 이용한 객체를 자신이 의존하고 있는 인터페이스가 어떤 클래스로 구현되어 있는지 몰라도 된다. 따라서, 테스트하기 더 쉬워진다.

4. 가독성이 높아진다.
클래스 내의 기능들을 별도로 분리하게 되어 자연스레 가독성이 높아진다.

DI 구현 방법

포스트에서는 Java Config 방식만 설명하고, XML Config 방식은 소개하지 않겠습니다 :)

  • Spring IoC 컨테이너에서 @Component Annotation이 붙은 객체를 Component Scan하여 Bean으로 등록한다.
  • @Autowired Annotation은 Type을 통한 DI를 할 때 사용한다. 스프링 컨테이너가 알아서 해당 타입의 Bean을 찾아서 주입해준다.

1. 필드(Field) 주입

변수 선언부에 @Autowired Annotation을 붙인다.

@Component
public class CafeController {
    @Autowire
    private CafeService cafeService;
}    

2. 새터(Setter) 주입

set Method를 정의해서 사용한다.

@Component
public class CafeController {
	private CafeService cafeService;
    
    @Autowired
    public void setCafeService(CafeService cafeService) {
    	this.cafeService = cafeService;
 	}
}    
       

3. 생성자(Constructor) 주입

Constructor에 @Autowired Annotaion을 붙여 의존성을 주입받을 수 있다.

@Component
public class CafeService {
	private CafeDAO cafeSDAO;
    
    @Autowired
    public setCafeService(CafeDAO cafeSDAO) {
    	this.CafeDAO = cafeSDAO;
 	}
}

@Component
public class CafeController {
	private final CafeService cafeService = new CafeService(new CafeDAO());
       

Spring 4.3 버전 이후로는 생성자가 한 개 뿐이라면, @Autowired Annotation을 생략해도 자동으로 생성자 주입이 동작한다. 단, 생성자가 2개 이상일 경우에는 명시적으로 작성을 해주어야 한다.

생성자 주입 방식을 사용해야 하는 이유

  • 객체가 생성될 때 모든 의존성이 주입되므로 의존성을 보장할 수 있다.

    • 필드 주입/새터 주입은 의존성이 있는 객체가 생성되지 않아도 객체 생성은 가능하여 메소드가 호출되면(런타임) 오류가 발생한다.
    • 생성자 주입은 의존성이 있는 객체가 생성되지 않으면 객체 생성이 불가능하여 어플리케이션 실행 시점에 오류가 발생한다. (개발자가 체크하기 쉽다.)
  • 객체의 불변성을 보장할 수 있다.

    • 필드에 final 키워드를 사용할 수 있고, 객체 생성 이후 의존성을 변경할 수 없어 안정성이 보장된다.
  • 코드 가독성이 좋다.

    • 해당 객체가 어떤 의존성을 가지고 있는지 명확히 알 수 있다.
  • DI 컨테이너와의 결합도가 낮기 때문에 테스트하기 좋다.

    • 스프링 컨테이너 없이 테스트를 할 수 있다.
profile
ᴘʜɪʟɪᴘᴘɪᴀɴs 3:14

0개의 댓글