개요
스프링을 처음 배우면 누구나 IoC와 DI에 대해서 학습을 하지만 작성자처럼 개발자가 개발에만 집중할 수 있게 하기 위해 객체의 생성과 관계 설정 같은 번거로운 작업들을 컨테이너에 떠넘기는 것 혹은 이와 비슷하게 개념적으로만 그런가 보다 하고 넘어가는 일들이 많은 거 같다. (아님 말고...)
그래서 스터디에서 발표도 준비할 겸 토비의 스프링을 다시 정독하면서 DI가 무엇이고, 왜 필요한지, 적용함으로 얻을 수 있는 이득 등에 대해 최대한 간단하게 살펴보려 한다. (쓰다 보면 길어질 수도 있다)
객체 스스로 사용할 객체를 선택하고 생성하는 것이 맞을까?
클라이언트의 주문에 대한 처리를 담당하는 주문 서비스 객체의 관심사는 무엇일까라고 생각하면 당연히 요청에 맞게 주문을 처리하는 것이다. 하지만 만약 객체 스스로 자신이 사용할 객체를 선택하고 생성한다면 이에 대한 관심사까지 담당하게 된다고 볼 수 있다.
서로 다른 관심사끼리 응집되어 있다면 변경 시 서로의 변화에 영향을 받기 때문에 서로에게 영향을 주지 않기 위해 DI를 사용해 주문 서비스 객체의 핵심 관심사와 종류가 다른 관심사를 분리시킨다.
정적으로 의존 관계를 설정하면 왜 안 되는 걸까?
public class OrderService {
private OrderRepository orderRepository = new OrderRepository();
}
public class OrderRepository {
}
위의 경우는 DI를 적용하지 않았기 때문에 객체 스스로가 사용할 객체를 선택하고 생성하는 방식을 사용한다. 이미 엎드려 보고 물구나무서서 봐도 누구나 OrderService 객체가 OrderRepository 객체를 사용한다는 것을 알 수 있다.
이런 정적인 의존 관계 설정 방식을 사용하면 설계와 코드 레벨에서 객체 간의 의존 관계가 직접적으로 드러나게 된다. 이게 왜 문제일까 생각해 보면 위에서 언급한 관심사의 분리 측면에서의 문제도 있지만, 클라이언트 객체(OrderService)가 자신이 사용할 서버 객체(OrderRepository)와 긴밀한 관계에 있게 되어 직접적으로 알고 있기 때문에 서버 객체의 변화에 따른 영향을 직접적으로 받게 된다.
동적으로 의존 관계가 설정된다면 어떨까?
동적으로 의존 관계가 설정된다는 것은 설계나 코드 레벨에 객체 간의 실질적인 의존 관계가 직접적으로 드러나지 않고 런타임 시에 의존 관계가 설정되는 것이다. 이 부분도 설명만 들어서는 이해가 어려우니 코드로 살펴보겠다.
public class OrderService {
private OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
public interface OrderRepository {
}
public class OrderRepositoryA implements OrderRepository {
}
public class OrderRepositoryB implements OrderRepository {
}
위의 코드만 보고 OrderService가 어떤 OrderRepository의 구현 클래스를 사용하는지 알 수 있을까?
당연히 알 수가 없다. 필드와 생성자가 구현 클래스의 타입을 직접적으로 명시하지 않고 인터페이스를 타입으로 받고 있기 때문이다. 따라서 두 객체의 연관 관계는 설계나 코드 레벨에서는 OrderRepository의 구현 클래스 중 하나와 설정된다는 것만 알 수 있고, 런타임 시까지는 직접적인 연관 관계를 알 수 없다는 것이다.
그렇다면 외부로부터 주입받으면 무조건 DI일까?
public class OrderService {
private OrderRepository orderRepository;
public OrderService(OrderRepositoryA orderRepositoryA) {
this.orderRepository = orderRepositoryA;
}
}
public interface OrderRepository {
}
public class OrderRepositoryA implements OrderRepository {
}
public class OrderRepositoryB implements OrderRepository {
}
DI의 궁극적인 목표는 의존 관계를 런타임 시에 설정하는 것인데 위의 코드처럼 생성자의 파라미터에 OrderService 객체 스스로 자신이 주입받고 싶은 관계를 직접적으로 명시하고 있다면 DI라고 볼 수 없다.
기존의 정적인 방식과 마찬가지로 설계와 코드 레벨에서 이미 OrderService 객체가 어떤 객체와 연관 관계를 맺을지 알 수 있기 때문이다. 하지만 DI를 의존성 주입이라는 관점에서만 바라본다면 외부로부터 의존성을 주입 받고 있긴 하니 DI라고 볼 수도 있다.
그래도 DI의 가치를 생각해본다면 인터페이스를 활용해 런타임 시에 동적이 관계를 맺고 객체 간의 결합도를 낮추는 것이 객체지향적인 관점에서 더 좋다고 생각한다.
객체 간의 DI를 해주는 객체
이쯤되면 런타임 시에 객체를 생성하고 객체들간의 의존 관계를 설정해주는 역할은 누가 수행하는지 궁금해질텐데, 이런 역할을 수행하는게 바로 많이 들어봤을 스프링 컨테이너다. 이번 글의 주제는 DI이기 때문에 컨테이너에 대해서는 나중에 자세히 다루고 지금은 넘어가겠다.
DI의 장점
DI 자체가 지금까지 살펴봤듯이 객체지향 설계와 프로그래밍 원칙을 따른 기술이기 때문에 이들을 지켰을 때 얻을 수 있는 장점들을 자연스럽게 모두 얻을 수 있다고 보면 된다.
관심사의 분리를 통해 응집도가 높은 코드를, 인터페이스를 통해 결합도가 낮은 코드를 작성할 수 있기 때문에 객체의 변경으로 인해 다른 객체에 발생하는 영향을 줄일 수 있고, 코드의 재사용성과 유연성, 유지보수성을 높일 수 있다.
또한, 테스트 수행에 있어서도 대리 객체의 주입을 통해 손쉽게 외부 의존성을 대체하거나 특정 모듈이나 클래스를 고립시키고 테스트할 수 있다.
마치며
정말 최대한 간략하게 정리하려 노력했지만 세상 일이란게 마음대로 되지 않는다. DI라는 개념 자체가 연관된 것들도 많고 워낙 복잡한 개념이다 보니 요점을 전달하면서 간략하게 적는다는게 참 쉽지 않다. 어쨋든 글을 작성하면서 개인적으로도 생각이 정리되다 보니 많은 도움이 되어 다행이다. (글을 읽는 사람들이 도움이 될진 모르겠지만...)
'Back-End > Spring' 카테고리의 다른 글
Spring REST Docs 도입기 (6) | 2024.10.27 |
---|---|
스프링 면접 질문 정리 : 템플릿 (0) | 2024.04.09 |
스프링 면접 질문 정리 : 테스트 (0) | 2024.04.08 |
토비의 스프링으로 면접 준비하기 (0) | 2024.04.08 |
엔티티에서 값을 의미 있게 표현하는 방법은 무엇일까? (0) | 2024.04.05 |