Spring

의존성 주입(Dependency Injection, DI)

신동편 2023. 7. 23. 22:10
728x90

의존관계란?

 

"A가 B를 의존한다."는 것은 "의존대상 B가 변하면, 그것이 A에 영향을 미친다."라고 말할 수 있다.

 즉, B의 기능이 추가 또는 변경되면, 그것으로 인해서 A에도 영향이 미친다는 것이다.

 

예를 들자면, 어떤 제품의 설계도가 바뀌었을 때 그 설계도로 인해서 생산자가 그 제품을 만드는 방법을 수정해야 한다.

설계도가 변함으로써 생산자의 행위에 영향을 미쳤다. 이를 "생산자가 설계도에 의존한다."라고 말할 수 있다.

class Producer {
	private Blueprint blueprint
    
    public Producer() {
    	blueprint = new Blueprint();
    }
}

코드로는 이렇게 표현할 수 있겠다.

 

두 클래스가 강하게 결합되어 있고, 객체들 간의 관계가 아니라 클래스 간의 관계가 맺어져 있는 문제가 있다.

 

위 코드에서, 생산자는 단 하나의 제품만을 생산할 수 있도록 구현되어 있다.

하지만 더 다양한 설계도를 의존받을 수 있게 구현하려면 인터페이스로 추상화해야 한다.

 

의존관계를 인터페이스로 추상화하게 되면, 더 다양한 의존관계를 맺을 수 있고, 실제 구현 클래스와의 관계가 느슨해지고, 결합도가 낮아진다.

class Producer {
    private Blueprint blueprint;

    public Producer() {
        blueprint = new ToyBlueprint();
        //blueprint = new ComputerBlueprint();
    }
}

interface Blueprint {
    newToyBlueprint();
    // ...
} 

class ToyBlueprint implements Blueprint {
    public Toy newToyBlueprint() {
        return new Toy();
    }
    // ...
}

 


의존성 주입 (Dependency Injection, DI)

 

위 예시들에서는 Producer 내부적으로 의존관계인 Blueprint가 어떤 값을 갖을지 직접 정하고 있다.

하지만 Blueprint가 어떤 값을 갖을지를 생산자가 일하는 공장의 사장이 정한다고 해보자.

 

그렇게 되면, 생산자가 의존하고 있는 설계도를 외부에서 결정하고 주입하는 것이다.

이 것을 의존관계를 외부에서 결정하고 주입하는 것, 즉, 의존성 주입(DI)라고 한다.

 

이 DI를 사용했을 때 다음과 같은 장점이 있다.

 

1. 단일 책임 원칙(Single Responsibility Principle, SRP) 준수

DI를 이용하여 객체 간의 의존성을 주입하면, 클래스가 단 하나의 책임만을 가지도록 설계할 수 있다.

클래스가 자신의 의존성을 직접 생성하지 않고 주입받기 때문에 단일 책임 원칙을 지키기 쉬워진다.

 

2. 개방 - 폐쇄 원칙(Open / Closed Principle, OCP) 준수

의존성 주입을 통해 클래스 간의 의존성이 줄어들고, 객체가 서로 느슨하게 결합된다. 이로 인해서 코드 유지보수가 용이해지고, 하나의 클래스를 수정했을 때 다른 클래스의 코드를 변경해야 하는 상황을 방지할 수 있는 유연한 구조를 가질 수 있다.

 

3. 의존관계 역전 원칙(Dependency Inversion Principle, DIP) 준수

객체가 직접 필요로 하는 의존성을 생성하지 않고 외부에서 주입받는다. 이렇게 주입받는 의존성은 주로 인터페이스를 통해 추상화된다. 따라서 상위 수준 모듈은 하위 수준 모듈의 구체적인 구현이 아닌 인터페이스에 의존하게 되어 DIP를 준수하게 된다.

 


의존성 주입 방법

 

1. 생성자 주입

생성자 주입은 객체를 생성할 때 의존성을 주입하는 것을 말한다. 클래스의 생성자를 통해 의존하는 객체들을 인자로 받아 필드에 할당하는 방법이다. 한 번 생성되면 의존성이 변경되지 않으므로 불변성을 가진 객체를 만들 수 있다.

@Service
public class Producer {
    private Blueprint blueprint;

    @Autowired
    public Producer(Blueprint blueprint) {
        this.blueprint = blueprint;
    }
}

//in Java
public class Producer {
    private Blueprint blueprint;

    public Producer(Blueprint blueprint) {
        this.blueprint = blueprint;
    }
}

public class PactoryOwner {
    private Producer producer = new Producer(new ToyBlueprint());

    public void changeBlueprint() {
        producer = new Producer(new = ComputerBlueprint());
    }
}

 

2. Setter 주입

setter Method를 통해서 의존성을 주입하는 방식이다. 객체 생성 후에도 의존성을 변경할 수 있다.

@Autowired로 주입할 대상이 없는 경우에는 오류가 발생한다. 주입할 대상이 없어도 동작하도록 하려면 @Autowired(required = false)를 통해 설정할 수 있다.

@Service
public class Producer {
    private Blueprint blueprint;

    @Autowired
    public void setBlueprint(Blueprint blueprint) {
        this.blueprint = blueprint;
    }
}

//in Java
public class Producer {
    private Blueprint blueprint = new ToyBlueprint();

    public void setBlueprint(Blueprint blueprint) {
        this.blueprint = blueprint;
    }
}

