Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

3차 세미나 실습 과제 #13

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open

3차 세미나 실습 과제 #13

wants to merge 4 commits into from

Conversation

sebbbin
Copy link
Member

@sebbbin sebbbin commented Apr 30, 2024

🍃 Issue

close #12

⚡️ 구현 내용

🔑 api 명세서(클릭)

① Exception

CustomizedException.java
에러메세지를 반환하기 위해서 구현

public class CustomizedException extends BusinessException {
    public CustomizedException(ErrorMessage errorMessage) {
        super(errorMessage);
    }
}

SuccessStatusResponse.java
GET API 에서 SuccessStatusResponse를 Data를 담아보낼 수 있도록 변경

public record SuccessStatusResponse(
        int status, String message,  Object data
) {
    public static SuccessStatusResponse of(SuccessMessage successMessage){
        return new SuccessStatusResponse(successMessage.getStatus(), successMessage.getMessage(), null); //데이터 없을 시 null
    }
    public static SuccessStatusResponse of(int status, String message, Object data) {
        return new SuccessStatusResponse(status, message, data);
    } //data가 있을 시 -> 해당 of 를 통해서 데이터도 반환

}

② Blog

멤버의 블로그 소유권을 검증하는 절차가 한 번만 나오는 것이 아닌, 여러 메소드에서 검증하는 것을 보고 이를 따로 메서드로 분리해서 검증하고자 구현한 메서드

// 멤버의 블로그 소유권을 검증하는 메서드
    public void validateBlogMember(Long blogId, Long memberId) {
        Blog blog = this.findBlogById(blogId);
        if (!blog.getMember().getId().equals(memberId)) {
            throw new CustomizedException(ErrorMessage.UNAUTHORIZED_ACCESS);
        }
    }

만약 둘이 불일치 할 시 UNAUTHORIZED_ACCESS 메세지를 반환합니다.

③ Post

POST - 블로그 글 작성

PostCreateRequest.java
블로그 글 작성 시 해당 조건을 지켜야 작성할 수 있도록 Validation을 설정해주었습니다.

public record PostCreateRequest(
        @NotBlank(message = "제목을 입력해주세요.")
        @Size(max = 100, message = "제목은 100자 이하로 작성해주세요.")
        String title,
        @NotBlank(message = "내용을 입력해주세요.")
        @Size(max = 1000, message = "내용은 1000자 이하로 작성해주세요.")
        String content) {

}
스크린샷 2024-05-01 오전 7 03 09 스크린샷 2024-05-01 오전 7 02 50

PostService.java

public String writePost(Long blogId, Long memberId, PostCreateRequest postCreateRequest) {
        // 블로그 소유권 검증
        blogService.validateBlogMember(blogId, memberId);
        // 글 생성 및 저장 로직
        Post post = postRepository.save(Post.create(
                blogService.findBlogById(blogId), // Blog 객체 검색
                postCreateRequest.title(),
                postCreateRequest.content()
        ));
        return post.toString();
    }

PostController.java
@Valid 지정

@PostMapping("/post")
    public ResponseEntity<SuccessStatusResponse> writePost(@RequestHeader Long blogId, @RequestHeader Long memberId,
                                                           @Valid @RequestBody PostCreateRequest  postCreateRequest) {
        return ResponseEntity.status(HttpStatus.CREATED).header("Location", postService.writePost(blogId, memberId, postCreateRequest))
                .body(SuccessStatusResponse.of(SuccessMessage.POST_CREATE_SUCCESS));
    }
  1. header로 블로그 아이디, 멤버 아이디를 받은 뒤 블로그 소유권 검증(해당 멤버 아이디의 블로그 아이디와 일치하는지, 접근 권한이 있는지)을 진행
  2. 일치할 시 PostCreateRequest에 맞게 작성된 Body를 DB에 저장
  3. 성공 시 POST_CREATE_SUCCESS 메세지를 반환

memberId의 blogId와 header의 blogId 불일치
스크린샷 2024-05-01 오전 6 19 40

blogId의 memberId와 header의 memberId 불일치
스크린샷 2024-05-01 오전 6 20 19

GET - 작성된 글 전체 불러오기

PostResponse.java

public record PostResponse(Long id, String title, String content) {
}

PostRepository.java

public interface PostRepository extends JpaRepository<Post, Long> {
    List<Post> findByBlogId(Long blogId);
}

PostService.java

    public List<Post> findAllPostsByBlogId(Long blogId, Long memberId) {
        // 블로그 소유권 검증
        blogService.validateBlogMember(blogId, memberId);
        List<Post> posts = postRepository.findByBlogId(blogId);
        if (posts.isEmpty()) {
            throw new CustomizedException(ErrorMessage.POST_NOT_FOUND_BY_BLOG_ID_EXCEPTION);
        }
        return posts;
    }

PostController.java

