그동안 배운 것들을 적용해 기존 만들었던 CRUD 게시판 처음부터 다시 만들어보기

 

<변경점>

1.Spring Security 적용 (JWT방식 커스텀 필터 사용)

2.Controller에 @Valid 어노테이션 적용 ( 간단해서 보류 )

2.5 기존 Article, Comment 등록/수정/삭제 기능 재구현 (추가)

3.JPA 연관관계 변경 (양방향 -> 단방향)

4.Custom Exception 처리하기

5.ERD, API 작성

 

<패키지 구조>

현재 있는 기능 : 회원기능 / 게시글 / 댓글(대댓글)

Controller // Service // Repository // Exception // Security // Handler // Jwt관련

                                                          =>묶어서 Utils

 

 

<게시글 관련 기능 구현>

 

Controller

게시글 전체목록 조회시에는 게시판의 형태대로 제목, 작성자만 가져오게 했다.

게시글 상세 조회(게시물클릭) 시에는 게시글의 필드를 모두 가져오게 했다.

둘을 구분짓기 위해 ResponseDto를 ListResponseDto // DetailResponseDto로 나누어 받게 함

 

Service

Controller에 맞게 구현했다. Article 조회 시 Comment를 어떻게 가져올지 아직 정하지 못했기 때문에, ArticleDetailResponseDto 작성을 보류했다. Comment에 대댓글 기능을 추가할 생각이기 때문에, 게시글에 달린 댓글을 모두 긁어와서 Article에 List로 나갈 댓글 목록을 만들어줄 생각이다. 자세한건 Comment에서

 

*추가 : 게시글 삭제 요청에 대해 DB에서 삭제하는 것이 아닌, Article의 isDeleted 필드를 true로 만들어주는 방식으로 하기로 했다. 따라서 게시글 긁어오는 부분을 findAllByIsDeletedFalse()로 대체하였다

게시글 상세조회 요청에 대해 가져온 게시글이 isDeleted면 ArticleDeletedException을 throw하도록 바꾸었다.

 

 

<Comment 관련 기능>

Comment 기능에 대댓글 기능을 구현할 생각이다. 구현 방법으로는 여러가지가 있을 것이다.

1. Comment 내부에 Comment필드를 가져 연관관계 맺어주기

- Comment와 Comment의 연관관계를 정의할 수가 없다. (댓글인지, 대댓글인지에 따라 관계가 달라지니까)

 

2. 대댓글 entity를 만들어 관리

- 대댓글이 한 depth로만 달리면 괜찮은 방법일 수 있지만, 대댓글에도 대댓글을 달 수 있는 경우 계속 Entity를 만들어주어야 하기 때문에 좋지 않다고 생각함

 

3. Comment에 commentType 필드를 가져서 게시글에 단 댓글인지 Comment에 단 댓글인지 분류

- Article을 조회했을 때 commentType이 ArticleComment인 댓글을 가져오고, 해당 댓글들에 대해서 commentType이 CommentComment 인 애들을 전부 조회한 뒤 Id를 비교해야 하기 때문에 쿼리가 불필요하게 많이 나가게 될 것 같다.

 

4. 댓글과 대댓글을 구분짓지 않고 Comment 엔티티로 관리하며, nullable한 rootComment 필드를 가지게 한다.

    Article 조회 시, 해당 Article에 달린 댓글 전부를 긁어온다. CommentBuilder 메소드를 하나 만들어 Comment를 구분하       고, 댓글이 대댓글을 포함하도록 하는 로직을 구현한다.

 

==> 4번은 추가적인 로직을 구현해야 해서 복잡할 것 같지만, 이렇게 하는게 맞는 방법 같아서 이렇게 하기로 함.

 

일단, 댓글과 대댓글의 관계를 정리해주는 로직을 제외하고, Comment 등록/삭제/수정 기능을 구현하려고 한다.

