지난 글에서는 Board의 구현에 대해 작성하였고, 이번 글에서는 Board와 Post의 관계를 고려하여 Post를 구현하는 것을 작성해보고자 한다.
private Long id;
private String title;
private String content;
private String writer;
private String password;
private Long boardId;
public interface PostRepository {
PostDto create(Long boardId, PostDto dto);
PostDto read(Long boardId, Long postId);
Collection<PostDto> readAll(Long boardId);
boolean update(Long boardId, Long postId, PostDto dto);
boolean delete(Long boardId, Long postId, String password);
}
InMemoryBoardRepository
와 마찬가지로 실제 DB에 연결되어 프로젝트가 돌아가는 것이 아니라 로컬에서 실행되므로 클래스 명에 InMemory를 붙였다.@Repository
public class InMemoryPostRepository implements PostRepository {
private final BoardRepository boardRepository;
private final Map<Long, PostDto> memory = new HashMap<>();
private Long lastIndex = 0L;
public InMemoryPostRepository(@Autowired BoardRepository boardRepository) {
this.boardRepository = boardRepository;
}
@Override
public PostDto create(Long boardId, PostDto dto) {
BoardDto boardDto = this.boardRepository.read(boardId);
if (boardDto == null) return null;
dto.setBoardId(boardId);
lastIndex++;
dto.setId(lastIndex);
memory.put(lastIndex, dto);
return dto;
}
@Override
public PostDto read(Long boardId, Long postId) {
PostDto postDto = memory.getOrDefault(postId, null);
if(postDto == null) {
return null;
}
else if (!Objects.equals(postDto.getBoardId(), boardId)) {
return null;
}
return postDto;
}
@Override
public Collection<PostDto> readAll(Long boardId) {
if(boardRepository.read(boardId) == null) return null;
Collection<PostDto> postList = new ArrayList<>();
memory.forEach((postId, postDto) -> {
if (Objects.equals(postDto.getBoardId(), boardId))
postList.add(postDto);
});
return postList;
}
@Override
public boolean update(Long boardId, Long postId, PostDto dto) {
PostDto targetPost = memory.getOrDefault(postId, null);
if(targetPost == null) {
return false;
}
else if(!Objects.equals(targetPost.getBoardId(), boardId)) {
return false;
}
else if(!Objects.equals(targetPost.getPassword(), dto.getPassword())) {
return false;
}
targetPost.setTitle(
dto.getTitle() == null ? targetPost.getTitle() : dto.getTitle());
targetPost.setContent(
dto.getContent() == null ? targetPost.getContent() : dto.getContent());
return true;
}
@Override
public boolean delete(Long boardId, Long postId, String password) {
PostDto targetPost = memory.getOrDefault(postId, null);
if (targetPost == null) {
return false;
}
else if(!Objects.equals(targetPost.getBoardId(), boardId)) {
return false;
}
else if(!Objects.equals(targetPost.getPassword(), password)) {
return false;
}
memory.remove(postId);
return true;
}
}
@Repository
어노테이션을 붙인다.implements PostRepository
를 작성한다.boardRepository
변수를 가지고 있다. 생성자에서 주입받아 사용한다.lastIndex
변수는 PK인 id를 위한 변수이다.memory
변수는 Long 타입을 key로, PostDto 타입을 value로 갖는다. Long 형의 데이터 타입(PK)로 PostDto를 조회하는 등의 행위를 함을 알 수 있다.구현된 메소드를 하나씩 살펴보자.
Board보다 로직이 까다로운데, Board에 대한 검증이 있어서 그렇다.
create : 매개변수로 들어온 boardId 값이 존재하는지 검증하고, 존재한다면 해당 boardId를 세팅하고 인덱스 값을 증가시켜 memory에 dto를 저장한다. 존재하지 않는다면 null을 리턴한다.
read : Map의 getOrDefault 메소드를 이용해 PostDto를 반환한다. 검증은 두 단계를 거치는데, 첫 번째는 매개변수로 받은 postId 값을 통해 postDto가 존재하는지 확인한다. 존재하지 않으면 null을 리턴한다. 두 번째는 매개변수로 받은 boardId가 postId로 찾은 Post의 boardId와 동일한지 확인한다. 동일하지 않다면 null을 리턴한다. 두 경우에 걸리지 않는다면 postDto를 반환한다.
readAll : 매개변수로 전달받은 boardId에 해당하는 모든 post를 Collection에 담아서 반환한다. boardId가 null 이라면 null을 반환한다. 그렇지 않다면 빈 ArrayList를 만들고, forEach를 이용해 boardId에 대한 검증을 하고 검증이 정상적으로 되었다면 postList에 postDto를 추가한다.
update : 매개변수로 전달받은 postId로 post를 찾는다. post가 존재하지 않으면 false를 리턴한다. postId로 찾은 boardId와 매개변수로 전달받은 boardId가 일치하지 않는다면 false를 리턴한다. postId로 찾은 비밀번호와 dto에 담긴 비밀번호가 일치하지 않으면 false를 리턴한다. false에 걸리지 않는다면, 제목과 내용이 null이 아닌 경우에 한해서 업데이트를 진행하고 true를 리턴한다.
delete : update와 유사하기 때문에 생략한다.
@RestController
@RequestMapping("board/{boardId}/post")
public class PostController {
private static final Logger logger = LoggerFactory.getLogger(PostController.class);
private final PostRepository postRepository;
public PostController(@Autowired PostRepository postRepository) {
this.postRepository = postRepository;
}
@PostMapping
public ResponseEntity<PostDto> createPost(@PathVariable("boardId") Long boardId,
@RequestBody PostDto dto) {
PostDto result = this.postRepository.create(boardId, dto);
return ResponseEntity.ok(result.passwordMasked());
}
@GetMapping("{postId}")
public ResponseEntity<PostDto> readPost(@PathVariable("boardId") Long boardId,
@PathVariable("postId") Long postId) {
PostDto postDto = this.postRepository.read(boardId, postId);
if (postDto == null) return ResponseEntity.notFound().build();
else return ResponseEntity.ok(postDto.passwordMasked());
}
@GetMapping ResponseEntity<Collection<PostDto>> readPostAll(@PathVariable("boardId") Long boardId) {
Collection<PostDto> postList = this.postRepository.readAll(boardId);
if(postList == null) return ResponseEntity.notFound().build();
else return ResponseEntity.ok(postList);
}
@PutMapping("{postId}")
public ResponseEntity<?> updatePost(@PathVariable("boardId") Long boardId,
@PathVariable("postId") Long postId,
@RequestBody PostDto dto) {
if (!postRepository.update(boardId, postId, dto))
return ResponseEntity.notFound().build();
return ResponseEntity.noContent().build();
}
@DeleteMapping("{postId}")
public ResponseEntity<?> deletePost(@PathVariable("boardId") Long boardId,
@PathVariable("postId") Long postId,
@RequestParam("password") String password) {
if (!postRepository.delete(boardId, postId, password))
return ResponseEntity.notFound().build();
return ResponseEntity.noContent().build();
}
}
@RequestMapping("board/{boardId}/post")
이렇게 작성한 이유는, Post가 어떤 Board에 속해있는지 나타내기 위함이다.passwordMasked()
return ResponseEntity.ok()
와 같이 작성하면 응답에 항상 비밀번호가 노출된다.public PostDto passwordMasked() {
return new PostDto(
this.id,
this.title,
this.content,
this.writer,
"****",
this.boardId
);
}
****
로 나오게 된다.연관관계를 가지는 객체의 CRUD 로직을 작성할 때는 추가적인 검증 과정이 필요함을 깨달았다. 이전에 했던 프로젝트에서는 이와 같은 검증 과정을 철저하게 작성하지 않았는데, API를 리뉴얼하는 겸 해당 내용을 반영해서 구멍 없는 로직을 작성해야겠다.