서비스에 인터페이스를 사용해야 하나?

한국 스프링 사용자 모임(KSUG)의 페이스북 그룹에서 스프링으로 웹 애플리케이션을 개발하면서 서비스에 인터페이스를 사용해야 하느냐는 토론이 있었습니다.

원래 그 글의 댓글로 작성한 글인데 무슨 일인지 댓글이 등록되지 않아서 이렇게 블로그에 올립니다.


모든 프로그래밍 원칙과 장치는 ‘적절히 써야 한다’는 대전제 아래에서 논의돼야 한다는 점을 먼저 밝히고 제 생각을 말하고 싶습니다. 프로그래밍 원칙을 위반한다고 지구가 무너지거나 감옥에 갇히지는 않죠. 누가 죽지도 않고요. 그렇다고 해도 어떤 원칙을 위반했을 때에는 원칙의 중요도 만큼 원칙을 무시한 결정이 적절했음을 설득하거나 그로 인해 생기는 문제를 해결할 책임을 위반하기로 결정한 사람(또는 조직)이 수용하고 감당하면 됩니다.

KSUG에서 논의된 내용을 요약하면

  • 인터페이스 없어도 AOP 잘 됨
  • 인터페이스 하나에 서비스 하나 뿐임
  • 인터페이스 때문에 가독성이 떨어짐
  • 테스트를 하면 인터페이스 구현체를  하나 더 만들게 됨
  • 테스트도 요즘 목 프레임워크 기술이 좋아서 인터페이스가 꼭 필요하지 않음
  • 프로젝트 규모와 중요도에 따라 다를 듯
  • 인터페이스를 나중에 추출하는 방법도 있음

저도 인터페이스가 비용인 건 인정합니다. 모든 코드는 비용이죠. 다만, 그 비용을 들인 만큼 얻는 가치가 있다면 투자하면 되고 없다면 투자해서는 안 될 것입니다. 프로그래밍에서 간접화와 추상화와 관련된 코드를 비용으로 따진다면 간접비(overheads)에 해당할 것입니다. 따라서 과도한 간접화나 추상화는 피하도록 노력해야 합니다. 그렇지만 단순히 간접화나 추상화와 관련된 투자가 비용이라서 쓰면 안 된다는 식의 논리는 빈약입니다. 투자회수율(RoI)을 생각해야지 총비용만 무조건 줄이겠다는 생각은 현명하지 않습니다.

같은 맥락에서 @김지헌님이 링크 거신 동영상 강좌(?)에서 문제 삼은 “모든 클래스에는 인터페이스를 만든다”는 과격하고 경직된 정책은 저도 반대입니다. 그러나 저 화자께서 한 쪽의 극단적인 상황을 예로 들면서 그 반발로 또 다른 극단으로 치우치시는 것 같아서 저 강좌의 주장을 그대로 인정하지 못하겠습니다. 저 강좌를 얼마 전에 보고 따로 글을 쓰고 싶었는데 시간이 될지는 모르겠습니다.

OOP의 핵심, 메시징

살짝 한 발 물러서서 객체 지향 프로그래밍(OOP)이 뭔지 생각해 보는 게 좋을 것 같습니다. 최초의 OOP 언어라고 불리우는 두 언어가 있(었)습니다. 하나는 67년도에 나온 시뮬라 67이고 또 하나는 80년도에 나온 스몰토크-80입니다. 우리나라의 많은 사람들이 (아마도 마이크로소프트 덕에) C++를 통해서 OOP를 접했는데, C++는 시뮬라 67에서 OOP의 특성을 물려 받은 언어입니다. 우리가 OOP를 생각하면 흔히 떠오르는 객체, 클래스, 상속 같은 언어적 장치는 모두 시뮬라 67에서 소개되었습니다. 시뮬라 67에서는 프로그램을 작은 여러 독립 프로그램(객체)으로 나누고 이 작은 독립 프로그램들이 병렬적으로 서로 협력하며 동작하도록 하는 프로그래밍 모델을 도입했습니다.

하지만 정착 “객체 지향 프로그래밍”이란 용어 자체는 스몰토크-80에서 처음 소개되었습니다. 스몰토크를 만든 사람 중 하나이고 OOP란 용어를 고안한 앨런 케이는 OOPSLA 97에서 “내가 ‘객체 지향’이란 말을 만들 때에는 C++는 염두에 두지 않았다고 할 수 있다(Actually I made up the term “object-oriented”, and I can tell you I did not have C++ in mind)”라고 말했고, OOP를 정의해 달라는 서신 요청에 대해서 장문의 답장을 하면서“나에게 OOP는 메시징과 상태 처리 과정의 지역적 보존, 보호, 은익과 모든 것의 극단적인 지연 바인딩 뿐입니다(OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things)”라고 말했습니다.

