Notice
Recent Posts
Recent Comments
Link
«   2024/12   »
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31
Archives
Today
Total
관리 메뉴

밤빵's 개발일지

[TIL]20241123 PUBG 프로젝트 roster 응답 구조 재작업 본문

개발Article

[TIL]20241123 PUBG 프로젝트 roster 응답 구조 재작업

최밤빵 2024. 11. 23. 05:58

▶ 문제 상황

PUBG 프로젝트에서 매치 데이터를 가져와서 roster 데이터를 정리하는 과정에서 문제를 겪었다. 초기 로스터 응답은 원하는 형태와 다르게 나왔다. 응답 데이터에서 로스터의 attributes에는 참가자의 세부 정보가 중복되어 포함되어 있어서 로스터의 데이터 구조가 복잡하고 불필요한 정보가 많고, pubg데이터와 너무나도 다른 구조의 데이터가 나왔다. 

 

초기 응답 형식은 이랬다. 

{
  "type": "roster",
  "id": "e73c7460-eba1-489c-a834-8d393c0499b5",
  "attributes": {
    "stats": {
      "rank": 11, // 있어야하는 데이터 
      "teamId": 9, // 있어야하는 데이터 
      "assists": 0,
      "boosts": 0,
      "damageDealt": 0.0,
      "deathType": null,
      "headshotKills": 0,
      "heals": 0,
      "killPlace": 0,
      "killStreaks": 0,
      "kills": 0,
      "longestKill": 0.0,
      "name": null,
      "playerId": null,
      "revives": 0,
      "rideDistance": 0.0,
      "roadKills": 0,
      "swimDistance": 0.0,
      "teamKills": 0,
      "timeSurvived": 0,
      "walkDistance": 0.0,
      "weaponsAcquired": 0,
      "winPlace": 0,
      "dbnos": 0
    },
    "shardId": "kakao",
    "actor": null
  },
  "relationships": {
    "rosters": null,
    "assets": null,
    "participants": {
      "data": [
        {
          "type": "participant",
          "id": "73c63a5c-fb60-4f3d-bb37-0d30580002f3"
        },
        {
          "type": "participant",
          "id": "65b8dad4-f153-4117-81ac-b3f9985bb1b3"
        }
      ]
    }
  }
}

→ 초기 응답은 로스터의 attributes 내에 참가자의 세부 정보를 포함하고 있어, 데이터의 관계가 불분명하고 각 객체가 가진 역할이 명확하지 않았다. 이런 구조는 데이터를 직렬화하거나 역직렬화할 때 오류를 유발하고, 로스터와 참가자의 역할을 정확히 나누는 데 어려움을 겪게 했다.


▶ 문제 해결 과정

이 문제를 해결하기 위해, DTO와 엔티티 구조를 새롭게 설계하고, 각 로스터와 참가자가 명확히 분리되도록 데이터 구조를 변경했다. DTO 클래스인 MatchesDto의 설계를 전면적으로 수정해서, 로스터와 참가자의 역할을 명확히 구분했다.

 

초기에는 stats라는 하나의 필드로 로스터와 참가자가 같은 필드들을 공유하고 있었기때문에 로스터와 참가자의 정보가 구분되지 않게 만들었고, 불필요한 데이터를 함께 보여주게 되는 문제가 있었다. 해결하기 위해 RosterStatsParticipantStatsstats 필드를 분리해서, RosterStats에는 로스터와 관련된 팀 랭킹(rank)과 팀 ID(teamId)만 포함하고, 나머지 개별적인 전투 통계는 ParticipantStats에 포함시켜 각 객체의 역할을 더욱 명확히 구분했다.

 

로스터의 attributes에는 팀의 통계 정보만 남기고, 참가자 관련 데이터는 모두 제거했다. 이를 위해 RosterAttributes 클래스에는 팀 랭킹(rank)과 팀 ID(teamId)만 포함하도록 해서 참가자 정보는 relationships 객체 내에서 participants로 정의하고, 로스터와 참가자 간의 관계를 명확히 했다. 이렇게 함으로써 로스터는 팀의 정보만을 담고, 참가자는 별도로 관리되도록 설계했다.

 

MatchesService 클래스의 서비스 로직을 개선하여 데이터 변환 과정을 추가하고, 로스터와 참가자 데이터를 변환하여 각각의 역할을 명확히 하는 과정이 필요했다. 특히 mapRoster() 메서드를 작성하여 로스터 데이터를 별도로 매핑하고, mapParticipant() 메서드를 사용해 참가자를 독립적으로 관리하도록 했다. 이 과정에서 직렬화와 역직렬화를 위해 Jackson 라이브러리의 ObjectMapper를 적극 활용하여, DTO와 엔터티 간의 변환을 동적으로 처리했다.