또한, 댓글 삭제 시에도 대댓글은 유지하기 위해 댓글 삭제 요청에 대해 DB에서 댓글을 삭제하는 것이 아닌, isDeleted flag를 true로 바꿔주는 방식으로 구현하려고 한다. 이에 따라 CommentResponseDto의 생성자에는 isDeleted를 확인하고, 삭제된 댓글일 시에는 Dto의 content를 "삭제된 댓글"로 바꿔주어야한다.

추가로, 테스트를 위해 Article에서 Comment를 조회하는 부분을 일단 모든 Comment를 조회하도록 구현하려고 한다.

 

(Article상세조회부분)

Article상세조회

(CommentController)

user의 고유ID만 넘겨주면 좋겠지만, 서비스에서 User의 권한을 확인하고 관리자일 시 요청을 처리해주도록 API가 설계되어있기 때문에 User객체 자체를 넘긴다

 

(CommentService)

delete요청에 대해 DB에서 삭제하는 것이 아니라, comment 엔티티의 isDeleted 값을 true로 바꾸는 메서드를 구현했다.

Comment 엔티티의 생성자와 메서드

Comment의 생성자는 Article에 단 Comment인 경우와 대댓글의 경우를 나누어서 오버로딩 해주었다.

기존에는 Comment의 생성자에 CommentRequestDto를 받도록 하였는데, CommentRequest가 바뀔 가능성이 있다는 점을 생각해보면 이에 따라 도메인인 Comment 엔티티의 생성자가 바뀌어야 하는것은 좋지 않다고 생각했다.(의존성문제)

따라서 생성자로 필드값을 받게 한 뒤, 

Dto의 Comment 생성 메서드

requestDto에서 Comment객체를 만드는 메서드를 통해 Comment를 생성하도록 하였다.

이렇게 하면 Comment요청을 받는 방식이 바뀐다 하더라도 도메인인 Comment 엔티티를 바꾸는 것이 아니라 Dto의 메서드만 바꾸면 된다.

 

 

 

(CommentService 내부 메서드 리팩토링)

Comment를 Optional로 불러오는 부분과, 해당 코멘트 삭제/수정 요청에 대해 작성자가 맞는지 혹은 관리자권한이 있는지를 검증하는 부분은 중복되기 때문에 따로 빼주었다.

 

<엔티티 연관관계 변경>

기존에는 한 객체에서 다른 객체를 조회할 일이 있으면 연관이 있다고 생각하여 @OneToMany나 @ManyToOne을 사용하여 연관관계를 맺었다. 전체적인 설계를 먼저 하고, 그에 맞게 연관관계를 맺어주는 것이 더 좋은 것 같다.

 

1. User와 Article의 관계 : 단방향 @ManyToOne

- 기존에는 User가 @OneToMany로 Article을, Article이 @ManyToOne으로 User를 바로보게 하였다.

   이렇게 했을 때 장점은 user.getArticles로 작성 글 목록을 한번에 가져올 수 있다는 것이 있다.

   회원 Entity가 모든 요청에 조회되는데 비해 비중이 굉장히 낮은 이점일 뿐더러, 이런식으로 설정 했을 때 fetch되어야하     는 시점이 언제인지, Article의 연관관계는 어떻게 되는지 등 생각할 것이 많아진다. 따라서 이번엔 

   Article이 User를 단방향 @ManyToOne으로 바라보는 관계로 설정하였다. 게시글이 조회될 땐 거의 무조건 작성자를  알    아야 하기 때문이다.

 

*Article의 User필드는 user_id가 아닌 User 객체로 한 이유는 Article을 조회할 때 username(작성자 닉네임)을 알아야하는데, Article의 필드에 가지고 있는 외래키는 고유id이다. 따라서 Article을 가지고 올 때 User객체를 같이 가져와야 작성자명을 찾기 위한 추가 쿼리가 나가지 않게 된다.

 

2. User와 Comment의 관계 : 단방향 @ManyToOne

- 1과 마찬가지의 이유로 Comment가 @ManyToOne으로 User를 바라보게 하였다.

 

