시스템 환경

Hardware Resources : CPU 8 Core, 4GB Memory

Test Tool : k6 

Mornitoring Tool : Prometheus , Grafana

Application : Spring Boot Application (Boot Version 3.2.5)

DB : Mysql 

 

모니터링 환경

- K6 에서 조금 더 세세한 정보를 수집하기 위해서 InfluxDB + Grafana 연동

- SpringBoot Application 관련 메트릭 정보 수집을 위해 SpringBoot Actuator + Prometheus + Grafana 연동

- 시스템 메트릭 수집을 위해 Node Exporter + Prometheus + Grafana 연동

 

테스트 시나리오

- 약 40만개의 일정 데이터로 테스트를 진행했습니다. (일정 = 고정일정 + 변동일정 + 일반일정 ) 

   본 테스트에서는 일반일정 데이터는 거의 추가하지 않고 (약 1만개) 고정일정 + 변동일정 데이터만 추가하여 진행했다.

 

- 일정관리 웹 서비스로써 일정조회에 대한 요청이 대다수 일 것이라고 판단하고 테스트를 진행했다. 

- 테스트 시나리오는 일정 조회 Api 에 Virtual User를 활용하여 Get 요청을 보내 유저의 최근 3개월 내 모든 일정을 조회한다.

- 응답시간 목표를 95% 이상의 요청은 500ms 이하로 설정했다.

- CPU사용량, Memory 사용량  P95~P99 응답시간, TPS , 에러율 등을 고려하여 부하를 판단했다.

- IntelliJ Profiler , (디비 수준에서의 병목 쿼리 파악하기.) 병목 지점을 파악했다.

1. 성능 테스트

먼저 시스템의 임계점을 분석하기 위해 스트레스 테스트를 진행했고,  0명의 가상 유저부터 70명의 가상유저까지 점차적으로 늘리는 Ramping-vus 방식으로 테스트를 진행했다. 

StressTest Result

 

Virtual User의 수가 약 40명 정도 부근에 가까워 졌을때 부터 지연율이 높아졌으며 p95 기준 응답시간 목표인 500ms 보다 더 늦은약 500ms~1s 사이의 응답 시간을 보여 주었다. 그렇기 때문에 해당 지점을 임계치를 초과한 지점으로 보고 45Vus 를 유지한 상태에서 부하테스트를 진행하였고 결과 역시나 성능이 좋지 않음을 확인할 수 있었다.

Load Test

 

2. 병목 지점 파악

Spring Profiler

부하 테스트 이후 먼저 Spring profiler를 통해 실행결과를 확인했다.

 

 다음은 일정 조회시 조회 시나리오에 맞게 분석한 결과이다.

 

- 1. memberRepository.find - 단순 조회

 

< 전체 실행시간>
< Cpu 사용시간>

- 2. vScheduleRepository.find - 단순 조회 

- 3. fScheduleDetailRepository.find - 1번의 left join

- 4. nScheduleDetailRepository.find - 1번의 left join

<2,3,4 전체 실행시간>
<2,3,4 cpu 사용시간>

 

위 결과에서 확인할 수 있는건 cpu 사용시간에 비해 전체 실행시간이 훨씬 길다는 것을 확인할 수 있었다. 즉 전체 실행 시간 대비 CPU Time이 매우 적으므로, 대부분의 시간은 I/O 대기 시간이라고 볼 수 있다.

 

 추가적으로 Hikary CP 의 커넥션을 확인해 보면 모든 커넥션이 사용중이고, 평균 9개에서 최대 15개의 요청이 커넥션을 얻기 위해 대기중이라는 것을 확실히 확인할 수 있었다. 그렇기 때문에 DB에서의 병목을 파악해서 위 문제를 해결하고자 했고 쿼리 실행 계획을 활용해서 위 시나리오들의 각 쿼리들에 대해서 분석해보았다. 

