블로그로 돌아가기
Product

만남 시스템 설계 — 예약금으로 노쇼를 줄이는 행동 경제학

오프라인 만남의 노쇼 문제를 해결하기 위해 썸블링이 설계한 카드 예약금 시스템, 8가지 만남 상태 머신, 그리고 만남 후기가 매칭 품질을 높이는 선순환 구조까지

Team Somebling ·
#만남 시스템 #예약금 #행동 경제학 #상태 머신 #UX

“만나기로 했는데 안 나왔어요”

채팅은 잘 됐습니다. 소울 점수도 높았고, 대화도 자연스럽게 이어졌어요. 약속 날짜를 정하고, 장소도 골랐습니다. 그런데 당일, 상대방은 나타나지 않았습니다.

데이팅 앱을 사용하는 사람이라면 한 번쯤 겪어봤을 상황입니다. 혹은 반대로 본인이 그 자리에 나타나지 못한 쪽이었을 수도 있습니다. 노쇼(No-Show)는 데이팅 앱 생태계에서 가장 오래된 문제 중 하나입니다.

업계 리서치에 따르면 데이팅 앱을 통해 약속을 잡은 경우 약 30% 이상이 노쇼로 끝난다고 알려져 있습니다. 온라인 대화와 오프라인 만남 사이의 심리적 장벽, 익명성이 주는 책임감 희석, 그리고 “더 좋은 상대가 나타나면 어떡하지”라는 옵션 마인드셋이 복합적으로 작용한 결과입니다.

노쇼가 남기는 피해는 단순히 시간 낭비에 그치지 않습니다. 기다리다 혼자 돌아온 사람은 당혹감과 자존감 하락을 동시에 경험합니다. “내가 별로였던 걸까”라는 생각은 이후 매칭 활동 자체를 회피하게 만들 수 있습니다. 앱 입장에서도 노쇼가 반복되면 사용자가 이탈하고, 커뮤니티 신뢰도가 무너집니다.

썸블링을 설계하면서 우리는 이 문제를 정면으로 다뤄야 한다고 판단했습니다. 단순히 “매너를 지켜주세요”라는 문구를 넣는 것으로는 아무것도 바뀌지 않습니다. 사람의 행동을 실제로 바꾸려면, 행동 경제학이 말하는 구조적 인센티브가 필요합니다.


행동 경제학에서 답을 찾다

심리학자 다니엘 카너먼(Daniel Kahneman)과 아모스 트버스키(Amos Tversky)가 정립한 손실 회피(Loss Aversion) 이론은 이렇게 말합니다. 사람은 같은 크기의 이득을 얻는 것보다 손실을 피하는 것을 약 2배 더 강하게 원한다고. 즉, 10,000원을 버는 기쁨보다 10,000원을 잃는 고통이 훨씬 크게 느껴진다는 뜻입니다.

이 원리를 만남 시스템에 적용하면 단순합니다. 약속을 지키지 않았을 때 무언가를 잃는 구조를 만들면, 사람들은 훨씬 더 강하게 약속을 지키려 합니다.

이와 관련된 또 다른 개념이 Commitment Device입니다. 사전에 비용이나 노력을 투입함으로써 자신의 미래 행동을 특정 방향으로 고정하는 장치입니다. 체중 감량을 위해 개인 트레이너에게 선불로 돈을 내는 것, 금연을 위해 지인들에게 공개 선언을 하는 것이 대표적인 예입니다.

실제 연구들도 이를 뒷받침합니다. 의료 분야에서 예약 노쇼를 줄이기 위해 소액 예약금을 도입했을 때 노쇼율이 60~80% 감소한 사례들이 보고되어 있습니다. 레스토랑 업계에서도 예약금 제도를 도입한 식당이 그렇지 않은 식당에 비해 노쇼율이 현저히 낮다는 것이 확인되어 있습니다.

썸블링의 선택은 명확했습니다. 현금 결제가 아닌 앱 내 재화인 카드(Card) 를 예약금으로 활용하는 것. 카드는 서비스 내에서 가치를 갖는 재화이지만, 실제 현금 결제에서 발생하는 마찰과 심리적 부담은 훨씬 낮습니다. 그러면서도 “잃고 싶지 않다”는 심리는 충분히 작동합니다.


