유틸 클래스의 생성 시에 좋은 개발 습관

StudyBug·2025년 11월 30일

서론

커서 방식의 페이징을 사용할 때 커서 정보를 인코딩해서 클라이언트에게 전달하는 방식을 사용하고 있다. 그 이유는 클라이언트와의 유연성 때문이다. 직접적으로 정보를 전달한다면 요구사항이 변함에 따라 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가지이다.

  • CurstorUtil을 일반 클래스가 아닌 final 클래스로 작성했다는 점
    • final 클래스는 최종 클래스라는 뜻으로, 상속이 불가함을 말한다. 즉, 다른 클래스에 CursorUtil이 가진 기능을 넘기지 않겠다는 의도이다.
  • public 생성자가 아닌 private 생성자를 정의했다는 점
    • CursorUtil.메서드 와 같은 방식으로만 사용하게 하기 위해 private 생성자를 사용했다. 즉, 외부에서 CursorUtil 객체를 생성하지 못하게 하여 같은 일을 하는 객체가 2개 이상 존재하지 않도록 막은 것이다.

결론

유틸 클래스를 final 클래스private 생성자 를 사용하여 상속과 인스턴스화를 금지할 수 있었다. 이를 통해 순수하게 static 메서드를 통한 도구 역할만을 수행할 수 있게 설계할 수 있었다. 명확한 코드를 통해 동료 개발자 또한 의도를 쉽게 파악할 수 있을 것이다.

profile
갈 길이 먼 개발자 꿈나무

0개의 댓글