Project/Glol

프로젝트 - 전적 갱신 리그 로직 리팩토링기

현인 2022. 12. 19. 20:11

최근 라이엇 API를 이용해서 프로젝트를 하나 개발하고 있는데요,
전적 갱신 API 개발을 마치고 보니, 리팩토링 하고 싶은 것들이 몇 개 있어 그 몇 개를 리팩토링한 것을 글로 담아보았습니다.

 

전적 갱신의 로직은 크게 총 세가지로 분류할 수 있는데 다음과 같습니다.

 

  1. 한 게임에 참여한 소환사 등록
  2. 경기 정보 저장
  3. 전적 갱신을 시도한 소환사의 리그 정보 등록

 

오늘 할 리팩토링은 이 중 3번인 리그 정보 등록입니다

 

전적 갱신을 시도한 소환사의 리그 정보 등록

소환사 리그 정보 등록은 랭크 게임에 대한 정보를 나타내는데요,
랭크 별 현재 티어와 점수, 승리한 수와 패배한 수를 나타냅니다.

 

  • 출처 op.gg의 필자의 리그 정보

리그는 자유랭크와 솔로랭크로 나뉘어, 각 랭크별로 티어와 점수가 따로 존재합니다.

리그 정보 등록의 리팩토링 전 로직은 아래 코드를 설명하면서 보겠습니다.

 

@Transactional  
override fun saveLeague(name: String) {  
    val soloLeague = leagueRepository.findLeagueBySummonerNameAndQueueType(name, SOLO_RANK)  
        ?: League()  // (1)
    val freeLeague = leagueRepository.findLeagueBySummonerNameAndQueueType(name, FREE_RANK)  
        ?: League()  // (2)

    val findSummoner = summonerRepository.findSummonerByName(name)  // (3)

    getLeague(name).forEach { league ->  // (4)
        if (league.queueType == SOLO_RANK) 
            soloLeague.leagueUpdate(league, findSummoner)  // (5)
        if (league.queueType == FREE_RANK)  
            freeLeague.leagueUpdate(league, findSummoner)  // (6)
    }  

    leagueRepository.saveAll(mutableSetOf(soloLeague, freeLeague))  // (7)
}

 

위에서부터 쭉 보시면

  • (1), (2) 공통적으로 각 솔로, 랭크에 해당하는 리그를 가져오고 엘비스 연산자를 통해 null일 경우 새 League를 생성해 반환합니다.
  • (3) 소환사의 리그 정보를 불러오기 위해 DB에 존재하는 소환사의 정보를 가져옵니다.
  • (4) getLeague() 를 통해 소환사 리그 정보를 불러오고(MutableSet<LeagueDto>를 반환함)
  • (5), (6) queueType 별로 league의 정보를 아까 불러온 정보로 업데이트 합니다
  • (7) 업데이트가 완료 된 리그들을 DB에 저장합니다.(이때 트랜잭션이 열린 경우 기존에 저장된 리그를 불러와서 leagueUpdate로 데이터가 변경된 경우 Dirty Checking을 통해 League가 Update 됩니다)

 

override fun getLeague(name: String): MutableSet<LeagueDto> =  
    if (!summonerRepository.existsSummonerByName(name)) {  
        val summoner = remoteSummonerFacade.getSummonerByName(name)  

        remoteLeagueFacade.getLeague(summoner.id)  
    } else {  
        leagueCustomRepository.getLeagueEntryByName(name)  
    }

saveLeague()에서 사용된 4번에 해당하는 getLeague() 메소드입니다.

 

간단하게 설명하자면 DB에 유저가 존재하지 않을 경우 유저부터 Riot API를 통해 조회합니다.

이후 League 정보를 불러오고 반환합니다. DB에 유저가 존재하면 DB에서 League를 조회한 후 반환합니다.

 

문제점을 찾아보자

 

getLeague의 조건식

가장 먼저 찾은 문제점은 바로 getLeague() 의 조건식입니다.

 

saveLeague() 메소드는 전적 갱신 내부에서 실행되는데요,
이 전적 갱신 메소드를 실행할 때 이미 유저가 존재하는 지에 대한 검증식이 존재합니다.
따라서 getLeague에서는 유저 존재 검증을 하지 않도록 변경하고,

