> ## Content Index
> Fetch the complete content index at: https://www.popit.kr/llms.txt
> Use this file to discover other available public pages before exploring further.

# 커머스 코드 자산화 개발 일지-2 상품을 팔지 않고 오퍼를 판다
- URL: https://www.popit.kr/commerce-code-asset-2-sell-offers-not-products/
- Published: 2020-02-25T22:01:07.000Z
- Updated: 2020-04-03T00:35:00.000Z
- Author: 유 영모
- Tags: #소프트웨어, golang, DDD, xorm, 커머스, 엔터티, 도메인 주도 설계, ValueObject, 개발일지, antdesgin, 오퍼, Offering, 값객체

# 지난 이야기

[지난 글](https://www.popit.kr/commerce-code-asset-1-start/)에서는 개발 일지를 쓰게 된 배경을 설명했고 간단하게 상품 마이크로서비스를 구현해 보았다.

# 상품을 보다 유연하게 다루기

몇 년 전 함께 일했던 기획자가 상품(Product)을 분리하자는 제안을 했다. 상품에는 잘 변하지 않는 요소(바코드, 제조사, 원가 등)와 자주 변하는 요소(판매 조건 등)가 있는데 상품 하나로 다루다 보니 유연성이 떨어진다는 것이었다. 유연성이 떨어진다는 말은 무엇을 의미하는 것일까? '[커머스 혹은 유통 도메인 설계에 대한 연작](https://www.popit.kr/commerce-distribution-domain-design-series/)'에서 같은 고민의 흔적을 엿볼 수 있다.

*사용자가 보통 상품(商品)이라고 부를 때 말이나 화면에 보이는 표기는 상품이지만, 정보는 층을 나눠 저장하자는 제안입니다. 판매자(커머스) 입장에서 Product란 개념은 생산하거나 판매하려고 구입한 실제 물건을 지칭하고, Product를 지칭하거나 참조하지만 판매 관련한 정보는 별도의 개념(Item)으로 구분하자는 것이죠. 몇 가지 이유가 있는데 대략 예를 들면 이렇습니다.*

- *제조사가 다루는 관리번호와 판매자가 부여하는 식별번호는 다르다.*
- *같은 상품이라도 프로모션이나 수수료 계약에 따란 다른 가격을 부여해 판매조건이 달라진다.*
- *같은 상품 여러 개 혹은 다른 상품을 묶어 파는 경우는 흔히 있다.*
- *일차 판매자가 팔고 남은 물건을 다른 판매사가 사다가 파는 형태(아웃렛 혹은 Off-price-retails)가 있다.*
- *파는 단위를 고정하지 않고 구매할 때 단위와 가격이 정해지기도 한다. (슈퍼마켓에서 저울에 달아 사는 과일처럼)*
- [*/commerce-distribution-domain-design-series/*](https://www.popit.kr/commerce-distribution-domain-design-series/)

위에서 언급한 이유는 **왜** **상품을 유연하게 다루어야 하는지를** 잘 설명해준다.

상품을 유연하게 다루기 위해 상품을 판매할 때 변하는 정보와 변하지 않는 정보를 구분하고 변하는 정보를 다음 그림과 같이 별도의 개념(Item)을 만들어서 두 가지 구성요소로 정의하고 있다.

![출처 : /commerce-distribution-domain-design-series/](https://www.popit.kr/content/images/wp/2020/02/image-20200223-055416-1024x362.png)

출처 : /commerce-distribution-domain-design-series/

하지만 Item 개념은 좋으나 그대로 차용해서 쓰기에는 단어가 너무나도 보편적이고 일반적이다. 개인적인 경험으로 이런 단어를 사용했을 때 의사소통에 좋지 않았다. 왜냐하면 모두 자신이 생각하는 Item이 있기 때문이다. 대체할 수 있는 다른 단어는 없을까? [안영회](https://www.popit.kr/author/tony/) 님과 대화 중에서 찾을 수 있었다. 바로 **Offering(간단히 오퍼).** 사전에서는 아래와 같이 정의한다.

> (사람들이 사용하거나 즐기도록) 제공된\[내놓은\] 것 - 네이버 사전

좀 더 찾아보니 마케팅과 커머스 분야에서 정의와 그림을 발견할 수 있었다.

> MARKETING, COMMERCE  
>  
>  
>  
> **a product or service that is offered for sale.**  
>  
> \- [https://dictionary.cambridge.org/ko/%EC%82%AC%EC%A0%84/%EC%98%81%EC%96%B4/offering](https://dictionary.cambridge.org/ko/%EC%82%AC%EC%A0%84/%EC%98%81%EC%96%B4/offering?ref=popit.kr)

![출처 : https://i.pinimg.com/originals/89/9d/7f/899d7fe08717ad589e9855bd21b093d6.jpg](https://www.popit.kr/content/images/wp/2020/02/899d7fe08717ad589e9855bd21b093d6-1.jpg)

출처 : https://i.pinimg.com/originals/89/9d/7f/899d7fe08717ad589e9855bd21b093d6.jpg

너무 흔하게 쓰지 않는(한국에서) 단어이고 뜻이 딱이어서 **Item 대신 Offering을 사용하기로 결정**했다.

# 오퍼(Offering) 개념 설계

상품은 여러 개의 오퍼를 가질 수 있으며 오퍼는 여러 개의 혜택(Customer Benefit, Coupon)을 가진다. 그리고 **고객은 상품이 아닌 오퍼를 구매한다.**

![image-20200223-050459](https://www.popit.kr/content/images/wp/2020/02/image-20200223-050459.png)

상품을 판매하기 위해 오퍼를 만든다. 오퍼를 만드는 행위를 **판촉 활동**이라고 부른다.

# 의도적으로 구현 대상과 범위 줄이기

오퍼의 대상과 범위를 Product로 의도적으로 한정하였다. 그 이유는 개발 주기를 일주일로 정해서(제약 조건) 진행하고 있기 때문이기도 하고 주기 안에서 작고 가치 있는 기능을 배포하는 것이 목표이기 때문이다.

이번 주기에는 상품에 오퍼를 생성할 수 있는 것이 목표이고 오퍼의 ‘Customer Benefit’의 극히 일부만 구현할 생각이다.

# 오퍼 생성 화면

[Ant Design](https://ant.design/?ref=popit.kr)을 사용하여 오퍼 생성 화면을 간단하게 아래와 같이 만들었다.

![image-20200224-011335](https://www.popit.kr/content/images/wp/2020/02/image-20200224-011335.png)

기간을 어떻게 입력받을 지 고민이었지만 [DatePicker 컴포넌트](https://ant.design/components/date-picker/?ref=popit.kr) 중 RangePicker 컴포넌트로 별다른 달력에 대한 의존성 없이 쉽게 구현할 수 있었다.

![image-20200224-011835](https://www.popit.kr/content/images/wp/2020/02/image-20200224-011835.png)

화면에서 ‘Customer Benefit'은 여러 개를 추가할 수 있다. 이렇게 반복되는 요소를 [Form 컴포넌트](https://ant.design/components/form/?ref=popit.kr)로부터 배열로 데이터를 받고 싶었는데 방법을 찾아보니 아래처럼 getFieldDecorator 첫 번째 인자에 배열식으로 선언하면 되었다.  

```js
// ...
import {Button, Form, Input, DatePicker, Divider, Icon, Select, message} from "antd";

class OfferingForm extends React.Component {
  // ...
  render() {
    const { getFieldDecorator } = this.props.form;
    return (
        {/* ... */}
        this.state.customerBenefits.map((customerBenefit, idx) => (
        {/* ... */}
        <Form.Item label="이름">
          {getFieldDecorator(`customerBenefits[${idx}].name`, {
            rules: [
              {
                required: true,
                message: '이름은 필수 입니다.',
              },
            ],
          })(<Input />)}
        </Form.Item>
        <Form.Item label="할인 기준 금액">
          {getFieldDecorator(`customerBenefits[${idx}].ruleset.discountThreshold`, {
            rules: [
              {
                required: true,
                type: 'number', transform: (value) => Number(value),
                message: '할인 기준 금액은 필수 입니다.',
              },
            ],
          })(<Input />)}
        </Form.Item>
        {/* ... */}
      </div>
      {/* ... */}
    )
  }
}

