Spring Framework/Spring MVC

관습적인 추상화 Service와 ServiceImpl 구조에 대해서

Wings of Freedom 2023. 12. 26. 18:48

스프링에 대해 공부하면서 그리고 프로젝트를 진행해보고 여러 예제 코드들을 접해보면서 느낀 것은 관습적으로 브릿지 패턴을 이용한 추상화를 사용하고 있다는 점입니다.

 

 

관습적인 추상화

계층화된 아키텍처인 MVC 패턴을 적용한 대부분의 프로젝트에서는 그 중에서도 Service 계층에서는 MemberService 와 같이 서비스를 인터페이스로 생성하고 MemberServiceImpl 이라는 구현체를 생성해서 사용하는 방식으로 대부분의 설계가 이루어집니다.

 

토비의 스프링이나 여러 객체지향과 스프링 관련 책들을 보면 이와 같은 패턴으로 설계를 해야하는 이유에 대해서 잘 설명하고 있습니다. 인터페이스와 구현체의 분리를 통해 특정 기술이나 외부환경에 독립적으로 보다 자유로운 확장이 가능해진다는 OCP 원칙에 입각한 분명한 장점이 존재합니다.

 

 

https://ko.wikipedia.org/wiki/브릿지_패턴

 

 

하지만 실제로 대부분의 프로젝트나 예제 코드를 보면 인터페이스와 구현체 클래스 사이의 관계가 1:1로 구성되어 실질적으로 인터페이스를 사용하는 것에 대한 이점을 전혀가져가지 못함에도 불구하고 관습적으로 이러한 추상화 패턴을 적용하고 있습니다.

 

이러한 관습적인 추상화의 장점과 단점은 무엇일까요? 맹목적으로 이러한 추상화 방식을 프로젝트에 적용하는 것이 올바른 설계 방법일까요? 이러한 질문들에 초점을 맞추어 한번 고민을 해보았습니다.

 

 


 

관습적인 추상화의 장점과 단점

이러한 추상화 방식은 브릿지 패턴이 가지고 있는 장점과 단점과 같다고 생각해도 무방할 것 같습니다. 

 

앞서 언급했듯이 인터페이스와 구현체 클래스를 분리함으로써 구현체를 독립적으로 확장할 수 있으며, 구현체 클래스를 변경하거나 확장해도 이를 사용하는 클라이언트의 코드에는 영향을 주지 않습니다. 

추상화를 통한 구현방식은 객체지향의 특징 중 하나인 다형성과 객체지향의 다섯가지 원칙 중 하나인 OCP 원칙을 가장 잘 실현해주는 설계방식이라고 할 수 있습니다. 

 

 

 

https://www.journaldev.com/1491/bridge-design-pattern-java

 

 

관습적인 추상화의 많은 장점에도 불구하고 이러한 설계 방식은 구조가 복잡해진다는 단점을 가지고 있습니다. 복잡화된 구조인 만큼 코드를 분석하고 확인하는 과정에서도 인터페이스를 거쳐서 다시 그 인터페이스의 구현체들을 확인하는 단계가 추가되면서 가지는 불편함도 분명 존재할 것 입니다.

 

그리고 제가 생각했을 때의 가장 큰 단점은 인터페이스와 구현체가 1:1 관계로 이루어지는 설계에서는 이러한 장점들을 잘 살리지 못한다는 것입니다. 그럼에도 불구하고 반드시 관습적인 추상화를 적용해야 할까요?

 


관습적인 추상화를 통한 설계 방식을 꼭 적용해야 할까?

이와 관련해서는 크게 두 가지 의견으로 나누어지는 것 같습니다. 먼저 이러한 관습적인 추상화를 적용해야 한다는 입장에서의 의견은 토비의 스프링에 잘 나와있습니다.

