eco-dessert-platform / backend

Apache License 2.0
0 stars 0 forks source link

CursorResponse data를 가공해서 반환할 수 있는 클래스 개발 #297

Open yunyechan9893 opened 1 month ago

yunyechan9893 commented 1 month ago

History

@Getter
@RequiredArgsConstructor
public class CursorPageResponse<T> {

    private final List<T> data;
    private final Long nextCursor;
    private final Boolean hasNext;

    // 데이터를 인자값으로 넣는 순간 다른 데이터로 가공할 수 없음
    public static <T> CursorPageResponse<T> of(List<T> data, int pageSize, ToLongFunction<T> idExtractor) {
        boolean hasNext = data.size() > pageSize;
        Long nextCursor = -1L;

        if (hasNext) {
            T lastReponse = data.get(pageSize - 1);
            nextCursor = idExtractor.applyAsLong(lastReponse);
            data = data.subList(0, pageSize);
        }

        return new CursorPageResponse<>(data, nextCursor, hasNext);
    }
}

🚀 Major Changes & Explanations

sunwon12 commented 1 month ago

예찬님 data = data.subList(0, pageSize); 이 부분이 데이터 사이즈를 pageSize에 맞게 -1 해주는 부분입니다

따라서 ProcessedDataCursorResponse<T, U>는 필요 없을 것 같습니다

yunyechan9893 commented 1 month ago

순원님 data를 -1 해준 후 별도로 data를 가공하고 샆을때땐 어떻게 하나요??

===============tmi return으로 해당 클래스를 반환해줌으로써 data를 -1 해준 후 별도의 데이터 가공을 해줄 수 없는 문제가 있습니다!

그래서 U와 Function을 도입함으로써 이 문제를 해결했습니다

sunwon12 commented 1 month ago

순원님 data를 -1 해준 후 별도로 data를 가공하고 샆을때땐 어떻게 하나요??

===============tmi return으로 해당 클래스를 반환해줌으로써 data를 -1 해준 후 별도의 데이터 가공을 해줄 수 없는 문제가 있습니다!

그래서 U와 Function을 도입함으로써 이 문제를 해결했습니다

쿼리 반환 데이터가 사이즈가 11이라고 한다면 이 11사이즈의 데이터를 가공해주고 마지막 CursorReponse가 될 때 데이터를 -1해줘야 한다고 생각합니다.

저흰 10사이즈의 데이터만 쓸 건데 11사이즈의 데이터 가공은 불필요하지만, 1 사이즈의 데이터 처리로 인한 성능은 아주 미세할 것 같습니다

yunyechan9893 commented 1 month ago

데이터가 List 인 경우도 있지만,

class Animal {
    List<~> list;
    int count;
}

인 경우도 생각해야해요

@Getter
@RequiredArgsConstructor
public class CursorPageResponse<T> {

    private final List<T> data; // 이 데이터가 꼭 List가 될 것이라는 보장이 없음
    private final Long nextCursor;
    private final Boolean hasNext;

    public static <T> CursorPageResponse<T> of(List<T> data, int pageSize, ToLongFunction<T> idExtractor) {
        boolean hasNext = data.size() > pageSize;
        Long nextCursor = -1L;

        if (hasNext) {
            T lastReponse = data.get(pageSize - 1);
            nextCursor = idExtractor.applyAsLong(lastReponse);
            data = data.subList(0, pageSize);
        }

        return new CursorPageResponse<>(data, nextCursor, hasNext);
    }
}

인 경우 Animal을 data 인자값으로 넣을 수 없습니다

shoeone96 commented 3 weeks ago

카톡으로 질문주신 내용 여기에 남깁니다

주말에 나눴던 얘기 중 1번 방식은 현재 진행되고 있는 방식입니다.

1. 공통 조상 CustomPage를 상속받아서 필요한 멤버 변수가 추가로 있을 경우 추가로 자식의 멤버 변수에 추가

@Getter
@RequiredArgsConstructor
@AllArgsConstructor
public class CustomPage<T> {

    private T content;
    private Long nextCursor;
    private Boolean hasNext;

}
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class BoardCustomPage<T> extends CustomPage<T> {

    @JsonInclude(JsonInclude.Include.NON_NULL)
    private Long boardCount;
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private Long storeCount;

    public BoardCustomPage(
        T content,
        Long requestCursor,
        Boolean hasNext,
        Long boardCount,
        Long storeCount
    ) {
        super(content, requestCursor, hasNext);
        this.boardCount = boardCount;
        this.storeCount = storeCount;
    }
}

현재 CustomPage가 모든 페이지에 들어가는 공통 요소이고 BoardCustomPage는 이를 상속 받아 Board를 내려줄 때 필요한 요소를 추가한 형태입니다.

2. 공통 요소를 조상이 아닌 하나의 클래스로 선언 후 실제로 내려줘야 하는 클래스에 추가하는 형태입니다

@Getter
@RequiredArgsConstructor
@AllArgsConstructor
public class CustomPage<T> {

    private T content;
    private Long nextCursor;
    private Boolean hasNext;

}
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class BoardCustomPage<T> extends CustomPage<T> {

    private CustomPage<T> customPage;
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private Long boardCount;
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private Long storeCount;

}

위와 같은 방식으로 상속으로 속성을 그대로 받아오는 것이 아닌 멤버 변수로서 넣는 조합의 방식으로 구현한 것입니다.

관련 링크 전달드립니다. https://tecoble.techcourse.co.kr/post/2020-05-18-inheritance-vs-composition/

yunyechan9893 commented 3 weeks ago

중원님 의견 감사합니다~ 저렇게 사용해도 좋을 것 같네요

