크리스마스 기념해서 간단하게만 서비스를 구현하고 놀러가려고 한다. 이번에 구현해볼 서비스 기능은 회원 수 카운트 기능이다. 네이버 카페를 가면 전체 총 회원수만 나와있지만, 나는 멤버 등급 별로 회원 수를 출력해보고자 한다. JPQL을 활용해보고자 한다. JPA의 다른 기능도 있겠지만 이왕 다양하게 구현해보는거 간만에 쿼리문도 작성해보려고 한다.
//controller 함수
//총 회원 수 조회 (등급 별 분류하여 조회) : 등급 별 인원 수 카운트 하여 category, countNum만 넘기면 될듯
@GetMapping("/count")
public ResponseEntity<List<MemberResponseCntDto>> countMember(){
List<MemberResponseCntDto> memberResponseCntDto = memberService.countMember();
return ResponseEntity.status(HttpStatus.OK).body(memberResponseCntDto);
}
// return dto class 정의
@Data
@NoArgsConstructor
public class MemberResponseCntDto {
private MemberRole memberRole;
private Long memberCnt;
@Builder
public MemberResponseCntDto(MemberRole memberRole, Long memberCnt) {
this.memberRole = memberRole;
this.memberCnt = memberCnt;
}
public static List<MemberResponseCntDto> fromEntity(Map<MemberRole, Long> map){
ArrayList<MemberResponseCntDto> memberCntDto = new ArrayList<>();
for(Map.Entry<MemberRole, Long> entry : map.entrySet()){
memberCntDto.add(MemberResponseCntDto.builder()
.memberRole(entry.getKey())
.memberCnt(entry.getValue())
.build());
}
return memberCntDto;
}
}
여기까진 문제 없이 코드가 술술 작성되었다. 화이트크리스마스라서 그런가.. 하지만 기쁨도 잠시 테스트 코드에서 에러가 터졌다.
@Test
public void testTotalMemberCount(){
Member member = new Member("1234","1234","gyugyu1", MemberRole.ASSOCIATE, LocalDateTime.now(),0);
Member member1 = new Member("aaa","1234","aa", MemberRole.REGULAR, LocalDateTime.now(),0);
Member member2 = new Member("bbb","1234","bb", MemberRole.ASSOCIATE, LocalDateTime.now(),0);
Member member3 = new Member("ccc","1234","ccc", MemberRole.VIP, LocalDateTime.now(),0);
Member member4 = new Member("ddd","1234","dd", MemberRole.ASSOCIATE, LocalDateTime.now(),0);
Member member5 = new Member("eee","1234","eeeee", MemberRole.ASSOCIATE, LocalDateTime.now(),0);
em.persist(member);em.persist(member1); em.persist(member2);em.persist(member3);em.persist(member4);em.persist(member5);
em.flush(); em.clear();
List<Object[]> memberCntLists = memberRepository.countMemberByMemberRole();
for (Object[] index : memberCntLists){
logger.info(String.valueOf((MemberRole)index[0]));
logger.info(String.valueOf((Long)index[1]));
}
}
테스트 코드는 문제없는 것 같은데 알고보니 JPQL에서 문제가 있었던 것이다. JPA는 객체를 바라보고 쿼리문을 작성하는데, 아무 생각 없이 생성되어있던 mysql workbench의 member table 스키마를 보고 member_role로 작성했던 것이다.ㅎㅎ
// MemberRepository class
// SELECT M.MEMBERROLE, COUNT(M.MEMBER_ID) FROM MEMBER M WHERE M.MEMBERROLE != ADMIN GROUP BY M.MEMBERROLE
@Query("SELECT m.memberRole, COUNT(m) FROM Member m WHERE m.memberRole != 'ADMIN' GROUP BY m.memberRole")
List<Object[]> countMemberByMemberRole();
제대로 된 서비스 로직은 금방 작성했다. JPA가 DB에서 데이터를 리턴해올 때는 객체 리스트 타입으로 가져온다. 프론트 개발자와 함께 협업을 했었더라면? 을 가정하고 API 위주로 개발을 하고 있기 때문에 최대한 프론트 입장에서 응답 데이터 형식이 알기 쉬워야한다.
처음에 서비스 로직을 썼던 방식은 아래와 같다.
public List<MemberResponseCntDto> countMember(){
List<Object[]> memberCntLists = memberRepository.countMemberByMemberRole();
Map<MemberRole, Long> countMap = new HashMap<>();
for (Object[] index : memberCntLists) {
countMap.put((MemberRole)index[0], (Long)index[1]);
}
return MemberResponseCntDto.fromEntity(countMap);
}
근데 이렇게 했더니 회원이 ASSOCIATE인 회원 밖에 없으면 준회원인 회원 수만 리턴이 되고, 다른 등급은 표시가 되지 않는 것이다. 생각해보니 그도 그럴 것이, SQL 쿼리를 날려서 데이터를 얻을 때 레코드에 저장되어있는 내용만 나오지, 없는 칼럼의 내용은 나오지 않으니, JPQL 쿼리도 데이터베이스에 실제로 존재하는 데이터에 대해서만 결과를 반환할 것이다.
그래서 일단 MemberRole의 값들을 번거롭지만 미리 map에 넣어두고 이후에 덮어씌우는 방향으로 가야할 것 같았다.
// 총 회원 수 조회
public List<MemberResponseCntDto> countMember(){
List<Object[]> memberCntLists = memberRepository.countMemberByMemberRole();
Map<MemberRole, Long> countMap = new EnumMap<>(MemberRole.class);
for (MemberRole role: MemberRole.values()){
if(!role.equals(MemberRole.ADMIN)){
countMap.put(role,0L);
}
}
for (Object[] index : memberCntLists) {
countMap.put((MemberRole)index[0], (Long)index[1]);
}
return MemberResponseCntDto.fromEntity(countMap);
}

프론트 입장에선 모든 Role에 대해서 다 받아야 처리하기 편하지 않을까? 라는 생각에 서비스 로직을 번거롭지만 for문을 두번 사용해서 작성했다.