Just Do IT!
[Spring Boot] 북마크 생성/삭제 토글 형식으로 구현하기 본문
프로젝트에 북마크 기능이 있는데, boolean을 이용해서 삭제 여부를 update하는 soft delete를 구현하기로 했다.
북마크 db가 따로 있고 로그인한 사용자가 특정 문제집을 북마크하는 것이다.
처음 북마크를 한다면 db에 새로운 북마크가 생성되고,
북마크를 취소하는 경우 is_deleted가 true로 업데이트 되는 형식이다.
기능 구현 전에 api 명세서를 작성할 때는
- POST /api/books/{id}/bookmark
- UPDATE /api/books/{id}/bookmark
이렇게 두 가지를 따로 작성했었는데 토글 방식으로 구현할 수 있지 않을까 하다가 참고할만한 글을 발견해서
적용해보았고 포스트맨으로 테스트까지 마쳤다.
BookmarkResponse 생성
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BookmarkResponse {
private Long id;
private boolean isDeleted;
private Long userId; // User ID
private Long bookId; // Book ID;
}
Bookmark entity가 존재하지만 entity를 직접 사용하지 않고 따로 domain 폴더에 Response를 생성해주었다.
- 테이블과 매핑되는 Entity 클래스가 변경되면 여러 클래스에 영향을 끼치게 되는 반면 View와 통신하는 DTO 클래스(Request / Response 클래스)는 자주 변경되므로 분리해야 한다.
entity 대신 DTO를 사용하는 여러 이유가 있는데 지금 설명을 적을 건 아니라서 생략하고,
일단은 response 객체를 사용하려고 한다.
BookmarkController
@RestController
@RequestMapping("/api/books")
@RequiredArgsConstructor
public class BookmarkController {
private final BookmarkService bookmarkService;
@PostMapping("/{id}/bookmark")
public ResponseEntity<BookmarkResponse> addBookToBookmark(@AuthenticationPrincipal User user, @PathVariable("id") Long id) {
BookmarkResponse bookmarkResponse = bookmarkService.toggleBookmark(user, id);
return ResponseEntity.ok(bookmarkResponse);
}
}
북마크 생성/삭제 컨트롤러를 하나로 만들었다.
PostMapping으로 북마크를 생성하고, 취소하고, 다시 북마크하는 기능을 하나로 처리할 수 있도록 구현하였다.
bookmarkService의 toggleBookmakr 메소드를 사용하면 된다.
로그인한 사용자만 가능하기에 user가 필요하고, 특정 문제집을 북마크하기 때문에 문제집 id도 필요하다.
BookmarkRepository
@Repository
public interface BookmarkRepository extends JpaRepository<Bookmark, Integer> {
// 북마크하기
Optional<Bookmark> findByUserAndBook(User user, Book book);
}
다른 메소드 대신 지금 필요한 메소드만 적었다.
특정 사용자(User)와 특정 문제집(Book)의 북마크를 조회하는 기능을 제공하는 메소드이다.
사용자가 해당 문제집에 대해 북마크를 했는지 확인하고, 결과를 Optional로 반환한다.
BookmarkService
@Service
@RequiredArgsConstructor
public class BookmarkServiceImpl implements BookmarkService {
private final BookmarkRepository bookmarkRepository;
private final BookRepository bookRepository;
@Override
@Transactional
public BookmarkResponse toggleBookmark(User user, Long id) {
// 북마크할 문제집
Book book = bookRepository.findById(id)
.orElseThrow(() -> new BaseException(ErrorCode.BOOK_NOT_FOUND));
// 북마크 조회
Optional<Bookmark> existingBookmark = bookmarkRepository.findByUserAndBook(user, book);
BookmarkResponse bookmarkResponse;
if (existingBookmark.isPresent()) {
// 기존 북마크가 존재하는 경우
Bookmark bookmark = existingBookmark.get();
boolean newIsDeleted = !bookmark.isDeleted(); // 새로운 isDeleted 값 계산
// 업데이트된 북마크 저장
bookmarkRepository.save(Bookmark.builder()
.id(bookmark.getId())
.isDeleted(newIsDeleted)
.user(bookmark.getUser())
.book(bookmark.getBook())
.build());
// 응답 객체 생성
bookmarkResponse = BookmarkResponse.builder()
.id(bookmark.getId())
.isDeleted(newIsDeleted)
.userId(user.getId())
.bookId(book.getId())
.build();
} else {
// 새로운 북마크 생성
Bookmark newBookmark = Bookmark.builder()
.user(user)
.book(book)
.isDeleted(false) // 기본값은 false
.build();
// 새로운 북마크 저장
bookmarkRepository.save(newBookmark);
bookmarkResponse = BookmarkResponse.builder()
.id(newBookmark.getId())
.isDeleted(false)
.userId(user.getId())
.bookId(book.getId())
.build();
}
return bookmarkResponse;
}
}
bookmarkResponse를 사용하고 builder를 사용해서 코드가 좀 길어보이지만 구현하려는 기능은 간단하다.
- 북마크할 문제집 가져오기
- 북마크 조회
- 북마크 기능
- 기존 북마크가 존재하는 경우
- isDeleted 상태 업데이트 (true → false / false → true)
- 업데이트된 북마크 저장
- 응답 객체 생성
- 새로 북마크하는 경우
- 새로운 북마크 생성 (isDeleted 기본값은 false)
- 새로운 북마크 저장
- 응답 객체 생성
- 기존 북마크가 존재하는 경우
이러한 흐름을 가지고 있다.
포스트맨으로 테스트해보기
북마크하기
북마크 취소
이렇게 테스트해보면,
북마크가 생성되었다가 다시 한 번 테스트할 때 북마크가 취소되는 걸 볼 수 있다.
DB를 보면 isDeleted가 0이었다가 1로 변경된다.
이러한 흐름으로 구현해본 게 처음이라 좀 낯설기도 하다.
북마크나 좋아요는 soft delete를 하기 때문에 이런식으로 하나로 만드는 게 더 편할 것 같았다.
delete 여부를 boolean으로 관리해주기 때문에 훨씬 더 간편하게 post, update 하지 않고 하나로 처리해주었다.
나중에 성능 개선할 부분이 있을 것 같기 하지만 그 부분은 나중에 고민해봐야겠다.
참고 자료
spring boot,jpa 좋아요 토글방식 구현
좋아요 기능 구현 보통 좋아요 기능은 좋아요를 누르면 좋아요 개수가 +1 이되고,
velog.io
'개발 공부 > Spring' 카테고리의 다른 글
JPA 특정 엔티티 삭제 시 연관 관계 엔티티 삭제하기 (3) | 2024.10.29 |
---|---|
JPA Cascade 알아보기 (0) | 2024.10.29 |
@RestControllerAdivce 적용해서 Spring 전역 예외처리하기 (1) | 2024.10.21 |
[Spring Boot] H2 Database를 사용하여 테스트 코드 작성하기 (1) | 2024.10.17 |
JWT에서 Access Token, Refresh Token이 필요한 이유 (1) | 2024.09.11 |