검색

레이블이 ag-grid인 게시물을 표시합니다. 모든 게시물 표시
레이블이 ag-grid인 게시물을 표시합니다. 모든 게시물 표시

2024년 10월 29일

코딩 - Vue3 : AG-Grid 커뮤니티 버전 기반의 페이징 그리드 컴포넌트 만들기

 AG Grid Community 버전은 매우 인기 있는 오픈소스 그리드 컴포넌트로, 다양한 기능을 제공하지만 상업적인 용도로 사용할 때는 프로 버전으로의 업그레이드가 필요할 수도 있다. 커뮤니티 버전의 이점과 한계를 살펴보면 다음과 같다.

AG Grid Community  버전의 이점

  1. 무료 사용: 커뮤니티 버전은 오픈소스로 제공되므로 무료로 사용할 수 있다. 중소규모의 프로젝트나 상업적 용도가 아닌 경우에 적합.
  2. 강력한 성능:  AG Grid 는 많은 양의 데이터를 효율적으로 처리할 수 있는 그리드 컴포넌트로 기본적인 CRUD 작업을 포함하여 빠른 렌더링 성능을 지원.
  3. 광범위한 기능:
    • 기본적인 정렬, 필터링, 페이징, 그룹핑, 편집 등 그리드에서 필요한 대부분의 기능을 포함
    • 다양한 컬럼 레이아웃과 사용자 정의 셀 렌더러 등 유연한 데이터 표현이 지원
  4. 활발한 커뮤니티: AG Grid는 대규모 사용자 커뮤니티를 보유하고 있어 문제 발생 시 도움을 얻기 용의.
  5. 다양한 프레임워크 지원: React, Angular, Vue, 순수 자바스크립트 등 여러 프레임워크에서 사용할 수 있는 유연함

AG Grid Community 버전의 한계

  1. 고급 기능 제한: 커뮤니티 버전에는 고급 기능이 포함되어 있지 않음. 특히 아래 기능들이 프로 버전에만 포함.
    • 서버 사이드 모델(Server-Side Model): 대규모 데이터 집합에서 서버와 연동하여 그리드 데이터를 관리하는 기능.
    • 익스포트 기능: CSV, Excel 등의 형식으로 데이터를 내보내는 기능이 제한.
    • Tree Data 구조: 트리 구조의 데이터를 처리할 수 있는 고급 데이터 표현 방식 미지원.
    • 엔터프라이즈 레벨 필터링: 고급 필터링 기능은 프로 버전에만 포함.
  2. 기술 지원 부족: 커뮤니티 버전에서는 공식적인 기술 지원을 받을 수 없음. 문제 해결 시 커뮤니티에 의존해야 함.
  3. 사용 제한: 상업적 프로젝트에서 고급 기능이 필요하거나 성능 최적화 및 유지보수가 중요한 경우, 커뮤니티 버전만으로는 한계가 있을 수 있음.
AG Grid Community 버전은 무료로 사용할 수 있고, 기본적인 그리드 기능을 모두 제공하기 때문에 많은 프로젝트에서 유용하다. 그러나 고급 기능이나 대규모 데이터 처리가 필요한 경우 프로 버전으로 업그레이드하는 것이 필요하다. 

유료 버전에서는 rowModelType을 "serverSide" 로 설정하고 그리드가 준비되면 getRows 함수 내에서 서버 요청을 수행하고 결과를 그리드에 전달하는 데이터소스를 api.setServerSideDatasource() 함수를 사용하여 설정하는 방식으로 쉽게 구현할 수 있다.

AG Grid 커뮤니티 버전에서 서버 페이징은 (무한 스크롤을 구현하는 방법) rowModelType 값으로  "infinite" 을 사용하여 비슷한 방법으로 구현할 수 있다. 
  1. rowModelType을 "infinite"로 설정하여 그리드가 데이터를 한 번에 모두 로드하지 않고, 필요한 만큼 서버에서 요청하도록 한다.
  2. getRows 함수는 그리드가 추가 데이터를 요청할 때마다 서버에 데이터를 가져오는 역할을 한다.
  3. dataSource는그리드가 준비될 때 설정하고, 그리드가 데이터를 필요로 할 때마다 자동으로 서버에 데이터를 요청한다. ( 그리드 준비 여부는 onGridReady 이벤트를 사용)
개발 단계를 구체적으로 설명하면 아래와 같다.

