Skip to content

6‐5. 전화번호를 입력해 쿠폰을 나눠준다.

이건창 edited this page May 25, 2024 · 1 revision

결과물

개요

이커머스 세계에서 우린 친구들의 아이디를 알고 있는가? 라는 고민에서 이커머스에서 우리는 독립된 구매자로 인식받고 있어서 지인이 어떤 행동을 하는지 식별할 수가 없다. 그럼 독립된 구매자 간에 쿠폰을 어떻게 나눠줄 수 있을지 고민했고, 회원 정보를 몰라도 전화 번호를 이용해 쿠폰을 나눠 줄 수 있겠다 생각했다.

요즘 sns 피싱 문제가 대두되고 있는데 문제를 해결하려면 서비스에서 sns 를 전송하는 것보다 회원이 직접 전송하도록 구현했다. 메시지 템플릿을 반환받고 sender가 메시지 템플릿을 받자마자 바로 공유 할 수 있는 창이 떠지면 좋겠다는 생각도 들었다. 나중에 화면을 만들게 되면 추가해보도록 하겠다.

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

  • 회원은 쿠폰을 전달하기 위한 메시지 템플릿을 얻는다.
    • 전달하는 쿠폰은 유효해야 한다.
    • 전달하게 되면 더이상 회원이 가진 쿠폰이 아니다.
    • 전달한 쿠폰의 등록 기간은 6개월이다. 6개월 이내 쿠폰을 등록하지 않으면 소멸한다.
    • 전달 가능한 쿠폰은 사용자가 가지고 있어야 하고 게시한 쿠폰이 아니어야 한다.

게시한 쿠폰은 가게에서 직접 가져 갈 수 있으니 전달 가능한 쿠폰에서 제외했다. 이벤트 쿠폰은 이벤트에 한 번만 참여 가능하지만 발급된 이벤트 쿠폰 사용에는 제한이 없으니 큰 문제는 생기지 않는다.

모델링은 다음과 같다.

---
title: Gift Coupon Message
---

classDiagram

    GiftMessage *-- GiftCoupon
    GiftMessage *-- GiftCode
    
    class GiftMessage{
        +String message
        +Long senderId
        +LocalDate sendDate
        +GiftCoupon gift
        +GiftCode code
    }

    class GiftCoupon{
        +Long coupontId
        +Boolean isUsed
        +LocalDate validDate
    }

    class GiftCode {
       +String enrollCode
    }

Loading

사용자 플로우를 정리하면 다음과 같다.

sequenceDiagram
 actor shop owner

box send to coupon by phone number
 actor sender
 participant coupon service
 actor receiver
end

shop owner ->> sender : event coupon or hand out coupon


rect rgb(191, 123, 255)
note right of sender: send coupon
sender ->> coupon service : find giftable coupons
coupon service ->> sender : 
sender ->> coupon service: send coupon
coupon service ->> coupon service: find coupon
coupon service ->> coupon service: make message template
coupon service ->> sender: return message template
sender ->> receiver : send message
end

rect rgb(201, 23, 255)
note left of receiver: enroll coupon
opt 가입하지 않은 경우
receiver ->> coupon service: join service
coupon service ->> receiver: 
end
receiver ->> coupon service: enroll coupon
coupon service ->> coupon service: validate coupon
coupon service ->> receiver: 
end

receiver ->> shop owner : use coupon
Loading

사용자 플로우를 봤을 때 생성이 필요한 API는 두 개로 선물가능한 쿠폰 조회 API, 쿠폰 전달 API쿠폰 등록 API가 필요하다.

선물가능한 쿠폰 조회 API는 다음과 같다.

sequenceDiagram
 actor Sender
 participant GiftCouponMessageController
 participant FindAvailableGiftCouponsUseCase

Sender ->> GiftCouponMessageController: findGiftableCouopns(coupon id)
GiftCouponMessageController ->> FindAvailableGiftCouponsUseCase: findGiftableCoupon(member id, coupon id)
FindAvailableGiftCouponsUseCase ->> MemberAdapter: findMember
MemberAdapter ->> FindAvailableGiftCouponsUseCase: 
FindAvailableGiftCouponsUseCase ->> CouponAdapter: findCoupon
CouponAdapter ->> FindAvailableGiftCouponsUseCase: 
FindAvailableGiftCouponsUseCase ->> ValidateGiftableAdapter: isGiftable
ValidateGiftableAdapter ->> FindAvailableGiftCouponsUseCase: 

FindAvailableGiftCouponsUseCase ->> GiftCouponMessageController: return GiftableCouopns
GiftCouponMessageController ->> Sender: return List<Coupon>

Loading

쿠폰 전달 API는 다음과 같다.

sequenceDiagram
 actor Sender
 participant GiftCouponMessageController
 participant GiftCouponByMessageUseCase

Sender ->> GiftCouponMessageController: sendGiftableCouopns(coupon id)
GiftCouponMessageController ->> GiftCouponByMessageUseCase: sendGiftableCouopns(member id, coupon id)
GiftCouponByMessageUseCase ->> MemberAdapter: findMember
MemberAdapter ->> GiftCouponByMessageUseCase: 
GiftCouponByMessageUseCase ->> CouponAdapter: findCoupon
CouponAdapter ->> GiftCouponByMessageUseCase: 
GiftCouponByMessageUseCase ->> ValidateGiftableAdapter: isGiftable
ValidateGiftableAdapter ->> GiftCouponByMessageUseCase: 