export default OfferingForm = Form.create()(OfferingForm);
```

# 오퍼 API

먼저 객체 모델은 아래와 같다. Offering 엔터티는 CustomerBenefit [값 객체ValueObject](https://www.popit.kr/ddd-value-objects-and-microservices/)를 가지고 Product 엔터티를 참조한다.

![image-20200224-013746](https://www.popit.kr/content/images/wp/2020/02/image-20200224-013746.png)

[도메인 주도 설계Domain-Driven Desigin](http://www.yes24.com/Product/Goods/5312881?scode=032&OzSrank=2&ref=popit.kr)(이하 DDD)에서 엔터티는 매우 단순하게 설명하면 영속성Persistence을 가진 객체이다. 영속성이라고 하면 데이터베이스 같은 비휘발성 저장소에 저장해 관리하는 것을 의미하고 관계형 데이터베이스는 테이블과 연결할 수 있다.

데이터베이스 테이블에 저장할 객체를 대부분 엔터티로 모델링 하는 경우가 많다. 필자도 처음에는 CustomerBenefit을 엔터티로 설계했었다. [도메인 주도 설계 구현Implementing Domain-Driven Design](http://www.yes24.com/Product/Goods/25100510?scode=032&OzSrank=1&ref=popit.kr)에서는 엔터티의 남용으로 복잡해지는 도메인 모델을 언급하며 값 객체 사용을 노력해야 한다고 말한다.

> 가능한 위치에선 엔터티 대신 값 객체를 사용해 모델링하도록 노력해야 한다는 사실을 알게 되면 놀랄지도 모르겠다. 심지어 도메인 개념이 엔터티로 모델링돼야 할 때에도 엔터티의 설계는 자식 엔터티의 컨테이너보다는 값의 컨테이너로 동작하는 쪽으로 기울어야 한다. - 도메인 주도 설계 구현, 299쪽

쓰임새 관점에서 생각해보자. 오퍼는 따로 떼어서 사용하는가? 그렇다. 고객은 특정 오퍼를 구매한다. 그렇다면 CustomerBenefit은 따로 떼어서 사용하는가? 아니다. 현재로서는 **오퍼를 구성하는 값Value**에 불과하다. 따로 떼어서 사용하기 위해서는 식별성을 지녀야 한다. 그래서 엔터티의 중요한 특징 중 하나는 식별성이다.

> *ENTITY의 식별성을 관리하는 일은 매우 중요하지만 그 밖의 객체에 식별성을 추가하면서 시스템의 성능이 저하되고, 분석 작업이 별도로 필요하며, 모든 객체를 동일한 것으로 보이게 해서 모델이 혼란스러워질 수 있다. 소프트웨어 설계는 복잡성과의 끊임없는 전투다. 그러므로 우리는 특별하게 다뤄야 할 부분과 그렇지 않은 부분을 구분해야 한다. 하지만 이러한 범주에 속하는 객체를 단순히 식별성이 없는 것으로만 생각한다면 우리의 도구상자나 어휘에 추가할 게 그리 많지 않을 것이다. 사실 이 같은 객체는 자체적인 특징을 비롯해 모델에 중요한 의미를 지닌다. 이것들이 사물을 서술하는 객체다. *개념적 식별성을 갖지 않으면서 도메인의 서술적 측면을 나타내는 객체를 VALUE OBJECT라 한다. -** 도메인 주도 설계,100 쪽

이제부터 동작하는 코드로 풀어보자.

Offering 엔터티에 합성Composition으로 OfferingCustomerBenefit 값 객체 포함시켰다.  

```go
type Offering struct {
	Id int64 `xorm:"id pk autoincr"`
	Name  string `xorm:"name notnull"`
	ProductId int64 `xorm:"product_id notnull"`
	OfferingPrice  float64 `xorm:"offering_price notnull"`
	ImageUrl  string `xorm:"image_url notnull"`
	StartDate time.Time `xorm:"start_date notnull"`
	EndDate time.Time `xorm:"end_date notnull"`
	OfferingCustomerBenefits []OfferingCustomerBenefit `xorm:"customer_benefits notnull"`
	CreatedAt time.Time `xorm:"created_at notnull"`
	UpdatedAt time.Time `xorm:"updated_at notnull"`
}

