Jmeter

ChoRong0824·2025년 3월 30일
0

Web

목록 보기
51/51
post-thumbnail

테스트 코드작성 끝나서, 이제 Jmeter로 부하테스트를 해보면서 작업 진행하려고 합니다.
이에 앞서 Jmeter를 설치해주면 됩니다.
brew install jmeter -> 이제 jmeter가 있는 파일을 찾아 실행시키녀 됨 -> 그런데 저는 바로 실행 jmeter만 치면 바로 실행 가능합니다.

이런 식(위 사진 참고)으로 진행하시면 됩니다 ㅎㅎ

Jmeter 메인 화면입니다.
Test Plan -> Add -> Thread (Users) -> Thread Group 순서로 그룹을 만듭니다. (포스트맨 기준으로 컬랙션에 해당합니다.)

이렇게 그룹을 만들어준 다음엔, Http 요청할 샘플러들을 만들어줘야합니다.

이렇게 만들어진 basic을 보게되면,

프로토콜, 서버이름 or ip, 포트넘버 ,path, 엔코딩까지 있습니다.

이렇게 그 쓰레드 그룹의 세팅을 보면 쓰레드 프로펄티스에서 세팅할 수 있습니다.

Thread Group 설정

  • Number of Threads (users): 100 (예: 동시에 100명)
  • Ramp-Up Period: 1 (1초 안에 100명 진입)
  • Loop Count: 1 (한 번만 실행해도 충분)

http request 설정

HTTP Request -> Add -> Listener 들어가서

  1. View Result Tree
  2. Aggregate Graph
  3. View Results in Table

을 추가해주면 된다. 만일 추가적으로 Listener가 필요하면 원하는데로 추가하면 된다.

여기서 만약, 인증 권한을 줘야 한다면,
HTTP Request -> Add -> Config Element -> HTTP Header Manager를 생성하면 된다.

만일 권한을 넣어야 하거나 그외 추가적으로 헤더에 넣을게 필요하면 HTTP Header Manager에서 값을 주입하면 된다.

출처 및 참고
1,

일단 레디스 설치가 안되어있어서 에러 발생.

redis 설치하고, brew services start redis 명령어를 통해, redis를 백그라운드에서 자동 실행되게 설정.
-> 이 명령어를 통해, mac이 껏다 켜져도 redis가 자동으로 실행됨.
이후, redis-cli ping 를 통해 redis가 켜져있는지 확인.
-> PONG가 나오면 정상 실행중임.

ㅇㅋ.
일단 테스트에 앞서 팀원의 일라스틱 서치를 설치해야합니다.
머지중에 에러가 발생했기 때문입니다.

설치

brew tap elastic/tap
brew install elastic/tap/elasticsearch-full

로 설치 (elasticsearch-full은 플러그인 포함된 버전이며, 일반 버전도 설치 가능하지만, 보통 full 버전 쓰는 게 좋음)

백그라운드 서비스로 실행 (권장)

brew services start elastic/tap/elasticsearch-full

또는 일회성으로 실행(터미널에서 직접 실행)_
elasticsearch : 이 경우 터미널이 꺼지면 일라스틱 서치도 꺼짐.

잘 켜졌는지 확인
curl http://localhost:9200

jdk 를 똑바로 인식하지 못하는 문제가 발생.

일단, jdk 경로 찾기

/usr/libexec/java_home
이후엔. 아래와 같이 실행.

ES_JAVA_HOME=/Users/mun/Library/Java/JavaVirtualMachines/openjdk-23.0.2/Contents/Home /opt/homebrew/opt/elasticsearch-full/bin/elasticsearch

일라스틱서치가 로딩 도중 머신 러닝 관련 모듈에서 오류가 나서 종료됨.
ML기능 때문에 중간에 크래시 났음

