Skip to content

Commit

Permalink
Fix/#73 커서 페이지네이션 버그 수정 (#74)
Browse files Browse the repository at this point in the history
* refactor: LogData 및 Log 필드 타입 변경

- apiKey에 대한 필드 타입을 UUID에서 byte[]로 변경했습니다.
- 이에 따라 Log 필드 타입도 String에서 UUID로 변경했습니다.

* feat: UUID to byte[], byte[] to UUID 로직 추가

- UUID Convertor에 위 두개 로직을 추가했습니다.
- 이에 따른 테스트를 추가했습니다.

* refactor: findLogsByAppKey 메서드 로직 수정

- DB 엔티티 필드 타입 변경을 지원하기 위해 UUID to Byte[] 로직을 추가했습니다.
- 이에 따른 테스트를 수정했습니다.

* refactor: ResponseEntity Wrap 제거

- ResponseEntity로 감싸는 형태를 제거했습니다.

* feat: CursorPaginationResult 구현

- CursorPaginationResult 구현

* feat: body로 처리하기 위한 LogCursorRequest 구현

- LogCursorRequest 구현
  • Loading branch information
tidavid1 authored and LuizyHub committed Aug 19, 2024
1 parent 9cb75cb commit 6db827c
Show file tree
Hide file tree
Showing 12 changed files with 160 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package info.logbat_view.common.payload;

import info.logbat_view.domain.log.presentation.payload.response.LogCommonResponse;
import java.util.List;
import lombok.Getter;