func (Offering) TableName() string {
	return "offerings"
}

type OfferingCustomerBenefit struct {
	Type string `json:"type"`
	Name  string `json:"name"`
	Ruleset map[string]interface{} `json:"ruleset"`
	CreatedAt time.Time `json:"createAt"`
	UpdatedAt time.Time `json:"updateAt"`
}
```

Offering을 저장하는 OfferingRepository는 아래와 같다.  

```go
var (
	o *offeringRepository
)

func OfferingRepository() *offeringRepository {
	if o == nil {
		o = &offeringRepository{
		}
	}
	return o
}

type offeringRepository struct {
}

func (offeringRepository) Create(ctx context.Context, offering entitiy.Offering) error {
	if _, err := common.GetDB(ctx).Insert(&offering); err != nil {
		return err
	}

	return nil
}
```

[xORM](https://github.com/go-xorm/xorm?ref=popit.kr)은 기본적으로 Golang의 Struct를 관계형 데이터베이스에 JSON 형태로 저장해 준다.

![image-20200224-020412](https://www.popit.kr/content/images/wp/2020/02/image-20200224-020412-1024x323.png)

결과적으로 OfferingCustomerBenefit을 값 객체로 모델링함으로써 Offering 엔터티를 조회할 때 OfferingCustomerBenefit을 JOIN 할 필요가 없어지기 때문에 성능 측면에서 더 나아졌으며 모델을 다루는 것이 더 단순해졌다. 물론 단점도 있는데 관계형 데이터베이스에 친화적이지 않은 집합 데이터(OfferingCustomerBenefit)를 JSON으로 넣다 보니 SQL을 통해 customer\_benefits 칼럼 내용으로 검색하기 쉽지 않고 또한 JSON 스키마 관리를 애플리케이션에서 잘 하지 않으면 JSON 문자열을 객체로 만드는 과정에서 오류가 날 수 있다.

현재까지는 Repository 코드가 매우 단순했다. 테이블 하나에 대한 연산을 했기 때문이다. 하지만 연관 관계를 가지는 테이블을 함께 연산해야 하는 경우가 많은데(차후에 DDD [애그리게잇](https://www.popit.kr/one-repository-per-aggregate/)Aggreate으로 다룰 예정) xORM은 단순히 객체-테이블 매퍼Mapper 역할만 하기 때문에 이를 지원하지 않는다. 따라서 직접 구현해 주어야 한다.  
마지막으로 Controller 와 Service 코드는 아래와 같다.  

```go
type OfferingController struct {
}