1. EXPLAIN ANALYZE SELECT * FROM member WHERE email = 'normal1@example.com';

Res) -> Rows fetched before execution  (cost=0..0 rows=1) (actual time=251e-6..293e-6 rows=1 loops=1)

2. EXPLAIN ANALYZE SELECT *  FROM v_schedule  WHERE start_date_time >= '2025-01-01 00:00:00'    AND end_date_time <= '2025-03-08 23:59:59'    AND created_by = 'normal1@example.com'	

Result) Filter: ((v_schedule.created_by = 'normal1@example.com') and (v_schedule.start_date_time >= TIMESTAMP'2025-01-01 00:00:00') and (v_schedule.end_date_time <= TIMESTAMP'2025-03-08 23:59:59'))  (cost=20367 rows=2199) (actual time=0.229..126 rows=2 loops=1)
    -> Table scan on v_schedule  (cost=20367 rows=197977) (actual time=0.209..108 rows=199999 loops=1)
    
    
3. EXPLAIN ANALYZE SELECT *  FROM f_schedule_detail fd left outer join f_schedule f on f.f_schedule_id = fd.f_schedule_id WHERE fd.start_date_time >= '2025-01-01 00:00:00'    AND fd.end_date_time <= '2025-03-08 23:59:59'    AND fd.created_by = 'normal1@example.com'	

Res) Nested loop left join  (cost=18427 rows=1923) (actual time=0.213..117 rows=35 loops=1)
    -> Filter: ((fd.created_by = 'normal1@example.com') and (fd.start_date_time >= TIMESTAMP'2025-01-01 00:00:00') and (fd.end_date_time <= TIMESTAMP'2025-03-08 23:59:59'))  (cost=17754 rows=1923) (actual time=0.124..117 rows=35 loops=1)
        -> Table scan on fd  (cost=17754 rows=173117) (actual time=0.114..99.2 rows=175000 loops=1)
    -> Single-row index lookup on f using PRIMARY (f_schedule_id=fd.f_schedule_id)  (cost=0.25 rows=1) (actual time=0.00294..0.00298 rows=1 loops=35)
    
4. EXPLAIN ANALYZE SELECT *  FROM n_schedule_detail nd left outer join n_schedule n on n.n_schedule_id = nd.n_schedule_id WHERE nd.start_date_time >= '2025-01-01 00:00:00'    AND nd.end_date_time <= '2025-03-08 23:59:59'    AND nd.created_by = 'normal1@example.com'

Res) -> Nested loop left join  (cost=666 rows=64.8) (actual time=9.46..9.46 rows=0 loops=1)
    -> Filter: ((nd.created_by = 'normal1@example.com') and (nd.start_date_time >= TIMESTAMP'2025-01-01 00:00:00') and (nd.end_date_time <= TIMESTAMP'2025-03-08 23:59:59'))  (cost=607 rows=64.8) (actual time=9.46..9.46 rows=0 loops=1)
        -> Table scan on nd  (cost=607 rows=5829) (actual time=0.107..8.11 rows=5963 loops=1)
    -> Single-row index lookup on n using PRIMARY (n_schedule_id=nd.n_schedule_id)  (cost=0.814 rows=1) (never executed)

 

1번의 경우 이미 유니크 키로 지정되어있는 유저의 email을 기준으로 index가 생성되어 있기때문에 매우 빠른시간내에 조회하고 있다는 점을 확인할 수 있었다.

 

2번,3번,4번 같은 경우는 join 대상이 되는 테이블같은 경우는 PK 인덱스를 통해 빠르게 조회가 되는 것을 확인할 수 있지만 자기 테이블에 대해서는 인덱스 없이 Full-Scan 하고 있었기 때문에 적절한 인덱스를 생성하여 조회 시간을 감축시킬 수 있다고 생각했다. 

