본문 바로가기
스프링과 JPA 기반 웹 어플리케이션 개발/5부 모임

64. 모임 목록 조회

by Backchus 2020. 5. 4.

 

  • N+1문제 해결

 

모임 목록 요청을 처리하는 핸들러 작성

package me.weekbelt.studyolle.event;

@Controller
@RequestMapping("/study/{path}")
@RequiredArgsConstructor
public class EventController {

    // ......
    
    private final EventRepository eventRepository;

    // ......
    
    @GetMapping("/events")
    public String viewStudyEvents(@CurrentAccount Account account, @PathVariable String path, Model model) {
        Study study = studyService.getStudy(path);
        model.addAttribute("account", account);
        model.addAttribute("study", study);

        List<Event> events = eventRepository.findByStudyOrderByStartDateTime(study);
        List<Event> newEvents = new ArrayList<>();
        List<Event> oldEvents = new ArrayList<>();
        events.forEach(e -> {
            if (e.getEndDateTime().isBefore(LocalDateTime.now())) {
                oldEvents.add(e);
            } else {
                newEvents.add(e);
            }
        });
        model.addAttribute("newEvents", newEvents);
        model.addAttribute("oldEvents", oldEvents);

        return "study/events";
    }
}

 

EventRepository에 findByStudyOrderByStartDateTime() 메소드 추가

package me.weekbelt.studyolle.event;

// ......

@Transactional(readOnly = true)
public interface EventRepository extends JpaRepository<Event, Long> {

    List<Event> findByStudyOrderByStartDateTime(Study study);
}

 

모임 목록을 보여주는 study/events.html 작성

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments.html :: head"></head>
<body>
<nav th:replace="fragments.html :: main-nav"></nav>
<div th:replace="fragments.html :: study-banner"></div>
<div class="container">
    <div th:replace="fragments.html :: study-info"></div>
    <div th:replace="fragments.html :: study-menu(studyMenu='events')"></div>
    <div class="row my-3 mx-3 justify-content-center">
        <div class="col-10 px-0 row">
            <div class="col-2 px-0">
                <ul class="list-group">
                    <a href="#" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
                        새 모임
                        <span th:text="${newEvents.size()}">2</span>
                    </a>
                    <a href="#" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center">
                        지난 모임
                        <span th:text="${oldEvents.size()}">5</span>
                    </a>
                </ul>
            </div>
            <div class="col-10 row row-cols-1 row-cols-md-2">
                <div th:if="${newEvents.size() == 0}" class="col">
                    새 모임이 없습니다.
                </div>
                <div class="col mb-4 pr-0" th:each="event: ${newEvents}">
                    <div class="card">
                        <div class="card-header">
                            <span th:text="${event.title}">title</span>
                        </div>
                        <ul class="list-group list-group-flush">
                            <li class="list-group-item">
                                <i class="fa fa-calendar"></i>
                                <span class="calendar" th:text="${event.startDateTime}"></span> 모임 시작
                            </li>
                            <li class="list-group-item">
                                <i class="fa fa-hourglass-end"></i> <span class="fromNow" th:text="${event.endEnrollmentDateTime}"></span> 모집 마감,
                                <span th:if="${event.limitOfEnrollments != 0}">
                                    <span th:text="${event.limitOfEnrollments}"></span>명 모집 중
                                    (<span th:text="${event.numberOfRemainSpots()}"></span> 자리 남음)
                                </span>
                            </li>
                            <li class="list-group-item">
                                <a href="#" th:href="@{'/study/' + ${study.path} + '/events/' + ${event.id}}" class="card-link">자세히 보기</a>
                            </li>
                        </ul>
                    </div>
                </div>
            </div>
        </div>
        <div class="col-10 px-0 row">
            <div class="col-2"></div>
            <div class="col-10">
                <table th:if="${oldEvents.size() > 0}" class="table table-hover">
                    <thead>
                    <tr>
                        <th scope="col">#</th>
                        <th scope="col">지난 모임 이름</th>
                        <th scope="col">모임 종료</th>
                        <th scope="col"></th>
                    </tr>
                    </thead>
                    <tbody th:each="event: ${oldEvents}">
                    <tr>
                        <th scope="row" th:text="${eventStat.count}">1</th>
                        <td th:text="${event.title}">Title</td>
                        <td>
                            <span class="date-weekday-time" th:text="${event.endDateTime}"></span>
                        </td>
                        <td>
                            <a href="#" th:href="@{'/study/' + ${study.path} + '/events/' + ${event.id}}" class="card-link">자세히 보기</a>
                        </td>
                    </tr>
                    </tbody>
                </table>
            </div>
        </div>
    </div>
    <div th:replace="fragments.html :: footer"></div>
</div>
<script th:replace="fragments.html :: tooltip"></script>
<script th:replace="fragments.html :: date-time"></script>
</body>
</html>

 

모임에 몇자리가 남아있는지 확인하기 위해 Event 엔티티에 numberOfRemainSpots메소드 추가

package me.weekbelt.studyolle.domain;

@NoArgsConstructor
@Getter
@Setter
@EqualsAndHashCode(of = "id")
@Entity
public class Event {

    // ........
    
    public int numberOfRemainSpots() {
        return this.limitOfEnrollments - (int) this.enrollments.stream().filter(Enrollment::isAccepted).count();
    }

}

 

그런데 이렇게 호출을 하게되면 각각의 모임마다 enrollments를 조회하기때문에 모임이 많아진다면 N+1문제로 성능상에 문제가 생길것이 자명하다. 따라서 N+1을 해결해야한다.

 

Event Entity에서 Event를 호출할때 enollments를 같이 조인해서 호출하도록 설정을 한다.

package me.weekbelt.studyolle.domain;

@NamedEntityGraph(
        name = "Event.withEnrollments",
        attributeNodes = @NamedAttributeNode("enrollments")
)
public class Event {

    // .........
    
    @OneToMany(mappedBy = "event")
    private List<Enrollment> enrollments = new ArrayList<>();
    
    // .........

}

 

EventRepository에 EntityGraph를 설정한다.

package me.weekbelt.studyolle.event;

@Transactional(readOnly = true)
public interface EventRepository extends JpaRepository<Event, Long> {

    @EntityGraph(value = "Event.withEnrollments", type = EntityGraph.EntityGraphType.LOAD)  // 추가
    List<Event> findByStudyOrderByStartDateTime(Study study);
}

 

 

참고: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-JPA-%EC%9B%B9%EC%95%B1#

 

스프링과 JPA 기반 웹 애플리케이션 개발 - 인프런

이 강좌에서 여러분은 실제로 운영 중인 서비스를 스프링, JPA 그리고 타임리프를 비롯한 여러 자바 기반의 여러 오픈 소스 기술을 사용하여 웹 애플리케이션을 개발하는 과정을 학습할 수 있습니다. 이 강좌를 충분히 학습한다면 여러분 만의 웹 서비스를 만들거나 취직에 도움이 될만한 포트폴리오를 만들 수 있을 겁니다. 중급이상 웹 개발 프레임워크 및 라이브러리 Java Spring Spring Boot Spring Data JPA Thymeleaf 온라인 강의

www.inflearn.com

 

'스프링과 JPA 기반 웹 어플리케이션 개발 > 5부 모임' 카테고리의 다른 글

66. 모임 취소  (0) 2020.05.04
65. 모임 수정  (0) 2020.05.04
63. 모임 조회  (0) 2020.05.04
62. 모임 만들기 폼 서브밋  (0) 2020.05.04
61. 모임 만들기  (0) 2020.04.30