From fc9a753f87196de987b85de24dcd7a1b8c3b4319 Mon Sep 17 00:00:00 2001 From: Seokmyung Ham Date: Fri, 23 Aug 2024 08:18:14 +0900 Subject: [PATCH] =?UTF-8?q?[BE]=20=EC=95=BD=EC=86=8D=EC=9D=98=20UUID=20?= =?UTF-8?q?=EB=B0=9C=EA=B8=89=20=EB=B0=A9=EC=8B=9D=EC=9D=84=20=EA=B0=9C?= =?UTF-8?q?=EC=84=A0=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(MeetingService): UUID 생성 방식을 영어 대소문자, 숫자를 포함하는 8글자 랜덤 문자열로 변경 * refactor(MeetingControllerDocs): 스웨거 문서 예외상황 추가 * refactor(UuidGenerator): UUID 생성 로직을 인터페이스 컴포넌트로 분리하여 코드 개선 * refactor(MeetingService): uuid 생성 시 반복문을 for에서 do-while로 개선 --- .../meeting/MeetingControllerDocs.java | 7 ++- .../java/kr/momo/domain/meeting/Meeting.java | 2 +- .../domain/meeting/MeetingRepository.java | 2 + .../domain/meeting/RandomUuidGenerator.java | 13 +++++ .../kr/momo/domain/meeting/UuidGenerator.java | 6 ++ .../momo/exception/code/MeetingErrorCode.java | 1 + .../momo/service/meeting/MeetingService.java | 33 +++++++++-- .../meeting/fake/FakeUuidGenerator.java | 18 ++++++ .../service/meeting/MeetingServiceTest.java | 56 ++++++++++++++----- 9 files changed, 115 insertions(+), 23 deletions(-) create mode 100644 backend/src/main/java/kr/momo/domain/meeting/RandomUuidGenerator.java create mode 100644 backend/src/main/java/kr/momo/domain/meeting/UuidGenerator.java create mode 100644 backend/src/test/java/kr/momo/domain/meeting/fake/FakeUuidGenerator.java diff --git a/backend/src/main/java/kr/momo/controller/meeting/MeetingControllerDocs.java b/backend/src/main/java/kr/momo/controller/meeting/MeetingControllerDocs.java index 6e31009d7..7f6a456c6 100644 --- a/backend/src/main/java/kr/momo/controller/meeting/MeetingControllerDocs.java +++ b/backend/src/main/java/kr/momo/controller/meeting/MeetingControllerDocs.java @@ -29,10 +29,13 @@ public interface MeetingControllerDocs { @ApiErrorResponse.BadRequest(ERROR_CODE_TABLE_HEADER + """ | INVALID_NAME_LENGTH | 이름 길이는 1자 이상 5자 이하 까지 가능합니다. | | INVALID_PASSWORD_LENGTH | 비밀번호 길이는 1자 이상 10자 이하 까지 가능합니다. | + | PAST_NOT_PERMITTED | 과거 날짜로는 약속을 생성할 수 없습니다. | + """) + @ApiErrorResponse.InternalServerError(ERROR_CODE_TABLE_HEADER + """ + | UUID_GENERATION_FAILURE | 약속 생성 과정 중 키 생성에 실패했습니다. 잠시 후 다시 시도해주세요. | """) ResponseEntity> create(@RequestBody @Valid MeetingCreateRequest request); - @Operation( summary = "약속 확정", description = """ @@ -49,7 +52,7 @@ public interface MeetingControllerDocs { """) @ApiErrorResponse.Unauthorized(ERROR_CODE_TABLE_HEADER + """ | UNAUTHORIZED_TOKEN | 유효하지 않은 토큰입니다. | - """) + """) @ApiErrorResponse.Forbidden(ERROR_CODE_TABLE_HEADER + """ | ACCESS_DENIED | 접근이 거부되었습니다. | """) diff --git a/backend/src/main/java/kr/momo/domain/meeting/Meeting.java b/backend/src/main/java/kr/momo/domain/meeting/Meeting.java index 2adfff7db..8d6b9b1dc 100644 --- a/backend/src/main/java/kr/momo/domain/meeting/Meeting.java +++ b/backend/src/main/java/kr/momo/domain/meeting/Meeting.java @@ -28,7 +28,7 @@ public class Meeting extends BaseEntity { @Column(nullable = false, length = 20) private String name; - @Column(nullable = false, length = 40) + @Column(nullable = false, length = 8) private String uuid; @Column(nullable = false) diff --git a/backend/src/main/java/kr/momo/domain/meeting/MeetingRepository.java b/backend/src/main/java/kr/momo/domain/meeting/MeetingRepository.java index a80c1920d..ea6d1e0bc 100644 --- a/backend/src/main/java/kr/momo/domain/meeting/MeetingRepository.java +++ b/backend/src/main/java/kr/momo/domain/meeting/MeetingRepository.java @@ -6,4 +6,6 @@ public interface MeetingRepository extends JpaRepository { Optional findByUuid(String uuid); + + boolean existsByUuid(String uuid); } diff --git a/backend/src/main/java/kr/momo/domain/meeting/RandomUuidGenerator.java b/backend/src/main/java/kr/momo/domain/meeting/RandomUuidGenerator.java new file mode 100644 index 000000000..70f533531 --- /dev/null +++ b/backend/src/main/java/kr/momo/domain/meeting/RandomUuidGenerator.java @@ -0,0 +1,13 @@ +package kr.momo.domain.meeting; + +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.stereotype.Component; + +@Component +public class RandomUuidGenerator implements UuidGenerator { + + @Override + public String generateUuid(int length) { + return RandomStringUtils.randomAlphanumeric(length); + } +} diff --git a/backend/src/main/java/kr/momo/domain/meeting/UuidGenerator.java b/backend/src/main/java/kr/momo/domain/meeting/UuidGenerator.java new file mode 100644 index 000000000..f66dc070b --- /dev/null +++ b/backend/src/main/java/kr/momo/domain/meeting/UuidGenerator.java @@ -0,0 +1,6 @@ +package kr.momo.domain.meeting; + +public interface UuidGenerator { + + String generateUuid(int length); +} diff --git a/backend/src/main/java/kr/momo/exception/code/MeetingErrorCode.java b/backend/src/main/java/kr/momo/exception/code/MeetingErrorCode.java index f52651e96..fd38d4578 100644 --- a/backend/src/main/java/kr/momo/exception/code/MeetingErrorCode.java +++ b/backend/src/main/java/kr/momo/exception/code/MeetingErrorCode.java @@ -13,6 +13,7 @@ public enum MeetingErrorCode implements ErrorCodeType { INVALID_DATETIME_RANGE(HttpStatus.BAD_REQUEST, "날짜 또는 시간이 잘못되었습니다."), PAST_NOT_PERMITTED(HttpStatus.BAD_REQUEST, "과거 날짜로는 약속을 생성할 수 없습니다."), NOT_CONFIRMED(HttpStatus.NOT_FOUND, "아직 확정되지 않은 약속입니다."), + UUID_GENERATION_FAILURE(HttpStatus.INTERNAL_SERVER_ERROR, "약속 생성 과정 중 키 생성에 실패했습니다. 잠시 후 다시 시도해주세요."), MEETING_LOAD_FAILURE(HttpStatus.INTERNAL_SERVER_ERROR, "약속 정보를 불러오는데 실패했습니다. 약속을 다시 생성해주세요."); private final HttpStatus httpStatus; diff --git a/backend/src/main/java/kr/momo/service/meeting/MeetingService.java b/backend/src/main/java/kr/momo/service/meeting/MeetingService.java index 192ffa296..67da08d30 100644 --- a/backend/src/main/java/kr/momo/service/meeting/MeetingService.java +++ b/backend/src/main/java/kr/momo/service/meeting/MeetingService.java @@ -4,7 +4,6 @@ import java.time.LocalDate; import java.time.LocalTime; import java.util.List; -import java.util.UUID; import kr.momo.domain.attendee.Attendee; import kr.momo.domain.attendee.AttendeeRepository; import kr.momo.domain.attendee.Role; @@ -13,6 +12,7 @@ import kr.momo.domain.availabledate.AvailableDates; import kr.momo.domain.meeting.Meeting; import kr.momo.domain.meeting.MeetingRepository; +import kr.momo.domain.meeting.UuidGenerator; import kr.momo.exception.MomoException; import kr.momo.exception.code.AttendeeErrorCode; import kr.momo.exception.code.MeetingErrorCode; @@ -29,8 +29,12 @@ @RequiredArgsConstructor public class MeetingService { + private static final int MAX_UUID_GENERATION_ATTEMPTS = 5; + private static final int SHORT_UUID_LENGTH = 8; + private final JwtManager jwtManager; private final Clock clock; + private final UuidGenerator uuidGenerator; private final MeetingRepository meetingRepository; private final AvailableDateRepository availableDateRepository; private final AttendeeRepository attendeeRepository; @@ -49,17 +53,34 @@ public MeetingCreateResponse create(MeetingCreateRequest request) { return MeetingCreateResponse.from(meeting, attendee, meetingDates, token); } + private Meeting saveMeeting(String meetingName, LocalTime startTime, LocalTime endTime) { + String uuid = generateUniqueUuid(); + Meeting meeting = new Meeting(meetingName, uuid, startTime, endTime); + return meetingRepository.save(meeting); + } + + private String generateUniqueUuid() { + String uuid; + int attempts = 0; + + do { + uuid = uuidGenerator.generateUuid(SHORT_UUID_LENGTH); + attempts++; + } while (meetingRepository.existsByUuid(uuid) && attempts < MAX_UUID_GENERATION_ATTEMPTS); + + if (attempts >= MAX_UUID_GENERATION_ATTEMPTS) { + throw new MomoException(MeetingErrorCode.UUID_GENERATION_FAILURE); + } + + return uuid; + } + private void validateNotPast(AvailableDates meetingDates) { if (meetingDates.isAnyBefore(LocalDate.now(clock))) { throw new MomoException(MeetingErrorCode.PAST_NOT_PERMITTED); } } - private Meeting saveMeeting(String meetingName, LocalTime startTime, LocalTime endTime) { - Meeting meeting = new Meeting(meetingName, UUID.randomUUID().toString(), startTime, endTime); - return meetingRepository.save(meeting); - } - private Attendee saveHostAttendee(Meeting meeting, String hostName, String hostPassword) { Attendee attendee = new Attendee(meeting, hostName, hostPassword, Role.HOST); return attendeeRepository.save(attendee); diff --git a/backend/src/test/java/kr/momo/domain/meeting/fake/FakeUuidGenerator.java b/backend/src/test/java/kr/momo/domain/meeting/fake/FakeUuidGenerator.java new file mode 100644 index 000000000..c4303a9b4 --- /dev/null +++ b/backend/src/test/java/kr/momo/domain/meeting/fake/FakeUuidGenerator.java @@ -0,0 +1,18 @@ +package kr.momo.domain.meeting.fake; + +import kr.momo.domain.meeting.UuidGenerator; + +public class FakeUuidGenerator implements UuidGenerator { + + private static final String BASE_FAKE_UUID = "Momo"; + + @Override + public String generateUuid(int length) { + StringBuilder uuid = new StringBuilder(); + while (uuid.length() < length) { + uuid.append(BASE_FAKE_UUID); + } + + return uuid.substring(0, length); + } +} diff --git a/backend/src/test/java/kr/momo/service/meeting/MeetingServiceTest.java b/backend/src/test/java/kr/momo/service/meeting/MeetingServiceTest.java index 49806b586..aad2f4d1d 100644 --- a/backend/src/test/java/kr/momo/service/meeting/MeetingServiceTest.java +++ b/backend/src/test/java/kr/momo/service/meeting/MeetingServiceTest.java @@ -3,11 +3,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.junit.jupiter.api.Assertions.assertAll; -import static org.mockito.Mockito.doReturn; import java.time.Clock; import java.time.Instant; import java.time.LocalDate; +import java.time.LocalTime; import java.time.ZoneId; import java.util.List; import kr.momo.domain.attendee.Attendee; @@ -16,6 +16,8 @@ import kr.momo.domain.availabledate.AvailableDateRepository; import kr.momo.domain.meeting.Meeting; import kr.momo.domain.meeting.MeetingRepository; +import kr.momo.domain.meeting.UuidGenerator; +import kr.momo.domain.meeting.fake.FakeUuidGenerator; import kr.momo.exception.MomoException; import kr.momo.exception.code.AttendeeErrorCode; import kr.momo.exception.code.MeetingErrorCode; @@ -30,21 +32,32 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.mock.mockito.SpyBean; +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; @IsolateDatabase @SpringBootTest(webEnvironment = WebEnvironment.NONE) class MeetingServiceTest { - private static final Clock FIXED_CLOCK = Clock.fixed( - Instant.parse("2024-08-01T10:15:30Z"), ZoneId.of("Asia/Seoul") - ); + @TestConfiguration + static class TestConfig { - @SpyBean + @Bean + public UuidGenerator uuidGenerator() { + return new FakeUuidGenerator(); + } + + @Bean + public Clock fixedClock() { + return Clock.fixed(Instant.parse("2024-08-01T10:15:30Z"), ZoneId.of("Asia/Seoul")); + } + } + + @Autowired private Clock clock; @Autowired - private MeetingService meetingService; + private UuidGenerator uuidGenerator; @Autowired private MeetingRepository meetingRepository; @@ -55,6 +68,9 @@ class MeetingServiceTest { @Autowired private AttendeeRepository attendeeRepository; + @Autowired + private MeetingService meetingService; + @DisplayName("UUID로 약속 정보를 조회한다.") @Test void findByUUID() { @@ -99,11 +115,29 @@ void doesNotFindMeetingSharingMeetingIfUUIDNotExist() { .hasMessage(MeetingErrorCode.INVALID_UUID.message()); } + @DisplayName("UUID가 이미 존재하여 최대 생성 횟수를 초과하면 예외가 발생한다.") + @Test + void throwExceptionWhenUuidAlreadyExistsAfterMaxAttempts() { + Meeting meeting = new Meeting("momo", uuidGenerator.generateUuid(8), LocalTime.MIDNIGHT, LocalTime.NOON); + meetingRepository.save(meeting); + MeetingCreateRequest request = new MeetingCreateRequest( + "name", + "password", + "meetingName", + List.of(LocalDate.now().toString()), + "08:00", + "22:00" + ); + + assertThatThrownBy(() -> meetingService.create(request)) + .isInstanceOf(MomoException.class) + .hasMessage(MeetingErrorCode.UUID_GENERATION_FAILURE.message()); + } + @DisplayName("약속을 생성할 때 과거 날짜를 보내면 예외가 발생합니다.") @Test void throwExceptionWhenDatesHavePast() { //given - setFixedClock(); LocalDate today = LocalDate.now(clock); LocalDate yesterday = today.minusDays(1); MeetingCreateRequest request = new MeetingCreateRequest( @@ -220,10 +254,4 @@ void throwsExceptionWhenUnlockAttendeeGuest() { .isInstanceOf(MomoException.class) .hasMessage(AttendeeErrorCode.ACCESS_DENIED.message()); } - - private void setFixedClock() { - doReturn(Instant.now(FIXED_CLOCK)) - .when(clock) - .instant(); - } }