앨런 케이는 스퀵(squeak) 커뮤니티에게 프로토타입 기반 OOP와 클래스 기반 OOP에 대한 토론이 오가는 사이에 자신이 생각하는 OOP를 설명하기도 했는데, 그 글에서 자신이 객체라는 용어를 만들어 내서 사람들이 자꾸 별로 중요하지 않은 객체에 집중하게 만들었다면서 사과한다고 하고 OOP에서 가장 중요한 것은 클래스나 객체 같은 것이 아니라 메시징이라고 다시 강조했습니다.

앨런 케이는 메시징을 가장 유연한 추상화라고 생각했고 메시지는 모든 세부 구현과 정보를 은익한다고 생각했습니다. 그리고 메시지에 기반한 시스템 설계를 목표로 스몰토크(언어 이름에서 그가 생각한 OOP의 특징을 잘 알 수 있습니다)를 개발한 것입니다.

앨런 케이가 정의하거나 강조한 형태의 OOP가 35년이 지난 현 시점에서 우리에게 어떤 중요성이 있는지는 따로 논의할 문제이지만 그 용어를 만든 사람의 의도를 전혀 고려하지 않고 OOP를 논하는 것은 확실히 잘못된 것이겠죠. 무엇보다 우리는 OOP를 얘기하면서 너무 (엘런 케이가 중요하지 않다고 말한) 객체와 클래스 같은 언어적 장치에만 논의가 머물러 있다는 생각이 듭니다.

자바의 메시징, 인터페이스

그럼 메시징이란 뭘까요?  메시징을 따로 논하자면 메시징의 여러 요소들이 얘기될 수 있지만, 자바 입장에서 보면 메시징은 메서드 호출의 한 형태인데, 메서드 호출을 처리할 객체를 결정하는 시점이 컴파일 시점이 아닌 실행시점이라는 것이 일반 메서드 호출과 메시징을 구분하는 구별점이라고 말할 수 있습니다. 다른 말로 (함수형 언어에서 주로 사용하는) 동적 디스패치인 거죠. 저는 OOP의 특징을 하나만 뽑으라고 한다면 동적 디스패치를 뽑아야 한다고 말합니다.

앨런 케이가 말한 세 가지 중 두 번째, 상태 처리 과정의 지역적 보존, 보호, 은익은 객체와 관련된 특징들 입니다. 자바에서는 객체를 표현하는 데 클래스를 사용하죠. 그리고 나머지 두 가지, 메시징과 지연 바인딩과 관련된 자바의 장치는 인터페이스입니다. 추상 클래스의 추상 메서드를 생각할 수도 있지만, 자바는 단일 상속만 허용하면서 추상 클래스와 인터페이스의 용도를 분명히 구분했습니다. 추상 클래스는 구현상의 유사성을 모듈화하는 용도이고 인터페이스는 메시지를 형식 체계(Type System) 안에서 규약화(Specification) 한 것입니다. 즉 추상 클래스는 내부 구현의 모듈화 문제, 인터페이스는 클래스 외부의 타 객체와의 협력 문제를 주로 다룹니다.

자바가 엄격한 정적 형식 언어라는 특성 때문에 스몰토크와 같은 수준의 동적인 메시징은 불가능하기도 하고, 전략적으로 C++와 비슷한 형태로 만들면서 시뮬라 67 형태의 OOP가 되었지만, Objective-C의 프로토콜을 참조해서 최대한 스몰토크에서 말하는 OOP를 도입하려고 노력한 흔적이 바로 인터페이스입니다.

OOD, 인터페이스, 스프링

이런 아이디어는 디자인 패턴에서도 강조되는데, 디자인 패턴 1장에 보면 두 가지 설계 원칙을 강조합니다. 하나는 “구현이 아닌 인터페이스로 프로그램을 짜라”는 것이고 또 하나는 “클래스 상속 보다는 객체 구성을 선호하라”는 것입니다. 이 중 첫번째는 앨런 케이의 OOP에 대한 생각과 일맥상통하는 말입니다.

지금은 홈페이지에서 사라졌지만 피보탈로 넘어가기 전까지도 남아 있었던 스프링의 사명 선언문의 여섯 문항 중 두번째는 이렇습니다.

“클래스가 아닌 인터페이스로 프로그램을 작성하는 것이 최선이다. 스프링은 인터페이스를 사용함으로서 생기는 복잡성을 없앤다(It’s best to program to interfaces, rather than classes. Spring reduces the complexity of using interfaces to zero).”

