스프링/스프링부트+JPA - 인스타
스프링부트+JPA - 인스타그램 클론 코딩 07-2 - 프로필(이미지업로드/렌더링)
H-V
2021. 9. 26. 19:31
이지업 최주호 강사님 강의 참조
01 프로필 뷰 렌더링 하기
- profile.jsp에서 유저 프로파일 내용을 '${user.____}'으로 바꾸고 이미지 또한 forEach문으로 받아준다.
<h2>${user.name }</h2>
.
.
.
<c:forEach var="image" items="${user.images}">
<div class="img-box">
<a href=""> <img src="${image.postImageUrl}" />
</a>
<div class="comment">
<a href="#" class=""> <i class="fas fa-heart"></i><span>0</span>
</a>
</div>
</div>
</c:forEach>
→ 여기까지하고 테스트를 해보면 엑박이 뜨는걸 볼 수 있다. 경로가 제대로 잡히지 않아서이다.
- <img src ="">에 경로를 붙이기 위한 세팅
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Value("${file.path}")
private String uploadFolder;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
WebMvcConfigurer.super.addResourceHandlers(registry);
registry
.addResourceHandler("/upload/**")
.addResourceLocations("file:///" + uploadFolder)
.setCachePeriod(60 * 10 * 6).resourceChain(true)
.addResolver(new PathResourceResolver());
}
}
- uploadFolder 변수에 @Value 어노테이션을써서 YML에 정의되어있는 upload폴더 명을 넣음
- addResourceHandlers라는 클래스의 기능을 오버라이드 함
- registry.addResourceHandler를 써서 jsp파일에 "/upload/**" 이 나오면 아래 위치를 찾는 클래스가 발동 됨
- 나머지는 캐시 유지 시간, 발동 유무 등 문법적으로 따라오는 것들
- 다시 upload.jsp 가서 addResourceHandler가 발동하도록 경로 설정
<a href=""> <img src="/upload/${image.postImageUrl}" />
→ '/upload/**' 이 적히게되면 핸들러가 발동되고 경로를 찾아가서 이미지를 들고 오게 됨
- 하나의 파일은 뷰 렌더링이 잘 되지만 하나 이상올리면 500번 에러가 다음과 같이 뜬다
- 이런 오류를 일으키는 코드는 ImageService에 'System.out.println(imageEntity)'의 코드 때문이다.
그렇다면 왜 오류를 일으킬까?
- imageEntity는 Image 클래스를 받은 객체이므로 객체를 프린트 하게되면 객체안의 모든 변수들을 getter하게 된다
- 모든 변수들이 각각 게터식으로 불러와질때 매핑되어있는 변수들이 무한으로 왔다갔다하게되는 오류가 발생한다
imageEitnty(Image) → 모든 변수 게터화 그 중 User user 게터 → 모든 변수 게터화 그 중 'List<Image> images' 게터 → 다시 모든 변수 게터화 그중 User user 게터 → 모든 변수 게터화 그 중 'Listimages' 게터........
- 객체를 프린터할때에는 자동적으로 toString()이 적용되는데 직접 Image 모델에 만들어보면 아래와 같이 뜬다
- 그래도 강제로 만들어보면...
@Override
public String toString() {
return "Image [id=" + id + ", caption=" + caption + ",
postImageUrl=" + postImageUrl + ", user=" + user
+ ", createDate=" + createDate + "]";
}
------>
@Override
public String toString() {
return "Image [id=" + id + ", caption=" + caption + ",
postImageUrl=" + postImageUrl + ", createDate=" + createDate + "]";
}
→ user가 찍히는걸 볼 수 있다. 즉 위에 설명한대로 계속 무한 반복으로 왔다 갔다 되는 것. 여기서 user부분만 지워주면 무한으로 참조되는걸 막을 수 있고, 하나이상부터 이미지가 잘 나오게 된다.
※ JPA에서 무한참조를 정말 조심해야한다. 특히 오브젝트 출력시에는 이런 오류를 잘 생각 해놔야 한다. (컨트롤러에서 return 값으로 객체를 리턴할때도 마찬가지)
→ 그래서 테스트 이후에는 반드시 꼭 쓸데없는 프린터문이나 객체 생성을 지우는 버릇을 들이는게 좋다
02 open in view 개념 잡기
- YML에 세팅값중에 open-in-view라는 값이 있는데 이 값은 fetch 형태와 관련이 있다.
jpa: open-in-view: true hibernate: ddl-auto: update naming: physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl show-sql: true
- 한 요청의 프로세스를 예로 보자 (아래의 그림과 같다)
- 사진에 나와있듯이 어떠한 요청에 대한 응답이 끝나는 시점은 서비스에서 컨트롤러로 넘어갈때인데 fetch를 lazy(지연로딩)로 해버리면 세션종료가 되면 불러올수가 없다. (EAGER전략은 open-in-view 상관없이 가능)
- open-in-view를 true로 설정하게되면 이 세션 종료 시점 + 지연로딩을 VIEW단까지 미룰 수 있게 해준다. 이 점을 꼭 기억하여 fetch 활용 + 객체 호출시에 문제가 없도록 하자
03 회원정보수정 클릭시 오류 수정
- 똑같은 500번 오류가 뜬다 수정해보자. 오류지점을 잘 볼 줄 알아야한다. '회원정보수정'을 눌렀을때 오류가 뜨니 당연히 페이지로 이동시키는 컨트롤러부분이 문제가 있는 것. 또한 여전히 sysout으로 인한 객체 호출때문에 무한참조 오류 500번이 걸리는 것.
04 회원정보수정 완료 버튼시 오류 수정
- 버튼 클릭시 아래 오류가 뜬다
- Http메세지 관련 예외 이니 일반 컨트롤러는 아니다(컨트롤러는 http관련 코드가 없기 때문). UserApiController의 문제이다.
- 아래 코드를 보면 여기도 똑같이 'userEntity'라는 객체를 리턴하면 스프링의 MessageConverter가 자동으로 JSON으로 파싱하여 응답하는데 이때도 똑같이 User의 모든 변수를 GETTER하기 때문이 나오는 오류 (User모델의 Image 가 문제인것)
→ 즉 제이슨으로 파싱을 못하게 하면 해결 된다.
@OneToMany(mappedBy = "user", fetch = FetchType.LAZY)
@JsonIgnoreProperties({"user"})
private List<Image> images;
- User타입의 객체 호출
- 응답을 하기위해 User 클래스 모든 변수를 Getter 함
- 그중 @JsonIgnoreProperties를 가진 변수도 Getter가 됨
- 근데 @JsonIgnoreProperties로 인해 그 변수가 가진 user 타입의 변수는 호출 되지 않음
- 무한참조가 여기서 블락됨
05 게시물 수 / 등록-구독 버튼 수정
- 너무 쉽다. EL 표현식으로 뿌리기만 하면 끝
<li><a href=""> 게시물<span>${user.images.size()}</span>
- 사진등록과 구독하기 버튼은 로그인이 누구인지 상태에따라 구별되서 나와야 한다.
- Dto 작성
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public class UserProfileDto {
private boolean pageOwnerState;
private User user;
}
- Dto 없이 EL의 <c:choose>를 써서 해결이 가능하지만 Dto를 쓰는 이유를 다시 한번 상기하자
- 뷰 페이지에 자바코드를 많이 자주 넣는것은 좋지 않다
- 데이터를 만들어서 그 데이터만 뿌려주면 유지 보수에 탁월하다
→ 전체적으로 코드를 바꿔보자
- UserController (구분을 잘 짓기위해 파라미터명 수정+로그인 세션 할당)
@GetMapping("/user/{pageUserId}")
public String profile(@PathVariable int pageUserId, Model model,
@AuthenticationPrincipal PrincipalDetails principalDetails) {
UserProfileDto dto = userService.profile(pageUserId,principalDetails.getUser().getId());
model.addAttribute("dto", dto);
return "user/profile";
}
- UserService
@Transactional(readOnly = true)
public UserProfileDto profile(int pageUserId, int principalId) {
//SELECT * FROM image WHERE userId = :userId;
UserProfileDto dto = new UserProfileDto();
User userEntity = userRepository.findById(pageUserId).orElseThrow(()->
{
throw new CustomException("해당 프로파일은 없습니다.");
});
dto.setUser(userEntity);
dto.setPageOwnerState(pageUserId == principalId);
return dto;
}
- profile.jsp에 dto 받기
<h2>${dto.user.name }</h2>
<c:when test="${dto.pageOwnerState}">
<button class="cta" onclick="location.href='/image/upload'">사진등록</button>
</c:when>
<c:otherwise>
<button class="cta" onclick="toggleSubscribe(this)">구독하기</button>
</c:otherwise>
</c:choose>
<h4>${dto.user.bio}</h4>
<h4>${dto.user.website }</h4>
<c:forEach var="image" items="${dto.user.images}">
- 추가로 게시물 수도 데이터를 들고와서 뿌리자
→ UserProfileDto
private int imageCount;
→ UserService
dto.setImageCount(userEntity.getImages().size());
→ profile.jsp
<li><a href=""> 게시물<span>${dto.imageCount}</span>
* 프로파일 버튼 수정
- 현재 로그인 후 프로파일 버튼을 누르면 무조건 1번으로 간다. 수정하자 (header.jsp)
<li class="navi-item"><a href="/user/${principal.user.id}">