
Virgin Road는 신랑/신부가 방을 만들면 하객들이 편지를 남기는 서비스다. 방을 만들 때 이름, 결혼 날짜, 이메일을 입력한다. 이 이메일이 나중에 PDF 발송 대상이 된다.
초기 구현은 단순했다. 방 생성 요청이 오면 바로 DB에 INSERT하고 완료. 그런데 서비스를 조금 더 생각해보니 문제가 보였다.
이메일 소유 검증이 없으면 아무나 타인의 이메일로 방을 만들 수 있다.
실제로 이메일 주인이 나중에 방을 만들려고 하면 "이미 존재합니다" 에러만 뜬다. 본인이 만든 것도 아닌데 서비스를 쓸 수 없게 된다. 결혼식 당일 PDF도 엉뚱한 이메일로 발송된다.
이메일 인증을 추가하기로 했다.
처음엔 방을 먼저 만들고(ACTIVE), 이후에 인증하는 구조로 짰다. 그런데 이러면 인증 전에 이미 방이 활성화된 상태라 하객들이 편지를 쓸 수 있었다. 인증을 안 해도 방이 동작하는 것이다.
방 생성과 인증 완료를 묶어야 했다.
방 생성을 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 재사용 로직 덕분에 중복 레코드 쌓임도 막을 수 있었다.