대신 League 존재 유무에 따라 league를 DB에서 가져오거나 예외 반환과 함께 로깅을 하도록 변경하였습니다.

override fun getLeague(name: String): MutableSet<LeagueResponse> {  
    if (isExistsLeague(name))  
        return leagueCustomRepository.getLeagueEntryByName(name)  
    else {  
        log.info("${NOT_FOUND_LEAGUE.msg} in getLeague")  
        throw CustomException(NOT_FOUND_LEAGUE)  
    }  
}

또한 getLeague() 메소드는 리그 정보 등록 API(saveLeague)에 사용하지 않고, 리그 정보 조회 API에서만 사용하도록 변경하였습니다.

 

로직의 가독성

리팩토링을 하면서 saveLeague() 의 로직을 다시 볼 때 가독성이 조금 신경쓰였습니다.

전반적인 코드의 길이가 조금 짧은 편이지만 추상화 수준이 너무 낮기 때문에

처음 코드를 읽을 때 전반적인 가독성이 떨어진다고 생각하여 추상화 수준을 높일 필요가 있겠다고 생각했습니다.

그러면서 겸사겸사 soloLeague와 freeLeague의 리그 정보를 업데이트하는 방식 또한 변경하였습니다.

 

@Transactional
override fun saveLeague(name: String) {  
    val soloLeague = leagueCustomRepository.getLeague(name, SOLO_RANK) ?: League() // (1) 
    val freeLeague = leagueCustomRepository.getLeague(name, FREE_RANK) ?: League() // (2) 
    val findSummoner = summonerRepository.findSummonerByName(name)!! // (3) 
    val getLeague = getLeagueByName(findSummoner.name) // (4) 

    updateSummonerId(getLeague, findSummoner) // (5) 

    leagueUpdate(soloLeague, freeLeague, findSummoner, getLeague) // (6) 

    leagueRepository.saveAll(mutableSetOf(soloLeague, freeLeague)) // (7) 
}

 

위에서 처럼 차근차근 설명하자면

  • (1) (2)번은 위와 같습니다. 다만, CustomRepostiory(QueryDsl)을 통해 너무나 긴 메소드 명인 leagueRepository.findLeagueBySummonerNameAndQueueType() 보단 짧고 간결한 편이 가독성이 좋을 것 같아 변경하였습니다.
  • (3)번은 전적 갱신에서 유저가 존재할 경우 saveLeague 가 실행되므로 null 이 아니라는 !! 연산자를 사용했습니다.
  • (4)번 getLeagueByName() 라는 메소드를 통해 리그 정보를 불러옵니다. 자세히 설명하면 name으로 유저 정보를 DB에서 불러오고, 유저 정보를 통해 리그의 정보를 외부 API에서 불러옵니다.
  • (5)번 리그 정보를 불러올 때 같이 들어있는 summonerId를 update 합니다.(외부 API 호출에 필요한 정보)

 

private fun leagueUpdate(  
    soloLeague: League,  
    freeLeague: League,  
    findSummoner: Summoner,  
    league: MutableSet<LeagueDto>  
) {  
    soloLeague.leagueUpdate(league.elementAt(0), findSummoner)  
    freeLeague.leagueUpdate(league.elementAt(1), findSummoner)  
}

 

  • (6)번 Set은 원래 순서가 없지만, 외부 API에서 내려주는 값의 순서가 정해져 있어 위와 같이 로직을 작성하게 되었습니다. 우선 가독성을 위해 leagueUpdate 로직을 추상화 수준을 낮추어 해당 메소드로 추출하였고, League 클래스의 leagueUpdate를 사용하여 리그 정보를 업데이트하는 로직입니다.
  • (7)번은 원래와 같이 insert 쿼리 혹은 Dirty Checking을 통한 update를 수행합니다

 

아래는 리팩토링한 소스코드의 주소입니다.

 

https://github.com/Easy-Easy-Go/Glol-Server/blob/master/src/main/kotlin/com/server/glol/domain/league/service/impl/LeagueServiceImpl.kt