-
[Spring Boot] 우승멤버 등록, controller 테스트 코드 작성하기 1Backend/개발 2021. 8. 12. 02:37반응형
TIL 38일차 플젝
잠시 지원할것 들도 있고 주말에는 노느라고 공부를 쉬었다!
마요에 빨리 별 달고 아크샨 사진 넣어야되는데 테스트코드부분에서 걸려서 푸시를 못하고 있는게 .. 너무 마음에 걸린다
그치만 기능 대충 만들려고 공부 시작한거 아니니까 빨리 테스트코드 안되는거 해결하고 글 마저 쓰고 기능 업데이트 해야겠다! 일주일 넘게 잡고있으려니 답답 ㅠㅠ
대회 결과 - 우승자 등록 하기
37일차: 우승 멤버 별표표시 글에 대한 세부적인 테스트와 예외처리 과정을 나타낸글이다.
리그의 결과를 저장하기 위해 , 우승 멤버와 대회의 차수를 PostMapping으로 받아오려고 한다.
입력 값
은우승 멤버 5명
의 닉네임과 몇번째 대회인지를 나타내는리그번호
이다.다음의 틀에 맞게 구현을 해볼 것이다. 1,2번에 맞는 예외처리를 하고 이에 맞는 ControllerTest를 작성해보려고 한다.
//MemberController @PostMapping("/api/member/league-results/{leagueId}") public ResponseEntity<String> registerLeagueResults(@RequestParam("member") List<String> members,@PathVariable Long leagueId){ // 1. 이미 리그 결과가 등록되었을 경우 throw new IllegalArgumentException("이미 등록된 리그 결과입니다."); // 2. 등록되지 않은 참가자가 있는 경우 throw new IllegalArgumentException("등록되지 않은 참가자 입니다."); // 올바른 경우 memberService.updateWinMemberLeagueResult(members, leagueId); return new ResponseEntity<>("ok", HttpStatus.OK); }
@RequestParam
@Controller
컨트롤러로 사용할 클래스에서 사용할 수 있다.@RequestParam
어노테이션은HttpServletRequest
의getParameter()
와 같은 역할을 한다.다만, 지정한 키 값이 존재하지 않다면 BadRequest로 에러가 발생한다.
@RequestParam("key")로 받아온다면, 요청할때
key=value, key=value1
로 여러 번 넣어주거나key=value1,value2,value3
,
로 구분하여 한번에 넣어줄 경우 List로 바로 받아올 수 있다.Service
//MemberService @Transactional public void updateWinMemberLeagueResult(List<String> members, Long leagueId){ for(String member : members){ Long accountId = findMemberIdBySummonerName(member); memberRepository.findById(accountId).ifPresent( e-> { e.getLeagueResults().add(leagueId); memberRepository.save(e); }); } } @Transactional public boolean isExistLeagueResultByLeagueId(Long leagueId){ if(memberRepository.findLeagueResultsByLeagueId(leagueId).isEmpty()) return false; return true; }
MemberService
에updateWinMemberLeagueResult
(지난 글 참고)와 추가로isExistLeagueResultByLeagueId
를 구현했다.1) 이미 등록된 리그
- 등록하려는
leagueId
와 같은 값이 데이터베이스에 저장된league_id
에 있는지를 불러오는 기능에 대한 부분이다.
//MemberServiceTest @Test public void 등록되지않은_리그결과(){ // given Long leagueId = 3L; // when boolean result = memberService.isExistLeagueResultByLeagueId(leagueId); // then assertFalse(result); }
Service에 대한 테스트코드를 작성해보았다.
혹시 이부분에 long이 음수값이 들어올때 다른 예상치못한 예외가 발생할까 했는데, 그때도 그냥 false로 결과가 잘 나온다.
memberRepository.findLeagueResultsByLeagueId
를 구현하는데 있어서 일반 JPA 함수를 사용하지 못했는데,// MemberEntity @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name="member_league_results", joinColumns = @JoinColumn(name="member_id ")) private List<Long> leagueResults = new ArrayList<>();
member entity에
leagueResult
를 다음과 같이 ElementCollection으로 저장했기 때문에// MemberRepository @Query("select m from MemberEntity m join fetch m.leagueResults lr where lr = :leagueId") public List<MemberEntity> findLeagueResultsByLeagueId(@Param("leagueId") Long leagueId);
join을 통해 불러와야한다고 한다.
이때
join
과Join fetch
가 쿼리가 차이가 생기는데Hibernate: select memberenti0_.account_id as account_1_4_, memberenti0_.created as created2_4_, memberenti0_.modified as modified3_4_, memberenti0_.assists as assists4_4_, memberenti0_.deaths as deaths5_4_, memberenti0_.kills as kills6_4_, memberenti0_.last_game_played as last_gam7_4_, memberenti0_.lose as lose8_4_, memberenti0_.positions as position9_4_, memberenti0_.summoner_name as summone10_4_, memberenti0_.win as win11_4_, leagueresu1_.member_id as member_i1_3_0__, leagueresu1_.league_results as league_r2_3_0__ from members memberenti0_ inner join member_league_results leagueresu1_ on memberenti0_.account_id=leagueresu1_.member_id where leagueresu1_.league_results=?
Hibernate: select memberenti0_.account_id as account_1_4_, memberenti0_.created as created2_4_, memberenti0_.modified as modified3_4_, memberenti0_.assists as assists4_4_, memberenti0_.deaths as deaths5_4_, memberenti0_.kills as kills6_4_, memberenti0_.last_game_played as last_gam7_4_, memberenti0_.lose as lose8_4_, memberenti0_.positions as position9_4_, memberenti0_.summoner_name as summone10_4_, memberenti0_.win as win11_4_ from members memberenti0_ inner join member_league_results leagueresu1_ on memberenti0_.account_id=leagueresu1_.member_id where leagueresu1_.league_results=? Hibernate: select leagueresu0_.member_id as member_i1_3_0_, leagueresu0_.league_results as league_r2_3_0_ from member_league_results leagueresu0_ where leagueresu0_.member_id=? Hibernate: select leagueresu0_.member_id as member_i1_3_0_, leagueresu0_.league_results as league_r2_3_0_ from member_league_results leagueresu0_ where leagueresu0_.member_id=? Hibernate: select leagueresu0_.member_id as member_i1_3_0_, leagueresu0_.league_results as league_r2_3_0_ from member_league_results leagueresu0_ where leagueresu0_.member_id=? Hibernate: select leagueresu0_.member_id as member_i1_3_0_, leagueresu0_.league_results as league_r2_3_0_ from member_league_results leagueresu0_ where leagueresu0_.member_id=? Hibernate: select leagueresu0_.member_id as member_i1_3_0_, leagueresu0_.league_results as league_r2_3_0_ from member_league_results leagueresu0_ where leagueresu0_.member_id=?
fetchjoin은 한 쿼리에 다 ~불러오는데 join은 Member를 조회한 후 N만큼 쿼리를 더 날린다.
이부분이 N+1 문제인거같다..
지금 당장 쿼리 시간은 한 4초로 차이는 많이 없고, 무조건 리그 id와 연관된 멤버가 5명이기때문에 사실 상관 없을수도 있지만 N이 많아진다면 쿼리자체가 너무 많아지니까 문제가 될 것 같기도 하다...
사아알짝 이해가 되는거같기도하고오오오ㅗ.. N+1은 이해가 될때까지 계쏙 이렇게 애매하게 글을 쓸 것 같다.. ^^ ㅠㅠ
https://cobbybb.tistory.com/18
2) 등록되지 않은 참가자
등록되지 않은 참가자 부분은 이전에 만들어놓은
// MemberService @Transactional public Long findMemberIdBySummonerName(String summonerName) throws NullPointerException { return memberRepository.findMemberEntityBySummonerName(summonerName).getAccountId(); }
다음과 같은 함수가 있어서, 이걸 사용했는데 글을 쓰기 일주일 전에 작성한 코드를 보니 잘못 작성한 코드가 눈에 보여서 ㅎ.,ㅎ;; 코드를 수정해보았다.
// MemberController.registerLeagueResults 일부 // 수정 전 for( String member: members){ Long accountId = memberService.findMemberIdBySummonerName(member); if(!memberService.findMemberByAccountId(accountId).isPresent()) throw new IllegalArgumentException("등록되지 않은 참가자 입니다."); } // 수정 후 for( String member: members){ log.info("member : {}",member); try { memberService.findMemberIdBySummonerName(member); } catch( NullPointerException e){ throw new IllegalArgumentException("등록되지 않은 참가자 입니다. :"+member); } }
처음에는 accountId를 받아와서 다시 accountId로 MemberEntity를 조회했는데, 이미 NullpointerException으로 없는 경우 예외를 발생시키므로 수정 전과같이 코드를 짤 필요가 없었다 !!
그런데 수정 후와 같이
try..catch
를 사용하는 것도 별로 좋지 않은 것 같다.., 예외를 발생시키는 위치가 Controller와 Service중에 어디가 나은지, 둘다 써도 되는지 어떻게 GlobalExceptionHandler를 만들어내면 되는지.. 아직 와닿지가 않아서 고민중이다. @_@일단은 이전에 미리 플젝 뼈대를 만들어주신분(?)이 작성한 코드에 예외처리를 컨트롤러에서 throw new Illegal로 하고, view단에서 이 메세지를 그대로 보여주고 있어서 그대로 맞춰 구현해보고있다..
Controller.registerLeagueResults
// MemberController @PostMapping("/api/member/league-results/{leagueId}") public ResponseEntity<String> registerLeagueResults(@RequestParam("member") List<String> members,@PathVariable Long leagueId){ log.info("members: {}", members); log.info("leagueId: {}", leagueId); //이미 등록되었을 경우 boolean isExist = memberService.isExistLeagueResultByLeagueId(leagueId); log.info("isExist: {}",isExist); if(isExist) throw new IllegalArgumentException("이미 등록된 리그 결과입니다. leagueId:"+leagueId); for( String member: members){ log.info("member : {}",member); try { memberService.findMemberIdBySummonerName(member); } catch( NullPointerException e){ throw new IllegalArgumentException("등록되지 않은 참가자 입니다. :"+member); } } memberService.updateWinMemberLeagueResult(members, leagueId); return new ResponseEntity<>("ok", HttpStatus.OK); }
테스트 코드 작성
저번 경매 팀 생성 글에서 webmvctest와 슬라이스테스트에 대해서 잠깐 적었었는데, 제대로 이해하지 못했던 것 같다..(사실 아직도 ㅠㅠ)
보통 MockMvc를 사용할 때
mokito
의when
이나BDDMokito
의given
을 사용해서 service의 값을 지정해주고, mockMvc.andExpect와 같이 테스트를 해보는 것 같은데..글을 찾아보면 이 mokito는 미리 그 내부 단의 컨트롤러에 어떤 값을 넣었을 때 어떤 결과가 나오는지를 컨트롤러테스트를 작성하는 입장에서 다 알고 구현해야하니까? 좀 잘 알고써야한다는 글도 있었고... 하여튼 언제 왜 어떻게 쓰는지가 아직 하나도 머릿속에 정리가 되지 않는다!!!
그렇지만 일단 써보면서 익혀야할 것 같아서.. 써보려는데 지금까지 구현한 serviceTest 코드는 잘 성공하는데, controller에서 예외처리를 테스트하려고 보니까 자꾸 예상처럼 안돼서..
이부분을 좀 부딪히면서 겪어보고 다음에 제대로 정리해봐야겠다.. 오늘은 일단 주절주절 기록만 남겨놓겠습니다..
@Test public void 이미_등록된_리그() throws Exception { given(memberService.isExistLeagueResultByLeagueId(2L)) .willReturn(true); List<String> members = Lists.newArrayList("롤x콩,영x동,Elxly,에x킴,Gx대".split(",")); for (String member : members) { given(memberService.findMemberIdBySummonerName(member)).willReturn(1l); } mockMvc.perform(post("/api/member/league-results/2") .param("member", "롤x콩,영x동,Elxy,에x킴,Gx대")) .andReturn(); }
일단
leagueId:2
를 넣으면 true, 멤버 이름 넣으면 모두 존재하도록 넣어주었다.mockMvc를 그냥 수행했을 때
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.IllegalArgumentException: 이미 등록된 리그 결과입니다. leagueId:2 Caused by: java.lang.IllegalArgumentException: 이미 등록된 리그 결과입니다. leagueId:2 at com.mayo.lol.controller.api.MemberApiController.registerLeagueResults(MemberApiController.java:101) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
throw new IllegalArgumentException
을 발생시켰기 때문에 응답으로 보내질때 500 InternalServerError가 돼서 Nested exception이 되는건가..? nestedservletexception의 의미를 잘 모르겠다.. 컨트롤러에서 발생시킨 이 예외를 바로 처리해오지 못하는것 같아서, 뭔가 조치가 필요하답..!일단 성공하는 테스트케이스의 경우를 적어보겠다.
1. 컨트롤러에서 throw 대신 return new ResponseEntity<>(HttpStatus.xx);
그 전에 HTTP 상태코드를 어떻게할지 잘 정리된 글이 있어서 참고해서 다른 게시글로 정리해보았다.
// MemberController.registerLeagueResults 코드 일부 생략 /* 1.이미 등록된 리그 ** 400으로 하기엔 /:leagueId로 요청을 하고 있기 때문에 해당하는 요청에 맞는 리그가 이미 등록되어있어서 못하는 비즈니스 로직상 `409` 에러가 맞아보인다. */ if(isExist) return new ResponseEntity<>("이미 등록된 리그 결과입니다.", HttpStatus.CONFLICT); /* 2.등록되지 않은 참가자 ** String member로 값에 맞게 요청을 했을 경우 400에는 맞지 않는다. `404` 에러 */ catch( NullPointerException e){ return new ResponseEntity<>("등록되지 않은 참가자 입니다.",HttpStatus.NOT_FOUND); } //3. 올바른 경우 return new ResponseEntity<>("ok", HttpStatus.OK);
이렇게
return new ResponseEntity<>()
로 응답해주는 컨트롤러를 작성하고,@Test public void 이미_등록된_리그_HTTPSTATUS() throws Exception, IllegalArgumentException { given(memberService.isExistLeagueResultByLeagueId(2L)) .willReturn(true); List<String> members = Lists.newArrayList("롤xx대".split(",")); for (String member : members) { given(memberService.findMemberIdBySummonerName(member)).willReturn(1l); } mockMvc.perform(post("/api/member/league-results/2") .param("member", "롤xx대")) .andExpect(status().isConflict()) .andDo(print()); } @Test public void 등록되지_않은_회원_HTTPSTATUS() throws Exception, IllegalArgumentException { given(memberService.isExistLeagueResultByLeagueId(3L)) .willReturn(false); List<String> members = Lists.newArrayList("롤xx대".split(",")); for (String member : members) { if(member.equals("롤..")) given(memberService.findMemberIdBySummonerName(member)).willThrow(NullPointerException.class); else given(memberService.findMemberIdBySummonerName(member)).willReturn(1L); } mockMvc.perform(post("/api/member/league-results/2") .param("member", "롤xx대")) .andExpect(status().isNotFound()) .andDo(print()); } @Test public void 등록_성공_HTTPSTATUS() throws Exception, IllegalArgumentException { given(memberService.isExistLeagueResultByLeagueId(3L)) .willReturn(false); List<String> members = Lists.newArrayList("롤xx대".split(",")); for (String member : members) { given(memberService.findMemberIdBySummonerName(member)).willReturn(1L); } mockMvc.perform(post("/api/member/league-results/2") .param("member", "롤xx대")) .andExpect(status().isOk()) .andDo(print()); }
given에 예외에 맞는 리턴값, 예외를 만들어주어서
status().isConflict()
,status().isNotFound()
,status().isOk()
3개의 상황에 맞게 테스트 해보았다.처음 이 부분 테스트코드 통과를 했따..;ㅁ; 이렇게 힘들일인가..
근데 이렇게 짜면 기존에 작성된 코드랑 컨벤션을 맞출수가 없다. ㅠㅠ throw 에러를 발생시키고 글로벌 에러 핸들러를 작성해봐야할 것 같다.
throw new IllegalArgumentException
그냥 throw를 발생시킨 상태로 테스트코드를 통과하려면
@Test(expected = NestedServletException.class) public void 이미_등록된_리그_expected() throws Exception { given(memberService.isExistLeagueResultByLeagueId(2L)) .willReturn(true); List<String> members = Lists.newArrayList("롤두.....대".split(",")); for (String member : members) { given(memberService.findMemberIdBySummonerName(member)).willReturn(1l); } MvcResult mvcResult = mockMvc.perform(post("/api/member/league-results/2") .param("member", "롤....공대")) .andDo(print()); }
@Test(expected = NestedServletException.class)
를 넣어주면 테스트코드가 성공한다. 근데 이렇게하면andDo(print())
가 진행되지도 않고 그냥 그 전에 에러발생->nested맞아?맞네 성공 이러고 끝난다. 그래서 이건 아닌 것 같다.얼른 좀 더 찾아봐서 throw에 대해 전역적으로 에러 처리하고, 그거에 맞는 테스트코드 작성까지 하고.. 관련되서 찾아본 글들도 정리해봐야겠다.
그 부분은 더 찾아보고 다음 글에 올려야겠다.
그리고 다음에
swagger
Spring Rest Docs
와 같은 API 문서 작성하는 툴들을 사용해봐야겠다.생각할점 - request param에서의 예외인 badrequest, 내가 만든 409,404예외 총 3가지 예외처리 필요 / 이전에 throw된 예외도 같이..
글이 너무 지저분하다.. 나중에 내가 봐도 이해할 수 있을까 @_@ 에효 몰랑 그냥 ㄱ ㅣ록용인거로 ..출처
@RequestParam List형태로 받을 때 주의점
JPQL select query for ElementCollection
예외처리핸들러 나중에 참고할 블로그 https://bcp0109.tistory.com/303?category=981824
'Backend > 개발' 카테고리의 다른 글
[Spring Boot] GlobalExceptionHandler (0) 2021.08.18 [Spring Boot] 예외처리, 테스트코드에 관한 고민 (0) 2021.08.13 [Spring Boot] 우승 멤버 별표표시 , @ElementCollection (2) 2021.08.05 [Spring Boot] 팀 생성, DB 슬라이스 테스트 (0) 2021.08.05 [Spring Boot] mysql 연동 , DB 연관관계 고민 (0) 2021.08.04 - 등록하려는