Contents
1. 에러 관리1.1 에러 관련 시작1.1 error폴더에 ex폴더 만들자1.2 클래스 파일 만들기1.3 Exception400 클래스 만들어 주기1.4 또 클래스 만들기1.5 400 으로 돌아감1.6 클래스 쭉 만들기 전부 복사함1.7 유틸로 이동1.8 다시 글로벌익셉션 핸들러 클래스로 이동1.9 보드 레파지토리 이동2. 리팩토링 유저부터 시작2.1 유저 서비스클래스 만들자2.2 유저 레파지토리 이동이후 테스트 하기2.3 유저 서비스로 이동2.4 유저 리포지 토리 이동2.5 유저 테스트로 가서 한번 더 확인2.6 유저 서비스로 이동2.7 로그인메서드 다시이동2.8 유저 리포지토리2.9 유저 서비스로 이동2.10 유저 컨트롤러 이동실행 해보자보드쪽도 고쳐야 함2.11 보드 서비스 이동2.12 이것들 컨트롤러 리팩토링 해주자2.14 다시 컨트롤러2.15 보드 서비스로 이동2.16 다시 보드 컨트롤러2.17 업데이트 전환2.18 보드 서비스 클래스 이동2.19 컨트롤러 이동 수정2.20 글 수정하기 버튼 하자2.21 보드 서비스로 이동2.22 보드 리퀘스트 가서다시 보드 컨트롤러 이동2.23 보드 서비스 있어야 해서 다시 서비스로 이동테스트 2.24 컨트롤러 이동1. 에러 관리
1.1 에러 관련 시작

패키지 만들고




클래스 만들기

@RestControllerAdvice
@ExceptionHandler, @ModelAttribute, @InitBinder가 적용된 메서드들에 AOP를 적용해 Controller단에 적용하기 위하 고안된 어노테이션입니다.
클래스에 선언하면 되고 @RestController에 대한 전역적으로 발생하는 예외를 처리할 수 있습니다.
모든 쓰로우는 이 친구한테 날라간다
어드바이스라는 애가 결국 ds로 모이는데 이걸 쥐고 있다가 떤진다는 것임
controllerAdvice 파일 던지고
rest는 데이터 던짐(자바 스크립트 코드를 던질거임)

