legacy/JPA
[JPA] Spring Data JPA Repository -> 사용자 정의 Repository
heemang.dev
2024. 3. 30. 12:04
Spring Data JPA
기존에 Spring Data JPA가 제공하는 JpaRepository를 상속받았고, 그곳에 사용자 요청을 처리하는 코드들이 담겨있었다. 기존 코드들을 확인해 보자.
Service
JPAQueryFactory를 의존받아 사용하고 있다. Querydsl로 작성한 쿼리문이 service 레이어에 포함되어 있다. 참고로 select() 절에 QGetSectionResponseDto는 @QueryProjection을 통해 dto로 변환하여 클라이언트로 전달하고자 사용하였다.
서비스 레이어에 쿼리문이 포함되어 있으니 코드 가독성이 좋지 않아 보인다.
@Service
@RequiredArgsConstructor
public class SectionService {
private final SectionRepository sectionRepository;
private final UserRepository userRepository;
private final RetrospectiveRepository retrospectiveRepository;
private final TemplateSectionRepository templateSectionRepository;
private final LikesRepository likesRepository;
private final TeamRepository teamRepository;
private final JPAQueryFactory queryFactory;
// 섹션 전체 조회
@Transactional(readOnly = true)
public List<GetSectionsResponseDto> getSections(GetSectionsRequestDto request) {
Retrospective findRetrospective = getRetrospective(request);
Team findTeam = getTeam(request);
// 다른 팀이 작성한 회고보드는 조회할 수 없다.
if(findRetrospective.getTeam().getId() != findTeam.getId()) {
throw new ForbiddenAccessException("해당 팀의 회고보드만 조회할 수 있습니다.");
}
return queryFactory.
select(
new QGetSectionsResponseDto(section.id, user.username, section.content,
section.likeCnt, templateSection.sectionName, section.createdDate))
.from(section)
.join(section.retrospective, retrospective)
.join(section.user, user)
.join(section.templateSection, templateSection)
.where(retrospective.id.eq(request.getRetrospectiveId()))
.fetch();
}
private Team getTeam(GetSectionsRequestDto request) {
return teamRepository.findById(request.getTeamId())
.orElseThrow(
() -> new NoSuchElementException("Not Found Team id : " + request.getTeamId()));
}
private Retrospective getRetrospective(GetSectionsRequestDto request) {
return retrospectiveRepository.findById(request.getRetrospectiveId())
.orElseThrow(() -> new NoSuchElementException(
"Not Found Retrospective id : " + request.getRetrospectiveId()));
}
}
단위 테스트
Service 레이어에서 작성한 getSections()를 mock 객체를 사용하여 단위 테스트를 진행하는 코드이다. 문제없이 잘 테스트되지만, stubbing을 위한 코드가 너무 지저분하다.
@Transactional
@ExtendWith(MockitoExtension.class)
class SectionServiceTest {
... 생략
@Test
@DisplayName("회고 보드에 등록된 모든 섹션을 조회 할 수 있다.")
void getSectionsTest() {
//given
... 생략
when(queryFactory.select(
new QGetSectionsResponseDto(section.id, user.username,
section.content,
section.likeCnt, templateSection.sectionName,
section.createdDate)))
.thenReturn(jpaQuery);
when(jpaQuery.from(section)).thenReturn(jpaQuery);
when(jpaQuery.join(section.retrospective, retrospective))
.thenReturn(jpaQuery);
when(jpaQuery.join(section.user, user))
.thenReturn(jpaQuery);
when(jpaQuery.join(section.templateSection, templateSection))
.thenReturn(jpaQuery);
when(jpaQuery.where(retrospective.id.eq(retrospectiveId)))
.thenReturn(jpaQuery);
when(jpaQuery.fetch()).thenReturn(List.of(response));
//when
GetSectionsRequestDto request = new GetSectionsRequestDto();
ReflectionTestUtils.setField(request, "retrospectiveId", retrospectiveId);
ReflectionTestUtils.setField(request, "teamId", teamId);
List<GetSectionsResponseDto> results = sectionService.getSections(request);
//then
assertThat(results.size()).isEqualTo(1);
GetSectionsResponseDto result = results.get(0);
assertThat(result.getSectionName()).isEqualTo(createdTemplateSection.getSectionName());
assertThat(result.getSectionId()).isEqualTo(createdSection.getId());
assertThat(result.getCreatedDate()).isEqualTo(createdSection.getCreatedDate());
assertThat(result.getContent()).isEqualTo(createdSection.getContent());
assertThat(result.getUsername()).isEqualTo(createdUser.getUsername());
assertThat(result.getLikeCnt()).isEqualTo(createdSection.getLikeCnt());
}
}
사용자 정의 Repository
사용자 정의 인터페이스
public interface SectionRepositoryCustom {
List<GetSectionsResponseDto> getSections(Long retrospectiveId);
}
사용자 정의 인터페이스 구현체
구현체에 @Repostory와 같이 컴포넌트 스캔 대상으로 지정하는 애너테이션이 없다. 이는 구현체의 규칙을 지켰기 때문인데, 사용자 정의 인터페이스 + Impl과 같이 사용하면 Spring Data JPA가 구현체를 자동으로 스프링 빈으로 등록한다.
@RequiredArgsConstructor
public class SectionRepositoryCustomImpl implements SectionRepositoryCustom {
private final JPAQueryFactory queryFactory;
@Override
public List<GetSectionsResponseDto> getSections(Long retrospectiveId) {
return queryFactory.
select(
new QGetSectionsResponseDto(section.id, user.username, section.content,
section.likeCnt, templateSection.sectionName, section.createdDate))
.from(section)
.join(section.retrospective, retrospective)
.join(section.user, user)
.join(section.templateSection, templateSection)
.where(retrospective.id.eq(retrospectiveId))
.fetch();
}
}
Repository
사용자 정의 인터페이스를 Repository에서 상속받는다. SectionRepositoryCustom의 경우 스프링 빈에 등록된 구현체가 사용된다.
public interface SectionRepository extends JpaRepository<Section, Long>, SectionRepositoryCustom {
int countByRetrospectiveAndTemplateSection(Retrospective retrospective, TemplateSection templateSection);
}
단위 테스트
기존의 경우 Service 레이어에 작성된 쿼리문에 대하여 stubbing 처리를 해야 했다. 그러나 사용자 정의 repository를 사용하니 stubbing이 1줄로 처리가 되어 매우 깔끔해졌다.
@Transactional
@ExtendWith(MockitoExtension.class)
class SectionServiceTest {
@Test
@DisplayName("회고 보드에 등록된 모든 섹션을 조회 할 수 있다.")
void getSectionsTest() {
//given
.. 생략
GetSectionsResponseDto response = new GetSectionsResponseDto(
sectionId, createdUser.getUsername(), createdSection.getContent(), createdSection.getLikeCnt(),
createdTemplateSection.getSectionName(), createdSection.getCreatedDate()
);
// stubbing 처리가 간단해짐에 따라 가독성이 매우 좋아졌다.
when(sectionRepository.getSections(retrospectiveId)).thenReturn(List.of(response));
//when
GetSectionsRequestDto request = new GetSectionsRequestDto();
ReflectionTestUtils.setField(request, "retrospectiveId", retrospectiveId);
ReflectionTestUtils.setField(request, "teamId", teamId);
List<GetSectionsResponseDto> results = sectionService.getSections(request);
//then
assertThat(results.size()).isEqualTo(1);
GetSectionsResponseDto result = results.get(0);
assertThat(result.getSectionName()).isEqualTo(createdTemplateSection.getSectionName());
assertThat(result.getSectionId()).isEqualTo(createdSection.getId());
assertThat(result.getCreatedDate()).isEqualTo(createdSection.getCreatedDate());
assertThat(result.getContent()).isEqualTo(createdSection.getContent());
assertThat(result.getUsername()).isEqualTo(createdUser.getUsername());
assertThat(result.getLikeCnt()).isEqualTo(createdSection.getLikeCnt());
}
}