단계 설명 코드 예시
1 프로젝트에 AG Grid 설치 npm install --save ag-grid-community ag-grid-vue
2 컬럼 정의 및 그리드 옵션 설정 columnDefs와 gridOptions를 <script setup lang="ts">에서 정의하고 ref로 선언
3 Infinite Row Model 활성화 gridOptions에서 rowModelType을 'infinite'로 설정하고, paginationPageSize 및 cacheBlockSize를 정의
4 onGridReady 함수에서 서버 데이터 소스 설정 onGridReady에서 getRows 함수가 포함된 dataSource를 설정
5 getRows 함수에서 서버 데이터 요청 params.startRow, params.endRow를 기반으로 페이지와 페이지 크기를 서버에 요청
6 서버 응답 처리 서버의 데이터 응답을 받아 params.successCallback(data.rows, data.totalCount)을 호출하여 그리드에 전달
7 Vue 컴포넌트 마운트 시 그리드 참조 설정 onMounted에서 gridRef를 통해 그리드 API에 접근하여 초기 설정 완료


매번 서버 페이징이 필요한 경우 위와 같이 코딩하는 것은 여간 불편한 것이 아니다. 이런 이유에서 추상 형태의 DataSource 을 정의하고 공통으로 적용할 설정을 미리 정의하고 필요한 값만 전달하여 페이징 그리드를 구현하는 컴포넌트를 만들어 보았다.


아래는 PageableGridContent.vue 에서 사용하는 서버에서 데이터를 가져오는 PageableDataSource 에 대한 추상 AbstractPageDataSource 이다. 이를 통하여 좀더 쉽게 서버에서 데이터를 가져오는 데이터소스 구현을 가능하게 한다. ( 구현체에서는 규칙을 벗어 나지 않는 경우라면 서버 엔트포인트 값을 리턴하는 getPatchUrl () 만 구현하면 된다. 서버 프로그램은 스프링의 Pageable  형식으로 데이터를 리턴하는 것을 가정 하여 데이터소스 추상을 구현 했다.


위의 컴포넌트를 사용하여 1) 컴포넌트 임포트 2)  그리드 컬럼 정의 3) 태그를 사용하여 컴포넌트를 정의하는 것으로 그리드를 간단하게 구현 할 수 있다. 또한 컴포넌트에 직접 제어가능한 함수를 사용하여 새로고침, 선택된 데이터 접근, 필터 초기화 등을 제어할 수 있다. 그리드 컬럼에서 단수 데이터 출력이 아닌 버튼과 또는 이미지 출력 등의 작업이 필요한 경우는 사용자 정의 더러를 정의하고 버튼 클릭등의 이벤트는 이벤트 리스트너를 통하여 구현하였다. 


다음은 위의 AbstractPageDataSource 활용하여 구현한 예이다. 아래는 목록을 조회하는 기능과 추가로 여러 함수를 포함하고 있다.

구현 클래스는 추가 기능이 필요하면 PageableDataSource 을 포함하여 새로운 타입을 정의하고 이를 구현하는 클래스를 AbstractPageableDataSource 상속하여 만든다. 구현 클래스는 getFetchUrl 함수를 구현하는 것으로 완성되며 this 이슈를 해결하기 위하여 반인딩을 사용하여 스토어를 생성하게 한다.
  1. 필요한 새로운 PageableDataSource 을 포함하는 type 생성
  2. getFetchUrl 을 포함 새로운 type 을 구현하는 클래스 생성 
  3. 구현 클래스를 사용하여 스토어 생성

import { RoleModel } from "@/types/studio/RoleModel";
import {
API_HEADERS,
authHeader
} from "@/util/helpers";
import axios from "axios";
import { defineStore } from "pinia";

import type {
PageableDataSource
} from "@/types/studio/index";
import { AbstractPageDataSource } from "../AbstractPageDataSource";

// getById,getByName,loadById,saveOrUpdate 추가을 추가하여 IPageableRoleDataSource를 정의
type IPageableRoleDataSource = PageableDataSource & {
getById: (roleId: number) => RoleModel;
getByName: (name: string) => RoleModel;
loadById: (roleId: number) => Promise<RoleModel>;
saveOrUpdate: (item: RoleModel) => Promise<void>;
};
const baseUrl = `${import.meta.env.VITE_API_URL}/data/secure/mgmt/security/roles:find`;

