-
Notifications
You must be signed in to change notification settings - Fork 1
[FEAT] #179: swagger 설정 추가 #192
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
WalkthroughSpring 설정과 패키지 구조를 Changes
Sequence Diagram(s)sequenceDiagram
autonumber
participant Client as 클라이언트
participant Security as SpringSecurity
participant Filter as TokenAuthenticationFilter
participant App as 애플리케이션
Client->>Security: HTTP 요청
activate Security
Security->>Filter: doFilterInternal(...)
alt shouldNotFilter = true (Swagger, /auth, /test, /game, /apply, /check, OPTIONS)
Filter-->>Security: 필터 건너뜀
Security->>App: 요청 전달
App-->>Client: 응답 반환
else shouldNotFilter = false
Filter->>Filter: JWT 추출 및 검증
Filter-->>Security: SecurityContext 설정 (성공 시)
Security->>App: 요청 전달
App-->>Client: 응답 (검증 실패 시 401)
end
deactivate Security
sequenceDiagram
autonumber
participant Caller as 호출자
participant S3Service as S3Service
participant S3Client as AWS_S3Client(v2)
participant S3 as AWS_S3
Caller->>S3Service: upload(file, userId, type)
S3Service->>S3Service: key = user/{userId}/{type}/{filename}
S3Service->>S3Client: putObject(PutObjectRequest, RequestBody.fromInputStream(...))
S3Client->>S3: 업로드 요청
S3-->>S3Client: 업로드 응답
S3Client-->>S3Service: 업로드 완료
S3Service->>S3Client: utilities().getUrl(GetUrlRequest{bucket,key})
S3Client-->>S3Service: 파일 URL
S3Service-->>Caller: 키 또는 URL 반환
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related issues
Possibly related PRs
Poem
✨ Finishing Touches
🧪 Generate unit tests
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
CodeRabbit Configuration File (
|
|
CI status |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (15)
src/main/java/inha/gdgoc/global/util/EncryptUtil.java (1)
12-33: 긴급: 비밀번호 해시 구현 보안 취약점 해결 (HmacSHA256 → BCrypt)
현재EncryptUtil.encrypt/generateHashedValue는 HmacSHA256에 랜덤 salt를 키로 사용해 단회성 해시만 수행합니다. 이 방식은 반복(iterations)이나 워크 팩터(work factor)가 없어 비밀번호 기반 KDF의 역할을 하지 못하며, 무차별 대입 공격에 취약합니다. 또한 반환 문자열에 salt를 포함하지 않아 호출자가 별도 저장·관리해야 하므로, salt 누락 시 인증 로직 불일치 위험이 큽니다.대체 제안
-package inha.gdgoc.global.util; -... -public class EncryptUtil { - public static String encrypt(String oldPassword, byte[] salt) throws NoSuchAlgorithmException, InvalidKeyException { - return generateHashedValue(oldPassword, salt); - } - - public static byte[] generateSalt() { - SecureRandom random = new SecureRandom(); - byte[] salt = new byte[16]; - random.nextBytes(salt); - return salt; - } - - public static String generateHashedValue(String oldPassword, byte[] salt) - throws NoSuchAlgorithmException, InvalidKeyException { - Mac mac = Mac.getInstance("HmacSHA256"); - SecretKeySpec secretKeySpec = new SecretKeySpec(salt, "HmacSHA256"); - mac.init(secretKeySpec); - - byte[] hashedBytes = mac.doFinal(oldPassword.getBytes()); - return Base64.getEncoder().encodeToString(hashedBytes); - } -} +package inha.gdgoc.global.util; + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +public class EncryptUtil { + private static final PasswordEncoder BCRYPT = new BCryptPasswordEncoder(); + + /** 비밀번호 해시 생성 — salt와 work factor 포함 */ + public static String hashPassword(String rawPassword) { + return BCRYPT.encode(rawPassword); + } + + /** 비밀번호 검증 */ + public static boolean matches(String rawPassword, String hashed) { + return BCRYPT.matches(rawPassword, hashed); + } +}수정이 필요한 호출부
- src/main/java/inha/gdgoc/domain/user/service/UserService.java
•generateSalt()+encrypt(raw, salt)→hashPassword(raw)- src/main/java/inha/gdgoc/domain/user/entity/User.java
•updatePassword메서드 내EncryptUtil.encrypt(password, this.salt)→EncryptUtil.hashPassword(password)
• 기존salt필드는 제거하거나 용도 재검토- src/main/java/inha/gdgoc/domain/auth/service/AuthService.java
• 로그인 검증 로직에서encrypt(raw, salt).equals(storedHash)→EncryptUtil.matches(raw, storedHash)마이그레이션 가이드(해시 저장/검증 패턴)와 회귀 테스트 템플릿이 필요하시면 언제든 요청해 주세요.
src/main/resources/application-local.yml (1)
51-55: 로깅 카테고리 오타: org.hibername.type → org.hibernate.type오타로 인해 type 바인딩 로깅 레벨이 적용되지 않습니다.
적용 diff:
- org.hibername.type: trace + org.hibernate.type: tracesrc/main/java/inha/gdgoc/global/util/CookieUtil.java (2)
11-17: 쿠키 보안 속성(HttpOnly, Secure, SameSite) 기본 미설정인증/세션/토큰 쿠키라면 최소한 HttpOnly와 Secure 설정이 필요합니다. SameSite도 정책적으로 정해야 CSRF/크로스사이트 이슈를 줄일 수 있습니다.
아래처럼 기본 보안 속성을 부여하세요(Lax는 일반 웹 앱의 기본값으로 무난).
public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) { - Cookie cookie = new Cookie(name, value); - cookie.setPath("/"); - cookie.setMaxAge(maxAge); - - response.addCookie(cookie); + Cookie cookie = new Cookie(name, value); + cookie.setPath("/"); + cookie.setMaxAge(maxAge); + cookie.setHttpOnly(true); + cookie.setSecure(true); // HTTPS 전제 + response.addCookie(cookie); + // SameSite는 표준 API가 없어 헤더로 추가하거나 ResponseCookie 사용을 고려 + // response.addHeader("Set-Cookie", String.format("%s=%s; Max-Age=%d; Path=/; HttpOnly; Secure; SameSite=Lax", name, value, maxAge)); }대안(가독성/제어 우수): org.springframework.http.ResponseCookie를 사용하면 SameSite를 명시적으로 설정할 수 있습니다. 필요 시 제안 드리겠습니다.
36-47: 필수 조치:CookieUtil.serialize및deserialize메서드 삭제 및 대체 구현 적용현재 코드베이스 전체에서 해당 유틸리티 메서드는 오직
CookieUtil.java내부에서만 선언되어 있을 뿐, 외부에서 호출되지 않으므로 삭제해도 기능 상 영향이 없습니다. 보안 취약점을 제거하기 위해 아래와 같이 조치해 주세요.
- 대상 파일:
src/main/java/inha/gdgoc/global/util/CookieUtil.java- 삭제할 메서드:
public static String serialize(Object obj) { … }(36–39행)public static <T> T deserialize(Cookie cookie, Class<T> cls) { … }(41–45행)- 대체 방안(예시): JSON 직렬화 + HMAC 서명 또는 JWT 활용
--- src/main/java/inha/gdgoc/global/util/CookieUtil.java +// 삭제된 serialize/deserialize 메서드 자리 +// JSON + HMAC 서명 방식 예시 +public static String encodeSignedJson(Object obj, String secret) throws JsonProcessingException { + String json = new ObjectMapper().writeValueAsString(obj); + String payload = Base64.getUrlEncoder().withoutPadding() + .encodeToString(json.getBytes(StandardCharsets.UTF_8)); + String sig = hmacSha256(payload, secret); + return payload + "." + sig; +} + +public static <T> T decodeSignedJson(String token, Class<T> cls, String secret) + throws IOException { + String[] parts = token.split("\\."); + if (parts.length != 2) throw new IllegalArgumentException("Invalid token format"); + if (!hmacSha256(parts[0], secret).equals(parts[1])) { + throw new SecurityException("Signature mismatch"); + } + String json = new String(Base64.getUrlDecoder() + .decode(parts[0]), StandardCharsets.UTF_8); + return new ObjectMapper().readValue(json, cls); +}위와 같이 네이티브 Java 직렬화를 제거하고, 안전하게 서명된 JSON 혹은 JWT 기반 저장 방식을 적용해 주세요.
src/main/java/inha/gdgoc/domain/auth/service/RefreshTokenService.java (5)
32-41: 민감정보(Refresh Token) 로그 유출 위험 — 토큰 전문 출력 제거 필수.토큰 원문을 로그로 남기면 심각한 보안 이슈가 됩니다. 재사용/생성 여부만 로그로 남기고 토큰 값은 제거해 주세요.
- if (refreshToken.getExpiryDate().isAfter(LocalDateTime.now())) { - log.info("유효한 Refresh Token이 존재합니다. 재사용합니다: {}", refreshToken.getToken()); - return refreshToken.getToken(); - } + if (refreshToken.getExpiryDate().isAfter(LocalDateTime.now())) { + log.info("유효한 Refresh Token이 존재하여 재사용합니다."); + return refreshToken.getToken(); + }
43-51: 토큰 생성 로그에서도 원문 출력 제거 필요.- String newToken = tokenProvider.generateRefreshToken(user, duration, loginType); - log.info("새로운 Refresh Token 생성됨: {}", newToken); + String newToken = tokenProvider.generateRefreshToken(user, duration, loginType); + log.info("새로운 Refresh Token을 생성했습니다.");
53-61: 서비스 진입 로그에서 리프레시 토큰 원문 출력 제거.- log.info("리프레시 토큰 서비스 호출됨. 토큰: {}", refreshToken); + log.info("리프레시 토큰 서비스 호출됨.");
81-85: 불일치 시 DB 저장 토큰 원문 로그 제거.- if (!storedToken.getToken().equals(refreshToken)) { - log.info("DB에 저장된 토큰: {}", storedToken.getToken()); - throw new RuntimeException("리프레시 토큰이 일치하지 않습니다."); - } + if (!storedToken.getToken().equals(refreshToken)) { + throw new RuntimeException("리프레시 토큰이 일치하지 않습니다."); + }
116-145: 민감한 토큰 로그 제거 및 만료 시간 UTC 처리코드 전반에서 민감한 토큰 값이 로그에 노출되고 있어 보안 취약점으로 이어질 수 있습니다. 또한 만료 시간 계산 시 서버 로컬 타임존을 사용하고 있어, 서버 타임존 변경 시 예상치 못한 오류가 발생할 수 있습니다. 아래 사항을 반드시 수정해주세요.
• 수정 대상 위치
- RefreshTokenService.getOrCreateRefreshToken():
• 기존 토큰 재사용 로그 (refreshToken.getToken())
• 신규 토큰 생성 로그 (newToken)- RefreshTokenService.refreshAccessToken():
• 서비스 호출 로그 (log.info(... refreshToken))- RefreshTokenService.saveRefreshToken():
• “Before update”/“After update” 로그 (tokenEntity.getToken())
• 신규 엔티티 생성 로그 (tokenEntity.getToken())
• 만료 시간 생성부 (LocalDateTime.now().plus(expiredAt))- AuthService:
• Response Cookie 로그 (refreshCookie.toString())• 제안하는 코드 변경 예시
--- a/src/main/java/inha/gdgoc/domain/auth/service/RefreshTokenService.java +++ b/src/main/java/inha/gdgoc/domain/auth/service/RefreshTokenService.java @@ private void saveRefreshToken(String refreshToken, User user, Duration expiredAt) { - // 1. 만료 시간 로컬 시간으로 설정 (KST) - LocalDateTime expiryDate = LocalDateTime.now().plus(expiredAt); + // 1. 만료 시간 UTC 기준으로 설정 + LocalDateTime expiryDate = LocalDateTime.now(ZoneOffset.UTC).plus(expiredAt); @@ if (existingToken.isPresent()) { - log.info("Before update: {}", tokenEntity.getToken()); + log.info("Refresh Token 갱신 시작"); // 기존 엔티티 업데이트 tokenEntity.update(refreshToken, expiryDate); - log.info("After update: {}", tokenEntity.getToken()); + log.info("Refresh Token 갱신 완료"); refreshTokenRepository.save(tokenEntity); return; @@ // 3. 없으면 새로운 엔티티 생성 - log.info("새로운 Refresh Token 생성: {}", tokenEntity.getToken()); + log.info("새로운 Refresh Token 엔티티 생성");--- a/src/main/java/inha/gdgoc/domain/auth/service/RefreshTokenService.java +++ b/src/main/java/inha/gdgoc/domain/auth/service/RefreshTokenService.java @@ public String getOrCreateRefreshToken(...) - if (refreshToken.getExpiryDate().isAfter(LocalDateTime.now())) { - log.info("유효한 Refresh Token이 존재합니다. 재사용합니다: {}", refreshToken.getToken()); + if (refreshToken.getExpiryDate().isAfter(LocalDateTime.now(ZoneOffset.UTC))) { + log.info("유효한 Refresh Token이 존재합니다. 재사용합니다"); return refreshToken.getToken(); } @@ public String refreshAccessToken(String refreshToken) { - log.info("리프레시 토큰 서비스 호출됨. 토큰: {}", refreshToken); + log.info("리프레시 토큰 서비스 호출됨");--- a/src/main/java/inha/gdgoc/domain/auth/service/AuthService.java +++ b/src/main/java/inha/gdgoc/domain/auth/service/AuthService.java @@ public void someAuthMethod(...) { - log.info("Response Cookie에 저장된 Refresh Token: {}", refreshCookie.toString()); + log.info("Response Cookie에 Refresh Token을 저장했습니다");• 민감 로그 일괄 점검 스크립트
#!/bin/bash # 로그에 민감 토큰/쿠키 노출 여부 점검 rg -nP --hidden "log\.(info|debug|warn|error).*\b(Token|getToken|toString|cookie)\b" -g '!**/build/**' -C2위 변경을 통해
- 로그에 토큰 값이 남지 않아 유출 위험을 제거
- 만료 시간 계산에 UTC 기준을 적용해 타임존 이슈 방지
를 보장할 수 있습니다.
src/main/java/inha/gdgoc/domain/user/service/UserService.java (1)
65-71: 비밀번호 암호화 로직 전면 교체 필요현재 HMAC-SHA256 단일 해시(
EncryptUtil) 방식은 오프라인 크래킹에 취약하므로, Spring Security의PasswordEncoder(예:BCryptPasswordEncoder)로 전환이 필수입니다. 프로젝트 내 이미PasswordEncoder빈이 등록되어 있으니 이를 주입해 사용하는 방향으로 수정하세요.수정이 필요한 주요 지점:
UserService.saveUser(src/main/java/inha/gdgoc/domain/user/service/UserService.java:65–71)
•generateSalt()/encrypt()제거 →passwordEncoder.encode()사용
•toEntity()시그니처에서 salt 파라미터 제거User.updatePassword(src/main/java/inha/gdgoc/domain/user/entity/User.java:118–122)
•EncryptUtil.encrypt(...)호출 제거 →passwordEncoder.encode()적용AuthService로그인 검증 로직 (src/main/java/inha/gdgoc/domain/auth/service/AuthService.java:132–136)
• 입력 비밀번호 해시 비교(EncryptUtil.encrypt) →passwordEncoder.matches()사용- 프로젝트 전반의
EncryptUtil의존 제거 및 관련 필드/테이블 칼럼(salt) 정리예시 변경 (
UserService):- public void saveUser(UserSignupRequest req) throws NoSuchAlgorithmException, InvalidKeyException { - byte[] salt = generateSalt(); - String hashed = encrypt(req.getPassword(), salt); - User user = req.toEntity(hashed, salt); - userRepository.save(user); - } + private final PasswordEncoder passwordEncoder; + + public void saveUser(UserSignupRequest req) { + String hashed = passwordEncoder.encode(req.getPassword()); + User user = req.toEntity(hashed); // salt 파라미터 제거 + userRepository.save(user); + }Action Required: 위 3개 위치를 포함해
EncryptUtil사용을 전부 찾아(rg -nP 'EncryptUtil|generateSalt\(|encrypt\(' -g '!**/build/**') PasswordEncoder 방식으로 일관되게 대체하고, 엔티티 필드 및 DTO, 리포지토리 마이그레이션을 수행해주세요. 데이터 마이그레이션(기존 사용자 비밀번호) 및 테스트 코드 수정도 잊지 마시기 바랍니다.src/main/java/inha/gdgoc/domain/user/entity/User.java (2)
32-32: 중복 @builder: Lombok 충돌로 컴파일 실패 가능클래스 레벨(@line 32)과 생성자(@line 87)에 동시에 @builder가 선언되어 있습니다. Lombok은 동일한 타입에 빌더 두 개를 기본 이름(builder)로 생성하려 해 충돌합니다. 하나만 유지하세요.
다음과 같이 클래스 레벨의 @builder를 제거하는 것을 권장합니다(생성자 빌더만 유지).
-@Builder +// @Builder (class-level) 제거: 생성자 빌더만 유지대안: 생성자 @builder에 builderMethodName, builderClassName을 지정해 공존시키는 방법도 있으나, 불필요한 복잡도를 야기할 수 있습니다.
Also applies to: 87-87
119-121: 비밀번호 해싱 방식 취약(HMAC-SHA256+salt는 비밀번호 해싱 아님)현재 EncryptUtil은 HMAC-SHA256을 salt로 키잉하여 Base64로 저장합니다. 이는 비밀번호 저장에 적합한 KDF(BCrypt/Argon2/PBKDF2)가 아니며, GPU 브루트포싱에 취약합니다. SecurityConfig에 BCryptPasswordEncoder 빈이 이미 있으므로 이를 사용하세요. 또한 엔티티에서 해싱을 수행하면 DI가 어려워 테스트/유지보수성이 떨어집니다. 해싱은 서비스 계층으로 이동하는 것이 바람직합니다.
권장 변경(엔티티는 “해시된 값”만 세팅):
- public void updatePassword(String password) throws NoSuchAlgorithmException, InvalidKeyException { - this.password = EncryptUtil.encrypt(password, this.salt); - } + public void updatePassword(String hashedPassword) { + this.password = hashedPassword; + }서비스 계층 예시(엔티티 외부):
// 예: UserService 혹은 AuthService 내 @RequiredArgsConstructor public class UserService { private final PasswordEncoder passwordEncoder; public void changePassword(User user, String rawPassword) { String hashed = passwordEncoder.encode(rawPassword); user.updatePassword(hashed); } }추가로, salt 필드(byte[])는 BCrypt 사용 시 불필요합니다(BCrypt가 내부적으로 salt 포함). 유지하려면 PBKDF2/Argon2로 전환 고려.
src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java (1)
68-73: JWT 'id' 클레임 파싱 시 Integer 강제 캐스팅: ClassCastException 위험Long으로 넣고(Integer로) 꺼내고 있습니다. JJWT는 숫자 클레임을 Integer/Long/Double 등으로 역직렬화할 수 있어, 안전하게 Number로 받아야 합니다.
- UserRole userRole = UserRole.valueOf(claims.get("role", String.class)); - Long userId = claims.get("id", Integer.class).longValue(); - String username = claims.getSubject(); + UserRole userRole = UserRole.valueOf(claims.get("role", String.class)); + Number idNumber = claims.get("id", Number.class); + if (idNumber == null) { + throw new IllegalArgumentException("JWT에 사용자 id(claim 'id')가 없습니다."); + } + Long userId = idNumber.longValue(); + String username = claims.getSubject();src/main/java/inha/gdgoc/domain/auth/service/AuthService.java (1)
125-137: 패스워드 검증 로직 교체 필요(HMAC → BCrypt) 및 체크 예외 전파 제거현재 HMAC 기반 비교는 취약합니다. BCryptPasswordEncoder.matches 사용으로 교체하고, 메서드 시그니처의 체크 예외(throws) 전파를 제거하세요.
- public LoginResponse loginWithPassword(UserLoginRequest userLoginRequest, - HttpServletResponse response) - throws NoSuchAlgorithmException, InvalidKeyException { + public LoginResponse loginWithPassword(UserLoginRequest userLoginRequest, + HttpServletResponse response) { @@ - String hashedInputPassword = encrypt(userLoginRequest.password(), foundUser.getSalt()); - if (!foundUser.getPassword().equals(hashedInputPassword)) { + if (!passwordEncoder.matches(userLoginRequest.password(), foundUser.getPassword())) { return new LoginResponse(false, null); }추가(클래스 필드/주입; 파일 상단 import 및 생성자 주입 필요):
import org.springframework.security.crypto.password.PasswordEncoder; @RequiredArgsConstructor public class AuthService { private final PasswordEncoder passwordEncoder; ... }src/main/java/inha/gdgoc/global/security/TokenAuthenticationFilter.java (1)
54-61: 액세스 토큰 전체 로그 노출: 보안 위험토큰 전체를 로그에 남기면 탈취 위험이 큽니다. 마스킹하세요.
- log.info("요청 URI: {}, 추출된 access token: {}", request.getRequestURI(), token); + String masked = (token != null && token.length() > 12) + ? token.substring(0, 6) + "..." + token.substring(token.length() - 4) + : "***"; + log.info("요청 URI: {}, access token: {}", request.getRequestURI(), masked);
🧹 Nitpick comments (26)
src/main/resources/application-prod.yml (3)
5-6: 프로덕션에서 .env 파일 import는 신중히—운영 환경 우발 로드/우선순위 리스크spring.config.import로 file:.env[.properties]를 프로덕션 프로파일에서도 로드하면, 배포 환경의 워킹 디렉터리에 남아 있던 .env가 우발적으로 적용될 수 있고(컨테이너/서버 재사용 시), 설정 우선순위가 application-prod.yml과 섞이며 예기치 못한 오버라이드가 발생할 수 있습니다. 운영 환경에서는 가능하면 시스템 환경변수/비밀 관리(Param Store/Secrets Manager/KMS)만 사용하고 .env import는 제거하는 것을 권장합니다.
다음과 같이 prod에서만 import를 제거하는 패치를 제안합니다:
spring: - config: - import: optional:file:.env[.properties] + config: {}
46-49: 운영에서 Hibernate SQL 로그 debug는 과도—성능/민감정보 노출 우려org.hibernate.SQL을 debug로 두면 쿼리 로그가 대량으로 쌓이고, 간접적으로 PII 유출 리스크가 있습니다(바인딩 파라미터는 off이나 SQL 자체가 메타데이터를 드러낼 수 있음). 운영에서는 info 이상을 권장합니다.
logging: level: - org.hibernate.SQL: debug + org.hibernate.SQL: info org.hibernate.type: off
62-71: cloud.aws 프로퍼티 참조 위치 확인 및 AWS SDK v2 대응 리팩토링 제안application-prod.yml의
cloud.aws.*프로퍼티는 아직 아래 위치에서 직접 참조되고 있어, 단순 제거 시 런타임 에러가 발생합니다. AWS SDK v2로 마이그레이션하며 Spring Cloud AWS 의존성을 제거할 계획이라면 다음 지점을 검토・수정하세요.
- src/main/java/inha/gdgoc/global/config/s3/S3Config.java
• 15행:@Value("${cloud.aws.region.static}")- src/main/java/inha/gdgoc/domain/resource/service/S3Service.java
• 21행:@Value("${cloud.aws.s3.bucket}")권장 리팩토링
- application-prod.yml에서 spring-cloud-aws 전용 프로퍼티를 제거하고 환경변수 직접 주입용 custom 프로퍼티로 대체
src/main/resources/application-prod.yml cloud:
- aws:
credentials:access-key: ${AWS_ACCESS_KEY_ID}secret-key: ${AWS_SECRET_ACCESS_KEY}region:static: ${AWS_REGION}s3:bucket: ${AWS_RESOURCE_BUCKET}
- awsRegion: ${AWS_REGION}
- s3Bucket: ${AWS_RESOURCE_BUCKET}
2. S3Config.java에서 spring-cloud-aws 자격 증명・리전 바인딩 대신 기본 체인 사용 ```diff @Bean - public Region awsRegion(@Value("${cloud.aws.region.static}") String region) { + public Region awsRegion(@Value("${AWS_REGION}") String region) { return Region.of(region); } @Bean - public S3Client s3Client(AwsCredentialsProvider provider, Region region) { - return S3Client.builder() - .credentialsProvider(provider) // spring-cloud-aws 공급자 + public S3Client s3Client(Region region) { + return S3Client.builder() + .credentialsProvider(DefaultCredentialsProvider.create()) .region(region) .build(); }
- S3Service.java에서 버킷명 주입 프로퍼티 이름 변경
- @value("${cloud.aws.s3.bucket}")
- @value("${AWS_RESOURCE_BUCKET}")
private String bucketName;이렇게 수정 후 기존
cloud.aws.*설정을 제거하시면
• Spring Cloud AWS 의존성 제거 가능
• AWS SDK v2 기본 체인(DefaultCredentialsProvider, 시스템 환경변수 기반 리전) 활용
• 설정 충돌 및 혼동 최소화
됩니다.src/main/java/inha/gdgoc/global/config/querydsl/QueryDslConfig.java (1)
9-17: 경미한 리팩터 제안: 생성자 주입 대신 메서드 파라미터 주입현재는 @requiredargsconstructor로 EntityManager를 필드 주입 중입니다. 구성 빈에 한해 메서드 파라미터 주입이 더 간결하며 순환참조 리스크도 줄입니다(기능 동일).
-@Configuration -@RequiredArgsConstructor -public class QueryDslConfig { - private final EntityManager entityManager; - - @Bean - public JPAQueryFactory jpaQueryFactory() { - return new JPAQueryFactory(entityManager); - } -} +@Configuration +public class QueryDslConfig { + @Bean + public JPAQueryFactory jpaQueryFactory(EntityManager entityManager) { + return new JPAQueryFactory(entityManager); + } +}src/main/java/inha/gdgoc/global/exception/NotFoundException.java (1)
6-11: @ResponseStatus 전파 vs 전역 예외 처리 일관성@ResponseStatus를 개별 예외에 부착하는 방식은 간단하지만, 글로벌 에러 포맷(예: RFC7807/ProblemDetail)과 혼용 시 응답 일관성이 깨질 수 있습니다. 이미 @ControllerAdvice 기반 전역 핸들러가 있다면 해당 핸들러로 통일하는 것을 고려해 주세요.
원하시면 전역 예외 응답 스펙(ProblemDetail)로 마이그레이션 초안 드리겠습니다.
src/main/resources/application-dev.yml (1)
62-71: 개발용 AWS 버킷/자격 증명 관리 점검dev 프로파일에서 AWS_TEST_RESOURCE_BUCKET을 분리해 둔 점 좋습니다. 다만 cloud.aws.* 사용 여부는 prod와 동일 이슈입니다(SDK v2로 전환 시 불필요할 수 있음). 또한 로컬 개발 시에는 로컬 스택/로컬 미니오 같은 대안을 고려하면 비용과 안전성에 유리합니다.
원하시면 local 프로파일용 MinIO 설정 스니펫 제공해 드립니다.
src/main/resources/application-local.yml (1)
61-70: *cloud.aws.credentials.는 3.x에서 사용되지 않습니다 → 혼란 방지 위해 제거 또는 주석 권장Aawspring-cloud 3.x + AWS SDK v2 조합에서는 cloud.aws.credentials.* 프로퍼티를 읽지 않습니다(현재 S3Config도 DefaultCredentialsProvider 사용). 남겨두면 “설정했는데 왜 안 되지?” 혼선을 줄 수 있습니다.
혼선을 방지하려면 아래처럼 credentials 블록을 제거(또는 주석)하세요. region, s3.bucket은 유지 가능합니다.
cloud: aws: - credentials: - access-key: ${AWS_ACCESS_KEY_ID} - secret-key: ${AWS_SECRET_ACCESS_KEY} region: static: ${AWS_REGION} s3: bucket: ${AWS_TEST_RESOURCE_BUCKET}또는 README에 “AWS 자격증명은 OS 환경변수/프로파일/Role을 사용” 명시도 좋습니다.
src/main/java/inha/gdgoc/global/util/CookieUtil.java (1)
19-34: 쿠키 삭제 시에도 속성 미러링 필요생성 시 설정한 Path/Domain/SameSite/HttpOnly/Secure 속성과 동일하게 맞춰 만료시켜야 브라우저가 정확히 삭제합니다. 현재 Path만 "/"로 강제하고 있어 일부 브라우저/도메인 조합에서 잔존 가능성이 있습니다.
public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String name) { Cookie[] cookies = request.getCookies(); if (cookies == null) { return; } for (Cookie cookie : cookies) { if (name.equals(cookie.getName())) { - cookie.setValue(""); - cookie.setPath("/"); - cookie.setMaxAge(0); - response.addCookie(cookie); + cookie.setValue(""); + cookie.setPath(cookie.getPath() == null ? "/" : cookie.getPath()); + // cookie.setDomain(knownDomain); // 가능하면 도메인도 동일하게 + cookie.setMaxAge(0); + cookie.setHttpOnly(true); + cookie.setSecure(true); + response.addCookie(cookie); } } }SameSite를 사용 중이라면 동일 헤더로 만료 헤더도 추가하세요.
build.gradle (3)
73-75: Swagger UI 의존성 추가는 OK, 운영 노출 정책을 함께 정의하세요운영/스테이징 노출이 불필요하다면 다음 중 하나를 적용해 주세요.
- OpenApiConfig에 @Profile({"local","dev"}) 또는 @ConditionalOnClass(GroupedOpenApi.class) 추가
- application-prod.yml에 springdoc.api-docs.enabled=false, springdoc.swagger-ui.enabled=false
예시(조건부 로딩):
@Configuration @ConditionalOnClass(org.springdoc.core.models.GroupedOpenApi.class) public class OpenApiConfig { // ... }
60-60: java-dotenv는 현재 용도와 맞지 않습니다 → 제거 권장이 PR에서 DotenvLoader를 삭제하고 Spring의 spring.config.import를 채택했으므로, 런타임 Dotenv 의존성은 잉여입니다. 유지하면 운영에서 .env 오사용을 유도할 수 있습니다.
적용 diff:
- // --- 환경변수(.env) --- - implementation 'io.github.cdimascio:java-dotenv:5.2.2'
31-71: ConfigurationProperties 메타데이터 생성기 추가 제안JwtProperties 등 @ConfigurationProperties를 사용하고 있으므로 IDE 보조와 오타 방지에 configuration-processor를 추가하는 것이 좋습니다.
적용 diff(의존성 블록 내):
// --- Lombok --- compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + // --- Spring Configuration Processor (IDE 메타데이터) --- + annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'src/main/java/inha/gdgoc/global/config/jwt/JwtProperties.java (1)
1-16: 패키지 리로케이션 OK, 프로퍼티 키와 일치합니다application-*.yml의 jwt.selfIssuer/googleIssuer/secretKey와 필드가 일치하며, @component + @ConfigurationProperties("jwt") 조합도 동작에 문제 없습니다.
- 불변성과 바인딩 안정성을 위해 record + 생성자 바인딩으로 전환을 고려해볼 수 있습니다(선택). 예시는 아래와 같습니다.
// 예시 @ConfigurationProperties(prefix = "jwt") public record JwtProperties(String selfIssuer, String googleIssuer, String secretKey) {}이 경우 @component 대신 @ConfigurationPropertiesScan 또는 @EnableConfigurationProperties(JwtProperties.class)가 필요합니다.
src/main/java/inha/gdgoc/global/common/ErrorResponse.java (1)
7-23: Lombok @Getter로 간소화 👍불변 필드 + 명시 생성자 조합에 @Getter 적용 깔끔합니다. 직렬화/응답 용도에 충분합니다.
- 필요 시 @JsonInclude(JsonInclude.Include.NON_NULL) 추가로 응답 깔끔화 가능.
- 추후 표준화(타임스탬프, path 등)가 필요하면 RFC7807(Problem Details)로 확장도 고려해볼 만합니다.
src/main/java/inha/gdgoc/global/config/s3/S3Config.java (1)
24-30: S3Client 자원 해제(destroyMethod)와 타임아웃/재시도 설정 권장.
S3Client는SdkAutoCloseable을 구현합니다. 컨테이너 종료 시 안전하게 소켓을 닫도록destroyMethod="close"지정을 권장합니다. 또한 기본 타임아웃은 무제한에 가깝기 때문에 운영 환경에서는 합리적 타임아웃/재시도 정책을 설정하는 편이 안전합니다.-@Bean +@Bean(destroyMethod = "close") public S3Client s3Client(Region region, AwsCredentialsProvider provider) { - return S3Client.builder() - .region(region) - .credentialsProvider(provider) - .build(); + return S3Client.builder() + .region(region) + .credentialsProvider(provider) + .overrideConfiguration(cfg -> cfg + .apiCallTimeout(java.time.Duration.ofSeconds(30)) + .apiCallAttemptTimeout(java.time.Duration.ofSeconds(10))) + .build(); }src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java (1)
32-47: 경로 재작성 시 '/api/v1/' 같은 트레일링 슬래시 케이스 보완 제안.현재는
p.equals(fullPrefix)만 루트 매핑으로 변환합니다. 실제 스프링 매핑에서 간혹/api/v1/가 생성될 수 있어 해당 케이스까지 함께 처리하면 안전합니다. 또한 서버 정보 설정 시 기존 Servers를 완전히 덮어쓰므로, 공용 서버 설정이 있는 경우에는 병합이 필요할 수 있습니다.- if (p.equals(fullPrefix)) p = "/"; - else if (p.startsWith(fullPrefix + "/")) p = p.substring(fullPrefix.length()); + if (p.equals(fullPrefix) || p.equals(fullPrefix + "/")) { + p = "/"; + } else if (p.startsWith(fullPrefix + "/")) { + p = p.substring(fullPrefix.length()); + }추가로, 제품명/버전/설명을 노출하려면
OpenAPIInfo설정을 별도 빈으로 구성하는 것도 고려해 주세요.src/main/java/inha/gdgoc/domain/resource/service/S3Service.java (2)
24-24: 파라미터 네이밍 일관성(nit): s3key → s3Key 권장.자바 관례상 카멜케이스(
s3Key)가 가독성에 유리합니다. 추후 API/호출처 영향 범위를 고려하여 리팩터링 시 반영을 권장합니다.
38-41: 사설 버킷이라면 getUrl()은 접근 불가 — Presigned URL 필요 여부 확인.
S3Client.utilities().getUrl(...)은 서명되지 않은 단순 URL입니다. 버킷/오브젝트가 퍼블릭이 아니라면 접근이 불가능합니다. 다운로드/직접 접근이 필요하다면S3Presigner를 사용해 서명 URL을 발급하는 방식을 권장합니다. 필요 시S3Presigner빈을 구성해 드리겠습니다.버킷 정책이 퍼블릭인지(또는 CloudFront 등을 통해 공개하는지) 확인해 주세요. 프리사인드 URL이 필요하다면 알려 주세요. 해당 메서드 대체 코드를 제공하겠습니다.
src/main/java/inha/gdgoc/domain/user/entity/User.java (1)
105-117: null 삽입 가능성: 연관관계 편의 메서드 가드 필요null 인자가 넘어오면 리스트에 null이 추가됩니다(이후 NPE 위험). addStudy, addStudyAttendee 모두 null 가드를 적용하세요.
- public void addStudy(Study study) { - this.studies.add(study); - if (study != null && study.getUser() != this) { - study.setUser(this); - } - } + public void addStudy(Study study) { + if (study == null) return; + this.studies.add(study); + if (study.getUser() != this) { + study.setUser(this); + } + }- public void addStudyAttendee(StudyAttendee studyAttendee) { - this.studyAttendees.add(studyAttendee); - if (studyAttendee != null && studyAttendee.getUser() != this) { - studyAttendee.setUser(this); - } - } + public void addStudyAttendee(StudyAttendee studyAttendee) { + if (studyAttendee == null) return; + this.studyAttendees.add(studyAttendee); + if (studyAttendee.getUser() != this) { + studyAttendee.setUser(this); + } + }src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java (1)
63-66: 불필요한 throws 선언validToken은 런타임 예외를 던지는 JJWT 파서를 래핑합니다. 메서드 시그니처의 체크 예외 throws 표기는 불필요하며 호출부에 과도한 예외 선언을 강제합니다.
- public Claims validToken(String token) throws ExpiredJwtException, UnsupportedJwtException, - MalformedJwtException, SignatureException, IllegalArgumentException { + public Claims validToken(String token) {src/main/java/inha/gdgoc/domain/auth/service/AuthService.java (2)
79-85: Google 사용자 정보 응답 NPE 방지 및 오류 처리
userInfoResponse.getBody()가 null이거나"email"키가 없으면 NPE가 발생합니다. 실패 응답(status 4xx/5xx) 처리와 null 가드를 추가하세요.ResponseEntity<Map> userInfoResponse = restTemplate.exchange( "https://www.googleapis.com/oauth2/v2/userinfo", HttpMethod.GET, userInfoRequest, Map.class ); - Map userInfo = userInfoResponse.getBody(); - String email = (String) userInfo.get("email"); - String name = (String) userInfo.get("name"); + if (!userInfoResponse.getStatusCode().is2xxSuccessful() || userInfoResponse.getBody() == null) { + throw new IllegalStateException("구글 사용자 정보 요청 실패: " + userInfoResponse.getStatusCode()); + } + Map userInfo = userInfoResponse.getBody(); + String email = (String) userInfo.getOrDefault("email", null); + String name = (String) userInfo.getOrDefault("name", null); + if (email == null) { + throw new IllegalStateException("구글 사용자 정보에 email이 없습니다."); + }Also applies to: 87-90
144-156: Refresh Token 쿠키에.domain(...)속성 일관적으로 적용 및 중복 제거 제안현재 Google OAuth 로그인(라인 106–109)과 자체 회원가입 로그인(라인 144–147) 모두
ResponseCookie.from("refresh_token", …)빌더에.domain(...)호출이 빠져 있습니다. 동일한 도메인 정책을 유지하려면 두 위치에 모두 도메인 설정을 추가하고, 중복 코드를 공용 메서드로 분리하는 것을 권장드립니다.
변경 위치
- src/main/java/inha/gdgoc/domain/auth/service/AuthService.java
• Google 로그인 처리부 (약 106행)
• 자체 로그인 처리부 (약 144행)제안하는 코드 예시
- ResponseCookie refreshCookie = ResponseCookie.from("refresh_token", refreshToken) - .httpOnly(true) - .secure(true) - .sameSite("None") + ResponseCookie refreshCookie = buildRefreshCookie(refreshToken); ... + // AuthService 클래스 내 공용 메서드 + private ResponseCookie buildRefreshCookie(String token) { + String domain = isProdProfile() ? ".gdgocinha.com" : null; + return ResponseCookie.from("refresh_token", token) + .httpOnly(true) + .secure(true) + .sameSite("None") + .domain(domain) // dev 환경에서는 null로 처리되어 header에 미포함 + .path("/") + .maxAge(Duration.ofDays(1)) + .build(); + }
- 로그 레벨 조정
- 보안 민감 정보를 남기지 않도록
.toString()직접 로깅은 지양하고, 디버그 전용 메시지로 변경 바랍니다:
log.debug("Refresh Token 쿠키 설정 완료");위 리팩토링을 통해
- 쿠키 설정 코드 중복 제거
- 프로파일별 도메인 분기 적용
- 민감 로그 노출 최소화
를 동시에 달성할 수 있습니다.src/main/java/inha/gdgoc/global/security/TokenAuthenticationFilter.java (2)
25-37: shouldNotFilter 범위 적절—SecurityConfig와 경로 목록 중복 관리 필요Swagger, auth, 기타 공개 엔드포인트를 필터에서 배제하는 접근은 적절합니다. 다만 SecurityConfig의 permitAll 목록과 경로가 중복 관리되고 있어 추후 불일치 위험이 있습니다(아래 추가 코멘트 참조).
45-51: 중복 예외 경로 로직 제거 제안(shouldNotFilter로 대체 가능)shouldNotFilter가 이미 /auth/** 등을 포괄합니다. doFilterInternal 내 skipPaths 검사는 중복이며 유지보수 비용만 증가합니다.
- List<String> skipPaths = List.of("/auth/refresh", "/auth/login", "/auth/oauth2/google/callback", - "/auth/signup", "/auth/findId", "/auth/password-reset/request", "/auth/password-reset/verify", - "/auth/password-reset/confirm"); - if (skipPaths.contains(uri)) { - filterChain.doFilter(request, response); - return; - } + // shouldNotFilter에서 공통 관리src/main/java/inha/gdgoc/global/security/SecurityConfig.java (3)
38-41: permitAll 경로와 TokenAuthenticationFilter.shouldNotFilter 경로 중복동일 경로 목록을 두 곳에서 관리 중입니다. 상수/설정으로 단일화하여 드리프트를 방지하세요. 예: @ConfigurationProperties 혹은 public static final Set ALLOWLIST.
47-49: 인증 실패 상태코드: 403 → 401 권장인증 부재/실패는 일반적으로 401 Unauthorized가 적합합니다. 403은 인증은 되었으나 권한이 부족한 경우에 사용합니다. 클라이언트 UX/리트라이 로직에도 영향이 있으므로 수정 권장.
- response.setStatus(HttpStatus.FORBIDDEN.value()); + response.setStatus(HttpStatus.UNAUTHORIZED.value());
68-73: CORS 설정과 Secure 쿠키 정책 정합성 점검 요청
src/main/java/inha/gdgoc/global/security/SecurityConfig.java (67–74행)
config.setAllowedOrigins에"http://gdgocinha.com"이 포함되어 있으나,
inha/gdgoc/domain/auth/service/AuthService.java에서 생성되는refresh_tokenResponseCookie는
secure=true&sameSite("None")으로 설정되어 있어 HTTPS 환경이 아니면 브라우저가 쿠키를 전송하지 않습니다.
→ 개발(dev) 환경에서는 HTTP로 동작할 수 있도록secure옵션을 프로파일별로 분기 처리하거나,
운영(prod) 환경에서는allowedOriginPatterns("https://*.gdgocinha.com")을 사용해 HTTPS 전용만 허용하도록 분리 검토 바랍니다.필요 시, 프론트엔드에서 읽어야 하는 커스텀 헤더가 있다면
config.setExposedHeaders(List.of("헤더명1", "헤더명2"))로 추가 설정하세요.
(쿠키 자체는 JS에서 접근 불가하므로 일반적으로 불필요)※ 위 지침은 75–77행에도 동일 적용됩니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (22)
build.gradle(3 hunks)src/main/java/inha/gdgoc/config/DotenvLoader.java(0 hunks)src/main/java/inha/gdgoc/config/S3Config.java(0 hunks)src/main/java/inha/gdgoc/domain/auth/service/AuthService.java(6 hunks)src/main/java/inha/gdgoc/domain/auth/service/RefreshTokenService.java(1 hunks)src/main/java/inha/gdgoc/domain/resource/service/S3Service.java(1 hunks)src/main/java/inha/gdgoc/domain/user/entity/User.java(1 hunks)src/main/java/inha/gdgoc/domain/user/service/UserService.java(2 hunks)src/main/java/inha/gdgoc/global/common/ErrorResponse.java(1 hunks)src/main/java/inha/gdgoc/global/config/jwt/JwtProperties.java(1 hunks)src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java(1 hunks)src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java(1 hunks)src/main/java/inha/gdgoc/global/config/querydsl/QueryDslConfig.java(1 hunks)src/main/java/inha/gdgoc/global/config/s3/S3Config.java(1 hunks)src/main/java/inha/gdgoc/global/exception/NotFoundException.java(1 hunks)src/main/java/inha/gdgoc/global/security/SecurityConfig.java(3 hunks)src/main/java/inha/gdgoc/global/security/TokenAuthenticationFilter.java(2 hunks)src/main/java/inha/gdgoc/global/util/CookieUtil.java(1 hunks)src/main/java/inha/gdgoc/global/util/EncryptUtil.java(1 hunks)src/main/resources/application-dev.yml(1 hunks)src/main/resources/application-local.yml(1 hunks)src/main/resources/application-prod.yml(1 hunks)
💤 Files with no reviewable changes (2)
- src/main/java/inha/gdgoc/config/DotenvLoader.java
- src/main/java/inha/gdgoc/config/S3Config.java
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: kaswhy
PR: GDGoCINHA/24-2_GDGoC_Server#180
File: gdgoc/build.gradle:52-54
Timestamp: 2025-08-23T03:40:02.707Z
Learning: In the gdgoc project using Spring Boot 3, the user kaswhy confirmed that spring-cloud-aws 2.2.6.RELEASE dependency is currently working for them despite known compatibility concerns with Jakarta EE migration.
📚 Learning: 2025-08-23T03:40:02.707Z
Learnt from: kaswhy
PR: GDGoCINHA/24-2_GDGoC_Server#180
File: gdgoc/build.gradle:52-54
Timestamp: 2025-08-23T03:40:02.707Z
Learning: In the gdgoc project using Spring Boot 3, the user kaswhy confirmed that spring-cloud-aws 2.2.6.RELEASE dependency is currently working for them despite known compatibility concerns with Jakarta EE migration.
Applied to files:
build.gradle
🧬 Code graph analysis (5)
src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java (1)
src/main/java/inha/gdgoc/global/security/SecurityConfig.java (1)
Configuration(23-88)
src/main/java/inha/gdgoc/domain/user/entity/User.java (1)
src/main/java/inha/gdgoc/global/util/EncryptUtil.java (1)
EncryptUtil(10-33)
src/main/java/inha/gdgoc/domain/user/service/UserService.java (1)
src/main/java/inha/gdgoc/global/util/EncryptUtil.java (1)
EncryptUtil(10-33)
src/main/java/inha/gdgoc/domain/auth/service/AuthService.java (3)
src/main/java/inha/gdgoc/global/util/EncryptUtil.java (1)
EncryptUtil(10-33)src/main/java/inha/gdgoc/domain/auth/service/RefreshTokenService.java (1)
Slf4j(18-146)src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java (1)
RequiredArgsConstructor(29-131)
src/main/java/inha/gdgoc/global/common/ErrorResponse.java (1)
src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java (1)
Getter(121-130)
🔇 Additional comments (16)
src/main/java/inha/gdgoc/global/config/querydsl/QueryDslConfig.java (1)
1-1: 확인 완료 — 컴포넌트 스캔에 문제가 없습니다
GdgocApplication이inha.gdgoc패키지에 위치하며@SpringBootApplication에 별도의scanBasePackages지정이 없어, 기본적으로inha.gdgoc하위 패키지 전체를 스캔합니다.
따라서inha.gdgoc.global.config.querydsl.QueryDslConfig도 자동으로 빈으로 등록되어 정상 동작합니다.src/main/java/inha/gdgoc/global/exception/NotFoundException.java (1)
1-1: 기존 import 경로 참조 없음 확인
rg 명령(rg -n -P '\binha\.gdgoc\.exception\.NotFoundException\b' --type=java) 결과,inha.gdgoc.exception.NotFoundException잔여 참조가 없습니다.src/main/resources/application-dev.yml (1)
46-49: 개발에서 SQL 로그 debug 유지 OK개발/디버깅에 유용하며, org.hibernate.type을 off로 둬 파라미터 노출을 막은 점도 적절합니다.
src/main/java/inha/gdgoc/global/util/EncryptUtil.java (1)
1-1: 패키지 이동 OK—그러나 내부 구현은 보안상 재검토 필요패키지 정리는 좋습니다. 다만 아래 메서드 구현이 비밀번호 해시 용도로는 안전하지 않습니다(자세한 코멘트는 하단 참조).
build.gradle (3)
77-81: AWS BOM 도입 적절3.1.1 BOM으로 awspring-cloud 계열의 버전 정합성 확보가 됩니다. 이후 추가 모듈(예: spring-cloud-aws-starter-sqs 등)도 버전 지정 없이 사용 가능해집니다.
53-53: AmazonS3 참조 제거 검증 완료rg를 통해
AmazonS3및com.amazonaws.services.s3패턴 검색 시 코드베이스에 일치 항목이 없음을 확인했습니다.
3-3: File: build.gradle
Lines: 3-3Snippet showing the final state of code at these lines
id 'org.springframework.boot' version '3.3.6'Comment
Spring Boot 3.3.6 고정: 로컬 호환성 검증 요청
현 조합(boot 3.3.6 + springdoc 2.6.0 + flyway 10.21.0 + awspring-cloud 3.1.1)은 일반적으로 호환됩니다. 다만,
JAVA_HOME설정 오류로 인해 의존성 트리 확인이 실패했습니다. 아래 환경 설정을 적용한 뒤 로컬 빌드 및 의존성 충돌 검증을 부탁드립니다:#!/bin/bash # 올바른 JDK 경로로 JAVA_HOME 재설정 export JAVA_HOME=/path/to/valid/jdk # 의존성 트리 확인 ./gradlew -q dependencies --configuration runtimeClasspath | sed -n '1,200p'src/main/java/inha/gdgoc/global/config/s3/S3Config.java (1)
11-12: AWS SDK v2 기반 S3 클라이언트 구성 도입, 방향성 좋습니다.v1 → v2 마이그레이션에 맞춰 구성 클래스를 분리한 점과 기본 자격 증명 공급자 체인을 사용하는 선택이 합리적입니다.
src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java (2)
14-22: 버전별 GroupedOpenApi 분리 구성 깔끔합니다.
/api/v1/**,/api/v2/**로 그룹을 나눠 운영/문서화 모두에 유용합니다. 보안 설정에/v3/api-docs/**,/swagger-ui/**가 이미 허용되어 있어 접근성도 확보되어 있습니다.
24-30: 커스터마이저 분리 구조 적절합니다.그룹 공통 로직을 헬퍼(
groupedApi)로 모아 중복을 줄인 점 좋습니다.src/main/java/inha/gdgoc/domain/auth/service/RefreshTokenService.java (1)
3-3: 패키지 경로 변경 일치 — import 정리 OK.
TokenProvider의 글로벌 패키지 이동에 맞춘 수정으로 보이며, 기능 영향은 없습니다.src/main/java/inha/gdgoc/domain/user/service/UserService.java (1)
3-5: 글로벌 유틸 import 경로 업데이트 적절합니다.src/main/java/inha/gdgoc/domain/user/entity/User.java (2)
7-7: EncryptUtil 패키지 이동 반영 OKimport 경로 변경만으로 런타임 동작에 영향 없음. 리팩토링 방향(utilities → global.util)과도 일치합니다.
67-68: salt 필드 초기화 경로 확인 완료: 문제 없음
UserService.saveUser(…)에서generateSalt()로 생성한 값을userSignupRequest.toEntity(hashedPassword, salt)로 넘겨.salt에 설정하고 있음을 확인했습니다. 따라서@Column(nullable = false)제약 조건을 만족하며, 현 구현에서salt가null이 되어updatePassword시 NPE가 발생할 가능성은 없습니다.src/main/java/inha/gdgoc/global/config/jwt/TokenProvider.java (1)
102-107: 서명 키 처리 개선 및 JJWT 버전 확인 필요build.gradle에서 현재
io.jsonwebtoken:jjwt:0.9.1를 사용 중이므로, 0.11+에서 제공하는parserBuilder및Keys.hmacShaKeyForAPI는 아직 적용할 수 없습니다. 다음 두 가지 방안 중 하나를 선택해 적용해주세요:• 현 버전(0.9.1) 유지
- Base64 재인코딩 제거
- 시크릿이 Base64 문자열인 경우:
byte[] keyBytes = Base64.getDecoder().decode(jwtProperties.getSecretKey());- 일반 시크릿 문자열인 경우:
byte[] keyBytes = jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8);SecretKeySpec으로Key객체 생성 후 사용- .signWith(SignatureAlgorithm.HS256, - Base64.getEncoder().encodeToString(jwtProperties.getSecretKey().getBytes()) - ) + byte[] keyBytes = /* Base64 디코드 or UTF-8 바이트 */; + Key key = new SecretKeySpec(keyBytes, SignatureAlgorithm.HS256.getJcaName()); + .signWith(SignatureAlgorithm.HS256, key)- return Jwts.parser() - .setSigningKey( - Base64.getEncoder().encodeToString(jwtProperties.getSecretKey().getBytes()) - ) - .parseClaimsJws(token) - .getBody(); + return Jwts.parser() + .setSigningKey(key) + .parseClaimsJws(token) + .getBody();- 추가 import:
import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets;• JJWT 0.11.x 이상으로 업그레이드
build.gradle의존성 수정:- implementation 'io.jsonwebtoken:jjwt:0.9.1' + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'- 다음과 같이 변경:
- .signWith(SignatureAlgorithm.HS256, /* byte[] or Key */) + .signWith(Keys.hmacShaKeyFor(jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8)), SignatureAlgorithm.HS256)- Jwts.parser()… + Jwts.parserBuilder() + .setSigningKey(Keys.hmacShaKeyFor(jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8))) + .build() + .parseClaimsJws(token) + .getBody();- 추가 import:
import io.jsonwebtoken.security.Keys; import java.nio.charset.StandardCharsets;위 방안 중 팀의 정책(의존성 업그레이드 가능 여부 등)에 맞춰 선택・적용해 주세요.
src/main/java/inha/gdgoc/global/security/SecurityConfig.java (1)
33-45: 전반적 보안 체인 구성은 적절
- CSRF 비활성화 + 세션 무상태 + 사용자 정의 JWT 필터 삽입 흐름 적절합니다.
- Swagger 경로 공개 설정도 기대 동작과 부합합니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java (2)
37-53: 프리픽스 제거 커스터마이저에 가드 추가(와일드카드·빈 프리픽스 시 조기 반환 + 서버 설정 안전장치)현재 구현은 어떤
fullPrefix든 서버를 강제로 덮어씁니다. 실수로 와일드카드나 빈 문자열을 넘기면 잘못된 서버 URL이 반영됩니다. 안전장치를 두면 회귀에 강해집니다.private OpenApiCustomizer stripPrefixAndSetServer(String fullPrefix) { return openApi -> { + // 방어 로직: 와일드카드 또는 비어있는 프리픽스는 리라이트/서버 설정을 건너뜀 + if (fullPrefix == null || fullPrefix.isBlank() || fullPrefix.contains("*")) { + return; + } Paths src = openApi.getPaths(); if (src == null || src.isEmpty()) return; Paths dst = new Paths(); src.forEach((path, item) -> { String p = path; if (p.equals(fullPrefix)) p = "/"; else if (p.startsWith(fullPrefix + "/")) p = p.substring(fullPrefix.length()); dst.addPathItem(p, item); }); openApi.setPaths(dst); openApi.setServers(List.of(new Server().url(fullPrefix))); }; }추가로, 프리픽스 제거 결과가 충돌(동일 키)하는 경우 마지막 항목으로 덮어쓰게 됩니다. 현재 요구사항에서는 가능성이 낮지만, 충돌 로깅이 필요하면
put전 존재 여부 체크 후 로그 남기는 것을 고려하세요.
11-12: OpenAPI 메타 정보(Info) 추가 고려UI 식별성을 위해 제목/설명/버전 정보를 설정하면 좋습니다. 예시:
// 추가 제안: 별도 Bean @Bean public io.swagger.v3.oas.models.OpenAPI baseOpenAPI() { return new io.swagger.v3.oas.models.OpenAPI() .info(new io.swagger.v3.oas.models.info.Info() .title("GDGoC Server API") .description("GDGoC 서비스의 공개 REST API 명세") .version("v1")); }원하시면 프로젝트 메타데이터(패키지명, build.gradle 버전 등)를 반영해 자동화되는 템플릿으로 드리겠습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java (1)
src/main/java/inha/gdgoc/global/security/SecurityConfig.java (1)
Configuration(23-88)
🔇 Additional comments (2)
src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java (2)
19-23: 버전 그룹 구성은 방향성 좋습니다
v1,v2그룹을 별도로 나누고, 프리픽스 제거 + 서버 설정으로 UI에서 버전별로 베이스 경로를 명확히 보여주는 접근은 합리적입니다. 위의 가드/매칭 보완만 반영하면 충분히 안정적으로 동작할 것 같습니다.Also applies to: 24-27
29-35: 경로 매핑 누락 없음 확인
/api/v1또는/api/v2를 루트에 직접 매핑한 컨트롤러가 존재하지 않음을 확인했습니다.
따라서pathsToMatch(fullPrefix + "/**")만으로도 모든 엔드포인트가 포함되며, 추가 수정은 불필요합니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 0
🧹 Nitpick comments (3)
src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java (3)
32-38: groupedApi 구성 깔끔합니다
pathsToMatch(fullPrefix + "/**")와 전용 커스터마이저 결합이 의도에 부합합니다. 현재 구조 기준으로 불필요한 스캔을 크게 줄일 필요까지는 없어 보이나, 추후 API 스펙이 커질 경우packagesToScan("inha.gdgoc")추가를 고려하면 초기화 시간이 더 짧아질 수 있습니다.
40-52: 경로 리라이트 엣지 케이스 보완 제안(트레일링 슬래시·중복 키 방지)입력 프리픽스가
/api/v1/처럼 슬래시로 끝나거나, 소스에/api/v1와/api/v1/가 동시에 존재할 때 현재 로직은"/"로 중복 맵핑되어 마지막 항목으로 덮어쓰기 될 수 있습니다. 프리픽스를 정규화하고"/"케이스를 명시적으로 처리하면 안전합니다.아래 패치 제안:
- private OpenApiCustomizer stripPrefixAndSetServer(String fullPrefix) { - return openApi -> { + private OpenApiCustomizer stripPrefixAndSetServer(String fullPrefix) { + return openApi -> { Paths src = openApi.getPaths(); if (src == null || src.isEmpty()) return; Paths dst = new Paths(); src.forEach((path, item) -> { - String p = path; - if (p.equals(fullPrefix)) p = "/"; - else if (p.startsWith(fullPrefix + "/")) p = p.substring(fullPrefix.length()); + String p = path; + // 프리픽스 정규화: 끝 슬래시 제거 + final String prefix = fullPrefix.endsWith("/") + ? fullPrefix.substring(0, fullPrefix.length() - 1) + : fullPrefix; + if (p.equals(prefix) || p.equals(prefix + "/")) { + p = "/"; + } else if (p.startsWith(prefix + "/")) { + p = p.substring(prefix.length()); + } dst.addPathItem(p, item); });
53-55: servers 강제 덮어쓰기는 환경에 따라 오작동 가능 — 비어있을 때만 설정하거나 컨텍스트패스를 포함하세요현재는
servers를 항상[ { url: fullPrefix } ]로 교체합니다. 컨텍스트패스(/app) 또는 프록시 프리픽스가 있을 경우 실제 호출 경로가 어긋날 수 있습니다. 최소한servers가 비어있을 때만 설정하도록 완화하면 기존 상위 설정을 존중할 수 있습니다.아래와 같이 조건부 설정으로 변경 제안:
- openApi.setServers(List.of(new Server().url(fullPrefix))); + if (openApi.getServers() == null || openApi.getServers().isEmpty()) { + openApi.setServers(List.of(new Server().url(fullPrefix))); + }또는 컨텍스트패스를 포함한 값으로 설정하려면(선택):
// 클래스 필드 추가 // @Value("${server.servlet.context-path:}") // private String contextPath; // setServers 시 openApi.setServers(List.of(new Server().url((contextPath == null ? "" : contextPath) + fullPrefix)));프록시 환경을 적극 지원하려면
ForwardedHeaderFilter적용 및 관련 설정(server.forward-headers-strategy=framework)도 검토해 주세요.원하시면 환경별(로컬/스테이징/운영) 동작을 표로 정리해 드리고, 가장 안전한 설정 조합을 PR에 반영하는 패치를 같이 올리겠습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (1)
src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java (1)
src/main/java/inha/gdgoc/global/security/SecurityConfig.java (1)
Configuration(23-88)
🔇 Additional comments (2)
src/main/java/inha/gdgoc/global/config/openapi/OpenApiConfig.java (2)
22-30: v1/v2 그룹 구성 방향 OK — 프록시/컨텍스트패스 환경에서의 동작 확인 필요스크립트 실행 결과 JSON 파싱 오류(parse error: Invalid numeric literal)가 발생해
.servers[0].url및.paths정보가 정상 출력되지 않았습니다. 애플리케이션이 실제 배포된 컨텍스트패스(예:/app)나 리버스 프록시(X-Forwarded-Prefix) 환경에서도 Swagger UI의 “Try it out” 요청 경로가 올바르게 매핑되는지, 다음 사항을 직접 확인해 주세요.• 확인 대상
http://{HOST}:{PORT}/v3/api-docs/v1및/v2엔드포인트 응답이 유효한 JSON인지- 응답 내
.servers[0].url값이 실제 프록시/컨텍스트패스 적용 후의 절대 URL을 반영하는지.paths하위 엔드포인트 목록이 기대하는 API 경로(예:/api/v1/...,/app/api/v1/...)로 나오는지• 예시 검증 스니펫
BASE=${1:-http://localhost:8080} for g in v1 v2; do echo "=== API 그룹: $g ===" curl -s "$BASE/v3/api-docs/$g" | jq . # 전체 JSON 확인 echo "servers[0].url:" curl -s "$BASE/v3/api-docs/$g" | jq -r '.servers[0].url' echo "paths 키 일부:" curl -s "$BASE/v3/api-docs/$g" | jq -r '.paths | keys[:5][]' echo done위 명령으로 반환된 URL과 경로가 실제 환경에서 올바르게 매핑되는지 개발/스테이징 배포 환경에서도 반드시 검증 부탁드립니다.
14-20: SpringDoc 엔드포인트 노출 확인 필요현재 curl 응답에 Express 헤더가 보이며 404가 반환되고 있어, Java Spring Boot 애플리케이션이 정상 구동된 서버(올바른 포트 및 context-path)인지 확인이 필요합니다.
• Spring Boot 애플리케이션이 실제 구동 중인 포트 확인 (application.yml/properties 또는 실행 로그 참고)
• 올바른 포트로/v3/api-docs및/v3/api-docs/all요청
• 요청 시servers필드가 빈 배열([])이거나 유효한 URL로만 노출되는지 확인로컬에서 확인하실 때는 아래 스크립트를 참고해 주세요.
#!/bin/bash # 1) 실제 포트 확인 (환경변수 또는 설정 파일 기반) PORT=${PORT:-8080} BASE="http://localhost:${PORT}" echo "=== /v3/api-docs 상태 & servers 필드 ===" curl -s -D - "$BASE/v3/api-docs" -o /dev/null | head -n 10 curl -s "$BASE/v3/api-docs" | jq '.servers' echo -e "\n=== /v3/api-docs/all 상태 & servers 필드 ===" curl -s -D - "$BASE/v3/api-docs/all" -o /dev/null | head -n 10 curl -s "$BASE/v3/api-docs/all" | jq '.servers'확인 후
servers출력값이 빈 배열 또는 유효한 서버 URL만 노출되는지 최종 검증 부탁드립니다.
📌 연관된 이슈
✨ 작업 내용
💬 리뷰 요구사항(선택)
Summary by CodeRabbit
New Features
Bug Fixes
Refactor
Chores