[Springboot] 21 댓글 삭제 ( 제네릭, AJAX )

김호정's avatar
Sep 13, 2024
[Springboot] 21 댓글 삭제 ( 제네릭, AJAX )
 

제네릭

 
Object 타입은 .length 해서 길이를 알 수 없다.
 
그래서 그렇게 만들어두면 나중에 호출자가 다운캐스팅 해서 사용해야한다.
 
그래서 new의 제어권을 호출자에게 주자!
 
하고 제네릭이 나왔다.
 
제네릭은 반드시 new 하는 사람이 타입을 정해야 한다
 
타입을 아직 결정하지 않았을 때는 T는 Object 타입이다.
Box<String> 처럼 타입을 설정하면 T는 스트링 타입이다.
 
요청을 받으면 톰캣이 request 객체를 만드는데 ( → request 객체를 NEW 한다.)
이때 객체를 Object 타입으로 만든다.
→ request객체를 생성해서 전달해주는건 톰캣이 하는거라 Object 로 설정할
수 밖에 없지만 만약 new를 개발자가 할 수 있으면, 제네릭으로 타입을
설정하는게 낫다!
 
session도 스프링이 만드는데 내가 설정하는게 아니니까 Object 타입으로
세션을 만든다. 생성해서 전달해주는 측에서는 호출자가 무슨 타입을
담을지 모르니까 Object 타입으로 만들 수 밖에 없다.
 
 
즉, 제네릭은
 
박스가 있을 때 내가 만들 수 있으면 박스 설계를 제네릭으로 해둔다.
박스를 만들 때 사과를 담으면서 사과라고 박스에 적어두고.
바나나를 담으면 바나나라고 타입을 박스에 적어둔다.
이미 누가 다른 박스를 만들었을 때는 Object 타입으로만 박스를 만들어
둘 수밖에 없다. ⇒ 사용자가 뭘 담을지 모르니까!
 
 
제네릭은 메서드, 클래스에 사용하는데
 
  1. 매서드에 제네릭을 사용
→ 매개변수에 들어가는 건 stack 영역에 저장됨.
그리고 클래스에 설정된 건 heap 에 저장됨.
그래서 매서드에 <타입>을 적어줌
 
매서드에 <타입>을 적어줌
매서드에 <타입>을 적어줌
 
  1. 클래스에서 제네릭을 사용
→ 타입을 아직 모를때는 <B> 이렇게 적어주거나, <?> 와일드카드 사용해준다.
 
클래스에서 제네릭을 사용. Body에 어떤 객체를 담을 지 모르니까 <T>라고 적어줌
클래스에서 제네릭을 사용. Body에 어떤 객체를 담을 지 모르니까 <T>라고 적어줌
 
⭐⭐⭐⭐⭐
->Class의 <T>는 new할때 결정남
매서드가 static 이니까 new를 안해도 매서드의 제네릭 타입을 먼저 알 수 있다.
→ public static <B> Resp<?> ok (B body) {
} 는 static 이니까 메인이 실행되기 전에 static 영역에 미리 띄워져 있음.
그래서 body에 들어오는게 User 타입이면 이 ok매서드는 User 타입이 되는거임
→ ok 매서드를 호출할때 <B>의 타입이 결정됨
 
 
타입을 모르니까 T 를 설계 → 언제는 User, 언제는 Board가 들어오니까.
→ 리스트 타입의 컬렉션이나 언제는 제이슨 배열이 넘어갈 수 있음.
→ 뭐가 넘어오는지 상황에 따라서 프론트에서 파싱하는게 달라짐. (힘들어짐)
→ 그래서 항상 RESP같은 “공통응답DTO” 를 사용해서 넘겨주면
프론트가 잘 받아서 처리할 수 있다.
 
++
‘삭제’처럼 딱히 프론트에 돌려줄 데이터가 없으면 body에 NULL 을 넣어서
돌려줘도 된다.
 
