본문 바로가기
golang

[golang] go언어를 객체지향언어처럼 사용하는방법

by devjh 2021. 8. 16.
반응형

golang은 객체지향 언어가 아닙니다.

import만 하면 어디서든 해당패키지에 접근할 수 있으며

같은 패키지 내에서는 네임스페이스 충돌이 나서 객체지향 언어를 주로 사용하던분 한테는 뭔가 어색합니다. 

 

객체지향처럼 만들어봤습니다.

 

두가지 방법을 정리합니다.

 

https://github.com/jaeho310/golang-oop

 

GitHub - jaeho310/golang-oop

Contribute to jaeho310/golang-oop development by creating an account on GitHub.

github.com

https://github.com/jaeho310/golang-semi-oop

 

GitHub - jaeho310/golang-semi-oop

Contribute to jaeho310/golang-semi-oop development by creating an account on GitHub.

github.com

 

요구사항

아래와 같은 요구사항을 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] go언어의 의존성 주입(di)과 제어의 역전(ioc)

이번게시글에서는 go 언어에서의 제어의 역전과 의존성 주입에 대해 정리합니다. 구현방식은 아래 게시글의 첫번째 방법을 사용하였습니다. https://frozenpond.tistory.com/123 [golang] go언어를 객체지향

frozenpond.tistory.com

 

반응형

댓글