만남 신청 플로우

만남 신청은 채팅방 안에서 시작됩니다. 채팅으로 어느 정도 교류가 이루어진 뒤, 오프라인 만남을 원하는 쪽이 신청서를 작성합니다. 장소명, 장소 URL(지도 링크), 만남 일시, 그리고 짧은 메시지를 담아 상대방에게 전달합니다.

flowchart TD
    A[채팅방에서 만남 신청] --> B{상대방 응답}
    B -->|수락| C[쌍방 카드 5장 예약금 차감]
    B -->|거절| D[신청자 카드 5장 반환]
    B -->|24시간 미응답| E[자동 만료, 신청자 카드 반환]
    C --> F[만남 진행]
    F --> G{만남 결과}
    G -->|완료 확인| H[쌍방 카드 반환 + 후기 작성 유도]
    G -->|신청자 취소| I[신청자 카드 몰수, 수락자 카드 반환]
    G -->|수락자 취소| J[수락자 카드 몰수, 신청자 카드 반환]
    G -->|노쇼 신고| K[노쇼 측 카드 몰수, 상대방 카드 반환]

핵심은 수락 시점입니다. 상대방이 수락을 누르는 순간, 양측 모두에게서 카드 5장이 예약금으로 차감됩니다. 이 시점부터 두 사람은 모두 약속에 대한 실질적인 스킨인더게임(Skin in the Game)을 갖게 됩니다.

신청자가 일방적으로 취소하면? 신청자의 카드가 몰수되고 수락자에게는 반환됩니다. 반대로 수락자가 취소하면? 수락자의 카드가 몰수됩니다. 노쇼의 경우에도 동일한 원리가 적용됩니다. 약속을 지키지 않은 쪽이 비용을 부담하는 구조입니다.

만남이 정상적으로 완료되면, 양측 모두 카드를 돌려받습니다. 예약금은 만남을 완료하는 인센티브이기도 합니다.


8가지 만남 상태 머신

만남 신청부터 완료까지의 라이프사이클은 8가지 상태로 관리됩니다. 각 상태 전이는 명확한 조건과 결과를 가집니다.

stateDiagram-v2
    [*] --> PENDING: 만남 신청
    PENDING --> ACCEPTED: 상대방 수락
    PENDING --> REJECTED: 상대방 거절
    PENDING --> EXPIRED: 24시간 경과
    ACCEPTED --> COMPLETED: 만남 완료 확인
    ACCEPTED --> CANCELLED_BY_REQUESTER: 신청자 취소
    ACCEPTED --> CANCELLED_BY_ACCEPTOR: 수락자 취소
    ACCEPTED --> NO_SHOW: 노쇼 신고
    COMPLETED --> [*]
    REJECTED --> [*]
    EXPIRED --> [*]
    CANCELLED_BY_REQUESTER --> [*]
    CANCELLED_BY_ACCEPTOR --> [*]
    NO_SHOW --> [*]

각 상태별 카드 처리 로직은 다음과 같습니다.

상태신청자 카드수락자 카드설명
PENDING-5장 차감 대기-신청 직후, 수락 전까지는 신청자만 차감 검토
ACCEPTED-5장 차감-5장 차감수락 시점에 쌍방 차감
REJECTED반환-거절 시 신청자에게만 해당 없음 (차감 안 됐으므로)
EXPIRED반환-24시간 무응답, 신청자 카드 반환
COMPLETED+5장 반환+5장 반환만남 완료, 쌍방 반환
CANCELLED_BY_REQUESTER몰수+5장 반환신청자 귀책, 신청자 카드 몰수
CANCELLED_BY_ACCEPTOR+5장 반환몰수수락자 귀책, 수락자 카드 몰수
NO_SHOW노쇼 측 몰수노쇼 측 몰수노쇼 신고에 따라 귀책 측 카드 몰수

REJECTEDEXPIRED 상태에서는 실제로 카드가 먼저 차감되지 않습니다. 정확히는 ACCEPTED 전환 시점에 양측 모두의 카드를 원자적(atomic)으로 차감하는 방식입니다. 이를 통해 “거절당했는데 카드가 빠져나갔다”는 상황을 원천 차단합니다.


카드 경제: 왜 현금이 아닌 카드인가

