객체지향적인 Go 프로그래밍이란?

동료들의 부족한 설계 능력을 보고 있자니 할 수 없이 Go 코드[1]를 봐야 했다. 한편, 내 옆에 앉는 김형준님은 회사의 거의 모든 코드를 리뷰하며 나에게 감상평을 한다. 종종 번거롭다는 생각이 들기도 하지만, 워낙 열심히 설명을 하시니 다른 일을 하다가도 잠시 집중력을 발휘하여 들어보려고 노력한다. 꼭, 뭔가 직접적인 도움을 주지 못하더라도 이런 경우 그저 들어주는 행위만으로도 상대방에게 상당한 기여를 할 수 있다고 믿기 때문이다.

Go를 쓴다고 실력이 나아지는 것은 아니다

중국 동료들의 설계 역량이 매우 떨어지는데 초보 수준의 Go 개발이 유행이다. 한 글자를 애용하는 짧은 변수명, 다중 리턴, 그리고 매개변수에 함수를 중첩해서 던지는 식의 3단 콤보는 가독성을 무척 떨어지게 했다. 코드 저장소에서 보이는 대체적인 조류가 짧게 짜는 것을 '좀 멋있어 하는 식'으로 보였다. 코드를 구조화 하는 노력은 눈에 띄지 않았다. 주업이 개발이 아닌지라 아쉽다는 생각만 하던 차에 기회가 왔다.

언젠가 김형준님이 'Go는 새로운 언어라 자바처럼[2] 보고 배울만한 코드가 많지 않나'하는 식으로 넋두리를 했다. 그 말을 들은 탓에 여가 시간에 동료 개발자가 쓴  Go 책을 펼쳐서 객체지향 프로그래밍을 다룬 4장만 찾아서 읽고 따라해봤다. 그리고 뒤이어 육아로 바쁜 동료  주말에 마침 비는 시간이 생겼다고 노트북을 싸들고 커피숍에서 보자고 했다. 지난 주에 개념적인 설명을 했던 것을 어떻게 구현할지 의견이 듣고 싶다고 한다.

여러 관점을 하나의 객체로 구현해보고 싶은 의도

아래 그림은 특별히 어떻게 구현할지 고려하지 않고, 개념적으로 실현하고 싶은 의도를 빠르게 기록한 내용이다. 에게 설명할 때 함께 본 페이지를 캡쳐했다.

상품 개념에 대한 스케치

상품 개념에 대한 스케치

 어떻게 시작할지 막연한 듯했다. 설명을 들을 때 개념으로는 이해가 되었는데, 코딩을 하려고 했더니 막상 무엇부터 해야 할지 모르겠다고 했다. 잘되었다 싶었다. 전에 Go 책을 읽으며 이런 식으로 짜면 객체지향스러운 Go코드가 나오지 않을까 생각에만 머물던 아이디어가 있었다. 서툰 내 솜씨로 긴 삽질을 하느니 둘이 앉아서 하면 서로에게 유익하겠다 싶었다. 그렇게 커피숍에서 한 시간 남짓 함께 시간을 보냈더니 꽤 만족스러운 아기 발걸음 결과를 얻었다.

Go 코드로 폴리모피즘 구현

우선 위 그림은 같은 Product 객체이지만, 상황에 따라 다른 관점을 표현할 수 있으면 좋겠다는 의도가 담긴 그림이다. 그림에서 코드로 반영한 부분은 두 개의 관점View으로 나누는 것이다. 먼저, 상품Product이 판매할 때는 어떤 것인지 식별이 되고 나면 가격조건이 중요하다. 이를 Priceable 이라 칭하고, 소재나 공급사 같이 생산/공급/물류에서 중요한 관점을 Material이라 부르기도 한다. Go로는 어떻게 구현할까?

상품 식별자를 나타내는 ProductID 타입을 정의하고, Priceable과 Material 두 개의 구조체를 반환하는 팩토리 메소드를 정의할 때 리시버receiver[3]로 ProductID 객체를 연결해주면 된다. 이렇게 폴리모피즘 구현이 가능하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
type ProductID int64
func (p ProductID) Material() Material {
        // 어디에선가 product 속성들을 가져와야 함
        return Material{}
}
func (p ProductID) Priceable() Priceable {
        // 어디에선가 product 속성들을 가져와야 함
        return Priceable{}
}
type Material struct {
        ProductID ProductID
        소재Code string
        생산자Code string
}
type Priceable struct {
        ProductID ProductID
        Pricer
}