3. Comment와 Article의 관계 : 연관관계없이 필드값으로 id 가지고있기

-  Article의 경우 전체 글 목록을 보여줄 때는 Comment를 알 필요가 없다. 따라서 게시글 상세보기 시에만 Comment 엔티티에서 Article의 id로 검색하여 댓글을 가져오면 된다.

Comment에서 Article에 대해 알아야할 정보는 Article의 고유번호밖에 없다. Comment를 통해서 해당 댓글이 달린 Article의 내용이나 작성자를 조회할 일이 없기 때문이다. 따라서 따로 연관관계를 맺어주지 않고, Article의 고유id를 하나의 필드article_id로 가지게 하였다. Article에서 해당 글의 Comment를 가지고 올 때는 findby article_id로 쿼리를 날리면 된다.

저장할 때는 어차피 요청 쿼리스트링에서 해당 게시글의 id를 가지고 들어오기 때문에 Article 객체를 찾는 추가적인 쿼리가 나갈일이 없다.

 

 

간단한 게시판 CRUD를 만들고 로그인, 회원가입 기능을 구현했다.

그런데, 현재는 서비스 부분에서 발생하는 Exception들을 처리해주지 않아 클라이언트 요청을 처리할 수 없을 때 아무런 응답도 주지 않고 있다.

 

클라이언트의 요청을 처리할 수 없을 때 처리할 수 없음을 알리고, 왜 처리할 수 없는지를 알려주는 것은 중요하다.

 

예를 들면 회원가입 요청 시, 닉네임에 숫자5가 들어가면 안된다는 것을 서버에서만 알고있고 클라이언트에게는 알려주지 않는다고 하자. Exception을 처리하지 않으면 유저는 계속

와 같이 알 수 없는 응답만 받을 것이다.

 

이를 해결하기 위해 @RestControllerAdvice, @ExceptionHandler를 사용했다.

 

@RestControllerAdvice : 컨트롤러에서 발생하는 Exception을 전역적으로 처리할 수 있도록 하는 어노테이션. 파라미터를 주어 적용범위를 특정 패키지나 클래스로 지정할 수 있다. @ControllerAdvice + @ResponseBody로, 바디를 리턴할 수 있다.

@ControllerAdvice

@ControllerAdvice나 @RestControllerAdvice가 달린 클래스 내부 메소드에는 @ExceptionHandler를 선언할 수 있다.

코드와 같이 @ExceptionHandler(Exception.Class)를 선언하면, 해당 Exception을 Controller에서 잡아서 처리해줄수있다.

파라미터에 custom Exception을 주면 해당 Exception을 잡아올 수 있다.

 

Exception이 발생한 경우 클라이언트에게 이를 알려주려면, 무언가를 리턴해주어야한다. HTTP 통신의 Response는 

헤더 // 바디 // HttpStatus로 구성된다.

기존에 내가 하던 것 처럼 Dto를 반환하게 되면, 헤더는 null, 바디에는 Dto의 내용을 JSON으로 파싱한 값이 담기고, 요청에 따른 응답이 리턴되었기에 HTTP status는 200(OK)로 반환하게 된다.

우리는 Exception을 처리했기 때문에 그에 맞춰 HTTP status를 바꿔줘야한다.

ResponseEntity 클래스는 Controller의 반환타입 그 자체인데, 여기에 Dto만 담아 리턴하면 기존에 하던것과 같다.

HttpStatus enum을 같이 담으면 헤더만 비어있게 되며, 헤더도 HttpHeaders 객체에 담아서 반환할 수 있다.

 

제네릭 타입을 받기 때문에 우리가 원하는 내용을 Body에 담을 수 있고, HttpStatus를 포함하는 다양한 생성자가 오버로딩 되어있어 편리하게 사용할 수 있다.

 

<구현>

전역으로 발생하는 IllegalArgumentException을 받아, 그 메세지를 Body에 담고, 내가 원하는 HttpStatus코드와 함께 클라이언트에게 리턴해준다. 쉽게 구현할 수 있다.

 

 