@GetMapping("/post")
    public ResponseEntity<SuccessStatusResponse> findAllPostsByBlogId(@RequestHeader Long blogId, @RequestHeader Long memberId) {
        List<Post> posts = postService.findAllPostsByBlogId(blogId, memberId);
        List<PostResponse> responses = posts.stream()
                .map(post -> new PostResponse(post.getId(), post.getTitle(), post.getContent()))
                .collect(Collectors.toList());
        return ResponseEntity.ok(SuccessStatusResponse.of(
                SuccessMessage.POST_ALL_FIND_SUCCESS.getStatus(),
                SuccessMessage.POST_ALL_FIND_SUCCESS.getMessage(),
                responses
        ));
    }
  1. 블로그 소유권 검증
  2. post 리스트화
  3. 그런데 empty일 시 아무것도 없음을 반환
  4. 조건 충족 시 blogId의 전체 글 반환

GET - 작성된 글 한개 불러오기

PostService.java

public Post findPostById(Long blogId, Long memberId, Long postId) {
        // 블로그 소유권 검증
        blogService.validateBlogMember(blogId, memberId);
        // 특정 포스트 ID로 게시물 검색
        return postRepository.findById(postId)
                .orElseThrow(() -> new CustomizedException(ErrorMessage.POST_NOT_FOUND_BY_POST_ID_EXCEPTION));
    }

PostController.java

 @GetMapping("/post/{postId}")
    public ResponseEntity<SuccessStatusResponse> getPostById(@RequestHeader Long blogId, @RequestHeader Long memberId,
                                                             @PathVariable Long postId) {
        Post post = postService.findPostById(blogId, memberId, postId);
        PostResponse postResponse = new PostResponse(post.getId(), post.getTitle(), post.getContent());
        return ResponseEntity.ok(new SuccessStatusResponse(
                SuccessMessage.POST_FIND_SUCCESS.getStatus(),
                SuccessMessage.POST_FIND_SUCCESS.getMessage(),
                postResponse
        ));
    }
  1. 블로그 소유권 검증
  2. 특정 포스트 ID로 게시물 검색
  3. 그런데 없을 시 postId로 찾을 수 없음을 반환
  4. 조건 충족 시 blogId의 postId 글 반환

🍀 결과

POST - 블로그 글 작성

image

GET - 작성된 글 전체 불러오기

스크린샷 2024-05-01 오전 6 21 55

GET - 작성된 글 한개 불러오기

스크린샷 2024-05-01 오전 6 22 16

?의문점 및 고려사항

  1. member랑 blog랑 one to one관계면 굳이 memberId를 requestheader에 넣지 않아도 될 것이라고 생각했는데 둘의 권한 검증을 위해 두개 다 넣어주었습니다.
  2. pathvarialble이 아닌 requestheader을 통해 validation을 한 이유는 보안적으로 더 우수할 것 같아 선택했습니다.
  3. 멤버의 블로그 소유권을 검증하는 절차가 중복돼서 메서드로 분리했습니다.
  4. controller의 이 return 부분을 간소화할 수 있을까요?
return ResponseEntity.ok(new SuccessStatusResponse(
                SuccessMessage.POST_FIND_SUCCESS.getStatus(),
                SuccessMessage.POST_FIND_SUCCESS.getMessage(),
                postResponse
        ));

@sebbbin sebbbin changed the title 3차 세미나 과제 3차 세미나 실습 과제 Apr 30, 2024
Copy link

@Parkjyun Parkjyun left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

세빈님 과제하시느라 고생하셨습니다~


private final PostService postService;

@PostMapping("/post")
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rest api에서는 단수형보다 복수형이 더 선호된다고 알고 있습니다!

그리고 글 작성시에 /api/v1/post로 보내시고 있는데
rest api�는 리소스의 식별이 url에 의해 명확하게 이루어 져야 한다 생각해서
저는 /v1/blogs/{blogId}/posts를 엔드포인트로 사용하고 있습니다
blogid를 pathvariable이 아닌 헤더로 보내주면 api가 어떤 블로그에 대해 글을 작성하는지 명확해지지 않는다고 생각하는데 세빈님의 생각은 어떠신지 궁금합니다

Comment on lines +28 to +29
return ResponseEntity.status(HttpStatus.CREATED).header("Location", postService.writePost(blogId, memberId, postCreateRequest))
.body(SuccessStatusResponse.of(SuccessMessage.POST_CREATE_SUCCESS));
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

status와 헤더를 직접 입력하는 대신에 .created를 사용해보아도 좋을 것 같아요

