Spring 숙련 230304 #7 페이징 / 폴더 / @ManyToMany

김춘복·2023년 3월 4일
0

Spring 공부

목록 보기
13/14

페이징

: 프로그램을 한번에 처리할 수 있는 적당한 크기(페이지)로 분할해 페이지 단위로 처리하는 것
데이터 수만개를 한번에 내보내면 부하가 심해지니까

페이징 설계

  • Client -> Server
  1. 페이징
    a. page : 조회할 페이지 번호 (1부터 시작)
    b. size : 한 페이지에 보여줄 상품 개수 (10개로 고정)

  2. 정렬
    a. sortBy(정렬항목)
    ㄱ. id: product table의 id
    ㄴ. title : 상품명
    ㄷ. lprice : 최저가
    b. isAsc (오름차순?)
    ㄱ. true : 오름차순(asc)
    ㄴ. false : 내림차순(desc)

  • 쿼리스트링 예시
    /api/products?sortBy=String&isAsc=boolean&size=int&page=int

  • Server -> Client
    스프링에는 Page 구현체가 존재한다.

    number: 조회된 페이지 번호 (0부터 시작)
    content: 조회된 상품 정보 (배열)
    size: 한 페이지에 보여줄 상품 개수
    numberOfElements: 실제 조회된 상품 개수
    totalElements: 전체 상품 개수 (회원이 등록한 모든 상품의 개수)
    totalPages: 전체 페이지 수 totalElement / size 한 결과를 소수점 올림
    first: 첫 페이지인지? (boolean)
    last: 마지막 페이지인지? (boolean)

  • Page객체는 import org.springframework.data.domain.Page; 임포트 하면 나온다.

구현

  • Controller : page에서 1을 빼준다.
    // 관심 상품 조회하기
    @GetMapping("/products")
    public Page<Product> getProducts(
            @RequestParam("page") int page,
            @RequestParam("size") int size,
            @RequestParam("sortBy") String sortBy,
            @RequestParam("isAsc") boolean isAsc,
            HttpServletRequest request
    ) {
        // 응답 보내기 1페이지가 0번 인덱스이니 page -1을 파라미터로 넣는다(컨트롤러에서만 한번 빼주면 됨)
        return productService.getProducts(request, page-1, size, sortBy, isAsc);
    }
  • Service
    @Transactional(readOnly = true)
    public Page<Product> getProducts(HttpServletRequest request,
                                     int page, int size, String sortBy, boolean isAsc) {
        // 페이징 처리 isAsc가 true면 .ASC false면 .DESC
        Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;
        Sort sort = Sort.by(direction, sortBy);         // 파라미터로 (정렬방향, 정렬기준)
        Pageable pageable = PageRequest.of(page, size, sort);   // pagable 객체를 만들어 페이지 size sort를 넣어준다.

        // Request에서 Token 가져오기
        String token = jwtUtil.resolveToken(request);
        Claims claims;

        // 토큰이 있는 경우에만 관심상품 조회 가능
        if (token != null) {
            // Token 검증
            if (jwtUtil.validateToken(token)) {
                // 토큰에서 사용자 정보 가져오기
                claims = jwtUtil.getUserInfoFromToken(token);
            } else {
                throw new IllegalArgumentException("Token Error");
            }

            // 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
            User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
            );

            // 사용자 권한 가져와서 ADMIN 이면 전체 조회, USER 면 본인이 추가한 부분 조회
            UserRoleEnum userRoleEnum = user.getRole();
            System.out.println("role = " + userRoleEnum);

            Page<Product> products;

            if (userRoleEnum == UserRoleEnum.USER) {
                // 사용자 권한이 USER일 경우
                products = productRepository.findAllByUserId(user.getId(), pageable);
            } else {
                products = productRepository.findAll(pageable);
            }

            return products;

        } else {
            return null;
        }
    }

폴더 설계