ES_JAVA_HOME=/Users/mun/Library/Java/JavaVirtualMachines/openjdk-23.0.2/Contents/Home /opt/homebrew/opt/elasticsearch-full/bin/elasticsearch
Mar 31, 2025 12:01:15 AM sun.util.locale.provider.LocaleProviderAdapter <clinit>
INFO: Invalid locale provider adapter "COMPAT" ignored.
Mar 31, 2025 12:01:15 AM sun.util.locale.provider.LocaleProviderAdapter <clinit>
WARNING: COMPAT locale provider has been removed
[2025-03-31T00:01:15,935][INFO ][o.e.n.Node               ] [SeongJunui-MacBookPro.local] version[7.17.4], pid[10630], build[default/tar/79878662c54c886ae89206c685d9f1051a9d6411/2022-05-18T18:04:20.964345128Z], OS[Mac OS X/15.3.1/aarch64], JVM[Oracle Corporation/OpenJDK 64-Bit Server VM/23.0.2/23.0.2+7-58]
[2025-03-31T00:01:15,937][INFO ][o.e.n.Node               ] [SeongJunui-MacBookPro.local] JVM home [/Users/mun/Library/Java/JavaVirtualMachines/openjdk-23.0.2/Contents/Home], using bundled JDK [false]
[2025-03-31T00:01:15,937][INFO ][o.e.n.Node               ] [SeongJunui-MacBookPro.local] JVM arguments [-Xshare:auto, -Des.networkaddress.cache.ttl=60, -Des.networkaddress.cache.negative.ttl=10, -XX:+AlwaysPreTouch, -Xss1m, -Djava.awt.headless=true, -Dfile.encoding=UTF-8, -Djna.nosys=true, -XX:-OmitStackTraceInFastThrow, -XX:+ShowCodeDetailsInExceptionMessages, -Dio.netty.noUnsafe=true, -Dio.netty.noKeySetOptimization=true, -Dio.netty.recycler.maxCapacityPerThread=0, -Dio.netty.allocator.numDirectArenas=0, -Dlog4j.shutdownHookEnabled=false, -Dlog4j2.disable.jmx=true, -Dlog4j2.formatMsgNoLookups=true, -Djava.locale.providers=SPI,COMPAT, --add-opens=java.base/java.io=ALL-UNNAMED, -Djava.security.manager=allow, -XX:+UseG1GC, -Djava.io.tmpdir=/var/folders/dd/9vsv1wtx2fx2m5bgmdglp4n40000gn/T/elasticsearch-6617545340270957761, -XX:+HeapDumpOnOutOfMemoryError, -XX:+ExitOnOutOfMemoryError, -XX:HeapDumpPath=data, -XX:ErrorFile=logs/hs_err_pid%p.log, -Xlog:gc*,gc+age=trace,safepoint:file=/opt/homebrew/var/log/elasticsearch/gc.log:utctime,pid,tags:filecount=32,filesize=64m, -Xms8192m, -Xmx8192m, -XX:MaxDirectMemorySize=4294967296, -XX:InitiatingHeapOccupancyPercent=30, -XX:G1ReservePercent=25, -Des.path.home=/opt/homebrew/Cellar/elasticsearch-full/7.17.4/libexec, -Des.path.conf=/opt/homebrew/etc/elasticsearch, -Des.distribution.flavor=default, -Des.distribution.type=tar, -Des.bundled_jdk=true]
[2025-03-31T00:01:17,365][INFO ][o.e.p.PluginsService     ] [SeongJunui-MacBookPro.local] loaded module [aggs-matrix-stats]
org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:166) ~[elasticsearch-7.17.4.jar:7.17.4]
	... 6 more
uncaught exception in thread [main]

Exception: java.security.AccessControlException thrown from the UncaughtExceptionHandler in thread "main"

원인 요약

Failure running machine learning native code.
...
To bypass this problem by running Elasticsearch without machine learning functionality
set [xpack.ml.enabled: false].

Elasticsearch가 x-pack의 Machine Learning 기능을 사용하려고 하는데,
현재 Mac 환경에서 필요한 native 코드 실행이 안 돼서 오류가 난 거.

vi /opt/homebrew/etc/elasticsearch/elasticsearch.yml

xpack.ml.enabled: false
파일 마지막줄에 위 옵션 추가

다시 실행
ES_JAVA_HOME=/Users/mun/Library/Java/JavaVirtualMachines/openjdk-23.0.2/Contents/Home /opt/homebrew/opt/elasticsearch-full/bin/elasticsearch

아직 해결 못함.
일단,

키바나 설치

brew install elastic/tap/kibana-full

brew services start elastic/tap/kibana-full 자동실행

