[Go] SOLID in Go - 인터페이스 분리 원칙

SOLID in Go · 9/10
  1. SOLID란?
  2. 구조체와 메서드
  3. 인터페이스
  4. 컴포지션
  5. 패키지
  6. 단일 책임 원칙
  7. 개방 폐쇄 원칙
  8. 리스코프 치환 원칙
  9. 인터페이스 분리 원칙
  10. 의존 관계 역전 원칙

인터페이스 분리 원칙 (Interface Segregation Principle)

클라이언트는 자신이 이용하지 않는 메서드에 의존하지 않아야 한다.

 인터페이스를 정의할 때 ‘이 기능도 필요할 것 같고 저 기능도 필요할 것 같아’라고 생각해서 처음부터 너무 많은 메서드를 인터페이스에 포함시킬 수 있습니다. 그러다 보면 인터페이스를 구현할 때 불필요한 기능을 억지로 구현해야 하는 문제가 발생할 수 있습니다. 인터페이스 분리 원칙은 이를 방지하기 위한 원칙으로, 인터페이스를 잘게 분리하여 클라이언트의 용도에 맞는 인터페이스만을 제공하는 것을 의미합니다.

너무 많은 기능을 가진 인터페이스

 Machine 인터페이스는 문서 출력, 팩스 그리고 스캔까지 총 세 개의 메서드를 가지고 있습니다. 흔하게 볼 수 있는 복합기를 추상화한 것이라고 볼 수 있습니다.

type Machine interface {
	Print(d Document)
	Fax(d Document)
	Scan(d Document)
}

 그런데 문서 출력만 가능한 오래된 프린터가 있습니다. 90년대 윈도 os를 그대로 사용하는 연구실도 있으니 놀라운 일은 아니죠. 이 프린터로 Machine 인터페이스를 구현하는 과정에서 팩스와 스캔은 기능 구현이 불가능하다 보니 이 메서드들을 호출하게 되면 시스템에 문제(패닉)가 발생하게 됩니다.

type OldFashionedPrinter struct{}

func (o OldFashionedPrinter) Print(d Document) {
	fmt.Printf("Print: %s\n", d.Content)
}

func (o OldFashionedPrinter) Fax(d Document) {
	panic("operation not supported")
}

func (o OldFashionedPrinter) Scan(d Document) {
	panic("operation not supported")
}

var _ Machine = (*OldFashionedPrinter)(nil)
package main

import (
	"solid/isp"
)

func main() {
	p := isp.OldFashionedPrinter{}
	d := isp.Document{Content: "This is a document"}
	p.Scan(d)
}
$ go run ./main.go
panic: operation not supported

 이처럼 클라이언트가 자신이 사용하지 않는 메서드에 의존하게 됨으로 인해 불필요한 문제 상황을 야기할 수 있습니다.

인터페이스 분리

 Machine 인터페이스를 분리하여 다음과 같이 세 개의 인터페이스를 정의했습니다. 이제 각 인터페이스는 하나의 메서드만을 가지고 있습니다.

type Printer interface {
	Print(d Document)
}

type Scanner interface {
	Scan(d Document)
}

type Faxer interface {
	Fax(d Document)
}

 클라이언트는 세 개의 인터페이스 중 자신이 필요한 인터페이스만을 선택하여 구현할 수 있습니다. 앞서 구현하지도 못하는 메서드에 억지로의존하던 상황과는 큰 차이를 보여줍니다.

type CustomPrinter struct{}

func (c CustomPrinter) Print(d Document) {
	fmt.Printf("Print: %s\n", d.Content)
}

func (c CustomPrinter) Scan(d Document) {
	fmt.Printf("Scan: %s\n", d.Content)
}

var _ Printer = (*CustomPrinter)(nil)
var _ Scanner = (*CustomPrinter)(nil)

 작게 쪼개진 인터페이스들은 필요에 따라 다른 인터페이스에 임베딩하여 사용할 수도 있습니다.

type CustomMachine interface {
	Printer
	Scanner
}

var _ CustomMachine = (*CustomPrinter)(nil)

인터페이스 분리 원칙의 이점 및 사용 시 주의할 점

이점

  • 더 작은 인터페이스: 인터페이스를 더 작고 관련성 높은 부분으로 분할함으로써 각 인터페이스의 목적과 책임이 명확해집니다.
  • 느슨한 결합: 클라이언트가 자신이 사용하지 않는 메서드에 의존하지 않게 됨으로써 변경이 쉬워지고 유지보수가 용이해집니다.
  • 유연성 및 확장성: 시스템을 확장하거나 변경할 때 필요한 인터페이스를 선택적으로 구현할 수 있으므로 새로운 기능 출가 또는 기존 기능의 수정을 최소화하고 코드 재사용성을 높일 수 있습니다.

주의할 점

  • 과도한 분리: 인터페이스를 지나치게 분리하게 되면 비슷한 기능을 가진 여러 개의 작은 인터페이스가 만들어질 수 있습니다.
  • 복잡성 증가: 너무 많은 작은 인터페이스로 인해 오히려 코드와 시스템 구조를 이해하는 것이 어려워질 수 있습니다.

마치며

 인터페이스 분리 원칙에 대해 알아보았습니다. 하나의 인터페이스에 너무 많은 기능을 집어넣는다거나 인터페이스를 너무 잘게 쪼개서 문제 상황과 마주치게 될 수 있다는 것. 즉, 과유불급이다! 그렇다고 정말 필요한 기능만 딱 정의를 한다는 것이 생각만큼 쉽지는 않을 것 같습니다. 많은 연습이 필요할 것 같네요.


참고자료

https://www.udemy.com/course/design-patterns-go/

글에서 수정이 필요한 부분이나 설명이 부족한 부분이 있다면 댓글로 남겨주세요!