정리하면, OOP의 핵심은 메시징(동적 디스패칭)이고 자바에서 이 메시징과 가장 유사한 것은 인터페이스라는 것, 그리고 인터페이스로 설계하는 것이 OOD에서 매우 중요하며 스프링이 해결하려는 문제 중 중요한 한가지는 이 인터페이스로 프로그래밍을 할 때 생기는 복잡성을 제거하려는 것이라는 사실입니다.

인터페이스 실효성, 응집 단위의 크기(granularity)

이 정도로 OOP 언어인 자바에서 인터페이스의 의미와 스프링과 인터페이스의 관계는 충분히 거론되었다고 생각합니다.

이제 제 입장을 밝히겠습니다. (서설이 너무 길어!)

저는 높은 응집도와 느슨한 결합 원칙(High Cohesion, Low Coupling)에 따라서 응집도가 높아야 하는 부분에서는 인터페이스나 다른 추상화 장치를 쓰지 않고 적절한 수준에서 강한 결합을 유지하는 것이 가능하다고 생각합니다. 한 클래스 안에서는 클래스 간의 협동보다 높은 결합도가 유지될 수 있고, 한 클래스와 그 내부 클래스 간의 협동은 별도 클래스들 간의 협동보다 결합도가 높을 수 있으며, 한 패키지 안의 클래스들은 타 패키지에 있는 클래스보다 결합도가 높아도 문제가 되지 않을 수 있습니다.

인터페이스는 요구되는 결합 강도에 따라서 적절히 사용하면 됩니다. 클래스와 내부 클래스 사이의 협동에 불필요한 인터페이스를 쓰는 것은 과잉일 수 있습니다. (자바의 기본 가시성으로 서로의 내부를 열어 볼 수 있는 사이인) 한 패키지 안에서만 사용될 인터페이스도 과잉이라고 생각할 수 있습니다.

하지만 (객체 하나 이상으로 구성된 단위 기능인) 컴포넌트라면, 그 컴포넌트가 아무리 단순하더라도 인터페이스를 쓰는 것이 적절하다고 생각합니다. 컴포넌트의 구현이 단순하다는 사실 자체가 은익되어야 할 정보입니다. 외부에서는 해당 컴포넌트가 단순한지, 수백개의 클래스로 구성된 거대한 물건인지, 외부 클라우드 시스템의 수백개 노드로 돌아가는 원격 서비스인지 몰라야 합니다.

저는 솔직히 스몰토크처럼 모든 것을 객체로 취급한다는 철학에 약간은 회의적인 시각을 가지고 있습니다. 스몰토크는 모든 프로그래밍 요소를 객체와 메시징이라는 메타구조로 단순화하고 문제를 해결하려고 하는데, 세밀한 수준(fine-grained)에서의 OOP는 실효성이 떨어지지 않나 생각됩니다. 1 + 1 같은 작은 일은 굳이 OOP 개념을 적용할 필요가 없다는 이야기 입니다.

다른 이야기지만, 그런 의미에서 (순수 OOP 언어이면서 동시에 FP 언어인)스칼라에서 시도하는 함수형 OOP가 좋아 보입니다. 세밀한 문제 해결은 FP로, 큰 문제 해결은 OOP로 풀어가는 형태랄까요. 람다가 들어오면서 자바 8도 메서드 수준의 추상화가 가능해졌으니 자바도 객체의 크기(granularity)가 지금 보다는 조금 더 커질 것으로 예상됩니다.

제 얘기의 요점은 OOP는 응집의 단위가 클 수록 유효성이 커진다는 이야기입니다. 우리가 개발하는 애플리케이션을 구현수준, 객체 설계 수준, 애플리케이션 아키텍처 수준으로 애플리케이션을 조망하는 수준을 나눈다고 할 때, 구현 수준, 클래스 내부를 열어서 작업할 때에서는 인터페이스를 새로 생성할 일이 거의 없을 것입니다. 그리고 객체 설계 단계, 즉 마이크로 아키텍처 단계에서는 필요에 따라 인터페이스를 적절히 도입하면 됩니다. 하지만 과도하게 만들지 않도록 인터페이스가 필요한 시점에 리펙터링을 통해 클래스에서 인터페이스를 추출하는 방식을 적극적으로 적용할만 합니다.

마지막으로 애플리케이션 전체를 조망하는 애플리케이션 아키텍처 수준에서라면 정말 예외적인 상황이 아닌 이상 인터페이스를 먼저 만들고 그 인터페이스를 구현한 클래스를 만들 것입니다. 컴포넌트 경계의 유연성은 매우 명백하게 중요하기 때문입니다.

서비스를 컴포넌트라고 생각할지 말지는 개발하는 사람의 판단에 따라 다를 것입니다. 하지만 “일반적”으로 서비스는 컴포넌트입니다. 서비스라는 말 자체가 컴포넌트와 유의어이기도 합니다. 따라서 서비스는 “일반적”으로 인터페이스를 통해서 접근하는 것이 매우 타당합니다.