세상에 변하는 것과 변하지 않는 것이 있지만, 객체지향의 세계에서는 모든 것이 변한다. 여기서 변한다는 것은 오브젝트에 대한 설계와 이를 구현한 코드가 변한다는 의미이다. 소프트웨어 개발에서의 끝이란 개념은 없고 사용자의 비즈니스 프로세스와 그에 따른 요구사항은 끊임없이 바뀌고 발전한다. 그래서 개발자가 객체를 설계할 때 가장 염두에 두어야 할 사항은 미래를 어떻게 대비할 것인가이다.

-토비의 스프링 3.1  1장 중-

 

책에 잘 나와있는 것 처럼 객체는 변화하며 개발자는 끊임없이 이에 대비해야 합니다. 미래를 위한 설계를 통해 변화에 효과적으로 대처할 수 있다는 것입니다. 

그런 점에 있어서 우리가 설계한 인터페이스와 구현체 클래스가 당장에 1:1 관계를 맺고있을지 모르지만 서비스가 커지고 변화함에 따라서 얼마든지 구현체 클래스는 확장될 가능성을 가지고 있습니다. 그렇기 때문에 인터페이스와 구현체를 분리한 설계를 통해 미래의 변화에 유연하게 대처할 수 있어야 한다는 것입니다.

 

반면에 반대의 입장을 가지고 있는 의견은 LichKing님의 블로그의 글을 참고할 수 있었습니다.

서비스를 인터페이스로 만들었던건 관례로 굳어지게 되었는데 개발은 Transaction Script 형식으로 진행하다 보니까 관례는 관례대로 남고 애초에 그렇게 하자고 했던 이유는 사라져버리게 된 것이다. 그래서 내린 결론은 한 메서드에서 모든 역할을 다하는 이런 절차지향적인 코드에서는 사실 서비스를 인터페이스로 할 필요는 없다는 것이다. 물론 인터페이스를 만들지 말자 보다는 애초에 인터페이스를 만들었던 이유를 잘 살리는 것이 바람직 하다는 것이다.

-LichKing 블로그 중-

 

사실 반대의 입장이라기 보다는 정확히 그 의미를 잘 알고서 사용해야한다는 의미에 가깝습니다.

"관습적으로 그냥 원래 이렇게 해왔으니까?", "예제에 그냥 이렇게 나와있던데?", "이렇게 하래요" 와 같은 무책임한 말이 아니라 이러한 인터페이스와 구현체를 분리한 설계를 통한 이점들과 이유와 근거에 대해서 설계를 한 개발자 당사자는 명확히 이해하고 있어야 한다는 것입니다.

 

두 가지 글에서 알 수 있듯이 이러한 추상화 방법이 가지고 있는 장점들은 명확하게 존재합니다. 하지만 대부분이 관습적으로 이를 프로젝트에 적용하면서도 "왜?" 사용하는지에 대한 이유에 대해서 잘 인지하고 있지 못하고 있습니다. 그렇기 떄문에 이러한 추상화를 통한 설계방식을 적용한 이유와 장단점에 대해서 명확히 알고 사용하는 것이 중요할 것 같습니다.

 


추상화를 통한 설계 방식의 네이밍 컨벤션에 대해서 

그렇다면 또 한가지 고민해볼 수 있는 점은 바로 ServiceServiceImpl 의 네이밍 컨벤션에 관한 문제입니다. 보통의 경우 인터페이스에는 Service를 그리고 이를 구현한 구현체 클래스에는 ServiceImpl 이라는 접미사를 붙인 네이밍을 사용하거나 접두사에 인터페이스임을 명시하기 위해 I 라는 접두어를 사용하고 있습니다. 

 

 

과연 이러한 네이밍 방식은 적절한 방법일까요? 우리가 흔히 말하는 읽기 좋은 코드 클린 코드에 부합하는 네이밍 일까요? 

Java에서 직접적으로 이러한 설계에 대해 네이밍을 적용하고 있는지 알 수 있는 대표적인 사례로 Collection 프레임워크를 들 수 있습니다. 

 

 

Collection 프레임워크 중에서 List 의 구조에 대해서 간단하게 살펴보면, List 인터페이스와 이를 구현한 하위 구현체들에는 ArrayListImpl, LinkedListImpl 등이 아니라 ArrayListLinkedList 라는 네이밍으로 표현하고 있는 것을 볼 수 있습니다. 