각 일정들의 조회를 동반하는 쿼리 중 id로 조회하는걸 제외하면 start_date_time, created_by를 기준으로 조회를 하는 경우가 대다수이고 거의 대부분의 시나리오에서 위와 같은 모든 일정을 start_date_time, end_date_time 사이의 일정을 조회하기 때문에start_date_time, end_date_time,created_by 에 대해서 복합 인덱스를 생성하는 것이 좋을 것이라 판단했다.

 

3. Index 적용

인덱스를 설계한다고 했을 때, start_date_time,end_date_time,created_by 순으로 인덱스를 설계한다고 가정하면 첫 번째 컬럼이 범위 검색이므로 인덱스가 제대로 작동하지 않을 수 있다고 생각했고 실제로도 그랬다. 따라서 (created_by, start_date_time, end_date_time) 의 복합 인덱스를 생성했다.

 

고정일정 인덱스 적용 후 

Nested loop left join  (cost=22.5 rows=35) (actual time=0.14..0.311 rows=35 loops=1)
    -> Index range scan on fd using f_detail_created_start_end_idx over (created_by = 'normal1@example.com' AND '2025-01-01 00:00:00' <= start_date_time), with index condition: ((fd.created_by = 'normal1@example.com') and (fd.start_date_time >= TIMESTAMP'2025-01-01 00:00:00') and (fd.end_date_time <= TIMESTAMP'2025-03-08 23:59:59'))  (cost=16 rows=35) (actual time=0.0772..0.223 rows=35 loops=1)
    -> Single-row index lookup on f using PRIMARY (f_schedule_id=fd.f_schedule_id)  (cost=0.259 rows=1) (actual time=0.00219..0.00223 rows=1 loops=35)

변동일정 인덱스 적용 후 

Index range scan on v_schedule using v_schedule_created_start_end_idx over (created_by = 'normal1@example.com' AND '2025-01-01 00:00:00' <= start_date_time <= '2025-03-08 23:59:59'), with index condition: ((v_schedule.created_by = 'normal1@example.com') and (v_schedule.start_date_time between '2025-01-01 00:00:00' and '2025-03-08 23:59:59'))  (cost=1.16 rows=2) (actual time=0.0883..0.112 rows=2 loops=1)


일반일정 인덱스 적용 후

Nested loop left join  (cost=1.08 rows=1) (actual time=0.0591..0.0591 rows=0 loops=1)
    -> Index range scan on nd using n_detail_created_start_end_idx over (created_by = 'normal1@example.com' AND '2025-01-01 00:00:00' <= start_date_time), with index condition: ((nd.created_by = 'normal1@example.com') and (nd.start_date_time >= TIMESTAMP'2025-01-01 00:00:00') and (nd.end_date_time <= TIMESTAMP'2025-03-08 23:59:59'))  (cost=0.71 rows=1) (actual time=0.0581..0.0581 rows=0 loops=1)
    -> Single-row index lookup on n using PRIMARY (n_schedule_id=nd.n_schedule_id)  (cost=1.11 rows=1) (never executed)

 

 

4. Index 적용 후 비교

Index 적용 후 적용 전과 같은 조건으로 테스트를 진행했다. 

 

Spring Profiler 로 비교

<전체 실행시간>
<CPU 사용시간>

 

Index 적용 전과 CPU 사용시간은 크게 다르지 않은 것을 확인할 수 있다. 그리고 전체 실행시간은 약 180만 ms -> 약 6만 ms 로 감축된 것을 확인할 수 있다.

 

<Hikari CP Connection>

HikariCP의 Connection 사용 또한 처음 동시접속이 있었을때 pending 상태 이후로는 안정적으로 처리하는 것을 확인할 수 있었다.

또한 Grafana를 통해 확인해보면 req_duration (p95) 기준으로 4.94(4940ms)초에서 57.10ms 로 약 86배 줄어든 것을 확인할 수 있다.

 

위와 같이 성능 개선을 확인하고 Virture User의 수를 늘려서 추가적으로 스트레스 테스트를 진행해보았다.

