의존성 주입
Coddy GO 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨. 107개 중 45번째.
의존성 주입은 구조체가 의존성을 내부에서 생성하는 대신 외부에서 받는 기법입니다. Go에서는 인터페이스를 사용하여 이 패턴을 자연스럽고 강력하게 구현할 수 있습니다.
구조체 내부에 특정 구현을 하드코딩하는 대신 인터페이스를 받습니다. 이렇게 하면 구조체의 코드를 변경하지 않고도 구현을 교체할 수 있습니다:
type Notifier interface {
Send(message string) string
}
type EmailNotifier struct{}
func (e EmailNotifier) Send(message string) string {
return "Email: " + message
}
type SMSNotifier struct{}
func (s SMSNotifier) Send(message string) string {
return "SMS: " + message
}
이제 구체 타입이 아니라 인터페이스에 의존하는 구조체를 만드세요:
type OrderService struct {
notifier Notifier // 인터페이스를 통해 주입된 의존성
}
func NewOrderService(n Notifier) *OrderService {
return &OrderService{notifier: n}
}
func (o *OrderService) PlaceOrder(item string) string {
return o.notifier.Send("Order placed: " + item)
}
OrderService는 이메일을 사용하는지 SMS를 사용하는지 알 필요도 없고 신경 쓰지도 않습니다. 서비스를 생성할 때 dependency를 주입합니다:
func main() {
emailService := NewOrderService(EmailNotifier{})
fmt.Println(emailService.PlaceOrder("Book")) // Email: 주문 완료: Book
smsService := NewOrderService(SMSNotifier{})
fmt.Println(smsService.PlaceOrder("Phone")) // SMS: 주문 완료: Phone
}
이 접근 방식은 코드를 더 유연하고 테스트하기 쉽게 만들어 줍니다. 테스트 중에는 실제로 메시지를 보내지 않는 모의 notifier를 주입할 수 있습니다. 프로덕션에서는 실제 구현을 주입합니다. 두 시나리오 모두에서 구조체는 변경되지 않습니다.
챌린지
쉬움의존성 주입의 강력한 기능을 보여 주는 logging system을 만들어 봅시다. service가 로그가 실제로 어디로 전달되는지 알거나 신경 쓰지 않아도, 서로 다른 destination에 로그를 쓸 수 있는 service를 만들 것입니다.
코드를 세 개의 파일로 구성합니다:
logger.go:Loggerinterface를 Define하고,Log(message string) stringmethod를 요구하도록 합니다. 그런 다음 두 가지 logger 구현을 만듭니다:ConsoleLogger:Prefixfield를 가집니다. ItsLogmethod는[CONSOLE] [Prefix]: [message]를 반환합니다.FileLogger:Filenamefield를 가집니다. ItsLogmethod는[FILE:[Filename]] [message]를 반환합니다.
service.go:Loggerinterface(구체적인 type이 아님)에 의존하는AppServicestruct를 만듭니다.Logger를 accepts하고AppService포인터를 반환하는 constructor functionNewAppService를 포함합니다.AppService에DoWork(task string) string이라는 method를 추가합니다. 이 method는 injected logger를 사용해Processing: [task]message를 log하고, logger가 반환하는 값을 그대로 반환합니다.main.go: input에서 configuration을 읽고, 두 logger type을 모두 만든 다음, 각각을 별도의AppServiceinstance에 inject하고, 제공된 task와 함께 각 service에서DoWork를 Call합니다. 각 service Call의 결과를 출력합니다.
다음 input이 제공됩니다:
- Line 1: Console logger prefix
- Line 2: File logger filename
- Line 3: 처리할 task
예를 들어 INFO, app.log, user authentication이 주어지면 output은 다음과 같아야 합니다:
[CONSOLE] INFO: Processing: user authentication
[FILE:app.log] Processing: user authenticationAppService는 console에 log하는지 file에 log하는지 알 필요가 없다는 점에 주목하세요. 단순히 injected된 logger에서 Log를 Call합니다. 이러한 flexibility가 의존성 주입의 본질입니다. 동일한 service code가 완전히 다른 logging 구현과 함께 작동합니다.
직접 해보기
package main
import (
"bufio"
"fmt"
"os"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// 콘솔 로거 접두사 읽기
prefix, _ := reader.ReadString('\n')
prefix = prefix[:len(prefix)-1]
// 파일 로거 파일 이름 읽기
filename, _ := reader.ReadString('\n')
filename = filename[:len(filename)-1]
// 처리할 작업 읽기
task, _ := reader.ReadString('\n')
if len(task) > 0 && task[len(task)-1] == '\n' {
task = task[:len(task)-1]
}
// TODO: 접두사로 ConsoleLogger 생성
// TODO: 파일 이름으로 FileLogger 생성
// TODO: ConsoleLogger가 주입된 AppService 생성
// TODO: FileLogger가 주입된 또 다른 AppService 생성
// TODO: 각 서비스에서 task로 DoWork를 호출하고 결과 출력
fmt.Println("result1")
fmt.Println("result2")
}
이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.
객체 지향 프로그래밍의 모든 레슨
8에러 처리와 OOP
error 인터페이스사용자 정의 에러 타입에러 래핑 (fmt.Errorf)센티넬 에러errors.Is()와 errors.As()Panic, Defer, Recover요약 - 파일 파서직접 연습해 보세요: 온라인 Go 컴파일러