: 위의 페이지 기능 만으로는 원하는 관심상품 찾기 힘듬.
폴더 별로 관심상품 저장/관리할 수 있는 기능을 추가.

  • 요구사항
  1. 폴더생성 : 회원별 폴더 추가. 한번에 1~N개 추가 가능

  2. 관심 상품에 폴더 설정 : 관심상품에 폴더 아이콘 추가. N개 설정 가능. 관심상품 등록 시점에서는 폴더 저장x. 관심상품 별로 1번에서 생성한 폴더를 선택하여 추가할 수 있다.

  3. 폴더별 조회 : 회원은 폴더별로 관심상품 조회 가능. 전체 클릭시 폴더와 상관없이 전체 상품 조회. 폴더명 클릭시 폴더별 ㅓㅈ장된 관심상품 조회.

폴더 table 설계

  • 폴더 Table에 필요한 정보
  1. 폴더명 : 회원이 등록한 폴더 이름
  2. 회원ID : 폴더를 등록한 회원의 ID / 회원이 생성한 폴더는 그 회원한테만 보여야한다.
  • 하지만 아직까지 연관관계 설정을 안했기 때문에 폴더의 userId와 실제 user의 Id가 매칭x

JPA 연관관계 이용한 폴더 table 설계

  • 회원 Entity 관점
    회원 1명이 여러 폴더를 가질 수 있다. 따라서 @OneToMany로 설정
public class User {
    @OneToMany
    private List<Folder> folders;
}
  • 폴더 Entity 관점
    폴더 여러개를 회원 1명이 가질 수 있음. 따라서 @ManyToOne으로 설정
public class Folder{
    @ManyToOne
	@JoinColumn(name = "USER_ID", nullable = false)
	private User user;
}

name : 외래키 이름 / nullable : 외래키 null 허용 여부

  • 객체의 연관관계 : 폴더를 소유한 회원 id 가 아닌 객체를 저장

  • DB의 연관관계 : 외래키를 통한 관계 생성


  • @ManyToMany : 실무에서는 잘 안쓴다.
   @ManyToMany
    private List<Folder> folderList = new ArrayList<>();

product 안에서 단방향으로 folderList를 manytomany 일단 설정.
이렇게 되면 JPA가 내가 만들지 않은 PRODUCT_FOLDER_LIST라는 테이블을 알아서 하나 생성한다. 이 중간테이블은 더 넣고싶은 정보가 있어도 수정이 불가능. 오로지 mapping되는 정보만 들어감. 그리고 숨겨져있어서 예상못한 query가 JPA에서 자동으로 나갈 수 있다. 그래서 쓰지 않는게 좋다.

  • 그래서 중간 테이블을 직접 하나 만들어 위의 그림처럼 양쪽테이블에서는 @OneToMany, @OneToMany, 새로 만든 테이블에서는 양쪽을 @ManyToOne @ManyToOne으로 받아줘서 풀어준다.
  • 중복 폴더명 해결 방법.
    .findAllByUserAndNameIn(user, folderNames) : 유저에게 어떤 폴더이름들이 있는지 조회
// 입력으로 들어온 폴더 이름을 기준으로, 회원이 이미 생성한 폴더들을 조회합니다.
List<Folder> existFolderList = folderRepository.findAllByUserAndNameIn(user, folderNames);

List<Folder> folderList = new ArrayList<>();
// 생성자를 통해 폴더이름과 토큰으로 확인한 유저 정보 넣고 폴더를 새로 만들어서 폴더 리스트에 삽입
for (String folderName : folderNames) {
       // 이미 생성한 폴더가 아닌 경우만 폴더 생성
     if (!isExistFolderName(folderName, existFolderList)) {
           Folder folder = new Folder(folderName, user);
           folderList.add(folder);
      }
 }
// 리스트라 saveAll
return folderRepository.saveAll(folderList);

--------------------------------------------------------------------------------
private boolean isExistFolderName(String folderName, List<Folder> existFolderList) {
    // 기존 폴더 리스트에서 folder name 이 있는지?
   for (Folder existFolder : existFolderList) {
       if (existFolder.getName().equals(folderName)) {
             return true;
       }
   }
	return false;
}
profile
꾸준히 성장하기 위해 매일 log를 남깁니다!

0개의 댓글