Skip to content

7. 쿠폰을 발급하는 행위에 재미를 넣어주고 싶어져서 탄생한 기능

이건창 edited this page May 11, 2024 · 6 revisions

결과물

개요

최근 나는 쿠폰을 돈을 사용하기 위한 마케팅 수단이라는 인식을 가지고 있다. 그래서 쿠폰 발급을 꺼리는 경우가 종종 있다.

쿠폰 발급의 무게감을 조금 덜어낸다면 사용자들의 접근성이 개선된다고 생각했고 쿠폰 발급 행위에 재미를 넣어봤다. 그래서 쿠폰을 발급하면 10% ~ 30% 중에서 정해는 쿠폰 발급 방식을 생각했다. 가설 검증 방법은 사용자가 쿠폰을 발급하는 행위와 이벤트로 쿠폰을 발급하는 행위 간 사용 횟수를 비교한다.

요구사항을 정리하면 다음과 같다.

  • 10 ~ 30 사이 정수를 선택해 정률 쿠폰 발급한다.
  • 사용자 쿠폰 발급하는 행위와 이벤트로 쿠폰 발급하는 행위 간 사용 횟수를 비교한다. 성공 횟수만 포함한다.
  • 사용 횟수 측정 기간은 이벤트 기간으로 한정한다.
  • 진행중인 이벤트만 참여할 수 있다.

검증 목록은 다음과 같다.

  • 사용자는 동일한 이벤트에 중복 참여할 수 없다.
  • 사용자는 진행중인 이벤트만 참여할 수 있다.
  • 10 ~ 30 사이 발급되는 확률은 다음과 같다.
할인 금액 기준 확률
10% ~ 15% 20%
16% ~ 20% 30%
21% ~ 25% 30%
26% ~ 30% 20%

모델링

회원이 쿠폰을 발급하는 시나리오는 다음과 같다.

  1. 가게 주인은 가게에 대한 이벤트를 발행한다.
  2. 회원은 가게에 대한 이벤트를 조회한다.
  3. 회원은 이벤트를 선택해 쿠폰을 발급한다.
  4. 회원은 이벤트 쿠폰을 사용한다.

쿠폰과 이벤트는 다른 트랜잭션 단위를 가지기 때문에 이벤트를 관리하지 않기로 했다. 관리해야 할 기능은 이벤트 쿠폰 발급 기능이벤트 쿠폰 사용 기능이다.

  • (X) 이벤트 발생 기능
  • (X) 이벤트 조회 기능
  • (O) 이벤트 쿠폰 발급 기능
  • (O) 이벤트 쿠폰 사용 기능

도메인 모델링에서 변경점은 다음과 같다.

---
title: 이달의 민족
---
classDiagram
    Shop *-- PublishedEventCouponBook
    ShopOwner *-- Shop
    
    class PublishedEventCouponBook{
        -CouponBook publishedEventCoupons
        +issueEventCoupon(Coupon coupon) void
        +showEventCoupons() List~Coupon~
    }

    class ShopOwner{
        +issueEventCouponInShop(Coupon coupon) void
        +showEventCouponBookInShop() List~Coupon~
    }

    class Shop{
        -CouponBook publishedEventCouponBook

        +issueEventCoupon(Coupon coupon) void
        +showEventCoupons() List~Coupon~
    }

Loading

데이터 모델링에서는 event_coupon_book 테이블만 생성된다고 보면 된다.

---
title: 이달의 민족
---

