프론트에서는 정상적으로 리뷰 수정에 대한 필드값인 star, review를 넘겨주고있다.
프론트에서의 해당 값을 서버 API로 전송하는 JavaScript코드는 다음과 같다.
<script>
// 리뷰 수정 버튼 클릭 이벤트 핸들러
function submitReviewModify() {
// 수정할 리뷰의 정보 가져오기
const reviewId = $('#reviewId').val();
const modifiedRating = $('#ratings-hidden-modify').val();
alert(modifiedRating)
const modifiedReview = $('#reviewTextarea').val();
// Ajax 요청을 보낼 URL 설정
const url = '/api/reviews/' + reviewId;
// 서버로 보낼 데이터
const data = {
star: modifiedRating,
review: modifiedReview
};
alert(JSON.stringify(data));
// 쿠키에서 JWT 토큰을 추출합니다.
const jwtToken = getCookie("Authorization");
// Ajax 요청 설정
$.ajax({
type: "PUT",
url: url,
data: JSON.stringify(data), // 데이터를 JSON 문자열로 변환
contentType: 'application/json', // 요청의 컨텐츠 타입을 JSON으로 설정
headers: {
"Authorization": jwtToken // JWT 토큰을 "Bearer" 스키마와 함께 추가
},
success: function () {
// 수정 요청이 성공한 경우
// 모달 창을 닫고 페이지를 다시 로드하여 수정 내용을 업데이트
alert('리뷰 수정에 성공했습니다.');
$('#reviewModal').modal('hide');
window.location.reload(); // 현재 페이지 리로드
},
error: function () {
// 수정 요청이 실패한 경우만
alert('작성자만 수정 가능합니다. 로그인을 진행해주세요.');
window.location.href = '/login';
}
});
}
</script>
JavaScript
복사
서버에서는 ReviewController가 RequestDto로 해당 값들을 받아오고 있다.
// 리뷰 수정
@PutMapping("/reviews/{id}")
public ResponseEntity<StatusResponseDto> updateReview(@PathVariable Long id, @RequestBody ReviewRequestDto requestDto,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
logger.info("Received JSON data: {}", requestDto); // <- 로그 추가
return reviewService.updateReview(id, requestDto, userDetails.getUser());
}
JavaScript
복사
해당 수정 요청이 들어올때 Hibernate 또한 콘솔에서 업데이트 로그를 정상적으로 기록하고 있다.
Hibernate:
/* update
for com.meta.springcafeservice.entity.Review */update reviews
set
modified_at=?,
review=?,
star=?,
store_id=?,
user_id=?
where
id=?
Java
복사
문제는 review는 업데이트되는데, star별점만 업데이트가 되지 않는다. 이를 위해서 서버단에서 해당 JSON 데이터가 RequestDto로 전달 될 때 어떤 형태인지 확인하기 위해서 @Slf4j 로 데이터 형태를 살펴보고자 한다. 기본적으로 위 코드를 String으로 변환하여 로그를 찍어보면
2023-09-22T12:08:35.621+09:00 INFO 94514 --- [nio-8080-exec-3] c.s.s.controller.ReviewController : Received JSON data: com.sparta.springcafeservice.dto.ReviewRequestDto@702a0504 다음과 같은 주소값만 알게되서 확인 하기 어렵다. 내부 값을 보기 위해서 ReviewRequestDto에서 Json을 원하는 형태로 볼 수 있도록 @Override로 toString()에 대한 메소드를 재정의하게 되었다.
@Getter
public class ReviewRequestDto {
private byte star;
private String review;
private Long storeId;
@Override
public String toString() {
return "ReviewRequestDto{" +
"star='" + star + '\'' +
", review='" + review + '\'' +
'}';
}
}
Java
복사
이와 같이 JSON파일을 로그로 찍어보는데 toString()메소드를 재정의하여 실제 내부 값들을 확인하는 로그를 살펴보니
2023-09-22T12:18:25.022+09:00 INFO 95258 --- [nio-8080-exec-1]
c.s.s.controller.ReviewController :
Received JSON data:
ReviewRequestDto{star='4', review='asdfasdfaㅁㅁㅁㅁㅁㅁㅁㅁㅁ'}
Java
복사
다음과 같이 정상적으로 RequestDto 부분까지 star=4라는 값을 가지고 있다. 다음 흐름으로 넘어간다.
다음은 위처럼 정상적인 RequestDto객체는 reviewService의 updateReview() 메소드의 매개변수로 들어가게 된다.
// 리뷰 수정
@PutMapping("/reviews/{id}")
public ResponseEntity<StatusResponseDto> updateReview(@PathVariable Long id, @RequestBody ReviewRequestDto requestDto,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
logger.info("Received JSON data: {}", requestDto); // <- 로그 추가
return reviewService.updateReview(id, requestDto, userDetails.getUser());
}
JavaScript
복사
return 부분에서 updateReview가 호출되는데 그곳으로 Dto객체가 들어간다.
ReviewService의 updateReview()메소드는 다음과 같으며 해당 부분에서 객체의 상태를 살펴보았다.
// 리뷰 수정
@Transactional
public ResponseEntity<StatusResponseDto> updateReview(Long id, ReviewRequestDto requestDto, User user) {
return handleServiceRequest(() -> {
Review review = checkReviewExist(id);
validateUserAuthority(user.getId(), review.getUser());
review.update(requestDto);
return new StatusResponseDto("리뷰가 수정되었습니다.", 200);
});
}
Java
복사
정상적으로 requestDto가 매개변수로 입력되있으며 정상적인 Dto객체가 넘어왔을 것으로 예측된다. 로그를 통해 확인해본다.
2023-09-22T12:24:04.019+09:00 INFO 95669 --- [nio-8080-exec-1]
c.s.s.controller.ReviewController :
Deliverd DTO Object:
ReviewRequestDto{star='4', review='asdfasdfaㅁㅁㅁㅁㅁㅁㅁㅁㅁ'}
Java
복사
여기서도 정상적으로 star는 4를 전달받고 있다. 다음으로는 review.update()를 통해 정상적인 Dto객체가 update() 로직을 수행하게 된다. 해당 부분으로 이동해본다.
public void update(ReviewRequestDto requestDto) {
this.review = requestDto.getReview();
}
Java
복사
requestDto객체는 Review엔티티에서 update 메소드의 매개변수로 정상적인 객체 requestDto를 전달받는다. 현재 위 코드에서는 requestDto객체를 통해 getReview()로 review라는 리뷰내용을 엔티티의 review라는 필드에 값을 오버랩하고 있다.
하지만 star를 얻어오지도, this.star에 오버랩하지도 않고 있다.
올바르게 별점도 업데이트 하기 위해서는 아래와같이 수정해주었다.
@Entity @Getter
@NoArgsConstructor
@Table(name = "reviews")
public class Review extends TimeStamped {
...
@Column(nullable = false, columnDefinition = "TEXT")
private String review;
@Column(nullable = false)
private byte star;
...
public void update(ReviewRequestDto requestDto) {
this.review = requestDto.getReview();
this.star = requestDto.getStar();
}
Java
복사
이러면 정상적으로 엔티티에 업데이트된 star별점이 저장될 것이다. 이제 올바르게 수정에 대해서 동적으로 별점의 갯수도 변경되는 것으로 문제는 해결되었다.
마지막 부분에 대한 트랜잭션 환경에 대한 원리 파악
JPA에서 영속성 컨텍스트 관리를 통해 데이터베이스와의 Dirty Checking이 진행된다. DB의 해당 리뷰의 row를 꺼내오고 위 persistence context에 올려진 위 entity 객체를 비교하여 차이점을 찾아낸다. 이후 변동 사항에 대해서 Queue Stack에서 대기하던 update문이 commit을 통해 한번에 업데이트문을 DB에 실행 시키며 업데이트를 진행할 것이다.
이는 리뷰 수정에서 review.update(requestDto)라는 메소드 호출부가 포함된 updateReivew() 메소드 자체가 @Transactional 어노테이션으로 트랜잭션 환경으로 관리되고 있기 때문에 이와 같이 엔티티 객체를 수정하고 JPA 트랜잭션을 커밋하는 순간 변동사항을 업데이트 하는 방식으로 작동한다.
문제 해결을 통해 느낀점
1.
이번 문제는 처음에 API를 개발 할 때, 이 부분에 대해서 테스트가 부족했다. 리뷰의 메시지만 변경되는 것을 확인해서 별점까지 확인해보는 단위 테스트가 부족했던 점이 현재 프론트까지 연결하는 급한 시점에서 다시 서버사이드를 체크해봐야하는 문제가 발생했다. 이를 통해 테스트는 실제로 여러 필드를 업데이트하거나 추가하는 API일수록 각 부분에 대한 정밀한 테스트가 필요 할 수도 있겠다고 생각했다.
2.
해당 이슈를 해결하면서, 내 레벨에서는 생각보다 오래걸릴 가능성이 있었던 문제였다. 그 이유는 콘솔에 아무런 오류가 나타나지 않기 때문에, 정상적으로 업데이트가 진행되기 때문에 오히려 오류에 대한 추적할 근거가 부족했다. IDE 조차도 빌더 자체도 “누락”에 대한것은 알기 어렵다. 개발자가 의도한 것인줄 알기 때문이고 알 필요도 없기 떄문이다. 이것은 개발 시점에서의 “누락” 에 대한 문제기 때문에 규모가 아주 커지는 프로젝트에서는 찾기가 어려울 수 있다는 생각이 들었다.
3.
하지만 어떻게보면 이런 오류를 어떤 광범위한 정보에 의존하지 않고 직접 근거를 추적하며 해결하고 생각하면서 시도한 첫번째 슈팅이 아닐까 생각이 들었다. 현재 이 문제는 프론트-ajax요청-컨트롤러-서비스-리포지토리-db까지 연결되는 데이터의 이동 흐름을 알고 있어야 추적 할 수 있다. 또한 그 이동 사이에 데이터가 어떤 형태로 타입으로 이동되는지도 파악되어야 하며 그 부분은 어디서 변환되는지(예로 requestDto라는 클래스에서 객체로 변화했다는 부분을 인지해야 하는것처럼) 로그를 추적할 대상을 정확하게 짚어내야 했다.
4.
기본적으로 Hibernate의 쿼리를 보면서 업데이트 자체는 진행되고 있다는 단서를 얻게 되면서 보다 데이터 흐름에 따라 이 오류를 잡아야겠다는 생각을 했었다. 이를 위해 사용했던 프론트에서는 간단하게 alert를 통해 데이터를 살펴보았고, LoggerFactory로 Controller 계층부터 점진적으로 객체를 추적하는 것을 실제로 해보며 그 데이터가 어디서 잘못 변화 할 지 유심히 살펴보게 되었다.
5.
예전에는 이러한 문제가 발생하면 구글링, GPT등 너무 의존적인 방법을 사용했었다. 하지만 개발자로 성장해나가면서 앞으로 이런 문제가 발생하면 어느 시점, 어떤 객체, 어떤 로직에서 문제일지 개발자스러운 접근 방법을 시도해보고 싶었고, 이번엔 아주 단순한 트러블이었지만 오류를 찾아나가는 과정 자체를 즐기면서 할 수 있었다.