Conversation
|
Caution Review failedThe pull request is closed. Walkthrough소셜 로그인(Google/Kakao)과 JWT 기반 인증 도입: Feign 클라이언트 추가, Redis 리프레시 토큰 저장소 도입, 토큰 발급/검증/갱신/로그아웃/탈퇴 흐름 구현, 보안·CORS·Swagger·Redis 설정 및 User 도메인 재설계가 포함됩니다. Changes
Sequence Diagram(s)sequenceDiagram
participant Client
participant AuthCtrl as AuthController
participant LoginSvc as KakaoLoginService/GoogleLoginService
participant AuthClient as KakaoAuthClient/GoogleAuthClient
participant ApiClient as KakaoApiClient/GoogleApiClient
participant UserRepo as UserRepository
participant JWT as JwtTokenProvider
participant Redis as RedisRefreshTokenStore
Client->>AuthCtrl: POST /auth/oauth/kakao (code)
AuthCtrl->>LoginSvc: kakaoLogin(code)
LoginSvc->>AuthClient: getAccessToken(code)
AuthClient-->>LoginSvc: KakaoTokenResDto
LoginSvc->>ApiClient: getUserInfo(accessToken)
ApiClient-->>LoginSvc: KakaoUserInfoResDto
LoginSvc->>UserRepo: findByLoginTypeAndSocialId(...)
alt 신규 사용자
LoginSvc->>UserRepo: save(new User)
end
LoginSvc->>JWT: createAccessToken(userId)
JWT-->>LoginSvc: accessToken
LoginSvc->>JWT: createRefreshToken(userId)
JWT-->>LoginSvc: refreshToken
LoginSvc->>Redis: saveRefreshToken(userId, refreshToken, expireMs)
Redis-->>LoginSvc: OK
LoginSvc-->>AuthCtrl: AuthResDto
AuthCtrl-->>Client: DataResponse<AuthResDto>
sequenceDiagram
participant Client
participant AuthCtrl as AuthController
participant AuthSvc as AuthService
participant JWT as JwtTokenProvider
participant Redis as RedisRefreshTokenStore
Client->>AuthCtrl: POST /auth/token/refresh (refreshToken)
AuthCtrl->>AuthSvc: refreshAccessToken(refreshToken)
AuthSvc->>JWT: validateRefreshToken(token)
JWT-->>AuthSvc: valid
AuthSvc->>JWT: getUserId(token)
JWT-->>AuthSvc: userId
AuthSvc->>Redis: getRefreshToken(userId)
Redis-->>AuthSvc: storedToken
alt storedToken != provided
AuthSvc-->>AuthCtrl: throw STOLEN_REFRESH_TOKEN
else match
AuthSvc->>JWT: createAccessToken(userId)
JWT-->>AuthSvc: newAccessToken
AuthSvc->>JWT: createRefreshToken(userId)
JWT-->>AuthSvc: newRefreshToken
AuthSvc->>Redis: saveRefreshToken(userId, newRefreshToken, expireMs)
Redis-->>AuthSvc: OK
AuthSvc-->>AuthCtrl: TokenRefreshResDto
AuthCtrl-->>Client: DataResponse<TokenRefreshResDto>
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (5)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 16
🤖 Fix all issues with AI agents
In @build.gradle:
- Around line 53-56: Update the jjwt dependencies to the patched release by
changing the artifact versions for io.jsonwebtoken:jjwt-api,
io.jsonwebtoken:jjwt-impl, and io.jsonwebtoken:jjwt-jackson from 0.11.5 to
0.13.0 in build.gradle; after updating, run a build to ensure there are no API
or transitive dependency breaks and adjust any code using deprecated
signWith()/setSigningKey() overloads if compilation warnings/errors surface.
- Around line 50-51: The FeignClient dependency is pinned to an older patch
version; update the implementation coordinate
'org.springframework.cloud:spring-cloud-starter-openfeign:4.1.2' to the newer
patch release 4.1.6 to pick up security and bug fixes (replace the version
string in the implementation declaration).
In @docker-compose.yml:
- Around line 22-30: The redis service (container_name ongil-redis) is exposed
without authentication and maps host port 6379, which is insecure; update
docker-compose to require a password by adding an environment variable
REDIS_PASSWORD (populate it in .env), set the redis service environment to read
that value, and modify the service command (currently ["redis-server",
"--appendonly", "yes"]) to include the requirepass flag (e.g., --requirepass
"$REDIS_PASSWORD") or point to a conf file that uses requirepass; also avoid
exposing port 6379 publicly (remove or restrict the ports mapping or bind it to
127.0.0.1) and ensure the backend service is updated to use REDIS_PASSWORD for
authenticated connections.
In
@src/main/java/com/ongil/backend/domain/auth/client/google/GoogleAuthClient.java:
- Around line 9-18: The @PostMapping on GoogleAuthClient is missing the consumes
attribute causing incorrect Content-Type for token exchange; update the
@PostMapping annotation on the GoogleAuthClient interface (the method
getAccessToken) to include consumes =
MediaType.APPLICATION_FORM_URLENCODED_VALUE so the Feign request uses
application/x-www-form-urlencoded (same pattern as KakaoAuthClient).
In @src/main/java/com/ongil/backend/domain/auth/controller/AuthController.java:
- Around line 64-71: The logout endpoint can receive null userId because
SecurityConfig currently permits all /auth/**; update either SecurityConfig to
require authentication for "/auth/logout" and "/auth/withdraw" (remove them from
.permitAll()) so authenticated principal is guaranteed, or enforce non-null
principal at the controller level by changing the parameter handling in
AuthController.logout (and withdraw) to require a principal (e.g., use
@AuthenticationPrincipal(required = true) or explicitly check if userId is null
and return 403) and ensure AuthService.logout handles only non-null IDs; pick
one approach and apply consistently to both logout and withdraw.
In @src/main/java/com/ongil/backend/domain/auth/dto/response/AuthResDto.java:
- Around line 23-25: The field declaration uses the type name as the field name
(LoginType LoginType) which violates Java naming conventions; rename the field
to start with a lowercase identifier (e.g., LoginType loginType) in AuthResDto
and update all usages accordingly, including the mapping/conversion logic in
AuthConverter.java (adjust getter/setter references or constructor params) so
references to the old symbol are replaced with the new field name.
In @src/main/java/com/ongil/backend/domain/auth/service/GoogleLoginService.java:
- Around line 47-58: The code queries existsByLoginTypeAndSocialId before
calling findByLoginTypeAndSocialId/save, introducing a TOCTOU race that can
create duplicate users; change the flow in GoogleLoginService to call
findByLoginTypeAndSocialId(LoginType.GOOGLE, socialId) first, capture the
Optional result, determine isNewUser from optional.isEmpty(), and only call
userRepository.save(...) when the Optional is empty (use Optional.orElseGet or
manual ifEmpty save), removing the separate existsByLoginTypeAndSocialId check
so all decisions derive from the single lookup and avoid the race between exists
and save.
In @src/main/java/com/ongil/backend/domain/auth/service/KakaoLoginService.java:
- Around line 101-108: Both providers handle missing emails inconsistently;
unify by extracting the email with null-safety in a shared place and use it from
both services. Create or reuse a helper (e.g., a new static method
extractEmailFromSocial(...) or move the existing extractEmail from
KakaoLoginService to a shared util) that accepts provider identifier and user
info (or overloads for KakaoUserInfoResDto and GoogleUserInfoResDto), performs
null checks and returns either the real email or a synthesized one like
"{id}@{provider}.user"; then update GoogleLoginService to call that helper
instead of directly using userInfo.email(), and update KakaoLoginService to use
the same helper to ensure consistent behavior across providers.
In @src/main/java/com/ongil/backend/domain/user/entity/User.java:
- Around line 56-57: The addresses field in the User entity is initialized with
new ArrayList<>() but lacks @Builder.Default, so using Lombok's @Builder will
set it to null; update the User class by annotating the addresses field with
@Builder.Default (similar to the existing points field) so the builder preserves
the default empty list for addresses while keeping the @OneToMany(mappedBy =
"user") and the List<Address> addresses = new ArrayList<>() declaration.
In @src/main/java/com/ongil/backend/domain/user/repository/UserRepository.java:
- Around line 11-16: The repository methods findByLoginTypeAndSocialId and
existsByLoginTypeAndSocialId are fine, but you must add a composite index and a
unique constraint on the User entity for the loginType and socialId fields to
improve lookup performance and prevent duplicate social accounts; update the
User class @Table annotation to include an index named idx_login_type_social_id
with columnList "login_type,social_id" and a uniqueConstraints entry for
{"login_type","social_id"}, and verify the column names used match the @Column
mappings for the loginType and socialId fields.
In @src/main/java/com/ongil/backend/global/config/SecurityConfig.java:
- Line 35: SecurityConfig currently permits "/h2-console/**" for all profiles;
remove or conditionally restrict that exception. Edit the SecurityConfig class
to either delete the "/h2-console/**" entry from the requestMatchers permitAll()
chain, or annotate SecurityConfig with @Profile("local") or wrap the H2 matcher
addition in a profile check so "/h2-console/**" is only permitted in the local
profile; ensure the change affects the method building the HttpSecurity
requestMatchers configuration so other endpoints remain unchanged.
In @src/main/java/com/ongil/backend/global/config/SwaggerConfig.java:
- Around line 39-46: The Server instantiation in SwaggerConfig (the Server
server block that uses the profile variable) hardcodes environment URLs and uses
HTTP; move these URLs and descriptions into external config (e.g.,
application.yml under swagger.server.*) and read them into SwaggerConfig (via
@Value or Environment/ConfigurationProperties) instead of hardcoding; for
production/dev ensure the production URL uses HTTPS (e.g.,
https://api.ongil.com) and set server.setUrl(...) and server.setDescription(...)
from the injected configuration values so no IPs or protocols are hardcoded.
In
@src/main/java/com/ongil/backend/global/security/jwt/JwtAuthenticationFilter.java:
- Around line 43-47: In JwtAuthenticationFilter (e.g., inside doFilterInternal
or the method that builds Authentication), remove the duplicated
SecurityContextHolder.getContext().setAuthentication(auth) call so the
authentication is set only once; keep a single call to
SecurityContextHolder.getContext().setAuthentication(auth) immediately after
constructing the UsernamePasswordAuthenticationToken (variable auth) and delete
the redundant second invocation.
- Around line 26-27: The JwtAuthenticationFilter currently injects
UserRepository but never uses it; either remove the unused field and its
constructor parameter to clean up dependencies, or use UserRepository inside the
token validation flow (e.g., in doFilterInternal or wherever you
resolve/validate tokens via jwtTokenProvider) to fetch the user
(userRepository.findById or findByUsername) after validating the token and
reject the request if the user is missing; update the JwtAuthenticationFilter
constructor and fields accordingly when removing the dependency, or add the
lookup and null-handling logic where token-to-user mapping occurs.
In @src/main/resources/application-local.yml:
- Line 15: The application-local.yml currently sets "ddl-auto: create", which
drops and recreates all tables on each start; change the "ddl-auto" value in
application-local.yml from "create" to "update" for local development so schema
changes are applied without wiping data, and restrict "create-drop" (or
"create") to automated test profiles only; verify there are no
environment-specific overrides expecting "create" and update any CI/test profile
configuration to use "create-drop" if needed.
🧹 Nitpick comments (28)
src/main/java/com/ongil/backend/global/config/SwaggerConfig.java (1)
27-27: 다중 프로필 시나리오를 고려한 로직 개선 제안.현재 코드는 첫 번째 활성 프로필만 사용합니다. 일반적인 경우 문제없지만, 다중 프로필 환경에서 더 명확한 처리를 원한다면 특정 프로필 우선순위를 정의하거나
env.acceptsProfiles(Profiles.of("dev"))를 사용하는 것을 고려해보세요.♻️ 다중 프로필 우선순위 처리 예시
-String profile = env.getActiveProfiles().length > 0 ? env.getActiveProfiles()[0] : "local"; +String profile = "local"; +if (env.acceptsProfiles(Profiles.of("dev"))) { + profile = "dev"; +} else if (env.acceptsProfiles(Profiles.of("prod"))) { + profile = "prod"; +}src/main/resources/application.yml (3)
13-14: 환경 변수 패턴 일관성 개선 제안.다른 설정은
${ENV_VAR:default}패턴을 사용하지만, 포트는 하드코딩되어 있습니다. 일관성을 위해 환경 변수 패턴 적용을 권장합니다.♻️ 일관된 설정 패턴 제안
server: - port: 8080 + port: ${SERVER_PORT:8080}
22-34: 향후 OAuth 설정 통합 고려.PR 목표에 명시된 "OAuth2/OIDC 표준 추상화 및 공통화"를 고려할 때, 현재 Kakao/Google 개별 설정은 향후 공통
oauth2.provider구조로 리팩터링할 여지가 있습니다.🏗️ Spring Security OAuth2 표준 구조 참고
Spring Security OAuth2의 표준 구조를 참고하면 향후 확장성이 향상됩니다:
spring: security: oauth2: client: registration: kakao: client-id: ${KAKAO_CLIENT_ID} client-secret: ${KAKAO_CLIENT_SECRET} redirect-uri: ${KAKAO_REDIRECT_URI} authorization-grant-type: authorization_code scope: profile_nickname, account_email google: client-id: ${GOOGLE_CLIENT_ID} client-secret: ${GOOGLE_CLIENT_SECRET} redirect-uri: ${GOOGLE_REDIRECT_URI} scope: email, profile provider: kakao: authorization-uri: https://kauth.kakao.com/oauth/authorize token-uri: https://kauth.kakao.com/oauth/token user-info-uri: https://kapi.kakao.com/v2/user/me google: authorization-uri: https://accounts.google.com/o/oauth2/v2/auth token-uri: https://oauth2.googleapis.com/token user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo이 구조는 새로운 OAuth 프로바이더 추가 시 일관된 패턴을 제공합니다.
16-20: JWT 알고리즘은 이미 명시되어 있습니다.검토 결과,
JwtTokenProvider.java의 36, 46, 58번 줄에서SignatureAlgorithm.HS256이 명시적으로 지정되어 있습니다. JJWT 라이브러리는 알고리즘을 Java 코드에서 정의하는 방식으로 동작하므로, YAML에 알고리즘을 추가하는 것은 불필요합니다.다만 개선 사항으로,
@ConfigurationProperties를 통한 JWT_SECRET 길이 검증(HS256 최소 32바이트)을 추가하면 애플리케이션 시작 시점에 유효성을 명확히 할 수 있습니다:@ConfigurationProperties(prefix = "jwt") @Validated public class JwtProperties { @NotBlank @Size(min = 32, message = "JWT secret must be at least 32 characters") private String secret; // ... }현재 구현은 안전하지만, 위 검증을 추가하면 운영 편의성이 향상됩니다.
docker-compose.yml (1)
22-30: Redis 헬스체크 및 리소스 제한 추가를 권장합니다.프로덕션 안정성을 위해 다음 개선 사항을 고려하세요:
- 헬스체크: Redis가 정상 작동 중인지 확인하여
depends_on이 실제 준비 상태를 기다리도록 설정- 리소스 제한: 메모리 무제한 사용으로 인한 시스템 장애 방지
- 백업 전략: AOF 외에 RDB 스냅샷 병행 사용
♻️ Redis 헬스체크 및 리소스 제한 추가
redis: image: redis:7-alpine container_name: ongil-redis ports: - "127.0.0.1:6379:6379" restart: always volumes: - ./redis_data:/data command: ["redis-server", "--appendonly", "yes"] + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 3s + retries: 3 + start_period: 10s + deploy: + resources: + limits: + cpus: '0.5' + memory: 512M + reservations: + cpus: '0.25' + memory: 256M그리고 backend의
depends_on을 다음과 같이 수정:depends_on: - - redis + redis: + condition: service_healthysrc/main/java/com/ongil/backend/domain/auth/dto/response/GoogleUserInfoResDto.java (1)
1-8: LGTM! Google 사용자 정보 DTO가 적절하게 구성되었습니다.Java record를 사용한 불변 DTO 설계가 깔끔하며, Google OAuth2 userinfo 엔드포인트의 표준 응답 필드(
sub,name,picture)를 올바르게 매핑하고 있습니다.sub필드에 대한 주석도 유용합니다.선택 사항:
sub필드명이 OAuth2 표준이지만, 코드 가독성을 위해@JsonProperty를 사용하여 더 명확한 이름으로 매핑하는 것을 고려할 수 있습니다:♻️ 필드명 가독성 개선 (선택사항)
+import com.fasterxml.jackson.annotation.JsonProperty; + public record GoogleUserInfoResDto( + @JsonProperty("sub") - String sub, // 구글의 고유 식별자 (카카오의 id 역할) + String googleId, // 구글의 고유 식별자 (sub 클레임) String name, String email, String picture ) {}다만 현재 구조도 OAuth2 표준을 그대로 따르므로 충분히 명확합니다.
src/main/java/com/ongil/backend/domain/auth/dto/response/TokenRefreshResDto.java (1)
1-7: LGTM! 토큰 리프레시 응답 DTO가 적절하게 구성되었습니다.Java record를 사용한 불변 DTO가 Refresh Token Rotation(RTR) 구현에 적합하며, 새로운 액세스 토큰과 리프레시 토큰 쌍을 반환하는 구조가 명확합니다.
선택 사항: 클라이언트가 토큰 만료 시점을 알 수 있도록
expiresIn필드를 추가하는 것을 고려할 수 있습니다:♻️ 토큰 만료 정보 추가 (선택사항)
public record TokenRefreshResDto( String accessToken, - String refreshToken + String refreshToken, + Long expiresIn // 액세스 토큰 만료까지 남은 초 (초 단위) ) { }이를 통해 클라이언트가 토큰 갱신 시점을 더 효율적으로 관리할 수 있습니다. 다만 현재 구조도 기능적으로 충분합니다.
src/main/java/com/ongil/backend/domain/user/converter/UserConverter.java (2)
8-11: Lombok @UtilityClass 사용 시 static 키워드 중복
@UtilityClass는 모든 메서드를 자동으로 static으로 만들기 때문에 Line 11의public static선언이 중복됩니다. 명시적 선언을 제거하거나, 일관성을 위해 프로젝트 컨벤션에 따라 유지하세요.♻️ 선택적 리팩토링
- public static UserInfoResDto toUserInfoResDto(User user) { + public UserInfoResDto toUserInfoResDto(User user) {
11-23: Null 안정성 고려 권장User 객체나 개별 필드가 null일 가능성이 있다면 방어적 코딩을 고려하세요. 현재 구현은 User가 항상 유효한 상태라고 가정합니다. 도메인 규칙에 따라 fail-fast 전략이 의도된 것이라면 문제없지만, null 체크 추가를 검토해보세요.
♻️ Null 안정성 개선 예시
public static UserInfoResDto toUserInfoResDto(User user) { + if (user == null) { + throw new IllegalArgumentException("User cannot be null"); + } return UserInfoResDto.builder() .userId(user.getId())또는 Java Optional 활용:
public static UserInfoResDto toUserInfoResDto(User user) { return UserInfoResDto.builder() .userId(user.getId()) .name(user.getName()) .loginType(user.getLoginType()) .phone(Optional.ofNullable(user.getPhone()).orElse("")) // ... 필요한 필드에 대해 Optional 처리 .build(); }src/main/java/com/ongil/backend/domain/auth/dto/request/TokenRefreshReqDto.java (1)
6-10: @notblank 사용 권장Line 8의
@NotNull은 빈 문자열("")이나 공백 문자열을 허용합니다. 리프레시 토큰은 실제 값이 있어야 하므로@NotBlank를 사용하는 것이 더 적절합니다.♻️ 제안하는 변경
public record TokenRefreshReqDto( @Schema(description = "리프레시토큰") - @NotNull + @NotBlank(message = "리프레시 토큰은 필수입니다") String refreshToken ) {추가로 import 구문도 업데이트가 필요합니다:
-import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.NotBlank;src/main/java/com/ongil/backend/domain/auth/client/google/GoogleApiClient.java (1)
9-9: URL 외부화 고려 권장Google API URL이 하드코딩되어 있습니다. 프로덕션 환경에서는 문제없지만, 테스트나 다른 환경(예: 개발 서버, 모킹)을 위해
application.yml로 외부화하는 것을 고려하세요.♻️ 설정 외부화 예시
application.yml:google: api: url: https://www.googleapis.com코드 변경:
-@FeignClient(name = "googleApiClient", url = "https://www.googleapis.com") +@FeignClient(name = "googleApiClient", url = "${google.api.url}") public interface GoogleApiClient {src/main/java/com/ongil/backend/domain/auth/client/kakao/KakaoApiClient.java (1)
13-14: Authorization 헤더 형식을 문서화하는 것을 권장합니다.bearerToken 파라미터가 "Bearer {token}" 형식으로 전달되어야 하는지 JavaDoc이나 주석으로 명시하면 API 사용자가 더 명확하게 이해할 수 있습니다.
📝 문서화 제안
+ /** + * 카카오 사용자 정보를 조회합니다. + * @param bearerToken Bearer 토큰 형식의 인증 헤더 (예: "Bearer {access_token}") + * @return 카카오 사용자 정보 + */ @GetMapping("/v2/user/me") KakaoUserInfoResDto getUserInfo(@RequestHeader("Authorization") String bearerToken);src/main/java/com/ongil/backend/domain/auth/dto/response/KakaoUserInfoResDto.java (1)
5-17: 널 안정성 처리를 고려하세요.현재 모든 필드가 nullable하므로, 카카오 API가 부분적인 응답을 반환할 경우 NPE가 발생할 수 있습니다. 다운스트림 코드에서 null 체크를 수행하거나, 필수 필드에 대한 검증 로직을 추가하는 것을 권장합니다.
src/main/java/com/ongil/backend/domain/auth/client/kakao/KakaoAuthClient.java (2)
14-20: 파라미터 캡슐화를 고려하세요.5개의 개별 파라미터는 메서드 시그니처를 복잡하게 만들고 유지보수성을 저하시킵니다. Request DTO 객체로 캡슐화하면 가독성과 확장성이 향상됩니다.
♻️ 리팩토링 제안
별도 DTO 클래스 생성:
public record KakaoTokenRequest( @JsonProperty("grant_type") String grantType, @JsonProperty("client_id") String clientId, @JsonProperty("redirect_uri") String redirectUri, String code, @JsonProperty("client_secret") String clientSecret ) {}Feign 클라이언트 메서드 단순화:
@PostMapping(value = "/oauth/token", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) -KakaoTokenResDto getAccessToken( - @RequestParam("grant_type") String grantType, - @RequestParam("client_id") String clientId, - @RequestParam("redirect_uri") String redirectUri, - @RequestParam("code") String code, - @RequestParam("client_secret") String clientSecret -); +KakaoTokenResDto getAccessToken(@SpringQueryMap KakaoTokenRequest request);참고:
@SpringQueryMap은 객체 필드를 form 파라미터로 자동 매핑합니다.
13-20: Feign 로깅 활성화 시 clientSecret 노출 대비 인터셉터 추가를 검토하세요.현재 설정에서는 Feign 로깅이 활성화되지 않아 즉시 위험은 없습니다. 다만 향후 디버깅 목적으로
logging.level.feign=DEBUG설정이 적용되면@RequestParam으로 전달되는 clientSecret이 평문으로 로그에 기록될 수 있습니다. Spring의ClientHttpRequestInterceptor또는 Feign 요청 인터셉터를 통해 민감정보(client_secret, client_id 등)를 마스킹하는 필터를 추가하여 로깅 단계에서 보호하는 것을 권장합니다.src/main/java/com/ongil/backend/domain/auth/service/GoogleLoginService.java (1)
26-31: 변수명 네이밍 컨벤션 위반
googleclientId,googleclientSecret,googleredirectUri변수명이 Java camelCase 컨벤션을 따르지 않습니다.♻️ 제안된 수정
- @Value("${google.client-id}") - private String googleclientId; - @Value("${google.client-secret}") - private String googleclientSecret; - @Value("${google.redirect-uri}") - private String googleredirectUri; + @Value("${google.client-id}") + private String googleClientId; + @Value("${google.client-secret}") + private String googleClientSecret; + @Value("${google.redirect-uri}") + private String googleRedirectUri;src/main/java/com/ongil/backend/global/config/redis/RedisConfig.java (1)
19-22: 프로덕션 환경을 위한 Redis 설정 고려기본적인 Redis 연결 설정이 구현되어 있습니다. 프로덕션 환경에서는 다음 설정들을 고려하세요:
- 커넥션 풀링 (Lettuce connection pool)
- 비밀번호 인증
- SSL/TLS 연결
- Timeout 설정
현재 로컬 개발 환경용으로는 적합합니다.
src/main/java/com/ongil/backend/domain/auth/dto/response/AuthResDto.java (1)
31-33: snake_case 필드명 사용
expires_in이 snake_case를 사용하고 있습니다. OAuth2 스펙 호환을 위한 것이라면@JsonProperty를 사용하여 Java 필드는 camelCase로 유지하는 것이 좋습니다.♻️ 제안된 수정
+import com.fasterxml.jackson.annotation.JsonProperty; + @Schema(description = "액세스 토큰 만료 시간") @NotNull - Integer expires_in + @JsonProperty("expires_in") + Integer expiresInsrc/main/java/com/ongil/backend/domain/auth/service/AuthService.java (2)
17-18: 클래스 레벨@Transactional과 메서드 레벨 중복클래스에 이미
@Transactional이 선언되어 있으므로logout()(Line 48)과withdraw()(Line 53)의@Transactional어노테이션은 중복입니다. 제거해도 동일하게 동작합니다.♻️ 중복 어노테이션 제거
- @Transactional public void logout(Long userId) { refreshTokenStore.removeRefreshToken(String.valueOf(userId)); } - @Transactional public void withdraw(Long userId) { refreshTokenStore.removeRefreshToken(String.valueOf(userId)); userRepository.deleteById(userId); }
53-57:withdraw메서드의 사용자 존재 여부 검증 고려
userRepository.deleteById(userId)는 Spring Data JPA 3.x에서 존재하지 않는 ID에 대해 예외를 던지지 않습니다. 데이터 정합성을 위해 사용자 존재 여부를 먼저 확인하거나, 삭제 전 조회 후 삭제하는 패턴을 고려해 보세요.♻️ 사용자 존재 확인 후 삭제
public void withdraw(Long userId) { + if (!userRepository.existsById(userId)) { + throw new AppException(ErrorCode.USER_NOT_FOUND); + } refreshTokenStore.removeRefreshToken(String.valueOf(userId)); userRepository.deleteById(userId); }src/main/java/com/ongil/backend/global/security/jwt/JwtTokenProvider.java (2)
81-83:getUserId호출 시 사전 검증 필요
getUserId는 내부에서parseClaims를 호출하므로, 유효하지 않은 토큰에 대해 예외가 발생합니다. 현재 코드에서는validateToken또는validateRefreshToken이후에 호출되고 있어 문제가 없지만, 메서드 자체의 방어적 처리나 Javadoc에 사전 검증 요구사항을 명시하면 향후 오용을 방지할 수 있습니다.
62-70: 토큰 검증 실패 시 로깅 고려
validateToken에서 예외 발생 시 로그를 남기면 보안 모니터링 및 디버깅에 도움이 됩니다. 민감 정보(토큰 자체)는 로깅하지 않도록 주의하세요.♻️ 검증 실패 로깅 추가
+import lombok.extern.slf4j.Slf4j; + +@Slf4j @Component public class JwtTokenProvider { // ... public boolean validateToken(String token) { try { parseClaims(token); return true; } catch (Exception e) { + log.debug("Token validation failed: {}", e.getMessage()); return false; } }src/main/java/com/ongil/backend/domain/auth/service/KakaoLoginService.java (2)
51-62: 중복 DB 쿼리 최적화 가능
existsByLoginTypeAndSocialId와findByLoginTypeAndSocialId가 연속으로 호출되어 동일한 조건으로 DB를 두 번 조회합니다.findByLoginTypeAndSocialId만 사용하고Optional.isEmpty()로 신규 유저 여부를 판단하면 쿼리 1회로 줄일 수 있습니다.♻️ 단일 쿼리로 최적화
- // 신규 유저 확인 - boolean isNewUser = !userRepository.existsByLoginTypeAndSocialId(LoginType.KAKAO, socialId); - - User user = userRepository.findByLoginTypeAndSocialId(LoginType.KAKAO, socialId) - .orElseGet(() -> userRepository.save( + Optional<User> existingUser = userRepository.findByLoginTypeAndSocialId(LoginType.KAKAO, socialId); + boolean isNewUser = existingUser.isEmpty(); + + User user = existingUser.orElseGet(() -> userRepository.save( User.builder() .loginType(LoginType.KAKAO) .socialId(socialId) .email(extractEmail(userInfo)) .profileImg(extractProfileImg(userInfo)) .name(extractNickname(userInfo)) .build() ));
22-24:@Transactional어노테이션 추가 권장
kakaoLogin메서드에서 DB 쓰기 작업(userRepository.save)이 수행됩니다. Spring Data JPA가 암시적 트랜잭션을 제공하지만, 명시적인@Transactional선언으로 트랜잭션 경계를 명확히 하는 것이 좋습니다.♻️ @transactional 추가
@Service @RequiredArgsConstructor +@Transactional(readOnly = true) public class KakaoLoginService { // ... + @Transactional public AuthResDto kakaoLogin(String code) {src/main/java/com/ongil/backend/global/config/SecurityConfig.java (1)
32-38: 중복된permitAll규칙Line 34의
/auth/**가 이미/auth/oauth/kakao와/auth/token/refresh를 포함하므로 Line 36은 중복입니다. 중복 규칙을 제거하면 설정이 더 명확해집니다.♻️ 중복 규칙 제거
.authorizeHttpRequests(auth -> auth .requestMatchers("/ping").permitAll() .requestMatchers("/auth/**").permitAll() .requestMatchers("/h2-console/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll() - .requestMatchers("/auth/oauth/kakao", "/auth/token/refresh").permitAll() .anyRequest().authenticated() )src/main/java/com/ongil/backend/domain/user/entity/User.java (1)
26-31:loginType+socialId복합 유니크 제약조건 고려동일한 소셜 ID가 다른 로그인 타입에서 중복 사용되는 것을 방지하려면
loginType과socialId조합에 대한 유니크 제약조건이 필요합니다. 현재UserRepository.findByLoginTypeAndSocialId조회 로직과 일관성을 위해 DB 레벨에서도 보장하는 것이 좋습니다.♻️ 복합 유니크 제약조건 추가
@Entity -@Table(name = "users") +@Table(name = "users", uniqueConstraints = { + @UniqueConstraint(columnNames = {"login_type", "social_id"}) +}) @NoArgsConstructor(access = AccessLevel.PROTECTED)src/main/java/com/ongil/backend/domain/auth/controller/AuthController.java (1)
37-53: 소셜 로그인 엔드포인트 간 코드 중복을 고려해보세요.카카오와 구글 로그인 메서드가 동일한 구조를 따르고 있습니다. 향후 추가 소셜 로그인 제공자가 늘어날 경우, 공통 인터페이스나 전략 패턴을 도입하여 중복을 줄이는 것을 권장합니다.
PR 목표에서 언급된 "OAuth2/OIDC 표준 추상화" 개선 방향과 일치합니다.
♻️ 리팩토링 제안: 공통 인터페이스 도입
향후 다음과 같은 공통 인터페이스를 고려할 수 있습니다:
public interface SocialLoginService { AuthResDto login(String code); }그러면 컨트롤러를 다음과 같이 단순화할 수 있습니다:
@PostMapping("/oauth/{provider}") public ResponseEntity<DataResponse<AuthResDto>> socialLogin( @PathVariable String provider, @Valid @RequestParam("code") @NotBlank String code ) { SocialLoginService service = socialLoginServiceFactory.getService(provider); AuthResDto res = service.login(code); return ResponseEntity.ok(DataResponse.from(res)); }다만 현재 단계에서는 2개의 제공자만 있으므로, 향후 확장 시 고려하시면 됩니다.
src/main/java/com/ongil/backend/global/common/exception/ErrorCode.java (1)
20-23: 인증 에러 코드가 잘 정의되었으나, 보안 메시지 노출을 고려하세요.HTTP 상태 코드와 메시지가 적절하게 설정되어 있습니다. 다만
STOLEN_REFRESH_TOKEN의 "탈취된 토큰으로 의심됩니다" 메시지는 공격자에게 RTR 메커니즘에 대한 정보를 제공할 수 있습니다.현재 구현은 RTR 패턴에서 정상 사용자에게 명확한 안내를 제공하려는 의도로 보이며, 이는 타당한 설계 선택입니다. 다만 보안 정책에 따라 더 일반적인 메시지("인증에 실패했습니다. 다시 로그인해주세요.")로 변경하는 것도 고려할 수 있습니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (35)
build.gradledocker-compose.ymlsrc/main/java/com/ongil/backend/Application.javasrc/main/java/com/ongil/backend/domain/auth/client/google/GoogleApiClient.javasrc/main/java/com/ongil/backend/domain/auth/client/google/GoogleAuthClient.javasrc/main/java/com/ongil/backend/domain/auth/client/kakao/KakaoApiClient.javasrc/main/java/com/ongil/backend/domain/auth/client/kakao/KakaoAuthClient.javasrc/main/java/com/ongil/backend/domain/auth/controller/AuthController.javasrc/main/java/com/ongil/backend/domain/auth/converter/AuthConverter.javasrc/main/java/com/ongil/backend/domain/auth/dto/request/TokenRefreshReqDto.javasrc/main/java/com/ongil/backend/domain/auth/dto/response/AuthResDto.javasrc/main/java/com/ongil/backend/domain/auth/dto/response/GoogleTokenResDto.javasrc/main/java/com/ongil/backend/domain/auth/dto/response/GoogleUserInfoResDto.javasrc/main/java/com/ongil/backend/domain/auth/dto/response/KakaoTokenResDto.javasrc/main/java/com/ongil/backend/domain/auth/dto/response/KakaoUserInfoResDto.javasrc/main/java/com/ongil/backend/domain/auth/dto/response/TokenRefreshResDto.javasrc/main/java/com/ongil/backend/domain/auth/service/AuthService.javasrc/main/java/com/ongil/backend/domain/auth/service/GoogleLoginService.javasrc/main/java/com/ongil/backend/domain/auth/service/KakaoLoginService.javasrc/main/java/com/ongil/backend/domain/user/controller/UserController.javasrc/main/java/com/ongil/backend/domain/user/converter/UserConverter.javasrc/main/java/com/ongil/backend/domain/user/dto/response/UserInfoResDto.javasrc/main/java/com/ongil/backend/domain/user/entity/User.javasrc/main/java/com/ongil/backend/domain/user/enums/UserRole.javasrc/main/java/com/ongil/backend/domain/user/repository/UserRepository.javasrc/main/java/com/ongil/backend/domain/user/service/UserService.javasrc/main/java/com/ongil/backend/global/common/exception/ErrorCode.javasrc/main/java/com/ongil/backend/global/config/SecurityConfig.javasrc/main/java/com/ongil/backend/global/config/SwaggerConfig.javasrc/main/java/com/ongil/backend/global/config/redis/RedisConfig.javasrc/main/java/com/ongil/backend/global/config/redis/RedisRefreshTokenStore.javasrc/main/java/com/ongil/backend/global/security/jwt/JwtAuthenticationFilter.javasrc/main/java/com/ongil/backend/global/security/jwt/JwtTokenProvider.javasrc/main/resources/application-local.ymlsrc/main/resources/application.yml
💤 Files with no reviewable changes (1)
- src/main/java/com/ongil/backend/domain/user/enums/UserRole.java
🧰 Additional context used
📓 Path-based instructions (2)
src/main/java/**/*.java
⚙️ CodeRabbit configuration file
SOLID 원칙, 스프링 어노테이션, 의존성 주입 패턴, 예외 처리에 중점을 둔다
Files:
src/main/java/com/ongil/backend/domain/auth/client/kakao/KakaoApiClient.javasrc/main/java/com/ongil/backend/domain/auth/client/google/GoogleApiClient.javasrc/main/java/com/ongil/backend/domain/auth/dto/response/AuthResDto.javasrc/main/java/com/ongil/backend/global/config/redis/RedisConfig.javasrc/main/java/com/ongil/backend/domain/auth/service/GoogleLoginService.javasrc/main/java/com/ongil/backend/domain/user/converter/UserConverter.javasrc/main/java/com/ongil/backend/domain/auth/service/AuthService.javasrc/main/java/com/ongil/backend/domain/auth/client/google/GoogleAuthClient.javasrc/main/java/com/ongil/backend/global/security/jwt/JwtAuthenticationFilter.javasrc/main/java/com/ongil/backend/domain/auth/dto/response/KakaoUserInfoResDto.javasrc/main/java/com/ongil/backend/domain/user/dto/response/UserInfoResDto.javasrc/main/java/com/ongil/backend/domain/auth/converter/AuthConverter.javasrc/main/java/com/ongil/backend/domain/user/repository/UserRepository.javasrc/main/java/com/ongil/backend/domain/auth/client/kakao/KakaoAuthClient.javasrc/main/java/com/ongil/backend/domain/auth/dto/response/TokenRefreshResDto.javasrc/main/java/com/ongil/backend/domain/auth/service/KakaoLoginService.javasrc/main/java/com/ongil/backend/domain/auth/dto/response/GoogleTokenResDto.javasrc/main/java/com/ongil/backend/global/config/redis/RedisRefreshTokenStore.javasrc/main/java/com/ongil/backend/global/config/SwaggerConfig.javasrc/main/java/com/ongil/backend/domain/user/service/UserService.javasrc/main/java/com/ongil/backend/global/common/exception/ErrorCode.javasrc/main/java/com/ongil/backend/domain/auth/dto/response/GoogleUserInfoResDto.javasrc/main/java/com/ongil/backend/domain/user/controller/UserController.javasrc/main/java/com/ongil/backend/Application.javasrc/main/java/com/ongil/backend/domain/auth/controller/AuthController.javasrc/main/java/com/ongil/backend/domain/auth/dto/response/KakaoTokenResDto.javasrc/main/java/com/ongil/backend/domain/auth/dto/request/TokenRefreshReqDto.javasrc/main/java/com/ongil/backend/global/config/SecurityConfig.javasrc/main/java/com/ongil/backend/global/security/jwt/JwtTokenProvider.javasrc/main/java/com/ongil/backend/domain/user/entity/User.java
**/*.{yml,yaml,properties}
⚙️ CodeRabbit configuration file
스프링 설정, 보안 설정, DB 연결, 환경 설정을 검증한다
Files:
src/main/resources/application.ymlsrc/main/resources/application-local.ymldocker-compose.yml
🧬 Code graph analysis (5)
src/main/java/com/ongil/backend/global/security/jwt/JwtAuthenticationFilter.java (1)
src/main/java/com/ongil/backend/global/security/jwt/JwtTokenProvider.java (1)
Component(17-97)
src/main/java/com/ongil/backend/global/config/SwaggerConfig.java (1)
src/main/java/com/ongil/backend/global/config/SecurityConfig.java (1)
Configuration(16-45)
src/main/java/com/ongil/backend/domain/user/service/UserService.java (1)
src/main/java/com/ongil/backend/global/common/exception/EntityNotFoundException.java (1)
EntityNotFoundException(4-8)
src/main/java/com/ongil/backend/domain/user/controller/UserController.java (1)
src/main/java/com/ongil/backend/PingController.java (1)
RestController(6-13)
src/main/java/com/ongil/backend/global/security/jwt/JwtTokenProvider.java (1)
src/main/java/com/ongil/backend/global/security/jwt/JwtAuthenticationFilter.java (1)
Component(22-60)
🔇 Additional comments (25)
src/main/java/com/ongil/backend/global/config/SwaggerConfig.java (3)
19-23: 의존성 주입 패턴이 올바르게 적용되었습니다.
@RequiredArgsConstructor와Environment주입을 통해 프로필 기반 설정을 구현하는 접근 방식이 Spring 모범 사례를 따르고 있습니다.
29-37: JWT 보안 스키마가 올바르게 구성되었습니다.Bearer 토큰 방식의 JWT 인증이 Swagger UI에 적절히 통합되었으며,
SecurityRequirement를 통해 전역 보안 요구사항이 명확히 정의되었습니다. SecurityConfig의 JWT 필터와도 일관성이 있습니다.
60-65: 개발 편의성을 위한 좋은 설정입니다.
persistAuthorization을 활성화하여 Swagger UI에서 인증 정보가 유지되도록 설정한 것은 개발자 경험을 개선하는 실용적인 접근입니다.@Primary를 사용한 빈 오버라이드도 적절합니다.src/main/resources/application.yml (2)
22-27: Kakao OAuth 설정은 완전하며 정상 구현입니다.코드베이스를 검토한 결과, KakaoLoginService는 표준적인 OAuth2 토큰 교환 패턴을 따르고 있습니다:
authorization-uri는 필요하지 않음: 프론트엔드에서 카카오 인증 페이지로 리다이렉트하고 인가코드를 받은 후, 백엔드는 이 코드를 토큰으로 교환합니다.
KakaoAuthClient.getAccessToken()호출 시 하드코딩된 authorization-uri는 없습니다.scope는 프론트엔드 책임: OAuth2 스코프는 초기 인증 요청(프론트엔드)에서 요청되며, 백엔드의 토큰 교환 단계에서는 설정할 필요가 없습니다.
KakaoTokenResDto가 응답 스코프를 처리합니다.현재 YAML 설정(client-id, client-secret, redirect-uri, token-uri, user-info-uri)은 백엔드가 필요로 하는 모든 정보를 포함하고 있습니다.
Likely an incorrect or invalid review comment.
29-34: Google OAuth 설정은 의도된 설계이며 Kakao보다 더 깔끔한 구조입니다.현재 구현은 양쪽 모두 Feign 클라이언트에 URL을 하드코딩하는 동일한 패턴을 따릅니다:
GoogleAuthClient(url = "https://oauth2.googleapis.com")GoogleApiClient(url = "https://www.googleapis.com")KakaoAuthClient(url = "https://kauth.kakao.com")KakaoApiClient(url = "https://kapi.kakao.com")두 서비스 모두 yml에서
client-id,client-secret,redirect-uri만 읽고 사용합니다. Kakao의 ymltoken-uri,user-info-uri속성은 실제로 코드에서 사용되지 않으므로 redundant합니다.권한 부여 엔드포인트는 프론트엔드에서 처리하며, 토큰 및 사용자 정보는 고정된 Feign 클라이언트 URL을 통해 조회합니다. 추가 yml 속성은 불필요합니다.
src/main/java/com/ongil/backend/Application.java (1)
5-5: LGTM! OpenFeign 클라이언트 활성화가 올바르게 구성되었습니다.Google 및 Kakao OAuth Feign 클라이언트를 위한 Spring Cloud OpenFeign 지원이 적절하게 활성화되었습니다.
@EnableFeignClients가 별도의basePackages속성 없이 사용되어com.ongil.backend패키지 전체를 스캔하며, 이는 프로젝트 구조상 적절합니다.Also applies to: 10-10
src/main/java/com/ongil/backend/domain/auth/dto/response/GoogleTokenResDto.java (1)
5-10: LGTM! 표준 OAuth2 토큰 응답 DTO 구조Google OAuth2 토큰 응답 필드를 올바르게 매핑하고 있으며, record를 사용한 불변 DTO 패턴이 적절합니다.
src/main/java/com/ongil/backend/domain/auth/dto/response/KakaoTokenResDto.java (1)
5-12: LGTM! Kakao OAuth2 토큰 응답 구조 정확Kakao OAuth2 토큰 응답의 모든 필드를 올바르게 매핑하고 있으며, record를 활용한 불변 DTO 패턴이 적절합니다. refreshToken 및 scope 필드 포함으로 Kakao 토큰 갱신 플로우를 지원할 수 있습니다.
src/main/java/com/ongil/backend/domain/auth/client/google/GoogleApiClient.java (1)
11-12: Authorization 헤더 형식 검증 - 현재 올바르게 구현됨GoogleApiClient.getUserInfo() 메서드는 Authorization 헤더를 String으로 받으며, GoogleLoginService에서 호출 시 "Bearer " + accessToken 형식으로 올바르게 전달되고 있습니다. 현재 구현이 OAuth2 Bearer 토큰 형식을 준수하고 있으므로 추가 조치는 불필요합니다.
src/main/java/com/ongil/backend/domain/user/service/UserService.java (1)
14-23: 구현이 깔끔하고 올바릅니다.사용자 정보 조회 로직이 명확하며, Spring 표준 패턴을 잘 따르고 있습니다:
- 의존성 주입이 적절하게 구성됨
- 엔티티 미존재 시 명시적 예외 처리
- 단일 책임 원칙 준수
src/main/java/com/ongil/backend/domain/auth/service/GoogleLoginService.java (1)
41-70: 전체 로그인 플로우 구현 확인Google OAuth 토큰 교환, 사용자 정보 조회, JWT 발급, Redis 저장 플로우가 잘 구현되어 있습니다.
@Transactional적용으로 데이터 일관성도 보장됩니다.src/main/java/com/ongil/backend/global/security/jwt/JwtAuthenticationFilter.java (1)
37-48: 삭제된 사용자에 대한 토큰 검증 고려현재 JWT 토큰의 서명과 만료만 검증하고 있습니다. 사용자가 탈퇴(Hard Delete)한 경우에도 토큰이 만료되기 전까지 유효한 것으로 처리됩니다.
PR 목표에 명시된 대로 탈퇴 시 Redis 토큰을 삭제하지만, Access Token은 Redis에 저장되지 않으므로 여전히 유효할 수 있습니다.
보안 요구사항에 따라 DB 또는 Redis에서 사용자/세션 유효성을 추가 검증하는 것을 고려하세요.
src/main/java/com/ongil/backend/domain/user/dto/response/UserInfoResDto.java (1)
8-37: DTO 구조 적절함Record를 사용한 불변 DTO 설계와 Swagger
@Schema어노테이션을 통한 API 문서화가 잘 되어 있습니다.
phone필드가 포함되어 있으므로, 로깅 시 PII(개인식별정보) 마스킹 처리가 필요할 수 있습니다.src/main/java/com/ongil/backend/domain/auth/converter/AuthConverter.java (1)
11-21: Converter 유틸리티 클래스 구현 적절함
@UtilityClass를 사용한 정적 유틸리티 패턴이 잘 적용되었습니다. 단일 책임 원칙(SRP)을 준수하며, User 엔티티에서 AuthResDto로의 변환 로직이 명확합니다.Line 17의
.LoginType(...)호출은 AuthResDto의 필드명 수정 시 함께 변경이 필요합니다.src/main/java/com/ongil/backend/domain/user/controller/UserController.java (2)
35-42: 인가(Authorization) 검증 누락 가능성
GET /user/{userId}엔드포인트에서 현재 로그인한 사용자가 해당userId의 정보에 접근할 권한이 있는지 확인하지 않습니다. 모든 인증된 사용자가 다른 사용자의 정보를 조회할 수 있습니다.의도된 동작(공개 프로필)인지 확인이 필요합니다. 그렇지 않다면 본인 또는 관리자만 접근하도록 제한해야 합니다.
🔒 권한 검증 추가 예시
@GetMapping("/{userId}") @Operation(summary = "특정 사용자 정보 조회 API", description = "ID를 통해 특정 사용자의 정보를 조회") public ResponseEntity<DataResponse<UserInfoResDto>> getUserInfo( + @AuthenticationPrincipal Long currentUserId, @PathVariable(name = "userId") Long userId ) { + if (!currentUserId.equals(userId)) { + throw new ForbiddenException("본인의 정보만 조회할 수 있습니다."); + } UserInfoResDto res = userService.getUserInfo(userId); return ResponseEntity.ok(DataResponse.from(res)); }
26-33: 내 정보 조회 API 구현 적절함
@AuthenticationPrincipal을 통해 현재 로그인한 사용자 ID를 안전하게 추출하고 있습니다./me패턴을 사용한 RESTful 설계가 적절합니다.src/main/java/com/ongil/backend/global/config/redis/RedisRefreshTokenStore.java (1)
10-41: LGTM! Redis 토큰 저장소 구현이 깔끔합니다.
@RequiredArgsConstructor를 통한 생성자 주입이 적절합니다.- 키 프리픽스(
RT:)를 상수로 분리하여 관리성이 좋습니다.- TTL 설정이 올바르게 적용되어 토큰 만료 관리가 자동화됩니다.
src/main/java/com/ongil/backend/domain/auth/service/AuthService.java (1)
24-46: RTR(Refresh Token Rotation) 구현이 적절합니다.
- 토큰 검증 → Redis 저장값 비교 → 새 토큰 세트 발급 → Redis 업데이트 흐름이 정확합니다.
- 토큰 탈취 감지 시 기존 토큰을 즉시 무효화하는 보안 패턴이 잘 적용되었습니다.
src/main/java/com/ongil/backend/global/config/SecurityConfig.java (1)
24-44: 전반적으로 적절한 JWT 기반 보안 설정입니다.
- Stateless 세션 관리, CSRF 비활성화, JWT 필터 통합이 올바르게 구성되었습니다.
- 프로덕션 배포 시
cors(Customizer.withDefaults())를 명시적인 CORS 정책으로 교체하는 것을 권장합니다.src/main/java/com/ongil/backend/domain/user/entity/User.java (1)
13-19: 엔티티 구조 개선이 적절합니다.
@Builder(toBuilder = true)로 부분 업데이트가 용이합니다.LoginTypeenum으로 소셜 로그인 타입 관리가 명확해졌습니다.- JPA 어노테이션과 Lombok 조합이 적절합니다.
src/main/java/com/ongil/backend/global/security/jwt/JwtTokenProvider.java (1)
46-47:SignatureAlgorithm.HS256사용은 현재 버전에서 문제없습니다프로젝트가 jjwt 0.11.5를 사용 중이므로
SignatureAlgorithm.HS256은 deprecated 상태가 아닙니다. 이 버전에서는 표준 API이며, deprecation은 0.12.0 이상에서만 적용됩니다. 현재 코드는 사용 중인 라이브러리 버전에 맞게 올바르게 작성되었습니다.Likely an incorrect or invalid review comment.
src/main/java/com/ongil/backend/domain/auth/controller/AuthController.java (3)
27-35: 의존성 주입과 클래스 구조가 잘 설계되었습니다.생성자 주입 패�ン과
@RequiredArgsConstructor를 활용하여 SOLID의 의존성 역전 원칙을 준수하고 있으며, 각 서비스별로 책임이 명확하게 분리되어 있습니다.
55-62: 토큰 리프레시 엔드포인트가 적절하게 구현되었습니다.요청 바디를 통한 리프레시 토큰 전달과 검증이 올바르게 설정되어 있으며, RTR(Refresh Token Rotation) 패턴과 잘 맞습니다.
73-80: 회원 탈퇴 엔드포인트의 HTTP 메서드 선택이 적절합니다.리소스 삭제에 대한 DELETE 메서드 사용이 RESTful 원칙에 부합하며, Hard Delete 방식도 PR 목표에 명시된 대로 구현되었습니다.
다만 로그아웃과 동일하게,
userId가 null일 경우에 대한 안전성 검증이 필요합니다.src/main/java/com/ongil/backend/global/common/exception/ErrorCode.java (1)
26-26: 사용자 에러 코드가 적절하게 추가되었습니다.HTTP 404 상태와 명확한 메시지로 사용자 미존재 시나리오를 잘 표현하고 있습니다.
| redis: | ||
| image: redis:7-alpine | ||
| container_name: ongil-redis | ||
| ports: | ||
| - "6379:6379" | ||
| restart: always | ||
| volumes: | ||
| - ./redis_data:/data | ||
| command: ["redis-server", "--appendonly", "yes"] No newline at end of file |
There was a problem hiding this comment.
Redis 인증이 구성되지 않아 보안 위험이 있습니다.
Redis 서비스가 비밀번호 없이 구성되어 있으며, 포트 6379가 호스트에 직접 노출되어 있습니다. 이는 프로덕션 환경에서 심각한 보안 위험을 초래합니다:
- 인증 없이 누구나 Redis에 접근 가능
- 리프레시 토큰과 같은 민감한 세션 데이터가 노출될 수 있음
- 외부 공격자가 Redis 명령어를 실행할 수 있음
🔐 Redis 인증 및 보안 강화 설정
redis:
image: redis:7-alpine
container_name: ongil-redis
ports:
- - "6379:6379"
+ - "127.0.0.1:6379:6379" # 로컬호스트만 접근 가능하도록 제한
restart: always
volumes:
- ./redis_data:/data
- command: ["redis-server", "--appendonly", "yes"]
+ command:
+ - redis-server
+ - --appendonly yes
+ - --requirepass ${REDIS_PASSWORD}
+ - --maxmemory 256mb
+ - --maxmemory-policy allkeys-lru그리고 backend 서비스에 Redis 비밀번호 환경 변수를 추가하세요:
environment:
SPRING_PROFILES_ACTIVE: prod
PROD_DB_URL: ${PROD_DB_URL}
PROD_DB_USERNAME: ${PROD_DB_USERNAME}
PROD_DB_PASSWORD: ${PROD_DB_PASSWORD}
SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_PORT: 6379
+ SPRING_DATA_REDIS_PASSWORD: ${REDIS_PASSWORD}.env 파일에 REDIS_PASSWORD 추가:
REDIS_PASSWORD=your_secure_password_here
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| redis: | |
| image: redis:7-alpine | |
| container_name: ongil-redis | |
| ports: | |
| - "6379:6379" | |
| restart: always | |
| volumes: | |
| - ./redis_data:/data | |
| command: ["redis-server", "--appendonly", "yes"] | |
| redis: | |
| image: redis:7-alpine | |
| container_name: ongil-redis | |
| ports: | |
| - "127.0.0.1:6379:6379" # 로컬호스트만 접근 가능하도록 제한 | |
| restart: always | |
| volumes: | |
| - ./redis_data:/data | |
| command: | |
| - redis-server | |
| - --appendonly yes | |
| - --requirepass ${REDIS_PASSWORD} | |
| - --maxmemory 256mb | |
| - --maxmemory-policy allkeys-lru |
🤖 Prompt for AI Agents
In @docker-compose.yml around lines 22 - 30, The redis service (container_name
ongil-redis) is exposed without authentication and maps host port 6379, which is
insecure; update docker-compose to require a password by adding an environment
variable REDIS_PASSWORD (populate it in .env), set the redis service environment
to read that value, and modify the service command (currently ["redis-server",
"--appendonly", "yes"]) to include the requirepass flag (e.g., --requirepass
"$REDIS_PASSWORD") or point to a conf file that uses requirepass; also avoid
exposing port 6379 publicly (remove or restrict the ports mapping or bind it to
127.0.0.1) and ensure the backend service is updated to use REDIS_PASSWORD for
authenticated connections.
src/main/java/com/ongil/backend/domain/auth/client/google/GoogleAuthClient.java
Show resolved
Hide resolved
src/main/java/com/ongil/backend/domain/auth/controller/AuthController.java
Show resolved
Hide resolved
| Server server = new Server(); | ||
| if ("dev".equals(profile)) { | ||
| server.setUrl("http://3.38.199.67:8080"); | ||
| server.setDescription("운영 서버"); | ||
| } else { | ||
| server.setUrl("http://localhost:8080"); | ||
| server.setDescription("로컬 서버"); | ||
| } |
There was a problem hiding this comment.
서버 URL을 외부 설정으로 이동하고 HTTPS를 사용하세요.
두 가지 중요한 문제가 있습니다:
- 하드코딩된 서버 IP: Line 41의
3.38.199.67:8080은 코드에 직접 포함되어 있어 서버 변경 시 재배포가 필요합니다. - HTTP 프로토콜 사용: 운영 환경에서 HTTP를 사용하면 JWT 토큰이 평문으로 전송되어 중간자 공격(MITM)에 취약합니다.
🔒 권장 개선사항
application.yml에 서버 URL 정의:
swagger:
server:
dev:
url: https://api.ongil.com
description: 운영 서버
local:
url: http://localhost:8080
description: 로컬 서버SwaggerConfig.java 수정:
@Configuration
@RequiredArgsConstructor
public class SwaggerConfig {
private final Environment env;
+
+ @Value("${swagger.server.${spring.profiles.active:local}.url}")
+ private String serverUrl;
+
+ @Value("${swagger.server.${spring.profiles.active:local}.description}")
+ private String serverDescription;
@Bean
public OpenAPI openAPI() {
- String profile = env.getActiveProfiles().length > 0 ? env.getActiveProfiles()[0] : "local";
-
// ... SecurityScheme 설정 ...
Server server = new Server();
- if ("dev".equals(profile)) {
- server.setUrl("http://3.38.199.67:8080");
- server.setDescription("운영 서버");
- } else {
- server.setUrl("http://localhost:8080");
- server.setDescription("로컬 서버");
- }
+ server.setUrl(serverUrl);
+ server.setDescription(serverDescription);
return new OpenAPI()
// ...
}
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In @src/main/java/com/ongil/backend/global/config/SwaggerConfig.java around
lines 39 - 46, The Server instantiation in SwaggerConfig (the Server server
block that uses the profile variable) hardcodes environment URLs and uses HTTP;
move these URLs and descriptions into external config (e.g., application.yml
under swagger.server.*) and read them into SwaggerConfig (via @Value or
Environment/ConfigurationProperties) instead of hardcoding; for production/dev
ensure the production URL uses HTTPS (e.g., https://api.ongil.com) and set
server.setUrl(...) and server.setDescription(...) from the injected
configuration values so no IPs or protocols are hardcoded.
src/main/java/com/ongil/backend/global/security/jwt/JwtAuthenticationFilter.java
Outdated
Show resolved
Hide resolved
src/main/java/com/ongil/backend/global/security/jwt/JwtAuthenticationFilter.java
Show resolved
Hide resolved
| jpa: | ||
| hibernate: | ||
| ddl-auto: update | ||
| ddl-auto: create |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the application-local.yml file to see the actual content and context
cat -n src/main/resources/application-local.ymlRepository: IT-Cotato/12th-OnGil-BE
Length of output: 639
🏁 Script executed:
# Check git diff to see what was actually changed
git diff HEAD src/main/resources/application-local.yml 2>/dev/null || echo "No unstaged changes"
# Check git log to see recent changes to this file
git log --oneline -5 src/main/resources/application-local.yml
# Check the previous version of the file
git show HEAD:src/main/resources/application-local.yml | head -20Repository: IT-Cotato/12th-OnGil-BE
Length of output: 522
🏁 Script executed:
# Use git blame to see the history of line 15
git blame -L 15,15 src/main/resources/application-local.yml
# Check git log with patch to see when ddl-auto was changed
git log -p --follow -- src/main/resources/application-local.yml | grep -A 5 -B 5 "ddl-auto" | head -50Repository: IT-Cotato/12th-OnGil-BE
Length of output: 144
🏁 Script executed:
# Check the full git history of application-local.yml to see all changes
git log --all --pretty=format:"%h %s" -- src/main/resources/application-local.yml
# Look for previous versions that might have had `update`
git log -p --all -- src/main/resources/application-local.yml | grep -B 10 -A 10 "ddl-auto" | head -100Repository: IT-Cotato/12th-OnGil-BE
Length of output: 248
🏁 Script executed:
# Check the specific commit that modified application-local.yml
git show c600ae5:src/main/resources/application-local.yml 2>/dev/null || echo "File might not exist in that commit"
# Check the diff between c600ae5 and its parent
git show c600ae5 -- src/main/resources/application-local.ymlRepository: IT-Cotato/12th-OnGil-BE
Length of output: 1141
🏁 Script executed:
# Check the commit that changed ddl-auto from update to create
# Look at commits between c600ae5 and 337f9db
git log --oneline c600ae5..337f9db -- src/main/resources/application-local.yml
# Show the actual diff of the change
git show 337f9db -- src/main/resources/application-local.ymlRepository: IT-Cotato/12th-OnGil-BE
Length of output: 915
🏁 Script executed:
# Check what happened between c600ae5 and 337f9db - look for any intermediate commits
git log --oneline c600ae5..HEAD -- src/main/resources/application-local.yml
# Check if the file was deleted and recreated
git log --all --oneline --diff-filter=D -- src/main/resources/application-local.yml
# Get more context about the 337f9db commit
git show --stat 337f9dbRepository: IT-Cotato/12th-OnGil-BE
Length of output: 6384
🏁 Script executed:
# Check the exact state of the repository - is this in a PR or already committed?
git status
# Look at the git log between the two commits to understand the timeline
git log --oneline c600ae5..337f9db
# Check if there were any files deleted between c600ae5 and 337f9db
git diff --name-status c600ae5 337f9db | head -20Repository: IT-Cotato/12th-OnGil-BE
Length of output: 1438
로컬 환경에서 create 설정은 재시작마다 모든 데이터베이스 테이블을 삭제하고 재생성합니다.
ddl-auto: create는 애플리케이션 시작 시 기존 테이블을 전부 DROP 후 재생성합니다. 로컬 개발 환경에서는:
- 서버 재시작 시마다 축적된 테스트 데이터가 완전히 사라짐
- 상태 기반 시나리오 테스트 불가 (예: 소셜 로그인 후 토큰 갱신, 재가입 흐름)
- 디버깅 시 데이터 재현 어려움
- Redis 토큰과 DB 유저 정보 간 정합성 문제 발생 가능
로컬 개발에는 update를 사용하고, 자동화 테스트에만 create-drop을 사용하세요.
이 변경이 의도적인지 확인 필요합니다.
🤖 Prompt for AI Agents
In @src/main/resources/application-local.yml at line 15, The
application-local.yml currently sets "ddl-auto: create", which drops and
recreates all tables on each start; change the "ddl-auto" value in
application-local.yml from "create" to "update" for local development so schema
changes are applied without wiping data, and restrict "create-drop" (or
"create") to automated test profiles only; verify there are no
environment-specific overrides expecting "create" and update any CI/test profile
configuration to use "create-drop" if needed.
| data: | ||
| redis: | ||
| host: ${SPRING_DATA_REDIS_HOST:localhost} | ||
| port: ${SPRING_DATA_REDIS_PORT:6379} |
There was a problem hiding this comment.
Redis 보안 및 프로덕션 설정 누락.
Redis는 리프레시 토큰과 같은 민감 데이터를 저장하지만, 다음 필수 설정이 누락되었습니다:
- 인증:
password설정이 없어 프로덕션 환경에서 무단 접근 위험. - SSL/TLS: 암호화되지 않은 통신으로 토큰 탈취 가능.
- 타임아웃:
timeout,lettuce.pool.*설정 부재로 연결 지연 시 행(hang) 발생 가능.
🔒 프로덕션 대응 Redis 설정 제안
data:
redis:
host: ${SPRING_DATA_REDIS_HOST:localhost}
port: ${SPRING_DATA_REDIS_PORT:6379}
+ password: ${SPRING_DATA_REDIS_PASSWORD:}
+ ssl:
+ enabled: ${SPRING_DATA_REDIS_SSL_ENABLED:false}
+ timeout: 3000
+ lettuce:
+ pool:
+ max-active: 8
+ max-idle: 8
+ min-idle: 2
+ max-wait: -1ms📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| data: | |
| redis: | |
| host: ${SPRING_DATA_REDIS_HOST:localhost} | |
| port: ${SPRING_DATA_REDIS_PORT:6379} | |
| data: | |
| redis: | |
| host: ${SPRING_DATA_REDIS_HOST:localhost} | |
| port: ${SPRING_DATA_REDIS_PORT:6379} | |
| password: ${SPRING_DATA_REDIS_PASSWORD:} | |
| ssl: | |
| enabled: ${SPRING_DATA_REDIS_SSL_ENABLED:false} | |
| timeout: 3000 | |
| lettuce: | |
| pool: | |
| max-active: 8 | |
| max-idle: 8 | |
| min-idle: 2 | |
| max-wait: -1ms |
There was a problem hiding this comment.
Actionable comments posted: 8
🤖 Fix all issues with AI agents
In @build.gradle:
- Around line 27-29: The build uses ext.set('springCloudVersion', "2023.0.1")
which is incompatible with Spring Boot 3.3.5; update the Spring Cloud version by
changing the ext property (the set call for 'springCloudVersion') to "2023.0.2"
or a newer compatible release, ensuring any dependencyManagement import or
plugin that references springCloudVersion picks up the new value and then run a
build to verify compatibility.
In @src/main/java/com/ongil/backend/domain/auth/service/GoogleLoginService.java:
- Line 44: In GoogleLoginService, add a null check for userInfo.sub() before
assigning it to socialId: verify that userInfo and userInfo.sub() are non-null,
and if sub() is null handle it by throwing a clear exception or returning a
failed authentication result (e.g., IllegalArgumentException or a custom
AuthException) and log the condition; ensure the rest of the method uses the
validated socialId variable so no NPE or invalid socialId is persisted.
- Around line 41-57: The googleLogin method has a TOCTOU race between
existsByLoginTypeAndSocialId and the subsequent findByLoginTypeAndSocialId/save
which can cause duplicate-insert errors; annotate googleLogin with
@Transactional (or run it in a transactional boundary) and simplify logic to
attempt findByLoginTypeAndSocialId first, creating and saving the User only if
absent; additionally catch DataIntegrityViolationException around the save to
re-query findByLoginTypeAndSocialId and return the existing user if a
unique-constraint collision occurred, referencing googleLogin,
userRepository.findByLoginTypeAndSocialId, userRepository.save, and User.builder
in your changes.
In @src/main/java/com/ongil/backend/domain/auth/service/KakaoLoginService.java:
- Around line 41-62: kakaoLogin has the same TOCTOU race as GoogleLoginService:
using userRepository.existsByLoginTypeAndSocialId and then
findByLoginTypeAndSocialId + save can create a duplicate user under concurrency;
make kakaoLogin transactional (add @Transactional on kakaoLogin or the service
class) and replace the exists/find/save pattern with a single find-or-create
flow that handles duplicate-key exceptions (catch
DataIntegrityViolationException or the repository-specific exception thrown by
save and then refetch the existing user), mirroring the fix applied in
GoogleLoginService (use userRepository.findByLoginTypeAndSocialId().orElseGet(()
-> userRepository.save(...)) inside a @Transactional boundary and handle
concurrent save errors by re-querying).
- Line 48: In KakaoLoginService, the line that sets socialId by calling
userInfo.id().toString() can NPE if userInfo.id() is null; update the code
around userInfo.id() (used when computing socialId) to null-check the id before
calling toString(), e.g., handle null by throwing a clear exception or using a
safe fallback, and ensure socialId is only assigned from a non-null id value
with an explicit error path (include references to userInfo.id(), socialId, and
KakaoLoginService).
In @src/main/java/com/ongil/backend/domain/user/entity/User.java:
- Around line 64-66: The OneToMany mapping for the User.addresses field
currently uses cascade = CascadeType.ALL but omits orphanRemoval, so Address
entities removed from the addresses list are not deleted from the DB; update the
annotation on the addresses field in the User class (the @OneToMany mappedBy =
"user" declaration for the List<Address> addresses) to include orphanRemoval =
true if you want removed collection elements to be deleted, and ensure any
business code that removes Address objects from the list relies on this
behavior.
In @src/main/java/com/ongil/backend/global/config/SecurityConfig.java:
- Around line 34-40: The SecurityConfig currently permits "/h2-console/**" but
doesn't disable frame options required for the H2 console iframe; update the
SecurityConfig HttpSecurity configuration (the same method that builds the
authorizeHttpRequests chain) to disable frame options so the H2 Console can
render (use the headers().frameOptions().disable() call or the equivalent lambda
form in the HttpSecurity builder). Ensure this change is applied alongside the
existing authorizeHttpRequests block in SecurityConfig.
In
@src/main/java/com/ongil/backend/global/security/jwt/JwtAuthenticationFilter.java:
- Around line 33-44: The filter currently accepts any valid JWT (including
refresh tokens) because JwtAuthenticationFilter calls
jwtTokenProvider.validateToken(token); change the check to ensure only access
tokens are accepted by using an access-token specific validator (e.g.,
jwtTokenProvider.validateAccessToken(token)) or by explicitly rejecting tokens
whose "type" claim equals "refresh" before creating the Authentication; keep the
rest of the flow (getUserId, build UsernamePasswordAuthenticationToken, set
SecurityContext) unchanged so only access tokens can authenticate requests.
🧹 Nitpick comments (7)
src/main/java/com/ongil/backend/global/config/CorsConfig.java (1)
23-47: WebMvcConfigurer와 CorsConfigurationSource Bean의 중복 설정
addCorsMappings()와corsConfigurationSource()Bean이 동일한 CORS 정책을 중복 정의하고 있습니다. Spring Security를 사용할 경우, Security 필터 체인이 먼저 적용되어WebMvcConfigurer의 CORS 설정이 무시될 수 있습니다.
SecurityConfig에서corsConfigurationSourceBean을 사용하고 있으므로,addCorsMappings()메서드는 제거해도 됩니다.♻️ 중복 제거 제안
@Configuration -public class CorsConfig implements WebMvcConfigurer { +public class CorsConfig { private static final List<String> ALLOWED_ORIGINS = List.of( "https://ongil-fe.vercel.app", - "http://localhost:3000", - "http://3.38.199.67:8080", - "http://localhost:8080" + "http://localhost:3000" ); - @Override - public void addCorsMappings(CorsRegistry registry) { - registry.addMapping("/**") - .allowedOrigins(ALLOWED_ORIGINS.toArray(new String[0])) - .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH") - .allowedHeaders("*") - .exposedHeaders("Authorization", "accessToken", "refreshToken") - .allowCredentials(true) - .maxAge(3600); - } - // SecurityFilterChain에서 사용할 Bean @Bean public CorsConfigurationSource corsConfigurationSource() {src/main/java/com/ongil/backend/global/security/jwt/JwtAuthenticationFilter.java (1)
26-47: 인증 실패 시 로깅 추가 권장토큰 검증 실패 시 디버깅을 위한 로깅이 없어 운영 환경에서 문제 추적이 어려울 수 있습니다.
📝 로깅 추가 예시
+import lombok.extern.slf4j.Slf4j; + +@Slf4j @Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(...) { String token = resolveToken(request); if (token != null && jwtTokenProvider.validateToken(token)) { Long userId = jwtTokenProvider.getUserId(token); List<SimpleGrantedAuthority> authorities = List.of(new SimpleGrantedAuthority("ROLE_USER")); Authentication auth = new UsernamePasswordAuthenticationToken(userId, null, authorities); SecurityContextHolder.getContext().setAuthentication(auth); + log.debug("JWT 인증 성공: userId={}", userId); + } else if (token != null) { + log.debug("JWT 검증 실패: token={}", token.substring(0, Math.min(10, token.length())) + "..."); } filterChain.doFilter(request, response); }src/main/java/com/ongil/backend/domain/user/entity/User.java (1)
38-39: socialId 컬럼에 길이 제약 조건 추가를 권장합니다.
length = 100으로 제약되어 있지만,socialId는 길이 제약이 없습니다. OAuth 제공자별로 socialId 형식이 다를 수 있으므로 (예: Kakao는 숫자, Google은 21자리 문자열), 적절한 길이 제약을 추가하면 DB 스키마가 더 명확해집니다.♻️ 제안
- @Column(name = "social_id", nullable = false) + @Column(name = "social_id", nullable = false, length = 100) private String socialId;src/main/java/com/ongil/backend/domain/auth/service/GoogleLoginService.java (2)
26-31: 변수 네이밍 컨벤션 통일을 권장합니다.
googleclientId,googleclientSecret,googleredirectUri가 camelCase 컨벤션을 따르지 않습니다.googleClientId,googleClientSecret,googleRedirectUri로 변경하면 가독성이 향상됩니다.♻️ 제안
- @Value("${google.client-id}") - private String googleclientId; - @Value("${google.client-secret}") - private String googleclientSecret; - @Value("${google.redirect-uri}") - private String googleredirectUri; + @Value("${google.client-id}") + private String googleClientId; + @Value("${google.client-secret}") + private String googleClientSecret; + @Value("${google.redirect-uri}") + private String googleRedirectUri;
71-84: Feign Client 호출에 대한 예외 처리가 없습니다.
googleAuthClient.getAccessToken()및googleApiClient.getUserInfo()호출 시 네트워크 오류, 4xx/5xx 응답 등이 발생할 수 있습니다. 적절한 예외 처리 또는 Feign ErrorDecoder를 통한 처리가 필요합니다.Feign ErrorDecoder를 전역으로 설정하거나, 서비스 레벨에서
FeignException을 catch하여 의미 있는 예외로 변환하는 것을 권장합니다.src/main/java/com/ongil/backend/domain/auth/service/KakaoLoginService.java (2)
26-31: 변수 네이밍 컨벤션 통일을 권장합니다.
kakaoclientId,kakaoclientSecret,kakaoredirectUri가 camelCase 컨벤션을 따르지 않습니다.kakaoClientId,kakaoClientSecret,kakaoRedirectUri로 변경을 권장합니다.
1-115: GoogleLoginService와 KakaoLoginService의 코드 중복을 개선하는 것을 권장합니다.두 서비스의 로그인 플로우가 거의 동일합니다:
- OAuth 토큰 획득
- 사용자 정보 조회
- 신규 유저 확인 및 저장
- JWT 발급
- Redis에 refresh token 저장
- AuthResDto 반환
Template Method 패턴이나 Strategy 패턴을 적용하여 공통 로직을 추출하면 유지보수성이 향상됩니다. PR 목표에서 언급된 "OAuth2/OIDC 공통화 및 표준화" 계획과 연결됩니다.
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
.github/PULL_REQUEST_TEMPLATE.mdbuild.gradlesrc/main/java/com/ongil/backend/domain/auth/client/google/GoogleAuthClient.javasrc/main/java/com/ongil/backend/domain/auth/controller/AuthController.javasrc/main/java/com/ongil/backend/domain/auth/converter/AuthConverter.javasrc/main/java/com/ongil/backend/domain/auth/dto/response/AuthResDto.javasrc/main/java/com/ongil/backend/domain/auth/service/GoogleLoginService.javasrc/main/java/com/ongil/backend/domain/auth/service/KakaoLoginService.javasrc/main/java/com/ongil/backend/domain/user/entity/User.javasrc/main/java/com/ongil/backend/global/config/CorsConfig.javasrc/main/java/com/ongil/backend/global/config/SecurityConfig.javasrc/main/java/com/ongil/backend/global/security/jwt/JwtAuthenticationFilter.javasrc/main/java/com/ongil/backend/global/security/jwt/JwtTokenProvider.java
🚧 Files skipped from review as they are similar to previous changes (3)
- src/main/java/com/ongil/backend/domain/auth/client/google/GoogleAuthClient.java
- src/main/java/com/ongil/backend/global/security/jwt/JwtTokenProvider.java
- src/main/java/com/ongil/backend/domain/auth/dto/response/AuthResDto.java
🧰 Additional context used
📓 Path-based instructions (1)
src/main/java/**/*.java
⚙️ CodeRabbit configuration file
SOLID 원칙, 스프링 어노테이션, 의존성 주입 패턴, 예외 처리에 중점을 둔다
Files:
src/main/java/com/ongil/backend/domain/auth/converter/AuthConverter.javasrc/main/java/com/ongil/backend/domain/auth/service/GoogleLoginService.javasrc/main/java/com/ongil/backend/global/config/CorsConfig.javasrc/main/java/com/ongil/backend/global/security/jwt/JwtAuthenticationFilter.javasrc/main/java/com/ongil/backend/domain/auth/service/KakaoLoginService.javasrc/main/java/com/ongil/backend/domain/user/entity/User.javasrc/main/java/com/ongil/backend/domain/auth/controller/AuthController.javasrc/main/java/com/ongil/backend/global/config/SecurityConfig.java
🧬 Code graph analysis (4)
src/main/java/com/ongil/backend/global/config/CorsConfig.java (1)
src/main/java/com/ongil/backend/global/config/SecurityConfig.java (1)
Configuration(17-47)
src/main/java/com/ongil/backend/global/security/jwt/JwtAuthenticationFilter.java (1)
src/main/java/com/ongil/backend/global/security/jwt/JwtTokenProvider.java (1)
Component(17-96)
src/main/java/com/ongil/backend/domain/user/entity/User.java (2)
src/main/java/com/ongil/backend/domain/address/entity/Address.java (1)
Entity(12-61)src/main/java/com/ongil/backend/global/common/entity/BaseEntity.java (1)
Getter(14-26)
src/main/java/com/ongil/backend/global/config/SecurityConfig.java (1)
src/main/java/com/ongil/backend/global/config/CorsConfig.java (1)
Configuration(13-48)
🔇 Additional comments (9)
.github/PULL_REQUEST_TEMPLATE.md (1)
6-8: 👍 PR 템플릿 개선이 좋습니다.추가된 "🛠️ 추후 리팩토링 및 고도화 계획"과 "📸 스크린샷 (선택)" 섹션은 이번 PR처럼 복잡한 아키텍처 변경(OAuth 통신, Redis 토큰 저장소, 토큰 갱신 정책 등)을 문서화할 때 특히 유용할 것 같습니다. 변경사항이 명확하고 일관성 있게 구조화되었습니다.
src/main/java/com/ongil/backend/global/config/CorsConfig.java (1)
16-21: ALLOWED_ORIGINS에 백엔드 서버 IP가 포함된 것 같습니다.
http://3.38.199.67:8080과http://localhost:8080은 백엔드 서버 주소로 보입니다. CORS 설정은 프론트엔드 오리진만 허용하면 되므로 불필요한 항목일 수 있습니다. 의도된 설정인지 확인해 주세요.build.gradle (1)
31-64: 의존성 추가 확인 - 전반적으로 적절합니다.OpenFeign, JWT(jjwt 0.13.0), Redis 의존성이 적절히 추가되었습니다.
jjwt-impl과jjwt-jackson을runtimeOnly로 설정한 것도 올바른 구성입니다.src/main/java/com/ongil/backend/global/config/SecurityConfig.java (1)
25-46: 전반적인 Security 설정이 적절합니다.
- Stateless 세션 관리로 JWT 기반 인증에 적합
- CORS, CSRF, formLogin, httpBasic 설정이 REST API에 맞게 구성됨
- 인증 규칙 순서가 올바르게 설정됨 (specific rules before general)
- JWT 필터가 적절한 위치에 삽입됨
- DI를 통한 의존성 주입으로 SOLID 원칙 준수
src/main/java/com/ongil/backend/domain/auth/converter/AuthConverter.java (1)
8-21: LGTM! 유틸리티 클래스 구조가 적절합니다.
@UtilityClass사용과 정적 메서드를 통한 DTO 변환 로직이 깔끔하게 구현되어 있습니다. Single Responsibility Principle을 잘 준수하고 있습니다.Line 19의
(int)캐스팅은 일반적인 access token 만료 시간(분~시간 단위)에서는 문제없지만, 약 24일(~2.1B ms) 이상의 값이 설정되면 오버플로우가 발생할 수 있습니다. 현재 사용 범위에서는 안전하나, 향후 설정 변경 시 주의가 필요합니다.src/main/java/com/ongil/backend/domain/user/entity/User.java (1)
14-22: 인덱스 및 유니크 제약 조건 설정이 적절합니다.
(login_type, social_id)복합 인덱스와 유니크 제약 조건을 통해 중복 가입 방지 및 조회 성능 향상이 잘 구현되어 있습니다.src/main/java/com/ongil/backend/domain/auth/controller/AuthController.java (3)
38-54: OAuth 엔드포인트 HTTP 메서드 일관성을 검토하세요.Kakao는
POST, Google은GET을 사용하고 있습니다. OAuth 제공자의 리다이렉트 방식에 따라 결정될 수 있지만, 보안 관점에서GET요청의code파라미터는 서버 액세스 로그에 기록될 수 있습니다.만약 프론트엔드에서 code를 전달하는 방식이라면 두 엔드포인트 모두
POST로 통일하는 것을 권장합니다. OAuth 제공자 리다이렉트를 직접 받는 경우라면GET이 불가피합니다.
28-82: 전반적으로 컨트롤러 구조가 잘 설계되어 있습니다.
@Validated와@Valid를 통한 입력 검증- Swagger
@Operation어노테이션을 통한 API 문서화DataResponse래퍼를 통한 일관된 응답 형식- 적절한 HTTP 메서드 사용 (logout은 POST, withdraw는 DELETE)
SOLID 원칙 중 Single Responsibility Principle을 잘 준수하고 있으며, 각 서비스로 비즈니스 로직을 위임하여 컨트롤러가 얇게 유지되고 있습니다.
65-81: @AuthenticationPrincipal Long userId는 올바르게 구성되었습니다.JwtAuthenticationFilter에서 JWT 토큰에서 추출한 userId(Long 타입)를 UsernamePasswordAuthenticationToken의 principal로 직접 설정하고 있으므로, @AuthenticationPrincipal 어노테이션이 올바르게 Long 타입을 추출할 수 있습니다. 현재 구현은 의도한 대로 동작합니다.
| public AuthResDto googleLogin(String code) { | ||
| String googleToken = getGoogleAccessToken(code); | ||
| GoogleUserInfoResDto userInfo = getGoogleUserInfo(googleToken); | ||
| String socialId = userInfo.sub(); | ||
|
|
||
| boolean isNewUser = !userRepository.existsByLoginTypeAndSocialId(LoginType.GOOGLE, socialId); | ||
|
|
||
| User user = userRepository.findByLoginTypeAndSocialId(LoginType.GOOGLE, socialId) | ||
| .orElseGet(() -> userRepository.save( | ||
| User.builder() | ||
| .loginType(LoginType.GOOGLE) | ||
| .socialId(socialId) | ||
| .email(extractEmail(userInfo)) | ||
| .profileImg(extractProfileImg(userInfo)) | ||
| .name(extractName(userInfo)) | ||
| .build() | ||
| )); |
There was a problem hiding this comment.
동시성 이슈: @transactional 누락 및 경쟁 조건(Race Condition) 발생 가능성
-
existsByLoginTypeAndSocialId체크와findByLoginTypeAndSocialId+save사이에 TOCTOU(Time-of-Check to Time-of-Use) 경쟁 조건이 존재합니다. 동시에 같은 socialId로 두 요청이 들어오면 둘 다isNewUser = true로 판단하고 중복 저장을 시도할 수 있습니다. -
DB의 unique constraint가 두 번째 삽입을 막겠지만,
DataIntegrityViolationException이 발생하여 사용자에게 에러가 전달됩니다.
🐛 제안: @transactional 추가 및 예외 처리
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.dao.DataIntegrityViolationException;
+@Transactional
public AuthResDto googleLogin(String code) {
String googleToken = getGoogleAccessToken(code);
GoogleUserInfoResDto userInfo = getGoogleUserInfo(googleToken);
String socialId = userInfo.sub();
- boolean isNewUser = !userRepository.existsByLoginTypeAndSocialId(LoginType.GOOGLE, socialId);
-
- User user = userRepository.findByLoginTypeAndSocialId(LoginType.GOOGLE, socialId)
- .orElseGet(() -> userRepository.save(
- User.builder()
- .loginType(LoginType.GOOGLE)
- .socialId(socialId)
- .email(extractEmail(userInfo))
- .profileImg(extractProfileImg(userInfo))
- .name(extractName(userInfo))
- .build()
- ));
+ Optional<User> existingUser = userRepository.findByLoginTypeAndSocialId(LoginType.GOOGLE, socialId);
+ boolean isNewUser = existingUser.isEmpty();
+
+ User user = existingUser.orElseGet(() -> {
+ try {
+ return userRepository.save(
+ User.builder()
+ .loginType(LoginType.GOOGLE)
+ .socialId(socialId)
+ .email(extractEmail(userInfo))
+ .profileImg(extractProfileImg(userInfo))
+ .name(extractName(userInfo))
+ .build()
+ );
+ } catch (DataIntegrityViolationException e) {
+ // 동시 요청으로 인한 중복 삽입 시 기존 유저 조회
+ return userRepository.findByLoginTypeAndSocialId(LoginType.GOOGLE, socialId)
+ .orElseThrow(() -> new IllegalStateException("User not found after constraint violation"));
+ }
+ });🤖 Prompt for AI Agents
In @src/main/java/com/ongil/backend/domain/auth/service/GoogleLoginService.java
around lines 41 - 57, The googleLogin method has a TOCTOU race between
existsByLoginTypeAndSocialId and the subsequent findByLoginTypeAndSocialId/save
which can cause duplicate-insert errors; annotate googleLogin with
@Transactional (or run it in a transactional boundary) and simplify logic to
attempt findByLoginTypeAndSocialId first, creating and saving the User only if
absent; additionally catch DataIntegrityViolationException around the save to
re-query findByLoginTypeAndSocialId and return the existing user if a
unique-constraint collision occurred, referencing googleLogin,
userRepository.findByLoginTypeAndSocialId, userRepository.save, and User.builder
in your changes.
src/main/java/com/ongil/backend/domain/auth/service/GoogleLoginService.java
Show resolved
Hide resolved
src/main/java/com/ongil/backend/domain/auth/service/KakaoLoginService.java
Show resolved
Hide resolved
src/main/java/com/ongil/backend/domain/auth/service/KakaoLoginService.java
Outdated
Show resolved
Hide resolved
| .authorizeHttpRequests(auth -> auth | ||
| .requestMatchers("/ping", "/h2-console/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll() | ||
| .requestMatchers("/auth/logout", "/auth/withdraw").authenticated() | ||
| .requestMatchers("/auth/oauth/kakao", "/auth/oauth/google", "/auth/token/refresh").permitAll() | ||
| .requestMatchers("/auth/**").permitAll() | ||
| .anyRequest().authenticated() | ||
| ) |
There was a problem hiding this comment.
H2 Console 사용 시 Frame Options 설정 필요
/h2-console/**을 permitAll로 설정했지만, H2 Console은 iframe을 사용하므로 frameOptions 비활성화가 필요합니다. 없으면 H2 Console UI가 정상 동작하지 않습니다.
🛠️ Frame Options 설정 추가
http
.cors(cors -> cors.configurationSource(corsConfigurationSource))
.csrf(AbstractHttpConfigurer::disable)
+ .headers(headers -> headers.frameOptions(frameOptions -> frameOptions.sameOrigin()))
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In @src/main/java/com/ongil/backend/global/config/SecurityConfig.java around
lines 34 - 40, The SecurityConfig currently permits "/h2-console/**" but doesn't
disable frame options required for the H2 console iframe; update the
SecurityConfig HttpSecurity configuration (the same method that builds the
authorizeHttpRequests chain) to disable frame options so the H2 Console can
render (use the headers().frameOptions().disable() call or the equivalent lambda
form in the HttpSecurity builder). Ensure this change is applied alongside the
existing authorizeHttpRequests block in SecurityConfig.
| // 2. 토큰이 유효한지 검사 | ||
| if (token != null && jwtTokenProvider.validateToken(token)) { | ||
| Long userId = jwtTokenProvider.getUserId(token); | ||
|
|
||
| // 3. 스프링 시큐리티 인증 객체 생성 | ||
| List<SimpleGrantedAuthority> authorities = List.of(new SimpleGrantedAuthority("ROLE_USER")); | ||
|
|
||
| Authentication auth = new UsernamePasswordAuthenticationToken(userId, null, authorities); | ||
|
|
||
| // 4. SecurityContext에 인증 정보 저장 (이 요청이 실행되는 동안만 유효) | ||
| SecurityContextHolder.getContext().setAuthentication(auth); | ||
| } |
There was a problem hiding this comment.
Access Token과 Refresh Token 구분 검증 필요
JwtTokenProvider.validateToken()은 토큰 타입을 구분하지 않아, Refresh Token이 Authorization 헤더로 전달될 경우에도 인증이 통과할 수 있습니다. JwtTokenProvider에는 validateRefreshToken()이 별도로 존재하므로, Access Token만 허용하는 검증 로직 추가를 고려해 주세요.
🔒 Access Token 전용 검증 메서드 제안
JwtTokenProvider에 Access Token 전용 검증 메서드를 추가하거나, 기존 validateToken()에서 type claim이 없는 경우만 허용하도록 수정:
// JwtTokenProvider에 추가
public boolean validateAccessToken(String token) {
try {
Claims claims = parseClaims(token);
return claims.get("type") == null; // refresh token은 type=refresh claim이 있음
} catch (Exception e) {
return false;
}
} // JwtAuthenticationFilter에서 사용
-if (token != null && jwtTokenProvider.validateToken(token)) {
+if (token != null && jwtTokenProvider.validateAccessToken(token)) {Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
@src/main/java/com/ongil/backend/global/security/jwt/JwtAuthenticationFilter.java
around lines 33 - 44, The filter currently accepts any valid JWT (including
refresh tokens) because JwtAuthenticationFilter calls
jwtTokenProvider.validateToken(token); change the check to ensure only access
tokens are accepted by using an access-token specific validator (e.g.,
jwtTokenProvider.validateAccessToken(token)) or by explicitly rejecting tokens
whose "type" claim equals "refresh" before creating the Authentication; keep the
rest of the flow (getUserId, build UsernamePasswordAuthenticationToken, set
SecurityContext) unchanged so only access tokens can authenticate requests.
🔍️ 작업 내용
✨ 상세 설명
1. OpenFeign을 활용한 소셜 로그인 인프라 구축
RestTemplate대비 가독성이 높고 유지보수가 용이한Spring Cloud OpenFeign을 사용하여 외부 API(Kakao, Google)와의 통신 인터페이스를 정의했습니다.KakaoAuthClient(인가 코드 교환) 및KakaoApiClient(사용자 정보 획득)를 통한 연동 완료GoogleAuthClient,GoogleApiClient)isNewUser: true플래그 반환 및 DB 자동 저장 (Hard Delete 이후 재가입 시 신규 유저 판정 확인)2. Redis 기반 보안 강화 및 토큰 관리
3. 유저 관리 기능 (Withdrawal)
🛠️ 추후 리팩토링 및 고도화 계획
1. OAuth2/OIDC 표준 추상화 및 공통화
KakaoLoginService,GoogleLoginService로 분리된 로직을 공통 인터페이스로 추상화하여, 새로운 소셜 로그인 수단 추가 시 기존 코드의 수정 없이 설정만으로 확장이 가능한 구조로 개선 예정OAuth2Client를 프로젝트 요구사항에 맞게 커스텀하여, 상이한 유저 정보 파싱 로직을OAuth2User객체로 표준화2. 인증 및 토큰 보안 강화
💬 리뷰 요구사항
Summary by CodeRabbit
새로운 기능
인프라 / 환경
문서화
✏️ Tip: You can customize this high-level summary in your review settings.