프로젝트 엔티티 연관관계를 개선하는 과정을 진행하면서 여러 삽질을 했다. 약 5~6가지 삽질을 했는데, 이번 포스팅에선 그 중 하나인 4번째 삽질, DB 정규화 위반 문제점 중 하나인 갱신 이상을 무시하고 성능 및 가독성을 개선하려 한 후기에 대해 적어보려 한다.
삽질한 부분의 도메인 구조
- 회원이 모임(Team)에 가입할 때, 모임에서 사용할 닉네임을 지어 가입하게 된다. 이 때 TeamParticipation이 생성된다.
- TeamParticipation에는 모임에서 사용하는 닉네임이 들어있다. (즉, TeamParticipation은 Member와 Team의 중간테이블)
- 특정 모임(Team)은 여러 롤링페이퍼(Rollingpaper)를 소유할 수 있다. (1:N @ManyToOne 단방향)
- 특정 롤링페이퍼(Rollingpaper)는 여러 메시지(Message)를 소유할 수 있다. (1:N @ManyToOne 단방향)
위 구조에서 메시지 작성자 id를 통해 메시지 수신인의 닉네임을 찾기 위해선 아래 과정을 거쳐야 한다.
message - rollingpaper - team
<-(join)->
team_participation
하지만 위 정보만 요구하는 것이 아니다. 본인이 작성한 메시지 정보를 요구하는 DTO는 아래 값들을 응답으로 보내줘야 한다.
무려 4개의 엔티티에 포함돼있는 값들에 대한 정보를 요구한다.
현재 구조대로라면 서비스 로직에서 전부 처리하기엔 N+1 문제가 너무 많이 발생할 것처럼 보였다.
또한 엔티티에 해당하는 대부분의 컬럼 정보를 요구하는 것이 아닌, 일부분만 요구하는 것이기 때문에 fetch Join을 하더라도 불필요한 정보들을 너무 많이 가져오는 것 같았다.
그렇다고 JPQL로 쿼리 한번에 처리하기엔 로직이 너무 복잡하다.
참고로 이 쿼리를 제외한 나머지 쿼리들은 join이 거의 없거나 한번만 있다.
연관관계 개선을 마음먹게 된 계기
TeamParticipation은 rollingpaper, message, team에서 따로 가져올 방법이 없기 때문에 현재 구조에서는 theta join을 이용하는 수밖에 없었다. 따라서 실제 SQL문으로는 cross join이 나가게 되고, 이는 치명적인 성능 저하를 일으킨다.
또한, 이 쿼리는 가독성에도 심각한 문제가 있다. 가독성이 낮은 코드는 변경에 유연하지 못하고, 버그를 쉽게 인지하지 못한다는 단점이 존재한다. 실제로 위 쿼리에도 심각하진 않지만 사소한 버그가 존재하고 있었는데 리팩터링 전까지 아무도 발견하지 못했다.
(정상적인 루트로 발견할 수 있는 버그가 아니라서 다행이었다.)
cross join을 발생시키는 세타 조인을 없애는 것이 1순위, 가독성 개선을 2순위로 엔티티 연관관계를 조금 손보기로 했다.
메시지 수신인의 해당 팀 nickname을 얻기 위해 TeamParticipation을 세타조인하는 것이기 때문에 TeamParticipation과 Team끼리 양방향 연관관계를 맺을까도 고민했고, 그 외 여러 방법을 고민해보았다.
그 중 한가지 방법이 바로 rollingpaper에 TeamParticipation에 존재하는 nickname을 컬럼으로 가지기이다.
롤링페이퍼 쪽에도 수신인 닉네임 정보가 필요한 DTO가 존재했기 때문이다.
또한 nickname 자체를 가져버린다면 .getTeamParticipation().getNickname()으로 발생하는 N+1 문제도 처리할 수 있다고 생각했기 때문이다.
DB 제3정규화의 문제점(갱신 이상)을 그대로 가지는 코드
제3 정규화에 대한 내용은 아래 링크를 참고하자.
https://ko.wikipedia.org/wiki/%EC%A0%9C3%EC%A0%95%EA%B7%9C%ED%98%95
근데 사실 위 내용보다 아래 예시를 보는 게 이해가 빠를 수도 있다.
TeamParticipation
회원(Member)과 모임(Team)의 중간테이블인 TeamParticipation에서는 회원이 해당 모임에서 사용할 닉네임을 컬럼으로 가진다.
Rollingpaper
Rollingpaper에 수신인의 닉네임 정보를 컬럼으로 가지도록 추가해주었다.
다른 엔티티에서 같은 컬럼을 중복하게 가지게 된다. 이렇게 될 경우 DB 제3정규화를 위반할 때 가지는 문제점 중 하나인 `갱신 이상`을 가지게 된다.
사실 이때까지만 해도 중복한 컬럼을 가지면 왜 그렇게 크게 문제가 되는지 실감하지 못했다.
당시에 나는 성능 향상, 가독성 개선에만 집중했기 때문에 아래 생각밖에 하지 못했다.
변경 전: Message - Rollingpaper - Team - TeamParticipation (theta join) 으로 닉네임 가져오기
변경 후: Message - Rollingpaper - addressNickname
(join 존재 안함. 특정 케이스에선 fetch Join으로 N+1 문제도 막을 수 있을 듯!)
분명 select 관련 쿼리들은 훨씬 편리해지고 가독성도 나아진 것을 확인할 수 있다.
우리가 없애려고 했던 목표 1순위인 theta join (cross join)도 사라졌고, queryDSL로 인한 가독성 개선해도 성공해서 목표 2순위까지 달성한 것을 확인할 수 있다.
사실 이때까지만 해도 문제를 잘 몰랐다.
우아한테크코스 4기 크루가 아래와 같은 말을 하기 전까지는...
모임에 들어가서 닉네임을 수정하면, 롤링페이퍼에 적힌 수신자 닉네임들은 모두 잘 바껴?
그럼 뭐가 문제인 걸까?
위에서도 말했지만 한 번 더 얘기하겠다.
바로 nickname이 수정될 때 발생한다.
DB 제3정규화 위반 시 발생할 수 있는 문제점들을 감수하고 동일 컬럼인 nickname을 TeamParticipation에도 넣고 Rollingpaper에도 넣어두었다.
그렇기 때문에 TeamParticipation에서 nickname을 수정할 때, Rollingpaper의 닉네임도 추가로 수정해주어야 한다는 번거로움이 존재하게 된다.
번거롭기만 하면 다행이지, Rollingpaper와 TeamParticipation을 theta join해주는 find쿼리도 날려주어야 되고, bulk update 쿼리를 추가로 날려서 N+1문제까지 유발시킨다!
이는 연관관계 개선 전보다 더 심각한 성능저하 이슈를 발생시킨다.
따라서 이러한 경우를 막기 위해 Rollingpaper와 TeamParticipation을 @ManyToOne 단방향 연관관계로 세팅하든지, 다른 방법을 탐색하든지 하는 방향으로 틀어야 한다.
사실 정규화 위반 정도만 조금 알고 있었고 어떤 정규화인진 몰랐는데 CS 마스터 제로가 제3정규화라고 한마디 해줘서 알게 됐다.
DB Normalization Rules가 괜히 있는 것이 아니라는 걸 몸소 느꼈다. 확실히 이론적으로 공부할 때보다 직접 삽질해야 크게 와닿는 듯하다.
추가로, 이 포스팅을 `우아한테크코스 4기`에 넣어야될지, `CS/DB`에 넣어야될지 모르겠다.
일단 우아한테크코스 4기 카테고리로 분류하고, 나중에 DB 관련 공부를 해서 CS/DB에는 더 자세히 작성하든지 해야겠다.
+) 221002 추가
현재 케이스가 DB 제3정규화 위반 코드인지, 아니면 중복 컬럼으로 인한 문제점이 제3정규화 위반의 갱신이상 문제점을 가지게 될 뿐 제3정규화는 아닌 것인지 의견이 갈렸다. 그렇기 때문에 글을 일부 수정했다.
관련 이슈
https://github.com/woowacourse-teams/2022-nae-pyeon/issues/492
참고
도움을 준 사람
- 우아한테크코스 4기 제로
- 우아한테크코스 4기 알파
'JAVA > 우아한테크코스 4기' 카테고리의 다른 글
[221111] 우아한테크코스 4기 리크루팅데이 후기 (24) | 2022.11.11 |
---|---|
[221021] 우아한테크코스 4기 최종 데모데이 후기 (0) | 2022.10.29 |
[220928] 우아한테크코스 레벨4 - MVC 구현하기 미션 후기 (2) | 2022.09.28 |
[220913] 우아한테크코스 레벨4 - 톰캣 구현하기 미션 후기 (0) | 2022.09.13 |
[우아한테크코스] 레벨3 레벨로그 인터뷰 후기 (10) | 2022.09.05 |