스프링/스프링부트+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)'의 코드 때문이다.

    그렇다면 왜 오류를 일으킬까?
  1. imageEntity는 Image 클래스를 받은 객체이므로 객체를 프린트 하게되면 객체안의 모든 변수들을 getter하게 된다
  2. 모든 변수들이 각각 게터식으로 불러와질때 매핑되어있는 변수들이 무한으로 왔다갔다하게되는 오류가 발생한다
    imageEitnty(Image) → 모든 변수 게터화 그 중 User user 게터 → 모든 변수 게터화 그 중 'List<Image> images' 게터 → 다시 모든 변수 게터화 그중 User user 게터 → 모든 변수 게터화 그 중 'List images' 게터........
  • 객체를 프린터할때에는 자동적으로 toString()이 적용되는데 직접 Image 모델에 만들어보면 아래와 같이 뜬다

@Data가 toString() 기능을 들고 있기 때문

  • 그래도 강제로 만들어보면...
@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;
  1. User타입의 객체 호출
  2. 응답을 하기위해 User 클래스 모든 변수를 Getter 함
  3. 그중 @JsonIgnoreProperties를 가진 변수도 Getter가 됨
  4. 근데 @JsonIgnoreProperties로 인해 그 변수가 가진 user 타입의 변수는 호출 되지 않음
  5. 무한참조가 여기서 블락됨

굿!

 

 

 

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를 쓰는 이유를 다시 한번 상기하자
  1. 뷰 페이지에 자바코드를 많이 자주 넣는것은 좋지 않다
  2. 데이터를 만들어서 그 데이터만 뿌려주면 유지 보수에 탁월하다

→ 전체적으로 코드를 바꿔보자

 

  • 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}">