[Study] 게시글 생성, 조회, 수정, 삭제 Rest api 구현

joy0987·2023년 11월 2일

스터디

목록 보기
4/4

요구사항

  1. 게시글 작성 기능
  • 게시글은 제목, 내용을 포함한다.
  • 게시글이 저장될 때, id(PK, primary key)도 같이 Auto-increment 형식으로 저장돼야 한다.
  • 게시글 작성에 성공했을 때, 응답값으로 작성된 게시글에 대한 정보를 보여주어야 한다.
  1. 게시글 전체 조회 기능
  • 게시글을 조회할 때 id, 제목, 내용의 값이 포함돼야 한다.
  1. 특정 게시글 조회 기능
  • 게시글의 id(PK, primary key)로 특정 게시글을 조회한다.
  • 게시글을 조회할 때 id, 제목, 내용의 값이 포함돼야 한다.
  1. 특정 게시글 수정 기능
  • 게시글의 id(PK, primary key)로 특정 게시글을 수정할 수 있어야 한다.
  • 게시글의 제목, 내용을 수정할 수 있다.
  • 게시글 수정에 성공했을 때, 응답값으로 수정된 게시글에 대한 정보를 보여주어야 한다.
  1. 특정 게시글 삭제 기능
  • 게시글의 id(PK, primary key)로 특정 게시글을 삭제할 수 있어야 한다.




사용 기술

  • java 17
  • Spring Boot 3.0.12
  • JUnit5
  • MariaDB




구현

Post (Entity)

package com.example.joy0987.post.entity;

import jakarta.persistence.*;
import lombok.*;

@Entity
@Getter
@Setter
@ToString
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Post {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "POST_ID")
    private int postId;

    @Column(name = "POST_TITLE", nullable = false, length = 300)
    private String postTitle;

    @Column(name = "POST_CONTENT", nullable = false, length = 10)
    private String postContent;

    public void update(String title, String content) {
        this.postTitle = title;
        this.postContent = content;
    }

}

게시글 수정을 위한 update 메서드를 엔티티 클래스에 선언했다.


Controller

package com.example.joy0987.post.api;

import com.example.joy0987.post.dto.PostUpdateRequestDTO;
import com.example.joy0987.post.dto.PostRequestDTO;
import com.example.joy0987.post.dto.PostResponseDTO;
import com.example.joy0987.post.entity.Post;
import com.example.joy0987.post.service.PostService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@Slf4j
@RequiredArgsConstructor
@RequestMapping("/posts")
public class PostController {

    private final PostService postService;

    @GetMapping
    public ResponseEntity<?> getPostList() {
        log.info("[getPostList]");

        try {
            List<Post> responseDTO = postService.getPostList();
            return ResponseEntity.ok(responseDTO);
        } catch (NullPointerException e) {
            return ResponseEntity.badRequest().body("게시글이 존재하지 않습니다.");
        }
    }

    @GetMapping("/{postId}")
    public ResponseEntity<?> getPost(
            @PathVariable("postId") int postId
    ) {
        log.info("[getPost] postId : {}", postId);

        try {
            PostResponseDTO responseDTO = postService.getPost(postId);
            return ResponseEntity.ok(responseDTO);
        } catch (NullPointerException e) {
            return ResponseEntity.badRequest().body(e.getMessage());
        }
    }

    @PostMapping
    public ResponseEntity<?> insertPost(
        @RequestBody PostRequestDTO requestDTO
    ) {
        log.info("[insertPost] postTitle : {}", requestDTO.getPostTitle());

        if (requestDTO.getPostTitle() == null || requestDTO.getPostContent() == null) {
            return ResponseEntity.badRequest().body("게시글 제목 또는 내용이 비어있습니다.");
        }

        try {
            Post saved = postService.insertPost(requestDTO);
            return ResponseEntity.ok(saved);
        } catch (RuntimeException e) {
            return ResponseEntity.internalServerError().body(e.getMessage());
        }
    }

    @PutMapping
    public ResponseEntity<?> updatePost(
            @RequestBody PostUpdateRequestDTO requestDTO
    ) {
        log.info("[updatePost] postId : {}", requestDTO.getPostId());

        if (requestDTO.getPostTitle() == null || requestDTO.getPostContent() == null) {
            return ResponseEntity.badRequest().body("게시글 제목 또는 내용이 비어있습니다.");
        }

        try {
            PostResponseDTO responseDTO = postService.updatePost(requestDTO);
            return ResponseEntity.ok(responseDTO);
        } catch (RuntimeException e) {
            return ResponseEntity.internalServerError().body(e.getMessage());
        }
    }

    @DeleteMapping("/{postId}")
    public ResponseEntity<?> deletePost(
            @PathVariable("postId") int postId
    ) {
        log.info("[deletePost] postId : {}", postId);

        try {
            postService.deletePost(postId);
            return ResponseEntity.ok("Delete Success");
        } catch (RuntimeException e) {
            return ResponseEntity.internalServerError().body(e.getMessage());
        }
    }
}

Service

package com.example.joy0987.post.service;

import com.example.joy0987.post.dto.PostUpdateRequestDTO;
import com.example.joy0987.post.dto.PostRequestDTO;
import com.example.joy0987.post.dto.PostResponseDTO;
import com.example.joy0987.post.entity.Post;
import com.example.joy0987.post.repository.PostRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@Slf4j
@RequiredArgsConstructor
public class PostService {