++
RESP 안에 생성자를 만들어 넣는것보단
static으로 OK, FAIL 매서드를 만들어서 return 하는게 낫다.
 
 
util → Resp 응답객체 생성
util → Resp 응답객체 생성
package org.example.springv3.core.util; import lombok.AllArgsConstructor; import lombok.Data; @AllArgsConstructor @Data public class Resp<T> { private Integer status; private String msg; private T body; public static <B> Resp<?> ok(B body){ return new Resp<>(200, "성공", body); } public static Resp<?> fail(Integer status, String msg){ return new Resp<>(status, msg, null); } }
 
 
BoardController
BoardController
BoardController에서 Resp 응답 객체를 사용해서 return 하는거 연습해보자.
 
v1
→ User 객체를 new해서 값을 담고 ok매서드의 body에 User를 담아주기
 
notion image
 
body에 User 객체가 잘 담겨서 넘어간 걸 볼 수 있다.
→ 나중에 AJAX요청에 응답할 때,
Resp로 return 해서 body에 있는 데이터를 프론트가 꺼내쓰게 되는 것.
 
v2
notion image
 
User 객체를 2개 return 하고 싶으면 ?
→ ArrayList에 담아서 Resp body에 싣어 보낸다.
 
Arrays.asList
Arrays.asList로 생성된 리스트의 크기는 고정됨!
→ 한번 생성하고 나서 리스트의 크기를 변경할 수 없음
notion image
 
notion image
notion image
 
 
배열을 body에 담아서 return
 
/test/v2를 때려보면
 
notion image
 
JSON 컬렉션, JSON 어레이 형태로 body에 데이터가 잘 들어온다.
 
body : [
{
“id” : 1,
“username” : “ssar”,
“password” : “1234”,
“email” : “ssar@nate.com”,
“createdAt” : null
},
{
 
}
]
 
배열안에 객체가 들어있는 모습이군. 뭔지 알겠다!
 
v3
 
예외처리할 때 throw 를 날려서 new Exception을 하면 ?
 
notion image
notion image
 
호출하는순간 자바스크립트가 뜬다.
이렇게 하면 안된다.
 
AJAX로 데이터를 응답하기를 요청했다면
이때 Exception은 데이터 그대로 응답해야하지 위처럼 view를 응답하면 안됨.
(→ Exception404는 자바스크립트 코드를 발동시키고 추가로 화면을 이동시키거나 할때 사용하도록 해놓은 거니까 이럴때 쓰면 안됨)
 
notion image
 
Resp를 사용해서 return 하면
 
notion image
 
Resp ( → status , msg, body ) 구조로 넘어가서 프론트가 이걸 잘 받아서 처리할 수 있다.
 
notion image
근데 Status code 상태코드를 200으로 전달하는 문제가 있다.
브라우저도 이게 에러라는거 알아야하는데
우리가 Resp 만들어서 404를 JSON으로 전달하니까 못알아차리나 ?
 
그래서 HttpServletResponse 를 사용해서 직접 status를 설정해줄 수 있다.
 
v4
notion image
setStatus를 하면 브라우저에게 상태를 알려준다.
Resp.fail의 status와는 별개로!
 
notion image
브라우저의 상태도 404로 잘 변경된 것을 확인할 수 있다.
 
근데 HttpServletResponse 를 작성하는 이 v4는 귀찮은 방법이다.
 
업그레이드된 방법이 v5
 
v5
notion image
 
ResponseEntity를 이용해서 Resp객체를 응답함과 동시에 브라우저?에
HttpStatus.Not_Found 를 알려준다.
 
notion image
404!
 
이제 GlobalApiExceptionHandler 를 만들어보자.
 
기존 핸들러(→ 일반적인 view를 요청했을 때 쓰는 핸들러 = GlobalExceptionHandler)는 자바스크립트를 발동시키기 때문에
(→ 글자가 아니라 자바스크립트를 발동시키기 위해 우리가 만든 Script를
return 하도록 해두었음)
notion image
 
그래서 AJAX 요청한 것을 처리할 때는 이 핸들러를 쓰면 안된다.
 