RPS(Requests Per Seconds)가 23에서 744로 약 32배 증가하여 처리량이 크게 개선되었음을 확인할 수 있다. 추가적인 개선은 시스템의 메모리와 CPU 사용량을 모니터링하고 적절량 만큼의 HikariCP 의 max_pool_size를 늘린다면 추가적인 성능 개선이 가능 할 것 같 DB 레벨에서의 지연이 어플리케이션에 얼마나 큰 영향을 미치는지 실감할 수 있었다.

 

사용 Tools : SQL Procedure , Gatling

Tool 선정

개발 서버용 더미 데이터를 추가하기 위해 여러가지 툴을 검색해 보았다.

가장 간편하게 사용할 수 있는 방법을 위주로 보았는데 후보군에 Mockaroo, Procedure 가 있었다. Mockaroo 는 프로그래밍 코드 없이 쉬운 UI 로 간단하게 데이터를 생성할 수 있다는 장점이 있었는데 무료버전은 테스트 데이터를 한번에 1000rows 밖에 생성하지 못했고 비교적 더미데이터를 생성하는 쿼리 포맷자체도 간단했기에 Procedure를 작성하기로 했다.

데이터 생성 및 ERD

내가 생성하려는 데이터는 (게시글,게시글 좋아요, 게시글 스크랩) ,(댓글 및 대댓글, 댓글 좋아요), (변동일정), (고정일정,고정일정 세부사항), (일반일정,일반일정 세부사항),(유저, 유저의 auth 정보) 였다.

 

1. 유저 데이터 추가

Member 테이블 구조에 맞게 Procedure 를 작성했고 총 1000개의 더미 데이터를 삽입했다.

```sql
BEGIN
    DECLARE i INT DEFAULT 1;

    WHILE i <= 1000 DO
        INSERT INTO member (
            username, 
            nickname, 
            email, 
            password, 
            gender, 
            mode, 
            role, 
            image_file, 
            created_at, 
            created_by, 
            updated_at, 
            updated_by, 
            score, 
            phone_number, 
            auth_email, 
            auth_phone
        ) VALUES (
            CONCAT('홍길동', i), 
            CONCAT('닉네임', i), 
            CONCAT('normal', i, '@example.com'), 
            '$2a$12$vVyp1MKvgHaS68VKu/gyjeaFqHiXzKiu8Cq5A8jeoLZzHM900.0X2', 
            'MALE', -- 성별 예제 값 (M: 남성, F: 여성)
            'MILD', -- 모드 예제 값
            'NORMAL_USER', 
            NULL,
            NOW(), -- 생성 시간
            'ADMIN', -- 생성자
            NOW(), -- 업데이트 시간
            'ADMIN', -- 업데이트자
            FLOOR(RAND() * 100), -- 점수 (0~99 랜덤 값)
            CONCAT('011', LPAD(i, 8, '0')),
            false, 
            false 
        );
        SET i = i + 1;
END WHILE;
END

```

 

2. 인증 데이터 추가

 Auth 테이블 구조에 맞게 Procedure 를 작성했고 앞서 생성한 멤버 데이터와 1:1 매핑이 되는 1000개의 더미 데이터를 삽입했다.

```sql
BEGIN
    DECLARE i INT DEFAULT 1;

    WHILE i <= 1000 DO
        INSERT INTO AUTH (
            email, refresh_token, created_at, created_by, updated_at, updated_by
        ) VALUES (
            CONCAT('normal', i, '@example.com'), 
            UUID(),                           
            NOW(),                              
            'ADMIN',          
            NOW(),                             
            'ADMIN'
        );

        SET i = i + 1;
    END WHILE;
END
```

 

 

3. 게시글 ,게시글 좋아요, 게시글 스크랩 데이터 추가

 

 

게시글 약 1만개 , 게시글 좋아요 약 2.3만개 , 게시글 스크랩 약 2.3만개 생성