구분 지어서 타입만 같으면 쭉 올라감
터트리면 보드 레파지토리에 Runtime(게시글
+rn 없다? 리플래이스 하면 되니까

히스토리 백 할거임

연습하려면 testBoard만든거 Service에서 BoardService에서
throw new RuntimeException(”안녕”)
1.1 error폴더에 ex폴더 만들자

번외
회사는 모든 예외를 다 만든다.
상수로 적을까? 아니다 다른걸로 관리한다
리플래시 토큰? 에?
다 적어두면 장점은 GPT한테 한글로 바꿔줘 하면 영어버전, 한글버전 문자열로 할 수 있다 무조건 변수로 바꿔서 사용해야 한다.
컨버팅이 가능하다 상수로는 앱이나 서버에 있으면 보안적으로 안 좋다
- 우리는 딱 5개만 만들거다
1.2 클래스 파일 만들기

예외 설명
10X
기다려
20X 201은 created → insert잘 됐다 그래서 200으로 퉁쳐서 지금은 사용할 거임
성공
30X
딴걸 돌려줄게
회원가입 요청했으니까 메인 페이지 돌려주면 따른거 돌려주는 것
40X
클라이언트의 잘못(요청자의 잘못)
50X
서버의 잘못(예상치 못한 예외가 발생했다는 거임)
500번 때 터지는 것은 에러로그를 남겨야 한다
왜 남겨야 할까?
계속 이 서버를 눈으로 보면서 운영할 게 아니니까 (놀러 갔을 때 고객센터에서 전화가 왔어 안된다 해서 나는 로고가 없으니까 해줄 수 있는게 없다) Db등 어디든 적어둬야 한다 (상황을 알아야 수정이 가능하다) 오류 로고를 봐야 한다 , 그리고 오류 로고를 봐야 한다 이것 까지 봐야 디버깅이 가능하다
뭐하다가? 이거 이거 하다가요? 오류 로고 를 보여줘야 함!
전부 다 서버에 기록해야 한다!!! sout말고 에러 로고 남기는 게 따로 있다!!
1.3 Exception400 클래스 만들어 주기
- RuntimeException 상속하자!!
런타임 익셉션 걸리면 400 클래스도 걸린다
실행중에 터지는 거는 전부 runtime예외다 한 곳에 모일거다
→ 유효성 검사할 때 터트릴 것이다

1.4 또 클래스 만들기

401 인증관련 → 인증 안됐을 때 터트림
1.5 400 으로 돌아감
생성자 만들기


전체적인 구조
부모의 생성자로 감
- RuntimeException

- RuntimeException의 부모인 Exception

- Exception의 부모인 Throwable


이거는 뭐지?


- 만약 이렇게 타고 타고 들어가서 확인하고 싶으면
ctrl + 클릭 해주자 그러면 설명을 볼 수 있다
조금 더 자세한 설명
Throwable - getMessage()라는 메서드 들고있다
Exception
RuntimeException 이거 때리면 부모의 getMessage로 때려진다
Exception400
주의
만약 Exception400 에 getMessage()메서드를 만들면 동적 바인딩 돼서 Exception400에 때려지게 된다!
1.6 클래스 쭉 만들기 전부 복사함
401은 400 복사 붙여넣기 403은 401 붙여넣기


이런 식으로 만들어 주자
- 런타임 익셉션한테 자식 5개 준거임
강아지 고양이 분리 가능 똑같은 동물이지만
1.7 유틸로 이동
스크립트 클래스 만들기

복사하기
- “”” 없으면 스트링 빌더를 사용해야 한다!!

- 스크립트 클래스에
package shop.mtcoding.blog.core.util;
public class Script {
//스테틱이여서 new안해도 됨
//이 친구의 역할 문장열 받아서 리턴하는 것
public static String back(String msg){
String errMsg = """
<script>
alert('$msg');
history.back();
</script>
""".replace("$msg", msg);
return errMsg;
}
}
지금 안됨 오류남 Script가 import안됨


package shop.mtcoding.blog.core.util;
public class Script {
//스테틱이여서 new안해도 됨
//이 친구의 역할 문장열 받아서 리턴하는 것
public static String back(String msg) {
String errMsg = """
<script>
alert('$msg');
history.back();
</script>
""".replace("$msg", msg);
return errMsg;
}
//메시지 하나 띄우고 화면 이동시켜주는 애
// 만약 지금 보드 목록페이지에 있어
//주소에 강제로 로컬 host/save-form으로 때렸어 가려면 로그인 한 후 들어가야 하는데
// 인증 안되면 그 화면 가면 안된다. 제일 좋은 거는 메시지 주고 href로 로그인 창으로 가는게 좋다
/*
페이지 404를 찾을 수 없습니다라는 페이지를 만들 필요 없음
*/
public static String href(String msg, String url) {
String errMsg = """
<script>
alert('$msg');
location.href = '$url';
</script>
""".replace("$msg", msg)
.replace("$url", url);
return errMsg;
}
}
설정한거 전부 초기화 시키는 법(위 오류 해결)


실행하기 해결됨
1.8 다시 글로벌익셉션 핸들러 클래스로 이동
400, 401, 403, 404, 500 에러 메서드 추가 및 변경 해주기
package shop.mtcoding.blog.core.error;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import shop.mtcoding.blog.core.error.ex.*;
@RestControllerAdvice
public class GlobalExceptionHandler {
//정확하게 표현하려고 runtimeexception이 아닌 exception 400으로
//유효성 검사 실패 (잘못된 클라이언트의 요청)
//바디데이터 잘 못 터졌을 때
@ExceptionHandler(Exception400.class)
public String ex(Exception400 e) {
return Script.back(e.getMessage());
}
//인증 실패(클라이언트가 인증 없이 요청했거나 인증하다가 실패했거나)
@ExceptionHandler(Exception401.class)
public String ex(Exception401 e) {
return Script.back(e.getMessage());
}
//권한 실패 (인증은 돼있는데 삭제하려는 게시글이 내가 적은게 아니다)
@ExceptionHandler(Exception403.class)
public String ex(Exception403 e) {
return Script.back(e.getMessage());
}
//페이지를 찾을 수 없다 하면 안됨 리소스가 DB자원이 될 수 도 있어서
//서버에서 리소스(자원)를 찾을 수 없을 때
@ExceptionHandler(Exception404.class)
public String ex(Exception404 e) {
return Script.back(e.getMessage());
}
//서버에서 심각한 오류가 발생했을 때
@ExceptionHandler(Exception500.class)
public String ex(Exception500 e) {
return Script.back(e.getMessage());
}
}
만약 내가 못 잡는 애러가 있다면 이것을 잡아채기 위한 예외가 필요하다
가끔 포스트 맨(우리 앱이 아닌 다른 걸로)으로 보낼 수 있다 이러면 서버 터진다
일반적인 에러가 아닌 공격이다
내가 알고 있으니까 500 터트리면 된다
하지만 내가 완전히 모르겠는데 터지면 이 때 필요한 게 있다
그래서
이것 추가
//서버에서 심각한 오류가 발생했을 때(에러 모를 때)
@ExceptionHandler(Exception.class)
public String ex(Exception e) {
return Script.back(e.getMessage());
}
최종
package shop.mtcoding.blog.core.error;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import shop.mtcoding.blog.core.error.ex.*;
@RestControllerAdvice
public class GlobalExceptionHandler {
//정확하게 표현하려고 runtimeexception이 아닌 exception 400으로
//유효성 검사 실패 (잘못된 클라이언트의 요청)
//바디데이터 잘 못 터졌을 때
@ExceptionHandler(Exception400.class)
public String ex(Exception400 e) {
return Script.back(e.getMessage());
}
//인증 실패(클라이언트가 인증 없이 요청했거나 인증하다가 실패했거나)
@ExceptionHandler(Exception401.class)
public String ex(Exception401 e) {
return Script.back(e.getMessage());
}
//권한 실패 (인증은 돼있는데 삭제하려는 게시글이 내가 적은게 아니다)
@ExceptionHandler(Exception403.class)
public String ex(Exception403 e) {
return Script.back(e.getMessage());
}
//페이지를 찾을 수 없다 하면 안됨 리소스가 DB자원이 될 수 도 있어서
//서버에서 리소스(자원)를 찾을 수 없을 때
@ExceptionHandler(Exception404.class)
public String ex(Exception404 e) {
return Script.back(e.getMessage());
}
//서버에서 심각한 오류가 발생했을 때(에러 알고 있을 때)
@ExceptionHandler(Exception500.class)
public String ex(Exception500 e) {
return Script.back(e.getMessage());
}
//서버에서 심각한 오류가 발생했을 때(에러 모를 때)
@ExceptionHandler(Exception.class)
public String ex(Exception e) {
return Script.back(e.getMessage());
}
}
완전히 모르는 에러 메시지로 돌리면 안된다 취악해짐 파악하기 쉬워서
1.9 보드 레파지토리 이동
원래

변경시

이거를 이제는 404로 터트릴 수 있다
400에러는 바디 데이터 잘못 터졌을 때 실행한다!
2. 리팩토링 유저부터 시작
2.1 유저 서비스클래스 만들자
지금 우리는 회원가입할 때 터졌음.. 이미 쌀이 있는데 회원가입 해서
동일한 유저 있는지 확인을 유효성검사로 할 수 있을까? 못함 컨트롤러에서 못함 그래서 서비스에 추가
- 유저 레파지토리에 의존해야 한다
- 하지만 절 대 의존하면 안되는 것은 Usercontroller에서 BoardService 의존하면 안된다 그러면 서비스 분리하거나 삭제하려면 힘들다 중복이여도 되니까 다른 서비스를 넣지 말자! 프로그램 상 안 되는 거는 아닌데 만들기 힘들다 허용하는 것 하나 서비스에서는 다 의존해도 상관없음!! 하지만 서비스는 서비스 만은 의존하지 말자!! 트랜잭션 관리 어떻게 할건디!!

joinDTO에 유저내임 조회 해야 함 동일한 이름 있으면 안되니까
2.2 유저 레파지토리 이동

//유저이름 중복 체크
public User findByUsername(String username) {
Query query = em.createQuery("select u from User u where u.username=:username ", User.class);
query.setParameter("username", username);
User user = (User) query.getSingleResult();
return user;
}
- 문제
haha로 회원가입 요청 허용함 ( 먼저 조회할거임 → 못 찾으니까 noResultException으로 됨 아직 예외 안 잡아서 예외 터짐!!)
이후 테스트 하기
@Test
public void findByUsername_test() {
String username = "haha";
User user = userRepository.findByUsername(username);
}

이러면 Exception.class 이걸로 모르는 에러로 터진다
2.3 유저 서비스로 이동
- 만약 오류가 난다는 것을 모른다면
@Transactional
public void 회원가입(UserRequest.JoinDTO joinDTO) {
//만약 잘 됐다 생각하고 만들자
User oldUser = userRepository.findByUsername(joinDTO.getUsername());
if(oldUser != null) {
throw new Exception400("이미 존재하는 유저네임입니다");
}
userRepository.save(joinDTO.toEntity());
}
유저 못 찾았으면 → 테스트 안 했으면 정상로직이라 생각할 것이다.
실제로는 리턴도 안 됨 찾을 때만 들어감
못 찾으면 절대 null 이 안 들어 온다는 것임 try catch로 잡아야 한다
못 찾았으면 null이 된다고 넣으면 된다
2.4 유저 리포지 토리 이동

비지니스 처리 안 함거임
2.5 유저 테스트로 가서 한번 더 확인
@Test
public void findByUsername_test() {
String username = "haha";
User user = userRepository.findByUsername(username);
System.out.println("user = " + user);
}
미리 다 잡고 가야 함!! 안 그러면 모르는 예외가 터질 수 있고 습관을 드려야 한다!!
이런 연습을 해야한다!
2.6 유저 서비스로 이동
유저 있는게 잘못 된거니까
- 팁
if (oldUser == null) {
userRepository.save(joinDTO.toEntity());
}else {
throw new Exception400("이미 존재하는 유저네임입니다");
}
이래 짜지 말고
정상적인 코드를 if else로 쓰지 말자
! = 로 비 정상 로직만 if로 걸어버리자
if (oldUser != null) {
throw new Exception400("이미 존재하는 유저네임입니다");
}
userRepository.save(joinDTO.toEntity());
정상 로직은 밖에 비정상 로직만 if로 걸어 버리면 가독성이 좋다
왜 정상이면 리턴이 필요없을까?
이거 하면 컨트롤러에서 비즈니스 로직을 해야한다!!
비 정상적인 거는 서비스에서 다 걸러줘서 깔끔해 진다
2.7 로그인메서드 다시이동
public void 로그인(UserRequest.LoginDTO loginDTO) {
//조회 안 되면 noresultException터짐 이거를 401로 잡아야 한다
User user = userRepository.findByUsernameAndPassword(loginDTO.getUsername(), loginDTO.getPassword());
}
귀찮으면 이거 다 서비스에서 잡자 하면
public User findByUsernameAndPassword(String username, String password) throws NoResultException {
Query query = em.createQuery("select u from User u where u.username=:username and u.password=:password", User.class);
query.setParameter("username", username);
query.setParameter("password", password);
User user = (User) query.getSingleResult();
return user;
}
이렇게 할 수도 있다
우리는 이걸로 안하고
2.8 유저 리포지토리
//조회 커리
public User findByUsernameAndPassword(String username, String password) {
Query query = em.createQuery("select u from User u where u.username=:username and u.password=:password", User.class);
query.setParameter("username", username);
query.setParameter("password", password);
try {
User user = (User) query.getSingleResult();
return user;
}catch (Exception e) {
throw new Exception401("인증되지 않았습니다");
}
}
인증 되면 user리턴 됨
2.9 유저 서비스로 이동
void를 User, return 을 user로 변경
public User 로그인(UserRequest.LoginDTO loginDTO) {
//조회 안 되면 noresultException터짐 이거를 401로 잡아야 한다
User user = userRepository.findByUsernameAndPassword(loginDTO.getUsername(), loginDTO.getPassword());
return user;
}
//원래는 회원가입 할 때 비번을 해시화 시켜서 암호화 해야함 아직은 안할거임
2.10 유저 컨트롤러 이동
의존을 userService로 해야함 레파지토리가 아닌
@PostMapping("/join")
public String join(UserRequest.JoinDTO joinDTO) {
userService.회원가입(joinDTO);
return "redirect:/login-form";
}
이런 식으로 넘기면 된다
@PostMapping("/login")
public String login(UserRequest.LoginDTO loginDTO) {
User sessionUser = userService.로그인(loginDTO);
//null이 아니면 세션에 넣어준다
// 리다이랙트 해도 살아있다
System.out.println("12312323123213" + sessionUser.getUsername());
// 이거는 세션 유저 담아야 해서 필요하다
session.setAttribute("sessionUser", sessionUser);
return "redirect:/board";
}
실행 해보자
3가지 있다
로케이션 리로드 → 새로고침
href → 페이지 이동
히스토리 백 → 뒤로 이동
왜 뒤로 이동하냐? 새로고침 하면 내가 적은 글이 없어진다!
적은거 다시 적어야 해서 별로임
보드쪽도 고쳐야 함
수정하기, 삭제하기, 글쓰기, 목록보기, 상세보기
2.11 보드 서비스 이동
추가
@Transactionsal
public void 게시글쓰기(BoardRequest.SaveDTO saveDTO, User sessionUser) {
//누가 적었는지 알아야 해서 세션 넣어야 함
boardRepository.save(saveDTO.toEntity(sessionUser));
//게시글 인증이 여기서 해야 할까? 안 해도 됨 DB안 가도 되니까 컨트롤러에서 해도 욈
}
게시글 삭제 주의 사항 및 코드 짤 때 팁
게시글 삭제할 때 트랜잭션 걸어야 한다
미리 한글로 적어 보고 코드 짜보자
- 컨트롤러에서 부터 게시글 ID(번호) 받기
- 게시글이 DB에 존재 하는지 확인 없으면 404 터트리기(못 찾았으니까) if ifelse로
- 내가 쓴 글인지 권한 체크 하기 (내가 안 쓴 글이면 403 권한 없음 터트리기) if if else로
- 게시글 삭제
if(게시글 존재하나){
if(내가 쓴 글 이니){
여기서 게시글 삭제 해야 함
}else{}
}else 아니
이거는 별로다
- 그래서 이렇게 만들자
if(게시글 존재 안하지?){throw해주기}
if(내가 쓴 글 아니지?) {throw}
정상로직 실행
컨트롤러 없으니 테스트 못하니까 아직 서비스의존 안 되있으니까 단위 테스트를 실행한다
개발자들이 스프링 할 때 globalExceptionHandler 이해하고 만들지 않는다
한군데 해서 만들고 싶으면 RestControllerAdvice를 붙이고 ExceptionHandler를 붙여서 최종적으로 써
그냥 쓸 수 있는데
자세히 보니
모든 트라이 케치 원리 A → B요청 B→ C 요청 뭐 가장 좋은 방법은 throw해줘서 제일 처음 애 한테 책임 전가하는게 좋다
장고를 한다면 GlobalExceptionHandler가 없는지 찾아본다 이게 없으면 안 좋아서 안 쓰기에 있을거임
스프링 매력적인거 는 어노테이션과 리플랙션으로 만들어 졌기 때문이다!
사용법을 그냥 익혀서 외우고 나중에 강의 찾아 보는 것도 좋은 방법일지도
2.11.2 게시글 삭제 코드
@Transactional
public void 게시글삭제(int id, User sessionUser) {
Board board = boardRepository.findById(id);
// 게시글이 있는가? 이거는 리퍼지토리의 findById에서 알아서 throw해준다!(설정 했었음)
// 2. 내가 쓴 글인가?
if (sessionUser.getId() != board.getUser().getId()) {
throw new Exception403("본인이 작성한 글이 아닙니다");
}
// 정상 로직 실행
boardRepository.deleteById(id);
}
- 팁
정상 로직을 if에 넣는 것 보다 비정상로직을 if에 넣는게 훨 가독성이 좋다!
정상 로직으로 하면
- 권한이 없다 그러면 메시지 리턴 해줘서 컨트롤러에서 메시지 들어 갈 수 있게 만들어야 한다 즉 복잡해 진다!
2.12 이것들 컨트롤러 리팩토링 해주자
- 의존하고 있는 보드 레파지토리 삭제
- 게시글 삭제 추가해야 함 밑에 있음
@PostMapping("/board/{id}/delete")
public String delete(@PathVariable("id") int id) {
User sessionUser = (User) session.getAttribute("sessionUser");
//원래는 조회를 하고 삭제해야하는데 V1이라 그냥 함
boardService.게시글삭제(id, sessionUser);
return "redirect:/board";
}
- 게시글 쓰기
@PostMapping("/board/save")
public String save(BoardRequest.SaveDTO saveDTO) { //스프링 기본 견략 = x-www-form-urlencoded 파싱 매개변수만 같으면 됨 화면에 폼테그 name 과 반드시 같아야 한다!!
//세션유저가 null이면 인증 안됨 null아니면 인증됨
User sessionUser = (User) session.getAttribute("sessionUser");
//인증 체크 필요함
//터트려라! 401이면 href하는게 좋다
if (sessionUser == null) {
throw new Exception401("로그인이 필요합니다");
}
boardService.게시글쓰기(saveDTO, sessionUser); //보드 레파지토리 객체가 어디있나? Ioc에 있다 autoWrid해서 가져옴
return "redirect:/board";
}
익셉션 401 로직 바꿔야 한다 href가 좋음
로그인 삭제 안 했는데 ?
2.13 globalexceptionhandler페이지로 이동
401 변경
//인증 실패(클라이언트가 인증 없이 요청했거나 인증하다가 실패했거나)
@ExceptionHandler(Exception401.class)
public String ex(Exception401 e) {
return Script.href("인증되지 않았습니다", "/login-form");
}
원래는 로그 다 남겨야 한다
2.14 다시 컨트롤러
- 게시글 삭제
@PostMapping("/board/{id}/delete")
public String delete(@PathVariable("id") int id) {
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null) {
throw new Exception401("로그인이 필요합니다");
}
//원래는 조회를 하고 삭제해야하는데 V1이라 그냥 함
boardService.게시글삭제(id, sessionUser);
return "redirect:/board";
}
이걸 안하고 들어갈 수 있겠지
- 메인 보기
2.15 보드 서비스로 이동
추가
public List<Board> 게시글목록보기(){
List<Board> boardList = boardRepository.findAll();
return boardList;
}
2.16 다시 보드 컨트롤러
list 즉 게시글 목록
@GetMapping("/board")
public String list(HttpServletRequest request) {
List<Board> boardList = boardService.게시글목록보기();
//key값 모델스
//외부에서 /board요청 -> 톰캣에 감 rqeust객체로 만들어둠 -> 때림 -> Model에 rqeust객체 주입됨 -> 이 데이터를 reqeust객체에 넣어버림
request.setAttribute("models", boardList);
return "board/list";//파일의 경로 넣으면 되는데 고정적으로 되어 있음 확장자 자동으로 해주는가? 라이브러리로 머스테치 설정해줘서(templates에 머스테치로 찾아줌)
}
2.17 업데이트 전환
보드컨트롤러 update-form메서드로 이동
Board board = boardRepository.findById(id);
굳이 서비스 안 해도 괜찮은데 다른 애들 도 서비스에서 연결하고 있으니 일관성 있는게 좋다 안 하면 꼬일 수 있다.
2.18 보드 서비스 클래스 이동
//권한체크 반드시 해야 해서 조회만 한다고 끝나지 않음
public Board 게시글수정화면가기(int id, User sessionUser) {
//일단 조회를 해! 만약 터트리는 거를 null로 설정 했으면 따로 해줘야 함
Board board = boardRepository.findById(id);
// 앞에서 처리했다 생각하고 이 코드를 적은 거임 안 했으면 null이 뜸...
if(board.getUser().getId() != sessionUser.getId()) {
throw new Exception403("게시글을 수정할 권한이 없습니다.");
}
return board;
}
2.19 컨트롤러 이동 수정
- 인증체크
유효성 검사는 제대로 안됐으면 DB접근하지 말라는 말임
@GetMapping("/board/{id}/update-form")
public String updateForm(@PathVariable("id") int id, HttpServletRequest request) {
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null) {
throw new Exception401("인증되지 않았습니다.");
}
Board board = boardService.게시글수정화면가기(id, sessionUser);
request.setAttribute("model", board);
//이 친구도 오류 잡자!
//못찾으면 터진다! null 안들어옴 우리가 설정해서
// Board board = boardRepository.findById(id);
//리퀘스트에 담는다
// request.setAttribute("model", board);
return "board/update-form";
}
서비스는 컨트롤러의 책임이다 (세션이 있다 가정하고 진행했기 때문이다!)
에러 페이지 만들지 말고 JS를 사용해라
게시글 쓰는 것 조차 인증이 필요해서 수정하자!
수정필요!
@GetMapping("/board/save-form")
public String saveForm() {
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null) {
throw new Exception401("인증되지 않았습니다.");
}
return "board/save-form";
}
동일한게 너무 많은 곳에 분포 돼있다.
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null) {
throw new Exception401("인증되지 않았습니다.");
}
핵심로직은 아닌데 필요한 것을
- 부가로직이라고 한다!
프로그램 짜면서 넣을 필요 없다 나중에 하면 된다.유효성 검사도 같다.
2.20 글 수정하기 버튼 하자
업데이트 수정
2.21 보드 서비스로 이동
DTO안해서 다시 바꿈
2.22 보드 리퀘스트 가서
복사하고 entity는 지우자
@Data
public static class SaveDTO {
private String title;
private String content;
//포린키 받아야 하는데 클라이언트는 모름 세션에서 꺼내와야 한다 세션에 id있어서
//퍼시스트 할거다 보드 오브젝트로 해야함
public Board toEntity(User sessionUser) {
return Board.builder()
.title(title)
.content(content)
.user(sessionUser)
.build();
}
}
- 바꾼
package shop.mtcoding.blog.board;
import lombok.Data;
import shop.mtcoding.blog.user.User;
public class BoardRequest {
@Data
public static class UpdateDTO {
private String title;
private String content;
}
@Data
public static class SaveDTO {
private String title;
private String content;
//포린키 받아야 하는데 클라이언트는 모름 세션에서 꺼내와야 한다 세션에 id있어서
//퍼시스트 할거다 보드 오브젝트로 해야함
public Board toEntity(User sessionUser) {
return Board.builder()
.title(title)
.content(content)
.user(sessionUser)
.build();
}
}
}
중요
DTO는 동일한 모양이어도 중복해서 사용하기!
일반 유저는 DB를 보는게 아닌 화면을 본다
만약 사장님이 왔어 음 잘되는구만 하고 몇 달 있다가 사장이 내용은 변경하는데 제목은 변경 하지마 했으면 SaveDTO 이거를 지울 수 없으니 의존성 제거하려고 함! 공유 하면 바꿀 수 없다
미래를 위해서
다시 보드 컨트롤러 이동
2.23 보드 서비스 있어야 해서 다시 서비스로 이동
@Transactional
public void 게시글수정(int id, BoardRequest.UpdateDTO updateDTO, User sessionUser){
//1. 조회(없으면 404)
Board board = boardRepository.findById(id);
//2. 권한 체크
if (board.getUser().getId() != sessionUser.getId()) {
throw new Exception403("게시글을 수정할 권한이 없습니다.");
}
//3. 게시글 수정
board.setTitle(updateDTO.getTitle());
board.setContent(updateDTO.getContent());
}
쿼리 없지? 이게 더티 체킹이다!!
짧은 설명
reqeste에 가방 있는데 id, updateDTO 있고
락카에 유저 세션정보 있음
놀러옴 DB커낵션 만들어 지지 가방들고 DB갈 수 있게
컨트롤러 가서 인증, 유효 체크 하고 서비스 오면
트랜잭션(수정) 딱 걸림
이제 아무도 보드를 수정할 수 없음(고립성)
- 조회를 함 DB에 조회한 거니 응답이 테이블 데이터 → ORM 해서 퍼시스턴스 컨택스트에 담고있다(객체로 있다) board받고 돌아옴 → 이거는 영속화 돼있음
- 권한 체크 하고
- 영속화 돼있는 애 값을 바꿈
트랜젝션 종료시 영속화 된 애를 변화 감지해서 업데이트 쿼리 자동으로 날라감!!!!!
테스트
//조회된 애 수정 되는지 궁금한니까 하는거
//트랜잭션 끝나면 플러쉬를 하는데 여기서는 트랜잭션 못거니까
@Test
public void updateByIdV2_test() {
// given 조회 먼저함
int id = 1;
Board board = boardRepository.findById(id);
//when
board.setTitle("제목10");
board.setContent("내용10");
//트랜잭션이 종료되면 flush(); 느낌 내려고 함
em.flush();
}
결과

