nGrinder 부하 테스트

두주·2024년 2월 22일
3

TIL

목록 보기
57/58

동기

현재 쿠폰을 등록하는 API의 TPS가 12로 매우 낮은 수치이다.

  • TPS는 Transaction Per Second로, 초당 처리할 수 있는 트랜잭션을 의미한다.

평균 테스트 시간도 요청부터 응답까지 약 58초가 걸리는 상황이라
정상적인 서비스가 가능하다 라고 보기 어려운 상태이다.

    @Transactional
    override fun getCommonCoupon(request : GetCouponRequest) : GetCouponResponse {
        val userCheck = memberRepository.findByIdOrNull(/*userPrincipal.id*/ 2L)
            ?: throw IllegalArgumentException("Invalid Member")

        val couponCheck = commonCouponRepository.findByCouponNumber(request.couponNumber)?.apply {
            if (available && couponCount > useCount) {
                memberId = userCheck
                useCount += 1

            } else {
                throw IllegalArgumentException("사용할 수 없는 쿠폰입니다.")
            }
        } ?: throw IllegalArgumentException("쿠폰 번호가 틀렸습니다.")

        commonCouponRepository.save(couponCheck)

        return GetCouponResponse(couponNumber = couponCheck.couponNumber)

해결 과정

DAY 1

처음에는 DB에 접근하는 로직이 너무 많나? 하는 생각이 들었다.
(실제로 SELECT 쿼리가 4번이나 찍혔다)

쿼리를 줄여보자.

1. UseCount ++ 에서 Repository에서 직접 처리

생각은 단순했다. UseCount를 가져와서 UseCount = UseCount + 1 을 한 다음
다시 저장하는 것 보다 Repository에서 직접 연산을 하면 적어도 한 단계는 줄어드니까
조금 더 빠를 것 같았다.

    @Modifying
    @Query("update CommonCouponEntity c set c.useCount = c.useCount + 1 where c.couponNumber = :couponNumber")
    fun incUseCount(@Param("couponNumber") couponNumber : String) : Int

결과는 여전히 TPS 12


DAY 2

쿼리는 반드시 4개가 발생하는 것은 어쩔 수 없다는 것을 깨달아 버렸다.

User 정보의 일치 여부를 확인하고, Coupon의 유효성 검증을 위해서는 반드시 진행되어야 했다.
그렇다면, FetchJoin을 이용해서 1개의 쿼리로 합치는 것은 어떨까

1. FetchJoin 쓰기

    override fun findByCouponNumberWithMember(couponNumber: String, memberId: Long): CommonCouponEntity {
        return queryFactory
            .selectFrom(coupon)
            .join(coupon.memberId, member)
            .fetchJoin()
            .where(coupon.couponNumber.eq(couponNumber))
            .fetchOne() ?: throw IllegalArgumentException("쿠폰 번호가 잘못되었습니다.")
    }

QueryDSL을 사용해서 한 번의 쿼리로 필요한 모든 정보를 불러올 수 있도록 변경했다.

쿼리는 4번에서 1번으로 줄었지만, 여전히 TPS는 12로 조금도 줄지 않았다.

대체 왜 ?

도대체 왜 TPS가 12에서 변동내역이 없을까.
내 코드에서 더 줄일 수 있는 방법을 찾지 못해 환경을 체크했다.

1. Supabase

내가 하고있는 프로젝트는 PostgreSQL을 쓰는 Supabase로 선택했었다.

모든 팀원들이 가장 익숙하게 잘 다룰 수 있고,
무료로 제공하면서도 성능이 나쁘지 않다고 생각해 왔었다.

그러던 와중에 사실 가장 먼저 의심했어야 할 것이 supabase가 아닌가?
무료 요금제를 쓰면서 많은 처리를 기대한 것이 문제 아닐까? 싶었다.

show max_connections;

확인해 본 결과, max_connections는 60이다.

이 중 내가 보통 3~5개정도 connection을 잡아먹고 있고,
팀원이 3명에 노트북이 2대니 아무리 적게 잡아도 3~40connection은
기본적으로 할당이 되어있는 것 같다.

실제로 작업 중 reached max connections라는 에러를 종종 마주했었는데
신경을 안썼더니 더 크게 돌아와버린게 아닐까.

해결법

안그래도 생각했던 처리 방식을 구현하기 위해 Redis를 도입하려고 했었다.

local에서 redis를 통해 구현하고, 테스트하면 더 좋은 결과를 얻을 수 있지 않을까?

Redis 사용

Redis를 설치하고, 도입해보면서 느낀 글은 다음 주제로 작성할 예정이다.

  • Redis 를 GUI로 사용할 수 있는 툴이다
  • RedisInsight
  • Hash를 사용하여 Key : Value { Key1 : Value1, Key2 : Value2 } 를 사용했다.

테스트 결과

헉..

물론 무료DB에서 local Redis로 바뀐 만큼 차이가 엄청 클 것이라고 예상은 했지만
내 예상을 뛰어넘는 수치가 나왔다.

TPS 12 -> 2618

평균 테스트 시간도 비교가 안 될 만큼 줄었고, 그로 인해 실행 테스트도
훨씬 늘어났음에도 에러가 없이 테스트가 무사히 종료되었다.

사실 local DB에서 설치하고 테스트 한 것이 아닌 만큼,
이 테스트가 얼마나 부족한 테스트인지는 잘 알고있다.

주어진 방법에서 최선을 다해 구현해 보았고
힘들게 구현한 소득이 눈 앞에 테스트 결과로 나타나니
뿌듯함을 감출 수가 없었다.

재미있다.

profile
야옹.

3개의 댓글

comment-user-thumbnail
2024년 2월 27일

너무 좋은 글이네요~ 잘 봤습니다^~^

1개의 답글
comment-user-thumbnail
2024년 2월 27일

잘 보고갑니다!

답글 달기