class PageableRoleDataSource
extends AbstractPageDataSource
implements IPageableRoleDataSource
{
getById(roleId: number) {
return this.dataItems.value.find(
(item: RoleModel) => item.roleId === roleId
);
}

getByName(name: string) {
return this.dataItems.value.find((item: RoleModel) => item.name === name);
}

async loadById(roleId: number) {
const headers = { ...API_HEADERS, ...authHeader() };
const category = await axios
.get(
`${import.meta.env.VITE_API_URL}/data/secure/mgmt/security/roles/${roleId}`,
{ headers: headers }
)
.then((response) => {
const data = response.data;
return data;
})
.catch((err) => {
this.alertStore.error(err);
});

return category;
}

async saveOrUpdate(item: RoleModel) {
const headers = { ...API_HEADERS, ...authHeader() };
await axios
.post(
`${import.meta.env.VITE_API_URL}/data/secure/mgmt/security/roles/${item.roleId}`,
JSON.stringify(item),
{ headers: headers }
)
.then((response) => {
const dataItems = response.data;
})
.catch((err) => {
this.alertStore.error(err);
});
}

// API 엔드포인트 URL을 제공. 이 클래스만 구현하면 그리드는 정상 동작한다.
getFetchUrl(): string {
return baseUrl;
}
}

// 필수로 표기된 부분은 반드시 동일하게 리턴해야 한다.
export const usePageableRolesStore = defineStore("mgmt-pageable-roles-store", () => {
const dataSource = new PageableRoleDataSource();
return {
isLoaded: dataSource.isLoaded, // 필수
dataItems: dataSource.dataItems, // 필수
total: dataSource.total, // 필수
pageSize: dataSource.pageSize,// 필수
getById: dataSource.getById.bind(dataSource),
getByName: dataSource.getByName.bind(dataSource),
loadById: dataSource.loadById.bind(dataSource),
saveOrUpdate: dataSource.saveOrUpdate.bind(dataSource),
setPage: dataSource.setPage.bind(dataSource),// 필수
setSort: dataSource.setSort.bind(dataSource),// 필수
setFilter: dataSource.setFilter.bind(dataSource),// 필수
fetch: dataSource.fetch.bind(dataSource), // 필수
};
});

예제 프로그램 실행 화면은 아래와 같다.


2024년 7월 8일

코딩 - Vue3 & Spring : AG-Grid 페이징, 필터 구현하기

  ◼︎ 환경

  • Model : MacBook Pro (14-inch, 2021)
  • CPU : Apple M1 Pro
  • MENORY : 16GB
  • DISK : 512 GB SSD
  • OS : macOS 13.2.4 (22F66)
  • TOOLS : Visual Studio Code, Java 11, Gradle, Docker
  • Version Control : GitHub
  • Programming Language : Java, Vue3
  • Front-End Framework : Vue 3.4.30, Vuetify 3.6.10, ag-grid-vue3 31.3.2
  • Back-End Framework : Spring Boot 2.7.12, Spring Security 5.7.7 
  • DBMS : MySql 8.0.33
  • Cloud : OCI (free tier account)

AG-Grid 을 이용한 그리드 구현

Vue3 에서 AG-Grid 는 다음과 같은 3단계 과정을 통하여 만들 수 있다.

① Vue3 용 ag-grid-vue3 패키지 설치

npm install --save ag-grid-vue3

② AG-Grid CSS 스타일 추가

import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
import "ag-grid-community/styles/ag-theme-quartz.css"; // Optional Theme applied to the grid

③ AG-Grid 설정 ( 그리드를 구현한 vue 파일에서 )
gridOptions를 사용하여 그리드의 전반적인 공통 설정을 관리하고, columnDefs를 사용하여 각 컬럼의 설정을 관리한다. 이를 통해 그리드의 설정을 더 쉽게 체계적으로 관리할 수 있다. 


<template>
<AgGridVue class="ag-theme-quartz"
:gridOptions="gridOptions"
:columnDefs="columnDefs"
:rowData="gridData"
@grid-ready="onGridReady"
style="height:600px;"></AgGridVue>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';

import { AgGridVue } from "ag-grid-vue3"; // Vue Data Grid Component
import type { GridApi, IGetRowsParams } from 'ag-grid-community';
import type { GridOptions, ColDef } from 'ag-grid-community';

// ag-grid
const loader = ref(false);
const gridData = ref([]);
const gridApi = ref<GridApi | null>();

