7.0 KiB
interface 생성 기준
목적
인터페이스는 “있으면 좋아 보이는 추상화”가 아니라, 경계와 계약을 안정적으로 표현해야 할 때만 만든다.
기본값은 “인터페이스를 무조건 만들지 않는다”이다.
공식 의미
- 인터페이스는 구현과 분리된 계약(contract)을 표현하는 타입이다.
- 구현체는 인터페이스가 정한 메서드 계약을 따른다.
- 인터페이스는 추상 메서드, default method, static method, 상수를 가질 수 있다.
- Spring DI는 의존 객체를 생성자/팩토리 메서드/세터를 통해 주입하며, 인터페이스나 추상 베이스 타입에 의존할 때 결합도가 낮아지고 테스트 대역 사용이 쉬워진다.
- Spring AOP는 대상이 인터페이스를 구현하면 JDK dynamic proxy를 기본으로 사용한다.
기본 규칙
1. 기본값은 “필요할 때만 만든다”
다음 중 하나가 아니라면 인터페이스를 만들지 않는다.
- 모듈/레이어 경계를 표현해야 한다
- 교체 가능한 구현이 실제로 존재하거나 가까운 미래에 예상된다
- 테스트에서 대역(stub/mock/fake)으로 치환하는 가치가 크다
- 프레임워크/프록시/AOP/플러그인 구조상 계약 타입이 분명히 필요하다
- 라이브러리/외부 모듈에 공개할 안정된 API 계약이 필요하다
2. “구현체 1개” 자체는 금지 근거가 아니지만, “이유 없는 인터페이스”는 금지
구현체가 1개여도 아래 중 하나면 인터페이스를 둘 수 있다.
- application port
- 외부 연동 client contract
- repository-like boundary
- 인증/토큰/암호화 같은 교체 가능한 정책
반대로 구현체가 1개이고 아래도 아니면 인터페이스를 만들지 않는다.
- 내부 helper/service
- 단순 orchestration class
- 프레임워크가 요구하지 않는 내부 컴포넌트
3. 레이어 경계는 인터페이스를 우선 검토
특히 다음 경계는 인터페이스를 우선 검토한다.
- application
port/in - application
port/out - infrastructure adapter가 구현하는 계약
- 외부 시스템 client contract
- 교체 가능한 정책 객체
즉 “안쪽이 바깥 구현을 모르면 좋은 곳”은 인터페이스 후보가 된다.
4. 내부 구현 디테일에는 기본적으로 인터페이스를 만들지 않는다
같은 모듈 내부에서만 쓰이고, 교체 가능성도 낮고, 테스트 seam 가치도 낮은 클래스는 concrete class 그대로 둔다.
금지 예:
UserService+UserServiceImplEmailNormalizer+EmailNormalizerImplAuthFacade+AuthFacadeImpl
단, 정말 계약 타입이 먼저이고 구현이 뒤따르는 구조면 예외다.
5. 인터페이스는 “역할”을 표현해야 한다
인터페이스 이름은 구현 방식이 아니라 역할/능력을 드러내야 한다.
좋은 방향:
UserReaderPasswordHasherJwtSignerTokenIssuerUserRepositoryOAuthClient
지양:
DefaultUserServiceCommonManagerBaseHandler
6. 인터페이스는 작고 응집도 있게 유지
인터페이스는 하나의 역할/계약에 집중해야 한다.
금지:
- unrelated method를 한 인터페이스에 몰아넣기
- “편해서” 여러 책임을 한 계약에 합치기
- consumer마다 일부만 필요한 fat interface
7. 인터페이스는 구현 세부보다 호출 계약을 고정
인터페이스는 아래를 고정해야 한다.
- 어떤 입력을 받는가
- 어떤 결과를 돌려주는가
- 어떤 예외/실패 의미가 가능한가
- 어떤 side effect/보장이 있는가
반대로 아래는 인터페이스에 새지 않게 한다.
- HttpClient/WebClient/JPA/Redis/Jackson 등 기술 세부
- 프레임워크 구체 타입
- 구현체 내부 최적화 방식
8. default method는 “계약 핵심”보다 “하위 호환/작은 공통 동작”에만 제한
Oracle 문서상 default method는 기존 구현과의 binary compatibility를 유지하면서 기능을 추가할 수 있다.
프로젝트에서는 아래일 때만 제한적으로 허용한다.
- 라이브러리/공용 계약의 하위 호환이 중요하다
- 매우 작은 convenience 동작이다
- 구현체 대부분에 동일하게 자연스럽다
기본값은 추상 메서드다.
비즈니스 핵심 로직을 default method로 밀어 넣지 않는다.
9. 인터페이스 static method는 그 계약에만 밀접한 helper일 때만
Oracle 문서상 인터페이스는 static method를 가질 수 있다.
프로젝트에서는 그 helper가 해당 인터페이스 계약과 아주 밀접할 때만 허용한다.
그 외 일반 helper는 별도 타입/유틸로 분리한다.
10. 인터페이스 상수 남용 금지
인터페이스는 상수 묶음 용도로 만들지 않는다.
상수는 계약의 본질이 아닐 경우 별도 적절한 소유 타입에 둔다.
11. Spring 프록시/AOP 때문에 인터페이스를 만들 수는 있지만, 그 이유를 과장하지 않는다
Spring은 인터페이스가 있으면 JDK dynamic proxy를 기본으로 사용한다.
하지만 “프록시 가능”만으로 모든 클래스 앞에 인터페이스를 두지 않는다.
기본 판단 순서:
- 이 타입이 경계/계약인가?
- 교체/테스트/AOP 가치가 있는가?
- concrete class로 두는 것이 더 단순한가?
12. 테스트를 위해서만 인터페이스를 남발하지 않는다
Spring DI 문서는 인터페이스/추상 베이스 타입이 테스트 대역 사용을 쉽게 한다고 설명한다.
하지만 “테스트가 쉬워 보인다”는 이유만으로 의미 없는 인터페이스를 만들지 않는다.
다음도 대안이 될 수 있다.
- package-private concrete class 테스트
- 더 작은 collaborator 분리
- test fixture/fake 구현
- 포트 레벨에서만 seam 만들기
13. public API / multi-module contract는 인터페이스 우선 검토
다른 모듈/패키지/팀이 사용할 public contract면 인터페이스를 우선 검토한다.
이 경우 호출자와 구현체를 느슨하게 분리할 가치가 크다.
14. 조기 추상화 금지
겉보기 유사성만 보고 인터페이스를 먼저 만들지 않는다.
다음 질문 중 “예”가 충분히 쌓일 때 만든다.
- 정말 다른 구현이 필요한가?
- 호출자가 구현이 아니라 계약에 의존해야 하는가?
- 이 추상화가 6개월 뒤에도 자연스러운가?
- 이 인터페이스가 테스트/교체/경계 보호에 실제 도움 되는가?
프로젝트 기준 요약
- 인터페이스 기본값은 “필요할 때만”
- application port / 외부 경계 / 교체 정책은 인터페이스 우선
- 내부 helper/orchestration에는 기본적으로 인터페이스 금지
XService+XServiceImpl자동 생성 금지- 역할 중심 이름 사용
- 기술 세부를 계약에 노출하지 않음
- default/static method는 제한적으로만 허용
- 프록시 가능성만으로 인터페이스를 만들지 않음
- 조기 추상화 금지