Service의 interface / Impl 구조에 대한 고찰

스프링에서 Service 를 구현할 때 왜 굳이 Service라는 interface 를 정의하고 ServiceImpl 구현체를 만드는 것일까? 많은 스프링 책에서는 이렇게 해야 의존성 결합을 낮추는 디자인 패턴이라는 점을 강조하며 좋다고 설명하고 있다. 하지만 정확히 실무에서 이 구조가 어떤 이득이 있는지, 어떤 형태의 서비스를 구현할 때 장점이 발휘되는지와 같은 구체적인 설명이 없어 매번 명확히 이해하지 못한 채로 대충 넘기곤 했다. 그러던 중 이 구조에 관한 질의를 받았고, 잘 답변하지 못했었다. 그래서 “다 그렇게 하니까 우리도 하는게 좋아 보인다.” , “그렇게 해왔으니까 관습처럼 하자” 라는 답변을 하지 않기 위해서라도 이번 기회에 스스로 정리해보려 한다.


인터페이스란 무엇인가

Service 를 왜 interface로 만들까? 라는 의문에 해답을 찾기 위해서는 일단 interface 가 무엇이고 Service는 무엇인가? 라는 의문부터 갖게 된다. 우선 근본적으로 인터페이스란 무엇인지 부터 생각해 본다.

자바 프로그래밍 언어에서의 인터페이스의 정의란 무엇인가? 아래는 위키 백과에 정의된 자바의 인터페이스에 대한 내용이다.

인터페이스(interface)는 자바 프로그래밍 언어에서 클래스들이 구현해야 하는 동작을 지정하는데 사용되는 추상 자료형이다. 이들은 프로토콜과 비슷하다. 인터페이스는 interface라는 키워드를 사용하여 선언하며, 메소드 시그너처와 상수 선언(static과 final이 둘 다 선언되는 변수 선언)만을 포함할 수 있다. 자바 8 미만의 모든 버전을 기준으로 인터페이스의 모든 메소드는 구현체(메소드 바디)를 포함하고 있지 않다. 자바 8부터, default와 static 메소드는 interface 정의에 구현체를 가지고 있을 수 있다.

출처 : 위키피디아

위에서 설명하듯 인터페이스의 핵심은 두 가지라 생각한다. 하나는 클래스들이 구현해야 하는 동작(메서드)을 지정 한다라는 것이고, 다른 하나는 추상 자료형이라는 것이다. 인터페이스를 정확히 이해하기 위해서는 이 두 가지 핵심 내용을 하나씩 생각해 볼 필요가 있다.

첫 번째로 “클래스들이 구현해야 하는 동작을 지정” 한다는 말은 단순히 클래스들이 구현해야하는 메서드를 가지고 있다라는 의미이지만, 한 단계 더 생각해보면 같은 인터페이스를 구현하고 있는 클래스들 끼리는 동일한 메서드의 시그니처를 가지고 있다라는 것을 의미한다.

둘 째로 “추상 자료형”이라는 것을 생각해보자. 인터페이스는 메서드의 시그니처만 가지고 있을 뿐 구현체가 없는 생김새를 가지고 있으며 반드시 인터페이스를 사용하는 클래스에서는 모든 메서드를 구현해야 한다는 룰이 존재한다. 이 말은 곧 사용하는 클래스에 구현을 위임한다는 것이고 인터페이스는 이를 통해 사용자에게 메서드 구현을 숨기면서 완전한 추상화를 달성할 수 있게 된다. 그리고 인터페이스는 하나의 자료형 타입으로써 사용 될 수 있기 때문에 “추상 자료형”이라 표현할 수 있다.


인터페이스를 사용하는 이유는 무엇인가

그럼 인터페이스를 사용하는 이유는 무엇일까, 다르게 말하면 인터페이스를 사용해야 하는 때는 어떨 때 인가? 위에서 언급한 인터페이스의 두 가지 핵심 내용을 정리해 보니 해답을 찾을 수 있었다. 위 두 내용을 정리해 보면, 서로 다른 클래스가 동일한 인터페이스를 구현한다고 가정했을 때, 두 클래스는 같은 인터페이스 자료형으로 취급(추상 자료형)할 수 있고 동작은 각각 다르지만 같은 메서드의 시그니처로 호출할 수 있다. 라는 말이 된다. 이 의미는 곧 다형성(Polymorphism) 을 의미 한다. 인터페이스를 사용해야 하는 이유는 바로 다형성을 활용하기 위함이고, 인터페이스를 사용해야하는 때는 다형성이 필요로 할 때이다. 인터페이스의 다형을 활용하려면 인터페이스:구현객체의 1:N 구조가 필요하며 구인 구직 플랫폼의 사용자 모델 같은 경우를 예로 들 수 있다.