// ag-grid options for common.
const gridOptions: GridOptions = {
defaultColDef: {
flex: 1,
minWidth: 100,
resizable: true,
sortable: true,
filter: true,
cellStyle: {
display: 'flex',
alignItems: 'center'
}
},
columnTypes: {
string: {
filter: 'agTextColumnFilter',
filterParams: { suppressAndOrCondition: true },
cellStyle: { textAlign: 'center' },
},
number: {
filter: 'agNumberColumnFilter',
filterParams: { suppressAndOrCondition: true },
cellStyle: { textAlign: 'right' },
},
},
};


// define grid coloums
const columnDefs = [
{ field: 'groupId', headerName: 'ID', type: 'number',
width: 100, sortable: true },
{ field: 'name', headerName: 'Name', sortable: true , type:"text",
flex:3 , cellRenderer : GroupCellRenderer}, // 사용자 정의 셀 렌더러.
{ field: 'description', headerName: 'Description', type: 'string',
width: 50, sortable: false , filter:false},
];

const onGridReady = (params: any) => {
gridApi.value = params.api;
};

  • gridOptions : 그리드의 전반적인 설정을 정의. (defaultColDef, rowData, paginationPageSize, cacheBlockSize, rowSelection, getRowHeight 등)
  • columnDefs: 각 컬럼의 설정을 정의. 각 컬럼에 대해 headerName, field, sortable, filter, cellStyle, cellRenderer 등을 설정.
  • rowData: 그리드에 표시할 데이터를 설정.
  • onGridReady: 그리드가 준비되면 gridApi 인스턴스을 저장하여 그리드 제어에 사용

서버 사이드 페이징

서버 사이드 페이징 기능을 사용하지 않는 경우라면 간단하게 아래와 같이 rowData 에 해당하는 gridData 객체에 데이터를 강제로 주입해주기만 하면 쉽게 구현이 가능하지만 페이지, 필터, 소팅 등의 기능들을 서버에서 처리하는 것은 조금 더 많은 수고가 필요하다.  


onMounted(async () => {
fetch(`https://api.example.com/data`)
.then(response => response.json()) .then(data => { gridData.value = data.rows; }) .catch(error => { console.error(error); });
});


유료 버전을 사용하는 경우라면 gridOptions 의 rowModelType 값을 'serverSide' 로 변경하고 추가로 datasource 를 설정하면 쉽게 구현이 되는것으로 보이지만 무료 커뮤니티 버전은 좀더 복잡한 과정을 진행해야 한다.

rowModelType 은 그리드가 데이터를 로드하고 관리하는 방식을 결정한다. AG-Grid는 다양한 데이터 로딩 및 관리 요구를 충족시키기 위해 여러 가지 rowModelType을 제공하며, 각 타입은 특정 사용 사례에 최적화되어 있다. 무료 버전에서 서버 사이드 페이징을 구현하려면 rowModelType 값을 infinite 을 사용하여 구현해야 한다.


<AgGridVue class="ag-theme-quartz"
:rowModelType="'infinite'"
:gridOptions="gridOptions"
:columnDefs="columnDefs"
:pagination="true"
:paginationPageSize="pageSize"
:cacheBlockSize="cacheBlockSize"
@grid-ready="onGridReady"
style="height:600px;"></AgGridVue>


페이징 기능을 사용하기 위하여 아래와 같은 설정을 추가한다. 가독성을 고려하여 gridOptions 에 추가하지 않고 AgGridVue 의 개별 속성 값을 직접 설정하는  방식을 사용했다.  
  • pagination : 그리드에서 페이징 기능을 사용할지를 결정하는 옵션. 기본적으로 pagination 속성을 true로 설정하면 페이징이 활성화된다. 페이징이 활성화되면 그리드는 한 번에 일정한 수의 행만 표시하고, 사용자가 페이지를 전환할 수 있도록 페이지 내비게이션 컨트롤을 제공한다.
  • paginationPageSize : 한 페이지에 표시할 행(row)의 수를 설정하는 옵션. 이 속성을 설정하면 사용자가 그리드 페이지를 전환할 때마다 이 수만큼의 행이 표시되며 기본값은 100이다. (참고로 서버에서 데이터를 페이징하여 가져오는 값과는 구분된다.)
  • cacheBlockSize : infinite 또는 serverSide row model을 사용할 때 한 번에 서버에서 가져올 데이터의 블록 크기를 설정하는 옵션. 이 속성은 서버에서 데이터를 페이징 방식으로 가져올 때 유용하며 기본값은 100이다. (-> 서버에 전달하는 페이징 크기 값)