이상은 제가 따르는 제 원칙입니다. 서비스가 모두 클래스 하나로 되어 있는 컴포넌트이고 아주 작으며 앞으로 커질일도 없다면, 그건 특수한 상황이니 그 상황에 맞게 작업하겠죠. 어떤 사람은 클래스 여러 개를 합쳐서 서비스 컴포넌트의 크기를 키울지도 모르겠습니다. 어떤 사람은 정말 서비스에 인터페이스를 만들지 않겠다고 할 수도 있겠죠. 그건 그 분이 판단해서 선택할 문제입니다.

프로그래밍 메타포로서의 OOP, 메타 경계

하지만 저는 아무리 애플리케이션 규모가 작은 상황에도 저는 가능한 서비스에 인터페이스를 만들 것 같습니다. 왜냐하면 앞에서 말한 메시징 때문입니다. 인터페이스의 기능적인 면 외에도 OOP의 메시지와 같이 인터페이스는 제가 제 애플리케이션을 조망할 때 추상화 수준을 구분해 주는 경계를 제공해 줍니다. 저는 수시로 세부 구현을 하다가도 한 발 물러나 인터페이스 수준에서 특정 컴포넌트나 애플리케이션 전체를 바라보고 다시 특정 영역의 세부로 들어가 코딩을 합니다. 이렇게 두 세 단계의 추상화 수준을 오가면서 세부에 집중하거나 전체를 보기 때문에, 사고의 틀을 제공하는 도구로서 인터페이스가 매우 유용합니다.

OOP는 명령 한줄 한줄, 그리고 함수 하나 하나에서 더 높은 수준에서 문제를 접근하도록 해줍니다. 전 제가 모두 설계하고 구현하는 프로젝트에서도 클래스를 블랙박스로 취급하는 연습을 합니다. 일부로 세부 구현을 잊고 객체 간의 상호 구조에만 집중하는 거죠.

저는 세부에 집착하곤 하는 프로그래머로서 우리가 너무 큰 그림을 잊는 것은 아닌지 되돌아 봐야 한다고 생각합니다. 세부 구현은 잊고 객체라는 블랙박스와 그 객체 사이의 협력이라는 상위  영역에서 시스템을 좀 더 넓게 바라보는 시점과 세부를 바라보는 시점 모두를 균형 있게 유지하고 서로 섞이지 않게 구분할 수 있다면, 그리고 그런 방식의 개발이 가능하도록 각 객체의 책임을 분명하게 정의하고 API를 잘 설계할 수 있다면 훨씬 좋은 SW를 만들 수 있을 것입니다.  그리고 그런 식으로 일할 때 인터페이스는 매우 중요한 역할을 할 것입니다.

정리

다시 정리하면 서비스에 인터페이스를 적용하겠다는 이유가 결국 두 가지인데, 서비스가 인터페이스를 적용하기에 충분히 큰 애플리케이션의 구성 단위이므로 인터페이스를 적용하는 것이 매우 타당해 보인다는 것이 하나이고, OOP의 핵심 개념인 메시징의 관점에서 제가 사고하는 수준을 구분해 주기 때문에 서비스 정도라면 아무리 단순한 애플리케이션에서도 인터페이스를 사용해서 얻는 이득이 있다는 것이 또 한 이유입니다.

가독성 이야기가 나왔습니다. 추상화나 간접화가 구현내용까지 다가가는데 여러 단계가 있는 것을 가독성이라고 표현하신 것 같습니다. 하지만 “원칙상” 좋은 OOD라면 객체간의 협력(메시징)이라는 큰 그림으로 파악되는 수준의 내용과 개별 컴포넌트나 클래스 속 세부 구현이 잘 분리되어야 합니다. 그리고 여러 클래스의 세부 구현을 옮겨 다니면서 봐야 하는 일이 최소화되어야 하고요. 프로그래머가 모든 세부 구현과 클래스 수준의 협력 구조를 모두 머리에 담아두고 있어야 한다면 뭔가 OOP에 맞게 설계된 것이 아닐 수 있습니다.

한가지, 오해가 생길까봐 첨언하면, 인터페이스를 쓰자는 것이 선행 정밀 설계를 의미하지 않습니다. 저는 지속적으로 개선되는 설계 또는 창발적 설계를 지지하는 사람입니다.

이상 어겨도 감옥가지 않는 “원칙”과 “일반론”에 대한 제 생각이었습니다.

  • 2015/1/6 토론 중에 추가로 쓴 내용을 합쳤습니다.
  • 2015/1/8 글을 읽기 좋게 정리하고 소제목을 달았습니다.
  • 2016/1/25 오타를 수정하고 몇가니 문장을 다듬었습니다.