이전에 패키지 구조를 개선 한 후 본격적으로 미리 만들어두었던 기획서에 맞게 기능을 다시 구현하기로 하였다.
그래서 처음으로 돌아가서 엔티티 설계부터 다시 하게 되었다.
기존에도 크게 문제가 있던 수준까지는 아니었지만, 전체 기능을 파악하고 설계했던 것이 아니라서 몇몇 컬럼이 빠져있기도 했고, 회원과 관련된 부분이 설계되지 않아서 완성된 느낌은 아니었다.
그래서 이번에는 회원 엔티티까지 설계를 해서 전체적으로 mvp 정도의 느낌은 나오도록 재설계 했고, 댓글 같은 기능은 추후에 구현할 기능이라서 일단 모델 설계에서도 제외했다. 또 실제 기능과 관련없는 운영을 위한 통계 테이블도 아직 내 수준에서는 완벽하게 설계하기 힘들다고 판단했고, 제대로 된 근거도 많이 찾지 못해 나중에 데이터베이스 설계를 더 깊게 배운 후 설계하기로 결정하였다. 어짜피 기능과 관련된 부분은 아니라서 지금 당장 필요하다고 생각하지는 않았다.
이 설계에 관해서는 해야 될 얘기가 좀 많아서 1~3편 정도로 나누어 작성하기로 하였다. 1편은 전체적인 구조와 각 엔티티의 역할을 설명하려고 한다.
테이블들의 전체 구조는 다음과 같다.

우선 테이블들의 이름은 집계 테이블들을 제외하고 단수형으로 통일하였다. 물론 복수형으로 쓰는 경우도 있고 나도 지금까지는 복수형으로 쓰긴 했지만 다시 찾아보니 단수형으로 쓰는 경우도 꽤 있다는 것을 알게 되었다.
또 하나의 종류의 데이터만 저장하는 엔티티 라고 본다면 복수형으로 쓰는 것 보다는 단수형이 더 자연스럽다고 판단했다. 하지만 집계 같은 통계용 테이블들은 맥락상 3일치, 7일치 혹은 특정 기간 사이의 조회수, 좋아요 수 등 여러 종류의 데이터를 담을 수 있기 때문에 novel_daily_stats 와 같이 더 명확히 복수형으로 통일하였다.
그리고 user, novel, episode 3개의 엔티티는 api 에서 외부에 노출되어야 하는 엔티티인데, 여러번 고민하다가 공개용 대체 키(public id)를 두는 것으로 결정하였다. 자세한 근거는 2편에서 설명한다.
또 생성일, 수정일, 삭제일 같은 기존의 timestamp 컬럼들은 datetime으로 변경하였다. 이유는 공부 정리에도 잘 쓰여있어서 간단히 말하면 timestamp 는 2038년까지밖에 지원하지 못하고, 이제는 datetime 도 timestamp 의 기능을 모두 사용할 수 있어서 굳이 timestamp 를 사용하지는 않았다.
마지막으로 소개글 같은 선택적 필드는 NULL 허용으로 변경해 명시적으로 NULL 을 저장하도록 하였다. 기존에는 빈 문자열을 저장하였지만, 생각해보니 문자열 만큼은 비어있다 와 존재하지 않는다가 명확히 구분되어야 사용자가 입력한 값인지, 처음부터 입력한 적이 없었는지를 구분할 수 있기 때문에 NULL 이 더 어울린다고 판단했다.
다만 정수형은 수학적으로도 기본값을 0으로 설정하는게 더 자연스럽고, 리스트 조회에서도 결과가 없다면 빈 배열을 반환하는게 결과를 조회했지만 비어있다 라는 맥락이라서 더 자연스럽다고 생각한다.
그리고 기존에는 에러 응답의 원인에 null 을 넣으면 빈 객체로 대체되도록 하였었는데, 이 부분도 생각해보면 빈 객체는 에러는 발생하였는데 원인이 비어있다 라는 맥락이라서 조금 부자연스럽다고 보았다. 차라리 null 를 명시적으로 설정하는 것이 에러는 발생하였지만 응답으로 보여줄 수 있는 원인은 없다(숨긴다) 라는 맥락이 더 잘 드러난다고 보아서 조만간 수정할 예정이다.
소설 엔티티에서 새로 추가된 컬럼은 아래와 같다.
첫번째로 프롤로그 삭제 횟수는 비즈니스 요구사항에 따라 추가하였다. 요구사항에서 프롤로그는 삭제 후 1회에 한해 재등록 가능하다는 사항이 있었다. 즉 프롤로그를 처음 한 번 삭제 한 후 다시 재등록은 가능하지만, 두 번 삭제 이후부터는 재등록이 불가능 하다. 이유는 반복적으로 재등록을 하게 되면 독자의 몰입을 깨뜨릴 수 있기 때문이다. 대신 수정은 제한 없이 가능하다. 이를 구현하기 위해서는 episode, novel 두 엔티티 중 한쪽에는 프롤로그의 삭제 횟수를 저장해야 하는데, episode 에 저장하면 일단 프롤로그가 삭제되었을 때 이전 횟수를 알 수 없고, 가장 큰 문제가 오직 프롤로그 만을 위한 컬럼인데 일반 회차들도 그 값을 들고 있어야 해서 명확하지도 않고 공간도 낭비되게 된다. 그래서 소설 자체가 관리해야 하는 값으로 보고 novel에 저장하기로 결정하였다. 이렇게 하면 특정 소설의 프롤로그 삭제 횟수 라는 의미가 되어 더 명확해지고 공간 낭비도 없어지게 된다. 또 이 컬럼을 카운트로 하지 않고 boolean 같은 값으로 할수도 있겠지만, 그렇게 되더라도 결국 삭제 횟수를 알아야 플래그를 설정할 수 있으므로 오히려 복잡도만 늘어난다고 생각했다. 필요하다면 Java 애플리케이션 쪽에서 엔티티에 canUploadPrologue 같은 도메인 메서드를 두어 더 명확하게 코딩 할 수 있으므로 카운트만 저장해도 충분하다고 판단하였다.
두번째로 가장 마지막으로 등록한 회차의 번호 는 성능과 요구사항 모두를 만족시키기 위해 추가하였다. 일단 기존에는 episode 쪽에 MAX 쿼리를 이용해 해당 소설의 가장 큰 회차 번호를 실시간으로 얻어오고 있었다. 하지만 이는 아래 두가지 문제가 있다.
이런 이유로 novel 쪽에 가장 마지막 회차의 번호를 저장하도록 하였다. 제일 큰 이유는 비즈니스 요구사항이고, 부가적으로 얻을 수 있는 효과로 MAX 쿼리를 날리지 않아도 되어 약간의 성능 향상이 있다 라고 볼 수 있다.