업데이트 쿼리 실행함
만약
@Test
public void updateByIdV2_test() {
// given 조회 먼저함
int id = 1;
Board board =new Board();
//when
board.setTitle("제목10");
board.setContent("내용10");
//트랜잭션이 종료되면 flush();
em.flush();
}
이러면 발동 될까? 안됨
- 이거는 비영속 객체니까
em.persist(board) 해서 영속화 해야지 업데이트 된다!!
그냥 네이티브 쿼리 사용하면 안됨?
이렇게 하면 캐시가 쌓인다!!
만약 조회 두번하면 어떻게 될까?
@Test
public void updateByIdV2_test() {
// given 조회 먼저함
int id = 1;
Board board = boardRepository.findById(id);
Board board1 = boardRepository.findById(id);
//when
board.setTitle("제목10");
board.setContent("내용10");
//트랜잭션이 종료되면 flush();
em.flush();
}
이러면 2번 조회할까? 아님 1번만 조회함
캐싱해서 한번 찾은 거 그대로 가지고 온다
근디 지금은 2번 조회함 뭔가가 잘못됐음
왜 2번 찾음?
public Voard findByIdV3(int id){
Board board =em.find(Board.class, id);
return board;
}
- 우리가 직접 쿼리를 짜면 캐싱이 안되네!
클래스에 id 있으면 가지고 온나 이런 말이다.
@Test
public void updateByIdV2_test() {
// given 조회 먼저함
int id = 1;
Board board = boardRepository.findByIdV3(id);
Board board1 = boardRepository.findByIdV3(id);
//when
board.setTitle("제목10");
board.setContent("내용10");
//트랜잭션이 종료되면 flush();
em.flush();
}
이러면 캐싱 돼서 한번만 조회한다!!!
그림 설명