```sql
BEGIN
    DECLARE board_idx INT DEFAULT 1; -- 게시글 ID
    DECLARE member_count INT DEFAULT 1000; -- 멤버 수
    DECLARE max_boards INT DEFAULT 10000; -- 게시글 수
    DECLARE max_hearts INT DEFAULT 6; -- 게시글 좋아요 최대 수 (0~5개)
    DECLARE max_scraps INT DEFAULT 6; -- 게시글 스크랩 최대 수 (0~5개)

    -- 게시글 삽입
    WHILE board_idx <= max_boards DO
        -- 게시글 삽입
        INSERT INTO BOARD (
            member_id, title, contents, created_at, updated_at, total_like, total_scrap
        ) VALUES (
            FLOOR(RAND() * member_count) + 1, -- 랜덤 멤버
            CONCAT('Board Title ', board_idx),
            CONCAT('Content for board ', board_idx), 
            NOW(), 
            NOW(), 
            0, 
            0 
        );


        SET @last_board_id = LAST_INSERT_ID();

        -- 랜덤하게 게시글 좋아요 생성 (0~5개)
        SET @board_hearts = FLOOR(RAND() * max_hearts); -- 좋아요 수
        WHILE @board_hearts > 0 DO
            INSERT INTO BOARD_HEART (
                board_id, member_id
            ) VALUES (
                @last_board_id, -- 게시글 ID
                FLOOR(RAND() * member_count) + 1 -- 랜덤 멤버 ID
            );

            -- BOARD 테이블의 total_like 업데이트
UPDATE BOARD
SET total_like = total_like + 1
WHERE board_id = @last_board_id;

SET @board_hearts = @board_hearts - 1;
END WHILE;

        -- 랜덤하게 게시글 스크랩 생성 (0~5개)
        SET @board_scraps = FLOOR(RAND() * max_scraps); -- 스크랩 수
        WHILE @board_scraps > 0 DO
            INSERT INTO BOARD_SCRAP (
                board_id, member_id
            ) VALUES (
                @last_board_id, -- 게시글 ID
                FLOOR(RAND() * member_count) + 1 -- 랜덤 멤버 ID
            );

            -- BOARD 테이블의 total_scrap 업데이트
UPDATE BOARD
SET total_scrap = total_scrap + 1
WHERE board_id = @last_board_id;

SET @board_scraps = @board_scraps - 1;
END WHILE;

        SET board_idx = board_idx + 1;
END WHILE;
END
```

게시글
게시글 좋아요
게시글 스크랩

4. 댓글, 대댓글, 댓글 좋아요 데이터 추가

 

댓글(대댓글 포함) 약 24만개 , 댓글 좋아요 약 22만개 

 

