본문 바로가기
Sketcher

FullCalender 성능 최적화(& 리팩토링)

by 완두완두콩 2022. 5. 6.

FullCaleder 오픈소스를 이용한 스케줄 관리 졸업작품 프로젝트를 진행하는 도중

다양한 문제가 계속해서 발생했고 차근차근 수정하려 하는 중인데, 

현재 직면한 가장 큰 문제는 쿼리 성능 최적화이다!..

 

전체 스케줄 페이지를 클릭하는 경우 , DB 에 스케줄 데이터가 100개도 안됨에도 불구하고

버벅이며 페이지가 열리는 현상이 발견됐고, 로그를 확인해보니 어마어마한

쿼리문이 찍히고 있었다.

(단 한번의 클릭만으로 어마어마한 스크롤 길이..)

 

다른 문제들보다 가장 시급한 문제라고 생각됐고, 최근 들었던 강의(쿼리 성능 최적화)를 활용할 수 있는 좋은 기회라 생각돼서

실제 프로젝트에 적용해보려 한다.

 

쿼리문을 분석해보니 다음과 같은 코드가 스케줄의 개수만큼 반복되고 있음을 확인할 수 있었다.

양방향 연관관계 매핑으로 인해 계속해서 left outer join 이 발생하고 있었는데, 너무 많은 join 이 반복돼서 

이로 인해 성능이 안나오고 있는 것이라고 추측해보았다.

ManagerAssignSchedule 과 UpdateReq 와 걸려있는 연관관계
ShceduleUpdateReq 에 걸려있는 양방향 연관관계

(이제 보니 지연로딩 설정도 안해주어서 뒷 부분에서 지연로딩 설정도 추가하였다.)

그렇기에 fetch join 을 사용하여 반복적인 left_outer_join 을 하는 것이 아닌, inner join 으로

쿼리를 줄이고자 했으나 fetch join 을 사용하니 내가 예상한 것과는 다른 쿼리가 출력되는 것을 확인할 수 있었다.

이미 위의 쿼리에서 문제가 있었는데 잡아내지 못하다가 fetch join 을 하니 정확한 문제점이 보였다.

바로 inner join 을 한 뒤, ScheduleUpateReq 클래스와 ManagerAssignSchedule 의 id 값이 일치하는 데이터만 불러오는

이상한 현상이 생긴 것!..

 

-> 2023.1.24 현재, 다시 확인해보니 이는 inner join 이기 때문에 데이터 값이 일치하는 데이터만 가져오는 것이 맞다! 지금에 와서 생각해보니 left outer join 으로 수정했어도 문제가 해결 됐을 것 같다.

하지만 하단처럼 주 테이블을 바꿔주긴 했어야 했다고 생각한다!..

 

이는 ScheduleUpateReq 에 수정요청을 한 데이터 값만 불러오는 쿼리로 , 내가 전혀 의도하지 않은 쿼리가 생성됐고

화면 출력 결과 또한 ScheduleUpateReq 값이 존재하는 데이터만 출력되게 됐다.

 

업데이트 요청을 보낸 스케줄만 조회가 됐다!..

(단 두개의 데이터만 달력에 출력됨을 확인..)

왜 이러한 문제가 생기는가 찾아본 결과, OneToOne 의 양방향 연관관계 매핑에 문제가 있었다.

일대일 관계는 사용해본적이 없어서 큰 생각 없이 아 외래키가 있는 곳을 주 테이블로 생각하고 @JoinColumn 을

사용해야지! 했었는데 이번 경우는 조금 생각을 달리 했어야 했다.

UpdateReq 에 존재하는 외래키

 

보통의 경우, 외래키가 있는 곳을 주 테이블로 생각해서 연관관계를 매핑하는 것이 올바르다.

(OneToOne 을 제외하면 외래키를 설정하는데에 있어 어려움이 없다. 하지만 OneToOne 의 경우, 외래키를 어디에 두어야 할지

선택에 따라 달라지고, 많이 사용되는 주 테이블에 외래키를 두고 연관관계를 매핑하는게 편한 것 같다.)

 

UpdateReq 테이블의 경우 사용하는 경우가 거의 없고, AssignSchedule 의 경우 여러 코드에서 사용하는 경우가 많았다.

그렇기에 AssignSchedule 을 주 테이블로 두고, 외래키 또한 AssignSchedule 에 두고 연관관계를 작성해야 한다.

 

기존의 경우 : UpdateReq 를 주 테이블로 설정한 경우

UpdateReq 테이블을 먼저 join 한 다음 AssignSchedule 의 값과 비교하여 일치하는 값만 출력

 

바뀐 경우 : AssignSchedule 을 주 테이블로 설정한 경우

AssignSchedule 에만 접근해서 데이터를 가져오고 끝.

UpdateReq 를 조회하고자 하는 경우, ManagerAssignSchedule 에 outer join 으로 접근해서

UpateReq 가 null 이 아닌 값을 찾아오고 서로 데이터를 비교해서 일치하는 ID 값만 출력.

 

