지난 면접에서 왜 DI를 사용하는지에 대한 질문을 받고 명확하게 대답하지 못 해 해당 부분을 다시 공부하고자 한다. 물론 해당 부분은 이전에 공부했었던 부분이다. 다만 기억해내지 못했을 뿐이다. 이번 기회를 통해 다시 한 번 DI를 사용하는 이유와, 스프링 프레임워크에서 지원하는 DI 방법 등을 공부하고 체화시켜보고자 한다.
DI(Dependency Injection)는 왜 사용할까?
가장 기본으로 돌아가보자. 의존성 주입은 왜 사용할까? 기술적인 이유에 앞서, 개념적으로 접근해보고자 한다.
- 의존하는 대상을 직접 생성하는 것은 개념적으로 맞지 않다.
DI는 의존하는 대상을 외부에서 주입해주는 것이다. '의존'한다는 것은 그럼 무엇일까? 말 그대로 의존한다는 것이다. 즉, '특정 대상이 없으면 존재할수가 없다' 는 말이다. 예를 들어, 상품을 주문하는 이커머스 프로그램을 제작하는 과정에서 Order라는 객체를 만들어다고 가정해보자. Order 객체를 만들기 위해선 반드시 Order의 대상인 Product가 있어야 할 것이다. 주문하고자하는 상품이 없으면 당연히 주문도 할 수 없다. 이 때, 우리는 Order가 Product에 의존한다고 할 수 있다.
그럼 생각해보자, 그럼 Order가 스스로 Product를 만들어내는 것이 옳을까? 아래와 같은 코드가 있다고 가정해보자.
public class Order {
private final Product product;
public Order() {
this.product = new Product();
}
}
위 코드에서는 주문이 스스로 자신에게 할당된 상품을 만들어내고 있다. 여기서부터가 말이 안된다. 아니 내가 주문할 상품은 내가 정해야지, 갑자기 주문에 상품이 자기 멋대로 들어가버린다고? 그건 그렇다 쳐, 근데 이건 없는 상품인데 어떻게 주문에 들어가있는거야?
우리가 작성한 코드는 우리가 시키는데로 돌아가는 코드일 뿐이지 연금술이 가능한게 아니다. 저런 식으로 자기에게 필요한 의존 객체를 마음대로 만들 수 있다면 애초에 의존이라는 개념 자체가 발생하지 않는다. 특정 객체가 스스로 만들 수 있으면, 그건 객체를 만드는 데 필요한 값이 아니다. 결국 객체가 스스로 자신에게 필요한 의존 객체를 만드는 것은 의존이라는 개념 자체를 부정하는 행위인 것이다.
개념적으로 의존성 주입이라는 것이 왜 필요한지 알아봤으면, 이제는 기술적으로 알아보자.
- DI를 하지 않으면, 클래스 간 결합도가 지나치게 높아진다.
위의 예시를 다시 사용해보자, 만약 Product에 상품 이름 정보가 필요해 이를 추가해야하는 요구사항이 발생하면 어떻게 해야할까? Product 클래스에 String name 이라는 필드를 추가할 것이다. 문제는, 이러한 변경 사항이 Order 클래스에도 영향을 미치게 된다. 즉, 변경이 전파된다.
public class Order {
private final Product product;
public Order() {
// this.product = new Product();
this.product = new Product("상품 이름");
}
}
Product에 이름을 부여하는 요구사항이 추가됐을 뿐인데, Order의 코드에도 수정사항이 발생했다. 만일 Product에 추가적인 요구사항이 더 발생한다면, 그만큼 Order에도 변화가 생기게 된다. 이렇게 결합도가 높은 구조는 작은 변화에도 지나치게 많은 코드 수정을 요할 뿐더러, 코드의 재사용성도 낮춘다. 대신 의존성 주입을 사용하면 아래와 같이 코드를 수정할 수 있다.
public class Order {
private final Product product;
public Order(Product product) {
this.product = product;
}
}
이 구조에서는 Product에 변화가 생겨도 Order에 변화가 발생하지 않는다. 이미 생성된 Product를 Order에 넣어주고, Order는 이를 받아서 사용하기만 하면 된다. 의존성 주입을 이용하면 이와 같이 코드 간 결합도를 낮출 수 있다.
스프링에서의 의존성 주입
다음으로 알아볼 부분은 스프링에서의 의존성 주입의 사용 방법이다. 기본적으로 스프링 프레임워크가 없을 때 의존성 주입은 생성자 주입 방법과 setter를 통한 주입 방법이 있다. 스프링 프레임워크를 사용하면 @Autowired 애너테이션을 활용한 필드 주입 방법을 추가적으로 사용할 수 있다. 세 방법의 적용 예시는 아래 코드와 같다.
// 생성자 주입 방법
public class Order {
private final Product product;
public Order(Product product) {
this.product = product;
}
}
// setter 주입 방법
public class Order {
private Product product;
public void setProduct(Product product){
this.product = product;
}
}
// 필드 주입 방법
public class Order {
@Autowired
private Product product;
}
그래서 이렇게 많은 방법 중 어떤 방법을 써야할까? 그냥 셋 다 써도 되는걸까? 아쉽게도 아니다. 스프링 진영에서는 생성자 주입 방법을 사용하기를 적극 권장하고있다.
- 왜 생성자 주입 방법을 사용해야 하나?
1. 객체의 불변성을 보장할 수 있다.
생성자 주입 방법과 다른 방법들의 차이는 의존 객체를 선언할 때 final을 선언할 수 있다는 것이다. final 키워드를 붙임으로써, 우리는 해당 객체가 불변함을 보장할 수 있고 해당 객체의 변경 가능성을 미리 배제할 수 있다. 또한 final 키워드를 붙였기 때문에, 의존 객체 주입의 누락 문제를 컴파일 단계에서 확인할 수 있다.
2. 테스트 코드 작성에 유리하다.
Mockito나 SpringBootTest 등의 도움을 최대한 받지 않고 순수한 자바 코드로 작성하는 것이 더욱 유용한 테스트 코드를 작성하는 방법이다. 만일 필드 주입을 사용하면, 스프링 프레임워크 상에서 해당 테스트 코드가 작성되지 않기 때문에 의존성 주입이 되지 않는다. setter 주입을 사용하면, 1번의 문제가 동일하게 발생한다. 생성자 주입을 사용하면, 컴파일 시점에 의존성 주입이 이루어져 객체가 생성되며 당연히 의존 객체 주입의 누락과 같은 문제를 컴파일 시점에 발견할 수 있게 해준다.
이외에도 순환참조를 미리 발견할 수 있다는 이점도 있으나, 스프링부트 2.6 이후로는 이를 원칙적으로 막아놓기 때문에 따로 기술하지는 않는다.
이번 기회를 통해, 왜 DI를 사용해야 하며, 왜 생성자 주입 방법을 사용해야 하는지를 다시 한 번 익힐 수 있는 시간이 되었다. 사실 당연하게 DI를 하고, 생성자 주입 방법을 사용하고 있었는데 이번 기회를 통해 다시 한 번 당연한 것의 이유를 파악할 수 있는 시간이었다. 다음에 비슷한 질문을 받는 일이 생긴다면, '당연히 그렇게 하는건데요' 같은 답변은 안 할 수 있을 것 같다(물론 위의 내용을 이미 숙지한 상태이며, 다른 사람들도 당연히 알고 있는 것이라고 여겨 이렇게 답변하는 경우는 제외하고).
'Programming > Interview' 카테고리의 다른 글
HTTPS는 왜 쓸까? : HTTPS 동작 과정 알아보기 (0) | 2024.03.04 |
---|---|
JWT 돌아보기 (0) | 2024.02.20 |
난수 테스트는 어떻게 해야할까? (면접 코드 리팩토링) (0) | 2024.02.01 |
String vs StringBuffer vs StringBuilder (0) | 2024.01.19 |
0.1 + 1.1 != 1.2 (0) | 2024.01.18 |
댓글