새로운 핸들러를 만들어줘야 한다!
++
상태코드 넘길때 그냥 String ok = “ok”; 이거 넘기면 안되나 ? 하는데
String ok는 안됨 → 프론트에서 파싱을 못한다!
 
GlobalApiExceptionHandler 생성
GlobalApiExceptionHandler 생성
 
안의 내용은 일단 기존 핸들러의 400 ~ 500까지 복사해서 붙여넣어준다.
 
! ) 서버에서 심각한 오류가 발생했을때 사용하는 ex 매서드는 만들어주지 않는다.
ex 매서드는 API 핸들러에서는 필요없다. 이미 기존 핸들러에 있으니까 여기서도 쓰면 충돌남
 
notion image
 
API로 요청할때는 예외처리는 API 달려있는 Exception으로 return 할 거임.
→ 애는 Script를 발동시켜서 응답하는게 아니라 JSON형태로 응답하는걸로
만든다!
 
notion image
 
Api핸들러안에 return 하는걸 Api상태코드로 다 바꿔준다.
 
400 bad_request
401 unauthorized
403 forbidden
404 not_found
500 internal_server_error
 
package org.example.springv3.core.error; import org.example.springv3.core.error.ex.*; import org.example.springv3.core.util.Resp; import org.example.springv3.core.util.Script; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class GlobalApiExceptionHandler { // 유효성 검사 실패 (잘못된 클라이언트의 요청) @ExceptionHandler(ExceptionApi400.class) public ResponseEntity<?> ex400(Exception e) { return new ResponseEntity<>(Resp.fail(400, e.getMessage()), HttpStatus.BAD_REQUEST); } // 인증 실패 (클라이언트가 인증없이 요청했거나, 인증을 하거나 실패했거나) @ExceptionHandler(ExceptionApi401.class) public ResponseEntity<?> ex401(Exception e) { return new ResponseEntity<>(Resp.fail(401, e.getMessage()), HttpStatus.UNAUTHORIZED); // 인증안됨 } // 권한 실패 (인증은 되어 있는데, 삭제하려는 게시글이 내가 적은게 아니다) @ExceptionHandler(ExceptionApi403.class) public ResponseEntity<?> ex403(Exception e) { return new ResponseEntity<>(Resp.fail(403, e.getMessage()), HttpStatus.FORBIDDEN); // 권한없음 } // 서버에서 리소스(자원) 찾을 수 없을때 @ExceptionHandler(ExceptionApi404.class) public ResponseEntity<?> ex404(Exception e) { return new ResponseEntity<>(Resp.fail(404, e.getMessage()), HttpStatus.NOT_FOUND); } // 서버에서 심각한 오류가 발생했을때 (알고 있을 때) @ExceptionHandler(ExceptionApi500.class) public ResponseEntity<?> ex500(Exception e) { return new ResponseEntity<>(Resp.fail(500, e.getMessage()), HttpStatus.INTERNAL_SERVER_ERROR); } }
 
++
 
왜 404가 있는데 HttpStatus.Not_Found 도 써줘야하는가 ?
 
notion image
 
→ 프론트엔트가 보는거(JSON)랑 브라우저에 알려주는 거랑 2개 보내려고
404, HttpStatus.Not_Found 각각 써줬다.
 
ExceptionApi404를 return 하는걸로 매서드를 만들자
 
v6
notion image
throw new ExceptionApi404("페이지를 찾을 수 없습니다.");
 
v5 처럼 new ResponseEntity해서 상태코드 return 할 필요도 없고,
예외 발생 부분은 throw 바로 하면 된다.
 
/test/v6 때리면
컨트롤러에 들어와서 throw 부분에서 GlobalApiExceptionHandler
가 발동?한다.
 
 
notion image
GlobalApiExceptionHandler 에서 애를 때린다.
 
결과는
 
notion image
Resp도 JSON으로 잘 넘어갔고 , httpStatus도 404로 잘 바뀌었다.
 
→ v6를 사용하면 컨트롤러에서는 v5처럼 Resp.fail을 직접 리턴할 일은 없음.
무조건 정상리턴! 잘못(예외)되면 throw 하면 됨!
 
notion image
 
앞으로 이렇게 throw 하면 됨!
 

 
이제 form action 을 안쓰고 AJAX를 사용해야 하는지 ?
 
form 태그로 포스트 요청해도 됨 → 이건 브라우저가 요청하는 것
브라우저한테는 코드를 돌려줄 수 없고 무조건 html을 돌려줘야함
a 태그 혹은 form 태그 요청을 하면 응답을 받아서 무조건 새로고침을 한다.
전체를 다시그리는 리랜더링을 하는데 이건 프로토콜(약속)이다.
 
휴대폰은 브라우저가 아니니까 브라우저의 프로토콜이 적용되지 않는다.
그래서 새로고침이 되지 않는다.
 
form태그 말고 자바스크립트 AJAX로 요청하면 데이터를 응답할 수 있음
 
부분 리로딩을 하고 싶다! 댓글을 삭제하고 싶다!
→ 그럼 AJAX로 요청하고 응답받아서 해당 댓글 부분만 remove 하면
그 부분만 사라진다.
 
서버가 서버사이드 랜더링( → 서버에게 부하가 크다)을 안해줘도
CSR 을 하면 브라우저가 연산을 해야해서 그럼 서버대신 브라우저의
부하가 커진다. 브라우저(클라이언트)는 수만명인데, 서버는 1개니까
서버에 부하가 많이 걸리는것 보다는 낫다.
그러니 부라우저에게 부하를 전가?하는 CSR 을 하기 시작했다.
그리고 응답하는 용량에 따라서 트래픽 비용이 달라지는데
html대신 JSON으로 응답하면 트래픽 비용이 많이 준다.
(html 속 그 코드를 다 만들어서 전달하려면 용량이 커지는데,
JSON으로 응답하면 필요한 데이터만 담겨있으니까 용량이 수십배 줄어든다.
당연이 JSON으로 전달하면 트래픽 비용도 수십배 줄어든다.)
 
전체를 다 AJAX를 사용하면 개발자가 많이 필요하고 인건비도 늘어난다.
그래서 나온 리액트로 개발하면 데이터 넣는게 쉬워서 개발 인건비도 줄어든다.
 
AJAX
  1. 비동기 통신을 할 수 있다.
  1. 부분 리로딩을 할 수 있다 (핵심!)
 

 
댓글 삭제 가즈아
 
*
원래 reply 컨트롤러 안 만들고 board 컨트롤러에서 다 처리하는데 일단 지금은
reply 컨트롤러 만들어보자.
 
ReplyController
ReplyController
 
댓글 삭제 버튼을 눌렀을 때, 작동하는 매서드를 만들어주자.
notion image
/api/reply/{id}
인터셉터에 인증로직을 짜놨으니 url 앞에 /api 만 추가해주면 인터셉터에서 인증이 발동되는데
일단 지금은 인증 안할거라서 /api 지우고 reply/{id}만!
 
일단 서비스는 만들지 말고, AJAX 연습하게 바로 상세페이지에서 script 를 작성해보자.
 
detail.mustache로 가서 삭제 버튼 쪽에 수정 :)
notion image
 