노리 설치 문제였음.

docker ps
개발 환경에서 (docker compose up)
docker compose down

docker compose up
docker compose down
docker compose up -d

docker exec -it es sh

bin/elasticsearch-plugin install analysis-nori

➜ count10shop git:(feat/coupon) docker compose stop
➜ count10shop git:(feat/coupon) docker compose start

노리가 다운이 안되어있어서, 노리를 다운 받은 후 진행해야 되는 것이었음.


제이미터로 실행. 에러. 뭐가 문제일까?

2025-03-31T03:47:46.108+09:00  WARN 3057 --- [count10shop] [nio-8080-exec-8] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.HttpMediaTypeNotSupportedException: Invalid mime type "application-json": does not contain '/']

👉 "application-json" → 잘못된 MIME 타입
👉 "application/json" → 올바른 MIME 타입 (슬래시 / 있어야 함)

http 헤더 매니저에 ㅋㅋㅋㅋ -로 해준 것을 /로 수정해주면 됩니다.
또한, 로그인 쪽에 jwt가 헤더에 포함되어 반환되는 것을 확인했기에 추가해줍니다.

헤더쪽에 토큰 반환되는 것을 확인.


수정해주고 진행할 수 있으니ㅏ,
일단 저는 시큐리티 컨피그쪽에 빠른 진행을 위해 패스를 해둔 상태입니다.
따라서 이번엔 토큰은 없이 진행.

..???????? 내가 의도한 것은 1개 성공하면 나머진 실패해야하는 것인데 전부 다 실패한 것으로 보인다고..? 이건 무엇...? 코드가 잘못된 것 같은데 코드 다시 봐야할 것 같습니다...ㅠ

어노테이션 잘 붙어있는데..? 😱
사실 어노테이션이 안 붙어잇길 기도했다. 왜냐하면 PESSIMISTIC_write 락이 무의미해져서 전부 실패한 것일 수도 있기 때문이다.
existsByUserIdAndCouponId() 이 락 없이 먼저 호출된다면, 여러 스레드가 동시에 중복 발급 여부를 체크하고 모두 false를 받고 통과함.
이후, findByIdForUpdate()로 락을 걸고, addIssuedQuantity()에서 경쟁 -> 그 중 하나만 성공. 하지만 이미 db에는 발급된 쿠폰이 존재하기 때문에, 그 후부터는 IllegalStateException 발생.
즉, 적어도 한 명은 성공해야 하는데 전부 다 실패중.

흠,,
일단 쓰레드를 100개로 테스트 진행해서 문제일 수도 있으니까,
3개만 스레드를 주고 테스트 진행.

2025-03-31T04:33:21.080+09:00 ERROR 4633 --- [count10shop] [nio-8080-exec-4] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception

java.lang.NullPointerException: Cannot invoke "xyz.tomorrowlearncamp.count10shop.domain.common.util.JwtUtil.substringToken(String)" because "this.jwtUtil" is null
	at xyz.tomorrowlearncamp.count10shop.config.JwtAuthenticationFilter.doFilterInternal(JwtAuthenticationFilter.java:43) ~[main/:na]
	at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116) ~[spring-web-6.2.5.jar:6.2.5]
	at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.wrapFilter(ObservationFilterChainDecorator.java:240) ~[spring-security-web-6.4.4.jar:6.4.4]
	at org.springframework.security.web.ObservationFilterChainDecorator$ObservationFilter.doFilter(ObservationFilterChainDecorator.java:227) ~[spring-security-web-6.4.4.jar:6.4.4]
	at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:63) ~[tomcat-embed-core-10.1.39.jar:10.1.39]
	at java.base/java.lang.Thread.run(Thread.java:840) ~[na:na]

!!? 락과는 문제가 없어보이는 에러가 발생한 것 같습니다.
해당 문제의 핵심은 지금 JwtAuthenticationFilter에서 jwtUtil이 null이라는 점입니다. 이게 핵심인 것 같습니다.
왜 null이 되었는지 정확한 원인을 확인해보도록 하겠습니다.

즉, JwtAuthenticationfilter클래스의 jwtUtil 필드가 Spring 컨테이너에서 주입되지 않아서 null인 것 같습니다.
-> 이렇게 생각한 이유

