슬기로운 개발생활

[Spring] 구현 클래스가 아닌 인터페이스에 DI(의존성 주입)하는 이유

by coco3o
반응형

회사 코드를 보다가 인터페이스를 @Autowired 하여 사용하는 것을 보고
"왜 구현 클래스가 아닌 인터페이스에 DI(Dependency Injection) 하는거지?" 하는 의문이 들어 찾아보았다.

결론부터 얘기하면 구현 클래스가 아닌 인터페이스를 의존하는 것은 객체지향 설계원칙(SOLID) 중
개방 폐쇄의 원칙(OCP)과 의존성 역전 원칙(DIP)을 기반한 전략 패턴이었다.

※ 전략 패턴 ?
객체들이 할 수 있는 행위 각각에 대해 전략 클래스를 생성하고, 유사한 행위들을 캡슐화 하는 인터페이스를 정의하여, 객체의 행위를 동적으로 바꾸고 싶은 경우 직접 행위를 수정하지 않고 전략을 바꿔주기만 함으로써 행위를 유연하게 확장하는 방법을 말한다.
즉, 객체가 할 수 있는 행위들 각각을 전략으로 만들어 놓고, 동적으로 행위의 수정이 필요한 경우 전략을 바꾸는 것만으로 행위의 수정이 가능하도록 만든 패턴이다.
출처

다음 이미지를 보자.

 

1. 개방 폐쇄의 원칙(Open-Closed Principle) 준수

컨트롤러 입장에서는 Service 클래스의 비즈니스 로직에 대해서는 관심이 없다.
그냥 인터페이스에 있는 메소드를 이용해 로직을 구성할 뿐이기 때문에 Service 클래스의 내부 로직이 어떻게 변하던 영향을 받지 않는다. (변화에 닫혀 있다.)
반대로 Service 클래스는 인터페이스에서 규정된 규칙만 잘 지키면 언제든 로직을 변경할 수 있고,
인터페이스를 구현한 새로운 클래스를 하나 만들어서 기존 클래스를 대체할 수도 있다. (확장에는 열려 있다.)

 

2. 의존성 역전 원칙(Dependency Inversion Principle) 준수

Service 클래스가 인터페이스를 구현하고 있다면 코드 상에서 컨트롤러는 인터페이스만을 바라보고 있다.
인터페이스는 컴파일 시점에 어떤 클래스를 담을지 결정하지 않고 런타임 시점에 스프링 컨테이너에 존재하는 인터페이스 구현체 빈 중 하나를 주입받게 된다.
즉, 구현체가 런타임 시점에 지정되므로 컨트롤러는 실제 구현 클래스가 누군지 런타임 시점까지 알지 못하며, 컨트롤러와 서비스는 느슨한 결합도를 가지게 된다.

위에서 인터페이스가 없다고 가정하게 되면 컨트롤러와 서비스는 매우 강한 결합도를 가지게 되며, 만약 서비스를 다른 클래스로 변경하게 되면 컨트롤러도 마찬가지로 수정해줘야 한다.
이는 개방 폐쇄 원칙에 어긋남과 동시에 의존성 역전의 원칙에도 어긋나게 된다.

구현 클래스를 직접 의존하는 것보다 중간에 추상화 계층을 만들어서 의존성 주입을 해주는 것이 확실히 객체지향적인 전략 패턴을 갖는데,
그렇다면 클래스를 만들때마다 인터페이스를 만들어주면서 결합도를 낮추고 확장성을 유지 해야할까?

대답은 NO.
개방 폐쇄의 원칙(OCP), 의존성 역전 원칙(DIP)에 기반한 전략 패턴에 따라 인터페이스로 주입 받음으로써 클래스들의 기능 분리와 확장성을 추구하는게 맞지만, 이는 설계의 영역일 뿐이다.

두 클래스의 기능이 의존적이라 한 쪽이 바뀔 때 다른 한 쪽도 바껴야하는 구조라면 굳이 인터페이스를 사용해 전략 패턴을 구사하지 않아도 된다.
또한 향후 확장성이나 변동성을 고려하지 않아도 되는 서비스라면 굳이 전략 패턴을 사용 할 이유가 없다.
다만, 강한 결합으로 이루어진 객체 구조는 객체지향적이지 않다는 측면에서 그리 권장되는 구조는 아니다.

따라서 단순히 인터페이스 사용이 권장되니까 모든 클래스마다 인터페이스를 만들어서 사용하는 것은 전혀 도움 되지 않는 행위라고 생각한다.
인터페이스를 미리 완벽하게 작성해서 사용하는 것은 현실적으로 어려울 뿐더러 구현체를 만들면서 역으로 인터페이스를 수정하는 일이 계속 발생할텐데, 기능 확장이 없을 때 이런 과정을 반복하는 것은 의미가 없다.

만약 인터페이스를 사용해 전략 패턴을 구사한다면 기능 분리와 확장성을 고려한 명확한 이유가 있어야 할 것이다.

참고

반응형

블로그의 정보

슬기로운 개발생활

coco3o

활동하기