Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ spring:
properties:
hibernate:
format_sql: true
# 지연 로딩 연관(회원의 직책·트랙 등)을 목록 크기만큼 한 번에 읽는다(N+1 방지).
default_batch_fetch_size: 100
open-in-view: false

# 배포는 컨테이너를 내렸다 올린다. 처리 중인 요청과 큐에 남은 비동기 메일(@Async)을 마치고 종료한다.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.bcsdlab.bcsdinternalapiv2.member;

import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

import com.bcsdlab.bcsdinternalapiv2.IntegrationTestSupport;
import com.bcsdlab.bcsdinternalapiv2.member.model.Member;
import com.bcsdlab.bcsdinternalapiv2.member.model.MemberRole;
import com.bcsdlab.bcsdinternalapiv2.member.model.MemberStatus;
import com.bcsdlab.bcsdinternalapiv2.member.model.MemberType;
import com.bcsdlab.bcsdinternalapiv2.member.model.Position;
import com.bcsdlab.bcsdinternalapiv2.member.repository.MemberRepository;
import com.bcsdlab.bcsdinternalapiv2.member.repository.PositionRepository;
import com.bcsdlab.bcsdinternalapiv2.track.model.TrackMaster;
import com.bcsdlab.bcsdinternalapiv2.track.repository.TrackMasterRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.persistence.EntityManagerFactory;
import java.util.List;
import org.hibernate.SessionFactory;
import org.hibernate.stat.Statistics;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.test.context.TestPropertySource;

/** 명부 목록은 회원 수와 무관하게 일정한 쿼리 수로 조회한다(회원마다 직책·트랙을 따로 읽지 않는다). */
@TestPropertySource(properties = "spring.jpa.properties.hibernate.generate_statistics=true")
class MemberDirectoryQueryCountIntegrationTest extends IntegrationTestSupport {

private static final int MEMBER_COUNT = 20;

@Autowired
private MemberRepository memberRepository;

@Autowired
private PositionRepository positionRepository;

@Autowired
private TrackMasterRepository trackMasterRepository;

@Autowired
private PasswordEncoder passwordEncoder;

@Autowired
private EntityManagerFactory entityManagerFactory;

@Test
void 명부_조회_쿼리_수는_회원_수에_비례하지_않는다() throws Exception {
List<TrackMaster> tracks = trackMasterRepository.findAll();
List<Position> positions = positionRepository.findAll();
for (int i = 0; i < MEMBER_COUNT; i++) {
memberRepository.save(Member.builder()
.studentNumber("2030%04d".formatted(i))
.name("명부" + i)
.track(tracks.get(i % tracks.size()))
.generation("24-하")
.memberType(MemberType.REGULAR)
.university("한국기술교육대학교")
.department("컴퓨터공학부")
.positions(positions.isEmpty() ? List.of() : List.of(positions.get(i % positions.size())))
.email("dir" + i + "@bcsd.club")
.phoneNumber("0100000%04d".formatted(i))
.status(MemberStatus.ACTIVE)
.role(MemberRole.MEMBER)
.build());
}
String token = adminToken();

Statistics statistics = entityManagerFactory.unwrap(SessionFactory.class).getStatistics();
statistics.clear();
mockMvc.perform(get("/v1/admin/members").param("size", "50")
.header("Authorization", "Bearer " + token))
.andExpect(status().isOk())
.andExpect(jsonPath("$.members.length()").value(MEMBER_COUNT + 1));

assertThat(statistics.getPrepareStatementCount()).isLessThan(15);
}

private String adminToken() throws Exception {
memberRepository.save(Member.builder()
.studentNumber("20309999")
.password(passwordEncoder.encode("Temp1234"))
.name("관리자")
.track(trackMasterRepository.findByCode("BACKEND").orElseThrow())
.generation("16")
.memberType(MemberType.MENTOR)
.university("한국기술교육대학교")
.department("컴퓨터공학부")
.email("admin@bcsd.club")
.phoneNumber("01099999999")
.status(MemberStatus.ACTIVE)
.role(MemberRole.ADMIN)
.build());
String body = mockMvc.perform(post("/v1/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"studentNumber\":\"20309999\",\"password\":\"Temp1234\",\"rememberMe\":false}"))
.andExpect(status().isOk())
.andReturn().getResponse().getContentAsString();
return new ObjectMapper().readTree(body).get("accessToken").asText();
}
}
Loading