public class PactoryOwner {
    private Producer producer = new Producer();

    public void changeBlueprint() {
        producer.setBlueprint(new ComputerBlueprint());
    }
}

 

3. Field 주입

필드 주입은 필드 자체에 의존성을 주입하는 방식이다.

의존성을 주입하고자 하는 필드에 @Autowired 애노테이션을 붙이면 된다.

 

@Service
public class Producer {
    @Autowired
    private Blueprint blueprint;

    // ...
}

 

필드 주입 방식은 아래와 같은 절차로 동작한다.

 

주입받으려는 빈의 생성자를 호출하여 빈을 찾거나 빈 팩토리에 등록하고, 생성자 인자에 사용하는 빈을 찾거나 만든다.

그리고 필드에 주입하는 것이다.

 

해당 방식의 문제는 주입할 빈이 없어도 빈 생성이 가능하다는 것이다. 즉, 필드에 의존성을 주입하지 않아도 스프링에서 에러가 터지지 않는다.

 

그래서 앱이 구동된 이후 실제로 메서드를 호출할 때 NPE가 발생하게 된다.

또한 필드 주입은 반드시 DI 프레임워크가 존재해야 하므로 반드시 사용을 지양해야 한다.

따라서 실제 코드와 무관한 테스트 코드나 설정을 위해 불가피한 경우에만 이용하자.

 


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

 

스프링 공식 문서

 

 

1. 불변성과 안정성

생성자 주입 방식은 객체를 생성할 때 모든 필수 의존성을 주입받기 때문에 객체의 불변성과 안정성을 보장할 수 있다. 한 번 생성된 후에는 불변하므로 의존성이 중간에 변경되지 않다.

 

수정자(setter) 주입을 이용하면 수정 가능성을 열어두는 것이다. 실제로 개발을 할 때 의존 관계의 변경이 필요한 상황은 거의 없다. 따라서 수정의 가능성을 열어두는 것은 불필요하고, 유지보수성을 떨어뜨린다.

 

따라서 생성자 주입을 통해서 변경의 가능성을 아예 배제하고 불변성을 보장하는 것이 좋다.

 

2. 테스트 코드

테스트 코드는 순수 자바 코드로 작성하는 것이 가장 좋다. 테스트 코드가 특정 프레임워크에 의존하는 것은 침투적이므로 좋지 못하기 때문인데, 생성자 주입이 아닌 다른 방법으로 작성된 코드는 순수 자바 코드로 단위테스트를 작성하는 것이 어렵다.

@Service
public class Producer {
    private Blueprint blueprint;

    @Autowired
    public Producer(Blueprint blueprint) {
        this.blueprint = blueprint;
    }
    
    public int productionYear() {
    	return 2023;
    }
}

//test code
class ProducerTest {

    @InjectMock
    Producer producer
    
    @Mock
    Blueprint blueprint;
    
    @Test //Success
    void ProducerTest() {
    	// given
    	Producer producer = new Producer(blueprint);
        
        //when & then
        assertEquals(2023, producer.productionYear());
    }
}

테스트 코드에서 @Autowired를 사용하기 위해 스프링을 사용하면 단위 테스트가 아니게 된다.

또, 컴포넌트들을 등록하고 초기화하는 시간 때문에 테스트 비용이 증가하게 된다.

그렇다고, 대안으로 리플렉션을 사용하면 깨지기 쉬운 테스트가 된다.

 

반면 위와 같이 생성자 주입을 사용하면, 컴파일 시점에 객체를 주입받아 테스트 코드를 작성할 수 있으며, 주입하는 객체가 누락될 경우 컴파일 시점에서 캐치할 수 있다.

 

3. final / Lombok

생성자 주입을 사용할 때 필드 전체에 final키워드를 사용할 수 있다.

Lombok의 @RequiredArgsConstructor 어노테이션과 결합하여 코드를 간결하게 작성할 수 있다.

 

컴파일 시점에 누락된 의존성을 확인할 수 있게 된다.

반면, 다른 방법들은 객체의 생성 이후에 호출되기 때문에 final키워드를 사용할 수 없다.

 

@Service
@RequiredArgsConstructor
public class Producer {

    private final Blueprint blueprint;

    public int productionYear() {
    	return 2023;
    }
}

 

4. 순환참조 에러 방지

생성자 주입을 사용하면 애플리케이션 구동 시점(객체의 생성 시점)에 순환 참조 에러를 예방할 수 있다. 

 

@Service
public class Producer {
    @Autowired
    private Blueprint blueprint;

    public void create(String toyName) {
    	blueprint.draw(toyName);
    }
}

@Service
public class Blueprint {
    @Autowired
    private Producer producer;

    public void draw(String toyName) {
    	producer.create(toyName);
    }
}

Pruducer와 Blueprint는 서로를 의존하고 있다.

 

위 두 메서드는 계속해서 서로를 호출할 것이고, 메모리에 CallStack이 쌓여 StackOverFlow에러가 발생하게 될 것이다.

이러한 순환참조 문제를 생성자 주입을 사용하면 방지할 수 있다.

 

이유는 애플리케이션 구동 시점(객체의 생성 시점)에 에러가 발생하는데, 그러한 이유는 Bean에 등록하기 위해 객체를 생성하는 과정에서 순환 참조가 발생하기 때문이다.

 

 

 

 

728x90