페이징 처리에 앞서프로젝트 최적화를 위헤 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)
댓글 남기기