```sql
BEGIN
    DECLARE board_idx INT DEFAULT 1; -- 게시글 ID
    DECLARE reply_idx INT DEFAULT 1; -- 댓글 카운터
    DECLARE member_count INT DEFAULT 1000; -- 멤버 수
    DECLARE max_replies INT DEFAULT 10; -- 게시글당 댓글 수

    WHILE board_idx <= 10000 DO
        SET reply_idx = 1;

        WHILE reply_idx <= max_replies DO
            -- 댓글 삽입
            INSERT INTO REPLY (
                member_id, board_id, parent_id, contents, total_like, created_at, updated_at
            ) VALUES (
                FLOOR(RAND() * member_count) + 1, 
                board_idx,                        
                NULL,                            
                CONCAT('Reply content for board ', board_idx, ' reply ', reply_idx),
                0,                               
                NOW(),                           
                NOW()                             
            );

            -- 방금 삽입된 댓글의 ID 가져오기
            SET @last_reply_id = LAST_INSERT_ID();

            -- 랜덤하게 대댓글 생성 (0~3개)
            SET @sub_replies = FLOOR(RAND() * 4); -- 대댓글 수
            WHILE @sub_replies > 0 DO
                INSERT INTO REPLY (
                    member_id, board_id, parent_id, contents, total_like, created_at, updated_at
                ) VALUES (
                    FLOOR(RAND() * member_count) + 1, 
                    board_idx,                        
                    @last_reply_id,                   -- 대댓글의 parent_id
                    CONCAT('Sub-reply for reply ', @last_reply_id),
                    0,                                
                    NOW(),                            
                    NOW()                             
                );
                SET @sub_replies = @sub_replies - 1;
END WHILE;

            -- 랜덤하게 댓글 좋아요 생성 (0~5개)
            SET @hearts = FLOOR(RAND() * 6); -- 좋아요 수
            WHILE @hearts > 0 DO
                INSERT INTO REPLY_HEART (
                    reply_id, member_id
                ) VALUES (
                    @last_reply_id, -- 좋아요가 눌린 댓글 ID
                    FLOOR(RAND() * member_count) + 1 -- 랜덤 멤버
                );

                -- REPLY 테이블의 total_like 업데이트
UPDATE REPLY
SET total_like = total_like + 1
WHERE reply_id = @last_reply_id;

SET @hearts = @hearts - 1;
END WHILE;

            SET reply_idx = reply_idx + 1;
END WHILE;

        SET board_idx = board_idx + 1;
END WHILE;
END
```

 

댓글
댓글 좋아요

5. 고정일정 추가

 

 

일정 약 1000개,고정일정 약 1000개 , 고정일정 세부사항 약 15000개 

```sql
BEGIN
    DECLARE member_idx INT DEFAULT 1; -- 멤버 ID
    DECLARE max_members INT DEFAULT 1000; -- 멤버 수
    DECLARE max_schedules INT DEFAULT 3; -- 각 멤버당 일정 수
    DECLARE schedule_idx INT DEFAULT 1; -- 일정 인덱스
    DECLARE schedule_id BIGINT DEFAULT NULL; -- 일정 ID
    DECLARE f_schedule_id BIGINT DEFAULT NULL; -- F_SCHEDULE ID
    DECLARE schedule_start_date DATETIME DEFAULT NULL; -- 일정 시작일
    DECLARE schedule_end_date DATETIME DEFAULT NULL; -- 일정 종료일
    DECLARE current_loop_date DATETIME DEFAULT NULL; -- 반복 중인 날짜
    DECLARE loop_date DATETIME DEFAULT NULL; -- 반복할 날짜

    WHILE member_idx <= max_members DO
        SET schedule_idx = 1;

        WHILE schedule_idx <= max_schedules DO
            -- 변수 초기화
            SET schedule_id = NULL;
            SET schedule_start_date = NULL;
            SET schedule_end_date = NULL;
            SET current_loop_date = NULL;
            SET loop_date = NULL;

            -- SCHEDULE 테이블에 일정 삽입
            INSERT INTO SCHEDULE (
                member_id, title, common_description, start_date, end_date, created_at, created_by, updated_at, updated_by, dtype
            ) VALUES (
                member_idx,
                CONCAT('Schedule ', member_idx, ' - ', schedule_idx),
                CONCAT('Description for schedule ', schedule_idx, ' of member ', member_idx),
                NOW() + INTERVAL (schedule_idx * 10) DAY,
                NOW() + INTERVAL (schedule_idx * 12) DAY,
                NOW(),
                CONCAT('normal', member_idx, '@example.com'),
                NOW(),
                CONCAT('normal', member_idx, '@example.com'),
                'fixed'
            );

            SET schedule_id = LAST_INSERT_ID();

            -- 데이터 검증
            IF schedule_id IS NULL THEN
                SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Failed to retrieve LAST_INSERT_ID.';
            END IF;

            SELECT start_date, end_date 
            INTO schedule_start_date, schedule_end_date
            FROM SCHEDULE
            WHERE SCHEDULE.schedule_id = schedule_id;

            IF schedule_start_date IS NULL OR schedule_end_date IS NULL THEN
                SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Failed to retrieve start_date or end_date.';
            END IF;

            -- F_SCHEDULE 테이블에 삽입
            INSERT INTO F_SCHEDULE (schedule_id) VALUES (schedule_id);
            SET f_schedule_id = LAST_INSERT_ID();

            -- 반복 일정 생성
            SET current_loop_date = schedule_start_date;

            WHILE current_loop_date <= schedule_end_date DO
                SET loop_date = CONCAT(DATE(current_loop_date), ' 13:00:00');

                INSERT INTO F_SCHEDULE_DETAIL (
                    schedule_id, detail_description, start_date, end_date, created_by, created_at, updated_by, updated_at
                ) VALUES (
                    f_schedule_id,
                    CONCAT('Detail description for schedule ', schedule_idx, ' of member ', member_idx),
                    loop_date,
                    DATE_ADD(loop_date, INTERVAL 1 HOUR),
                    CONCAT('normal', member_idx, '@example.com'),
                    NOW(),
                    CONCAT('normal', member_idx, '@example.com'),
                    NOW()
                );

                SET current_loop_date = DATE_ADD(current_loop_date, INTERVAL 1 DAY);
            END WHILE;

            SET schedule_idx = schedule_idx + 1;
        END WHILE;

        SET member_idx = member_idx + 1;
    END WHILE;
END
```

