제목, 내용을 포함한다.id(PK, primary key)도 같이 Auto-increment 형식으로 저장돼야 한다.id, 제목, 내용의 값이 포함돼야 한다.id(PK, primary key)로 특정 게시글을 조회한다.id, 제목, 내용의 값이 포함돼야 한다.id(PK, primary key)로 특정 게시글을 수정할 수 있어야 한다.제목, 내용을 수정할 수 있다.id(PK, primary key)로 특정 게시글을 삭제할 수 있어야 한다.
- java 17
- Spring Boot 3.0.12
- JUnit5
- MariaDB
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 메서드를 엔티티 클래스에 선언했다.
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());
}
}
}
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);
}
}
JpaRepository만 상속받고, 따로 구현한 건 없었다.
package com.example.joy0987.post.dto;
import lombok.Builder;
@Builder
public record PostResponseDTO (
int id,
String title,
String content
) {};
record 가 아닌 일반 클래스로 구현했고, Entity로 변경하는 메서드를 추가해주었다.
hasError()를 통해 유효성 검사를 진행했어서, 반드시 사용해야하는 것인줄 알았다. @Valid만 사용해도 Exception이 발생한다고 한다.@Valid 유효성 검사 과정
- SpringBoot 에서 모든 요청은 프론트 컨트롤러인 DispatcherServlet 을 통해 Controller 로 전달됩니다.
- 전달 과정에서는 컨트롤러 메소드의 객체를 만들어주는 ArgumentResolver 가 동작하는데, @Valid 어노테이션도 이 ArgumentResolver 에 의해 처리가 됩니다.
- 검증 도중 오류가 있다면 MethodArgumentNotValidException 예외가 발생하고, 디스패처 서블릿에 기본으로 등록된 예외 리졸버(Exception Resolver)인 DefaultHandlerExceptionResolver 에 의해 400 BadRequest 가 발생하게 됩니다.
public sealed interface DTO permits C {
@Builder
record Create(
Long id,
String title,
String content
) implements ResponseDTO {
}
... 그 외 record
}