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 반환
  }
}

 
 
아따 빡시네.. 개발 다된거 갑자기 비트마스크에 꽂혀서 이미 콤마로 작업다된거 지우고 변경하는 작업에 하루 다 날려버렸네..
줸장된장말미잘