▶서비스 로직 코드 조각 설명

서비스 로직에서의 핵심은 mapRoster()mapParticipant() 메서드를 통해 데이터를 명확하게 나누고 매핑하는 것이었다. mapRoster() 메서드는 로스터 데이터를 매핑할 때, 로스터의 ID와 랭크, 팀 ID와 같은 팀 관련 정보만 남기고, 참가자와의 관계를 relationships로 이동시켰다. 이를 통해 로스터 자체가 가지는 정보와 로스터에 속한 참가자들 간의 관계를 명확히 구분할 수 있었다.

private MatchesRoster mapRoster(MatchesDto.Included included, List<MatchesDto.Included> includedList) {
    MatchesDto.RosterAttributes attributes =
            objectMapper.convertValue(included.getAttributes(), MatchesDto.RosterAttributes.class);

    MatchesRoster roster = new MatchesRoster();
    roster.setRosterId(included.getId());
    roster.setRank(attributes.getStats().getRank());
    roster.setTeamId(attributes.getStats().getTeamId());
    roster.setShardId(attributes.getShardId());
    roster.setWon("true".equals(attributes.getWon()));

    // 참가자 매핑
    List<MatchesParticipant> participants = new ArrayList<>();
    if (included.getRelationships() != null) {
        MatchesDto.RosterRelationships relationships =
                objectMapper.convertValue(included.getRelationships(), MatchesDto.RosterRelationships.class);

        if (relationships.getParticipants() != null) {
            for (MatchesDto.DataRef participantRef : relationships.getParticipants().getData()) {
                MatchesParticipant participant = mapParticipant(participantRef, includedList);
                participants.add(participant);
            }
        }

        // 팀 데이터 설정 (null로 초기화)
        MatchesDto.Team team = new MatchesDto.Team();
        team.setData(null);
        relationships.setTeam(team);

        // relationships를 포함한 데이터로 설정
        included.setRelationships(relationships);
    }

    roster.setParticipants(participants);
    return roster;
}

→ objectMapper를 사용하여 included.getAttributes()RosterAttributes로 변환하고, 로스터와 관련된 팀 통계 정보만 유지했다. 참가자와의 관계는 relationships 내에 설정하고, 참가자 정보를 반복적으로 매핑하여 로스터와 참가자 간의 연결을 명확히 하고, team 관계는 명시적으로 null로 설정하여 잘못된 직렬화를 방지했다.


▶최종 응답 형식 이후, 최종적으로 조정한 응답은 다음과 같이 개선되었다.

{
  "type": "roster",
  "id": "c28eb68b-48cf-4923-9c5f-e4209239cbe9",
  "attributes": {
    "stats": {
      "rank": 9,
      "teamId": 4
    },
    "won": "false",
    "shardId": "kakao"
  },
  "relationships": {
    "team": {
      "data": null
    },
    "participants": {
      "data": [
        {
          "type": "participant",
          "id": "c0dfe87c-c890-4493-989a-b06ba5401655"
        },
        {
          "type": "participant",
          "id": "f8ebaccb-b4e8-4c0a-b6a8-beeba1ce982f"
        }
      ]
    }
  }
}

→ 이 응답에서는 로스터의 attributes가 팀의 정보만을 포함하며, relationshipsparticipants가 명확히 분리되어 있다. 데이터를 직관적으로 이해할 수 있게 해주고, 각 객체가 자신의 역할을 명확히 수행하도록 구조화했다.


이번 작업을 통해, 데이터 구조의 일관성과 응집도가 API 응답 설계에 있어 매우 중요하다는 것을 배웠다. 로스터와 참가자의 관계를 명확히 표현함으로써 데이터의 직렬화와 역직렬화 과정에서 발생하던 오류를 줄일 수 있었고, 유지보수와 확장성 측면에서도 효율적인 구조를 얻을 수 있었다. 특히, DTO 설계와 데이터의 관계 매핑을 개선하는 과정에서 많은 시행착오를 겪었다. 로스터와 참가자의 관계를 명확히 하고, 동적 처리와 직렬화/역직렬화를 정교하게 다루면서 데이터의 구조적 일관성을 유지하는 것이 얼마나 중요한지 체감할 수 있었다. 이러한 경험을 통해 복잡한 데이터 구조를 다룰 때 어떤 접근 방식이 효과적인지에 대해 많은 것을 배울 수 있었다.