페이징 처리에 앞서프로젝트 최적화를 위헤 open-in-view: false 는 기본적으로 false 놓아야 하며, 지연로딩 전략을 사용한다.
@ManyToOne(fetch = FetchType.LAZY)
지연 전략시 다음과 같은 FetchType이 Lazy
[JPA] Lazy 로딩으로 인한 JSON 반환 오류 (No serializer found for class 가 나온다
org.hibernate.proxy.pojo.bytebuddy.ByteBuddyInterceptor and no properties discovered to create BeanSerializer)
따라서.
application.yml 다음과 같이 설정
spring jackson: serialization: fail-on-empty-beans: false
1. PageableCustom 생성
import org.springframework.data.domain.Page; import org.springframework.data.domain.Slice; import lombok.Getter; @Getter public class PageableCustom { private boolean first; private boolean last; private boolean hasNext; private int totalPages; private long totalElements; private int page; private int size; public PageableCustom() { } public PageableCustom(Page<?> page) { this.first = page.isFirst(); this.last = page.isLast(); this.hasNext = page.hasNext(); this.totalPages = page.getTotalPages(); this.totalElements = page.getTotalElements(); this.page = page.getNumber() + 1; this.size = page.getSize(); } public PageableCustom(Slice<?> slice) { this.first = slice.isFirst(); this.last = slice.isLast(); this.hasNext = slice.hasNext(); this.page = slice.getNumber() + 1; this.size = slice.getSize(); } }
2.PageCustom 생성
import lombok.Getter; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.SliceImpl; import java.io.Serializable; import java.util.List; @Getter public class PageCustom<T> implements Serializable { private static final long serialVersionUID = 1L; private List<T> content; private PageableCustom pageableCustom; public PageCustom(List<T> content, Pageable pageable, Long total) { this.content = content; this.pageableCustom = new PageableCustom(new PageImpl<T>(content, pageable, total)); } public PageCustom(List<T> content, Pageable pageable, boolean hasNext) { this.content = content; this.pageableCustom = new PageableCustom(new SliceImpl<T>(content, pageable, hasNext)); } }
================================================= =================================================
3. 컨트롤 처리 예
import org.springframework.data.domain.Pageable; import org.springframework.data.web.PageableDefault; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import com.cos.photogramstart.config.auth.PrincipalDetails; import com.cos.photogramstart.domain.image.ImageResDTO; import com.cos.photogramstart.service.ImageService; import com.cos.photogramstart.utils.PageCustom; import com.cos.photogramstart.web.dto.CMRespDto; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @RestController public class ImageApiController { private final ImageService imageService; @GetMapping("/api/image") public ResponseEntity<?> imageStory(@AuthenticationPrincipal PrincipalDetails principalDetails, @PageableDefault(size=3) Pageable pageable){ PageCustom<ImageResDTO> images= imageService.이미지스토리(principalDetails.getUser().getId(), pageable); return ResponseEntity.status(HttpStatus.OK).body(new CMRespDto<>(1,"성공", images)); } }
4.서비스
Image 엔티티를 반환처리하면 안 좋으므로 다음과 같이 ImageResDTO 커스텀 객체를 만든후 페이징 처리 한다.
Page<Image> 로 반환시킨후 커스텀 List<ImageResDTO> 에 데이터를 추가한후 PageCustom 반환 처리하면 된다.
import java.io.File; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.UUID; import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.cos.photogramstart.config.auth.PrincipalDetails; import com.cos.photogramstart.domain.image.Image; import com.cos.photogramstart.domain.image.ImageRepository; import com.cos.photogramstart.domain.image.ImageResDTO; import com.cos.photogramstart.utils.PageCustom; import com.cos.photogramstart.web.dto.image.ImageUploadDto; import lombok.RequiredArgsConstructor; import lombok.extern.log4j.Log4j2; @Service @RequiredArgsConstructor @Log4j2 public class ImageService { private final ImageRepository imageRepository; @Transactional(readOnly = true) //영속성 컨텍스트 변경 감지를 해서, 더티체킹, flush 반영 x public PageCustom<ImageResDTO> 이미지스토리(long principalId, Pageable pageable){ /** 접속자만 구독 목록 가져오기 */ List<Long> ids =imageRepository.mySubscribes(principalId); Page<Image> pageImages=imageRepository.mStroy(ids, pageable); //Eager 전략일때는 영속성에서 데이터를 가져오지만 의 지연 전략으로 (FetchType.LAZY ) 인하여 user 객체 가져와야 한다. List<ImageResDTO> imageResDTOs=new ArrayList<ImageResDTO>(); for(Image image: pageImages) { ImageResDTO imageResDTO=image.toImageResDTO(); imageResDTO.setUser(userRepository.findById(image.getUser().getId()).orElse(null)); imageResDTOs.add(imageResDTO); } return new PageCustom<ImageResDTO>(imageResDTOs, pageImages.getPageable(), pageImages.getTotalElements()); } }
5. ImageRepository
import java.util.List; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; public interface ImageRepository extends JpaRepository<Image, Long> { @Query(value="select * from Image where userId = :userId" ,nativeQuery = true ) List<Image> findAllByUserImages(@Param(value = "userId") long userId); //@Query(value="SELECT * FROM image WHERE userid IN (SELECT toUserId FROM Subscribe WHERE fromUserId =:principalId) ORDER BY id DESC ", nativeQuery = true) /** * JPQL In 절 사용하기 * @param ids * @param pageable * @return */ @Query(value="SELECT i FROM Image i JOIN i.user u WHERE u.id in (:ids) ") Page<Image> mStroy(@Param(value = "ids") List<Long> ids, Pageable pageable); /** * 접속자의 구독목록 아이디만 가져오기 * @param principalId * @return */ @Query(value="SELECT t.id FROM Subscribe s JOIN s.fromUser u JOIN s.toUser t WHERE u.id =:principalId ") List<Long> mySubscribes(long principalId); }
엔티티
Image
import java.time.LocalDateTime; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.PrePersist; import com.cos.photogramstart.domain.user.User; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import lombok.ToString; @Builder @AllArgsConstructor @NoArgsConstructor @Data @Entity @ToString(exclude = "user") public class Image { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; private String caption; private String originalFilename; private String postImageUrl; @JoinColumn(name="userId") @ManyToOne(fetch = FetchType.LAZY) private User user; private LocalDateTime createDate; @PrePersist public void createDate() { this.createDate=LocalDateTime.now(); } public ImageResDTO toImageResDTO() { return ImageResDTO.builder() .id(id) .caption(caption) .originalFilename(originalFilename) .postImageUrl(postImageUrl) .user(user) .userId(user.getId()) .createDate(createDate) .build(); } }
ImageResDTO
@JsonIgnoreProperties({"images"}) 설정해 줘야 하는데 다음과 같은 영속성 오류가 발생하기 때문이다.
"trace": "org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: failed to lazily initialize a collection of role: com.cos.photogramstart.domain.user.User.images, could not initialize proxy - no Session; nested exception is com.fasterxml.jackson.databind.JsonMappingException: failed to lazily initialize
유저 객체를 불러올때 다시 image 객체를 불러오는 반복된처처로 could not initialize proxy - no Session; 라는 것이다.
따라서, json 반환시에는 user 에서 images 를 무시하는 처리로 @JsonIgnoreProperties 처리를 해준다.
import java.time.LocalDateTime; import com.cos.photogramstart.domain.user.User; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; import lombok.ToString; @Builder @AllArgsConstructor @NoArgsConstructor @Data @ToString(exclude = "user") public class ImageResDTO { private long id; private String caption; private String originalFilename; private String postImageUrl; @JsonIgnoreProperties({"images"}) private User user; private long userId; private LocalDateTime createDate; }
User
import java.time.LocalDateTime; import java.util.List; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.FetchType; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.OneToMany; import javax.persistence.PrePersist; import com.cos.photogramstart.domain.image.Image; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import lombok.ToString; @ToString(exclude = "images") @Builder @AllArgsConstructor @NoArgsConstructor @Getter @Setter @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long id; @Column(nullable = false, length=20, unique = true) private String username; @Column(nullable = false) private String password; private String name; private String website; //웹 사이트 private String bio; //자기소개 @Column(unique = true, nullable = false) private String email; private String phone; private String gender; private String profileImageUrl; //사진 private String role; //권한 @OneToMany(mappedBy = "user", fetch = FetchType.LAZY) private List<Image> images; //양방향 매핑 private LocalDateTime createDate; @PrePersist // 디비에 Insert 되기 직전에 실행 public void createDate() { this.createDate=LocalDateTime.now(); } }
결과
{ "code": 1, "message": "성공", "data": { "content": [ { "id": 12, "caption": "홍길동 ", "originalFilename": "2312211906whiuzletyv_0.jpg", "postImageUrl": "1cdb123e-db89-42f7-b96a-44e80d14da43_2312211906whiuzletyv_0.jpg", "user": { "id": 5, "username": "ssar", "password": "$2a$10$.NK35QvezqQoh3uvE5S0G.AmtILUd6upEcu4MUi0jm03z6fsW0Vry", "name": "홍길동", "website": null, "bio": null, "email": "ssar@gmail.com", "phone": null, "gender": null, "profileImageUrl": null, "role": "ROLE_USER", "createDate": "2023-12-22T17:38:56.687979" }, "userId": 5, "createDate": "2023-12-23T14:15:03.274058" }, { "id": 11, "caption": "333", "originalFilename": "2312200136ptuoqyzasv_0.jpg", "postImageUrl": "87cc4ed0-de8c-485f-8b02-61b95bc0961a_2312200136ptuoqyzasv_0.jpg", "user": { "id": 3, "username": "test3", "password": "$2a$10$qWtJO89AcvZKNFCg97dJN.AqbLmuc6two.D4s/knafkI5GoLxqoAq", "name": "김민종", "website": null, "bio": null, "email": "test3@gmail.com", "phone": null, "gender": null, "profileImageUrl": null, "role": "ROLE_USER", "createDate": "2023-12-20T19:58:47.300206" }, "userId": 3, "createDate": "2023-12-22T19:49:10.005323" }, { "id": 10, "caption": "333", "originalFilename": "2312200136pqlhjryrdw_0.jpg", "postImageUrl": "17e59775-9854-4d85-b525-0f18b7ad1701_2312200136pqlhjryrdw_0.jpg", "user": { "id": 3, "username": "test3", "password": "$2a$10$qWtJO89AcvZKNFCg97dJN.AqbLmuc6two.D4s/knafkI5GoLxqoAq", "name": "김민종", "website": null, "bio": null, "email": "test3@gmail.com", "phone": null, "gender": null, "profileImageUrl": null, "role": "ROLE_USER", "createDate": "2023-12-20T19:58:47.300206" }, "userId": 3, "createDate": "2023-12-22T19:49:01.636644" } ], "pageableCustom": { "first": true, "last": false, "hasNext": true, "totalPages": 2, "totalElements": 5, "page": 1, "size": 3 } } }
페이지 번호는 0 부
http://localhost:8080/api/image?page=0
http://localhost:8080/api/image?page=1
http://localhost:8080/api/image?page=2
소스
https://github.com/braverokmc79/EaszUp-Springboot-Photogram-Start
https://github.dev/braverokmc79/EaszUp-Springboot-Photogram-Start
댓글 ( 4)
댓글 남기기