golang은 객체지향 언어가 아닙니다.
import만 하면 어디서든 해당패키지에 접근할 수 있으며
같은 패키지 내에서는 네임스페이스 충돌이 나서 객체지향 언어를 주로 사용하던분 한테는 뭔가 어색합니다.
객체지향처럼 만들어봤습니다.
두가지 방법을 정리합니다.
https://github.com/jaeho310/golang-oop
https://github.com/jaeho310/golang-semi-oop
요구사항
아래와 같은 요구사항을 golang으로 표현 하겠습니다.
사용자는 홀에 주문을하고 홀은 주방에 요리를 요청합니다.
패키지구조
|-- dto
| `-- food_dto.go
|-- infrastructure
| |-- dining_hall.go
| `-- kitchen.go
`-- main.go
첫번째 방법(github.com/jaeho310/golang-oop)
첫번째 방법은 최대한 객체지향 언어처럼 구현한 방법입니다.
food_dto.go
package dto
// 게터세터를 사용하기 싫다면
// 멤버변수의 첫글자를 대문자로 사용하셔도 됩니다.
type FoodDto struct {
name string
count int
}
func (FoodDto) New(name string, count int) *FoodDto {
return &FoodDto{name: name, count: count}
}
func (food *FoodDto) SetName(name string) {
food.name = name
}
func (food *FoodDto) GetName() string {
return food.name
}
func (food *FoodDto) SetCount(count int) {
food.count = count
}
func (food *FoodDto) GetCount() int {
return food.count
}
golang은 public private이 없습니다.
대신 멤버변수를 카멜케이스로 명시하면 외부패키지에서 접근할수 없습니다.
세터에 추가로직이 필요한 경우에 파스칼케이스로 세터를 만들어 사용합니다
kitchen.go
package infrastructure
import (
"errors"
"golang-oop/dto"
"strconv"
)
type Kitchen struct{}
func (Kitchen) New() *Kitchen {
return &Kitchen{}
}
func (*Kitchen) Cook(foodDto *dto.FoodDto) (string, error) {
foodName := foodDto.GetName()
foodCount := foodDto.GetCount()
return foodName + strconv.Itoa(foodCount) + " 개", nil
}
멤버필드가 없더라도 파일명과 똑같이 struct를 하나 만들어줍니다.
모든 메서드들은 struct의 포인터가 접근할수 있도록 만들어줍니다.
golang 특성상 패키지이름만 갖고 메서드에 접근할수 있으므로 New도 struct가 접근할수 있도록 만들어줍니다.
dining_hall.go
package infrastructure
import (
"errors"
"golang-oop/dto"
)
type DiningHall struct {
kitchen *Kitchen
}
func (DiningHall) New(kitchen *Kitchen) *DiningHall {
return &DiningHall{kitchen}
}
func (diningHall *DiningHall) Order(foodDto *dto.FoodDto) (string, error) {
if foodDto.GetName() == "" || foodDto.GetCount() <= 0 {
return "", errors.New("주문을 확인해주세요")
}
response, err := diningHall.kitchen.Cook(foodDto)
if err != nil {
return "", err
}
return "주문하신 " + response + " 나왔습니다.", nil
}
홀에서는 주문이 정상적인지 확인하고 주방에 요청합니다.
모든 메서드는 dininghall 포인터만 접근할수 있도록 구현합니다.
golang 특성상 패키지이름만 갖고 메서드에 접근할수 있으므로 New도 struct가 접근할수 있도록 만들어줍니다.
main.go
package main
import (
"fmt"
"golang-oop/dto"
"golang-oop/infrastructure"
)
func main() {
myFood := dto.FoodDto{}.New("스테이크", 2)
kitChen := infrastructure.Kitchen{}.New()
diningHall := infrastructure.DiningHall{}.New(kitChen)
result, err := diningHall.Order(myFood)
if err != nil {
fmt.Println(err.Error())
return
}
fmt.Println(result)
}
이전파일을 보면 New메서드도 struct가 접근할수 있도록 구현해줬습니다.
인프라패키지에 New 메서드는 두개이므로 struct로 꼭 묶어줘야 합니다.
New 메서드는 포인터가 아닌 일반 struct가 접근가능하도록 구현해놨습니다.
홀에 식당의존성을 주입해준뒤 사용합니다.
두번째 방법(github.com/jaeho310/golang-semi-oop)
약식 방식입니다.
객체지향보다는 static하게 사용하는 느낌이 강합니다.
파일이름과 똑같은 더미 struct를 만들어주고 struct가 접근할수 있는 메서드를 사용하는건 같습니다.
그러나 의존성 주입은 하지않고 struct{}.메서드() 로 접근하여 네임스페이스 충돌을 피합니다.
개발속도와 편의성을 올리기위한 방법입니다.
food_dto.go
package dto
type FoodDto struct {
Name string
Count int
}
고랭이 제공해준 있는 그대로의 모습입니다.
kitchen.go
package infrastructure
import (
"golang-oop/dto"
"strconv"
)
type Kitchen struct{}
func (Kitchen) Cook(foodDto *dto.FoodDto) (string, error) {
foodName := foodDto.Name
foodCount := foodDto.Count
return foodName + strconv.Itoa(foodCount) + " 개", nil
}
파일이름과 같은 struct만 잘 만들어줍니다.
dto에서 게터가 없으니 그냥 꺼내줍니다.
dining_hall.go
package infrastructure
import (
"errors"
"golang-oop/dto"
)
type DiningHall struct {}
func (diningHall DiningHall) Order(foodDto *dto.FoodDto) (string, error) {
if foodDto.Name == "" || foodDto.Count <= 0 {
return "", errors.New("주문을 확인해주세요")
}
response, err := Kitchen{}.Cook(foodDto)
if err != nil {
return "", err
}
return "주문하신 " + response + " 나왔습니다.", nil
}
홀 구조체도 비어있습니다.
주방에 요리를 요청할때는 Kitchen{}.Cook() 으로 요청합니다.
main.go
package main
import (
"fmt"
"golang-oop/dto"
"golang-oop/infrastructure"
)
func main() {
myFood := &dto.FoodDto{Name: "스테이크", Count: 2}
result, err := infrastructure.DiningHall{}.Order(myFood)
if err != nil {
fmt.Println(err.Error())
return
}
fmt.Println(result)
}
주방의존성을 홀에 주입해줄 필요가 없습니다.
DiningHall{}.Order() 로 주문을 요청합니다.
실행결과
$ go run main.go
주문하신 스테이크2 개 나왔습니다.
결론
고랭은 같은 패키지내에 동일한 메서드를 만들면 충돌이 생기니 더미 구조체를 만들면 피할 수 있습니다.
1번, 2번 모두 많이 사용됩니다.
2번 방식을 사용하면 저수준의 모듈에 의존하게되어 추후 유지보수시 고생하게 될 확률이 존재합니다.
1번방식에 IOC를 적용시면 변화에 유연하게 대처가 가능하므로 1번 방식을 방식을 추천드립니다.
(IOC 적용 예제)
https://frozenpond.tistory.com/120
'golang' 카테고리의 다른 글
[golang] 고루틴(go routine)이란 (0) | 2021.11.15 |
---|---|
[golang] golang defer 사용법 및 예제(golang의 catch는 defer안에서) (0) | 2021.11.15 |
[golang] go언어의 의존성 주입(di)과 제어의 역전(ioc) (0) | 2021.08.15 |
[golang] go언어 예외처리 방법 (0) | 2021.08.15 |
[golang] golang echo framework와 layered architecture를 활용한 백엔드 api 서버 구축하기 (0) | 2021.07.14 |
댓글