해당 네이밍에서 Impl 을 접미사로 붙여서 사용하는 것은 불필요하고 무의미한 네이밍을 반복해서 사용하고 있을 뿐임을 잘 나타내 주고 있습니다. 

 

사실 이러한 점에 있어서 단순히 클래스 이름을 고유하게 만들기 위해 Impl 을 접미사로 붙이는 것이 전부라면 인터페이스를 갖는 것에 대해 다시 한번 생각해볼 필요가 있는 것 같습니다. 

이러한 문제가 발생하는 원인은 인터페이스와 구현체 사이의 1:1 관계에 있습니다. 즉, 인터페이스가 딱히 구현에 있어서 필요하지 않지만 설계 패턴을 만족시키기 위해서 위와 같은 네이밍을 사용하고 있기 때문에 발생하는 문제 입니다. 

 

 

때문에 어떠한 방법이 옳다라고 딱 잘라서 정의내릴 수는 없지만, 이러한 점들을 고려해서 어떠한 방법으로 네이밍을 하는 것이 더 좋은 방법으로 코드를 작성하는 것인가에 대해서는 고민해볼 필요가 있을 것 같습니다. 

 

 


 

프로젝트에 추상화 제대로 적용해봅시다

앞선 내용들을 통해서 프로젝트에 이러한 추상화를 적용하기 전에 고려할 두 가지 숙제가 주어졌습니다.

  • 인터페이스와 구현 클래스가 1:1 관계를 가지고 있는 프로젝트 구조에서 추상화를 적용해야하는가
  • 추상화를 적용한다면 네이밍을 어떻게 가져갈 것인가

 

이러한 고민들에 있어서 어떻게 프로젝트에 적용하는 것이 스프링을 스프링 답게 사용하면서도 읽기 좋은 코드, 객체지향적 특징을 잘 살릴 수 있는 코드가 될까? 라는 측면에서 답을 찾고자 노력했습니다.

 

당근마켓이라는 서비스는 일반 사용자도 존재하지만 지역 게시판에 자신의 가게를 홍보할 수 있는 사장님이라는 권한을 가진 사용자도 존재합니다. 떄문에 사용자 서비스는 일반 사용자들에 대한 서비스와 사장님으로 등록된 사용자들에 대한 서비스 등으로 구현을 달리 할 수 있으며, 서비스가 확장됨에 따라서 사용자의 유형도 얼마든지 확장될 가능성이 존재한다고 판단했습니다.

 

사용자 서비스는 사용자의 유형에 따라서 얼마든지 확장될 가능성을 가지고 있기 때문에 지금의 프로젝트가 1:1 관계를 가지고 있다고 하지만 얼마든지 1:N 관계를 갖는 서비스로 확장될 수 있습니다. 또한 이러한 확장에 있어서 사용자 유형이라는 고유의 특징을 구현체마다 가지고 있기 때문에 다음과 같이 구조를 변경할 수 있을 것 같습니다.

 

  • MemberService : 회원 서비스 인터페이스
    • GeneralMemberService : 일반 회원 서비스 구현체 클래스 
    • CeoMemberService : 사장님 회원 서비스 구현체 클래스
    • ...

 

코드에 정답이 존재하는 것은 아닙니다. 물론 제가 적용한 방법보다 좀 더 의미적으로 명확하고 좋은 방법들이 존재할 것입니다. 하지만 이러한 구조로 설계를 변경하면서 기존에 MemberServiceMemberServiceImpl 로 설계했을 때 보다 누가봐도 충분히 이름만 가지고 동작과 역할이 유추가능한 클래스 이름을 가지고 있으며, 기존에 추상화를 사용함으로써 얻을 수 있는 장점을 그대로 유지할 수 있는 구조로 개선할 수 있게 되었습니다.

 

 

 

출처: https://see-one.tistory.com/1 [See One:티스토리]