일정
고정일정 (엔티티 상속관계)
고정일정 세부사항

 

6.  변동일정 추가

 

변동일정 약 9만개 

 

```sql
BEGIN
    DECLARE member_idx INT DEFAULT 1; -- 멤버 ID
    DECLARE max_members INT DEFAULT 1000; -- 멤버 수
    DECLARE max_schedules INT DEFAULT 100; -- 각 멤버당 일정 수
    DECLARE schedule_idx INT DEFAULT 1; -- 일정 인덱스
    DECLARE random_start_date DATETIME; -- 랜덤 시작일
    DECLARE random_end_date DATETIME; -- 랜덤 종료일

    -- 각 멤버에 대해 일정 생성
    WHILE member_idx <= max_members DO
        SET schedule_idx = 1;

        WHILE schedule_idx <= max_schedules DO
            -- 시작일 생성 (무작위로)
            SET random_start_date = 
                '2023-01-01 00:00:00' + INTERVAL FLOOR(RAND() * 730) DAY + INTERVAL FLOOR(RAND() * 24) HOUR;

            SET random_end_date = random_start_date + INTERVAL 2 HOUR;

            INSERT INTO SCHEDULE (
                member_id, title, common_description, start_date, end_date, created_at, created_by, updated_at, updated_by, dtype
            ) VALUES (
                member_idx, -- 멤버 ID
                CONCAT('Schedule ', member_idx, ' - ', schedule_idx), 
                CONCAT('This is a Variable schedule for member ', member_idx), 
                random_start_date, -- 랜덤 시작일
                random_end_date, -- 종료일 (시작일 + 2시간)
                NOW(), -- 생성 시간
                CONCAT('normal', member_idx, '@example.com'), 
                NOW(), -- 수정 시간
                CONCAT('normal', member_idx, '@example.com'),
                'fixed' -- dtype
            );

            -- V_SCHEDULE 테이블에 삽입
            INSERT INTO V_SCHEDULE (schedule_id)
            VALUES (LAST_INSERT_ID());

            SET schedule_idx = schedule_idx + 1;
        END WHILE;

        SET member_idx = member_idx + 1;
    END WHILE;
END $$

```

고정일정

 

일정

 

 

7.일반일정 추가

 

