Skip to content

Comments

[BE] 대기열 1단계 구현 완#313

Open
JangBJ wants to merge 11 commits intodevelopfrom
be/feat/242
Open

[BE] 대기열 1단계 구현 완#313
JangBJ wants to merge 11 commits intodevelopfrom
be/feat/242

Conversation

@JangBJ
Copy link
Contributor

@JangBJ JangBJ commented Apr 17, 2025


🚀 어떤 기능을 구현했나요 ?

  • 과도한 트래픽이 몰릴 경우 해당 과부하를 천천히 부하가 오도록 큐 구현
  • 작업의 분류는 타입으로 결정
  • 현재는 로그인 부분만 구현 추후 결제에서도 가능하도록 구현 -> 현재 구현한 부분이 로그인 한 부분이기에 한가지의 큐만 생성되었지만 추후 결제에도 대기열을 붙이게 된다면 큐를 따로 분리해서 구현해야함
  • 현재 구조는 kafka, RabbitMQ같은 메시지 큐, redis stream, SQS 등을 사용안함 -> 추후 진행 예정
  • 부하시 카운트를 하고 해당 카운트를 넘으면 과부하로 간주 후 해당 작업 큐로 이동 -> 스케줄러를 이용해 부하를 천천히 진행할 수 있게 만들었습니다

🔥 어떤 문제를 마주했나요 ?

  • 로그인 대기열 구현 시 해당 작업을 무조건 큐에 넣었다가 뺄것인지 아니면 부하를 따져서 일반 로그인으로 진행한 후 과부하로 판단되면 해당 작업을 큐에 태울것인지에 대한 고민
  • 메시지 큐 없이 어떻게 상황을 알고 큐에서 작업을 꺼내서 실행해야하는지
  • 작업이 실패할 경우 어떠한 조치를 취해야할지?

✨ 어떻게 해결했나요 ?

  • AtomicInteger를 사용한 사용자 count후 일정 count가 넘게되면 대기열 진입 -> 이렇게 한 이유는 사용자 관점에서 조금의 딜레이도 안좋지 않을까 라는 생각으로 인해 위 방식으로 결정
  • 메시지 큐를 사용하지 않기에 스케줄러를 이용한 부하를 줄이는 형식으로 구현
  • 작업이 실패할 경우 DeadLetter큐를 생성해야하는 상황 생각 -> 아직 구현 미완

📝 어떤 부분에 집중해서 리뷰해야 할까요?

📚 참고 자료 및 회고

@JangBJ JangBJ added this to the 6차 마일스톤 milestone Apr 17, 2025
@JangBJ JangBJ self-assigned this Apr 17, 2025

import java.util.concurrent.Executor;

@EnableAsync
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

taskExecutor 메서드를 추후에 다른곳에서 비동기 실행하기 위해서 미리 붙여 놓으신건가요?