    @Autowired
    private final PostRepository postRepository;

    public List<Post> getPostList() {
        List<Post> posts = postRepository.findAll();

        return posts;
    }

    public Post insertPost(PostRequestDTO requestDTO) {
        Post saved = postRepository.save(Post.builder()
                .postTitle(requestDTO.getPostTitle())
                .postContent(requestDTO.getPostContent())
                .build());

        return saved;
    }

    public PostResponseDTO getPost(int postId) {
        Post post = postRepository.findById(postId).orElseThrow(() -> {
            log.error("[존재하지 않는 게시글 조회 요청] postId : {}", postId);
            throw new NullPointerException("게시글이 존재하지 않습니다");
        });

        PostResponseDTO postResponseDTO = PostResponseDTO.builder()
                .id(post.getPostId())
                .title(post.getPostTitle())
                .content(post.getPostContent())
                .build();

        return postResponseDTO;
    }

    public PostResponseDTO updatePost(PostUpdateRequestDTO requestDTO) {
        Post post = postRepository.findById(requestDTO.getPostId()).orElseThrow(() -> {
            log.error("[존재하지 않는 게시글 조회 요청] postId : {}", requestDTO.getPostId());
            throw new NullPointerException("게시글이 존재하지 않습니다.");
        });

        post.update(requestDTO.getPostTitle(), requestDTO.getPostContent());

        PostResponseDTO postResponseDTO = PostResponseDTO.builder()
                .id(post.getPostId())
                .title(post.getPostTitle())
                .content(post.getPostContent())
                .build();

        return postResponseDTO;
    }

    public void deletePost(int postId) {
        Post post = postRepository.findById(postId).orElseThrow(() -> {
            log.error("[존재하지 않는 게시글 삭제 요청] postId : {}", postId);
            throw new NullPointerException("게시글이 존재하지 않습니다.");
        });

        postRepository.deleteById(postId);
    }
}

Repository

JpaRepository만 상속받고, 따로 구현한 건 없었다.


ResponseDTO

package com.example.joy0987.post.dto;

import lombok.Builder;

@Builder
public record PostResponseDTO (
     int id,
     String title,
     String content
) {};

RequestDTO

record 가 아닌 일반 클래스로 구현했고, Entity로 변경하는 메서드를 추가해주었다.




피드백과 알게된 점

1. Getter, Setter ... 등 DTO에 사용되는 어노테이션을 모아둔 @Data 어노테이션을 사용할 수 있다.

  • 다만, Getter는 필요하고 Setter는 필요없는 경우처럼 커스텀이 필요할 수 있으므로 실무에서는 사용을 자제한다고 한다.

2. @Valid 어노테이션 사용시 BindingResult를 반드시 사용하지 않아도 된다.

  • 이전에 프로젝트할 때 BindingResult의 hasError()를 통해 유효성 검사를 진행했어서, 반드시 사용해야하는 것인줄 알았다. @Valid만 사용해도 Exception이 발생한다고 한다.

@Valid 유효성 검사 과정

  • SpringBoot 에서 모든 요청은 프론트 컨트롤러인 DispatcherServlet 을 통해 Controller 로 전달됩니다.
  • 전달 과정에서는 컨트롤러 메소드의 객체를 만들어주는 ArgumentResolver 가 동작하는데, @Valid 어노테이션도 이 ArgumentResolver 에 의해 처리가 됩니다.
  • 검증 도중 오류가 있다면 MethodArgumentNotValidException 예외가 발생하고, 디스패처 서블릿에 기본으로 등록된 예외 리졸버(Exception Resolver)인 DefaultHandlerExceptionResolver 에 의해 400 BadRequest 가 발생하게 됩니다.

3. Controller 테스트 시 MockMVC를 사용하자.

  • MockMVC : 개발한 웹 프로그램을 실제 서버에 배포하지 않고도 테스트를 위한 요청을 제공하는 수단 (기본제공)
  • Postman을 사용하지 않고 테스트를 할 수 있게 해준다.
  • MockMVC에 대해서 따로 정리해야겠다~!

4. sealed, permit, record 의 사용

public sealed interface DTO permits C {

@Builder
    record Create(
            Long id,
            String title,
            String content
    ) implements ResponseDTO {
    }
    
    ... 그 외 record
}
  • CRUD를 구현하다보면 구조가 다른 여러 DTO가 나올 수 있다. 이런 경우 Java17에서는 sealed 와 permits를 통해 DTO를 상위클래스로 추상화하고 용도에 따라 상세 DTO를 편리하게 구현할 수 있다.

5. Global ExceptionHandler

  • 공통 예외처리 핸들러를 구현하긴 했는데 핸들러가 예외를 못잡아서 뭔가 했더니 컨트롤러에서 catch를 해버렸기 때문에 안되는거였다...ㅎ service에서 예외를 던지면 공용 ExceptionHandler에서 잡아서 처리해주고 컨트롤러에서 반드시 필요한 catch만 사용할 수 있게된다.
  • Error 메세지가 중복되는 경우 핸들러에서 log를 찍어주면 좋다.
profile
아자아자

0개의 댓글