공부 내용을 정리하고 앞으로의 학습에 이해를 돕기 위해 작성합니다.
서비스 로직 구현
지난 시간에 작성한 테스트 코드를 기반으로, 서비스 로직을 구현한다.
각 기능(조회, 생성, 삭제)에 대한 테스트를 통과하기 위해 필요한 로직을 순차적으로 추가할 것이다.
ArticleRepository
ArticleRepository에서는 게시글의 다양한 속성을 기반으로 검색할 수 있도록 메서드를 추가한다.
특히 ContentContaining 메서드는 부분 검색을 가능하게 하여, 사용자가 입력한 키워드가 포함된 내용을 검색할 수 있도록 한다.
이를 통해 더 유연한 검색 기능을 제공할 수 있게 한다.
@RepositoryRestResource
public interface ArticleRepository extends
JpaRepository<Article, Long>,
QuerydslPredicateExecutor<Article>,
QuerydslBinderCustomizer<QArticle> {
Page<Article> findByTitleContaining(String title, Pageable pageable);
Page<Article> findByContentContaining(String content, Pageable pageable);
Page<Article> findByUserAccount_UserIdContaining(String userId, Pageable pageable);
Page<Article> findByUserAccount_NicknameContaining(String nickname, Pageable pageable);
Page<Article> findByHashtag(String hashtag, Pageable pageable);
@Override
default void customize(QuerydslBindings bindings, QArticle root){
...
}
}
ArticleService
ArticleService에서는 테스트에 기반한 로직 구현을 시작했다.
1. 게시글을 검색하는 테스트
테스트코드
@DisplayName("검색어 없이 게시글을 검색하면, 게시글 페이지를 반환한다.")
@Test
void givenNoSearchParameters_whenSearchingArticles_thenReturnsArticlePage() {
// given
Pageable pageable = Pageable.ofSize(20);
given(articleRepository.findAll(pageable)).willReturn(Page.empty());
// when
Page<ArticleDto> articles = sut.searchArticles(null, null, pageable);
// then
assertThat(articles).isEmpty();
then(articleRepository).should().findAll(pageable);
}
서비스 코드
@Transactional(readOnly = true)
public Page<ArticleDto> searchArticles(SearchType searchType, String searchKeyword, Pageable pageable) {
if (searchKeyword == null || searchKeyword.isBlank()) {
return articleRepository.findAll(pageable).map(ArticleDto::from);
}
return switch (searchType) {
case TITLE -> articleRepository.findByTitleContaining(searchKeyword, pageable).map(ArticleDto::from);
case CONTENT -> articleRepository.findByContentContaining(searchKeyword, pageable).map(ArticleDto::from);
case ID -> articleRepository.findByUserAccount_UserIdContaining(searchKeyword, pageable).map(ArticleDto::from);
case NICKNAME -> articleRepository.findByUserAccount_NicknameContaining(searchKeyword, pageable).map(ArticleDto::from);
case HASHTAG -> articleRepository.findByHashtag("#" + searchKeyword, pageable).map(ArticleDto::from);
};
}
- 검색어가 없는 경우
- 이 경우 모든 게시글을 검색해 반환해야 한다. 이때 DTO와 mapper를 사용한 것이 큰 도움이 된다.
- Page 객체 안의 내용을 map으로 변환하여 ArticleDto를 추가함으로써, 서비스는 도메인 코드와 ArticleDto만 알게 된다.
- 즉, 서비스는 컨트롤러의 자세한 구현을 알 필요 없이, 오직 도메인과 DTO 간의 매핑만을 처리하게 된다.
- 검색어가 있는 경우
- SearchType에 따라 각각 다른 쿼리를 실행하도록 switch 문을 사용했다.
- 예를 들어, SearchType.TITLE의 경우, findByTitleContaining 메서드를 호출하여 검색어가 포함된 제목을 가진 게시글을 검색하고, 그 결과를 ArticleDto로 변환해 반환한다.
- 검색어가 주어졌을 때, SearchType에서 만든 ENUM을 활용하여 서로 다른 쿼리를 생성한다.
- 예를 들어, 제목, 아이디 등 다양한 속성에 대해 검색 쿼리를 각각 호출하게 된다.
- 이로 인해 검색 기능이 보다 유연해졌으며, 다양한 조건에 맞는 검색이 가능해졌다.
- 해시태그 검색 시 #은 일일이 넣기 귀찮으니 자동으로 넣어준다.
- 하지만 이렇게 하면 #을 일부러 넣었을 때 두 개 들어가는 단점이 있지만 우선은 리팩토링 요소로 남겨두고 넘어간다.
게시글 페이지를 반환하는 기능 구현을 완료했다.
2. 특정 게시글을 조회하는 테스트
테스트 코드
@DisplayName("게시글을 조회하면, 게시글을 반환한다.")
@Test
void givenArticleId_whenSearchingArticle_thenReturnsArticle() {
// given
Long articleId = 1L;
Article article = createArticle();
given(articleRepository.findById(articleId)).willReturn(Optional.of(article));
// when
ArticleWithCommentsDto dto = sut.getArticle(articleId);
// then
assertThat(dto)
.hasFieldOrPropertyWithValue("title", article.getTitle())
.hasFieldOrPropertyWithValue("content", article.getContent())
.hasFieldOrPropertyWithValue("hashtag", article.getHashtag());
then(articleRepository).should().findById(articleId);
}
서비스 코드
@Transactional(readOnly = true)
public ArticleWithCommentsDto getArticle(Long articleId) {
return articleRepository.findById(articleId)
.map(ArticleWithCommentsDto::from)
.orElseThrow(() -> new EntityNotFoundException("게시글이 없습니다 - articleId: " + articleId));
}
- 이 테스트는 특정 ID로 게시글을 조회할 때, 해당 게시글이 올바르게 반환되는지 확인한다.
- 서비스 로직에서는 articleRepository.findById(articleId)를 사용해 해당 ID의 게시글을 찾고, 이를 ArticleWithCommentsDto로 변환하여 반환한다.
- 만약 게시글이 존재하지 않으면 EntityNotFoundException을 던진다.
특정 게시글을 조회하는 기능 구현을 완료했다.
3. 게시글을 생성하는 테스트
테스트코드
@DisplayName("게시글 정보를 입력하면, 게시글을 생성한다.")
@Test
void givenArticleInfo_whenSavingArticle_thenSavesArticle() {
// given
ArticleDto dto = createArticleDto();
given(articleRepository.save(any(Article.class))).willReturn(createArticle());
// when
sut.saveArticle(dto);
// then
then(articleRepository).should().save(any(Article.class));
}
서비스 코드
public void saveArticle(ArticleDto dto) {
articleRepository.save(dto.toEntity());
}
- 이 테스트는 입력된 게시글 정보를 기반으로 새 게시글이 생성되는지 확인한다.
- 서비스 로직에서는 ArticleDto를 Article 엔티티로 변환한 후, 이를 articleRepository.save()를 통해 저장한다.
- 이 과정을 통해 새로운 게시글이 데이터베이스에 정상적으로 저장되는지 확인할 수 있다.
게시글을 생성하는 기능 구현을 완료했다.
4. 게시글을 수정하는 테스트
테스트 코드
@DisplayName("게시글의 수정 정보를 입력하면, 게시글을 수정한다.")
@Test
void givenModifiedArticleInfo_whenUpdatingArticle_thenUpdatesArticle() {
// given
Article article = createArticle();
ArticleDto dto = createArticleDto("새 타이틀", "새 내용", "#springboot");
given(articleRepository.getReferenceById(dto.id())).willReturn(article);
// when
sut.updateArticle(dto);
// then
assertThat(article)
.hasFieldOrPropertyWithValue("title", dto.title())
.hasFieldOrPropertyWithValue("content", dto.content())
.hasFieldOrPropertyWithValue("hashtag", dto.hashtag());
then(articleRepository).should().getReferenceById(dto.id());
}
서비스 코드
public void updateArticle(ArticleDto dto) {
try {
Article article = articleRepository.getReferenceById(dto.id());
if (dto.title() != null) {article.setTitle(dto.title());}
if (dto.content() != null) {article.setContent(dto.content());}
article.setHashtag(dto.hashtag());
} catch(EntityNotFoundException e) {
log.warn("게시글 업데이트 실패. 게시글을 찾을 수 없습니다. - dto : {}", dto);
}
}
- 이 테스트는 게시글 수정 정보를 입력했을 때, 해당 게시글이 올바르게 수정되는지 확인한다.
- 서비스 로직에서는 getReferenceById()를 통해 수정할 게시글을 가져오고, 입력된 ArticleDto에 있는 값으로 해당 게시글의 필드를 업데이트한다.
- 만약 게시글이 존재하지 않으면 EntityNotFoundException이 발생하고, 이 경우 로그를 남긴다.
게시글을 수정하는 기능 구현을 완료했다.
4. 게시글을 삭제하는 테스트
테스트 코드
@DisplayName("게시글의 ID를 입력하면, 게시글을 삭제한다.")
@Test
void givenArticleId_whenDeletingArticle_thenDeletesArticle() {
// given
Long articleId = 1L;
willDoNothing().given(articleRepository).deleteById(articleId);
// when
sut.deleteArticle(1L);
// then
then(articleRepository).should().deleteById(articleId);
}
서비스 코드
public void deleteArticle(long articleId) {
articleRepository.deleteById(articleId);
}
- 이 테스트는 특정 ID의 게시글을 삭제할 때, 해당 게시글이 데이터베이스에서 삭제되는지 확인한다.
- 서비스 로직에서는 articleRepository.deleteById() 메서드를 사용해 해당 ID의 게시글을 삭제한다.
- 이 과정을 통해 삭제 기능이 정상적으로 작동하는지 검증할 수 있다.
이렇게 해서 서비스 로직까지 구현이 완료되었다.
위와 같이, 각 테스트 케이스에 맞춰 서비스 로직을 구현하고, 테스트를 통해 해당 로직이 의도한 대로 동작하는지 확인했다.
'BackEnd > Project' 카테고리의 다른 글
[Board] Ch03. 게시글 페이지 기능 구현 (0) | 2024.08.10 |
---|---|
[Board] Ch03. 게시판 페이지 기능 구현(2) (0) | 2024.08.10 |
[Board] Ch03. 게시글 페이지 기능 테스트 정의(2) (0) | 2024.08.09 |
[Board] Ch03. 게시글 페이지 기능 테스트 정의(1) (0) | 2024.08.09 |
[Board] Ch03. 게시판 페이지 기능 테스트 정의 (0) | 2024.08.09 |