누르면 조회수 상승(데이터 왜곡 발생) vs 1명의 사용자는 조회수 1만 증가

데이터 형태
create table board_user_read (
board_id int not null,
user_seq int not null,
key board_user_read_fk_02_idx (user_seq),
key board_user_read_fk_01_idx (board_id),
constraint board_user_read_fk_01 foreign key (board_id) references board (board_id),
constraint board_user_read_fk_02 foreign key (user_seq) references users (user_seq)
) ;
dao에 추가
// 조회수 처리
// 현재 사용자가 현재 게시글을 이전에 읽었는지 판단
int countBoardUserRead(BoardParamDto boardParamDto);
// 현재 사용자가 현재 게시글을 읽었음을 기록
int insertBoardUserRead(BoardParamDto boardParamDto);
// 현재 게시글의 조회수 증가
int updateBoardUserRead(int boardId);
mapper에 추가
<!-- 조회수 처리 -->
<select id="countBoardUserRead" parameterType = "com.mycom.myapp.board.dto.BoardParamDto">
select count(*)
from board_user_read
where board_id = #{boardId}
and user_seq = #{userSeq};
</select>
<insert id="insertBoardUserRead" parameterType = "com.mycom.myapp.board.dto.BoardParamDto">
insert into board_user_read( board_id, user_seq)
values (#{boardId}, #{userSeq});
</insert>
<update id="updateBoardUserRead" parameterType = "int">
update board
set read_count = read_count + 1
where board_id = #{boardId};
</update>
조회수 증가는 detail과 같이 작동하니까 service에 구현을 해서 controller가 관리하게 할 필요는 없음
boardServiceImpl 조회수 처리
@Override
public BoardResultDto detailBoard(BoardParamDto boardParamDto) {
BoardResultDto boardResultDto = new BoardResultDto();
try {
// 조회수 처리
int userReadCnt = boardDao.countBoardUserRead(boardParamDto);
if(userReadCnt == 0) { // 현재 사용자가 현재 게시글을 처음 조회하는 경우
// 조회 표시
boardDao.insertBoardUserRead(boardParamDto);
// 게시글 조회수 증가
boardDao.updateBoardUserRead(boardParamDto.getBoardId());
}
// 게시글 조회 코드
return boardResultDto;
}
번외)
어디를 가나 쿼리 잘 작성하는 사람이 필요하다고 함. 테이블 구조를 잘 알아야 함. ex. 휴가 가는 것만 해도 10번 넘게 작동한다고 함. + 기술, 도메인에 대한 비즈니스 개념
비즈니스 로직을 복잡하게 해보기.

ajax 에러가 큰 문제
기술 이름이 아니라, "웹페이지가 새로고침(F5) 없이 서버와 통신하는 기술
mvc로 문제 발생할 경우, 백 코드 자체를 수정해야 함 => 사용을 안하고 spring boot를 사용함
@GetMapping("/pages/login")
public String login() {
// NullPointerException 발생
String s = null;
s.length();
return "login";
}
실행시 다음과 같은 에러 발생
LoginInterceptor Requested URI >> /error
LoginInterceptor >> page request 거절
Spring FrameWork(SF)가 예외 발생했기 때문에 /error forwarding이 반복 => Interceptor 정책 /error 추가
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns(
// 다른 링크들
"/error" // 예외 발생시 Spring FrameWork가 forwarding하는 url 추가
);
}