물론, 완전한 코드는 아니다.[4] 그래서 조금 부연을 한다. ProductID 타입은 개념적으로는 Product 객체를 지칭하는 타입이다. 자바로 구현했다면 Product 인터페이스가 되었을 것이고, 두 개의 구조체를 Product 인터페이스를 구현한 클래스가 되었을 것이다.

한편, 팩토리 메소드 즉, Material()과 Priceable()는 향후에는 DB 에서 값을 꺼내오는 리포지토리repository 객체로 구현될 가능성이 높다.

가격 조회의 관문

두 개의 관점으로 객체를 나눈 것 이외에 추가로 시도한 것은 Priceable 구조체가 갖는 속성인 Pricer에 대한 내용이다. Pricer는 인터페이스 타입이다. 쇼핑몰이나 유통 관련 코드에서 가장 까다로운 작업이 정확한 가격을 다수 사용자에게 노출하는 일이다. 각종 프로모션[5]에 따라 구매자에게 제공하는 가격집합이 달라지는데, 많은 곳에서 이를 복잡한 쿼리로 구현한다. 이를 마이크로 서비스 형태로 대체하려는 야심을 담은 간단한 선언이다. 여기서 핵심 메소드는 최저가() 인데, 아직 미구현 상태이고 언젠가 구현하고 또 공유하길 기대한다.

1
2
3
4
5
6
7
type Pricer interface {
        원가() float64 // 갱신주기: 상품 등록 시점
        대표판매가() float64 // 갱신주기: 시즌
        최저가() float64 // 갱신주기: 수시
        최초판매가결정(price float64)
        시즌종료할인(discountRate float64)
}

여기서 설명할 부분은 아래 코드와 같이 Pricer라는 단일 인터페이스 덕분에 Priceable 타입 구조체는 가격 정보를 얻는 모든 메소드를 호출할 수 있다는 점이다. 다시 말해, 이들 메소드가 가격을 반영하고 충분한 조회 성능을 내면서 동시에 유효한 구매자에게만 적합한 가격을 보여주는 난해한 작업을 어떻게 만들어주는가에 상관없이 Pricer 인터페이스를 사용하면 한 곳에서 해결해주는 가격 조회의 통합 관문 역할을 해준다는 사실이다. 후에 구현만 된다면 그야 말로 대박이다. :)

1
2
3
                assert.Equal(t, productIds[0].Priceable().원가(),"원가")
                assert.Equal(t, productIds[0].Priceable().대표판매가(),10)
                assert.Equal(t, productIds[0].Priceable().최저가(),10)

맺음말

이 정도 짧은 글로 제목으로 던진 질문에 답이 될 순 없다. 아마 이 질문에 대해서는 나와 한 배를 탄  함께 답을 찾아가는 삶을 살 것이고, 여력[6]이 주어지면 글로 남길 것이다. 혹시 필요한 분이 있을까 싶어 코드 전문을 아래 남긴다.

질문이나 반론은 언제나 환영이니 댓글이나 메일을 주시기 바랍니다.

소통은 언제나 환영하니 메일 주세요

소통은 언제나 환영하니 메일 주세요

Go 코드 전문

product.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package product
type ProductID int64
func (p ProductID) Material() Material {
        // 어디에선가 product 속성들을 가져와야 함
        return Material{}
}
func (p ProductID) Priceable() Priceable {
        // 어디에선가 product 속성들을 가져와야 함
        return Priceable{}
}
type Material struct {
        ProductID ProductID
        소재Code string
        생산자Code string
}
type Priceable struct {
        ProductID ProductID
        Pricer
}
type Pricer interface {
        원가() float64 // 갱신주기: 상품 등록 시점
        대표판매가() float64 // 갱신주기: 시즌
        최저가() float64 // 갱신주기: 수시
        최초판매가결정(price float64)
        시즌종료할인(discountRate float64)
}
type Repository stru ct{}
func (r Repository) Index() []ProductID {
        return []ProductID{}
}
type Offer struct {
        ProductID
}
func (o Offer) Discount(rate float64) {
}