@Getter
public class CursorPaginationResult {

private final List<LogCommonResponse> data;
private final Integer size;
private final Long nextCursor;
private final Boolean hasNext;

private CursorPaginationResult(List<LogCommonResponse> data, Integer size, Long nextCursor,
Boolean hasNext) {
this.data = data;
this.size = size;
this.nextCursor = nextCursor;
this.hasNext = hasNext;
}

public static CursorPaginationResult of(List<LogCommonResponse> data, int size) {
boolean hasNext = data.size() > size;
List<LogCommonResponse> subList = hasNext ? data.subList(0, size) : data;
Long nextCursor = subList.get(subList.size() - 1).id();
return new CursorPaginationResult(subList, subList.size(), nextCursor, hasNext);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package info.logbat_view.common.util;

import java.nio.ByteBuffer;
import java.util.UUID;
import java.util.regex.Pattern;
import lombok.AccessLevel;
Expand All @@ -8,6 +9,7 @@
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class UUIDConvertor {

private static final int UUID_BYTE_LENGTH = 16;
private static final Pattern UUID_PATTERN = Pattern.compile(
"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$");

Expand All @@ -21,4 +23,24 @@ public static UUID convertStringToUUID(String uuid) {
return UUID.fromString(uuid);
}

public static UUID convertBytesToUUID(byte[] bytes) {
if (bytes.length != UUID_BYTE_LENGTH) {
throw new IllegalArgumentException("UUID 바이트 배열의 길이는 16이어야 합니다.");
}
ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
long mostSigBits = byteBuffer.getLong();
long leastSigBits = byteBuffer.getLong();
return new UUID(mostSigBits, leastSigBits);
}

public static byte[] convertUUIDToBytes(UUID uuid) {
if (uuid == null) {
throw new IllegalArgumentException("UUID는 필수 값 입니다.");
}
ByteBuffer byteBuffer = ByteBuffer.wrap(new byte[UUID_BYTE_LENGTH]);
byteBuffer.putLong(uuid.getMostSignificantBits());
byteBuffer.putLong(uuid.getLeastSignificantBits());
return byteBuffer.array();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,4 @@ public Flux<LogCommonResponse> findLogs(String appKey, Long id, Integer size) {
return logService.findLogsByAppKey(appKeyUUID, id, size).map(LogCommonResponse::from);
}


}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package info.logbat_view.domain.log.domain;

import info.logbat_view.common.util.UUIDConvertor;
import info.logbat_view.domain.log.domain.enums.LogLevel;
import java.time.LocalDateTime;
import java.util.UUID;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
Expand All @@ -11,14 +13,14 @@
public class Log {

private final Long id;
private final String appKey;
private final UUID appKey;
private final LogLevel level;
private final String data;
private final LocalDateTime timestamp;

public static Log from(LogData logData) {
return new Log(logData.getLogId(),
logData.getAppKey().toString(),
UUIDConvertor.convertBytesToUUID(logData.getAppKey()),
LogLevel.valueOf(logData.getLevel()),
logData.getData(),
logData.getTimestamp());
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package info.logbat_view.domain.log.domain;

import java.time.LocalDateTime;
import java.util.UUID;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.data.annotation.Id;
Expand All @@ -14,7 +13,7 @@ public class LogData {

@Id
private final Long logId;
private final UUID appKey;
private final byte[] appKey;
private final String level;
private final String data;
private final LocalDateTime timestamp;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package info.logbat_view.domain.log.domain.service;

import info.logbat_view.common.util.UUIDConvertor;
import info.logbat_view.domain.log.domain.Log;
import info.logbat_view.domain.log.repository.LogDataRepository;
import java.util.UUID;
Expand All @@ -13,7 +14,8 @@ public class LogService {

private final LogDataRepository logDataRepository;

public Flux<Log> findLogsByAppKey(UUID appKey, Long id, Integer size) {
public Flux<Log> findLogsByAppKey(UUID uuid, Long id, Integer size) {
byte[] appKey = UUIDConvertor.convertUUIDToBytes(uuid);
return logDataRepository.findByAppKeyAndLogIdGreaterThanOrderByLogId(appKey, id)
.take(size)
.map(Log::from);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package info.logbat_view.domain.log.presentation;

import info.logbat_view.common.payload.CursorPaginationResult;
import info.logbat_view.domain.log.application.LogViewService;
import info.logbat_view.domain.log.presentation.payload.response.LogCommonResponse;
import info.logbat_view.domain.log.presentation.payload.request.LogCursorRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

@Slf4j
@RequestMapping("/logs")
@RestController
@RequiredArgsConstructor
Expand All @@ -19,10 +21,12 @@ public class LogViewController {
private final LogViewService logViewService;

@GetMapping("/{appKey}")
public Flux<ResponseEntity<LogCommonResponse>> getLogs(@PathVariable String appKey,
@RequestParam(defaultValue = "-1", required = false) Long cursor,
@RequestParam(defaultValue = "10", required = false) Integer size) {
return logViewService.findLogs(appKey, cursor, size).map(ResponseEntity::ok);
public Mono<CursorPaginationResult> getLogs(@PathVariable String appKey, @RequestBody
LogCursorRequest request) {
log.info("appKey: {}, cursor: {}, size: {}", appKey, request.cursor(), request.size());
return logViewService.findLogs(appKey, request.cursor(), request.size() + 1)
.collectList()
.map(data -> CursorPaginationResult.of(data, request.size()));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package info.logbat_view.domain.log.presentation.payload.request;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

public record LogCursorRequest(Long cursor, Integer size) {

@JsonCreator
public LogCursorRequest(@JsonProperty("cursor") Long cursor,
@JsonProperty("size") Integer size) {
this.cursor = cursor == null ? -1 : cursor;
this.size = size == null ? 10 : size;
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package info.logbat_view.domain.log.repository;

import info.logbat_view.domain.log.domain.LogData;
import java.util.UUID;
import org.springframework.data.repository.reactive.ReactiveCrudRepository;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Repository;
Expand All @@ -10,6 +9,7 @@
@Repository
public interface LogDataRepository extends ReactiveCrudRepository<LogData, Long> {

Flux<LogData> findByAppKeyAndLogIdGreaterThanOrderByLogId(@NonNull UUID appKey,
Flux<LogData> findByAppKeyAndLogIdGreaterThanOrderByLogId(@NonNull byte[] appKey,
@NonNull Long id);

}
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,65 @@ private static Stream<Arguments> exceptionValues() {
);
}
}

@Nested
@DisplayName("바이트 배열을 UUID로 변환할 때")
class whenConvertBytesToUUID {

@Test
@DisplayName("정상적으로 변환한다.")
void willReturnSuccess() {
// Arrange
UUID expectedUUID = UUID.randomUUID();
byte[] bytes = UUIDConvertor.convertUUIDToBytes(expectedUUID);
// Act
UUID actualUUID = UUIDConvertor.convertBytesToUUID(bytes);
// Assert
assertThat(actualUUID).isEqualTo(expectedUUID);
}

@ParameterizedTest
@MethodSource("exceptionValues")
@DisplayName("바이트 배열의 길이가 16이 아닐 경우 예외를 던진다.")
void willThrowExceptionWhenBytesLengthIsNot16(byte[] bytes) {
// Act & Assert
assertThatThrownBy(() -> UUIDConvertor.convertBytesToUUID(bytes))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("UUID 바이트 배열의 길이는 16이어야 합니다.");
}

private static Stream<Arguments> exceptionValues() {
return Stream.of(
Arguments.of(new byte[0]),
Arguments.of(new byte[1]),
Arguments.of(new byte[17])
);
}
}

@Nested
@DisplayName("UUID를 바이트 배열로 변환할 때")
class whenConvertUUIDToBytes {

@Test
@DisplayName("정상적으로 변환한다.")
void willReturnSuccess() {
// Arrange
UUID expectedUUID = UUID.randomUUID();
// Act
byte[] actualBytes = UUIDConvertor.convertUUIDToBytes(expectedUUID);
// Assert
UUID actualUUID = UUIDConvertor.convertBytesToUUID(actualBytes);
assertThat(actualUUID).isEqualTo(expectedUUID);
}

@Test
@DisplayName("UUID가 null일 경우 예외를 던진다.")
void willThrowExceptionWhenUUIDIsNull() {
// Act & Assert
assertThatThrownBy(() -> UUIDConvertor.convertUUIDToBytes(null))
.isInstanceOf(IllegalArgumentException.class)
.hasMessage("UUID는 필수 값 입니다.");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;

import info.logbat_view.common.util.UUIDConvertor;
import info.logbat_view.domain.log.domain.Log;
import info.logbat_view.domain.log.domain.LogData;
import info.logbat_view.domain.log.domain.enums.LogLevel;
Expand All @@ -30,7 +31,8 @@ class LogViewServiceTest {
private LogService logService;

private final Long expectedId = 1L;
private final UUID expectedAppKey = UUID.randomUUID();
private final UUID expectedUUID = UUID.randomUUID();
private final byte[] expectedAppKey = UUIDConvertor.convertUUIDToBytes(expectedUUID);
private final LogLevel expectedLevel = LogLevel.INFO;
private final String expectedData = "data";
private final LocalDateTime expectedTimestamp = LocalDateTime.of(2024, 8, 15, 12, 0, 0, 0);
Expand All @@ -46,7 +48,7 @@ void findLogsByAppKey() {
given(logService.findLogsByAppKey(any(UUID.class), any(Long.class),
any(Integer.class))).willReturn(Flux.just(expectedLog));
// Act & Assert
logViewService.findLogs(expectedAppKey.toString(), -1L, 10)
logViewService.findLogs(expectedUUID.toString(), -1L, 10)
.as(StepVerifier::create)
.expectNextMatches(response -> {
assertThat(response)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;

import info.logbat_view.common.util.UUIDConvertor;
import info.logbat_view.domain.log.domain.LogData;
import info.logbat_view.domain.log.domain.enums.LogLevel;
import info.logbat_view.domain.log.repository.LogDataRepository;
Expand All @@ -30,7 +31,8 @@ class LogServiceTest {
private LogDataRepository logDataRepository;

private final Long expectedLogDataId = 1L;
private final UUID expectedAppKey = UUID.randomUUID();
private final UUID expectedUUID = UUID.randomUUID();
private final byte[] expectedAppKey = UUIDConvertor.convertUUIDToBytes(expectedUUID);
private final LogLevel expectedLogLevel = LogLevel.ERROR;
private final String expectedData = "data";
private final LocalDateTime expectedTimestamp = LocalDateTime.of(2024, 8, 15, 12, 0, 0, 0);
Expand All @@ -45,15 +47,15 @@ class whenGetAppsByAppKey {
@DisplayName("조회된 LogData들을 Log로 매핑해서 반환한다.")
void shouldReturnApps() {
// Arrange
given(logDataRepository.findByAppKeyAndLogIdGreaterThanOrderByLogId(any(UUID.class),
given(logDataRepository.findByAppKeyAndLogIdGreaterThanOrderByLogId(any(byte[].class),
any(Long.class))).willReturn(
Flux.just(expectedLogData));
// Act & Assert
StepVerifier.create(logService.findLogsByAppKey(expectedAppKey, 0L, 2))
StepVerifier.create(logService.findLogsByAppKey(expectedUUID, 0L, 2))
.expectNextMatches(log -> {
assertThat(log).extracting("id", "appKey", "level", "data", "timestamp")
.containsExactly(expectedLogDataId, expectedAppKey.toString(),
expectedLogLevel, expectedData, expectedTimestamp);
assertThat(log).extracting("id", "level", "data", "timestamp")
.containsExactly(expectedLogDataId, expectedLogLevel, expectedData,
expectedTimestamp);
return true;
})
.verifyComplete();
Expand Down

0 comments on commit 6db827c

Please sign in to comment.