java.lang.NullPointerException: 
Cannot invoke "xyz.tomorrowlearncamp.count10shop.domain.common.util.JwtUtil.substringToken(String)" 
because "this.jwtUtil" is null

해결 방법으로는
JwtUtil을 생성자 주입 하거나 @Autowired로 주입해줘야합니다.
왜냐하면,

package xyz.tomorrowlearncamp.count10shop.config;

import java.io.IOException;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import xyz.tomorrowlearncamp.count10shop.domain.common.dto.AuthUser;
import xyz.tomorrowlearncamp.count10shop.domain.common.etc.JwtProperties;
import xyz.tomorrowlearncamp.count10shop.domain.common.util.JwtUtil;
import xyz.tomorrowlearncamp.count10shop.domain.user.enums.UserRole;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

	@Autowired
	private  JwtUtil jwtUtil;

	@Override
	protected void doFilterInternal(
		HttpServletRequest httpRequest,
		@NonNull HttpServletResponse httpResponse,
		@NonNull FilterChain chain
	) throws ServletException, IOException {
		String authorizationHeader = httpRequest.getHeader("Authorization");

		if (authorizationHeader != null && authorizationHeader.startsWith(JwtProperties.TOKEN_PREFIX)) {
			String jwt = jwtUtil.substringToken(authorizationHeader);
			try {
				Claims claims = jwtUtil.extractClaims(jwt);

				if (SecurityContextHolder.getContext().getAuthentication() == null) {
					setAuthentication(claims);
				}
			} catch (SecurityException | MalformedJwtException e) {
				log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.", e);
				httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않는 JWT 서명입니다.");
			} catch (ExpiredJwtException e) {
				log.error("Expired JWT token, 만료된 JWT token 입니다.", e);
				httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, "만료된 JWT 토큰입니다.");
			} catch (UnsupportedJwtException e) {
				log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.", e);
				httpResponse.sendError(HttpServletResponse.SC_BAD_REQUEST, "지원되지 않는 JWT 토큰입니다.");
			} catch (Exception e) {
				log.error("Internal server error", e);
				httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
			}
		}
		chain.doFilter(httpRequest, httpResponse);
	}

	private void setAuthentication(Claims claims) {
		Long userId = Long.valueOf(claims.getSubject());
		String email = claims.get("email", String.class);
		UserRole userRole = UserRole.of(claims.get("userRole", String.class));

		AuthUser authUser = new AuthUser(userId, email, userRole);
		JwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(authUser);
		SecurityContextHolder.getContext().setAuthentication(authenticationToken);
	}

}

해당 코드는 @Component를 붙여서 JwtAuthenticationFilter를 빈으로 등록했고, SecurityConfig에서 생성자로 private final JwtAuthenticationFilter 주입을 받고 있습니다.
그런데, 문제는 @Component로 만든 JwtAuthenticationFilter는 JwtUtil이 자동 주입되지 않는다는 것입니다.

@RequiredArgsConstructor
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private JwtUtil jwtUtil; // @Autowired 없으면 null!
}

즉, 이건 @Autowired도 없고, final도 없고, 생성자 주입도 안 했으니… 결국 null이 들어감 -> NPE.

이제 코드 해결하겠습니다.

JwtAuthenticationFilter를 직접 Bean으로 등록하면서 생성자 주입 방식 사용

  1. wtAuthenticationFilter에서 @Component 제거, final 필드 + 생성자 생성
  2. SecurityConfig에서 직접 Bean으로 생성해서 등록.
구분 수정 내용
JwtAuthenticationFilter @Component, @RequiredArgsConstructor 제거 -> 생성자 수동 생성
JwtUtil 주입 생성자 주입 방식으로 명시적으로 주입하게끔 바꿈
SecurityConfig jwtAuthenticationFilter() 메서드로 Bean 등록하고 addFilterBefore()에서 사용

다시말해,

  • 자동 빈 등록 (@Component) + 수동 주입(new JwtAuthenticationFilter) 혼용 금지
  • 수동으로 JwtAuthenticationFilter(jwtUtil) 생성해서 등록하는 게 가장 안전하기떄문에 수정.