product_test.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
package product
import (
        "testing"
        "github.com/stretchr/testify/assert"
)
func TestMultiViewRealizationForProductObject(t *testing.T) {
        productID := ProductID(1)
        material := productID.Material()
        priceable := productID.Priceable()
        assert.Equal(t, material.ProductID, priceable.ProductID)
        assert.Equal(t, material.생산자Code,"생산자Code")
        assert.Equal(t, priceable.원가(),"원가")
}
func TestPersistenceInSafe(t *testing.T) {
        r := Repository{}
        productIds := r.Index()
        assert.Equal(t,len(productIds) > 0, true)
        t.Run("원가",func(t *testing.T) {
                assert.Equal(t, productIds[0].Pricable().원가(),"원가")
        })
        t.Run("대표판매가",func(t *testing.T) {
                productIds[0].Priceable().최초판매가결정(10)
                assert.Equal(t, productIds[0].Priceable().원가(),"원가")
                assert.Equal(t, productIds[0].Priceable().대표판매가(),10)
                assert.Equal(t, productIds[0].Priceable().최저가(),10)
        })
        t.Run("시즌오프",func(t *testing.T) {
                productIds[0].Pricable().시즌종료할인(0.2)
                assert.Equal(t, productIds[0].Priceable().원가(),"원가")
                assert.Equal(t, productIds[0].Priceable().대표판매가(),10)
                assert.Equal(t, productIds[0].Priceable().최저가(),10*0.8)
        })
        t.Run("오퍼할인",func(t *testing.T) {
                // 현재 최저가: 시즌오프 가격
                assert.Equal(t, productIds[0].Priceable().최저가(),10*0.8)
                // offer discount 적용
                o := Offer{ProductID: productIds[0]}
                o.Discount(0.3)
                assert.Equal(t, productIds[0].Priceable().최저가(),10*0.7)
        })
}

주석

[1] 필자가 코딩에 손땐지가 대략 7년 정도 되었으며, Java 개발자 출신이다.

[2] 내 기준으로 객체지향 프로그래밍 코드를 처음 본 것은 아마 스프링Spring 프레임워크 코드가 아니었을까 싶다. 책을 보면 배움이 빠를 줄 알고, 디자인패턴 관련 책을 죄다 읽고 나서도 스스로 써먹지 못했다. 2004년경 실제 프로젝트에 스프링을 적용하고 나서 삽질 과정에서 문서가 없어서 마지못해 코드를 열어보게 되었는데, 그때 비로소 신세계를 만났다. 소스 코드 자체가 그대로 엔터프라이즈Enterprise 즉, 기업용 응용프로그래밍에서 어떻게 디자인 패턴을 쓸 수 있는지 여실히 보여주었다. 아무튼 그러한 시간 낭비의 경험과 한국형이라는 척박함이 아직 존재하는 우리나라 개발 커뮤니티가 좀 더 나아질 수 있을까 하는 마음에 다듬어지지 않은 글이나마 얼른 써서 공유합니다.

[3] Go 언어 웹 프로그래밍 철저 입문 4장 참조 요망

[4] 앞서 말한대로 모호한 개념에 대해서 코드로 어떻게 구현해야 하는지 서로 확인하는 자리에서 작성한 것임을 잊지 말자.

[5] 통상적으로 구매자는 한 곳에서 물건을 사지만, 해당 물건은 공급과정에서 여러 회사를 거쳐오는 일이 흔히 있다. 그래서 물건이 팔리면 이익배분을 하게 되는데, 이익을 줄이면서 매출을 늘리려고 하는 프로모션 행사는 매우 다양한 형태로 만들어지고 유지된다.

[6] 필자는 29개월 아이와 1개월된 아이 육아중인 아내의 남편이다. 그래서, 글쓰는 시간 확보가 만만치는 않다.


Popit은 페이스북 댓글만 사용하고 있습니다. 페이스북 로그인 후 글을 보시면 댓글이 나타납니다.