public void validateBlogMember(Long blogId, Long memberId) {
Blog blog = this.findBlogById(blogId);
if (!blog.getMember().getId().equals(memberId)) {
throw new CustomizedException(ErrorMessage.UNAUTHORIZED_ACCESS);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

�403대신 401을 사용하신 이유가 있으실까요?
만약 나중에 로그인이 제대로 구현되어 401에대한 처리는 필터쪽에서 공통적으로 처리해준다면
해당 비즈니스 로직에서는 블로그에 대한 작성 권한이 있는가에 대한 예외(403)를 발생시키는 것이 적합하다 생각합니다!

Comment on lines +43 to +44
Blog blog = this.findBlogById(blogId);
if (!blog.getMember().getId().equals(memberId)) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

existsBy...를 사용해서 바로 boolean받아와도 좋을 것 같습니다!

Comment on lines +5 to +9
public class CustomizedException extends BusinessException {
public CustomizedException(ErrorMessage errorMessage) {
super(errorMessage);
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

비슷한 역할을 하는BusinessException이 있는데 CustomizedException을 만드신 이유가 있으실까요?

Comment on lines +35 to +37
List<PostResponse> responses = posts.stream()
.map(post -> new PostResponse(post.getId(), post.getTitle(), post.getContent()))
.collect(Collectors.toList());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dto로의 변환 작업은 service layer에서 수행하는 것이 어떨까요?
controller에서는 요청데이터의 유효성 검사, 서비스 메소드 호출, 응답상태, 데이터 전송정도만 하고
service에서 비즈니스로직, dto로의 변환을 해주는 것이 계층간의 역할분리에 도움이 된다 생각합니당

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

더해서 PostResponse에서 생성자가 아닌 정적 팩토리 메서드 패턴으로 매개변수로 Post 자체를 받으면
.map(PostResponse::of) 를 사용할 수 있는데, 한 번 고민해봐도 좋을 것 같아요 :)

}
public static SuccessStatusResponse of(int status, String message, Object data) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위에 작성하신 것처럼 SuccessMessage로 캡슐화해서 넘겨주는 것은 어떨까요?

Comment on lines +36 to +37
// 블로그 소유권 검증
blogService.validateBlogMember(blogId, memberId);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 블로그 소유권에 대한 검증을 하신 이유를 알 수 있을까요?
블로그의 글은 블로그 주인 말고도 모두 접근할 수 있어야 된다 생각해서 여쭤봅니당

public ResponseEntity<SuccessStatusResponse> getPostById(@RequestHeader Long blogId, @RequestHeader Long memberId,
@PathVariable Long postId) {
Post post = postService.findPostById(blogId, memberId, postId);
PostResponse postResponse = new PostResponse(post.getId(), post.getTitle(), post.getContent());
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기도 마찬가지로 service에서 변환해주는 것이 좋을 것 같습니다.

Comment on lines +46 to +47
// 블로그 소유권 검증
blogService.validateBlogMember(blogId, memberId);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위와 같은 의문이 드네용

@minwoo0419
Copy link

전체적으로 잘 구현하신 것 같네요! 고생하셨습니다!!

public void validateBlogMember(Long blogId, Long memberId) {
Blog blog = this.findBlogById(blogId);
if (!blog.getMember().getId().equals(memberId)) {
throw new CustomizedException(ErrorMessage.UNAUTHORIZED_ACCESS);

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 알기론 권한에 대한 문제는 403에러가 맞는 것 같습니다! 혹시라도 401에러를 반환해주신 특별한 이유가 있으신가요??

@lreowy lreowy requested a review from sohyundoh May 3, 2024 14:31
Copy link
Member

@lreowy lreowy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

세빈님 고생하셨습니당~!
덕분에 몰랐던 어노테이션 하나 알아가요
3주동안 코드리뷰조 같이 해서 좋았어요 ㅎㅁㅎ

import jakarta.validation.constraints.Size;

public record PostCreateRequest(
@NotBlank(message = "제목을 입력해주세요.")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@notblank 어노테이션은 처음 보는데 입력이 공백으로 처리되는 걸 막아주는 건가요? 궁금해서 남깁니당

Copy link
Contributor

@sohyundoh sohyundoh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

과제하느라 고생하셨습니다 :)

몇가지 좋은 코멘트가 이미 많이 담겨 있어서 구경헀네요!
저도 몇가지 남겨놓았으니 코드리뷰에 대한 답글 달아주세요 >< 모두들 궁금해하고 있네요!
화이팅입니다!

Comment on lines +35 to +37
List<PostResponse> responses = posts.stream()
.map(post -> new PostResponse(post.getId(), post.getTitle(), post.getContent()))
.collect(Collectors.toList());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

더해서 PostResponse에서 생성자가 아닌 정적 팩토리 메서드 패턴으로 매개변수로 Post 자체를 받으면
.map(PostResponse::of) 를 사용할 수 있는데, 한 번 고민해봐도 좋을 것 같아요 :)

Comment on lines +50 to +54
return ResponseEntity.ok(new SuccessStatusResponse(
SuccessMessage.POST_FIND_SUCCESS.getStatus(),
SuccessMessage.POST_FIND_SUCCESS.getMessage(),
postResponse
));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Response에도 정적 팩터리 메서드 패턴을 사용해도 좋을 것 같아요!

private final BlogService blogService;
private final MemberService memberService;

public String writePost(Long blogId, Long memberId, PostCreateRequest postCreateRequest) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

save를 수행할 경우 @Transactional을 사용하지 않아도 됩니다!!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3차 세미나 과제
5 participants