@Operation(summary = "로컬 회원 로그인")
ResponseEntity<String> signIn(SignInFormRequest request, HttpServletRequest httpRequest,
HttpServletResponse response);
ResponseEntity<?> signIn(SignInFormRequest request, HttpServletRequest httpRequest,
Copy link
Contributor

@KWAK-JINHO KWAK-JINHO Apr 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

저는 <?>를 사용하기보다는 명확한 타입을 정해놓고 메서드를 쪼개는 방식을 선호합니다. 아니면 customReponse< T > DTO를 생성해서 적절한 응답을 만들어주는것은 어떨까요?

void updateAdSMSAgree(User user, UserAdSMSRequest request);
void updateAdEmailAgree(User user, UserAdEmailRequest request);
void logout(User user);
CompletableFuture<String> signInType(SignInFormRequest request, HttpSession session);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CompletableFuture 사용하신 이유와 무엇인지 궁금합니다.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

대기열은 무엇인지 어떻게 작동하는 건가요?

// 작업 유형
private TaskType type;
// 작업에 필요한 데이터
private Map<String, Object> payload;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

일급 컬랙션이 아닌 Map을 사용하신 이유가 있을까요?

Comment on lines +38 to +44
Object obj = redisTemplate.opsForList().leftPop(TASK_QUEUE);
if (obj != null) {
Task task = (Task) obj;
redisTemplate.opsForSet().add(PROCESSING_SET, task);
return task;
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 레디스에서 작업 큐를 leftPop(가장 오래된 작업 pop?)
  2. 다시 그 작업을 redis에 add
  3. task 반환

이게 어떤 의미인가요?

Comment on lines +35 to +37
String loginId = (String) task.getPayload().get("loginId");
String password = (String) task.getPayload().get("password");
String sessionId = (String) task.getPayload().get("sessionId");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

되도록 하드 코딩보단 객체 지향적인 코드가 좋을 것 같아요

Comment on lines +41 to +54
User user = authService.attemptLogin(new SignInFormRequest(loginId, password));

// Redis에 직접 세션 데이터 저장
String sessionKey = "spring:session:sessions:" + sessionId;
redisTemplate.opsForHash().put(sessionKey, "sessionAttr:userId", user.getId());

// 세션 만료 시간 설정
redisTemplate.expire(sessionKey, 3600, TimeUnit.SECONDS);

log.info("사용자 {} 로그인 성공", user.getId());
} catch (Exception e) {
log.error("로그인 실패: {}", e.getMessage(), e);
throw new RuntimeException("로그인 처리 중 오류 발생", e);
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

객체로 작성하면 getkey(), creatformRequest() 식으로 할 수 있어서 관리가 편할 것 같아요


try {
// AuthService에 새 메서드 추가 필요
User user = authService.attemptLogin(new SignInFormRequest(loginId, password));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

무슨 기능일까요?


// Redis에 직접 세션 데이터 저장
String sessionKey = "spring:session:sessions:" + sessionId;
redisTemplate.opsForHash().put(sessionKey, "sessionAttr:userId", user.getId());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sessionAttr:userId 이부분이 중복되는 걸 보면 user 유일값 만 저장하시는 것같은데

기본형인 opsForValue()를 사용하지 않고
key, field, value 형식인 opsForHash()로 하신 이유가 있을까요?

Comment on lines +59 to +83
// bracket 표기법으로 내장 메트릭 값을 안전하게 읽음
let totalRequests = data.metrics["http_reqs"] ? data.metrics["http_reqs"].count : 0;
let immediateLoginCount = data.metrics["immediate_logins"] ? data.metrics["immediate_logins"].count : 0;
let queueLoginCount = data.metrics["queue_logins"] ? data.metrics["queue_logins"].count : 0;
let failureCount = data.metrics["failures"] ? data.metrics["failures"].count : 0;
let p50 = data.metrics["http_req_duration"] ? data.metrics["http_req_duration"]["p(50)"] : 'undefined';
let p95 = data.metrics["http_req_duration"] ? data.metrics["http_req_duration"]["p(95)"] : 'undefined';
let p99 = data.metrics["http_req_duration"] ? data.metrics["http_req_duration"]["p(99)"] : 'undefined';

let defaultSummary = textSummary(data, { indent: ' ', enableColors: true });

let custom = `
===========================
💻 TEST SUMMARY
===========================
총 요청 수: ${totalRequests}
- 즉시 로그인(200): ${immediateLoginCount}
- 대기열 진입(202): ${queueLoginCount}
- 기타 실패: ${failureCount}

** 응답 시간 분포 **
- p(50): ${p50} ms
- p(95): ${p95} ms
- p(99): ${p99} ms

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

굳입니다. 성능면에서 어느 정도 나왔는지 궁금합니다!

} else {
// 대기열 사용하지 않을 때는 즉시 로그인 처리
this.signIn(request, session);
return CompletableFuture.completedFuture("immediate-login-" + UUID.randomUUID());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

각 사용자를 구분하기 위해 UUID를 사용하신걸까요?

// 작업 실패 시 재시도 횟수 카운트
private int retryCount;
// 작업 상태 (PENDING, RETRY, FAILED, DONE)
private String status;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

작업 상태를 enum으로 관리할 생각이 있으신가요?

CompletableFuture<String> future = taskProducer.submitTask(
request.type(), request.payload());

if(queueService.getQueueSize()> 300) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서 300은 대기자가 300명 이상일 때 인가요..? 아니면 무슨 의미일까요? 이름으로 알기 쉽게 상수로 관리하는 건 어떻게 생각하시나요? 🤔

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants