커서 방식의 페이징을 사용할 때 커서 정보를 인코딩해서 클라이언트에게 전달하는 방식을 사용하고 있다. 그 이유는 클라이언트와의 유연성 때문이다. 직접적으로 정보를 전달한다면 요구사항이 변함에 따라 DTO를 수정해야하니 클라이언트와 서버 서로가 불편할 것이다.
여기서 페이징을 정보를 제공하는 도메인에서 커서 관련 처리를 매번 할텐데, 이 기능들을 위한 클래스를 어떻게 작성하면 좋을지 알아보려고한다.
커서 관련 기능을 제공하는 CursorUtil 클래스를 작성하려 한다. 좋은 유틸 클래스를 작성하려면 그 역할에 대해서 명확히 하는 것이 좋을 것 같다. 유틸 클래스는 말 그대로 도구이다. 도구란? 내가 필요할 때 언제든지 꺼내쓰면 되는 것이다. 또, 같은 도구가 여러 개 있는 것은 낭비이다. 이러한 관점에서 봤을 때, CursorUtil은 유일해야 한다.
아래는 위 관점으로 작성한 코드이다.
public final class CursorUtil {
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().registerModule(
new JavaTimeModule());
private CursorUtil() {
}
public static String encode(Object cursorObj) {
if (cursorObj == null) {
log.warn("null 커서 객체를 인코딩 시도했습니다. null을 반환합니다.");
return null;
}
try {
byte[] jsonBytes = OBJECT_MAPPER.writeValueAsBytes(cursorObj);
return Base64.getUrlEncoder().encodeToString(jsonBytes);
} catch (Exception e) {
throw new CustomException(ErrorCode.CURSOR_ENCODE_FAILED);
}
}
public static <T> T decode(String cursor, Class<T> clazz) {
if (!StringUtils.hasText(cursor)) {
return null;
}
try {
byte[] decodedBytes = Base64.getUrlDecoder().decode(cursor);
return OBJECT_MAPPER.readValue(decodedBytes, clazz);
} catch (Exception e) {
throw new CustomException(ErrorCode.CURSOR_DECODE_FAILED);
}
}
}
여기서 봐야할 점은 2가지이다.
CursorUtil.메서드 와 같은 방식으로만 사용하게 하기 위해 private 생성자를 사용했다. 즉, 외부에서 CursorUtil 객체를 생성하지 못하게 하여 같은 일을 하는 객체가 2개 이상 존재하지 않도록 막은 것이다.유틸 클래스를
final 클래스와private 생성자를 사용하여 상속과 인스턴스화를 금지할 수 있었다. 이를 통해 순수하게 static 메서드를 통한 도구 역할만을 수행할 수 있게 설계할 수 있었다. 명확한 코드를 통해 동료 개발자 또한 의도를 쉽게 파악할 수 있을 것이다.