R이 pc에 물어보고(있어?) DB들어감
그리고 pc에 B가 생김( 영속화된 객체)
Board board = boardRepository.findById(id);까지임 (new하면 안된다!!)
수정됨 영속화 된 코드 수정됨
트랙잭션 종료
퍼시스턴스 컨택스트에 변경 감지 로직 실행(내부적으로)
변경이 있으니 골라서 플러시(데이터 전송)함
이 때 업데이트 함
-> 더티 체킹
왜 더티 채킹하냐? → 성능이 올라간다
2.24 컨트롤러 이동
- 원래 코드
@PostMapping("/board/{id}/update")
public String update(@PathVariable("id") int id, @RequestParam("title") String title, @RequestParam("content") String content) {
// boardRepository.updateById(title, content, id);
return "redirect:/board/" + id;
}
변경!
@PostMapping("/board/{id}/update")
public String update(@PathVariable("id") int id, BoardRequest.UpdateDTO updateDTO) {
User sessionUser = (User) session.getAttribute("sessionUser");
if (sessionUser == null) {
throw new Exception401("로그인이 필요합니다");
}
boardService.게시글수정(id, updateDTO, sessionUser);
return "redirect:/board/" + id;
}
결과

수정 됨!

update쿼리 실행 된다!!
Share article