구인구직 플랫폼의 사용자 모델은 일반적으로 기업 사용자와, 일반 사용자로 분류되어 서비스를 제공하고 있기 때문에 UserService 라는 인터페이스에 CoperationUserService 와 GeneralUserService 라는 구현체가 존재하고 login, logout, getUserInfo 등 과 같은 추상 메서드를 구현하고 있을 거라는 상상해 볼 수 있다. 또한 1:N 구조이기 때문에 외부에선 어떤류의 사용자임을 코드로 구분하지 않고 다형성을 활용해 UserService 라는 추상 자료형으로 객체를 다뤄 코드의 재사용성을 최소화하고 있을 것이다.


1:1 구조에서의 다형성?

위 내용처럼 인터페이스를 사용하는 이유는 여러 구현체가 필요하여 다형성을 활용할 수 있는 상황에서나 의미 있는 구조임은 분명 맞다. 허나 초기 프로젝트를 구축할 때는 사용자 요구사항이 그렇게 많지 않은 대다가 디테일하지 않을 가능성이 크기 때문에 구현체가 여러개인 서비스를 만들 일은 그다지 많지 않을 수 있다. 때문에 인터페이스가 구현체 하나만을 위해 존재하는 1:1 구조로 많이 개발되고 이로 인해 의미 없는 구조라며 부정하는 이들도 많다.

허나 많은 개발자들이 말하는 것처럼 이 세계에서 사용자의 요구사항은 끊임 없이 변하고 추가될 수 있음을 유념해야 한다. 그래서 이왕이면 미래를 대비하여 모듈을 확장하거나 교체할 때 편리하도록 이 구조로 개발하는 것이 굳이 사용하지 않는 것보다는 좋아 보인다. 반대로, 정말이지 아무리 생각해봐도 절대 이 서비스는 여러 구현체가 존재할 일이 없을 것이라고 판단이되고 모든 팀원들이 동의를 한다면 굳이 의미 없이 맹목적으로 이 구조를 취할 필요는 없을 것이다.


이 구조의 장점

인터페이스의 장점과 동일하게 확장, 교체에 용이하다는 것이다. 확장하게 되면 다형성을 활용하여 추상화된 인터페이스 객체로 서비스를 다루면 편할것이고, 교체하게 되면 호출부분의 코드는 변경할 필요 없이 내부 서비스만 교체가 가능해서 편할것이다. 그런데 이런 편의를 누리기 위해서는 반드시 필요한 전재조건이 존재한다. 바로 잘 추상화 되어 있어야 한다. 이 객체가 인터페이스 객체임을 인지하고 인터페이스의 메서드를 정할 때 추상화 관점에서 많은 고민을 하여 작성해야한다. 그러니까 인터페에스 입장에서 생각해 메서드를 만들어야지 구현 객체 입장에서 메서드를 만들면 안된다는 것이다.

보통 많은 개발자들이 Service 에 비즈니스 코드를 작성해야 한다고 배웠기 때문에인지, 관습적으로 클래스 후미에 붙히는 Impl 이라는 키워드 때문인지 Service 인터페이스에 구현 클래스와 종속적인 메서드들을 자주 추가하곤 한다. 구현 클래스와 종속적인 메서드가 Service에 추가되면 새로운 구현 객체가 필요한 시점에는 그 인터페이스의 메서드들은 사용할 수 없거나 성격에 맞지 않는 시그니처일 가능성이 매우 크고, 그렇되면 효용성은 떨어지고 구조만 복잡한 코드가 될 뿐인 것이다.


이 구조의 단점

아무래도 그렇지 않은 구조에 비해서는 다소 복잡해 보이고 가독성, 직관성이 떨어져 보일 수 있는 것은 사실이다. 그리고 위에 언급한 대로 언제 추가될지 모르는 서브 구현 객체를 위해 인터페이스 입장에서 생각하려는 노력 자체가 비용이 될 수 있다. 애시당초 인터페이스 구조가 아니라면, 디펜던시나 추상화, 이런 생각할 필요 없이 그 클래스의 성격에 맞게 잘 구현만 하면 오히려 생산성이 더 좋을 수도 있다.


결론

설계에는 답이 없다. 답은 없지만 그래도 이왕이면 확장 가능한 형태로 설계를 하면 좋겠고 Impl 이라는 키워드에 매몰되어 우리가 service 또한 추상화 시켜 생각해야할 대상이라는 것을 잊지 않도록 노력해야 한다. 되도록이면 impl 이라는 이름보다는 구체적인 이름으로 구현 클래스 이름을 짓도록 하고, 이 구조의 장점인 추후 확장, 교체등을 대비하여 메서드를 정의할 때 인터페이스 입장에서 생각해서 메서드를 정의하자.


DevThink 카테고리 내 다른 글 보러가기

댓글남기기