만남 예약금을 설계할 때 가장 먼저 떠오르는 옵션은 현금 결제입니다. 하지만 우리는 여러 이유로 앱 내 재화인 카드를 선택했습니다.

마찰 최소화. 만남을 신청할 때마다 카드 번호를 입력하거나 PG 결제창을 띄우는 것은 심리적 장벽이 너무 큽니다. 카드는 이미 지갑 안에 있는 재화처럼 느껴지기 때문에, 예약금 차감이 유연하게 이루어집니다.

PG 수수료 회피. 소액 현금 결제에는 PG사 수수료가 붙습니다. 카드를 사용하면 이 비용이 발생하지 않습니다.

앱 내 순환 경제 형성. 카드는 다양한 경로로 획득할 수 있습니다. 가입 보너스, 소울 테스트 완료 보너스, 초대 코드 사용, 상점 구매까지. 카드가 앱 안에서 의미 있는 재화로 자리잡을수록, 카드를 잃는 것에 대한 아쉬움도 커집니다. 이는 예약금 시스템의 효과를 높이는 방향으로 작용합니다.

가치의 적절한 설정. 카드 5장이라는 예약금 규모는 “부담스럽지 않지만 아깝다”는 감각을 목표로 설정했습니다. 너무 적으면 손실 회피 효과가 작고, 너무 많으면 만남 자체를 망설이게 됩니다.

카드 순환 구조

[획득]                      [사용]
가입 보너스      ──→        만남 예약금 (5장)
소울 테스트 완료  ──→        추가 매칭 요청
초대 코드 사용   ──→        슈퍼라이크
상점 구매       ──→        Soul 리포트
                           프로필 언블러
flowchart TD
    subgraph Acquire["카드 획득"]
        A1["가입 보너스"]
        A2["소울 테스트 완료"]
        A3["초대 코드 사용"]
        A4["상점 구매"]
    end
    subgraph Use["카드 사용"]
        U1["만남 예약금 5장"]
        U2["추가 매칭 요청"]
        U3["슈퍼라이크"]
        U4["Soul 리포트"]
        U5["프로필 언블러"]
    end
    subgraph Return["카드 반환"]
        R1["만남 정상 완료<br/>→ 5장 반환"]
    end

    A1 & A2 & A3 & A4 --> U1 & U2 & U3 & U4 & U5
    U1 -->|"만남 완료"| R1
    R1 -->|"재순환"| U1

    style U1 fill:#FF6B9D,stroke:#FF6B9D,color:#fff
    style R1 fill:#7F5AF0,stroke:#7F5AF0,color:#fff

카드가 순환하면서, 활동적인 사용자는 자연스럽게 카드를 확보하고, 카드는 다시 만남으로 이어집니다.


만남 후기 시스템

만남이 완료되면 곧바로 후기 작성 화면이 열립니다. 후기는 매칭 카드에 노출되어 다음 사람의 매칭 결정에 영향을 미칩니다.

후기는 세 가지 요소로 구성됩니다.

별점 (1~5점). 전반적인 만남의 만족도를 숫자로 표현합니다. 단순하고 직관적입니다.

태그 선택. 8가지 태그 중 해당하는 것을 고릅니다.

태그의미
PUNCTUAL시간 약속을 잘 지켜요
KIND친절하고 배려가 넘쳐요
FUN_CONVERSATION대화가 너무 재밌어요
LOOKS_LIKE_PHOTO프로필 사진과 비슷해요
GOOD_MANNERS매너가 좋아요
COMFORTABLE함께 있으면 편안해요
THOUGHTFUL세심하게 챙겨줘요
GOOD_LISTENER잘 들어줘요

한줄 후기 (선택). 200자 이내의 짧은 코멘트로 더 구체적인 인상을 남길 수 있습니다.

작성된 후기는 ReviewSummaryBadge 형태로 매칭 카드에 노출됩니다. 평균 별점, 총 리뷰 수, 그리고 가장 많이 받은 상위 3개 태그가 표시됩니다. 매칭을 검토하는 상대방은 이 배지를 보고 “이 사람은 만남에서도 좋은 사람이구나”를 확인할 수 있습니다.

이것이 선순환의 핵심입니다. 좋은 만남 → 좋은 후기 → 매칭 매력도 상승 → 더 많은 매칭 기회 → 더 좋은 만남. 만남을 성의 있게 임한 사람이 더 많은 기회를 얻는 구조가 자연스럽게 형성됩니다.


MeetingService 핵심 로직

백엔드에서 만남의 생명주기를 관리하는 MeetingService의 핵심 부분을 살펴보겠습니다.

만남 신청

/**
 * 만남 신청. 신청자의 카드 잔액을 검증하고 PENDING 상태로 생성합니다.
 * 수락 시점에 쌍방 카드가 차감되므로, 신청 단계에서는 차감하지 않습니다.
 *
 * @param requesterId 신청자 ID
 * @param request     만남 신청 정보 (채팅방, 장소, 일시)
 * @return 생성된 MeetingRequest
 * @author Rojae
 */
@Transactional
public MeetingResponse requestMeeting(Long requesterId, MeetingRequestDto request) {
    ChatRoom chatRoom = chatRoomRepository.findById(request.getChatRoomId())
        .orElseThrow(() -> new BusinessException(ErrorCode.CHAT_ROOM_NOT_FOUND));

    // 신청자가 해당 채팅방 멤버인지 검증
    validateChatRoomMember(chatRoom, requesterId);

    // 진행 중인 만남이 이미 있는지 확인
    boolean hasActiveMeeting = meetingRequestRepository
        .existsByChatRoomIdAndStatusIn(
            chatRoom.getId(),
            List.of(MeetingStatus.PENDING, MeetingStatus.ACCEPTED)
        );
    if (hasActiveMeeting) {
        throw new BusinessException(ErrorCode.MEETING_ALREADY_RESPONDED);
    }

    // 신청자 카드 잔액 사전 검증 (수락 시 차감이지만, 잔액 없으면 신청 불가)
    int cardBalance = inventoryService.getCardBalance(requesterId);
    if (cardBalance < MEETING_DEPOSIT_CARDS) {
        throw new BusinessException(ErrorCode.INSUFFICIENT_INVENTORY);
    }

    Long acceptorId = chatRoom.getOtherMemberId(requesterId);

    MeetingRequest meeting = MeetingRequest.builder()
        .chatRoom(chatRoom)
        .requesterId(requesterId)
        .acceptorId(acceptorId)
        .placeName(request.getPlaceName())
        .placeUrl(request.getPlaceUrl())
        .meetingAt(request.getMeetingAt())
        .message(request.getMessage())
        .status(MeetingStatus.PENDING)
        .depositCards(MEETING_DEPOSIT_CARDS)
        .build();

    return MeetingResponse.from(meetingRequestRepository.save(meeting));
}

만남 수락 — 원자적 예약금 차감

수락 시점이 가장 중요합니다. 양측의 카드를 동시에 차감해야 하며, 한쪽만 차감되는 상황이 발생하면 안 됩니다.

/**
 * 만남 수락. 수락 즉시 신청자·수락자 쌍방의 카드를 원자적으로 차감합니다.
 *
 * @param acceptorId 수락자 ID
 * @param meetingId  만남 신청 ID
 * @return 수락된 MeetingRequest
 * @author Rojae
 */
@Transactional
public MeetingResponse acceptMeeting(Long acceptorId, Long meetingId) {
    MeetingRequest meeting = getMeetingAndValidateStatus(meetingId, MeetingStatus.PENDING);

    if (!meeting.getAcceptorId().equals(acceptorId)) {
        throw new BusinessException(ErrorCode.ACCESS_DENIED);
    }

    int depositCards = meeting.getDepositCards();

    // 쌍방 카드 잔액 검증
    inventoryService.validateCardBalance(meeting.getRequesterId(), depositCards);
    inventoryService.validateCardBalance(acceptorId, depositCards);

    // 원자적 차감 (같은 트랜잭션 내)
    inventoryService.deductCards(meeting.getRequesterId(), depositCards);
    inventoryService.deductCards(acceptorId, depositCards);

    meeting.accept();

    return MeetingResponse.from(meetingRequestRepository.save(meeting));
}

만남 완료 — 예약금 반환