<추가공부>

클라이언트에게 조금 더 자세한 정보를 주고싶을 땐 어떨까 ?

Exception 클래스를 상속받은 나만의 커스텀Exception 클래스를 만들어보자

MyException의 생성자에 message, HttpStatus를 받아 Exception 자체가 HttpStatus를 가지게 만들고,

서비스 부분의 코드에서 Exception을 MyException("아직 미구현입니다", HttpStatus.METHOD_NOT_ALLOWED)

같이 생성해주고, ExceptionHandler에서는 ResponseEntity에 좀 더 자세한 내용을 담고싶었다.

HttpStatus Enum

{

"error code" : 405

"error type" : "Method not allowed",

"message" : "아직 미구현입니다"

}

와 같은 내용을 전달하고 싶었다.

HttpStatus는 Status 코드와, reasonPhrase라는 해당 코드에 대한 설명을 담고있기 때문에 조금더 Exception에 대한 상세한 처리를 할 수 있을 것 같았다.

 

기존 코드
변경 희망 코드

처리되지 않은 예외라고 뜬다.............

이를 해결하기 위해서는 메서드 뒤에 throws MyException을 붙여야하는데, 현재 checkRegisterRequest에 이를 붙이면

이 메서드를 가져다쓰는 메서드 모두에 throws MyException을 달아줘야한다. 심지어 제일 시작점인 Controller에도  throws MyException을 붙여줘야 해결이 되는 것을 확인했다.

 

나는 @ExceptionHandler를 통해서 예외를 처리해주었다고 생각했는데 그게 아니었던 모양이다.

또한, IllegalException으로 처리했을 때는 왜 이런 문제가 발생하지 않았는지 궁금해졌다. 

결국 기본 제공되는 Exception으로 일단 처리해놓았고, 나중에 해결방안을 찾아보고 글 수정해야겠다.

https://github.com/mottoslo/h99-Spring-Assignments/tree/main/assignment2/Spring_assignment_Lv2

 

GitHub - mottoslo/h99-Spring-Assignments: Spring week01-03 Assignments

Spring week01-03 Assignments. Contribute to mottoslo/h99-Spring-Assignments development by creating an account on GitHub.

github.com

 

 

https://imslo.tistory.com/62 의 업그레이드 버전

 

Spring Boot 간단한 게시판 만들어보기 // POSTMAN

POSTMAN은 View 없이도 API 동작을 실험해볼 수 있는 도구. JPA, h2(데이터베이스), Lombok 사용 디렉터리 생성 Controller // Repository // Service 디렉터리 만들어주기 + DB의 테이블 역할을 할 entity, + 클라이언

imslo.tistory.com

추가 기능 :

-회원가입

-로그인

-JWT인증

//회원가입페이지 요청// - 미구현
GET "api/user/register/"
==================================================================
// 회원 가입 요청//
POST "/user/register/"

Request : "userid" : "userid",
"username" : "name",
"password" : "Password",
"email" : "askdjf@naver.com"

Response : "가입 성공하였습니다" (임시)
==================================================================
//로그인페이지 요청// - 미구현
GET "user/login"

==================================================================
//로그인 요청//
POST "user/login"

Request : "userid" : "userid",
"password" : "password"

Response : Header => Authorization : Bearer <JWT>
redirect:/api/article
==================================================================
//전체 게시물 조회//

GET "api/article"

Request : -

Response : "createdAt" : "timestamp",
"modifiedAt" : "timestamp",
"id" : Long,
"title" : "title",
"content" : "content",
"author" : "author"
== List<ResponseDTO>

==================================================================
//게시물 등록 //

POST "api/article"

Request : Header => Authorization : Bearer <JWT>
Body =>
"title" : "title",
"content" : "content"

Response :
{ResponseDTO}

===================================================================
//특정 게시물 조회(by id)

GET "api/article/{id}"

Request : -

Response : {ResponseDTO}

===================================================================
// 게시물 수정 (by id)//

PUT "api/article/{id}"

Request : Header => Authorization : Bearer <JWT>
Body =>
"title" : "title",
"content" : "content",

Response : {ResponseDTO}

===================================================================
// 게시물 삭제 (by id)//
DELETE "api/article/{id}"

Request : Header => Authorization : Bearer <JWT>

Response : "success" : true

===================================================================





 
 

추가공부할것 : HttpServlet // JPA 엔티티 연관관계

'공부 > Spring' 카테고리의 다른 글

Spring Security  (0) 2023.04.25
ExceptionHandler // ResponseEntity // HTTP 상태 반환  (0) 2023.04.23
Auth // JWT  (0) 2023.04.19
Spring Boot 간단한 게시판 만들어보기 // POSTMAN  (0) 2023.04.18
Spring MVC 패턴 이해하기  (0) 2023.04.16

POSTMAN은 View 없이도 API 동작을 실험해볼 수 있는 도구.

 

 

<Spring Boot, Postman으로 게시판 CRUD 만들어보기>

JPA, h2(데이터베이스), Lombok 사용

 

<1> 디렉터리 생성

Controller // Repository // Service 디렉터리 만들어주기

+ DB의 테이블 역할을 할 entity,

+ 클라이언트의 Request 데이터를 받아올 DTO(Data Transfer Object) 디렉터리 생성

DTO 사용 이유 : API 간 필요한 데이터만 주고받을 수 있도록(노출 ↓) ==> https://www.okta.com/kr/identity-101/dto/

 

데이터 전송 객체(DTO) 정의 및 사용 방법 | Okta Identity Korea

데이터 전송 객체(DTO)는 데이터 전송에 사용되는 설계 패턴입니다. 여기에서 DTO 패턴이 무엇인지, 그리고 데이터 전송 객체의 작동 원리에 대해 알아보세요.

www.okta.com

 

<2> API 명세

귀찮아서 중괄호 생략..... 요청 url과 METHOD는 역할에 맞게.

같은 api/article/{id}로 PUT요청, DELETE요청에 따라 Controller에 의해 다른 메서드로 매핑된다.

REST 설계에서 요청 URL의 / 구분은 하나의 계층을 나타내도록 하고 있다.

 

<3> Entity, Repository 작성

Article entity

Entity는 NoArgsConstructor가 있어야함

 

ArticleRepository를 만들어주고 JPA를 연동함

이제 이 ArticleRepository 클래스를 내 데이터베이스처럼 사용할 수 있다.

 

<4> Controller 적절하게 만들어주기

ArticleController 클래스 안의 요청들은 /api를 타고 오도록 @RequestMapping

각 요청은 METHOD에 맞게 작성하면 되고, 변수는 클라이언트의 요청에 따라 적절하게 받아준다.

Controller에서는 적절한 DTO를 사용하여 클라이언트의 요청을 받고, 이에따른 적절한 Service함수를 호출한다.

Service에서 적절한 작업을 마치고 넘어온 DTO를 다시 클라이언트에게 반환해주면 끗

 

<5> Service 작성

클래스 내에 알맞은 Repository를 final 변수로 가지고, 들어오는 요청과 서비스로직에 따라 Repository에 CRUD 해준다.

작업을 마치면 ResponseDto에 담아 다시 Controller로 내려준다.

 

 

<PostMan으로 확인해보기>

https://web.postman.co/workspace

 

Postman API | Postman Public Workspace

The Postman API enables you to programmatically access data stored in your Postman account. Getting started You can get started with the Postman API by forkin

www.postman.com

가입하고, workspace 열어준다.

로컬환경에서 테스트해볼 경우, 오류가 나고 아래에 Postman agent 설치하도록 나온다.

설치해주면 localhost에 요청보낼수있다.

요청 URL과 METHOD를 알맞게 입력 후, 요청데이터를 담아보낼 방법을 선택한 후, 그에맞게 입력

오른쪽 SEND버튼을 누르면 아래에 응답이 뜬다.

+ Recent posts