Spring에서는 어떤 객체를 빈으로 등록할지 명확하게 제어하지 않으면,
빈 주입 누락 or 중복 생성으로 NullPointerException(NPE), NoSuchBeanDefinitionException이 빈번하게 발생함.
그래서 필터같이 SecurityConfig에서 수동으로 주입해야 하는 경우는
절대 @Component 붙이지 말고, 명시적으로 Bean으로 만들어줘야 합니다.

이렇게 수정했음에도 해결되지 않았습니다..

2025-03-31T06:30:38.540+09:00 ERROR 7725 --- [count10shop] [nio-8080-exec-6] x.t.c.config.JwtAuthenticationFilter     : Internal server error

java.lang.NumberFormatException: For input string: "count10shop"
	at java.base/java.lang.NumberFormatException.forInputString(NumberFormatException.java:67) ~[na:na]
	at java.base/java.lang.Long.parseLong(Long.java:711) ~[na:na]
	at java.base/java.lang.Long.valueOf(Long.java:1163) ~[na:na]
	at xyz.tomorrowlearncamp.count10shop.config.JwtAuthenticationFilter.setAuthentication(JwtAuthenticationFilter.java:71) ~[main/:na]
	at xyz.tomorrowlearncamp.count10shop.config.JwtAuthenticationFilter.doFilterInternal(JwtAuthenticationFilter.java:51) ~[main/:na]
	at 
	at java.base/java.lang.Thread.run(Thread.java:840) ~[na:na]

Hibernate: 
    /* <criteria> */ select
        ic1_0.id 
    from
        issued_coupon ic1_0 
    join
        coupon c1_0 
            on c1_0.id=ic1_0.coupon_id 
    where
        ic1_0.user_id=? 
        and c1_0.id=? 
    fetch
        first ? rows only
2025-03-31T06:30:38.551+09:00 ERROR 7725 --- [count10shop] 
Hibernate: 
    /* <criteria> */ select
        ic1_0.id 
    from
        issued_coupon ic1_0 
    join
        coupon c1_0 
            on c1_0.id=ic1_0.coupon_id 
    where
        ic1_0.user_id=? 
        and c1_0.id=? 
    fetch
        first ? rows only
2025-03-31T06:30:38.842+09:00 ERROR 7725 --- [count10shop] [nio-8080-exec-8] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: java.lang.IllegalStateException: 이미 발급된 쿠폰입니다] with root cause