이제 데이터를 가져오는 datasource 에 대한 코딩이 필요하다. datasource 설정은 유연성을 고려하여 onGridReady 이벤트에서 설정한다. 주의할 것은 이벤트에서 인자로 전달되는 params 값을 사용하여 페이지 정보를 서버에 전달해야 하는 점이다.
 

function sortString (sort: SortModelItem[] ) {
if( sort.length > 0 ) {
return sort[0].colId + ',' + sort[0].sort ||''.toUpperCase();
}
return null;
}

const onGridReady = (params: any) => {
gridApi.value = params.api;
const dataSource = {
getRows: async (params: IGetRowsParams) => {
const page = Math.floor(params.startRow / pageSize.value);
const pageSizeValue = params.endRow - params.startRow;
const filterModel = params.filterModel;
const sortModel = params.sortModel;
try {
dataStore.setSort(sortModel);
dataStore.setFilter(filterModel);
dataStore.setPage(page);
filtersActive.value = Object.keys(filterModel).length > 0;
await axios.post(`https://api.example.com/data`,
JSON.stringify(filterModel),
{
params: {
...{
page: page,
size: cacheBlockSize.value,
sort: sortString(sortModel),
},
},
        })
        .then((response) => {
        const data = response.data;
        gridData.value = data
        total.value = data.totalElements;
        params.successCallback(gridData.value, data.totalElements );
        ).catch(error => {
    console.error(error);
        });
params.successCallback(gridData.value, total.value);
} catch (error) {
params.failCallback();
console.error(error);
}
}
};
params.api.setGridOption('datasource', dataSource);
};


위의 예는 서버에 post 방식으로 sorting, paging, filter 정보를 전달하고 있는데 Spring JPA 를 사용하고 있다면 아주 쉽게 서버 프로그램을 만들어 볼 수 있다.

추가로 서버에 데이터를 전송할 때에 Map 형식의  AG-Grid 필터값을 객체 배열로 변환하고 필터 타입을 약어로 변경하여 서버로 전송하도록 아래와 같은 변환 함수를 사용 변환하여 전송했다.


export const convertAgGridFilterToServerFormat = (agFilterModel) => {
const kendoFilters = Object.keys(agFilterModel).map((colId) => {
const filter = agFilterModel[colId];
let operator;
switch (filter.filterType) {
case 'text':
operator = convertNumberFilterType(filter.type);
break;
case 'number':
operator = convertNumberFilterType(filter.type);
break;
case 'boolean':
operator = 'eq';
break;
case 'date':
operator = convertNumberFilterType(filter.type);;
break;
default:
operator = 'eq';
}
return {
field: colId,
operator: operator,
value: filter.filter
};
});
return {
logic: 'and',
filters: kendoFilters
};
};

// 숫자 필터 타입 변환 함수
const convertNumberFilterType = (type) => {
switch (type) {
case 'equals':
return 'eq';
case 'notEqual':
return 'ne';
case 'lessThan':
return 'lt';
case 'lessThanOrEqual':
return 'lte';
case 'greaterThan':
return 'gt';
case 'greaterThanOrEqual':
return 'gte';
case 'contains':
return 'contains'
default:
return 'eq';
}
};

아래는 별도의 store 객체로 분리하여 서버 통신 모듈을 구성한 예이다.

import { API_HEADERS, authHeader, convertAgGridFilterToKendo } from "@/util/helpers";
import axios from "axios";
import { defineStore } from "pinia";
import { computed, ref } from "vue";
import { useAlertStore } from "../../alert.store";
import { GroupModel } from "@/types/models/GroupModel";
import type { SortModelItem } from 'ag-grid-community';

export const usePageableGroupsStore = defineStore("pageable-groups-store", () => {
// error state store
const alertStore = useAlertStore();

// state
const isLoaded = ref<boolean>(false);
const dataItems = ref<GroupModel[]>([]);
// pageable
const total = ref<number>(0);
const page = ref<number>(1);
const pageSize = ref<number>(20);
const sort = ref<SortModelItem[]>([]);
const filter = ref({ logic: "and", filters:[] });

// getters
const getById = computed(() => {
return (groupId: number) =>
dataItems.value.find((item) => item.groupId === groupId);
});

const getByName = computed(() => {
return (name: string) => dataItems.value.find((item) => item.name === name);
});

// setters
function setSort(newValue: SortModelItem[]) {
sort.value = newValue;
}
function setFilter(newValue:any) {
filter.value = convertAgGridFilterToKendo(newValue) ;
}

function setPage(newVal: number): void {
if (page.value === newVal) return;
page.value = newVal;
isLoaded.value = false;
}
// actions
async function loadById(groupId: number) {
const headers = { ...API_HEADERS, ...authHeader() };
const category = await axios.get(
`${ import.meta.env.VITE_API_URL }/data/secure/mgmt/security/groups/${groupId}`,
{ headers: headers }
)
.then((response) => {
const data = response.data;
return data;
})
.catch((err) => {
alertStore.error(err);
});
return category;
}

async function saveOrUpdate(item: GroupModel) {
const isNew = item.groupId === 0;
const headers = { ...API_HEADERS, ...authHeader() };
await axios
.post(
`${import.meta.env.VITE_API_URL}/data/secure/mgmt/security/groups/${item.groupId}`,
JSON.stringify(item),
{ headers: headers }
)
.then((response) => {
const dataItems = response.data;
isLoaded.value = false;
})
.catch((err) => {
alertStore.error(err);
});
}


function sortString () {
if( sort.value.length > 0 ) {
return sortField() + ',' + sortDir()||''.toUpperCase();
}
return null;
}
function sortField() {
if (sort.value.length > 0) {
return sort.value[0].colId;
}
return null;
}

function sortDir() {
if (sort.value.length > 0) {
return sort.value[0].sort ;
}
return null;
}

function pagableParams(){
return {
page: page.value,
size: pageSize.value,
sort: sortString(),
}
}

function filterParams() {
var params = {};
if (filter.value.filters.length > 0) {
filter.value.filters.forEach( item =>{
params[item.field] = item.value;
})
}
return params;
}


async function fetch() {
const headers = { ...API_HEADERS, ...authHeader() };
if( filter.value.filters.length > 0){
return await axios.post(
`${import.meta.env.VITE_API_URL}/data/secure/mgmt/security/groups:find`,
JSON.stringify(filter.value),
{
params: {
...pagableParams(),
},
headers: headers })
.then((response) => {
const data = response.data;
dataItems.value = [];
total.value = data.totalElements;
data.content.forEach((item) => {
dataItems.value.push(item);
});
isLoaded.value = true;
}).catch( err => {
alertStore.error(err);
});
}else{
await axios
.get( `${import.meta.env.VITE_API_URL}/data/secure/mgmt/security/groups`, {
params: {
...pagableParams()
},
headers: headers })
.then((response) => {
const data = response.data;
dataItems.value = [];
total.value = data.totalCount;
data.items.forEach((item) => {
dataItems.value.push(item);
});
isLoaded.value = true;
})
.catch((err) => {
alertStore.error(err);
});
}
}
return {
isLoaded,
dataItems,
total,
page,
pageSize,
getById,
getByName,
setFilter,
loadById,
fetch,
saveOrUpdate,
setPage,
setSort,
};
});



서버 프로그램 

Vue 프로그램에서 요청을 처리하기 위한 서버 프로그램은 스프링 컨트롤러를 이용하여 코딩하면 된다. 아래 컨트롤러 예제는 페이징과 소팅 값은 파라메터 형태로  필터 값은 바디 형태로 바인딩 하고 있다. (-> 페이징 및 소팅은 Spring 에서 제공하는 Pageable 을 활용하여 바인딩)


@PostMapping(value = "/data", produces = MediaType.APPLICATION_JSON_VALUE)
public Page<Group> findGroups(@RequestBody FilterModel filter, @PageableDefault(size = 100, sort = "userId", direction = Sort.Direction.DESC) Pageable pageable) {
return groupService.findGroups(filter.getFilters(), pageable);
}


필터는 별도의 필터 클래스를 정의하여 사용한다. 위 코드에서 groupService 는  에 해당하는 소스는 데이터베이스에서 데이터를 조회하기 위하여 JPA Repositry 을 기반으로 하는 GroupRepository 을 호출하게 된다. GroupRepository 클래스는 필터에 따른 동적 쿼리를 지원하기 위하여 JpaRepository 와 JpaSpecificationExecutor 을 상속 받도록 한다. 

groupService 서비스 클래스는 ⑴ 필터 객체(FilterModel) 배열과 페이징(Pageable) 객체를 인자로 받아  ⑵ Specification 객체를 생성하고 ⑶ Repository 객체의 findAll 함수를 호출하여 Page 형식으로 데이터를 응답받아 리턴하는 형태로 구현한다.