diff --git a/backEnd/src/main/java/com/quiz/ourclass/domain/member/controller/MemberController.java b/backEnd/src/main/java/com/quiz/ourclass/domain/member/controller/MemberController.java index 6a0b28b8..0ebbb231 100644 --- a/backEnd/src/main/java/com/quiz/ourclass/domain/member/controller/MemberController.java +++ b/backEnd/src/main/java/com/quiz/ourclass/domain/member/controller/MemberController.java @@ -1,15 +1,21 @@ package com.quiz.ourclass.domain.member.controller; import com.quiz.ourclass.domain.member.controller.docs.MemberControllerDocs; +import com.quiz.ourclass.domain.member.dto.TokenDTO; import com.quiz.ourclass.domain.member.dto.request.DefaultImageRequest; import com.quiz.ourclass.domain.member.dto.request.DeveloperAtRtRequest; import com.quiz.ourclass.domain.member.dto.request.MemberSignInRequest; import com.quiz.ourclass.domain.member.dto.request.MemberSignUpRequest; import com.quiz.ourclass.domain.member.dto.request.MemberUpdateRequest; import com.quiz.ourclass.domain.member.dto.request.UpdateFcmTokenRequest; +import com.quiz.ourclass.domain.member.dto.response.DefaultImagesResponse; +import com.quiz.ourclass.domain.member.dto.response.MemberMeResponse; +import com.quiz.ourclass.domain.member.dto.response.MemberUpdateResponse; +import com.quiz.ourclass.domain.member.dto.response.OIDCPublicKeysResponse; import com.quiz.ourclass.domain.member.service.MemberService; import com.quiz.ourclass.domain.member.service.client.KakaoOicdClient; import com.quiz.ourclass.domain.quiz.dto.request.QuizStartRequest; +import com.quiz.ourclass.domain.quiz.dto.response.QuizStartResponse; import com.quiz.ourclass.global.dto.MemberSimpleDTO; import com.quiz.ourclass.global.dto.ResultResponse; import io.swagger.v3.oas.annotations.Parameter; @@ -26,7 +32,6 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -41,37 +46,31 @@ public class MemberController implements MemberControllerDocs { /* 1. 회원가입 */ @PostMapping(value = "/", consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE}) - public ResponseEntity> signUp(@ModelAttribute MemberSignUpRequest request) { + public ResponseEntity> signUp( + @ModelAttribute MemberSignUpRequest request) { return ResponseEntity.ok(ResultResponse.success(memberService.signUpProcess(request))); } /* 2. 로그인 */ @PostMapping("/sign-in") - public ResponseEntity> signIn(@RequestBody MemberSignInRequest request) { + public ResponseEntity> signIn( + @RequestBody MemberSignInRequest request) { return ResponseEntity.ok(ResultResponse.success(memberService.signInProcess(request))); } /* 4. 테스트용 */ @GetMapping("/kakao-keys") - public ResponseEntity> getKakaoKeys() { + public ResponseEntity> getKakaoKeys() { return ResponseEntity.ok(ResultResponse.success(kakaoOicdClient.getKakaoOIDCOpenKeys())); } - /* 5. id-Token 받아서 Decoding 하기 */ - @PostMapping("/decode-id-token") - public ResponseEntity> decodeIdToken(@RequestParam String idToken) { - log.info(idToken); -// return ResponseEntity.ok(ResultResponse.success(oicdUtil.getUnsignedTokenClaims(idToken,"https://kauth.kakao.com", "edbf10bd8627e6eb676872109e996a9e"))); - return null; - } - - /* 6 개발자용 Access, RefreshToken 발급 */ @PostMapping("/developer-At") - public ResponseEntity> getAtRt(@RequestBody DeveloperAtRtRequest request) { + public ResponseEntity> getAtRt( + @RequestBody DeveloperAtRtRequest request) { return ResponseEntity.ok( ResultResponse.success(memberService.giveDeveloperAccessToken(request))); @@ -88,7 +87,7 @@ public ResponseEntity> saveFcmToken( /* 8. 기본 이미지 업데이트 */ @PatchMapping("/default-image") - public ResponseEntity> updateDefaultImage( + public ResponseEntity> updateDefaultImage( @ModelAttribute DefaultImageRequest request) { return ResponseEntity.ok( ResultResponse.success(memberService.updateDefaultImage(request).getPhoto())); @@ -97,13 +96,13 @@ public ResponseEntity> updateDefaultImage( /* 8. 기본 이미지 조회 */ @GetMapping("/default-image") - public ResponseEntity> getDefaultImages() { + public ResponseEntity> getDefaultImages() { return ResponseEntity.ok(ResultResponse.success(memberService.getDefaultImages())); } /* 9. 현 유저의 회원 정보 주기 */ @GetMapping("/") - public ResponseEntity> rememberMe() { + public ResponseEntity> rememberMe() { return ResponseEntity.ok( ResultResponse.success(memberService.rememberMe())); @@ -111,25 +110,25 @@ public ResponseEntity> rememberMe() { /* 10. 멤버 프로필 이미지 수정 */ @PatchMapping("/photo") - public ResponseEntity> updateProfile( + public ResponseEntity> updateProfile( @ModelAttribute MemberUpdateRequest request) { return ResponseEntity.ok(ResultResponse.success(memberService.updateProfile(request))); } @PostMapping("/start") - public ResponseEntity> certificatingUser( + public ResponseEntity> certificatingUser( @RequestBody QuizStartRequest request) { return ResponseEntity.ok(ResultResponse.success(memberService.certificatingUser(request))); } @DeleteMapping("/") - public ResponseEntity> deleteMe() { + public ResponseEntity> deleteMe() { memberService.deleteMe(); return ResponseEntity.ok(ResultResponse.success(null)); } @GetMapping("/{id}") - public ResponseEntity> select( + public ResponseEntity> select( @Parameter(name = "id", description = "멤버 PK 값", required = true, in = ParameterIn.PATH) @PathVariable(value = "id") Long id ) { diff --git a/backEnd/src/main/java/com/quiz/ourclass/domain/member/controller/docs/MemberControllerDocs.java b/backEnd/src/main/java/com/quiz/ourclass/domain/member/controller/docs/MemberControllerDocs.java index 057cec09..81a2115c 100644 --- a/backEnd/src/main/java/com/quiz/ourclass/domain/member/controller/docs/MemberControllerDocs.java +++ b/backEnd/src/main/java/com/quiz/ourclass/domain/member/controller/docs/MemberControllerDocs.java @@ -1,12 +1,16 @@ package com.quiz.ourclass.domain.member.controller.docs; +import com.quiz.ourclass.domain.member.dto.TokenDTO; import com.quiz.ourclass.domain.member.dto.request.DefaultImageRequest; import com.quiz.ourclass.domain.member.dto.request.MemberSignInRequest; import com.quiz.ourclass.domain.member.dto.request.MemberSignUpRequest; import com.quiz.ourclass.domain.member.dto.request.MemberUpdateRequest; import com.quiz.ourclass.domain.member.dto.request.UpdateFcmTokenRequest; +import com.quiz.ourclass.domain.member.dto.response.DefaultImagesResponse; +import com.quiz.ourclass.domain.member.dto.response.MemberMeResponse; import com.quiz.ourclass.domain.member.dto.response.MemberUpdateResponse; import com.quiz.ourclass.domain.quiz.dto.request.QuizStartRequest; +import com.quiz.ourclass.domain.quiz.dto.response.QuizStartResponse; import com.quiz.ourclass.global.dto.MemberSimpleDTO; import com.quiz.ourclass.global.dto.ResultResponse; import io.swagger.v3.oas.annotations.Operation; @@ -40,7 +44,7 @@ public interface MemberControllerDocs { description = "OIDC 토큰 인증에 실패했습니다.") }) @PostMapping - ResponseEntity> signUp(MemberSignUpRequest request); + ResponseEntity> signUp(MemberSignUpRequest request); @Operation(summary = "로그인", @@ -55,7 +59,7 @@ public interface MemberControllerDocs { description = "OIDC 토큰 인증에 실패했습니다.") }) @PostMapping - ResponseEntity> signIn(MemberSignInRequest request); + ResponseEntity> signIn(MemberSignInRequest request); @Operation(summary = "FCM 토큰 저장 및 갱신", description = "입력으로 들어오는 FCM 토큰을 저장 및 갱신 합니다.", @@ -74,8 +78,17 @@ ResponseEntity> saveFcmToken( description = "기본 이미지 조회에 성공하였습니다." ) }) + @GetMapping("/default-image") + public ResponseEntity> getDefaultImages(); + + @Operation(summary = "기본 이미지 정보 수정", + responses = { + @ApiResponse(responseCode = "200", + description = "기본 이미지 수정에 성공하였습니다." + ) + }) @PatchMapping("/default-image") - public ResponseEntity> updateDefaultImage( + public ResponseEntity> updateDefaultImage( @ModelAttribute DefaultImageRequest request); @Operation(summary = "현 유저의 회원정보 가져오기", @@ -84,7 +97,7 @@ public ResponseEntity> updateDefaultImage( description = "유저 정보 확인에 성공하였습니다.") }) @GetMapping("/") - public ResponseEntity> rememberMe(); + public ResponseEntity> rememberMe(); @Operation(summary = "프로필 이미지 수정", responses = { @@ -94,7 +107,8 @@ public ResponseEntity> updateDefaultImage( ) @PatchMapping("/photo") - public ResponseEntity> updateProfile(MemberUpdateRequest request); + public ResponseEntity> updateProfile( + MemberUpdateRequest request); @Operation(summary = "멤버 조회", description = "ID 값에 해당하는 멤버 Simple 정보(id, name, iamgeUrl)를 조회합니다.", responses = { @@ -104,14 +118,15 @@ public ResponseEntity> updateDefaultImage( } ) @GetMapping("{id}") - ResponseEntity> select( + ResponseEntity> select( @PathVariable(value = "id") Long id ); @PostMapping("/start") - public ResponseEntity> certificatingUser(QuizStartRequest request); + public ResponseEntity> certificatingUser( + QuizStartRequest request); @DeleteMapping("/") - public ResponseEntity> deleteMe(); + public ResponseEntity> deleteMe(); } diff --git a/backEnd/src/main/java/com/quiz/ourclass/domain/member/dto/request/MemberSignInRequest.java b/backEnd/src/main/java/com/quiz/ourclass/domain/member/dto/request/MemberSignInRequest.java index 2ba4f39a..8a88d2f9 100644 --- a/backEnd/src/main/java/com/quiz/ourclass/domain/member/dto/request/MemberSignInRequest.java +++ b/backEnd/src/main/java/com/quiz/ourclass/domain/member/dto/request/MemberSignInRequest.java @@ -1,6 +1,5 @@ package com.quiz.ourclass.domain.member.dto.request; -import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/backEnd/src/main/java/com/quiz/ourclass/domain/member/service/OidcService.java b/backEnd/src/main/java/com/quiz/ourclass/domain/member/service/OidcService.java index 9705ffc9..88c3071e 100644 --- a/backEnd/src/main/java/com/quiz/ourclass/domain/member/service/OidcService.java +++ b/backEnd/src/main/java/com/quiz/ourclass/domain/member/service/OidcService.java @@ -3,13 +3,10 @@ import com.quiz.ourclass.domain.member.dto.OIDCDecodePayload; import com.quiz.ourclass.domain.member.dto.OIDCPublicKeyDTO; import com.quiz.ourclass.domain.member.dto.response.OIDCPublicKeysResponse; -import com.quiz.ourclass.domain.member.repository.MemberRepository; import com.quiz.ourclass.domain.member.service.client.KakaoOicdClient; import com.quiz.ourclass.domain.member.service.oidc.OidcUtilImpl; import com.quiz.ourclass.global.exception.ErrorCode; import com.quiz.ourclass.global.exception.GlobalException; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Jws; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -31,20 +28,23 @@ public class OidcService { private final KakaoOicdClient kakaoOicdClient; /* - * 1. 유효성 검증 - * (1) 토큰 디코딩 -> (2) kid 값 알아내기 -> (3) kid 이용, RSA key 재료 얻어내기 -> RSA를 통한 유효성 검증 - * */ + * 1. 유효성 검증 + * (1) 토큰 디코딩 -> (2) kid 값 알아내기 -> (3) kid 이용, RSA key 재료 얻어내기 -> RSA를 통한 유효성 검증 + * */ - public OIDCDecodePayload certificatingIdToken (String idToken) { + public OIDCDecodePayload certificatingIdToken(String idToken) { // 해당 토큰의 서명 인증 할 수 있는 공개 키의 id (kid) 특정 String kid = oicdUtil.getKidFromUnsignedTokenHeader(idToken, iss, aud); // 카카오 인증 서버에서 이번 주기에 사용한 공개키 목록 전체 받아오기 -> 캐시화 필요!!! 너무 많이 요청하면 차단 당함! OIDCPublicKeysResponse keys = kakaoOicdClient.getKakaoOIDCOpenKeys(); // 이번 주에 카카오가 제공하는 공개키 중에 내 Resource Owner 의 id-token 인증 가능한 key 받아오기 - OIDCPublicKeyDTO nowKey = keys.getKeys().stream().filter(key -> key.getKid().equals(kid)).findFirst().orElseThrow(null); + OIDCPublicKeyDTO nowKey = keys.getKeys().stream().filter(key -> key.getKid().equals(kid)) + .findFirst().orElseThrow(null); - if(nowKey == null) throw new GlobalException(ErrorCode.CERTIFICATION_FAILED); + if (nowKey == null) { + throw new GlobalException(ErrorCode.CERTIFICATION_FAILED); + } // 해당 키로 서명 인증, 예외 안 터지고 인증 되면 Body 가져옴 return oicdUtil.getOIDCTokenBody(idToken, nowKey.getN(), nowKey.getE()); diff --git a/backEnd/src/main/java/com/quiz/ourclass/domain/quiz/controller/QuizController.java b/backEnd/src/main/java/com/quiz/ourclass/domain/quiz/controller/QuizController.java index 7a30b446..8f9b64ad 100644 --- a/backEnd/src/main/java/com/quiz/ourclass/domain/quiz/controller/QuizController.java +++ b/backEnd/src/main/java/com/quiz/ourclass/domain/quiz/controller/QuizController.java @@ -1,9 +1,12 @@ package com.quiz.ourclass.domain.quiz.controller; import com.quiz.ourclass.domain.quiz.controller.docs.QuizControllerDocs; +import com.quiz.ourclass.domain.quiz.dto.GamerDTO; +import com.quiz.ourclass.domain.quiz.dto.QuizGameDTO; import com.quiz.ourclass.domain.quiz.dto.request.MakingQuizRequest; import com.quiz.ourclass.domain.quiz.service.QuizServiceImpl; import com.quiz.ourclass.global.dto.ResultResponse; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.ResponseEntity; @@ -23,24 +26,25 @@ public class QuizController implements QuizControllerDocs { public final QuizServiceImpl quizService; @PutMapping("") - public ResponseEntity> makingQuiz(@RequestBody MakingQuizRequest request) { + public ResponseEntity> makingQuiz(@RequestBody MakingQuizRequest request) { quizService.makingQuiz(request); return ResponseEntity.ok(ResultResponse.success(null)); } @GetMapping("/{orgId}") - public ResponseEntity> getQuizList(@PathVariable("orgId") long orgId) { + public ResponseEntity>> getQuizList( + @PathVariable("orgId") long orgId) { return ResponseEntity.ok(ResultResponse.success(quizService.getQuizList(orgId))); } @GetMapping("/code/{quizGameId}") - public ResponseEntity> getQuizUrl( + public ResponseEntity> getQuizUrl( @PathVariable("quizGameId") long quizGameId) { return ResponseEntity.ok(ResultResponse.success(quizService.getQuizUrl(quizGameId))); } @GetMapping("/ranking/{quizGameId}") - public ResponseEntity> getRanking( + public ResponseEntity>> getRanking( @PathVariable("quizGameId") long quizGameId) { return ResponseEntity.ok(ResultResponse.success(quizService.getRanking(quizGameId))); } diff --git a/backEnd/src/main/java/com/quiz/ourclass/domain/quiz/controller/docs/QuizControllerDocs.java b/backEnd/src/main/java/com/quiz/ourclass/domain/quiz/controller/docs/QuizControllerDocs.java index 8a342df8..c3cf9678 100644 --- a/backEnd/src/main/java/com/quiz/ourclass/domain/quiz/controller/docs/QuizControllerDocs.java +++ b/backEnd/src/main/java/com/quiz/ourclass/domain/quiz/controller/docs/QuizControllerDocs.java @@ -1,5 +1,6 @@ package com.quiz.ourclass.domain.quiz.controller.docs; +import com.quiz.ourclass.domain.quiz.dto.GamerDTO; import com.quiz.ourclass.domain.quiz.dto.QuizGameDTO; import com.quiz.ourclass.domain.quiz.dto.request.MakingQuizRequest; import com.quiz.ourclass.global.dto.ResultResponse; @@ -8,6 +9,7 @@ import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -27,7 +29,7 @@ public interface QuizControllerDocs { """, content = @Content) }) @PutMapping("") - public ResponseEntity> makingQuiz(@RequestBody MakingQuizRequest request); + public ResponseEntity> makingQuiz(@RequestBody MakingQuizRequest request); @Operation(summary = "퀴즈 리스트 불러오기", @@ -38,7 +40,8 @@ public interface QuizControllerDocs { content = @Content) }) @GetMapping("/{orgId}") - public ResponseEntity> getQuizList(@PathVariable("orgId") long orgId); + public ResponseEntity>> getQuizList( + @PathVariable("orgId") long orgId); @Operation(summary = "퀴즈 URL 생성 및 전송", responses = { @@ -53,7 +56,7 @@ public interface QuizControllerDocs { @ApiResponse(responseCode = "403", description = "(message : \"해당 퀴즈의 [URL]을 생성할 권한이 없습니다.\")") }) @GetMapping("/code/{quizGameId}") - public ResponseEntity> getQuizUrl( + public ResponseEntity> getQuizUrl( @PathVariable("quizGameId") long quizGameId); @Operation(summary = "퀴즈 게임 실시간 랭킹 보기", @@ -62,6 +65,6 @@ public ResponseEntity> getQuizUrl( content = @Content), @ApiResponse(responseCode = "400", description = "(message : \"해당 퀴즈는 종료되었습니다.\")") }) - public ResponseEntity> getRanking( + public ResponseEntity>> getRanking( @PathVariable("quizGameId") long quizGameId); } diff --git a/backEnd/src/main/java/com/quiz/ourclass/domain/quiz/mapper/QuizGameMapper.java b/backEnd/src/main/java/com/quiz/ourclass/domain/quiz/mapper/QuizGameMapper.java index 428f2dce..e518a19e 100644 --- a/backEnd/src/main/java/com/quiz/ourclass/domain/quiz/mapper/QuizGameMapper.java +++ b/backEnd/src/main/java/com/quiz/ourclass/domain/quiz/mapper/QuizGameMapper.java @@ -23,7 +23,7 @@ public interface QuizGameMapper { @Transactional Quiz toQuiz(QuizGame quizGame, QuizDTO quizDTO); - QuizGameDTO toQuizGameDTO(QuizGame QuizGame); + QuizGameDTO toQuizGameDTO(QuizGame quizGame); FcmDTO toFcmDTO(String title, String body); diff --git a/backEnd/src/main/java/com/quiz/ourclass/domain/quiz/service/QuizReceive.java b/backEnd/src/main/java/com/quiz/ourclass/domain/quiz/service/QuizReceive.java index 3aa4cc1b..71a1bc27 100644 --- a/backEnd/src/main/java/com/quiz/ourclass/domain/quiz/service/QuizReceive.java +++ b/backEnd/src/main/java/com/quiz/ourclass/domain/quiz/service/QuizReceive.java @@ -63,11 +63,12 @@ public void receivedQuestion(QuestionRequest request) { @KafkaListener(topics = ConstantUtil.QUIZ_ANSWER, containerFactory = "answerResponseContainerFactory") public void receivedAnswer(AnswerResponse response) { log.info("보내줘야할 답 상세={}", response.toString()); - - if (response.submit().replaceAll(" ", "").equals(response.ans().replaceAll(" ", ""))) { + // replaceAll 은 항상 정규 표현식을 컴파일하므로 성능 오버헤드가 발생할 수 있음 + // replaceAll 은 정규 표현식을 사용하여 문자열을 대체하지만, replace는 정규 표현식을 사용하지 않고 단순히 문자열을 대체 + if (response.submit().replace(" ", "").equals(response.ans().replaceAll(" ", ""))) { log.info(String.valueOf( - response.submit().replaceAll(" ", "").equals(response.ans().replaceAll(" ", "")))); + response.submit().replace(" ", "").equals(response.ans().replaceAll(" ", "")))); int score = redisUtil diff --git a/backEnd/src/main/java/com/quiz/ourclass/domain/quiz/service/QuizServiceImpl.java b/backEnd/src/main/java/com/quiz/ourclass/domain/quiz/service/QuizServiceImpl.java index c023cec7..54325e22 100644 --- a/backEnd/src/main/java/com/quiz/ourclass/domain/quiz/service/QuizServiceImpl.java +++ b/backEnd/src/main/java/com/quiz/ourclass/domain/quiz/service/QuizServiceImpl.java @@ -40,7 +40,7 @@ public class QuizServiceImpl implements QuizService { private final CountdownService countdownService; @Value("${ulvan.url}") - private String UlvanUrl; + private String ulvanUrl; @Transactional @@ -88,7 +88,7 @@ public String getQuizUrl(long quizGameId) { .orElseThrow(() -> new GlobalException(ErrorCode.MEMBER_NOT_FOUND)); // 1. [UUID]를 이용해 퀴즈 게임 [URL]을 생성합니다. UUID uuid = UUID.randomUUID(); - String url = UlvanUrl + quizGameId + "/" + uuid; + String url = ulvanUrl + quizGameId + "/" + uuid; // 2. [URL]을 [REDIS]에 수명을 10분으로 두고 저장합니다. (퀴즈 방 입장할 때 체크용) redisUtil.setQuizGame(quizGameId, uuid); // 3. [URL]을 요청 당사자는 물론, 단체에 속한 모두에게 전송 합니다. diff --git a/backEnd/src/main/java/com/quiz/ourclass/global/config/AopConfig.java b/backEnd/src/main/java/com/quiz/ourclass/global/config/AopConfig.java index b3421c78..72b14863 100644 --- a/backEnd/src/main/java/com/quiz/ourclass/global/config/AopConfig.java +++ b/backEnd/src/main/java/com/quiz/ourclass/global/config/AopConfig.java @@ -1,6 +1,8 @@ package com.quiz.ourclass.global.config; import com.quiz.ourclass.global.dto.ResultResponse; +import com.quiz.ourclass.global.exception.ErrorCode; +import com.quiz.ourclass.global.exception.GlobalException; import jakarta.servlet.http.HttpServletRequest; import java.lang.reflect.Field; import java.lang.reflect.Modifier; @@ -29,7 +31,7 @@ public class AopConfig { private double afterTime = 0L; @Pointcut("execution(* com..controller..*(..))") - public void ControllerMethod() { + public void controllerMethod() { } // 컨트롤러 내의 모든 매소드에 대하여 Logging을 실행한다. 다만 어노테이션이 붙은 매소드는 실행하지 않는다. @@ -54,15 +56,36 @@ public Object logging(ProceedingJoinPoint pjp) throws Throwable { , pjp.getSignature().getDeclaringTypeName() , pjp.getSignature().getName() , logMsg); + // 결과 확인 - ResponseEntity> result = (ResponseEntity>) pjp.proceed(); + ResponseEntity> result = null; + + try { + result = (ResponseEntity>) pjp.proceed(); + } catch (Exception e) { + log.error("다음의 메소드 실행 중 에러가 발생함: {}({})", + pjp.getSignature().getDeclaringTypeName(), + pjp.getSignature().getName(), e); + } + // 끝시간 check afterTime = System.currentTimeMillis(); - if (result != null) { + if (result != null && result.getBody() != null) { log.info("-----------> RESPONSE : {}({}) = {} ({}ms)" , pjp.getSignature().getDeclaringTypeName(), pjp.getSignature().getName(), result.getBody().getData(), (afterTime - beforeTime) / 1000.0); + } else if (result != null) { + log.warn("-----------> RESPONSE : {}({}) = BODY 없음 ({}ms)", + pjp.getSignature().getDeclaringTypeName(), + pjp.getSignature().getName(), + (afterTime - beforeTime) / 1000.0); + } else { + // 어떠한 에러로 인해 Pjp 실행이 끝난 후 ResponseEntity 자체가 생성되지 않은 상황을 의미 + log.warn("-----------> RESPONSE : {}({}) = 에러로 인해 결과 반환 안됨. ({}ms)", + pjp.getSignature().getDeclaringTypeName(), + pjp.getSignature().getName(), + (afterTime - beforeTime) / 1000.0); } return result; } @@ -84,7 +107,7 @@ private String getObjectDetails(Object arg) { details.append((field.getName())).append("="); details.append((field.get(arg))).append(", "); } catch (IllegalAccessException e) { - throw new RuntimeException("특정 필드 접근에 실패했습니다.", e); + throw new GlobalException(ErrorCode.FAILED_TO_ACCESS_VARIABLE); } } @@ -109,7 +132,7 @@ private StringBuilder getHeaderDetail() { } else { - System.out.println("No HTTP request details available"); + log.warn("No HTTP request details available"); } return ans; diff --git a/backEnd/src/main/java/com/quiz/ourclass/global/config/CacheConfig.java b/backEnd/src/main/java/com/quiz/ourclass/global/config/CacheConfig.java index c1668f56..aab4113e 100644 --- a/backEnd/src/main/java/com/quiz/ourclass/global/config/CacheConfig.java +++ b/backEnd/src/main/java/com/quiz/ourclass/global/config/CacheConfig.java @@ -43,7 +43,7 @@ public RedisCacheConfiguration redisCacheConfiguration() { * */ @Bean public RedisCacheManagerBuilderCustomizer redisCacheManagerBuilderCustomizer() { - return (builder) -> builder + return builder -> builder .withCacheConfiguration("OIDC", RedisCacheConfiguration.defaultCacheConfig() .computePrefixWith(cacheName -> "OIDC:") diff --git a/backEnd/src/main/java/com/quiz/ourclass/global/config/SecurityConfig.java b/backEnd/src/main/java/com/quiz/ourclass/global/config/SecurityConfig.java index d2c98ef6..2eb46ee8 100644 --- a/backEnd/src/main/java/com/quiz/ourclass/global/config/SecurityConfig.java +++ b/backEnd/src/main/java/com/quiz/ourclass/global/config/SecurityConfig.java @@ -86,7 +86,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { (corsCustomizer -> corsCustomizer.configurationSource(corsConfigurationSource()))); http - .headers((headers) -> headers.frameOptions( + .headers(headers -> headers.frameOptions( HeadersConfigurer.FrameOptionsConfig::sameOrigin )); // (5) @@ -98,13 +98,13 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { ); http - .sessionManagement((sessionManagement) -> + .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS) ); // (7) http .authorizeHttpRequests( - (auth) -> + auth -> auth // (8) .dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll() .dispatcherTypeMatchers(DispatcherType.FORWARD).permitAll() diff --git a/backEnd/src/main/java/com/quiz/ourclass/global/config/kafka/ListenerConfig.java b/backEnd/src/main/java/com/quiz/ourclass/global/config/kafka/ListenerConfig.java index 15afd9b9..88e4e195 100644 --- a/backEnd/src/main/java/com/quiz/ourclass/global/config/kafka/ListenerConfig.java +++ b/backEnd/src/main/java/com/quiz/ourclass/global/config/kafka/ListenerConfig.java @@ -5,6 +5,7 @@ import com.quiz.ourclass.domain.quiz.dto.GamerDTO; import com.quiz.ourclass.domain.quiz.dto.request.QuestionRequest; import com.quiz.ourclass.domain.quiz.dto.response.AnswerResponse; +import com.quiz.ourclass.global.util.ConstantUtil; import java.util.Map; import org.apache.kafka.clients.consumer.ConsumerConfig; import org.apache.kafka.common.serialization.StringDeserializer; @@ -49,7 +50,7 @@ public ConsumerFactory consumerFactory() { .put(ConsumerConfig.GROUP_ID_CONFIG, kafkaGroup) .put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class) .put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, deserializer) - .put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest") + .put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, ConstantUtil.LATEST) .build(); return new DefaultKafkaConsumerFactory<>( @@ -75,7 +76,7 @@ public ConsumerFactory gamerDTOConsumerFactory() { .put(ConsumerConfig.GROUP_ID_CONFIG, "group-2") .put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class) .put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, deserializer) - .put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest") + .put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, ConstantUtil.LATEST) .build(); return new DefaultKafkaConsumerFactory<>( @@ -102,7 +103,7 @@ public ConsumerFactory questionRequestConsumerFactory() .put(ConsumerConfig.GROUP_ID_CONFIG, "group-3") .put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class) .put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, deserializer) - .put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest") + .put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, ConstantUtil.LATEST) .build(); return new DefaultKafkaConsumerFactory<>( @@ -128,7 +129,7 @@ public ConsumerFactory answerResponseConsumerFactory() { .put(ConsumerConfig.GROUP_ID_CONFIG, "group-4") .put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class) .put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, deserializer) - .put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest") + .put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, ConstantUtil.LATEST) .build(); return new DefaultKafkaConsumerFactory<>( consumerConfigurations, new StringDeserializer(), deserializer diff --git a/backEnd/src/main/java/com/quiz/ourclass/global/config/kafka/ProducerConfig.java b/backEnd/src/main/java/com/quiz/ourclass/global/config/kafka/ProducerConfig.java index 4a773b97..b9531c82 100644 --- a/backEnd/src/main/java/com/quiz/ourclass/global/config/kafka/ProducerConfig.java +++ b/backEnd/src/main/java/com/quiz/ourclass/global/config/kafka/ProducerConfig.java @@ -52,32 +52,32 @@ public KafkaTemplate kafkaTemplate() { // Gamer Template -> 총 게이머에 대한 명세(이름, id, 프로필 사진), 게이머의 랭킹 @Bean - public ProducerFactory GamerProducerFactory() { + public ProducerFactory gamerProducerFactory() { return new DefaultKafkaProducerFactory<>(producerConfigurations()); } @Bean public KafkaTemplate gamerTemplate() { - return new KafkaTemplate<>(GamerProducerFactory()); + return new KafkaTemplate<>(gamerProducerFactory()); } @Bean - public ProducerFactory QuestionProducerFactory() { + public ProducerFactory questionProducerFactory() { return new DefaultKafkaProducerFactory<>(producerConfigurations()); } @Bean public KafkaTemplate questionTemplate() { - return new KafkaTemplate<>(QuestionProducerFactory()); + return new KafkaTemplate<>(questionProducerFactory()); } @Bean - public ProducerFactory AnswerProducerFactory() { + public ProducerFactory answerProducerFactory() { return new DefaultKafkaProducerFactory<>(producerConfigurations()); } @Bean public KafkaTemplate answerTemplate() { - return new KafkaTemplate<>(AnswerProducerFactory()); + return new KafkaTemplate<>(answerProducerFactory()); } } diff --git a/backEnd/src/main/java/com/quiz/ourclass/global/exception/ErrorCode.java b/backEnd/src/main/java/com/quiz/ourclass/global/exception/ErrorCode.java index eae43c8e..7bd2bcdd 100644 --- a/backEnd/src/main/java/com/quiz/ourclass/global/exception/ErrorCode.java +++ b/backEnd/src/main/java/com/quiz/ourclass/global/exception/ErrorCode.java @@ -20,6 +20,7 @@ public enum ErrorCode { CANT_LOAD_KAFKA(HttpStatus.INTERNAL_SERVER_ERROR, "카프카 메타 데이터를 조회할 수 없습니다."), FAILED_TO_SENDING_MESSAGE(HttpStatus.BANDWIDTH_LIMIT_EXCEEDED, "메세지를 카프카로 전송하는데 실패했습니다. 재시도 중..."), + FAILED_TO_ACCESS_VARIABLE(HttpStatus.BAD_REQUEST, "특정 필드 접근에 실패했습니다."), //member diff --git a/backEnd/src/main/java/com/quiz/ourclass/global/util/ConstantUtil.java b/backEnd/src/main/java/com/quiz/ourclass/global/util/ConstantUtil.java index 3403d53d..613414ec 100644 --- a/backEnd/src/main/java/com/quiz/ourclass/global/util/ConstantUtil.java +++ b/backEnd/src/main/java/com/quiz/ourclass/global/util/ConstantUtil.java @@ -21,6 +21,8 @@ public abstract class ConstantUtil { public static final String REDIS_GROUP_KEY = "CHALLENGE_LEADER:"; public static final Long DEFAULT_TIMEOUT = 60L * 1000 * 20; public static final Long REDIRECT_TIME = 5L * 1000; + public static final String EXCEPTION_ATTRIBUTE = "exception"; + public static final String LATEST = "latest"; // 인스턴스화 방지 private ConstantUtil() { diff --git a/backEnd/src/main/java/com/quiz/ourclass/global/util/KafkaUtil.java b/backEnd/src/main/java/com/quiz/ourclass/global/util/KafkaUtil.java index 8a38ca50..583dc216 100644 --- a/backEnd/src/main/java/com/quiz/ourclass/global/util/KafkaUtil.java +++ b/backEnd/src/main/java/com/quiz/ourclass/global/util/KafkaUtil.java @@ -32,7 +32,17 @@ public void getTopicMeta(String topic) { descriptions.forEach((name, description) -> { log.info("Topic: {} \nPartitions: {}", name, description.partitions().size()); }); - } catch (InterruptedException | ExecutionException e) { + // 현재 쓰레드가 인터럽트 되면, 발생하는 예외 + // 인터럽트 되었다 => 다른 쓰레드가 현재 쓰레드에게 작업을 중단하고 가능한 빨리 종료하라는 신호를 보냈다. + // 지금은 InterruptedException 예외를 잡아서 서버 자체 예외만 던지고, 쓰레드 처리에 대한 조치를 하지 않고 있다. + // 이렇게 되면, 쓰레드가 정상 종료되지 않아 문제가 된다. + // 따라서 정상 종료 절차를 수행하고, 원래대로 우리 식의 예외를 수행해야 한다. + } catch (InterruptedException e) { + // InterruptedException 이 발생하면, 현재 쓰레드를 다시 인터럽트 시킨다. + // InterruptedException 으로 인터럽트를 캐치하면, 현재 쓰레드가 인터럽트 되었다는 정보를 잃기 때문이다. + Thread.currentThread().interrupt(); + throw new GlobalException(ErrorCode.CANT_LOAD_KAFKA); + } catch (ExecutionException e) { throw new GlobalException(ErrorCode.CANT_LOAD_KAFKA); } } @@ -45,7 +55,11 @@ public void deleteTopic(String topicName) { try { future.get(); log.info("Topic {} 가 성공적으로 삭제 되었습니다.", topicName); - } catch (InterruptedException | ExecutionException e) { + } catch (InterruptedException e) { + // 마찬가지의 이유로 interrupt 를 한번 더 던진다. + Thread.currentThread().interrupt(); + throw new GlobalException(ErrorCode.CANT_LOAD_KAFKA); + } catch (ExecutionException e) { throw new GlobalException(ErrorCode.CANT_LOAD_KAFKA); } } diff --git a/backEnd/src/main/java/com/quiz/ourclass/global/util/jwt/JwtAuthFilter.java b/backEnd/src/main/java/com/quiz/ourclass/global/util/jwt/JwtAuthFilter.java index d8be2813..b60f46d2 100644 --- a/backEnd/src/main/java/com/quiz/ourclass/global/util/jwt/JwtAuthFilter.java +++ b/backEnd/src/main/java/com/quiz/ourclass/global/util/jwt/JwtAuthFilter.java @@ -2,6 +2,7 @@ import com.quiz.ourclass.global.dto.FilterResponse; import com.quiz.ourclass.global.exception.ErrorCode; +import com.quiz.ourclass.global.util.ConstantUtil; import io.jsonwebtoken.Claims; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; @@ -42,13 +43,13 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse // 토큰 유효성 체크 -> 통과 못하면 바로 다음 filter 로 넘어간다. switch (jwtUtil.validateToken(token)) { case -1: - request.setAttribute("exception", ErrorCode.EXPIRED_TOKEN); + request.setAttribute(ConstantUtil.EXCEPTION_ATTRIBUTE, ErrorCode.EXPIRED_TOKEN); filterChain.doFilter(request, response); return; case -2: case -3: case -4: - request.setAttribute("exception", ErrorCode.INVALID_TOKEN); + request.setAttribute(ConstantUtil.EXCEPTION_ATTRIBUTE, ErrorCode.INVALID_TOKEN); filterChain.doFilter(request, response); return; case -5: @@ -65,7 +66,8 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse try { setAuthentication(info.getSubject()); } catch (UsernameNotFoundException e) { - request.setAttribute("exception", ErrorCode.NOT_FOUND_MEMBER.getMessage()); + request.setAttribute(ConstantUtil.EXCEPTION_ATTRIBUTE, + ErrorCode.NOT_FOUND_MEMBER.getMessage()); log.error("관련 에러: {}", e.getMessage()); } diff --git a/backEnd/src/main/java/com/quiz/ourclass/global/util/jwt/JwtUtil.java b/backEnd/src/main/java/com/quiz/ourclass/global/util/jwt/JwtUtil.java index dc9999ab..b64f841a 100644 --- a/backEnd/src/main/java/com/quiz/ourclass/global/util/jwt/JwtUtil.java +++ b/backEnd/src/main/java/com/quiz/ourclass/global/util/jwt/JwtUtil.java @@ -45,8 +45,8 @@ public class JwtUtil { * * (3) signatureAlgorithm: JWT의 전자서명 파트를 만들 때 쓰이는 알고리즘이다. * - * (4) ACCESS_TOKEN_TIME: 접근 토큰 수명 - * (5) REFRESH_TOKEN_TIME: 갱신 토큰 수명 + * (4) accessTokenTime: 접근 토큰 수명 + * (5) refreshTokenTime: 갱신 토큰 수명 * (6) Bearer Token Prefix * */ @@ -54,13 +54,18 @@ public class JwtUtil { @Value("${jwt.secret}") private String ingredient; private Key key; - private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS512; + + // final 필드를 static으로 선언하지 않으면, 클래스의 각 인스턴스 마다 해당 필드의 값이 복사됨. + // 이는 불필요하게 메모리가 사용된다. + // static 없이 final로 선언된 필드는 각 인스턴스가 고유한 값을 가질 수 있다는 의미를 내포 + // static으로 선언하면 클래스 수준에서 값을 공유한다는 의도가 명확해짐 + private static final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS512; @Value("#{new Integer('${jwt.token.access-expiration-time}')}") - private Integer ACCESS_TOKEN_TIME; + private Integer accessTokenTime; @Value("#{new Integer('${jwt.token.refresh-expiration-time}')}") - private Integer REFRESH_TOKEN_TIME; + private Integer refreshTokenTime; private static final String BEARER_PREFIX = "Bearer "; private final UserDetailsServiceImpl userDetailsService; @@ -101,7 +106,7 @@ public String createToken(long memberId, String role, boolean isAccess) { .claim("ROLE", role) .setIssuedAt(new Date(now.getTime())) .setExpiration( - new Date(now.getTime() + (isAccess ? ACCESS_TOKEN_TIME : REFRESH_TOKEN_TIME))) + new Date(now.getTime() + (isAccess ? accessTokenTime : refreshTokenTime))) .signWith(key, signatureAlgorithm) .compact(); @@ -137,20 +142,20 @@ public int validateToken(String token) { return 0; } catch (ExpiredJwtException e) { log.error("Expired JWT token, 만료된 JWT token 입니다."); - log.error("관련에러: {}", e.getMessage()); + log.error(e.getMessage()); return -1; } catch (io.jsonwebtoken.security.SignatureException | SecurityException | MalformedJwtException e) { log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다."); - log.error("관련에러: {}", e.getMessage()); + log.error(e.getMessage()); return -2; } catch (UnsupportedJwtException e) { log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다."); - log.error("관련에러: {}", e.getMessage()); + log.error(e.getMessage()); return -3; } catch (IllegalArgumentException e) { log.error("JWT claims is empty, 잘못된 JWT 토큰 입니다."); - log.error("관련에러: {}", e.getMessage()); + log.error(e.getMessage()); return -4; } } @@ -178,7 +183,7 @@ public Authentication createAuthentication(String id) { public void saveRefresh(long memberId, String accessToken, String refreshToken) { refreshRepository.save( - Refresh.of(memberId, accessToken, refreshToken, REFRESH_TOKEN_TIME / 1000)); + Refresh.of(memberId, accessToken, refreshToken, refreshTokenTime / 1000)); } /* @@ -216,5 +221,4 @@ public void deleteToken(Member member) { refreshRepository.deleteById(member.getId()); } - } diff --git a/backEnd/src/main/java/com/quiz/ourclass/global/util/scheduler/SchedulingService.java b/backEnd/src/main/java/com/quiz/ourclass/global/util/scheduler/SchedulingService.java index 3748b22c..dc769ae5 100644 --- a/backEnd/src/main/java/com/quiz/ourclass/global/util/scheduler/SchedulingService.java +++ b/backEnd/src/main/java/com/quiz/ourclass/global/util/scheduler/SchedulingService.java @@ -46,7 +46,7 @@ public void scheduleTask(T target, Consumer action, LocalDateTime executi @Transactional @Scheduled(cron = "0 0 3 * * *") - protected void relationShipScore() { + public void relationShipScore() { List organizations = organizationRepository.findAll(); @@ -64,7 +64,7 @@ protected void relationShipScore() { @Transactional @Scheduled(cron = "0 15 3 * * *") - protected void isolationScore() { + public void isolationScore() { List organizations = organizationRepository.findAll(); for (Organization organization : organizations) {