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.