func (oc OfferingController) Init(g *echo.Group) {
	g.POST("", oc.CreateOffering)
}

func (OfferingController) CreateOffering(ctx echo.Context) error {
	var offeringCreation dto.OfferingCreation

	if err := ctx.Bind(&offeringCreation); err != nil {
		return ctx.JSON(http.StatusBadRequest, dto.ApiError{
			Message: err.Error(),
		})
	}

	if err := offeringCreation.Validate(); err != nil {
		return ctx.JSON(http.StatusBadRequest, dto.ApiError{
			Message: err.Error(),
		})
	}

	if err := service.OfferingService().CreateOffering(ctx.Request().Context(), offeringCreation); err != nil {
		log.Errorf("Create Offering Error:  %s", err.Error())
		return ctx.JSON(http.StatusInternalServerError, dto.ApiError{
			Message: err.Error(),
		})
	}

	return ctx.JSON(http.StatusCreated, nil)
}

var (
	o *offeringService
)

func OfferingService() *offeringService {
	if o == nil {
		o = &offeringService{
		}
	}
	return o
}

type offeringService struct {
}

func (offeringService) CreateOffering(ctx context.Context, offeringCreation dto.OfferingCreation) error {
	offering := entitiy.NewOffering(offeringCreation)
	return repository.OfferingRepository().Create(ctx, offering)
}
```

---

- [커머스 코드 자산화 개발일지 - 1 시작](https://www.popit.kr/commerce-code-asset-1-start/)
- **커머스 코드 자산화 개발일지 - 2 상품을 팔지 않고 오퍼를 판다**
- [커머스 코드 자산화 개발일지-3 오퍼를 쇼핑몰에](https://www.popit.kr/commerce-code-asset-3-offers-in-mall/)
- [커머스 코드 자산화 개발일지 - 4 출시](https://www.popit.kr/commerce-code-asset-4-launch/)
- [커머스 코드 자산화 개발일지 - 5 장바구니(부제. 다중 저장소 지속성 입문)](https://www.popit.kr/commerce-code-asset-5-shopping-cart/)
- [커머스 코드 자산화 개발일지 - 6 결제 대행 서비스를 테스트 대역으로](https://www.popit.kr/commerce-code-asset-6-payment-gateway-test-double/)

---