다만, 이후 요구 사항이 추가되거나 기능이 더 나왔을 때가 고민입니다

1. 확장성이 있는가? 요구 사항이 늘어났을 경우도 처리할 수 있나?
    - count 말고 다른 필드값이 추가된다면? 
        - 클래스를 만들거나, 필드를 추가해야함
yunyechan9893 commented 3 weeks ago
/**
 * 커서 기반 페이지네이션 상태를 관리하는 클래스입니다.
 */
@Getter
@RequiredArgsConstructor
public class CursorPaginationResponse<T> {

    private final Long nextCursor;
    private final Boolean hasNext;

    /**
     * 페이지네이션이 없는 빈 응답을 생성합니다.
     *
     * @return 빈 {@link CursorPaginationResponse} 객체
     */
    public static <T> CursorPaginationResponse<T> empty() {
        return new CursorPaginationResponse<>(-1L, false);
    }

    /**
     * 페이지네이션 상태를 생성합니다.
     *
     * @param data        데이터 리스트
     * @param pageSize    페이지 크기
     * @param idExtractor 항목에서 ID를 추출하는 함수
     * @return {@link CursorPaginationResponse} 객체
     */
    public static <T> CursorPaginationResponse<T> of(List<T> data, int pageSize, ToLongFunction<T> idExtractor) {
        boolean hasNext = data.size() > pageSize;
        Long nextCursor = -1L;

        if (hasNext) {
            T lastElement = data.get(pageSize - 1);
            nextCursor = idExtractor.applyAsLong(lastElement);
        }

        return new CursorPaginationResponse<>(nextCursor, hasNext);
    }
}
import java.util.List;
import java.util.function.Function;

/**
 * 데이터 변환을 담당하는 클래스입니다.
 */
public class DataProcessor<T, U> {

    private final Function<List<T>, U> transformFunction;

    public DataProcessor(Function<List<T>, U> transformFunction) {
        this.transformFunction = transformFunction;
    }

    /**
     * 주어진 데이터를 변환 함수로 처리하여 반환합니다.
     *
     * @param data 변환될 데이터 리스트
     * @return 변환된 데이터
     */
    public U process(List<T> data) {
        return transformFunction.apply(data);
    }
}
/**
 * 처리된 데이터와 페이지네이션 상태를 포함하는 응답 클래스입니다.
 *
 * @param <T> 페이지네이션 되는 개별 항목의 타입
 * @param <U> 변환 함수 적용 후 반환되는 처리된 데이터의 타입
 */
@Getter
@RequiredArgsConstructor
public class ProcessedDataCursorResponse<U> {

    private final U data;
    private final Long nextCursor;
    private final Boolean hasNext;

    /**
     * 페이지네이션과 데이터 처리를 수행하여 응답 객체를 생성합니다.
     *
     * @param data            변환될 데이터 리스트
     * @param pageSize        페이지 크기
     * @param idExtractor     항목에서 ID를 추출하는 함수
     * @param transformFunction 데이터 리스트를 변환하는 함수
     * @return 처리된 데이터를 포함하는 {@link ProcessedDataCursorResponse} 객체
     */
    public static <T, U> ProcessedDataCursorResponse<U> of(
        List<T> data,
        int pageSize,
        ToLongFunction<T> idExtractor,
        Function<List<T>, U> transformFunction
    ) {
        CursorPaginationResponse<T> pagination = CursorPaginationResponse.of(data, pageSize, idExtractor);
        DataProcessor<T, U> processor = new DataProcessor<>(transformFunction);

        if (pagination.getHasNext()) {
            data = data.subList(0, pageSize);
        }

        U processedData = processor.process(data);

        return new ProcessedDataCursorResponse<>(processedData, pagination.getNextCursor(), pagination.getHasNext());
    }

    /**
     * 빈 응답을 반환합니다.
     *
     * @param processedData 처리된 빈 데이터
     * @return 빈 {@link ProcessedDataCursorResponse} 객체
     */
    public static <U> ProcessedDataCursorResponse<U> empty(U processedData) {
        return new ProcessedDataCursorResponse<>(processedData, -1L, false);
    }
}
shoeone96 commented 3 weeks ago

정확한 예시가 하나 더 있으면 흐름을 보는데 도움이 될 것 같다는 생각이 들지만

  1. 결국 response에 다른 값이 몇 개 씩 추가된다면 (페이지 외 부가적인 정보) 똑같이 새로운 클래스를 사용하거나 변수를 넣어줘야 하는 거 아닌가요? U 부분을 정의한 다른 객체를 넣어줘야 하는 것으로 보입니다
  2. 결국 U 부분에 해당하는 클래스를 각각 만드는 것보다 각 response에 해당하는 클래스를 만들고 필요에 따라 CustomPage와 조합하는 게 더 간단한 로직이 아닌가요?

위 방식의 확장성이 좋다는 부분과 유지보수에 좋은 측면에 대해 확 와닿지 않는 느낌이 있어 하나 예시를 들어 생성부터 반환까지의 로직을 플로우차트로 그려주시면 이해에 도움이 될 것 같습니다

yunyechan9893 commented 3 weeks ago

중원님 감사합니다. 그리다 보니 제가 생각하는 방향이 조금 잡히네요 확장성은 둘 다 문제가 안되네요(제가 잘못 생각하고 있었어요)

다시 보니 제가 계속 걸리는 부분이 책임 분리라고 생각해요 DTO에서 Cursor 로직을 수행하는 것보다 따로 Resolver를 만들어서 동작시키는 게 책임 분리에 더 좋을 것 같습니다.

하지만 이 또한 개발자가 생각하는 DTO의 역할에 따라 달라질 거 같아. 취향차이로 생각이 들어요!

image