문제 발생 상황
http://localhost:8080/stores/2/reviews 로 Document.ready로 DOM페이지 로드 완료 시 리뷰 목록을 불러오는 스크립트가 자동으로 실행된다.
해당 페이지에 접근하면 리뷰 목록 불러오기가 바로 실행되는데, 아래 사진과 같이 목록 불러오기 실패로 나타나고있다.
무한재귀는 막았지만 이제 서버에서 아무런 실패에 대한 오류 로그 반응이 없다.
서버의 문제보다는 프론트에서의 문제일 가능성이 높다.
Thymeleaf와의 관계 체크
뷰페이지 랜더링과 타임리프는 관계있을 가능성이 높다. 템플릿의 정확한 위치, 명칭, String인지 등이 중요하다.
타임리프는 HomeController에서 뷰페이지를 반환해야하므로 다음과 같이 구성했다.
HomeController.java
@Controller
@RequiredArgsConstructor
public class HomeController {
private final ReviewService reviewService;
...
// 리뷰 뷰페이지 렌더링
@GetMapping("/stores/{storeId}/reviews")
public String showReviewForm(@PathVariable Long storeId, Model model) {
List<Review> reviews = reviewService.getReviewsByStoreId(storeId);
model.addAttribute("reviews", reviews);
return "add-reviews";
}
}
Java
복사
이제 정상적으로 뷰페이지는 반환하게 복구했다.
이제 아직 남아있는 리뷰 목록이 왜안들어오는지 체크해야 한다.
우선 서버에서의 문제는 뷰페이지를 반환하는 메소드와 실제로 리뷰 목록을 불러오는 메소드가 구분되어있어야한다.
•
뷰페이지를 반환하는 메소드는 HomeController에서 지정한다.
◦
@Controller 로 Thymeleaf 를 통해서 String을 ViewResolver 가 경로를 스캔하게 된다. 이는 resources/templates/index.html 이라는 실제 경로에서 Thymeleaf가 Spring에서는 ViewResolver 역할을 하고 있으며 따라서 String의 반환값인 ‘index’ 만으로도 왼쪽의 루트로부터의 컨텍스트경로, 오른쪽의 .html이라는 확장자까지 붙여서 해석한다.
◦
해석이라기 보다는 사실 Thymeleaf 라이브러리(프레임워크)가 “이런식으로 해라” 라고 양식화되어있고 개발자가 그에 맞추어서 html파일의 명칭을 넣어야하는 것이다. 이것 또한 제어의 역전 IoC의 개념이 적용되고 있다고 봐야 한다. 개발자의 코딩보다, 프레임워크의 제어권에 맞춰야 되는 상황이기 때문이다.
서버에서의 리뷰목록을 불러오는 API 체크
뷰페이지 반환메소드와 별개로 리뷰 목록을 불러오는 백엔드의 API도 체크해야한다.
리뷰와 관련되어 있지만 실제로는 Store에서 리뷰목록을 불러오기 때문에 기능의 역할에 맞도록 가게 컨트롤러에서 목록을 보여주는 API를 작성했다.
StoreController.java
@RestController
@CrossOrigin(origins = "http://localhost:8080") // 로컬 웹 애플리케이션의 도메인을 지정
@RequestMapping("/api")
@RequiredArgsConstructor
public class StoreController {
private final StoreService storeService;
...
private final StoreService storeService;// Read - [select * from reviews where store_id = id]
@GetMapping("/stores/{storeId}/reviews")
public List<Review> getReviewsByStoreId(@PathVariable Long storeId) {
return storeService.getReviewsByStoreId(storeId);
}
}
Java
복사
StoreService.java
@Service
@RequiredArgsConstructor
public class StoreService {
private final StoreRepository storeRepository;
private final ReviewRepository reviewRepository;
private PasswordEncoder passwordEncoder;
...
// 특정 가게 리뷰 모두 조회
public List<Review> getReviewsByStoreId(Long storeId) {
return reviewRepository.findAllByStoreId(storeId);
}
}
Java
복사
여기서 Review를 DB에서 가져오는 로직은 실제론 ReviewRepository에 연관되어 있기 때문에 ReviewRepository에서 JPA Query Method를 작성했다.
@Repository
public interface ReviewRepository extends JpaRepository<Review, Long> {
List<Review> findAllByStoreId(Long storeId);
}
Java
복사
Hibernate:
/* <criteria> */ select
r1_0.id,
r1_0.created_at,
r1_0.modified_at,
r1_0.review,
r1_0.star,
r1_0.store_id,
r1_0.user_id
from
reviews r1_0
where
r1_0.store_id=?
Hibernate:
select
u1_0.id,
u1_0.email,
u1_0.password,
u1_0.point,
u1_0.regist_num,
u1_0.role,
s1_0.id,
s1_0.created_at,
s1_0.information,
s1_0.modified_at,
s1_0.store_address,
s1_0.store_name,
s1_0.user_id,
m1_0.store_id,
m1_0.id,
m1_0.image,
m1_0.menu_name,
m1_0.price,
u1_0.username
from
users u1_0
left join
stores s1_0
on u1_0.id=s1_0.user_id
left join
menus m1_0
on s1_0.id=m1_0.store_id
where
u1_0.id=?
Hibernate:
select
r1_0.store_id,
r1_0.id,
r1_0.created_at,
r1_0.modified_at,
r1_0.review,
r1_0.star,
r1_0.user_id
from
reviews r1_0
where
r1_0.store_id=?
Hibernate:
select
r1_0.store_id,
r1_0.id,
r1_0.created_at,
r1_0.modified_at,
r1_0.review,
r1_0.star,
r1_0.user_id
from
reviews r1_0
where
r1_0.store_id=?
Java
복사
이제 다시 테스트를해보니 두가지 오류가 나타났다.
•
StackOverflowError 이것은 무한루프와 관련되어있다.
•
java.lang.IllegalStateException: Cannot call sendError() after the response has been committed 에러
우선 무한루프부터 해결하고 그 다음 문제를 해결해야겠다.
StackOverflowError 재귀 문제 @JsonIgnore
무한루프가 또 나타난다. 아까 해결된줄 알았지만 뭔가 잘못되었다. 왜냐하면 Review 엔티티는 Store말고도 User와도 양방향 관계를 가지고 있어서 체크해야 할 것 같다.
023-09-21T14:17:59.600+09:00 WARN 8378 --- [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Failure while trying to resolve exception [org.springframework.http.converter.HttpMessageNotWritableException]
java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
at org.apache.catalina.connector.ResponseFacade.checkCommitted(ResponseFacade.java:503) ~[tomcat-embed-core-10.1.12.jar:10.1.12]
at org.apache.catalina.connector.ResponseFacade.sendError(ResponseFacade.java:347) ~[tomcat-embed-core-10.1.12.jar:10.1.12]
at jakarta.servlet.http.HttpServletResponseWrapper.sendError(HttpServletResponseWrapper.java:97) ~[tomcat-embed-core-10.1.12.jar:6.0]
at jakarta.servlet.http.HttpServletResponseWrapper.sendError(HttpServletResponseWrapper.java:97) ~[tomcat-embed-core-10.1.12.jar:6.0]
at jakarta.servlet.http.HttpServletResponseWrapper.sendError(HttpServletResponseWrapper.java:97) ~[tomcat-embed-core-10.1.12.jar:6.0]
at org.springframework.security.web.util.OnCommittedResponseWrapper.sendError(OnCommittedResponseWrapper.java:116) ~[spring-security-web-6.1.3.jar:6.1.3]
at org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver.sendServerError(DefaultHandlerExceptionResolver.java:581) ~[spring-webmvc-6.0.11.jar:6.0.11]
at org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver.handleHttpMessageNotWritable(DefaultHandlerExceptionResolver.java:548) ~[spring-webmvc-6.0.11.jar:6.0.11]
at org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver.doResolveException(DefaultHandlerExceptionResolver.java:221) ~[spring-webmvc-6.0.11.jar:6.0.11]
at org.springframework.web.servlet.handler.AbstractHandlerExceptionResolver.resolveException(AbstractHandlerExceptionResolver.java:141) ~[spring-webmvc-6.0.11.jar:6.0.11]
at org.springframework.web.servlet.handler.HandlerExceptionResolverComposite.resolveException(HandlerExceptionResolverComposite.java:80) ~[spring-webmvc-6.0.11.jar:6.0.11]
at org.springframework.web.servlet.DispatcherServlet.processHandlerException(DispatcherServlet.java:1341) ~[spring-webmvc-6.0.11.jar:6.0.11]
at org.springframework.web.servlet.DispatcherServlet.processDispatchResult(DispatcherServlet.java:1152) ~[spring-webmvc-6.0.11.jar:6.0.11]
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1098) ~[spring-webmvc-6.0.11.jar:6.0.11]
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974) ~[spring-webmvc-6.0.11.jar:6.0.11]
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1011) ~[spring-webmvc-6.0.11.jar:6.0.11]
at org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:903) ~[spring-webmvc-6.0.11.jar:6.0.11]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:564) ~[tomcat-embed-core-10.1.12.jar:6.0]
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885) ~[spring-webmvc-6.0.11.jar:6.0.11]
at jakarta.servlet.http.HttpServlet.service(HttpServlet.java:658) ~[tomcat-embed-core-10.1.12.jar:6.0]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205) ~[tomcat-embed-core-10.1.12.jar:10.1.12]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.12.jar:10.1.12]
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:51) ~[tomcat-embed-websocket-10.1.12.jar:10.1.12]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.12.jar:10.1.12]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.12.jar:10.1.12]
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:110) ~[spring-web-6.0.11.jar:6.0.11]
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174) ~[tomcat-embed-core-10.1.12.jar:10.1.12]
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149) ~[tomcat-embed-core-10.1.12.jar:10.1.12]
Java
복사
이문제와 연관되어 sendError()가 발생하는지 아직 원인을 알 수 없다. 우선 무한루프부터 해결하고 변경사항을 확인해야 한다.
2023-09-21T14:17:59.600+09:00 WARN 8378 --- [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Failure while trying to resolve exception [org.springframework.http.converter.HttpMessageNotWritableException]
java.lang.IllegalStateException: Cannot call sendError() after the response has been committed
at org.apache.catalina.connector.ResponseFacade.checkCommitted(ResponseFacade.java:503) ~[tomcat-embed-core-10.1.12.jar:10.1.12]
at org.apache.catalina.connector.ResponseFacade.sendError(ResponseFacade.java:347) ~[tomcat-embed-core-10.1.12.jar:10.1.12]
at jakarta.servlet.http.HttpServletResponseWrapper.sendError(HttpServletResponseWrapper.java:97) ~[tomcat-embed-core-10.1.12.jar:6.0]
at jakarta.servlet.http.HttpServletResponseWrapper.sendError(HttpServletResponseWrapper.java:97) ~[tomcat-embed-core-10.1.12.jar:6.0]
at jakarta.servlet.http.HttpServletResponseWrapper.sendError(HttpServletResponseWrapper.java:97) ~[tomcat-embed-core-10.1.12.jar:6.0]
at org.springframework.security.web.util.OnCommittedResponseWrapper.sendError(OnCommittedResponseWrapper.java:116) ~[spring-security-web-6.1.3.jar:6.1.3]
at org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver.sendServerError(DefaultHandlerExceptionResolver.java:581) ~[spring-webmvc-6.0.11.jar:6.0.11]
at org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver.handleHttpMessageNotWritable(DefaultHandlerExceptionResolver.java:548) ~[spring-webmvc-6.0.11.jar:6.0.1
Java
복사
Review Entity에서 아까 Store에 대한 재귀 방어를 했지만 User에 대한 @JsonIgnore를 놓쳤다.
Review.java
@Entity @Getter
@NoArgsConstructor
@Table(name = "reviews")
public class Review extends TimeStamped {
...
@JsonIgnore
@JoinColumn(name = "store_id")
@ManyToOne(fetch = FetchType.LAZY)
private Store store;
@JsonIgnore // <- 추가
@JoinColumn(name = "user_id")
@ManyToOne(fetch = FetchType.LAZY)
private User user;
}
Java
복사
또한 User Entity에도 ReviewList에 대한 방향 설정이 없었다.
User.java
@Entity @Getter
@NoArgsConstructor
@Table(name = "users")
public class User {
...
@OneToMany(mappedBy = "user", orphanRemoval = true)
private List<Review> reviews = new ArrayList<>();
...
}
Java
복사
일관성 유지와 팀 컨벤션에 맞추어 add,set보다 orphanRemoval로 데이터 일관성을 관리하고 있다.
이제 테스트를 통해 sendError는 함께 사라졌지만 다른 오류코드로 바뀌었다.
2023-09-21T14:37:45.428+09:00 INFO 9872 --- [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
2023-09-21T14:37:45.436+09:00 INFO 9872 --- [ main] c.s.s.SpringCafeserviceApplication : Started SpringCafeserviceApplication in 4.142 seconds (process running for 4.566)
2023-09-21T14:37:47.168+09:00 INFO 9872 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
2023-09-21T14:37:47.168+09:00 INFO 9872 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2023-09-21T14:37:47.169+09:00 INFO 9872 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
Hibernate:
/* <criteria> */ select
r1_0.id,
r1_0.created_at,
r1_0.modified_at,
r1_0.review,
r1_0.star,
r1_0.store_id,
r1_0.user_id
from
reviews r1_0
where
r1_0.store_id=?
Java
복사
우선 서버에서는 정상적으로 리뷰 테이블로부터 store_id에 따른 select문을 실행하고있다. 서버에서는 문제가 없어보이며 클라이언트로 이동한다.
클라이언트에서는 Type에러가 발생하고있다.
Uncaught TypeError: Cannot read properties of undefined (reading 'username')
at reviews:285:61
at Array.forEach (<anonymous>)
at displayReviews (reviews:283:17)
at Object.<anonymous> (reviews:270:17)
at c (jquery-3.6.0.min.js:2:28327)
at Object.fireWith [as resolveWith] (jquery-3.6.0.min.js:2:29072)
at l (jquery-3.6.0.min.js:2:79901)
at XMLHttpRequest.<anonymous> (jquery-3.6.0.min.js:2:82355)
(anonymous) @ reviews:285
displayReviews @ reviews:283
(anonymous) @ reviews:270
c @ jquery-3.6.0.min.js:2
fireWith @ jquery-3.6.0.min.js:2
l @ jquery-3.6.0.min.js:2
(anonymous) @ jquery-3.6.0.min.js:2
load (async)
send @ jquery-3.6.0.min.js:2
ajax @ jquery-3.6.0.min.js:2
getReviews @ reviews:263
(anonymous) @ reviews:296
e @ jquery-3.6.0.min.js:2
t @ jquery-3.6.0.min.js:2
setTimeout (async)
(anonymous) @ jquery-3.6.0.min.js:2
c @ jquery-3.6.0.min.js:2
fireWith @ jquery-3.6.0.min.js:2
fire @ jquery-3.6.0.min.js:2
c @ jquery-3.6.0.min.js:2
fireWith @ jquery-3.6.0.min.js:2
ready @ jquery-3.6.0.min.js:2
B @ jquery-3.6.0.min.js:2
Java
복사
클라이언트 측에서 발생하는 오류는 JavaScript 코드에서 'username' 속성을 찾을 수 없다는 내용으로 보여지며 어딘가 username이란것을 사용하고 있는 것 같다. JavaScript중에 있을 가능성이 높다.
서버에서는 해당 URL 응답으로 ResponseDto라는 객체를 보내는데 반환은 아래와 같이 username은 안보내주고있다. 아니 내가 필요가 없었기 때문에 이것만 반환하고 있던 것이다.
@Getter
@NoArgsConstructor
public class ReviewResponseDto {
private byte star;
private String review;
public ReviewResponseDto(Review updatedReview) {
this.star = updatedReview.getStar();
this.review = updatedReview.getReview();
}
}
Java
복사
실제로 서버에서의 반환은 star, review만 응답하게 되는데,
프론트의 JavaScript 요청에 따른 응답에서 append하는 부분에 JSON 응답 키 중에 username을 호출하고 있었다.
// 리뷰 목록을 화면에 표시
function displayReviews(reviews) {
const reviewContainer = $('.review-list'); // 리뷰 목록을 나타낼 컨테이너
reviewContainer.empty(); // 기존 리뷰를 지우고 새로운 리뷰 목록으로 대체
// 리뷰 목록을 반복해서 화면에 추가
reviews.forEach(function (review) {
const reviewDiv = $('<div class="review">');
const username = $('<strong>').text(review.user.username);
const reviewText = $('<p>').text(review.review);
reviewDiv.append(username);
reviewDiv.append(reviewText);
reviewContainer.append(reviewDiv);
});
}
Java
복사
나는 별점과 리뷰내용만을 원하기 때문에 서버를 변경하기 보다, 프론트엔드의 append 항목들을 star, review만으로 수정했다.
<script>
function getReviews() {
const storeId = $('#dynamicStoreId').val();
$.ajax({
type: "GET",
url: '/api/stores/' + storeId + '/reviews', // 수정된 URL 경로
dataType: "json"
})
.done(function (reviews) {
// 리뷰 목록을 성공적으로 가져온 경우
displayReviews(reviews);
})
.fail(function (error) {
alert('리뷰 목록을 가져오는데 실패했습니다.');
});
}
// 리뷰 목록을 화면에 표시
function displayReviews(reviews) {
const reviewContainer = $('.review-list'); // 리뷰 목록을 나타낼 컨테이너
reviewContainer.empty(); // 기존 리뷰를 지우고 새로운 리뷰 목록으로 대체
// 리뷰 목록을 반복해서 화면에 추가
reviews.forEach(function (review) {
const reviewDiv = $('<div class="review">');
const star = $('<strong>').text('별점: ' + review.star); // 별점 출력
const reviewText = $('<p>').text('리뷰: ' + review.review); // 리뷰 내용 출력
reviewDiv.append(star);
reviewDiv.append(reviewText);
reviewContainer.append(reviewDiv);
});
}
// 페이지 로딩 시 리뷰 목록 가져오기
$(document).ready(function () {
getReviews();
});
Java
복사
이에 따라 정상적으로 작동되도록 모두 수정이 완료되었다. JS전체 코드는 다음과 같다. document.ready를 통해 해당 페이지에 접근하면 리뷰목록자체가 모두 등장한다.
마지막으로 아무리그래도 양식을 맞추고자 간단하게 append 형식을 기존 css를 활용하고자 추가 꾸미기?를 들어간다.
가게 id를 보여주었던 히든 필드를 다시 히든 타입으로 변경해준다. 뒷처리는 깔끔하게!
그리고 Collaps 기능도 다시 달아주고, 로그인 상태에서만 작성할수있도록 토큰을 확인하는 로직을 추가하고, 로그인 안된 상태에서는 로그인필요하다는 문구와 함께 …/login 으로 리다이렉트 시켜주었다.