요구사항: 푼 문제 데이터를 어떻게 효율적으로 저장할 것인가?
우리 학회에서 진행 중인 프로젝트는 Solved.ac API를 사용해 백준에서 푼 문제 데이터를 불러오고 있다. 이때 각 학회원의 푼 문제 수는 Solved 엔티티에 저장하며 문제 티어와 사용자를 기준으로 각 데이터를 구분하고 있다.
이러한 방식에는 큰 문제가 있다. 백준에서는 문제 티어가 Unrated부터 Ruby 1까지 총 31단계로 나뉘는데, 이는 즉 Solved 테이블의 row 수가 (학회원 수) x 31이 된다는 의미이다. 학회원이 학기마다 유동적이지만 보수적으로 잡아도 100명인데, 이는 즉 row 수만 3000여개가 될 수 있다는 말이다. 이는 성능 및 유지보수 면에서 너무 비효율적인 방법이라고 생각했다.
그렇다고 각 난이도에 대해 컬럼 30개를 만드는 것은 유지보수 및 가독성 면에서 너무 안좋고, 이렇게 하면 Iterable하게 동작하도록 만들기도 어려울 것 같아 좀 더 개발 친화적인 방법을 생각해보기로 했다.
값 타입 컬렉션 고려
처음엔 이 데이터들을 List<Integer> 형태로 다루면 어떨까 생각했다. JPA에서는 List와 같은 컬렉션을 그대로 저장할 수 없기 때문에 값 타입을 컬렉션 형태로 저장할 수 있게 해주는 @ElementCollection을 고려했다.
값 타입 컬렉션의 단점
하지만 조금만 생각해봐도 이것이 해결책이 되지 못함을 알 수 있는데, 리스트 형태로 값을 다루기 쉬워지는 것 뿐이지 row 수는 변하지 않음을 알 수 있다.
그리고 치명적인 단점은 값 타입 컬렉션의 저장 방식인데, 값 타입 컬렉션은 컬렉션에 변경 사항이 발생할 경우 해당 엔티티와 연관된 모든 데이터를 삭제하고, 값 타입 컬렉션에 있는 현재 값을 모두 다시 저장한다는 것이다.
우리 서비스는 15분마다 전체 학회원의 푼 문제 수를 갱신하는데, 값 타입 컬렉션을 사용한다면 (학회원 수) x 31개의 row를 삭제하고 다시 저장한다는 의미가 된다. 이는 너무 비효율적인 방안이기 때문에 폐기했다.
우리 서비스의 특성 다시 이해해보기
나는 유저마다 푼 문제 데이터를 하나의 필드로 묶어 저장할 수 없을까 계속 고민하며, 우리 서비스의 데이터 특성을 다시 짚어보았다.
- 푼 문제 수 데이터가 티어별로 10000개를 넘을 일이 거의 없다고 봐야 한다. 백준 서비스가 시작된 이래 누적된 문제 수는, 각 티어별로 대략 1000문제이다. 예외적으로 Unrated는 대략 5000문제이다. 따라서 한 사람이 어떤 티어의 문제를 10000개 이상 풀 가능성은 거의 없다. 또한 푼 문제 수는 음수가 될 수 없다.
- 푼 문제 수 데이터는 각각을 따로 조회할 일이 거의 없다. 즉, 어떤 유저에 대해서 푼 문제 수 데이터를 가져온다면, 그 유저의 모든 푼 문제 수를 가져오지, Gold 3 푼 문제수를 가져올 일은 없다는 의미이다.
해결책: 문자열로 압축 저장하기
이번에 고급 백엔드 스터디에서 DB의 flag 자료형에 대해 학습했다. flag는 비트 하나가 조건 하나의 상태를 나타내며, 이것이 여러개 이어붙여진 형태인데, 나는 여기서 영감을 얻어서 이 데이터들도 붙여서 하나의 필드에 저장해보면 어떨까 하고 해결책을 생각해 보았다.
백준에 등록된 문제 수는 각 티어별로 10000개에 한참 못미치므로, 하나의 데이터는 4자리로 표현할 수 있을 것이다. 예를 들어 Silver 2에서 123문제를 풀었다면 Silver 2에 대해서 "0123" 이라는 문자열로 저장할 수 있다. 푼 문제 수가 음수가 될 수 없으므로 부호를 위한 자리도 필요하지 않다.
푼 문제 수 데이터는 각각을 조회할 일이 거의 없으므로, 각 티어별 푼 문제 수를 모두 이어붙여 문자열로 저장해도 된다. API를 통해 푼 문제 수를 갱신하여 점수를 계산할 때도, DB에서 티어별로 문제를 꺼내올 이유가 없고 어차피 전체를 비교하여 점수를 계산해야 하기 때문이다.
결국 row 수를 줄이는 동시에 애초에 하나의 필드로 관리할 수 있으므로 쿼리 성능 면에서 상당한 발전을 이룰 수 있다고 생각한다.
또한 @Embeddable을 통해 이 문자열 필드를 하나의 객체로 감싸면, 응집도도 챙기면서 이 객체만 사용하는 의미있는 메서드(ex. 문자열 파싱 메서드)들을 담을 수 있어 더 좋을 것이라 생각한다.