/**
 * 만남 완료 확인. 쌍방의 예약금 카드를 반환합니다.
 *
 * @param memberId  완료 확인 요청자 ID
 * @param meetingId 만남 신청 ID
 * @return 완료된 MeetingRequest
 * @author Rojae
 */
@Transactional
public MeetingResponse completeMeeting(Long memberId, Long meetingId) {
    MeetingRequest meeting = getMeetingAndValidateStatus(meetingId, MeetingStatus.ACCEPTED);

    validateMeetingParticipant(meeting, memberId);

    int depositCards = meeting.getDepositCards();

    // 쌍방 예약금 반환
    if (!meeting.isRequesterDepositReturned()) {
        inventoryService.addCards(meeting.getRequesterId(), depositCards);
        meeting.markRequesterDepositReturned();
    }
    if (!meeting.isAcceptorDepositReturned()) {
        inventoryService.addCards(meeting.getAcceptorId(), depositCards);
        meeting.markAcceptorDepositReturned();
    }

    meeting.complete();

    return MeetingResponse.from(meetingRequestRepository.save(meeting));
}

취소 — 귀책 측 카드 몰수

/**
 * 만남 취소. 취소 요청자의 카드는 몰수되고, 상대방 카드는 반환됩니다.
 *
 * @param memberId  취소 요청자 ID
 * @param meetingId 만남 신청 ID
 * @return 취소된 MeetingRequest
 * @author Rojae
 */
@Transactional
public MeetingResponse cancelMeeting(Long memberId, Long meetingId) {
    MeetingRequest meeting = getMeetingAndValidateStatus(meetingId, MeetingStatus.ACCEPTED);

    validateMeetingParticipant(meeting, memberId);

    boolean isRequester = meeting.getRequesterId().equals(memberId);
    int depositCards = meeting.getDepositCards();

    if (isRequester) {
        // 신청자 취소: 신청자 카드 몰수, 수락자 카드 반환
        inventoryService.addCards(meeting.getAcceptorId(), depositCards);
        meeting.cancelByRequester();
    } else {
        // 수락자 취소: 수락자 카드 몰수, 신청자 카드 반환
        inventoryService.addCards(meeting.getRequesterId(), depositCards);
        meeting.cancelByAcceptor();
    }

    return MeetingResponse.from(meetingRequestRepository.save(meeting));
}

트랜잭션 범위 안에서 상태 변경과 카드 처리가 함께 이루어지기 때문에, 카드 처리는 됐는데 상태 변경이 실패하거나 그 반대의 상황이 원천적으로 방지됩니다.


프론트엔드 UX: 부담 없는 만남 신청

예약금 시스템이 아무리 잘 설계되어 있어도, 신청 과정이 복잡하고 불안하면 아무도 사용하지 않습니다. UX의 목표는 명확했습니다. 예약금이 존재하되, 신청 자체는 부담 없이 만드는 것.

MeetingRequestSheet

채팅방 하단에서 올라오는 바텀시트 형태로 만남 신청이 이루어집니다. 장소명, 장소 URL(지도 링크), 만남 일시, 짧은 메시지를 입력합니다.

바텀시트 하단에는 예약금에 대한 안내가 명확하게 표시됩니다.

만남이 수락되면 카드 5장이 예약금으로 차감됩니다.
만남이 완료되면 카드가 반환됩니다.

정보를 숨기지 않고 투명하게 보여줌으로써 신뢰를 형성합니다. 예약금을 모르고 신청했다가 나중에 당황하는 상황을 만들지 않습니다.

MeetingReceivedCard

신청을 받은 쪽에는 채팅 스트림 위에 카드 형태로 만남 신청이 표시됩니다.

[만남 신청이 도착했어요]
장소: 성수 카페 XX
일시: 2026년 3월 15일 오후 2시
메시지: 한번 만나서 이야기 나눠봐요 :)

[수락하기]  [거절하기]
카드 5장이 예약금으로 차감됩니다

수락 버튼을 누르기 전에 예약금 차감에 대한 안내를 한 번 더 보여줍니다. 충분히 인지한 상태에서 수락하도록 유도합니다.

MeetingStatusBanner

만남이 수락된 뒤에는 채팅방 상단에 배너가 고정됩니다.

[만남 확정 D-3]
장소: 성수 카페 XX  |  일시: 3월 15일 오후 2시
[완료 확인]  [취소하기]