이제 form 태그로 요청 보내는건 안할 거니까 form 태그 지워줘!
 
notion image
 
type을 sumit으로 하면 전체 리로드 되니까 submit X
→ AJAX 요청할 때 버튼 타입은 button 으로 !
 
notion image
 
자바스크립트한테 문자열로 넘겨야 해서 ‘’ 추가.
문자열로 안보내면 뻗음
 
notion image
 
put, post 말고는 body데이터가 없다. ( → body데이터 보낼때는 content type 꼭 넣어줘야 한다. )
get, delete는 body없이 요청 보냄! 그래서 지금 delete에 body 데이터 안넣음
 
notion image
 
이 response 는 아직 객체가 아니다.
→ 이 안에는 header랑 body가 같이 들어가 있다. (console.log(response)해서 찍어보면 확인됨)
 
그러니 response 의 body를 파싱하자 -!
 
notion image
 
JSON으로 파싱하는 거 앞에도 await를 걸어준다.
 
파싱해서 콘솔에 responseBody를 찍어보면
 
notion image
 
삭제버튼을 클릭하면 찍힌다.
response와 responseBody를 콘솔에서 확인할 수 있다.
→ response의 body를 파싱한게 responseBody
→ 프론트에서 필요한 데이터가 responseBody다 들어있다!
→ 상태코드 같은건 response에서 꺼내써도 되지만 프론트가 더 편리하게
일할 수 있게 Resp에 담아서 보내준 거임
 
