Files

164 lines
7.0 KiB
Markdown

# 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` + `UserServiceImpl`
- `EmailNormalizer` + `EmailNormalizerImpl`
- `AuthFacade` + `AuthFacadeImpl`
단, 정말 계약 타입이 먼저이고 구현이 뒤따르는 구조면 예외다.
### 5. 인터페이스는 “역할”을 표현해야 한다
인터페이스 이름은 구현 방식이 아니라 역할/능력을 드러내야 한다.
좋은 방향:
- `UserReader`
- `PasswordHasher`
- `JwtSigner`
- `TokenIssuer`
- `UserRepository`
- `OAuthClient`
지양:
- `DefaultUserService`
- `CommonManager`
- `BaseHandler`
### 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를 기본으로 사용한다.
하지만 “프록시 가능”만으로 모든 클래스 앞에 인터페이스를 두지 않는다.
기본 판단 순서:
1. 이 타입이 경계/계약인가?
2. 교체/테스트/AOP 가치가 있는가?
3. 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는 제한적으로만 허용
- 프록시 가능성만으로 인터페이스를 만들지 않음
- 조기 추상화 금지