티스토리 뷰
📺 시리즈
2023.10.02 - [Go/디자인 패턴] - [Go] SOLID in Go - SOLID란?
2023.10.03 - [Go/디자인 패턴] - [Go] SOLID in Go - 구조체와 메서드
👾 인터페이스
인터페이스를 사용하면 구조체와 메서드를 사용해 구현한 구체화된 객체가 아닌 추상화된 객체를 통해 객체 간의 상호작용을 정의할 수 있습니다.
인터페이스는 메서드의 이름, 매개변수와 반환값의 타입을 정의하며, 이를 구현하는 것은 구조체 또는 별칭 타입과 같은 타입에 달려있습니다.
type DuckInterface interface {
Say()
Swim()
Walk(distance int) int
}
Go에서 인터페이스는 'implements'와 같은 구현을 위한 명시적인 키워드를 사용하지 않습니다. 그저 특정 타입이 인터페이스에 정의된 모든 메서드를 구현했다면 해당 타입이 인터페이스를 구현했다고 보는 덕 타이핑(Duck Typing) 방식을 사용합니다.
type Duck struct{}
func (d Duck) Say() {
println("Quack")
}
func (d Duck) Swim() {
println("Duck swimming")
}
func (d Duck) Walk(distance int) int {
return distance / 2
}
그렇다고 특정 타입이 인터페이스를 구현했는지 확인할 수 있는 방법이 없는 것은 아닙니다.
var _ DuckInterface = (*Duck)(nil)
이렇게 nil값을 Duck 타입의 포인터로 감싼 결과가 DuckInterface 타입의 변수에 할당이 가능한지를 통해 확인이 가능합니다.
인터페이스를 구현한 타입은 인터페이스로 사용될 수 있습니다.
func main() {
duck := Duck{} // Duck 구조체를 생성하여 duck에 할당
var duckIface DuckInterface
duckIface = duck // DuckInterface 인터페이스 타입의 변수에 duck을 할당
duckIface.Say() // duck.Say()를 호출한 것과 동일한 결과 출력
}
$ go run main.go
Quack
🧩 인터페이스는 왜 필요한가?
느슨한 결합도
인터페이스는 느슨한 결합을 가능하게 합니다. 느슨한 결합은 무엇일까요? 예제 코드와 함께 살펴보겠습니다.
강한 결합도
type Paypal struct{}
type PaymentServiceA struct {
paypal *Paypal
}
func (ps *PaymentServiceA) Pay(amount float32) string {
...
return fmt.Sprintf("%0.2f paid using naver pay", amount)
}
오직 한 가지 결제 방식을 제공하는 결제 서비스 A가 있습니다. 그리고 이 서비스는 Paypal을 결제 방식으로 제공하고 있습니다. 그런데 이 서비스가 한국 시장에 진출하려고 보니 한국은 Paypal보다는 네이버 페이가 점유율이 높다는 것을 알게 되었습니다. 그래서 Paypal대신 네이버 페이를 결제 방식으로 제공하기 위해 코드를 변경하고자 합니다.
네이버 페이로 교체하기 위해 Paypal 객체를 제거하니 산탄총을 맞은 것처럼 여기저기서 알 수 없는 빨간 줄들이 마구 나타납니다. 어찌어찌 Paypal 객체와 관련된 코드들을 싹 정리하고 다시 네이버 페이를 추가하려고 보니 차라리 서비스를 새로 만드는 것이 나았을지도 모른다는 생각이 듭니다.
왜 이런 문제가 발생할까요? PaymentServiceA 객체는 구체화된 Paypal 객체에 의존하고 있습니다. 이처럼 구체화된 객체에 의존하는 것은 결합도를 강하게 하여 새로운 기능을 추가하거나 코드를 변경하는 것을 어렵게 만듭니다.
느슨한 결합도
A사의 직원 한 명이 차라리 내가 새로 만드는 게 빠르겠다는 생각에 A사를 때려치우고 B사를 설립했습니다. 그리고 다음과 같이 서비스를 구현했습니다.
type PaymentMethod interface {
Pay(amount float32) string
}
type PaymentServiceB struct {
paymentMethod PaymentMethod
}
func (ps *PaymentServiceB) Pay(amount float32) string {
return ps.paymentMethod.Pay(amount)
}
이번에는 PaymentMethod라는 추상화된 객체를 정의하여 PaymentServiceB가 추상화된 객체에 의존하게 하였습니다. 이 방식은 구체화된 객체에 의존하는 방식과 어떤 차이가 있을까요?
PaymentServiceB는 추상화된 객체, 인터페이스에 의존하고 있기 때문에 그 뒤에 어떤 구체화된 객체가 사용되었는지에 상관없이 인터페이스에 정의된 기능들을 사용하기만 하면 됩니다. 즉, PaymentServiceB와 결제 방식들이 인터페이스를 통해 느슨하게 연결되어 있는 것입니다. 이렇게 되면 결제 방식을 변경하기 위해 모든 코드를 뜯어 고칠 필요 없이 인터페이스를 구현한 객체를 슬쩍 변경하기만 하면 됩니다. 그리고 결제 방식을 변경하기 위해 PaymentServiceB 자체를 건드릴 필요가 없으니 PaymentServiceB를 다른 곳에서 재사용할 수도 있습니다. 이처럼 인터페이스를 사용하여 추상화된 객체에 의존하게 함으로써 느슨한 결합을 가능하게 하고, 코드의 재사용성과 유지보수성을 높일 수 있습니다.
다형성(Polymorphism)
PaymentService는 PaymentMethod 인터페이스에 통해 그 뒤에 PaymentMethod 인터페이스를 저마다의 방식으로 구현한 객체들과 상호작용할 수 있습니다. Go는 이렇게 인터페이스를 사용하여 다형성을 제공합니다.
package main
import "fmt"
type PaymentMethod interface {
Pay(amount float32) string
}
type CreditCard struct{}
func (c *CreditCard) Pay(amount float32) string {
return fmt.Sprintf("%0.2f paid using credit card", amount)
}
type Paypal struct{}
func (p *Paypal) Pay(amount float32) string {
return fmt.Sprintf("%0.2f paid using paypal", amount)
}
type NaverPay struct{}
func (n *NaverPay) Pay(amount float32) string {
return fmt.Sprintf("%0.2f paid using naver pay", amount)
}
type PaymentService struct {
paymentMethod PaymentMethod
}
func (ps *PaymentService) Pay(amount float32) string {
return ps.paymentMethod.Pay(amount)
}
func main() {
svcWithCC := &PaymentService{paymentMethod: &CreditCard{}}
svcWithPP := &PaymentService{paymentMethod: &Paypal{}}
svcWithNP := &PaymentService{paymentMethod: &NaverPay{}}
fmt.Println(svcWithCC.Pay(100))
fmt.Println(svcWithPP.Pay(100))
fmt.Println(svcWithNP.Pay(100))
}
$ go run main.go
100.00 paid using credit card
100.00 paid using paypal
100.00 paid using naver pay
🙏 마치며
이번에는 인터페이스에 대해 간단하게 알아보았습니다. 인터페이스를 통해 객체 간의 느슨한 결합이 가능하고 코드의 재사용성과 유지보수성을 높일 수 있습니다. 그리고 Go에서는 구체화된 객체가 다형성을 가질 수는 없지만, 인터페이스를 통해 다형성을 제공할 수 있습니다.
다음 게시글에서는 컴포지션을 통해 Go의 또 다른 객체지향적 면모를 살펴보겠습니다.
📖 참고자료
글에서 수정이 필요한 부분이나 설명이 부족한 부분이 있다면 댓글로 남겨주세요!
'Go > 디자인 패턴' 카테고리의 다른 글
[Go] SOLID in Go - 단일 책임 원칙 (1) | 2023.10.11 |
---|---|
[Go] SOLID in Go - 패키지 (1) | 2023.10.10 |
[Go] SOLID in Go - 컴포지션 (1) | 2023.10.09 |
[Go] SOLID in Go - 구조체와 메서드 (0) | 2023.10.03 |
[Go] SOLID in Go - SOLID란? (1) | 2023.10.02 |