ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 비트마스크 적용 (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 반환
      }
    }

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

    이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

    댓글

Designed by Tistory.