GiftCouponByMessageUseCase ->> WrapGiftAdapter: wrap gift
WrapGiftAdapter ->> GiftCouponByMessageUseCase: 

GiftCouponByMessageUseCase ->> GiftCouponMessageController: return gift
GiftCouponMessageController ->> Sender: return gift message

Loading

쿠폰 등록 API는 다음과 같다.

sequenceDiagram
 actor Sender
 participant GiftCouponMessageController
 participant EnrollCouponByMessageUseCase

Sender ->> GiftCouponMessageController: enrollGiftCoupon(coupon code)
GiftCouponMessageController ->> EnrollCouponByMessageUseCase: enroll(member id, coupon code)
EnrollCouponByMessageUseCase ->> MemberAdapter: findMember
MemberAdapter ->> EnrollCouponByMessageUseCase: 
EnrollCouponByMessageUseCase ->> LoadGiftCouponStatePort: findGiftCoupon
LoadGiftCouponStatePort ->> EnrollCouponByMessageUseCase: 
EnrollCouponByMessageUseCase ->> EnrollCouponByMessageUseCase: member receive coupon
EnrollCouponByMessageUseCase ->> UpdateMemberStatePort: update
UpdateMemberStatePort ->> EnrollCouponByMessageUseCase: 

EnrollCouponByMessageUseCase ->> GiftCouponMessageController: 
GiftCouponMessageController ->> Sender: 

Loading

구현

책임 분리로 레이어가 분리되는 상황

책임 분리로 인해 레이어 사이 너무 많은 계층이 생기는 건 관리가 어려워진다는 생각을 했다. 현실적으로 SRP를 지키는건 무조건적으로 좋을까 고민했을 때 계층 사이 의존되는 클래스가 많아지면 사람의 인지 범위를 넘어서는 건 동일한게 아닐까 생각 들었다.

결론은 새로운 계층이 생길 때까지 상위 의존 영역에서 관리하기로 마음먹고 협의 후 쉽게 수정할 수 있도록 신경썼다. 함수를 파리머터에 관리하며 하나의 책임을 맡고 클래스가 맡아야 할 책임을 본문에 선언하며 관리했다.

class EnrollCouponByMessageService(
    ...
    // 쿠폰 코드를 사용하는 책임
    private val useGiftCouponCode: (GiftCouponCode) -> GiftCoupon = {
        useGiftCouponCodeStatePort.useBy(it)
        loadGiftCouponStatePort.findGiftCoupon(it)
    },
    private val updateMembersCoupon: (Member) -> Unit = { updateMemberStatePort.updateMembersCoupon(it) },
) : EnrollCouponByMessageUseCase {
    // 쿠폰을 등록하는 책임
    override fun enroll(memberId: Long, code: GiftCouponCode) {
        transactionArea.run {
            val member = findMember(memberId)
            val giftCoupon = useGiftCouponCode(code)
            member.receiveCoupon(giftCoupon.coupon)
            updateMembersCoupon(member)
        }
    }
}

레이어 사이 생성되는 의존 클래스는 어디에 둘지 어떻게 관리할지 항상 고민이다. 이런 규칙이 정해지면 쉽게 관리하면 되지만 생성이 필요할 때마다 팀을 불러세울 수 없다. 이런 상황에서는 해당 방법으로 관리하면서 추출 여부를 결정하는게 좋아보인다.

쿠폰 등록 과정을 간단하게 만들 수 있을까?

쿠폰 등록 과정을 찾아봤을 때 수동 입력 방식, URL 등록 방식 두 분류로 나뉘었다. 사용자는 적은 액션일 수록 쉽게 접근하기 때문에 URL 방식을 고려했다. URL 등록 방식을 고려하면 다음과 같은 문제점이 발생한다.

  • 등록 API가 GET 방식이어야 한다.
  • 변경되는 URL에 대비해야 한다.

카카오톡은 tiny url로 한 계층 더 추상화해 API와의 의존성을 떨어뜨렸다.

스크린샷 2024-05-23 오후 11 51 57

덕분에 GET 방식이 아니어도 되고, 변경되는 URL에 대비할 수 있다. 하지만 필자는 단축 URL을 사용하지 않고 진행해보려 한다. 단축 URL까지 사용할만큼 확장성있게 운영할 생각이 없기 때문이다.

마지막으로

고려한 아키텍처에 맞지 않은 레이어가 생성되려고 할 때마다 머리를 뜯어왔다. 도메인 처럼 구분해야 할까? 사용하는 레이어에만 사용하니 해당 레이어에서 디렉토리를 생성해야 하나? 등 선택에 혼란이 왔다. 이런 경험은 팀에게 명확한 기준을 제시할 필요성을 경험했다.

기준을 제시할 때 어렵지 않고 불편하지 않는 선에서 정하는게 좋지 않을까 생각든다.