ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring Boot] Trouble Shooting
    Backend/개발 2022. 1. 27. 00:00
    반응형

    프로젝트 ControllerTest , 게임 조회 구현 기록

    Jackson 버전

    java.lang.NoClassDefFoundError: com/fasterxml/jackson/databind/exc/InvalidDefinitionException

    의존성들의 버전이 맞지 않아서 생기는 오류였고 Jackson 2.13.1 최신 버전으로 바꿔주었다.

    Response 한글 깨짐 방지 처리

    # application.yml
    server:
      servlet:
        encoding:
          charset: UTF-8
          force: true

    Pagable

    스프링 데이터는 pageable에 PageRequest객체를 주입해준다.

    • page
    • size
    • sort

    글로벌 설정 가능, 개별 설정은 @PageagleDefault 어노테이션 사용

    페이지 파라미터가 2개 이상일경우 @Qualifier 어노테이션으로 접두어 설정하여 구분.

    Page<>로 조회하면 getTotalElements() getTotalPages()로 사이즈를 알아낼 수도 있다.

    - 추가로 이런 total정보를 가져오지 않아도 된다면 Slice를 이용하는게 성능면에서 더 좋다 

    Pageable 객체 선언 방법

    Pageable pageable = PageRequest.of(0,10, Sort.Direction.DESC,"gameCreated");

    List를 Page로 바꾸는 방법

    Page<GameRecordEntity> page = new PageImpl<>(gameRecordEntities, pageable, gameRecordEntities.size());

    @PageDefault

     @GetMapping("/api/game/result")
        public ResponseEntity<GameRecordResponseDto> result(@RequestParam(required = false) List<String> searchNames,
                                                                  @PageableDefault(size = 10, sort = "gameCreated", direction = Sort.Direction.DESC) Pageable pageable) {}

    요청시

    http://localhost:8080/api/game/result?page=1&size=2&sort=gameCreated,desc 

    이런식으로 파라미터 지정해줄 수 있다.

    RequestParam에 List로 값을 받아오는 경우

     @GetMapping("/api/game/result")
        public ResponseEntity<GameRecordResponseDto> result(@RequestParam(required = false) List<String> searchNames, @RequestParam(required = false) List<GameTypeCode> gameTypes,
                                                                  @PageableDefault(size = 10, sort = "gameCreated", direction = Sort.Direction.DESC) Pageable pageable) {
        }

    이런식으로 만들었을때

    .param("searchNames","감귤o,ddd크,짭d")
      .param("searchNames","또다른사람")
      .param("gameTypes","REGULAR,LEAGUE"))

    이렇게 콤마로 구분지어서 보내도 자동으로 String list로 읽어온다. 같은 key로 여러번 넣어도 잘 들어간다.

    그런데 만약에 "감귤, , 씨" 이런식으로 공백이 들어가면 null이되어서 검색에 문제가생긴다.

        @GetMapping("/api/game/result")
        public ResponseEntity<GameRecordResponseDto> result(@RequestParam(required = false) List<String> searchNames, @RequestParam(required = false) List<GameTypeCode> gameTypes,
                                                            @PageableDefault(size = 10, sort = "gameCreated", direction = Sort.Direction.DESC) Pageable pageable) {
            GameRecordResponseDto gameRecordResponseDto;
    
            // 빈 값 들어올 경우
            if (searchNames != null) {
                searchNames = searchNames.stream()
                        .filter(sn -> sn != null && !sn.equals(""))
                        .collect(Collectors.toList());
                if (searchNames.size() == 0) searchNames = null;
            }
            if (gameTypes != null) {
                gameTypes = gameTypes.stream()
                        .filter(gt -> gt != null && !gt.equals(""))
                        .collect(Collectors.toList());
                if (gameTypes.size() == 0) gameTypes = null;
            }
    
            if (searchNames != null && gameTypes != null) {
                gameRecordResponseDto = gameRecordService.findGameRecordByNameAndGameType(searchNames, gameTypes, pageable);
            } else if (searchNames != null && gameTypes == null) {
                gameRecordResponseDto = gameRecordService.findGameRecordByName(searchNames, pageable);
            } else if (searchNames == null && gameTypes != null) {
                gameRecordResponseDto = gameRecordService.findGameRecordByGameType(gameTypes, pageable);
            } else gameRecordResponseDto = gameRecordService.findGameRecord(pageable);
            return ResponseEntity.status(HttpStatus.OK)
                    .body(gameRecordResponseDto);
        }

    nullable이 되도록 Objects의 nonNull을 써서 그런 경우를 제거해주었다.

    이부분을 @valid나,, 뭔가 좀 더 제대로 처리할 수 있게 연구해봐야한다.. 그리고 저 if, else if문 엄청 반복되는구조 뭔가 맘에안든다..ㅠ

    그리고 빈 값 들어올 경우 빈 값을 제외하고 리스트를 다시 만들어주는데 그 때 size가 0이되면 null로 처리를 해주어야한다.

    참고 블로그

    verfiy와 then shoud

    Mockito의 verify와 같은 방식으로 BDDMockito에서는 then().should( ~ ) 을 쓰면 된다. 

    verify(gameRecordService, times(1)).findGameRecord(); // Mockito
    then(gameRecordService).should(times(1)).findGameRecord(); //BDD

    테스트 코드 짜면서 하는거 진도 너무 안나가서 힘들었는데 한번 틀 잡으니까 점점 알것같다!

    이런식으로 하나씩 늘어가니까 뭔가 뿌듯하긴하다 ㅎㅎ




    삭제 기능 구현


    DeleteIn vs DeleteById vs DeleteInBatch

    삭제 기능을 추가하려고 하는데 만약에 Id를 여러개 받을 수 있게되면 한번에 삭제하는게 나을지, 각각 삭제하는게 나을지 고민해보았다.

    거의 비슷해보이지만 일단 하나만 삭제할 때는 deleteById를 하기 전에 findById로 조회 한 후 없으면 삭제로직을 거쳐주려고 한다.

    일단 내가 findById로 찾고, 로직을 구현할거면 그냥 delete만 써도 된다고 한다.

    • delete + findById + 커스텀구현
    • deletById + 자동Exception

    그리고 DeleteByIdIn이 있는데, 이건 한번에 삭제도 되고 리스트에 존재하지 않는 값이 있어도 오류가 나지 않는다. 확인해보니까 select로 일단 삭제할 값을 확인한 후 , 하나씩 삭제하는 것을 볼 수 있다. 테스트코드에서의 성능비교일 뿐이지만 deleteByIdIn을 하면 389초, deleteById를 각각해도 369초로 얼마 차이 안나고, 삭제할 값을 확인하고 없으면 에러를 날려주며 삭제까지 하는게 오히려 성능이 좋았다.

    다음 블로그를 참고해보니 50만건을 삭제할 때도 50만건을 조회한 후 삭제하는것에 대해 문제점을 제시하고 있다.

    지금은 findById + delete를 사용하고, 다중 삭제가 필요하면 @Query를 사용해 조회 없이 한번에 삭제하는걸로 해야겠다. 

    Query로 Delete

        @Modifying(clearAutomatically=true)
        @Query("DELETE FROM GameRecordParticipantEntity e WHERE e.gameId in :gameIds")
        void deleteByGameIds(List<Long> gameIds);
    Hibernate: 
        delete 
        from
            game_record_participants 
        where
            game_id in (
                ? , ?
            )

    이렇게하면 한번에 지워진다. 대신 이것도 id가 없는값이 들어가도 오류가 안나서 앞단에 처리를 해줘야할것같다.

    @Query로 delete를 하니까 에러가 나서 보니 @Modifying을 넣어줘야 하는 것 같아서 뭔지 좀 찾아봤다.

    @Modifying의 clearAutomatically

    이해한 바를 요약하면 JPA에서 영속성 컨텍스테 있는 1차캐시를 통해 DB접근 횟수를 줄이는 작업이 일어나는데, 실제로 디비에 값을 변경하거나 삭제하게 되었을 경우 JPA의 캐시와 실제 DB가 동기화가 되지 않을 수 있다.

    그때 @modifying(clearAutomatically=true)로 활성해주면 1차 캐시를 비워 동기화가 일어날 수 있게 해준다. default는 false로 되어있다.

    @Transcational

    그리고 만약에 이 함수 상위에 @Transactional을 걸어주지 않을 경우

    InvalidDataAccessApiUsageException: Executing an update/delete query; nested exception is javax.persistence.TransactionRequiredException: Executing an update/delete query]

    이런 에러가 발생하니 서비스단에 @Transactional 처리를 해주거나 레파지토리 선언부분에 @Query @Modifying과 함께 @Transactional을 추가해주어야한다.

    import org.springframework.transaction.annotation.Transactional;

    잘 설명된 글

     

    @OneToMany의 Cascade와 orphanRemoval

    멤버를 지우면 멤버한테 기록된 리그결과도 지워지게 구현해보려고 했다.

        @OneToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL, mappedBy = "member")
        private List<LeagueResultEntity> leagueResults = new ArrayList<>();

    첫번째로는 cascade.ALL로 설정되었어서 멤버를지워도 리그 결과가 지워지지 않았다.

    //멤버에 리그결과 추가 - 멤버 save - 멤버삭제 - 리그결과 조회 테스트
    
    //fail
    assertThat(leagueResultRepository.findAll().size()).isEqualTo(0);

     

    CascadeType 정리 

    • CascadeType.PERSIST
      • 엔티티를 영속화 할 때 필드에 보유된 엔티티도 유지.
      • 부모가 자식의 전체 생명주기를 관리
      • 부모 엔티티가 자식 엔티티와의 관계를 제거해도 자식 엔티티는 삭제되지 않고 남아있음
        • ALL에서 적용된 PERSIST의 이 특징때문에 삭제가 안됐음
    • CascadeType.MERGE
      • 엔티티 상태를 병합할 때 필드에 보유된 엔티티도 병합
    • CascadeType.REFRESH
      • 엔티티를 새로 고칠 때 필드에 보유된 엔티티도 새로 고침
    • CascadeType.REMOVE
      • 엔티티를 삭제할 때 필드에 보유된 엔티티도 삭제
    • CascadeType.DETACH
      • 부모가 deatch를 수행하면 연관된 엔티티도 detach가 되어 변경사항이 반영되지 않음
    • CascadeType.ALL
      • 모든 Cascade 적용

    orphanremoval = true

    부모 엔티티가 삭제되면 자식 엔티티도 삭제되는 기능  

    PERSIST를 함께 사용하면 부모가 자식의 전체 생명주기를 관리하게 되고, 부모가 자식 엔티티와의 관계를 제거하면 자식은 고아로 취급되어 그대로 사라진다.

    PERSIST만 적용된 상태에서 부모를 지울때 자식을 지우려면 자식을 일일이 지워준 후 부모도 지우거나, orphanremoval을 사용한다. 아니면 REMOVE를 지정해주면 된다.   

    만약 부모를 '삭제'하는 것이 아니라 부모자식의 관계를 끊는 경우에 자동으로 자식을 삭제해주고 싶으면  orphanremoval 설정이 있어야 한다. 

    참고

     

    em.flush()  

    //GameParticipantService.java
    /**
     * gameIds와 연관된 participant 모두 삭제
     * member의 statistics정보도 함께 삭제하며, 최근게임 갱신, member의 게임이 0일경우 멤버도 삭제
     * @param gameIds
     */
    public void deleteGameRecordParticipant(List<Long> gameIds) {
        List<GameRecordParticipantV2Entity> entities = findParticipantByGameIds(gameIds);
        for (GameRecordParticipantV2Entity entity : entities) {
        ...
                memberV2Service.removeStatistics(entity, latestGamePlayed);
                em.flush();
        ...
        }
        gameRecordParticipantV2Repository.deleteByGameIds(gameIds);
    }

    게임 삭제 트랜잭션 안에서 참가자를 삭제하고 참가자와 관련된 정보를 멤버테이블에서도 삭제해주는 로직을 구현하는데 마지막 참가자 정보가 데이터베이스에 업데이트되지 않는 문제가 있었다. 

     

    지금까지 추측한 바로는 participantRepository.deleteByGameIds()가 직접 쿼리를 만든거라 자동으로 flush가 되는데, 마지막 removeStatistics에서 delete나 removeAll로 디비를 업데이트 시켜주는 쿼리가 디비에 반영되기 전에 participant를 지우는 과정에서 flush처리되면서 뭔가 문제가 생기는 것 같다.  

     

    트랜잭션 내부 호출시 전파되는 방식이나, 트랜잭션에서 flush가 실제로 수행되는 경우 3가지도 찾아보고 커밋될때 delete, insert, update등이 한번에 처리되면서 순서를 가지고있어서 먼저되는거 때문에 묻힌다는 글도 봤는데 확실하게 이 경우에 맞는 케이스를 아직 못찾았다..

    deleteByGameIds가 수행되기 전에 removeStatistics 쿼리가 모두 수행되도록 flush처리를 해주었는데 이부분은 두고두고 계속 고쳐나가봐야겠다. 

     

     

    findTopByOrderBy

    List<GameRecordParticipantEntity> findTop2ByAccountIdOrderByCreatedDesc(Long accountId);
        select 
        from
            game_record_participants gamerecord0_ 
        where
            gamerecord0_.account_id=? 
        order by
            gamerecord0_.created desc limit ?

     

     

     

    댓글

Designed by Tistory.