제네릭
Object 타입은 .length 해서 길이를 알 수 없다.
그래서 그렇게 만들어두면 나중에 호출자가 다운캐스팅 해서 사용해야한다.
그래서 new의 제어권을 호출자에게 주자!
하고 제네릭이 나왔다.
→ 제네릭은 반드시 new 하는 사람이 타입을 정해야 한다
타입을 아직 결정하지 않았을 때는 T는 Object 타입이다.
Box<String> 처럼 타입을 설정하면 T는 스트링 타입이다.
요청을 받으면 톰캣이 request 객체를 만드는데 ( → request 객체를 NEW 한다.)
이때 객체를 Object 타입으로 만든다.
→ request객체를 생성해서 전달해주는건 톰캣이 하는거라 Object 로 설정할
수 밖에 없지만 만약 new를 개발자가 할 수 있으면, 제네릭으로 타입을
설정하는게 낫다!
session도 스프링이 만드는데 내가 설정하는게 아니니까 Object 타입으로
세션을 만든다. 생성해서 전달해주는 측에서는 호출자가 무슨 타입을
담을지 모르니까 Object 타입으로 만들 수 밖에 없다.
즉, 제네릭은
박스가 있을 때 내가 만들 수 있으면 박스 설계를 제네릭으로 해둔다.
박스를 만들 때 사과를 담으면서 사과라고 박스에 적어두고.
바나나를 담으면 바나나라고 타입을 박스에 적어둔다.
이미 누가 다른 박스를 만들었을 때는 Object 타입으로만 박스를 만들어
둘 수밖에 없다. ⇒ 사용자가 뭘 담을지 모르니까!
제네릭은 메서드, 클래스에 사용하는데
- 매서드에 제네릭을 사용
→ 매개변수에 들어가는 건 stack 영역에 저장됨.
그리고 클래스에 설정된 건 heap 에 저장됨.
그래서 매서드에 <타입>을 적어줌

- 클래스에서 제네릭을 사용
→ 타입을 아직 모를때는 <B> 이렇게 적어주거나, <?> 와일드카드 사용해준다.

⭐⭐⭐⭐⭐
->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 하는게 낫다.

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에서 Resp 응답 객체를 사용해서 return 하는거 연습해보자.
v1
→ User 객체를 new해서 값을 담고 ok매서드의 body에 User를 담아주기

body에 User 객체가 잘 담겨서 넘어간 걸 볼 수 있다.
→ 나중에 AJAX요청에 응답할 때,
Resp로 return 해서 body에 있는 데이터를 프론트가 꺼내쓰게 되는 것.
v2

User 객체를 2개 return 하고 싶으면 ?
→ ArrayList에 담아서 Resp body에 싣어 보낸다.
Arrays.asList
Arrays.asList로 생성된 리스트의 크기는 고정됨!
→ 한번 생성하고 나서 리스트의 크기를 변경할 수 없음



배열을 body에 담아서 return
/test/v2를 때려보면

JSON 컬렉션, JSON 어레이 형태로 body에 데이터가 잘 들어온다.
body : [
{
“id” : 1,
“username” : “ssar”,
“password” : “1234”,
“email” : “ssar@nate.com”,
“createdAt” : null
},
{
}
]
배열안에 객체가 들어있는 모습이군. 뭔지 알겠다!
v3
예외처리할 때 throw 를 날려서 new Exception을 하면 ?


호출하는순간 자바스크립트가 뜬다.
이렇게 하면 안된다.
AJAX로 데이터를 응답하기를 요청했다면
이때 Exception은 데이터 그대로 응답해야하지 위처럼 view를 응답하면 안됨.
(→ Exception404는 자바스크립트 코드를 발동시키고 추가로 화면을 이동시키거나 할때 사용하도록 해놓은 거니까 이럴때 쓰면 안됨)

Resp를 사용해서 return 하면

Resp ( → status , msg, body ) 구조로 넘어가서 프론트가 이걸 잘 받아서 처리할 수 있다.

근데 Status code 상태코드를 200으로 전달하는 문제가 있다.
브라우저도 이게 에러라는거 알아야하는데
우리가 Resp 만들어서 404를 JSON으로 전달하니까 못알아차리나 ?
그래서
HttpServletResponse
를 사용해서 직접 status를 설정해줄 수 있다.v4

setStatus를 하면 브라우저에게 상태를 알려준다.
Resp.fail의 status와는 별개로!

브라우저의 상태도 404로 잘 변경된 것을 확인할 수 있다.
근데
HttpServletResponse
를 작성하는 이 v4는 귀찮은 방법이다.업그레이드된 방법이 v5
v5

ResponseEntity를 이용해서 Resp객체를 응답함과 동시에 브라우저?에
HttpStatus.Not_Found 를 알려준다.

404!
이제
GlobalApiExceptionHandler
를 만들어보자.기존 핸들러(→ 일반적인 view를 요청했을 때 쓰는 핸들러 =
GlobalExceptionHandler
)는 자바스크립트를 발동시키기 때문에 (→ 글자가 아니라 자바스크립트를 발동시키기 위해 우리가 만든 Script를
return 하도록 해두었음)

그래서 AJAX 요청한 것을 처리할 때는 이 핸들러를 쓰면 안된다.
새로운 핸들러를 만들어줘야 한다!
++
상태코드 넘길때 그냥 String ok = “ok”; 이거 넘기면 안되나 ? 하는데
String ok는 안됨 → 프론트에서 파싱을 못한다!