일반일정은 Gatling 으로 데이터를 삽입해주었다. 일반일정은 다른 일정들과 다르게  유저가 아래 폼 데이터를 요청하면 , 폼 데이터를 분석해 자동으로 일정을 생성해주기 때문에 일반일정 세부사항을 Procedure와 같이 특정패턴으로 데이터를 생성해서 주입할 수 없었기 때문이다.

일정 생성 폼

 

ChatGpt 의 도움을 받아 시뮬레이션 코드를 작성했다. 인증된 회원만 일정생성 리소스에 접근이 가능했기에 먼저 로그인 후에 각 유저별로 Authorization 헤더에 access token 을 먼저 주입시켜준 뒤 테스트 데이터를 삽입했다. 원래 Gatling은 앱 성능 테스트 도구인데 각 유저마다 인증 토큰이 주입된 요청을 날리기에 괜찮아 보여서 해당 툴을 사용했다. 

 

일반일정 약 1.8만개 

```scala
class LoginSimulation extends Simulation {

  // HTTP Protocol 설정
  val httpProtocol = http
    .baseUrl("http://localhost:8080") // API의 기본 URL
    .contentTypeHeader("application/x-www-form-urlencoded") // Content-Type 설정

  // 1000명의 순차적인 이메일 생성
  val userFeeder = (1 to 1000).map(i => Map(
    "email" -> s"normal$i@example.com", 
    "password" -> "password1"          
  )).iterator 
  
  val debugFeeder = exec { session =>
    val email = session("email").asOption[String]
    println(s"Feeder supplied email: ${email.getOrElse("No email in session")}")
    session
  }

  // 1. 로그인 요청
  val login = exec(debugFeeder) 
    .exec { session =>
      val email = session("email").as[String]
      val password = session("password").as[String]
      println(s"Attempting login with email: $email and password: $password")
      session
    }
    .exec(
      http("Login Request")
        .post("/login") // 로그인 API 엔드포인트
        .formParam("email", session => session("email").as[String]) 
        .formParam("password", session => session("password").as[String]) 
        .check(
          status.is(200),  
          header("Authorization").saveAs("authToken") // Authorization 헤더 추출
        )
    ).exitHereIfFailed 

  // 2. 보호된 리소스 요청
  val authenticatedRequest = exec { session =>
    val authToken = session("authToken").asOption[String]
    println(s"Using Authorization token: ${authToken.getOrElse("No token in session")}")
    val randomTitle = scala.util.Random.alphanumeric.take(10).mkString
    val randomDescription = scala.util.Random.alphanumeric.take(20).mkString
    val randomTotalAmount = scala.util.Random.between(100, 301) // 100~300 사이의 랜덤 값
    session
      .set("randomTitle", randomTitle)
      .set("randomDescription", randomDescription)
      .set("randomTotalAmount", randomTotalAmount)
  }.exec(
    http("Protected Resource Request")
      .post("/schedule/normal") 
      .header("Authorization", session => session("authToken").as[String])
      .header("Content-type", "application/json")
      .body(StringBody(session => {
        val title = session("randomTitle").as[String]
        val description = session("randomDescription").as[String]
        val totalAmount = session("randomTotalAmount").as[Int]

        s"""
        {
          "title": "$title",
          "commonDescription": "$description",
          "startDate": "2024-10-30",
          "endDate": "2024-11-30",
          "bufferTime": "01:00:00",
          "performInDay": "04:00:00",
          "isIncludeSaturday": true,
          "isIncludeSunday": true,
          "totalAmount": $totalAmount,
          "performInWeek": 4
        }
        """
      }))
      .asJson 
      .check(status.is(201)) 
  )

  val scn = scenario("Login and Access Protected Resource")
    .feed(userFeeder)    
    .exec(login)         
    .pause(1)            
    .exec(authenticatedRequest) 

  setUp(
    scn.inject(
      atOnceUsers(1000)
    ).protocols(httpProtocol)
  )
}
```

일정
일반일정
일반일정 세부사항

 

 

총 생성 데이터 

약 70만개 

+ Recent posts