java.lang.IllegalStateException: 이미 발급된 쿠폰입니다
	at org.springframework.security.web.ObservationFilterChainDecorionFilterChain.internalDoFilter(ApplicationFilterChain.java:
	at java.base/java.lang.Thread.run(Thread.java:840) ~[na:na]

(쓰레드 3개로 테스트 진행했습니다)
일단 로그를 통해 해결하겠습니다.

// 원인 
java.lang.NumberFormatException: For input string: "count10shop"

count10shop이라는 문자열을 Long.parseLong() 또는 Long.valueOf()를 통해 Long 타입으로 파싱하려다 실패한 것입니다.

어디서 ?

at xyz.tomorrowlearncamp.count10shop.config.JwtAuthenticationFilter.setAuthentication(JwtAuthenticationFilter.java:71)

근데 잘 처리되어있는 것 같은데, 혹시 다른 쪽 에러는 아닐까? 라는 생각을 했습니다.
에러났던 "JWT was signed with a different key"는 JWT가 서명된 키와 현재 서명을 검증하려는 키가 다를 때 발생하는 문제인데, 이 부분은 아닐까? 라는 생각을 하게 되었습니다.
JwtUtil 코드를 확인해보겠습니다.

(이미지 1)

JwtUtil을 살펴보면 JWT 생성 시에는 com.auth0.jwt.JWT 라이브러리(= Auth0)를 사용하고 있고,
JWT 검증 시에는 io.jsonwebtoken.Jwts (= JJWT) 라이브러리를 사용하고 있습니다.

이 두 라이브러리는 내부적으로 서명 방식이 서로 호환되지 않기 때문에,
JwtAuthenticationFilter에서 토큰 검증 시 "JWT was signed with a different key" 에러가 발생하는 중입니다.

따라서, JJWT로만 통일했습니다(이미지 1).

호호홍 테스트 잘됩니다~~


회고록

개요

JMeter를 이용해서 쿠폰 발급 api에 대해 동시성 부하 테스트를 진행하던 도중, 한 유저에게 중복으로 쿠폰이 발급되는 현상이 발생했습니다.

postman으로 단 건 테스트에선 정상적으로 동작했으며, “이미 발급된 쿠폰입니다”등 예외도 잘 던져졌기에 초기에는 코드에 문제가 없다고 판단했습니다.

그러나 부하 테스트(JMeter)에서는 동일 유저에게 중복 쿠폰이 발급되는 레이스 컨디션이 발생했고, 문제의 원인을 추적하는 과정을 거치며 해결했습니다.

1. 의심

“혹시 코드 문제인가?” → 아니었다. 인증 흐름은 정상이었으며, 내가 구현한 코드 로직도 정상이었다.

  • JwtAuthenticationFilter, JwtUtil 로직을 전부 확인했다.
    (내가 구현한 쿠폰 부분이 통과할 수 있도록 추가 및 일부 수정)
  • PostMan에서 요청 했을 때, 정상적으로 돌아갔다.
    → 즉, 코드 문제는 아니었다.

2. 전환

그래? 그러면 뭐가 문제지? 🧐 
→ 혹시..? Jmeter 설정 문제인가?

  • JMeter 테스트 계획을 처음엔 간단히 구성했는데, Postman 기준으로 JMeter도 동일하게 설정 수정.
  • 수정 후, 테스트하니 “이미 발급된 쿠폰”예외가 의도대로 발생.

3. 깨달음 : 디버깅은 로그와 함께

이 과정에서 느낀 점이 잇다.
”테스트 도구에서 발생하는 문제는 눈에 잘 안띄기 때문에 실제 요청이 어떻게 나가고 있는지 꼭 로그를 통해 확인해야 한다”는 것이었다.

  • 코드가 아니라 JMeter 설정이 문제였던 것.
  • 실제 요청이 어떻게 나가는지 로그를 반드시 확인해야 한다는 걸 다시 한 번 체감했습니다

4. 회고 : 의심하고, 확인하고, 다시 돌아보기

처음엔 “JWT 필터 수정하니 해치웠나?” 싶었고,

그 다음엔

“이거 락이 안 먹히는 거 아녀?” 싶었는데,
결국엔

JMeter 설정 실수 하나가 모든 원인이었다..ㅋㅋㅋ

덕분에 인증 흐름과 동시성 제어까지 다시 한 번 점검해볼 수 있었고, 문제의 원인은 항상 예상한 것보다 사소한 것에서 시작된다는 것을 느끼게 되었습니다.

5. 현재 시스템 구조 요약

쿠폰 생성 시: 수량 지정 (예: 100개)

발급 시

① 이미 발급된 쿠폰이 있는지 확인
② 수량 초과 여부 확인
③ 위 두 조건 통과 시 발급 (이때 비관적 락 적용)

즉, 발급 시점에 DB 비관적 락을 적용하여 Race Condition 방지.
테스트 기준, 문제없이 정확히 1회만 발급됨.

6. 향후 개선 방향

  • 잔여 수량 자체를 Redis로 캐싱하여 더 가볍게 동시성 제어
  • 분산 락을 활용한 확장성 확보
  • 쿠폰 발급 시도/실패 로그를 별도로 수집하여 모니터링 대시보드 구성도 고려

은성님 요ㅊ어

@AuthenticationPrincipal AuthUser authUser, 로 수정 요청.

profile
백엔드를 지향하며, 컴퓨터공학과를 졸업한 취준생입니다. 많이 부족하지만 열심히 노력해서 실력을 갈고 닦겠습니다. 부족하고 틀린 부분이 있을 수도 있지만 이쁘게 봐주시면 감사하겠습니다. 틀린 부분은 댓글 남겨주시면 제가 따로 학습 및 자료를 찾아봐서 제 것으로 만들도록 하겠습니다. 귀중한 시간 방문해주셔서 감사합니다.

4개의 댓글

comment-user-thumbnail
2025년 4월 20일

잘 읽었습니다 ^_^~

2개의 답글
comment-user-thumbnail
2025년 4월 20일

잘 읽었습니다 ^_^~

답글 달기