02. Service Layer의 작성
- -
이번 포스트에서는 게시판에 필요한 서비스를 선언하고 작성해보자.
Service Interface
BoardService
다음은 BoardService에 선언된 기능의 목록이다. 설명은 주석으로 대체한다.
public interface BoardService {
/* searchParams에 근거하여 Board를 조회한다. */
Page<BoardRecordForList> getBoards(Map<String, String> searchParams);
/* Board에 해당하는 Reply의 목록을 반환한다. */
List<ReplyRecordForBoardDetail> getRepliesByBoardNo(Long bno);
/* bno에 해당하는 Board의 상세 내용을 조회한다.(board, writer, attachment, reply) */
Optional<Board> getBoardDetailByBno(Long bno);
/* Board 편집에 대한 내용만 조회한다.(board, attachment) */
Optional<Board> getBoardSimpleByBno(Long bno);
/* Board를 작성한다. */
void writeBoard(BoardRecordForWrite board);
/* Board를 수정한다. */
void updateBoard(BoardRecordForEdit record);
/* Board를 삭제한다. */
void deleteBoard(long bno);
/* Reply를 등록한다. */
void writeReply(Long rno, Long mno, String content);
}
MemberService
다음은 MemberService에 선언된 기능의 목록이다. 역시 설명은 주석으로 대체한다.
public interface MemberService {
/* id, pass 기반으로 로그인 처리 */
MemberRecord login(String id, String pass);
/* id의 사용자가 있는지 여부 반환 */
boolean isExist(String id);
/* 회원 가입 처리 */
void join(MemberRecord member);
/* 회원 정보 수정 */
void updateProfile(MemberRecord member);
/* 회원 탈퇴 */
void leave(String userId);
}
이제 위에서 선언한 service 중 몇 가지를 구현해보면서 service 레이어의 역할에 대해 코드적으로 살펴보자.
서비스의 구현 1 - BoardService.getBoards
서비스의 역할
Service 영역의 역할은 Controller에 전달된 사용자의 요청사항을 처리하는 것이고 이 과정에서 만약 DB에 대한 작업이 필요하다면 Repository를 이용한다.
사용자는 다양한 형태로 게시물 목록을 요청할 수 있다.
- 검색 조건, 페이지 없이 단순히 목록을 본다.
- 특정 페이지를 요청한다.
이 요청을 처리하기 위해서 BoardQueryDSLRepo.getBoardListUsingSearchParam을 이용할 수 있는데 이 메서드는 페이징을 위한 Pageable과 조회 조건인 Map을 받는다.
Page<BoardRecordForList> getBoardListUsingSearchParam(Pageable pageable,
Map<String, String> searchParams);
즉 단지 기계적으로 [searchParams에 부합하는 정보를 N개씩으로 페이징 해서 X 페이지의 정보를 반환하는 쿼리]를 DB에 날리고 결과를 Page 객체로 반환하는 역할이다.
하지만 Controller에 전달된 사용자의 요청사항이 구체적이지 않기 때문에 Controller와 Repository 사이의 완충 역할을 Service가 해줘야 한다. 따라서 다음과 같이 Service를 작성해 볼 수 있다.
서비스 작성
서비스를 작성하기 위해서는 @Service를 추가하고 의존성으로 사용되는 Repository를 주입 받도록 처리하면 된다. 이후에는 인터페이스를 구현하고 필요한 로직을 작성해주면 된다.
여기서는 searchParams에서 page 정보를 확인하고 없다면 0 페이지로 설정해서 repo를 호출하도록 해보자.
@Service // Service 영역 빈임을 나타내는 Stereo type annotation
@RequiredArgsConstructor
@Slf4j
public class BoardServiceImpl implements BoardService {
private final BoardRepo brepo; // 생성자를 통한 의존성 주입
private final ReplyRepo rrepo;
private final MemberRepo mRepo;
@Override
public Page<BoardRecordForList> getBoards(Map<String, String> searchParams) {
int page = Integer.parseInt(Optional.ofNullable(searchParams.get("page")).orElse("0"));
PageRequest pageRequest = PageRequest.of(page, 10);
return brepo.getBoardListUsingSearchParam(pageRequest, searchParams);
}
}
단위테스트
이번에는 위 Service의 기능을 테스트 해보자.
기본적으로 Service 영역을 단위테스트 하기 위해서는 Mockito를 이용해서 의존성인 Repository를 모킹한다. SpringBoot에서 Mock을 이용한 단위테스트 기법은 다음을 참고하자. https://goodteacher.tistory.com/492
[spring test] 3. @Service Layer test
이번에는 service 영역에 대한 테스트를 처리해보자. @Service Layer Test @Service Layer@Service Layer는 통상 @Repository에 의존한다. 즉 @Repository가 없으면 컴파일조차 할 수가 없다.@Service@RequiredArgsConstructorpu
goodteacher.tistory.com
위에서 작성한 Service를 테스트 한다면 필요한 부분은 무엇이고 어떤 부분이 테스트 되면 좋을까?
- 일단 테스트 대상은 Service이지 Repository가 아니다. 따라서 BoardRepository의 메서드인 getBoardListUsingSearchParam 메서드는 mocking 해야할 필요가 있다.
- BoardRepository의 getBoardListUsingSearchParam 메서드는 1번 호출되어야 하며 이때 전달되는 parameter는 service에서 전달해준 PageRequest와 searchParams이다.
- service 호출 결과로는 BoardRepository의 메서드가 반환한 값이 반환되어야 한다.
다음은 단위테스트의 예이다. @SpringBootTest를 그냥 사용하면 통합테스트가 되지만 로딩하려는 클래스를 지정하면 단위테스트로 사용할 수 있다.
@SpringBootTest(classes={BoardServiceImpl.class})// BoardServiceImpl만 로딩,다른 의존성은 Mock으로 처리
public class BoardServiceTest {
@MockBean
private BoardRepo brepo;
@MockBean
private ReplyRepo rrepo;
@MockBean
private MemberRepo mrepo;
@Autowired
private BoardServiceImpl boardService;
@Test
public void getBoardList() {
// given: 반환될 값과 Mock 객체의 동작을 설정한다.
Page<BoardRecordForList> mockPage = new PageImpl<>(new ArrayList<>());
// 파라미터는 ArgumentMatcher를 이용해 필요한 타입의 임의의 객체를 사용한다.
when(brepo.getBoardListUsingSearchParam(ArgumentMatchers.any(PageRequest.class),
ArgumentMatchers.anyMap())).thenReturn(mockPage);
// when: 호출을 해본다.
Map<String, String> params = Map.of();
Page<BoardRecordForList> result = boardService.getBoards(params);
// then: 결과를 검증한다.
Assertions.assertEquals(mockPage, result);
// brpo는 1회 호출되어야 하며 brepo에 전달된 파라미터는 PageRequest 와 Map 타입이 전달되었다.
Mockito.verify(brepo, times(1)).getBoardListUsingSearchParam(any(PageRequest.class), anyMap());
}
}
서비스의 구현 2 - BoardService.getRepliesByBoardNo
서비스 작성
getRepliesByBoardNo는 게시글 번호를 받아서 댓글의 목록을 반환한다. ReplyRepository의 getRepliesByBoardNo는 Reply의 Entity를 반환하는데 Service는 record 객체로 반환할 계획이므로 이에 대한 변환 작업을 해주도록 하자.
public record ReplyRecordForBoardDetail (String writer, String content,
LocalDateTime created){
}
@Override
public List<ReplyRecordForBoardDetail> getRepliesByBoardNo(Long bno) {
return rrepo.getRepliesByBoardNo(bno).stream()
.map(entity -> new ReplyRecordForBoardDetail(entity.getWriter().getId(),
entity.getContent(),
entity.getCreated()))
.collect(Collectors.toList());
}
단위테스트
@MockBean
private ReplyRepo rrepo;
@Test
public void getRepliesByBoardNoTest() {
// given: 반환될 값과 Mock 객체의 동작 설정
long boardNo = 1L;
Member writer = Member.builder().id("hong").build();
List<Reply> replies = List.of(Reply.builder().rno(1L).content("content")
.writer(writer).build());
when(rrepo.getRepliesByBoardNo(boardNo)).thenReturn(replies);
// when
List<ReplyRecordForBoardDetail> result = boardService.getRepliesByBoardNo(boardNo);
// then
// rrepo의 메서드가 1번 호출 되었고 파라미터로 boardNo가 전달되었나?
verify(rrepo).getRepliesByBoardNo(boardNo);
Assertions.assertEquals(1, result.size());
Assertions.assertEquals(writer.getId(), result.get(0).writer());
Assertions.assertEquals("content", result.get(0).content());
}
'Spring Model > 05.Service' 카테고리의 다른 글
| @Transactional의 속성 2 (1) | 2023.10.23 |
|---|---|
| @Transactional의 속성 1 (0) | 2023.10.20 |
| 01. Service Layer (0) | 2020.06.01 |
소중한 공감 감사합니다