안의 내용은 일단 기존 핸들러의 400 ~ 500까지 복사해서 붙여넣어준다.
! ) 서버에서 심각한 오류가 발생했을때 사용하는 ex 매서드는 만들어주지 않는다.
ex 매서드는 API 핸들러에서는 필요없다. 이미 기존 핸들러에 있으니까 여기서도 쓰면 충돌남

API로 요청할때는 예외처리는 API 달려있는 Exception으로 return 할 거임.
→ 애는 Script를 발동시켜서 응답하는게 아니라 JSON형태로 응답하는걸로
만든다!

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 도 써줘야하는가 ?

→ 프론트엔트가 보는거(JSON)랑 브라우저에 알려주는 거랑 2개 보내려고
404, HttpStatus.Not_Found 각각 써줬다.
ExceptionApi404를 return 하는걸로 매서드를 만들자
v6

throw new ExceptionApi404("페이지를 찾을 수 없습니다.");
v5 처럼 new ResponseEntity해서 상태코드 return 할 필요도 없고,
예외 발생 부분은 throw 바로 하면 된다.
/test/v6 때리면
컨트롤러에 들어와서 throw 부분에서
GlobalApiExceptionHandler
가 발동?한다.

GlobalApiExceptionHandler
에서 애를 때린다.결과는

Resp도 JSON으로 잘 넘어갔고 , httpStatus도 404로 잘 바뀌었다.
→ v6를 사용하면 컨트롤러에서는 v5처럼 Resp.fail을 직접 리턴할 일은 없음.
무조건 정상리턴! 잘못(예외)되면 throw 하면 됨!

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

댓글 삭제 버튼을 눌렀을 때, 작동하는 매서드를 만들어주자.

/api/reply/{id}
인터셉터에 인증로직을 짜놨으니 url 앞에 /api 만 추가해주면 인터셉터에서 인증이 발동되는데
일단 지금은 인증 안할거라서 /api 지우고 reply/{id}만!
일단 서비스는 만들지 말고, AJAX 연습하게 바로 상세페이지에서 script 를 작성해보자.
detail.mustache로 가서 삭제 버튼 쪽에 수정 :)

이제 form 태그로 요청 보내는건 안할 거니까 form 태그 지워줘!

type을 sumit으로 하면 전체 리로드 되니까 submit X
→ AJAX 요청할 때 버튼 타입은 button 으로 !

자바스크립트한테 문자열로 넘겨야 해서 ‘’ 추가.
문자열로 안보내면 뻗음

put, post 말고는 body데이터가 없다. ( → body데이터 보낼때는 content type 꼭 넣어줘야 한다. )
get, delete는 body없이 요청 보냄! 그래서 지금 delete에 body 데이터 안넣음

이 response 는 아직 객체가 아니다.
→ 이 안에는 header랑 body가 같이 들어가 있다. (console.log(response)해서 찍어보면 확인됨)
그러니 response 의 body를 파싱하자 -!

JSON으로 파싱하는 거 앞에도 await를 걸어준다.
파싱해서 콘솔에 responseBody를 찍어보면

삭제버튼을 클릭하면 찍힌다.
response와 responseBody를 콘솔에서 확인할 수 있다.
→ response의 body를 파싱한게 responseBody
→ 프론트에서 필요한 데이터가 responseBody다 들어있다!
→ 상태코드 같은건 response에서 꺼내써도 되지만 프론트가 더 편리하게
일할 수 있게 Resp에 담아서 보내준 거임

컨트롤러에서 throw 하면 어떻게 전달될까?
throw해보면

response의 ok도 false로 바뀌고, status 도 403으로 바뀐 걸 확인할 수 있다.
body의 데이터도 잘 넘어왔다.
이제는 상세보기 페이지 CSR 하면된다. 삭제한 댓글 부분 DOM 없애기!

먼저 dom에 각자의 id 넣어주기
id는 보통 pk를 사용해서 만든다.

id 잘 들어갔다 : )

아이디를 선택해서 삭제하는 코드르 써준다.

댓글 1삭제

remove 됨
그럼 이제 서비스하고 다 만들어보자

인증을 위해 api 추가

컨트롤러에도 api 추가
이제 인증은 완료됨
서비스 만들자!

Repository 없네 먼저 만들자

++

고립성
내가 건드릴려고 한거 못건드리게 하는거!
내가 건드리고 있는데
이거보면 readonly = true 를 이해할 수 있다.
보고와서 쌤한테 readonly = true 설명해달라고 하기

하나의 비즈니스 로직이자 하나의 트랜젝션(일의 최소단위)!
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);
}
}

컨트롤러 작성


상태코드에따라 remove 할지말지 분기하는 조건문 넣어주기
조건문에 상태코드를 쓸지, msg를 쓸지 프론트엔드끼리 정해야 한다 : )

response.ok로 하면 더 간단하다고 한다!
++
AJAX를 사용하는 주 이유는 비동기통신이라서가 아니라 부분 리로딩이
가능하기 때문이다.
응답하는 데이터 . 트래픽마다 돈을 내니까 ajax 써서 부분 리로딩만 해서
트래픽 비용을 줄일 수 있다.
댓글 등록은
댓글 리스트 앞에 Prepend로 넣으면 됨!


Share article