Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions backend/JiShop/src/main/java/com/jishop/config/AsyncConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.jishop.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

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 메서드를 추후에 다른곳에서 비동기 실행하기 위해서 미리 붙여 놓으신건가요?

@Configuration
@EnableScheduling
public class AsyncConfig {

@Bean
public Executor taskExecutor() {
// Spring 제공 스레드 풀 정의
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 기본 스레드 수(유지되는 최소 스레드 수)
executor.setCorePoolSize(5);
// 최대 스레드 수(동시 실행 가능한 스레드 수)
executor.setMaxPoolSize(10);
// 작업을 큐에 최대로 대기시킬수 있는 허용량
executor.setQueueCapacity(100);
// 생성된 스레드 이름
executor.setThreadNamePrefix("TaskExecutor-");
// 쓰레드 풀 초기화 후 리턴
executor.initialize();
return executor;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,6 @@ public ObjectMapper redisObjectMapper() {
return mapper;
}

@Bean
public FilterRegistrationBean<SessionRepositoryFilter<?>> redisSessionFilterRegistration(SessionRepositoryFilter<?> sessionRepositoryFilter) {
FilterRegistrationBean<SessionRepositoryFilter<?>> registrationBean = new FilterRegistrationBean<>(sessionRepositoryFilter);
// /auth/* 패턴에만 Redis 세션 저장소 적용 (즉, 로그인 관련 요청에만)
registrationBean.setUrlPatterns(Arrays.asList("/auth/*"));
return registrationBean;
}

// 인기 검색어, RedisTemplate 빈 등록
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
Expand Down
30 changes: 10 additions & 20 deletions backend/JiShop/src/main/java/com/jishop/config/WebConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -64,23 +64,13 @@ public CookieSerializer cookieSerializer() {
return serializer;
}

// todo: 추후 반영 결정해야할 사항 (3/23)
/* @Bean
public FilterRegistrationBean<SecurityHeadersFilter> securityHeadersFilter() {
FilterRegistrationBean<SecurityHeadersFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new SecurityHeadersFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return registrationBean;
}*/

// todo: 추후 반영 결정해야할 사항 (3/23)
/*@Bean
public FilterRegistrationBean<CsrfFilter> csrfFilter() {
FilterRegistrationBean<CsrfFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new CsrfFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
return registrationBean;
}*/
}
// todo: 추후 반영 결정해야할 사항 (3/23)
/*@Bean
public FilterRegistrationBean<CsrfFilter> csrfFilter() {
FilterRegistrationBean<CsrfFilter> registrationBean = new FilterRegistrationBean<>();
registrationBean.setFilter(new CsrfFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
return registrationBean;
}*/
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@
import jakarta.servlet.http.HttpSession;
import org.springframework.http.ResponseEntity;

import java.util.concurrent.ExecutionException;

@Tag(name = "로컬 로그인 API")
public interface AuthController {
@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를 생성해서 적절한 응답을 만들어주는것은 어떨까요?

HttpServletResponse response) throws ExecutionException, InterruptedException;
@Operation(summary = "회원 로그아웃")
ResponseEntity<Void> logout(User user,HttpServletRequest request);
@Operation(summary = "로그인 상태 체크")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,43 +5,55 @@
import com.jishop.member.domain.User;
import com.jishop.member.dto.request.SignInFormRequest;
import com.jishop.member.service.AuthService;
import com.jishop.queue.service.QueueService;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.UUID;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

@RestController
@RequiredArgsConstructor
@RequestMapping("/auth")
public class AuthControllerImpl implements AuthController {

private final AuthService service;
private final QueueService queueService;

@PostMapping("/signin")
public ResponseEntity<String> signIn(@RequestBody @Valid SignInFormRequest request,
public ResponseEntity<?> signIn(@RequestBody @Valid SignInFormRequest request,
HttpServletRequest httpRequest,
HttpServletResponse httpServletResponse) {
// todo: IP 주소 로깅을 해야할까??
// String clientIp = getClientIp(httpRequest);
HttpServletResponse httpServletResponse) throws ExecutionException, InterruptedException {
// todo: IP 주소 로깅을 해야할까?? -> 해결책 다른 나라, 일단 다른 ip인 경우 해당 하면 알림 보내기?(4/17)

HttpSession session = httpRequest.getSession();
service.signIn(request, session);
try {
// 요청 처리 시작 시 카운터 증가
queueService.incrementActiveRequests();

Long userId = (Long) session.getAttribute("userId");
String welcomeMessage = service.loginStr(userId);
HttpSession session = httpRequest.getSession();
CompletableFuture<String> future = service.signInType(request, session);
String taskId = future.get();

// CSRF 토큰 생성 및 응답 헤더에 포함
String csrfToken = UUID.randomUUID().toString();
session.setAttribute("CSRF_TOKEN", csrfToken);
httpServletResponse.setHeader("X-CSRF-TOKEN", csrfToken);
if(taskId.startsWith("immediate-login-")){
Long userId =(Long) session.getAttribute("userId");
String welcome = service.loginStr(userId);
return ResponseEntity.ok(welcome);
}

return ResponseEntity.ok().contentType(MediaType.APPLICATION_JSON).body(welcomeMessage);
return ResponseEntity.accepted().body(Map.of(
"taskId", taskId,
"message", "로그인 요청이 대기열에 등록되었습니다."
));
} finally {
// 요청 처리 완료 시 카운터 감소 (예외 발생해도 실행됨)
queueService.decrementActiveRequests();
}
}

@GetMapping("/logout")
Expand All @@ -58,7 +70,7 @@ public ResponseEntity<Void> logout(@CurrentUser User user, HttpServletRequest re
@GetMapping()
public ResponseEntity<String> checkLogin(@CurrentUser User user) {
Long id = service.checkLogin(user);
if(id == null) return ResponseEntity.badRequest().body("로그인 타임 종료!");
if(id == null) return ResponseEntity.badRequest().body("로그아웃!");
return ResponseEntity.ok("로그인 중!");
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import com.jishop.member.dto.response.UserResponse;
import jakarta.servlet.http.HttpSession;

import java.util.concurrent.CompletableFuture;

public interface AuthService {

void signIn(SignInFormRequest form, HttpSession session);
Expand All @@ -20,4 +22,6 @@ public interface AuthService {
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.

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

User attemptLogin(SignInFormRequest form);
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,20 @@
import com.jishop.member.dto.response.UserResponse;
import com.jishop.member.repository.UserRepository;
import com.jishop.member.service.AuthService;
import com.jishop.queue.domain.TaskType;
import com.jishop.queue.service.QueueService;
import com.jishop.queue.service.TaskProducer;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.Map;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;

import java.time.Duration;
import java.util.function.Consumer;

Expand All @@ -22,8 +29,10 @@
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {

private final UserRepository userRepository;
private final QueueService queueService;
private final PasswordEncoder passwordEncoder;
private final UserRepository userRepository;
private final TaskProducer taskProducer;
private final RedisTemplate<String, Object> redisTemplate;

@Override
Expand Down Expand Up @@ -134,5 +143,33 @@ public void logout(User user) {
String cacheKey = "user::" + user.getId();
redisTemplate.delete(cacheKey);
}

public CompletableFuture<String> signInType(SignInFormRequest request, HttpSession session) {
if (queueService.useQueue()) {
Map<String, Object> payload = Map.of(
"loginId", request.loginId(),
"password", request.password(),
"sessionId", session.getId() // "sessionId"로 키 이름 통일
);
return taskProducer.submitTask(TaskType.LOGIN, payload);
} 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를 사용하신걸까요?

}
}

// 대기열을 위한 메서드 추가
public User attemptLogin(SignInFormRequest form) {
User user = userRepository.findByLoginId(form.loginId())
.orElseThrow(() -> new DomainException(ErrorType.USER_NOT_FOUND));

if(!passwordEncoder.matches(form.password(), user.getPassword())) {
throw new DomainException(ErrorType.USER_NOT_FOUND);
}
if(user.isDeleteStatus()) throw new DomainException(ErrorType.USER_NOT_FOUND);

return user;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.jishop.queue.controller;

import com.jishop.queue.dto.TaskRequest;
import org.springframework.http.ResponseEntity;

public interface TaskController {

ResponseEntity<?> addTask(TaskRequest request);
ResponseEntity<?> getTaskStatus(String taskId);
ResponseEntity<?> getQueueStatus();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package com.jishop.queue.controller;

import com.jishop.queue.domain.Task;
import com.jishop.queue.dto.TaskRequest;
import com.jishop.queue.service.QueueService;
import com.jishop.queue.service.TaskProducer;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

@RestController
@RequiredArgsConstructor
@RequestMapping("/pend")
public class TaskControllerImpl {

private final TaskProducer taskProducer;
private final QueueService queueService;

// 작업 추가
@PostMapping("/tasks")
public ResponseEntity<?> addTask(@RequestBody TaskRequest request) {
try{
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명 이상일 때 인가요..? 아니면 무슨 의미일까요? 이름으로 알기 쉽게 상수로 관리하는 건 어떻게 생각하시나요? 🤔

return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS)
.body(Map.of("taskId", future.get(), "status", "queued",
"message", "잠시 후 다시 시도해주세요!"));
}
return ResponseEntity.accepted()
.body(Map.of("taskId", future.get(), "status", "대기열 등록 완료"));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("error", e.getMessage()));
}
}

// 작업 상태 조회
@GetMapping("/tasks/{taskId}/status")
public ResponseEntity<?> getTaskStatus(@PathVariable String taskId) {
Task task = queueService.getTaskById(taskId);
if(task == null){
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(Map.of("message", "해당 작업을 찾을 수 없습니다!"));
}
return ResponseEntity.ok(Map.of("taskId", taskId, "status", task.getStatus()));
}

// 큐 상태 조회
@GetMapping("/queue/status")
public ResponseEntity<?> getQueueStatus() {
return ResponseEntity.ok(Map.of("queueSize", queueService.getQueueSize(),
"timestamp", LocalDateTime.now()));
}
}
Loading