방 생성 플로우 설계 - INACTIVE → ACTIVE

Junyoung·2026년 3월 24일

Virgin Road

목록 보기
3/5

배경

Virgin Road는 신랑/신부가 방을 만들면 하객들이 편지를 남기는 서비스다. 방을 만들 때 이름, 결혼 날짜, 이메일을 입력한다. 이 이메일이 나중에 PDF 발송 대상이 된다.

초기 구현은 단순했다. 방 생성 요청이 오면 바로 DB에 INSERT하고 완료. 그런데 서비스를 조금 더 생각해보니 문제가 보였다.


이메일 인증 없이 바로 만들면 생기는 문제

이메일 소유 검증이 없으면 아무나 타인의 이메일로 방을 만들 수 있다.

실제로 이메일 주인이 나중에 방을 만들려고 하면 "이미 존재합니다" 에러만 뜬다. 본인이 만든 것도 아닌데 서비스를 쓸 수 없게 된다. 결혼식 당일 PDF도 엉뚱한 이메일로 발송된다.

이메일 인증을 추가하기로 했다.


처음 구현한 인증 플로우의 문제

처음엔 방을 먼저 만들고(ACTIVE), 이후에 인증하는 구조로 짰다. 그런데 이러면 인증 전에 이미 방이 활성화된 상태라 하객들이 편지를 쓸 수 있었다. 인증을 안 해도 방이 동작하는 것이다.

방 생성과 인증 완료를 묶어야 했다.


INACTIVE → ACTIVE 2단계 플로우

방 생성을 2단계로 나눴다.

POST /rooms         →  INACTIVE 방 생성 + 인증 코드 이메일 발송
POST /rooms/verify  →  인증 코드 확인 + ACTIVE 활성화

1단계: DB에 방을 INACTIVE 상태로만 저장한다. 아직 인증이 안 된 미완성 방이다.
인증 코드를 생성해서 입력한 이메일로 발송한다.

2단계: 인증 코드가 맞으면 방을 ACTIVE로 전환한다. 이 시점부터 하객들이 편지를 쓸 수 있다. 개설 완료 안내 메일도 함께 발송한다.

@Transactional
public RoomCreatedResponse verifyAndActivate(VerifyCodeRequest request) {
    emailVerificationService.verify(request.getRoomCode(), request.getAuthCode());

    Room room = roomRepository.findByRoomCode(request.getRoomCode())
            .orElseThrow(() -> new BusinessException(ErrorCode.ROOM_NOT_FOUND));

    room.activate(); // INACTIVE → ACTIVE

    String shareUrl = frontendUrl + "/room/" + room.getRoomCode();
    mailService.sendRoomCreated(room.getEmail(), groomName, brideName, shareUrl);
    ...
}

이메일 중복 처리

같은 이메일로 방을 여러 번 만들려는 케이스를 처리해야 했다.

상황처리
ACTIVE 방이 이미 있음거부 (이미 운영 중인 방)
INACTIVE 방이 있음재사용 (정보 업데이트 + 인증 코드 재발송)
없음새로 생성

INACTIVE를 재사용하는 이유는, 인증 도중 이탈했다가 다시 시도하는 케이스 때문이다. 매번 새로 INSERT하면 INACTIVE 레코드가 계속 쌓인다. 같은 이메일로 만든 INACTIVE 방은 어차피 동일 인물이므로 덮어쓰는 게 맞다.

Room room = roomRepository.findByEmailAndStatus(request.getEmail(), Room.RoomStatus.INACTIVE)
        .map(existing -> {
            existing.updateInfo(
                    request.getGroomFirstName(),
                    request.getGroomLastName(),
                    request.getBrideFirstName(),
                    request.getBrideLastName(),
                    request.getWeddingDate()
            );
            return existing; // roomCode, email은 유지
        })
        .orElseGet(() -> {
            Room newRoom = Room.builder()
                    .roomCode(generateUniqueRoomCode())
                    ...
                    .build();
            return roomRepository.save(newRoom);
        });

상태 정의

public enum RoomStatus {
    INACTIVE, // 이메일 인증 대기 중
    ACTIVE,   // 정상 운영 중 (하객 편지 가능)
    CLOSED    // 결혼식 종료 후 (스케줄러가 전환)
}

CLOSED는 결혼식 당일 스케줄러가 PDF 발송 후 자동으로 전환한다.


정리

인증 없이 바로 방을 만드는 건 빠르게 구현할 수 있지만, 이메일 소유 검증이 빠져서 실제 서비스에서 문제가 생긴다. INACTIVE 상태를 두고 인증 완료 시 ACTIVE로 전환하는 방식으로 해결했다. 덤으로 INACTIVE 재사용 로직 덕분에 중복 레코드 쌓임도 막을 수 있었다.

profile
라곰

0개의 댓글