에러가 반복이 되지 않고 Whitelabel Error Page가 뜸
=> error.jsp 만들어 줌 => Whitelabel Error Page대신 error.jsp로 이동을 함
error.jsp를 jsp의 예외 처리 페이지로 변경을 위해 isErrorPage="true"로 파일 수정
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" isErrorPage="true"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4" crossorigin="anonymous"></script>
<title>error.jsp</title>
</head>
<body>
<h1>error.jsp</h1>
<p> request url : <%= request.getRequestURI() %></p>
<p> error is <%= exception.getMessage() %></p>
</body>
</html>
url을 확인을 해봤는데 error.jsp로 자신임
백엔드 로그에 ERROR가 뜬 것은 SF가 error 자체를 직접적으로 핸들링을 하지 못했다는 것 => 해결 방법 2가지
1. 개별 Controller Handler가 처리 - 방법 2보다 우선 순위는 더 높음(log 찍어보면 알 수 있음)
2. 전역 Handler가 처리
PageController에 ExceptionHandler 추가
// SF 예외 처리자가 처리
// 예외가 발생하면 이 메소드에서 처리
@ExceptionHandler(Exception.class)
public String pageExceptionHandler(Exception ex, Model model, HttpServletRequest request) {
model.addAttribute("exception", ex);
model.addAttribute("requestURI", request.getRequestURI());
return "error";
}
=>
error.jsp가 조금 더 편하게 데이터 가능
request를 통해 프론트가 요청한 url 확인
<!-- @ExceptionHandler로 테스트 -->
<p> request url : <%= request.getAttribute("requestURI") %></p>
<p> error is <%= exception.getMessage() %></p>
백엔드 로그에 WARN이 뜸 - ExceptionHandlerExceptionResolver가 핸들링 함
package com.mycom.myapp.config;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import jakarta.servlet.http.HttpServletRequest;
@ControllerAdvice // 개별 컨트롤러가 처리하지 않는 모든 예외 담당
public class GlobalExcetionHandler {
@ExceptionHandler(Exception.class)
public String exceptionHandler(Exception ex, Model model, HttpServletRequest request) {
model.addAttribute("exception", ex);
model.addAttribute("requestURI", request.getRequestURI());
return "error";
}
}
전역 Handler 추가 => 1번 방식과 동일한 결과
개별 Handler에서 발생할 수 있는 세부적인 에러를 설정하고, 처리가 안된다면, 전역 Handler로 처리하도록 설정
1. try-catch 밖
@Override
public BoardResultDto listBoard(BoardParamDto boardParamDto) {
BoardResultDto boardResultDto = new BoardResultDto();
String s = null;
s.length();
try {
//
}catch(Exception e) {
//
}
return boardResultDto;
}
백엔드 응답에 json 전달
ERROR => handler 추가 => WARN
error.jsp응답(html) 전달 => 프론트는 js로 처리 X
2. try-catch 안
catch 블럭 내용 실행 => 로그 및 응답 정상. 단, result=fail
handler를 넣어도 catch문 실행
// 데이터 요청 대응
@ControllerAdvice // 개별 컨트롤러가 처리하지 않는 모든 예외 담당
@ResponseBody // html 형태 아님을 명시 => json 같은 형태로 사용
public class GlobalExcetionHandler {
@ExceptionHandler(Exception.class)
public Map<String, String> exceptionHandler(Exception ex) {
System.out.println("exceptionHandler - Exception");
Map<String, String> map = new HashMap<>();
map.put("result","fail"); // front와 다양한 대화를 위해 map 형태
return map;
}
}
throws를 넣으면, try-catch가 없는 거처럼 처리됨?
오전에 진행한 detailBoard에 insertBoardUserRead와 updateBoardUserRead사이에 에러를 발생 시킴
insert 작동하고 에러 발생(rollback X)
1번과 동일하게 rollback 안됨
=> SF가 자동으로 트랜잭션 처리를 하지 않는다.
=> 특정 메소드 단위로 트랜잭션 처리 가능 = Spring AOP로 처리
=> @Transactional을 메소드에 선언 => AOP 매니저가 pointcut으로 관리(proxy)
@Transactional
=> rollback 수행함
기본적으로 작동방식은 Controller -> ServiceImpl -> 메소드다.
@Transactional를 붙이면, Controller -> ServiceImpl의 proxy -> 메소드를 호출하게 되는거고 setAutoCommit = false로 설정하기에 문제가 생기면, rollback 작동하는 것이다.
하지만, @Transactional를 붙여도 try-catch문에서 작동을 한다면, rollback이 되지 않는다.
proxy가 만들어졌지만, 인식을 못하기 때문이다.
Controller -> ServiceImpl의 proxy -> 메소드 -> 에러가 발생을 안해서(proxy 안에서 catch로 예외를 잡았기에 밖에서는 에러가 발생이 안되었다고 판단) commit이 되버리게 되는거다.
=> throw를 추가해서 에러가 발생된 걸로 처리하기 -> proxy가 에러가 발생했다고 전달 -> rollback 작동됨
}catch(Exception e) {
e.printStackTrace();
boardResultDto.setResult("fail");
throw new RuntimeException("~~~");
}
정상적인 프론트와 대화를 위해서는 전역 handler가 있어야 함. 프론트에 result:fail을 보내야 하기 때문
=> proxy에게 예외를 전달하는게 아니라 정책을 전달함
}catch(Exception e) {
e.printStackTrace();
boardResultDto.setResult("fail");
// throw new RuntimeException("~~~");
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
이렇게 하면, 전역 handler가 없어도 잘 작동함.
try-catch 미사용 => front에 대한 응답 구성 어려움
try-catch 사용시에는 => TransactionAspectSupport 이용
주의) 클래스 레벨의 @Transactional 사용 지양 = 모든 메소드가 proxy만들어지고 관리 코드가 동작하기 때문
개별 메소드에 @Transactional 방법도 있지만, AOP나 Configuration을 통해 일괄 트랜잭션 통제 가능
조회수 처리 >
예외 처리 >
@ControllerAdvice + @ResponseBody = @RestControllerAdvice
트랜잭션 >
@Transactional(propagation = ...)
private 메소드나 self-invocation(내부 메소드 호출)에는 @Transactional이 (AOP Proxy 문제로) 작동하지 않음 => 별도로 분리하거나 self-injection