-> 2023.1.24. inner join 이기 때문에 당연한 이야기이다!.. 부끄럽지만 SQL 에 대한 이해가 부족했다..

 

※ ERD 의 외래키 매핑을 바꾸어야 한다! 팀원들과 이야기 필요.

주 테이블을 바꾸어서 매핑한 결과, 위와 같은 단 한번의 쿼리로만 가져오는 것을 확인할 수 있다.

(쿼리문도 정상출력)

처음에는 왜 UpdateReq 테이블을 조인하지? 라고 생각하면서 별 생각 없이 음 쿼리가 많이 생성되고, 일반 조인으로 데이터를 가져오고 있으니 fetch join 으로 해결하면 되겠군

이라고 생각했으나!.. 

이제와서 생각해보니 UpdateReq 가 전혀 의미 없는 테이블인데 왜 가져오는지, 뭐가 문제인지 부터 고민했어야 했다.

필요가 없어진 쿼리문..

 

하지만  위의 코드로 해결을 본 후에도 여전히 버벅거린다.. 왜일까..

이번에는 User 테이블이 말썽이였다.

아마 스케줄이 생성되는 양 만큼 User 테이블에서 데이터를 하나하나 쿼리를 생성해서 가져오는 듯 했고

문제의 코드는 다음과 같다.(즉, 스케줄 개수만큼 User 조회 쿼리가 생성!..)

상단의 hash.put() 코드는 스케줄 데이터를 조회하기 위함이고, 아래의 user 코드는 Background 색상을 변경해주기

위한 코드인데, for 문 안에서 하나의 스케줄이 조회될 때마다 계속 user 를 찾아오는 불상사가 생겨서 이런 일이 발생하는 중이였다.

 

-> 2023.1.24. Optional 에서 null 을 그대로 뽑아오며 하단에 != null 을 통해 체크하다니..

 

이를 해결하기 위해서는 근본적으로 mangerAssignSchedule 데이터를 가져오면서 그 안에서 또 userService 에서 쿼리를 날리는

중첩 쿼리를 없애야 했다.

 

고민을 해본 결과, 굳이 저렇게 복잡하게 쿼리를 짤 필요가 없다는 생각이 들었다.

결국 내가 필요한 건 현재 User의 code 값이다.

그러면 굳이 user 테이블에서 이름이 같은(title이 동일) 값을 가져오기 보다는 그냥 ManagerAssignSchedule 에서

현재 User 를 불러오면 되는 일 아닌가?.. 

그렇게 되면 반복해서 User 테이블에 접근할 필요 없이

ManagerAssignSchedule 테이블을 조회하는 것만으로도 User 를 접근하는 것이 가능했다.

(양방향 연관관계로 매핑돼 있기 때문이다.)

(위의 코드에서는 잘 짰으면서 왜 아래에서는 저렇게 짯는지?...)

위와 같이 작성하니 다행히 문제 없이 정상 작동함을 확인할 수 있었다.

다만 문제점은 User 테이블에 또 다시 여러번 접근한다는 것!.. 

아래의 user 를 조회하는 쿼리문이 User 의 수만큼 또 다시 반복되기 시작했다.

조회할 때마다 User 가 반복돼서 조회되는 문제는 fetch join 을 사용하면 되지 않을까 라고 생각했다.( 드디어! )

 

일반 join 과 fetch join 의 차이점

  • 일반 Join
    • Fetch Join과 달리 연관 Entity에 Join을 걸어도 실제 쿼리에서 SELECT 하는 Entity는
      오직 JPQL에서 조회하는 주체가 되는 Entity만 조회하여 영속화
    • 조회의 주체가 되는 Entity만 SELECT 해서 영속화하기 때문에 데이터는 필요하지 않지만 연관 Entity가 검색조건에는 필요한 경우에 주로 사용됨
  • Fetch Join
    • 조회의 주체가 되는 Entity 이외에 Fetch Join이 걸린 연관 Entity도 함께 SELECT 하여 모두 영속화
    • Fetch Join이 걸린 Entity 모두 영속화하기 때문에 FetchType이 Lazy인 Entity를 참조하더라도
      이미 영속성 컨텍스트에 들어있기 때문에 따로 쿼리가 실행되지 않은 채로 N+1문제가 해결됨

 

 

fetch join 의 경우, 앞에서 설명했듯 연관 엔티티도 모두 영속화 하기 때문에 조회 시에

별도의 쿼리문이 생성되지 않는다. (N+1 문제 해결)

 

User 테이블을 fetch join 해서 조회한 결과

처음의 그 수많은 쿼리들이 모두 사라지고.. 단 한번의 쿼리만으로 전부 조회가 됐다!!

테스트 해본 결과, 버벅임 없이 바로바로 조회되는 것 같다!!

공부한 이론을 적용해서 정상적으로 작동하는 것 만큼 큰 기쁨이 없는 것 같다!!!

다른 페이지들도 쿼리문 생성 개수를 확인하고 최적화를 해봐야겠다!

 

댓글