만남 당일이 가까워질수록 카운트다운이 표시되어 자연스럽게 상기시켜 줍니다.

MeetingReviewSheet

만남 완료 후에는 후기 작성 바텀시트가 자동으로 열립니다. 강제는 아니지만, 첫 화면에서 바로 접근할 수 있도록 해서 작성 마찰을 최소화합니다.

이번 만남은 어떠셨나요?

⭐⭐⭐⭐⭐

[시간 약속 잘 지켜요]  [대화가 재밌어요]
[친절하고 배려 넘쳐요]  [매너가 좋아요]
[함께 있으면 편안해요]  [잘 들어줘요]
[세심하게 챙겨줘요]    [프로필 사진과 비슷해요]

한줄 후기 (선택)
_________________________________

[후기 남기기]

별점을 먼저 클릭하면 태그 선택이 활성화되는 순차적 UI로, 한 번에 모든 걸 요구하지 않고 단계적으로 참여를 유도합니다.


동시성 이슈 대응

만남 수락 시 양측 카드를 동시에 차감하는 과정에서 동시성 문제가 발생할 수 있습니다. 예를 들어 두 사람이 동시에 수락 버튼을 누르거나(있을 수 없는 시나리오지만), 같은 요청이 중복으로 전달되는 경우입니다.

이를 방지하기 위해 MeetingRequest 엔티티에 낙관적 락(Optimistic Lock)을 적용합니다.

@Entity
@Table(name = "meeting_requests")
public class MeetingRequest extends BaseEntity {

    @Version
    private Long version;  // 낙관적 락

    // ...
}

수락 로직에서 PENDING 상태를 먼저 확인한 뒤 ACCEPTED로 변경합니다. 만약 동시에 두 요청이 들어오면, 하나는 성공하고 나머지는 OptimisticLockException으로 실패하여 재시도하거나 에러를 반환합니다.

또한 카드 차감도 UPDATE ... WHERE cards >= :amount 형태의 조건부 업데이트로 처리하여, 잔액 부족 상태에서 음수가 되는 상황을 DB 레벨에서 차단합니다.


결과: 노쇼율 감소와 만남 품질 향상

예약금 시스템의 도입이 실제로 어떤 변화를 만들어낼 것인지는 서비스가 운영되면서 데이터로 확인되겠지만, 행동 경제학의 이론과 타 도메인의 사례들은 명확한 방향을 가리킵니다.

노쇼율 감소. 카드 5장이라는 예약금은 무의식적 노쇼를 방지합니다. “아 귀찮은데”라는 생각이 들 때, “그러면 카드 5장 날아가는데”라는 생각이 자동으로 따라옵니다.

만남 품질 향상. 예약금이 있으면 더 진지한 사람들이 만남을 신청합니다. 가볍게 떠보는 신청보다, 실제로 만나고 싶은 사람이 신청합니다. 수락하는 쪽도 더 신중하게 고민합니다.

후기 기반 신뢰 형성. 만남 후기가 쌓이면서 커뮤니티 내에서 신뢰도가 가시화됩니다. 좋은 후기를 많이 받은 사람은 매칭 카드에서 더 매력적으로 보이고, 이는 더 좋은 매칭으로 이어집니다.


데이팅 앱의 본질적인 가치는 온라인에서 시작한 연결을 오프라인의 실제 만남으로 이어주는 것입니다. 아무리 소울 점수가 높고 채팅이 잘 통해도, 실제로 만나지 않으면 아무 의미가 없습니다.

썸블링의 만남 시스템은 그 마지막 연결을 더 안전하고, 더 신뢰할 수 있고, 더 설레는 경험으로 만들기 위한 시도입니다. 예약금은 벌칙이 아닙니다. 서로에 대한 진지함의 증거입니다.

“당신의 만남이 기다려지는 약속이 되길.”


썸블링은 OCEAN 성격 모델 기반 AI 매칭 데이팅 앱입니다. 외모보다 내면의 연결을 추구합니다.

댓글

GitHub 계정으로 로그인하여 댓글을 남길 수 있습니다. 허위사실, 욕설, 사칭 등의 댓글은 통보 없이 삭제될 수 있습니다.

블로그로 돌아가기