추상화는 구현 뒤에 온다: Service와 Impl 관행 다시 보기
구체적인 구현에서 검증된 공통점을 추출하고, 구현체가 하나여도 실제 경계를 만드는 인터페이스만 남기는 설계 기준을 다룬다.
출처 및 AI 안내: 이 글은 제미니의 개발실무 유튜브를 기반으로 작성되었습니다.
gpt-5.6-sol모델을 사용해 생성·편집했습니다.
인터페이스는 경계나 여러 행위의 공통점을 더 선명하게 만들어야 한다. 그런데 많은 프로젝트는 UserService를 만들자마자 UserServiceImpl을 붙이고 거기서 멈춘다. 인터페이스 하나, 구현체 하나, 달라질 행위도 없다. 코드에서 필요가 드러나서 만든 추상화가 아니라 프로젝트 관습이 요구해서 만든 구조다.
이 구조도 동작에는 문제가 없을 수 있다. 아쉬운 점은 코드를 이해하는 비용이다. 인터페이스를 만난 독자는 다른 구현체가 있는지, 교체 가능성이나 의존성 제어가 중요한지 확인하게 된다. 매번 답이 “Impl 클래스 하나뿐”이라면, 코드는 독자의 주의를 쓰면서도 별다른 정보를 전하지 못한다.
인터페이스를 한정된 설계 자원으로 다루기
인터페이스가 주는 이점은 많지만 공짜는 아니다. 거의 모든 서비스에 관성적으로 인터페이스를 붙이면 중요한 추상화와 의례적인 추상화를 구별하기 어려워진다. 코드를 따라가는 경로는 덜 직관적이 되고, 이름이 전하는 정보는 줄어든다. 실제 구현에는 없는 유연성이 구조에 있는 것처럼 보일 수도 있다.
그래서 인터페이스는 아껴 쓸 필요가 있다. 여러 구현체가 의미 있는 계약을 공유하거나, 모듈 사이의 의존성을 뒤집거나 느슨하게 해야 하거나, 구현 세부 사항을 감춰야 하는 경계가 있을 때 사용한다. 인터페이스가 그 자체로 좋은 설계인지 묻는 데서 멈추지 말아야 한다. 이 인터페이스가 시스템에 관해 참인 정보를 다음 독자에게 전하는지가 기준이다.
구체적인 구현이 추상화를 드러내게 하기
더 나은 기본값은 구체적인 행위에서 출발하는 것이다. 시스템에 사용자 서비스 하나가 필요하다면 우선 그 클래스에 UserService라는 이름을 주고 구현한다. 아직 나타나지 않은 모든 변형을 미리 예측하지 않는다.
코드가 발전하면서 일반 사용자, 공통 사용자, 무거운 작업을 맡는 사용자 서비스처럼 실제로 서로 다른 역할이 생길 수 있다. 그 구현들에서 안정적인 공통 형태가 확인되면 인터페이스를 추출한다. 구현이 제공한 근거를 바탕으로 아래에서 위로 설계하는 것이다. 이때 UserService는 공통 계약의 이름이 되고, 구체 클래스에는 각 구현이 실제로 하는 일을 설명하는 이름을 붙일 수 있다.
이 순서는 추상화뿐 아니라 네이밍도 개선한다. 처음부터 UserService와 UserServiceImpl을 만들면 구현의 역할이 생기기도 전에 좋은 이름을 인터페이스가 차지한다. Impl은 무엇인가를 구현한다는 사실만 말할 뿐, 그 구현이 왜 존재하는지는 설명하지 못한다. 실제 변형이 생긴 뒤에는 모든 클래스를 일반적인 구현 접미사로 줄이지 않고 각 차이를 이름에 담을 수 있다.
추출한 계약이 반드시 Service로 끝날 필요도 없다. 공통 행위가 사용자를 등록하는 일이라면 역할 중심의 이름이 그 뜻을 더 직접적으로 전할 수 있다. 익숙한 Controller–Service–Repository 순서가 행위를 이해하기도 전에 어휘를 결정하게 두지 말자. 구현은 설계의 반대가 아니다. 구체적인 구현에서 얻은 근거로 설계를 발전시킬 수 있다.
구현체가 하나여도 인터페이스가 필요한 경우
구현체 수만으로 인터페이스의 가치를 판단할 수는 없다. 구현체가 하나여도 아키텍처 경계를 만든다면 인터페이스를 둘 이유가 있다. 예를 들어 도메인 모듈이 자신에게 필요한 기능을 인터페이스로 정의하고 다른 모듈이 구체 구현을 제공할 수 있다. 구현이 하나뿐이어도 도메인이 그 구현에 직접 의존하지 않게 하고, 모듈 사이의 결합을 느슨하게 만든다.
이는 모든 클래스 옆에 인터페이스를 자동 생성하는 것과 다르다. 모듈 경계에서는 그 인터페이스가 어떤 의존성을 제어하는지 팀이 짚어 설명할 수 있다. 추상화가 현재 맡은 일이 있는 셈이다. 현재의 경계나 변형이 뒷받침하지 않는 상황에서 “언젠가 구현체가 늘어날 수 있다”는 이유만 드는 것보다 훨씬 구체적이다.
따라서 “구현체가 몇 개인가?”보다 넓게 물어야 한다. 이 인터페이스가 어떤 의존성을 바꾸는지, 어떤 개념에 이름을 붙이는지, 무엇을 더 명확하게 만드는지 확인하자. 답이 구체적이라면 구현체가 하나라는 사실은 모순이 아니다.
프로젝트를 어중간한 버전으로 남기지 않기
기존 코드베이스에는 구성원이 이미 그 구조에 익숙하다는 현실적인 제약이 있다. 모든 Impl 접미사를 한 번에 없애는 작업은 명확성을 높이기보다 혼란을 키울 수 있다. 그렇다고 여러 네이밍과 계층 관습을 기약 없이 섞으면 프로젝트가 어중간하게 이행된 상태로 남는다.
기존 프로젝트라면 당장은 익숙한 관습을 유지하거나, 팀이 합의한 리팩터링 범위를 정해 함께 바꾸는 편이 낫다. 새 프로젝트는 더 작은 기본값을 택하기 가장 좋은 시점이다. 제한된 영역에서 실험할 수도 있다. 불필요한 인터페이스 하나를 제거하고 코드가 어떻게 읽히는지 비교한 뒤, 무엇을 얻고 잃었는지 팀과 논의한다.
목표는 인터페이스나 Impl을 규칙으로 금지하는 것이 아니다. 추상화를 자동으로 찍어 내는 습관을 멈추는 것이다. 먼저 구체적이고 역할이 잘 드러나는 구현을 만든다. 반복되는 행위나 실제 의존성 경계가 필요성을 증명할 때 계약을 추출한다. 그러면 인터페이스는 아무도 다시 묻지 않는 템플릿이 아니라, 코드가 실제로 필요로 하는 결정을 전달한다.