-
[MySQL/Java] DB 쿼리 성능개선 (1) test용 mock/bulk data 삽입 방법DB/MySQL 2023. 6. 26. 22:09반응형
Gotcha 프로젝트 리팩토링을 하면서 DB 성능 개선에 도전해보고 싶다는 목표!
굉장히 조그마한 프로젝트였기 때문에 쿼리를 신경 쓰지 않고 작성했는데,
데이터가 몇십, 몇백만 단위로 쌓이게 된다면 문제가 발생할 수 있을 것 같다는 생각이 들었다.
그래서 쿼리 성능 테스트를 진행해보기로 했다.
하지만.. 대량 데이터를 넣는 것부터 배울게 아주 많군요...!
우선 테스트하고자 하는 코드를 간략화 하면 아래와 같았다.
@CustomSpringBootTest @AutoConfigureMockMvc class ParticipantControllerTest { @Autowired EntityManager em; @Autowired MockMvc mockMvc; @Autowired ObjectMapper objectMapper; @Autowired PasswordEncoder passwordEncoder; private Long ROOM_ID; private Long ROOM_HAS_NO_PROBLEM_ID; private final Integer USER_PASSWORD = 1234; private final LocalDateTime GAME_START_TIME = LocalDateTime.now(); private final LocalDateTime GAME_END_TIME = LocalDateTime.now().plusDays(7); private final LocalDateTime USER_GAME_START_TIME = LocalDateTime.now().plusHours(1); private final LocalDateTime USER_GAME_END_TIME = LocalDateTime.now().plusHours(2); private final int NUMBER_OF_PROBLEMS = 5; private final String url = "http://localhost:8080/api/game/"; @BeforeEach void setUp() { // 유저(출제자) 생성 Member member = Member.builder() .email("yezi@naver.com") .organization("naver").build(); em.persist(member); // 방 생성 Room room = Room.builder() .title("ssafy") .code(101101) .hasReward(true) .member(member) .startTime(GAME_START_TIME) .endTime(GAME_END_TIME) .build(); em.persist(room); ROOM_ID = room.getId(); // 문제 없는 방 생성 Room roomB= Room.builder() .title("임시") .build(); em.persist(roomB); ROOM_HAS_NO_PROBLEM_ID = roomB.getId(); // 보상 생성 for (int i = 1; i <= 3; i++) { Reward reward = Reward.builder() .name("상품" + i) .grade(i) .room(room) .build(); em.persist(reward); } // 문제 생성 for (int i = 1; i <= NUMBER_OF_PROBLEMS; i++) { Problem problem = Problem.builder() .name("문제" + i) .hint("힌트입니다.") .imageUrl("url") .room(room) .build(); em.persist(problem); } // 참여자 생성(게임완료) String HASH_PWD = passwordEncoder.encode(USER_PASSWORD.toString()); LocalDateTime startTime = LocalDateTime.now().plusHours(1); for (int i = 200000; i > 0; i--) { LocalDateTime endTime = LocalDateTime.now().plusHours((i % 10) + 1); Participant participant = Participant.builder() .nickname("참여자" + i) .password(HASH_PWD) .startTime(startTime) .endTime(endTime) .duration(Duration.between(startTime, endTime)) .solvedCnt(i % 5) .isFinished(true) .room(room) .build(); em.persist(participant); } // 참여자 생성(게임완료) Participant participantA = Participant.builder() .nickname("YEZI") .password(HASH_PWD) .startTime(startTime) .endTime(startTime.plusDays(1)) .duration(Duration.between(startTime, startTime.plusMinutes(1))) .solvedCnt(1) .isFinished(true) .room(room) .build(); em.persist(participantA); // 참여자 생성(게임미완료) Participant participantB = Participant.builder() .nickname("TAEGYU") .password(HASH_PWD) .startTime(startTime) .solvedCnt(2) .isFinished(false) .room(room) .build(); em.persist(participantB); // 참여자 생성(가입만 완료) Participant participantC = Participant.builder() .nickname("DASOM") .password(HASH_PWD) .isFinished(false) .room(room) .build(); em.persist(participantC); // 참여자 생성(문제 없는 방) Participant participantTemp = Participant.builder() .nickname("YEZI") .password(HASH_PWD) .startTime(startTime) .endTime(startTime.plusDays(1)) .duration(Duration.between(startTime, startTime.plusDays(1))) .solvedCnt(3) .isFinished(true) .room(roomB) .build(); em.persist(participantTemp); } @Nested @DisplayName("랭킹 확인하기 API TEST") class GetRankList { @Test @DisplayName("해당하는 방 없음") void invalidRoom() throws Exception { RankInfoRequest request = RankInfoRequest.builder() .roomId(100000000L) .nickname("YEZI") .build(); mockMvc .perform(post(url + "rank") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsBytes(request)) ) .andExpect(jsonPath("$.status", is(404))) .andExpect(jsonPath("$.code", is("R100"))); } @Test @DisplayName("해당 유저를 찾을 수 없음") void invalidParticipant() throws Exception { RankInfoRequest request = RankInfoRequest.builder() .roomId(ROOM_ID) .nickname("MINSU") .build(); mockMvc .perform(post(url + "rank") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsBytes(request)) ) .andExpect(jsonPath("$.status", is(404))) .andExpect(jsonPath("$.code", is("P200"))); } @Test @DisplayName("랭킹 목록 불러오기 성공") void getRank() throws Exception { RankInfoRequest request = RankInfoRequest.builder() .roomId(ROOM_ID) .nickname("YEZI") .build(); MockHttpServletResponse response = mockMvc.perform(post(url + "rank") .content(objectMapper.writeValueAsBytes(request)) .contentType(MediaType.APPLICATION_JSON)) .andDo(print()) .andReturn() .getResponse(); BaseResponse<List<ParticipantRankListResponse>> arrayList = objectMapper.readValue(response.getContentAsString(), BaseResponse.class); assertEquals(4, arrayList.getResult().size()); } } }
기존 테스트 방법 : @BeforeEach 사용하여 테스트코드에서 Mock 데이터 삽입
기존에는 데이터를 삽입할 때 @BeforeEach를 사용하여 각 테스트를 돌기 전 데이터를 삽입했다.
그러다보니 테스트 개수가 많아질수록 똑같은 데이터를 지속적으로 삽입해야 해서 테스트 시간이 오래 걸렸다.
특히 몇십, 몇백만개 단위로 데이터를 넣어야 하니 데이터 넣는데만 20초가 넘게 걸리는 상황 발생;그래서 다른 테스트 방법을 찾아야겠다고 판단했다.
첫 번째 시도 : @BeforeAll을 사용하여 데이터 삽입
시행착오 1. @BeforeEach ➡️ @BeforeAll로 단순 변경
- 될 거라 생각 안했고, 당연히 에러 발생!
- 아래와 같은 에러가 발생하고, 하위의 테스트들은 모두 무시되었다.... @BeforeAll void setUp() { // 유저(출제자) 생성 Member member = Member.builder() .email("yezi@naver.com") .organization("naver").build(); em.persist(member); ...
javax.persistence.TransactionRequiredException: No EntityManager with actual transaction available for current thread - cannot reliably process 'persist' call
우선은 @BeforeAll 사용 방식이 잘못됐다는 것을 발견했다.
- Test 객체는 기본적으로 "PER-METHOD" 라이프사이클을 갖고 있다. 즉, 테스트 메소드를 실행할 때마다 테스트 객체를 새로 생성하게 되는데, 테스트 메소드를 실행하기에 앞서 @BeforeEach 어노테이션이 있는 메서드를 호출하여 설정 작업을 수행한다. 그러다보니 각 테스트 메서드마다 새로운 테스틑 객체가 생성되어, @BeforeAll에서 초기화한 값을 사용할 수 없게 된다.
- 따라서 @BeforeAll 어노테이션을 쓰기 위해서는 테스트 객체를 static으로 선언하거나, 테스트 객체의 라이프 스타일을 "PER-CLASS"로 변경해주어야 한다.시행착오 2. @TestInstance(TestInstance.Lifecycle.PER_CLASS)과 @Transactional 추가
- 그렇다면 이제 되어야 하지 않을까..? 했는데 똑같은 실패가 떴다. em이 계속 없다고 뜨는 것..!
- @BeforeAll의 경우 Spring Test에서 자체적으로 트랜젝션을 자동으로 시작하고 관리하지 않음. 그렇기 때문에 어디서 트랜잭션을 확성화해야 하는지에 대한 정보가 없어 엔티티의 영속화가 불가능한 것
- 그래서 아래와 같이 @Transactional을 붙여보았지만 똑같이 문제가 해결되지는 않았다.@CustomSpringBootTest @AutoConfigureMockMvc @TestInstance(TestInstance.Lifecycle.PER_CLASS) @Transactional class ParticipantControllerTest { ... @BeforeAll void setUp() { // 유저(출제자) 생성 Member member = Member.builder() .email("yezi@naver.com") .organization("naver").build(); em.persist(member); // 방 생성 Room room = Room.builder() ...
- 추가적인 방법으로는 명시적으로 트랜젝션을 시작하고 커밋해야 하는데.. 과연 이렇게 코드의 복잡성을 증가시키는게 좋은 방법인가에 대한 의문이 들었다. 따라서 아예 테스트가 아닌 디비에 데이터를 넣는 방식으로 재접근을 하기로 했다.
- 이 부분에 대해서는 좀 더 간단한 테스트코드로 몇 번 더 연습해 보아야겠다.두 번째 시도 : MySQL DB에 삽입
다음으로는 MySql로 쿼리를 한 번에 삽입하는 방법이다.
Procedure를 활용하여 함수를 짜고, 데이터를 삽입해줬다.-- Member 데이터 삽입 (게임룸 생성을 위한 서비스 가입 유저) INSERT INTO member VALUE (1, 'erin@gmail.com', 'erin', 'tistory', '1234', 'url', 'normal'); -- Room 데이터 삽입 (게임룸 생성) insert into room value (1, 0402, 'purple', date_add(now(), interval 12 day), 'Object를 찾아라',' eventUrl', false, 'logoUrl', date_add(now(), interval 2 day), 'title', '1'); -- Participant 데이터 삽입을 위한 함수 선언 (게임에 단순 참여한 유저) DELIMITER $$ CREATE PROCEDURE createParticipant() -- ⓐ createParticipant이라는 이름의 프로시져 BEGIN DECLARE i INT DEFAULT 1; -- ⓑ i변수 선언, default값으로 1설정 WHILE (i <= 200000) DO -- ⓒ for문 작성(i가 200_000이 될 때까지 반복) INSERT INTO participant (id, duration, end_time, is_finished, nickname, password, phone_number, solved_cnt, start_time, room_id) VALUES (i, ceiling(rand() * 10), date_add(now(), interval 12 day), true, concat('ssafy',i), '1234','010-7183-0996',ceiling(rand() * 10), date_add(now(), interval 2 day), 1); SET i = i + 1; -- ⓔ i값에 1더해주고 WHILE문 처음으로 이동 END WHILE; END$$ DELIMITER ; -- ⓕ구분 기호를 다시 ;로 바꿔주기 -- Participant 데이터 삽입 call createParticipant;
이제 데이터 삽입은 완료 됐다!!
반응형'DB > MySQL' 카테고리의 다른 글
[MySQL] Stored Procedure (0) 2024.01.22 [MySQL] Stored Function (0) 2024.01.21 [MySQL/Java] DB 쿼리 성능개선 (2) 랭킹 조회 속도 개선하기 (5s ➡️ 1s대로 줄이기!) (0) 2023.06.29