Backend
Go 인터페이스 설계 원칙 - Accept Interfaces, Return Structs
Go 인터페이스를 작고 명확하게 설계하는 기준과 포인터/값 수신자 선택법을 정리합니다.
Go 인터페이스 설계 원칙
핵심 규칙
- Accept interfaces: 입력은 추상화에 의존합니다.
- Return structs: 반환은 구체 타입으로 명확히 전달합니다.
- 인터페이스는 구현자가 아니라 소비자 패키지에서 정의합니다.
1) 입력은 인터페이스
type UserRepo interface {
FindByID(ctx context.Context, id string) (*User, error)
}
type Service struct {
repo UserRepo
}
func NewService(repo UserRepo) *Service {
return &Service{repo: repo}
}
이 구조면 테스트에서 mock 주입이 쉽고 구현체 교체 비용이 작습니다.
2) 반환은 구조체
func NewPostgresRepo(db *sql.DB) *PostgresRepo {
return &PostgresRepo{db: db}
}
반환 타입을 인터페이스로 감추면 호출자가 실제 기능을 알기 어렵고 타입 추론도 약해집니다.
3) 인터페이스는 작게
type UserFinder interface {
FindByID(ctx context.Context, id string) (*User, error)
}
type UserSaver interface {
Save(ctx context.Context, u *User) error
}
큰 인터페이스는 구현 부담과 결합도를 동시에 키웁니다.
4) 포인터 vs 값 수신자
선택 기준:
- 상태 변경 필요: 포인터 수신자
- 큰 구조체: 포인터 수신자
- 작은 불변 값: 값 수신자 가능
중요한 점은 혼용 최소화입니다. 타입 하나에서는 가능하면 한 스타일로 통일하세요.
5) 인터페이스 만족 조건 주의
포인터 수신자 메서드만 있으면 값 타입은 인터페이스를 만족하지 못합니다.
type S interface{ String() string }
type T struct{ v string }
func (t *T) String() string { return t.v }
var s S
// s = T{v:"x"} // 불가
s = &T{v: "x"} // 가능
실무 체크리스트
- 인터페이스가 3개 메서드를 넘어가면 분리 검토
- 생성자 반환 타입이 인터페이스인지 이유 확인
- 테스트 더블(mock/fake) 구현 난이도가 과도한지 확인
요약
Go 인터페이스 설계는 "추상화의 위치"가 핵심입니다. 소비자 기준의 작은 인터페이스와 명확한 구체 반환을 지키면 테스트성과 확장성이 동시에 확보됩니다.