notion image
컨트롤러에서 throw 하면 어떻게 전달될까?
 
throw해보면
 
notion image
 
response의 ok도 false로 바뀌고, status 도 403으로 바뀐 걸 확인할 수 있다.
body의 데이터도 잘 넘어왔다.
 
이제는 상세보기 페이지 CSR 하면된다. 삭제한 댓글 부분 DOM 없애기!
 
 
notion image
 
먼저 dom에 각자의 id 넣어주기
 
id는 보통 pk를 사용해서 만든다.
notion image
id 잘 들어갔다 : )
 
notion image
아이디를 선택해서 삭제하는 코드르 써준다.
 
notion image
댓글 1삭제
 
 
notion image
remove 됨
 
 
 
그럼 이제 서비스하고 다 만들어보자
 
notion image
인증을 위해 api 추가
 
notion image
컨트롤러에도 api 추가
 
이제 인증은 완료됨
 
서비스 만들자!
 
notion image
 
Repository 없네 먼저 만들자
 
notion image
 
++
 
 
notion image
고립성
 
내가 건드릴려고 한거 못건드리게 하는거!
내가 건드리고 있는데
 
이거보면 readonly = true 를 이해할 수 있다.
보고와서 쌤한테 readonly = true 설명해달라고 하기
 

 
notion image
 
하나의 비즈니스 로직이자 하나의 트랜젝션(일의 최소단위)!
 
db를 update하는거니 @Transactional 붙여주기
package org.example.springv3.reply; import lombok.RequiredArgsConstructor; import org.example.springv3.core.error.ex.ExceptionApi403; import org.example.springv3.core.error.ex.ExceptionApi404; import org.example.springv3.user.User; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Transactional(readOnly = true) // 트랜젝션 고립성때문에 생성해줌 @RequiredArgsConstructor @Service // Component 스캔을 위해 IoC에 띄우기 public class ReplyService { private final ReplyRepository replyRepository; @Transactional public void 댓글삭제(int id, User sessionUser){ // 삭제하기 전에 조회먼저. 없으면 throw Reply replyPS = replyRepository.findById(id).orElseThrow( () -> new ExceptionApi404("댓글을 찾을 수 없습니다.") ); // 권한체크 // boardId랑 UserId는 reply가 처음 select할때 가져왔으니까 여기서 또 select 안한다. // 만약 username이 필요한 경우에는 mFindById를 만들어서 조인해서 사용하면 select 한번만 치니까 속도에서 개선이 된다. if(replyPS.getUser().getId() != sessionUser.getId()){ throw new ExceptionApi403("댓글 삭제 권한이 없습니다."); } // replyRepository.deleteById(id); } }
notion image
컨트롤러 작성
 
 
notion image
notion image
상태코드에따라 remove 할지말지 분기하는 조건문 넣어주기
 
조건문에 상태코드를 쓸지, msg를 쓸지 프론트엔드끼리 정해야 한다 : )
notion image
response.ok로 하면 더 간단하다고 한다!
 
 
++
AJAX를 사용하는 주 이유는 비동기통신이라서가 아니라 부분 리로딩이
가능하기 때문이다.
응답하는 데이터 . 트래픽마다 돈을 내니까 ajax 써서 부분 리로딩만 해서
트래픽 비용을 줄일 수 있다.
 
댓글 등록은
댓글 리스트 앞에 Prepend로 넣으면 됨!
 
notion image
notion image
Share article

keepgoing