erDiagram

    event_coupon_book {
        long shop_id
        long coupon_id
        long event_id
    }
    shop {
        long id pk
	string name
	long boss_id
    }

    coupon {
	long id pk
	long shop_id
	string name
	string description
	string type
	int amount
    }

    event_coupon_book ||--o{ shop : have
    event_coupon_book ||--o{ coupon : contains
Loading

이벤트 참여로 쿠폰 발급받는 진행 흐름은 다음과 같다.

sequenceDiagram

actor 회원
회원 ->> IssueRandomCouponService : 이벤트에 참여한다.

IssueRandomCouponService ->> Member : 회원 식별자를 이용해 회원을 조회한다.
Member ->> IssueRandomCouponService : 회원 정보

IssueRandomCouponService ->> Event : 이벤트 식별자를 이용해 이벤트를 조회한다.
Event ->> IssueRandomCouponService : 이벤트 정보

rect rgb(200, 150, 255)
note right of IssueRandomCouponService: 이벤트에 참여
IssueRandomCouponService ->> RandomCouponIssueEvent: 이벤트에 참여한다.
RandomCouponIssueEvent ->> RandomCouponIssueEvent : 진행중인 이벤트인지 확인한다.
RandomCouponIssueEvent ->> RandomCouponIssueEvent : 이벤트에 참여했는지 확인한다.
RandomCouponIssueEvent ->> RandomCouponIssueEvent : 회원을 이벤트 참가자 명단에 추가한다.
RandomCouponIssueEvent ->> RandomCouponIssueEvent : 이벤트에 맞는 쿠폰 형식을 생성한다.
RandomCouponIssueEvent ->> IssueRandomCouponService : 이벤트 쿠폰 반환
end

IssueRandomCouponService ->> ShopOwner: 가게 주인은 생성된 이벤트 쿠폰을 이벤트 쿠폰북에 담는다.
ShopOwner ->> IssueRandomCouponService : 이벤트 쿠폰

IssueRandomCouponService ->> Member : 회원은 가게 주인이 가진 이벤트 쿠폰북에서 쿠폰을 꺼내 자신의 쿠폰북에 담는다.
Member ->> IssueRandomCouponService: 

rect rgb(191, 223, 255)
note right of IssueRandomCouponService: 상태 갱신
  IssueRandomCouponService ->> Member : 이벤트 쿠폰을 발급한 회원 상태를 갱신한다.
  IssueRandomCouponService ->> ShopOwner: 이벤트 쿠폰이 생성한 가게 주인 상태를 갱신한다.
  IssueRandomCouponService ->> Event : 이벤트 참여자가 생긴 이벤트 상태를 갱신한다.
end
IssueRandomCouponService ->> 회원 : 

Loading

구현

균등한 확률 정책 그리고 가설 검증

요구사항에 맞게 균등한 확률을 어떻게 유지할 수 있을지 고민했는데, 어떤 간격을 기준으로 확률을 유지하는지 고민해볼 필요가 있다. 일정 간격마다 확률을 모니터링해야 하는데 간격에 맞게 확률을 유지할 수 있거나 난수로 조절 할 수 있다.

  • 전체 간격을 모니터링해 일정 확률 유지하도록 조절
  • 균등한 확률을 가진 난수를 이용한 조절

서버를 stateless 상태로 유지하려면 전자는 외부 인프라까지 사용해야 하는데 부담이라 생각한다. 후자로도 일정한 결과를 보장할 수 있지 않을까 생각이 들었다. 그래서 100,000 건을 추출했을 때 원하는 확률이 나오면 진행하기로 결정했다. 균등한 난수 생성기를 사용하고 일정 범위로 확률을 제어하기로 했다. 0 ~ 1 사이 포함되는 영역만큼 확률이 된다. 간격에 맞게 확률만 제어하면 되니 세부적인 값 설정은 고려하지 않았다.

kotest의 속성 테스트를 활용해서 분포도도 확인하며 동작을 확인했다.

Statistics: [할인 금액을 선정한다.] (1000 iterations, 1 args) [statistic]

21 ~ 25                                                       314 (31%)
16 ~ 20                                                       288 (29%)
26 ~ 30                                                       216 (22%)
10 ~ 15                                                       182 (18%)

마지막으로

객체지향의 오해

누가 행위의 책임을 맡을지 고민하다 보니 계속 능동적인 객체에게 책임을 전가하는 오류를 범했다. 객체 지향을 유지하기 위해 복잡해질 필요가 있을까하는 부정적인 생각이 들 정도였다. 테스트 코드 작성이 너무 어려워져서 잠시 코딩을 멈추고 생각하는 시간을 가졌다. 덕분에 사물도 행위를 할 수 있음을 깨달았다.

객체들은 모두 능동적으로 행위를 할 수 있다. 객체의 범주는 능동적인 동물이 아니라 사물이 될 수 있고 행동이 될 수 있음을 유의하자. 만약 로직이 복잡해진다면 능동적인 객체만 일을 할당하는게 아닌가 의심하자.

생각 흐름대로 구현하지 말자.

처음 이벤트 쿠폰을 발급하고 전달하기 위해 CouponTypeEvent 유형을 추가해야 하나 했다. CouponType은 발급 방식인 PUBLISHED, HANDOUT인지 설명하고, 할인 방식에 따라 RateDiscountCoupon, FixDiscountCoupon 클래스로 분리하고 있었다. 쿠폰을 사용할 때 어떤 유형인지 확인하기 위해선 추가하는게 맞았지만 혼동을 야기한 원인을 고민해보기로 했다.

원인은 CouponType 이라는 범용적인 단어가 혼동을 준게 아닌가 싶었다. 우리는 단순히 유형, 타입만 나오면 Type 만 붙이기 급급하지 않나 고민해야 한다. 범용적인 스킬은 레거시 제조기일 수 있겠다는 생각이 든다. 실무에서도 편한 방법으로 구현하는 일로 이후에 수정이 어려워진 적이 있어서 상당히 경계해야 할 부분이다.