Language/Java
비트마스크 적용 (QueryDSL & Hibernate 6.x)
건담아빠
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 반환
}
}
아따 빡시네.. 개발 다된거 갑자기 비트마스크에 꽂혀서 이미 콤마로 작업다된거 지우고 변경하는 작업에 하루 다 날려버렸네..
줸장된장말미잘