-
비트마스크 적용 (QueryDSL & Hibernate 6.x)Language/Java 2024. 8. 28. 16:45
Java QueryDSL에서 비트마스크를 사용하는 이유는 여러 상태나 옵션을 효율적으로 저장하고, 이를 SQL 쿼리에서 쉽게 필터링하기 위해서이고, 비트마스크를 사용하면 여러 가지 조건을 하나의 필드로 관리할 수 있으며, 데이터베이스에서 복잡한 조건을 효율적으로 조회할 수 있다.
비트마스크 사용의 주요 이유
1. 상태 관리의 효율성
여러 상태나 옵션을 하나의 정수 값에 저장할 수 있다. 예를 들어, 사용자의 권한을 비트마스크로 관리하면, 권한 정보를 단일 열로 관리할 수 있다.
2. 간결한 쿼리
비트마스크를 사용하면 복잡한 조건을 간단한 비트 연산으로 표현할 수 있다. 여러 개의 OR 또는 AND 조건 대신, 비트 연산자를 사용해 단순한 조건으로 필터링할 수 있다.
3. 성능 최적화
비트 연산은 매우 빠르며, 데이터베이스 수준에서 효율적으로 처리된다. 특히, 복잡한 상태 조건을 SQL 쿼리에서 빠르게 평가할 수 있다.
frontend
상수
export const ExposureType = { GOBANG: 1 << 0, // 1 STATION: 1 << 1, // 2 ALGO: 1 << 2, // 4 UCEO: 1 << 3, // 8 UAGENT: 1 << 4, // 16 }; export const PicksExposureTypes = { [ExposureType.GOBANG]: { text: '고방' }, ... [ExposureType.UCEO]: { text: 'U사장님' }, }; export const getExposureTypeMaskByExposureTypeMask = (exposureTypeMask: number) => { const exposureTypes: number[] = []; if (exposureTypeMask & ExposureType.GOBANG) { exposureTypes.push(ExposureType.GOBANG); } if (exposureTypeMask & ExposureType.STATION) { exposureTypes.push(ExposureType.STATION); } if (exposureTypeMask & ExposureType.ALGO) { exposureTypes.push(ExposureType.ALGO); } if (exposureTypeMask & ExposureType.UCEO) { exposureTypes.push(ExposureType.UCEO); } if (exposureTypeMask & ExposureType.UAGENT) { exposureTypes.push(ExposureType.UAGENT); } return exposureTypes; }; export const getExposureTypeNamesByExposureTypeMask = (exposureTypeMask: number) => { const exposureTypeNames: string[] = []; if (exposureTypeMask & ExposureType.GOBANG) { exposureTypeNames.push(PicksExposureTypes[ExposureType.GOBANG].text); } if (exposureTypeMask & ExposureType.STATION) { exposureTypeNames.push(PicksExposureTypes[ExposureType.STATION].text); } if (exposureTypeMask & ExposureType.ALGO) { exposureTypeNames.push(PicksExposureTypes[ExposureType.ALGO].text); } if (exposureTypeMask & ExposureType.UCEO) { exposureTypeNames.push(PicksExposureTypes[ExposureType.UCEO].text); } if (exposureTypeMask & ExposureType.UAGENT) { exposureTypeNames.push(PicksExposureTypes[ExposureType.UAGENT].text); } return exposureTypeNames; }; ...
전송
const onSubmit = async (formData: FormData) => { // 서버로 비트마스크 값 전송 const l_exposureTypes: number[] = watch('exposureTypes') || []; formData.exposureType = l_exposureTypes.reduce((acc: number, type: number) => acc | type, 0); const l_uploadFiles = formData?.fileItems?.filter(function (el) { return el.mode !== FILE_MODE.DELETE; }); if (l_uploadFiles && l_uploadFiles.length < 1) { setError('fileItems', { type: 'manual', message: '커버이미지는 필수 입력입니다.', }); return false; } let response = null; if (props.data?.no) { response = await request.put(`/picks/${props.data?.no}`, formData); } else { response = await request.post('/picks', formData); } if (response.status === 200) { message.success('처리되었습니다.'); props.onFetch(); } else { context && context.handleError(response); } };
form
<NCheckboxGroup control={control} name="exposureTypes" rules={{ required: '제품구분을 선택해주세요.' }} disabled={false} rowClassName={cx('flex-items')} > {Object.keys(PicksExposureTypes).map((item: any, idx) => ( <Checkbox key={idx} value={item}> {PicksExposureTypes[item].text} </Checkbox> ))} </NCheckboxGroup>
list
const PicksList = forwardRef((props: PicksListProps, ref) => { const request = useRequest(); const [items, setItems] = useState<RecordData[] | null>(null); // Define the type for articleList const paginationRef = useRef<Pagination>({ current: 1, pageSize: 20 }); const filtersRef = useRef<Record<string, any>>({}); const sorterRef = useRef<Sorter | null>(null); // 내부 상태나 메서드를 ref로 노출 useImperativeHandle(ref, () => ({ reset: () => { fetch(); }, })); ... return ( <> ... <Column title="제품구분" dataIndex="exposureType" align="center" width={100} resize={true} filters={[ { text: '고방', value: ExposureType.GOBANG }, ... { text: 'U사장님', value: ExposureType.UCEO }, ]} filteredValue={filtersRef.current.exposureType || null} sorter={true} sortOrder={sorterRef.current?.field === 'exposureType' ? sorterRef.current.order : null} render={(value, record) => { return getExposureTypeNamesByExposureTypeMask(record.exposureType).join(', '); }} /> ... </> ) }
backend
Service layer
@Service @Transactional @RequiredArgsConstructor public class PicksService extends BaseService { private final PicksRepository picksRepository; private final PicksDao picksDao; final private AttachFileService attachFileService; public Page<BaseDto> findAll(PicksListRequestDto picksListRequestDto) { return picksRepository.findAll(picksListRequestDto) .map(PicksListDto::new); } public PicksDetailDto findByNo(Long picksNo) { Optional<Picks> optionalPicksEntity = picksRepository.findById(picksNo); // 엔티티를 DTO로 변환하여 반환 return optionalPicksEntity .map(PicksDetailDto::new) // PicksDetailDto로 변환 .orElse(null); // 또는 예외를 던지거나 원하는 방식으로 처리 } public void registered(PicksSaveDto picksSaveDto) throws IOException, CheckedException { Picks entity = Picks.builder() .exposureType(picksSaveDto.getExposureType()) .picksType(picksSaveDto.getPicksType()) .picksName(picksSaveDto.getPicksName()) .introDesc(picksSaveDto.getIntroDesc()) .locationDesc(picksSaveDto.getLocationDesc()) .fileSeq(picksSaveDto.getFileSeq()) .assortCode(picksSaveDto.getAssortCode()) .regDt(LocalDateTime.now()) .modDt(LocalDateTime.now()) .build(); picksRepository.save(entity); long picksNo = entity.getNo(); Optional<Picks> optionalPicksEntity = picksRepository.findById(picksNo); if (optionalPicksEntity.isPresent()) { // 파일 업로드 Long fileSeq = this.saveFileItem(picksNo, picksSaveDto, picksSaveDto.getFileItems()); Picks attacheFileEntity = optionalPicksEntity.get(); attacheFileEntity.setFileSeq(fileSeq); } } public void modified(Long picksNo, PicksSaveDto picksSaveDto) throws IOException, CheckedException { // 파일 업로드 Long fileSeq = this.saveFileItem(picksNo, picksSaveDto, picksSaveDto.getFileItems()); Optional<Picks> optionalPicksEntity = picksRepository.findById(picksNo); if (optionalPicksEntity.isPresent()) { Picks picksEntity = optionalPicksEntity.get(); picksEntity.setExposureType(picksSaveDto.getExposureType()); picksEntity.setPicksType(picksSaveDto.getPicksType()); picksEntity.setPicksName(picksSaveDto.getPicksName()); picksEntity.setIntroDesc(picksSaveDto.getIntroDesc()); picksEntity.setLocationDesc(picksSaveDto.getLocationDesc()); picksEntity.setFileSeq(fileSeq); picksEntity.setAssortCode(picksSaveDto.getAssortCode()); picksEntity.setModDt(LocalDateTime.now()); // 변경사항을 자동으로 저장 (Transactional에 의해) // picksRepository.save(picksEntity); 필요 없음, JPA가 변경 추적을 통해 자동 저장 } else { throw new EntityNotFoundException("Picks with id " + picksNo + " not found"); } } private Long saveFileItem(Long picksNo, PicksSaveDto picksSaveDto, List<FileItemDto> files) throws IOException, CheckedException { Long fileSeq = null; if (files != null) { String prefixPath = "picks/" + picksNo; fileSeq = attachFileService.fileUploadModifyDto(files, 1, picksSaveDto.getFileSeq(), prefixPath); } return fileSeq; } public void deleted(Long picksNo) { picksRepository.deleteById(picksNo); } ... }
Repository layer
@RequiredArgsConstructor @Repository public class PicksRepositoryImpl implements PicksRepositoryCustom { private final JPAQueryFactory queryFactory; public BooleanExpression matchExposureType(int exposureTypeMask) { return Expressions.numberTemplate(Integer.class, "function('bitand', {0}, {1})", picks.exposureType, exposureTypeMask).ne(0); } public Page<Picks> findAll(PicksListRequestDto picksListRequestDto) { Pageable pageable = QueryDslUtil.getPageable(picksListRequestDto.getPageNo(), picksListRequestDto.getPageSize(), picksListRequestDto.getSort()); // 동적 WHERE 절 구성 BooleanBuilder whereBuilder = new BooleanBuilder(); // 기본 조건 추가 whereBuilder.and(picks.picksType.eq(picksListRequestDto.getPicksType())); ... if (picksListRequestDto.getExposureType() != null) { whereBuilder.and(matchExposureType(picksListRequestDto.getExposureType())); } // 기본 쿼리 작성 JPAQuery<Picks> query = queryFactory .select(picks) .from(picks) .where(whereBuilder) .offset(pageable.getOffset()) // (2) 페이지 번호 .limit(pageable.getPageSize()) // (3) 페이지 사이즈 ; ... List<Picks> content = query.fetch(); ... return new PageImpl<>(content, pageable, count); // (6) PageImpl 반환 } }
아따 빡시네.. 개발 다된거 갑자기 비트마스크에 꽂혀서 이미 콤마로 작업다된거 지우고 변경하는 작업에 하루 다 날려버렸네..
줸장된장말미잘'Language > Java' 카테고리의 다른 글
Batch에서 FCM 발송 오류 트러블슈팅 (2) 2024.09.12 springboot 3.x + JPA + QueryDSL에서 p6spy 적용 (1) 2024.09.05 SpringBoot에서 QueryDSL 설정 및 사용 (0) 2024.08.16 SpringBoot에서 엑셀파일 암호화 후 내려받기 (0) 2024.06.18 Java Enum 적용 (0) 2024.01.15