의존성 주입
Coddy GO 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 107개 중 45번째.
의존성 주입(Dependency Injection)은 구조체(struct)가 내부에서 의존성을 직접 생성하는 대신 외부로부터 전달받는 기법입니다. Go에서 인터페이스(interfaces)는 이 패턴을 자연스럽고 강력하게 만들어 줍니다.
구조체 내부에 특정 구현을 하드코딩하는 대신, 인터페이스를 받아들입니다. 이를 통해 구조체의 코드를 변경하지 않고도 구현을 교체할 수 있습니다:
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
}
이제 구체적인 타입이 아닌 인터페이스에 의존하는 구조체(struct)를 생성합니다:
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를 사용하는지 알지 못하며 신경 쓰지도 않습니다. 서비스를 생성할 때 의존성을 주입합니다:
func main() {
emailService := NewOrderService(EmailNotifier{})
fmt.Println(emailService.PlaceOrder("Book")) // 이메일: 주문 완료: Book
smsService := NewOrderService(SMSNotifier{})
fmt.Println(smsService.PlaceOrder("Phone")) // SMS: 주문 완료: Phone
}
이 접근 방식은 코드를 더 유연하고 테스트 가능하게 만듭니다. 테스트 중에는 실제로 메시지를 보내지 않는 모의(mock) 알림기(notifier)를 주입할 수 있습니다. 프로덕션 환경에서는 실제 구현체를 주입합니다. 두 시나리오 모두에서 구조체(struct)는 변경되지 않은 상태로 유지됩니다.
챌린지
쉬움의존성 주입(dependency injection)의 강력함을 보여주는 로깅 시스템을 구축해 보겠습니다. 서비스가 로그가 실제로 어디로 가는지 알거나 신경 쓰지 않고도 다양한 목적지에 로그를 기록할 수 있는 서비스를 만들 것입니다.
코드는 세 개의 파일로 구성됩니다:
logger.go:Log(message string) string메서드를 요구하는Logger인터페이스를 정의합니다. 그런 다음 두 가지 다른 로거 구현체를 만듭니다:Prefix필드를 가진ConsoleLogger— 이 로거의Log메서드는[CONSOLE] [Prefix]: [message]를 반환합니다.Filename필드를 가진FileLogger— 이 로거의Log메서드는[FILE:[Filename]] [message]를 반환합니다.
service.go: 구체적인 타입이 아닌Logger인터페이스에 의존하는AppService구조체를 만듭니다.Logger를 인자로 받아AppService의 포인터를 반환하는 생성자 함수NewAppService를 포함하세요.AppService에DoWork(task string) string이라는 메서드를 추가합니다. 이 메서드는 주입된 로거를 사용하여Processing: [task]라는 메시지를 로깅하고, 로거가 반환하는 값을 그대로 반환합니다.main.go: 입력으로부터 설정을 읽고, 두 로거 타입을 모두 생성한 뒤, 각각을 별도의AppService인스턴스에 주입합니다. 그리고 제공된 작업(task)으로 각 서비스의DoWork를 호출합니다. 각 서비스 호출의 결과를 출력하세요.
다음 입력이 제공됩니다:
- 줄 1: 콘솔 로거 접두사 (Console logger prefix)
- 줄 2: 파일 로거 파일명 (File logger filename)
- 줄 3: 처리할 작업 (Task to process)
예를 들어, INFO, app.log, user authentication이 주어지면 출력은 다음과 같아야 합니다:
[CONSOLE] INFO: Processing: user authentication
[FILE:app.log] Processing: user authenticationAppService가 콘솔에 로깅하는지 파일에 로깅하는지 알지 못한다는 점에 주목하세요. 단지 주입된 로거의 Log를 호출할 뿐입니다. 이러한 유연성이 의존성 주입의 핵심입니다. 동일한 서비스 코드가 완전히 다른 로깅 구현체와 함께 작동할 수 있습니다.
치트 시트
의존성 주입(Dependency Injection)은 구조체가 내부에서 의존성을 직접 생성하는 대신 외부로부터 전달받는 기법입니다. 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)
}
서비스를 생성할 때 의존성을 주입합니다:
emailService := NewOrderService(EmailNotifier{})
fmt.Println(emailService.PlaceOrder("Book")) // Email: Order placed: Book
smsService := NewOrderService(SMSNotifier{})
fmt.Println(smsService.PlaceOrder("Phone")) // SMS: Order placed: Phone
이 방식은 구조체의 코드를 변경하지 않고도 서로 다른 구현체로 교체할 수 있게 함으로써 코드를 더 유연하고 테스트 가능하게 만듭니다.
직접 해보기
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: 각 서비스에서 작업을 사용하여 DoWork를 호출하고 결과 출력
fmt.Println("result1")
fmt.Println("result2")
}
이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.
객체 지향 프로그래밍의 모든 레슨
8에러 처리와 OOP
error 인터페이스사용자 정의 에러 타입에러 래핑 (fmt.Errorf)센티넬 에러errors.Is()와 errors.As()Panic, Defer, Recover요약 - 파일 파서