-
Notifications
You must be signed in to change notification settings - Fork 0
#10 카카오로그인구현 #11
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
#10 카카오로그인구현 #11
The head ref may contain hidden characters: "#10-\uCE74\uCE74\uC624\uB85C\uADF8\uC778\uAD6C\uD604"
Conversation
- TODO 임시 도메인 삭제 - jwt 토큰 발급 로직 구현 - CORS 설정
Walkthrough이번 변경 사항은 Spring Boot 애플리케이션에 OAuth2, JWT 기반 인증·인가 및 예외 처리 기능을 본격적으로 도입하는 대규모 리팩토링입니다. 기존 TODO 관련 코드는 제거되었고, 사용자·토큰 관리와 보안 구성이 추가되었으며, 글로벌 예외 처리와 다양한 도메인 엔티티, DTO, 리포지토리, 서비스가 신설되었습니다. Changes
Sequence Diagram(s)OAuth2 로그인 및 JWT 발급/인증 흐름sequenceDiagram
participant Client
participant SpringSecurity
participant OAuth2Provider
participant CustomOAuth2UserService
participant UserRepository
participant OAuth2LoginSuccessHandler
participant JwtUtil
participant TokenRepository
Client->>SpringSecurity: /oauth2/authorization/kakao 요청
SpringSecurity->>OAuth2Provider: 인증 요청
OAuth2Provider-->>SpringSecurity: 인증 완료, 사용자 정보 반환
SpringSecurity->>CustomOAuth2UserService: loadUser()
CustomOAuth2UserService->>UserRepository: findByEmail()
alt 신규 사용자
CustomOAuth2UserService->>UserRepository: save(User)
end
CustomOAuth2UserService-->>SpringSecurity: CustomOAuth2User 반환
SpringSecurity->>OAuth2LoginSuccessHandler: onAuthenticationSuccess()
OAuth2LoginSuccessHandler->>JwtUtil: createAccessToken(), createRefreshToken()
OAuth2LoginSuccessHandler->>TokenRepository: save(RefreshToken)
OAuth2LoginSuccessHandler-->>Client: accessToken, refreshToken JSON 반환
Refresh Token 재발급 흐름sequenceDiagram
participant Client
participant TokenController
participant JwtUtil
participant TokenService
participant TokenRepository
Client->>TokenController: /api/refresh-token POST (refreshToken)
TokenController->>JwtUtil: getUserId(), getRole()
TokenController->>TokenService: validateToken(refreshToken, userId)
alt 유효
TokenController->>JwtUtil: createAccessToken(), createRefreshToken()
TokenController->>TokenService: updateRefreshToken(userId, old, new)
TokenService->>TokenRepository: findByUserId(), delete(), save()
TokenController-->>Client: 새 accessToken, refreshToken 반환
else 유효하지 않음
TokenController-->>Client: 401 Unauthorized 반환
end
Poem
✨ Finishing Touches
🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
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: 22
🧹 Nitpick comments (32)
src/main/java/com/example/gtable/token/repository/TokenRepository.java (1)
9-11: 리포지토리 인터페이스가 잘 구현되었습니다!Spring Data JPA의 표준 패턴을 올바르게 따르고 있으며,
Optional<Token>반환 타입을 사용하여 null 안전성을 보장하고 있습니다.findByUserId메서드명도 Spring Data JPA의 메서드 명명 규칙에 잘 맞습니다.성능 최적화를 위해
userId컬럼에 데이터베이스 인덱스가 설정되어 있는지 확인해 주세요.src/main/java/com/example/gtable/user/repository/UserRepository.java (1)
9-11: 사용자 리포지토리가 올바르게 구현되었습니다!
findByEmail메서드는 OAuth2 로그인 플로우에서 기존 사용자를 확인하는 데 적절한 방법입니다.Optional반환 타입을 사용하여 null 안전성도 잘 보장하고 있습니다.이메일 기반 검색의 성능을 위해
src/main/java/com/example/gtable/global/security/exception/RefreshTokenNotFoundException.java (1)
3-9: 예외 클래스 구현이 깔끔합니다! 몇 가지 개선 제안이 있습니다.리프레시 토큰 관련 예외 처리 구조가 잘 정리되어 있네요. 다만 더 견고한 예외 처리를 위해 다음 개선사항들을 고려해보시면 좋겠습니다:
- Javadoc 문서화 추가 - 예외가 언제 발생하는지 명확히 기술
- serialVersionUID 추가 - 직렬화 호환성 보장
- 추가 생성자 고려 - 동적 메시지나 원인 예외를 받을 수 있는 생성자
+/** + * 리프레시 토큰을 찾을 수 없을 때 발생하는 예외 + * JWT 토큰 갱신 과정에서 유효하지 않거나 존재하지 않는 리프레시 토큰에 대해 발생 + */ public class RefreshTokenNotFoundException extends ResourceNotFoundException { + private static final long serialVersionUID = 1L; public RefreshTokenNotFoundException() { super(ErrorMessage.REFRESH_TOKEN_NOT_FOUND); } + + public RefreshTokenNotFoundException(String customMessage) { + super(ErrorMessage.REFRESH_TOKEN_NOT_FOUND, customMessage); + } }src/main/java/com/example/gtable/global/security/exception/BusinessException.java (1)
3-14: 견고한 예외 계층 구조의 기반이 되는 좋은 설계입니다!비즈니스 로직 예외의 기본 클래스로서 역할을 잘 수행할 것 같습니다. 몇 가지 세심한 개선사항을 제안드리겠습니다:
+/** + * 비즈니스 로직 관련 예외의 기본 추상 클래스 + * 표준화된 에러 메시지와 코드를 제공하여 일관성 있는 예외 처리를 지원 + */ public abstract class BusinessException extends RuntimeException { + private static final long serialVersionUID = 1L; + private final ErrorMessage errorMessage; protected BusinessException(ErrorMessage errorMessage) { super(errorMessage.getMessage()); this.errorMessage = errorMessage; } + + protected BusinessException(ErrorMessage errorMessage, Throwable cause) { + super(errorMessage.getMessage(), cause); + this.errorMessage = errorMessage; + } public String getCode() { return errorMessage.getCode(); } + + public ErrorMessage getErrorMessage() { + return errorMessage; + } }주요 개선사항:
- 직렬화 호환성:
serialVersionUID추가- 원인 예외 지원: 체이닝을 위한 추가 생성자
- ErrorMessage 접근: 전체 ErrorMessage 객체에 접근할 수 있는 getter 추가
src/main/java/com/example/gtable/global/security/exception/ResourceNotFoundException.java (1)
1-14: 잘 설계된 추상 예외 클래스입니다!추상 클래스로 설계하여 하위 클래스들이 일관된 방식으로 ErrorMessage를 처리하도록 한 것은 훌륭한 접근법입니다. 코드 구조가 깔끔하고 캡슐화도 잘 되어 있네요.
다만 클래스와 메서드에 JavaDoc 주석을 추가하시면 더욱 좋을 것 같습니다:
+/** + * 리소스를 찾을 수 없는 경우의 기본 예외 클래스 + * 하위 클래스에서 특정 ErrorMessage를 사용하여 구체적인 예외를 정의할 수 있습니다. + */ public abstract class ResourceNotFoundException extends RuntimeException { private final ErrorMessage errorMessage; + /** + * ErrorMessage를 사용하여 예외를 생성합니다. + * @param errorMessage 에러 상세 정보 + */ protected ResourceNotFoundException(ErrorMessage errorMessage) { super(errorMessage.getMessage()); this.errorMessage = errorMessage; } + /** + * 에러 코드를 반환합니다. + * @return 에러 코드 + */ public String getCode() { return errorMessage.getCode(); } }src/main/java/com/example/gtable/global/security/exception/UnauthorizedException.java (1)
3-19: 깔끔하게 구현된 인증 예외 클래스네요!기본 생성자에서
ErrorMessage.UNAUTHORIZED를 사용하고, 유연성을 위해 커스텀 ErrorMessage를 받는 생성자도 제공한 것이 좋습니다. 코드가 간결하면서도 필요한 기능을 모두 갖추고 있어요.문서화를 추가해주시면 더욱 완성도 높은 코드가 될 것 같습니다:
+/** + * 인증되지 않은 사용자의 접근을 나타내는 예외 클래스 + */ public class UnauthorizedException extends RuntimeException { private final ErrorMessage errorMessage; + /** + * 기본 UNAUTHORIZED 메시지로 예외를 생성합니다. + */ public UnauthorizedException() { super(ErrorMessage.UNAUTHORIZED.getMessage()); this.errorMessage = ErrorMessage.UNAUTHORIZED; } + /** + * 커스텀 ErrorMessage로 예외를 생성합니다. + * @param errorMessage 커스텀 에러 메시지 + */ public UnauthorizedException(ErrorMessage errorMessage) { super(errorMessage.getMessage()); this.errorMessage = errorMessage; } + /** + * 에러 코드를 반환합니다. + * @return 에러 코드 + */ public String getCode() { return errorMessage.getCode(); } }src/main/java/com/example/gtable/global/security/exception/ErrorResponse.java (1)
8-26: 깔끔하고 실용적인 에러 응답 클래스입니다!필드들을 final로 선언하여 불변성을 보장하고, 두 가지 생성자를 제공하여 유연성도 확보한 것이 좋습니다. 구조가 명확하고 사용하기 쉽게 설계되었네요.
한 가지 작은 개선 제안이 있습니다. 두 번째 생성자에서 외부에서 전달받은 Map을 직접 할당하는 것보다는 방어적 복사를 고려해보세요:
public ErrorResponse(String message, String code, Map<String, String> errors) { this.message = message; this.code = code; - this.errors = errors; + this.errors = errors != null ? new HashMap<>(errors) : new HashMap<>(); }이렇게 하면 외부에서 전달된 Map이 나중에 변경되어도 ErrorResponse 객체의 불변성을 완전히 보장할 수 있습니다.
문서화도 추가해주시면 더욱 좋을 것 같아요:
+/** + * API 에러 응답을 위한 표준 응답 클래스 + */ @Getter public class ErrorResponse { + /** 에러 메시지 */ private final String message; + /** 에러 코드 */ private final String code; + /** 상세 에러 정보 맵 */ private final Map<String, String> errors;src/main/java/com/example/gtable/global/security/oauth2/dto/OAuth2Response.java (1)
3-19: OAuth2 응답 인터페이스 설계가 훌륭합니다다양한 OAuth2 제공자를 지원하기 위한 추상화가 잘 설계되어 있네요. 각 메서드의 역할도 명확하고 주석으로 잘 설명되어 있습니다.
다만
getNickName()메서드명에서 대소문자 일관성을 확인해보시면 좋겠습니다. 일반적으로 Java에서는getNickname()과 같이 camelCase를 사용합니다.// 닉네임 - String getNickName(); + String getNickname();이렇게 수정하면 Java 명명 규칙에 더 부합할 것 같아요.
src/main/java/com/example/gtable/global/config/CorsConfig.java (1)
17-21: 보안 관점에서 CORS 설정을 검토해주세요현재
setAllowedHeaders(List.of("*"))로 모든 헤더를 허용하고 있는데, 보안상 필요한 헤더만 명시적으로 허용하는 것이 더 안전합니다.- config.setAllowedHeaders(List.of("*")); //클라이언트가 보낼 수 있는 헤더 + config.setAllowedHeaders(List.of( + "Authorization", + "Content-Type", + "X-Requested-With", + "Accept" + )); // 필요한 헤더만 명시적으로 허용이렇게 하면 보안성을 높일 수 있어요.
build.gradle (2)
27-46: 보안 관련 의존성 추가가 잘 구성되었습니다!OAuth2와 JWT 기반 인증 시스템 구현을 위한 의존성들이 적절히 추가되었고, 주석으로 잘 구분되어 있어 가독성이 좋습니다. 특히 JWT 라이브러리의 세 가지 모듈(api, impl, jackson)을 모두 포함시킨 것이 좋네요.
다만 한 가지 개선사항이 있습니다:
- 46번 라인의
spring-boot-starter-test가 31번 라인과 중복됩니다.중복된 테스트 의존성을 제거해주세요:
- testImplementation 'org.springframework.boot:spring-boot-starter-test'
41-43: JWT 라이브러리 버전 관리 권장사항JWT 라이브러리들이 모두 동일한 버전(0.12.3)으로 설정되어 있어 좋습니다. 향후 버전 업데이트 시 호환성을 위해 BOM(Bill of Materials) 사용을 고려해보시면 더욱 좋을 것 같습니다.
src/main/java/com/example/gtable/global/security/exception/ErrorMessage.java (1)
6-22: 에러 메시지 정의가 체계적으로 잘 구성되었습니다!Lombok 어노테이션을 적절히 사용하고, 에러 코드 패턴(g001, a001, t001 등)이 일관성 있게 정의되어 있어 유지보수에 좋습니다. 한국어 메시지도 명확하고 사용자 친화적이네요.
향후 보안 시스템이 확장될 때를 대비해 추가적인 에러 메시지들(예: JWT 토큰 관련, OAuth2 관련)을 고려해보시면 좋을 것 같습니다.
src/main/java/com/example/gtable/token/service/TokenService.java (2)
25-50: 토큰 검증 로직이 체계적으로 잘 구현되었습니다사용자 ID 기반 토큰 조회, 토큰 일치성 확인, 만료 여부 확인의 3단계 검증 과정이 매우 적절합니다. @transactional 어노테이션 사용도 좋네요.
다만 디버깅용 로그 메시지들을 정리해주시면 더 깔끔할 것 같습니다.
디버깅용 로그를 정리해주세요:
- log.info("여기에 걸렸니 ? -- 1 "); + log.debug("Refresh token not found for userId: {}", userId); - log.info("여기에 걸렸니 ? -- 2 "); + log.debug("Refresh token mismatch for userId: {}", userId); - log.info("여기에 걸렸니 ? -- 3 "); + log.debug("Refresh token expired for userId: {}", userId); - log.info("여기에 걸렸니 ? -- 4 "); + log.debug("Token validation successful for userId: {}", userId);
52-65: 토큰 업데이트 로직이 안전하게 구현되었습니다기존 토큰 존재 여부 확인, 토큰 일치성 검증 후 삭제-생성 방식으로 업데이트하는 것이 안전한 방식입니다. 커스텀 예외 사용도 적절하고, 30일 만료 기간 설정도 합리적이네요.
작은 개선 제안으로는 토큰 업데이트를 더 효율적으로 할 수 있는 방법을 고려해볼 수 있습니다.
토큰 업데이트를 더 효율적으로 개선할 수 있습니다:
- // 기존 토큰 삭제 및 새 토큰 저장 - tokenRepository.delete(token); - Token newToken = Token.toEntity(token.getUser(), newRefreshToken, LocalDateTime.now().plusDays(30)); - tokenRepository.save(newToken); + // 기존 토큰 업데이트 + token.updateRefreshToken(newRefreshToken, LocalDateTime.now().plusDays(30)); + tokenRepository.save(token);※ 이를 위해서는 Token 엔티티에
updateRefreshToken메소드 추가가 필요합니다.src/main/java/com/example/gtable/token/entity/Token.java (3)
36-36: expiredDate 필드에 @column 어노테이션이 누락되었습니다.데이터베이스 컬럼 매핑을 명시적으로 지정하는 것이 좋겠습니다.
다음과 같이 수정해주세요:
- private LocalDateTime expiredDate; + @Column(nullable = false) + private LocalDateTime expiredDate;
29-34: 성능 최적화를 위한 인덱스 추가를 고려해보세요.refresh token 조회가 빈번하게 발생할 것으로 예상되므로, 성능을 위해 인덱스를 추가하는 것을 권장드립니다.
다음과 같이 인덱스를 추가하시는 것을 고려해보세요:
@ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = false) private User user; -@Column +@Column(unique = true) // refresh token은 유니크해야 함 private String refreshToken;또한 엔티티 클래스에 인덱스를 추가할 수 있습니다:
@Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) @Table(name = "token", indexes = { + @Index(name = "idx_refresh_token", columnList = "refreshToken"), + @Index(name = "idx_user_id", columnList = "user_id") }) @Getter public class Token {
25-53: 토큰 만료 체크 기능 추가를 제안드립니다.엔티티에 비즈니스 로직을 추가하여 토큰 만료 여부를 쉽게 확인할 수 있도록 하면 좋겠습니다.
다음과 같은 메서드를 추가하시는 것을 고려해보세요:
// static method로 객체를 생성 - 생성 의도 파악 쉬웁 public static Token toEntity(User user, String refreshToken, LocalDateTime expiredDate){ return Token.builder() .user(user) .refreshToken(refreshToken) .expiredDate(expiredDate) .build(); } +// 토큰 만료 여부 체크 +public boolean isExpired() { + return LocalDateTime.now().isAfter(this.expiredDate); +} + +// 토큰 만료까지 남은 시간 (초 단위) +public long getSecondsUntilExpiration() { + return java.time.Duration.between(LocalDateTime.now(), this.expiredDate).getSeconds(); +}src/main/java/com/example/gtable/user/entity/User.java (2)
24-31: 이메일과 닉네임 필드에 길이 제한을 추가하는 것을 권장드립니다.데이터 무결성과 성능을 위해 컬럼 길이를 명시적으로 지정하시는 것이 좋겠습니다.
다음과 같이 길이 제한을 추가해보세요:
-@Column(nullable = false, unique = true) +@Column(nullable = false, unique = true, length = 100) private String email; // 카카오 이메일 -@Column(nullable = false) +@Column(nullable = false, length = 50) private String nickname; -@Column(nullable = false) +@Column(nullable = false, length = 500) private String profileImage;
16-37: 인덱스 추가로 조회 성능을 개선할 수 있습니다.이메일로 사용자를 조회하는 경우가 많을 것으로 예상되므로, 명시적인 인덱스 추가를 고려해보세요.
@Entity @NoArgsConstructor(access = AccessLevel.PROTECTED) -@Table(name = "user") +@Table(name = "user", indexes = { + @Index(name = "idx_email", columnList = "email"), + @Index(name = "idx_social_type", columnList = "socialType") +}) @Getter public class User {또한 필요에 따라 Import 문도 추가해주세요:
+import jakarta.persistence.Index;src/main/java/com/example/gtable/global/security/oauth2/dto/CustomOAuth2User.java (2)
14-18: @requiredargsconstructor가 실제로는 동작하지 않습니다
@RequiredArgsConstructor는final또는@NonNull필드만 대상으로 생성자를 만드는데,user필드가final이 아니므로 아무 생성자도 생성되지 않아 애노테이션이 의미가 없습니다. 이미 직접 생성자를 선언하셨으므로 애노테이션을 제거해 혼동을 줄이는 편이 좋겠습니다.-@RequiredArgsConstructor -public class CustomOAuth2User implements OAuth2User { +public class CustomOAuth2User implements OAuth2User {
30-38: 권한 생성 시SimpleGrantedAuthority사용 권장
익명 클래스를 직접 만들면 가독성과 불필요한 클래스 생성 비용이 증가합니다.new SimpleGrantedAuthority(user.getRole().getName())로 대체하면 간결하고 의도가 명확합니다.-Collection<GrantedAuthority> authorities = new ArrayList<>(); - -authorities.add(new GrantedAuthority() { - @Override - public String getAuthority() { - return user.getRole().getName(); // 유저의 권한 리턴 - } -}); +Collection<GrantedAuthority> authorities = + List.of(new SimpleGrantedAuthority(user.getRole().getName()));src/main/java/com/example/gtable/global/security/oauth2/CustomOAuth2UserService.java (2)
51-59: 신규 가입 로직에 트랜잭션·중복체크 보강 권장
동시에 동일 이메일로 두 번 이상 요청이 들어오면 중복 레코드가 저장될 수 있습니다.@Transactional로 감싸고, 이메일에UK를 두는 한편findByEmail(...).orElseGet()패턴으로 단일 경로에서 처리하면 안전합니다.
62-67: 중복 코드 제거로 가독성 향상 가능
return new CustomOAuth2User(user);가 두 군데 반복되고 있습니다. if/else 밖에서 한 번만 반환하도록 구조를 단순화할 수 있습니다.src/main/java/com/example/gtable/global/security/jwt/JwtAuthorizationFilter.java (2)
35-41: Bearer 토큰이 없을 때 불필요한 로그 발생
토큰이 없어 정상적으로 다음 필터로 넘기는 상황에서log.info("JwtAuthorizationFilter 1 ")가 찍히면 로그 노이즈가 증가합니다.debug레벨로 낮추거나 제거를 권장드립니다.
60-69: 토큰 타입 체크 로직 하드코딩 제거
문자열"accessToken"을 여러 곳에서 직접 비교하면 오타 위험이 있습니다. 상수 또는 enum 으로 관리해 주세요. 또한 else 분기에서도 위와 동일한 에러 포맷 문제를 확인해 주세요.src/main/java/com/example/gtable/global/security/jwt/JwtUtil.java (2)
19-24: 생성자에서 시크릿 키 초기화 방식을 개선해주세요.현재 구현에서 시크릿 키 생성 방식이 다소 복잡하게 되어 있습니다. JJWT 최신 버전에서는 더 간단하고 명확한 방식을 제공합니다.
다음과 같이 개선해보시는 것은 어떨까요?
- public JwtUtil(@Value("${jwt.secret}") String secret) { - this.secretKey = new SecretKeySpec( - secret.getBytes(StandardCharsets.UTF_8), - Jwts.SIG.HS256.key().build().getAlgorithm() - ); - } + public JwtUtil(@Value("${jwt.secret}") String secret) { + this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + }이를 위해 imports에 다음을 추가해주세요:
import io.jsonwebtoken.security.Keys;
26-45: 토큰 생성 메서드에서 예외 처리를 고려해주세요.토큰 생성 시 잘못된 입력값이나 시스템 오류로 인한 예외가 발생할 수 있습니다. 적절한 예외 처리나 검증 로직을 추가하시는 것을 권장드립니다.
예를 들어, 다음과 같은 검증을 추가해보시는 건 어떨까요?
public String createAccessToken(String tokenCategory, Long userId, String role, Long expiredMs) { if (userId == null || role == null || expiredMs <= 0) { throw new IllegalArgumentException("Invalid token parameters"); } // 기존 로직... }src/main/java/com/example/gtable/global/security/exception/GlobalExceptionHandler.java (5)
3-5: Import 문 정리를 권장드립니다.static import가 혼재되어 있어 가독성이 떨어질 수 있습니다. 일관성 있게 정리해주시면 좋을 것 같습니다.
다음과 같이 정리해보시는 건 어떨까요?
-import static com.example.gtable.global.security.exception.ErrorMessage.*; -import static org.springframework.http.HttpStatus.UNAUTHORIZED; -import static org.springframework.http.HttpStatus.*; +import static com.example.gtable.global.security.exception.ErrorMessage.*; +import static org.springframework.http.HttpStatus.*;
68-68: 로그 메시지를 메서드명과 일치시켜주세요.로그 메시지에 오타가 있고 메서드명과 일치하지 않습니다.
- log.error("handleMissingRequestValueExceptionException", e); + log.error("handleMissingRequestValueException", e);
75-75: 로그 메시지 중복 접미사를 제거해주세요."Exception"이 중복되어 있습니다.
- log.error("handleUnauthorizedExceptionException", e); + log.error("handleUnauthorizedException", e);
82-82: 로그 메시지 중복 접미사를 제거해주세요.여기도 동일한 문제가 있습니다.
- log.error("handleResourceNotFoundExceptionException", e); + log.error("handleResourceNotFoundException", e);
97-102: validation 오류 처리 로직을 개선해주세요.현재 필터링 로직이 불필요하고, 타입 캐스팅도 개선할 수 있습니다.
다음과 같이 더 간결하고 안전하게 구현해보시는 건 어떨까요?
private static Map<String, String> getErrors(MethodArgumentNotValidException e) { return e.getBindingResult() .getAllErrors() .stream() - .filter(ObjectError.class::isInstance) .collect(Collectors.toMap( error -> error instanceof FieldError ? ((FieldError)error).getField() : error.getObjectName(), ObjectError::getDefaultMessage, (msg1, msg2) -> msg1 + "; " + msg2 )); }공백을 추가하여 가독성도 개선했습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (37)
build.gradle(1 hunks)src/main/java/com/example/gtable/GTableApplication.java(1 hunks)src/main/java/com/example/gtable/TODO/TodoController.java(0 hunks)src/main/java/com/example/gtable/TODO/TodoRepository.java(0 hunks)src/main/java/com/example/gtable/TODO/TodoService.java(0 hunks)src/main/java/com/example/gtable/global/api/ApiError.java(1 hunks)src/main/java/com/example/gtable/global/api/ApiResult.java(1 hunks)src/main/java/com/example/gtable/global/api/ApiUtils.java(1 hunks)src/main/java/com/example/gtable/global/config/CorsConfig.java(1 hunks)src/main/java/com/example/gtable/global/config/SecurityConfig.java(1 hunks)src/main/java/com/example/gtable/global/security/exception/BusinessException.java(1 hunks)src/main/java/com/example/gtable/global/security/exception/ErrorMessage.java(1 hunks)src/main/java/com/example/gtable/global/security/exception/ErrorResponse.java(1 hunks)src/main/java/com/example/gtable/global/security/exception/GlobalExceptionHandler.java(1 hunks)src/main/java/com/example/gtable/global/security/exception/RefreshTokenNotFoundException.java(1 hunks)src/main/java/com/example/gtable/global/security/exception/ResourceNotFoundException.java(1 hunks)src/main/java/com/example/gtable/global/security/exception/TokenBadRequestException.java(1 hunks)src/main/java/com/example/gtable/global/security/exception/UnauthorizedException.java(1 hunks)src/main/java/com/example/gtable/global/security/jwt/JwtAuthorizationFilter.java(1 hunks)src/main/java/com/example/gtable/global/security/jwt/JwtUtil.java(1 hunks)src/main/java/com/example/gtable/global/security/oauth2/CustomOAuth2UserService.java(1 hunks)src/main/java/com/example/gtable/global/security/oauth2/OAuth2LoginSuccessHandler.java(1 hunks)src/main/java/com/example/gtable/global/security/oauth2/dto/CustomOAuth2User.java(1 hunks)src/main/java/com/example/gtable/global/security/oauth2/dto/KaKaoResponse.java(1 hunks)src/main/java/com/example/gtable/global/security/oauth2/dto/OAuth2Response.java(1 hunks)src/main/java/com/example/gtable/token/controller/TokenController.java(1 hunks)src/main/java/com/example/gtable/token/dto/AuthenticationResponse.java(1 hunks)src/main/java/com/example/gtable/token/dto/RefreshTokenRequest.java(1 hunks)src/main/java/com/example/gtable/token/entity/Token.java(1 hunks)src/main/java/com/example/gtable/token/repository/TokenRepository.java(1 hunks)src/main/java/com/example/gtable/token/service/TokenService.java(1 hunks)src/main/java/com/example/gtable/user/controller/UserController.java(1 hunks)src/main/java/com/example/gtable/user/dto/UserResponseDto.java(1 hunks)src/main/java/com/example/gtable/user/entity/Role.java(1 hunks)src/main/java/com/example/gtable/user/entity/SocialType.java(1 hunks)src/main/java/com/example/gtable/user/entity/User.java(1 hunks)src/main/java/com/example/gtable/user/repository/UserRepository.java(1 hunks)
💤 Files with no reviewable changes (3)
- src/main/java/com/example/gtable/TODO/TodoRepository.java
- src/main/java/com/example/gtable/TODO/TodoService.java
- src/main/java/com/example/gtable/TODO/TodoController.java
🧰 Additional context used
🧬 Code Graph Analysis (5)
src/main/java/com/example/gtable/token/dto/AuthenticationResponse.java (1)
src/main/java/com/example/gtable/token/dto/RefreshTokenRequest.java (1)
Getter(5-8)
src/main/java/com/example/gtable/global/config/CorsConfig.java (1)
src/main/java/com/example/gtable/global/config/SecurityConfig.java (1)
Configuration(19-64)
src/main/java/com/example/gtable/global/security/oauth2/dto/KaKaoResponse.java (2)
src/main/java/com/example/gtable/global/security/oauth2/dto/CustomOAuth2User.java (1)
RequiredArgsConstructor(14-60)src/main/java/com/example/gtable/global/security/jwt/JwtAuthorizationFilter.java (1)
RequiredArgsConstructor(25-93)
src/main/java/com/example/gtable/user/controller/UserController.java (1)
src/main/java/com/example/gtable/global/security/oauth2/dto/CustomOAuth2User.java (1)
RequiredArgsConstructor(14-60)
src/main/java/com/example/gtable/global/config/SecurityConfig.java (2)
src/main/java/com/example/gtable/global/config/CorsConfig.java (1)
Configuration(11-28)src/main/java/com/example/gtable/global/security/jwt/JwtAuthorizationFilter.java (1)
RequiredArgsConstructor(25-93)
🔇 Additional comments (18)
src/main/java/com/example/gtable/GTableApplication.java (1)
9-11: 포맷팅 변경 확인: 기능 영향 없음
메인 메서드의 들여쓰기만 조정되었으며 로직과 애플리케이션 실행 흐름에는 변동이 없습니다.src/main/java/com/example/gtable/global/api/ApiUtils.java (1)
3-4: 스타일 변경 확인: 로직 불변
Import 순서와 들여쓰기만 일관성 있게 조정되었으며, 응답 생성 메서드들의 기능에는 영향이 없습니다.Also applies to: 10-12, 14-16, 18-20
src/main/java/com/example/gtable/global/api/ApiResult.java (1)
4-6: 스타일 변경 확인: 기능 유지
들여쓰기 스타일을 탭으로 통일했지만, 제네릭 응답 래퍼 로직은 그대로 유지됩니다.Also applies to: 8-12, 14-16, 18-20
src/main/java/com/example/gtable/global/api/ApiError.java (1)
5-6: 스타일 변경 확인: 기능 유지
Import 순서 재배치 및 탭 들여쓰기 적용 외에는 에러 처리 클래스 로직에 변경 사항이 없습니다.Also applies to: 9-15, 17-19, 21-23
src/main/java/com/example/gtable/user/entity/SocialType.java (1)
1-5: 새 Enum 정의 검토: 적절
KAKAO,NAVER,SocialTypeenum이 간결하고 확장 가능하게 작성되었습니다.src/main/java/com/example/gtable/user/entity/Role.java (1)
8-10: 역할 정의가 명확하게 잘 되어 있습니다!USER와 ADMIN 역할을 enum으로 정의한 것이 좋고, Lombok을 사용해서 코드도 깔끔하네요.
src/main/java/com/example/gtable/user/controller/UserController.java (3)
1-14: Import 구문과 패키지 구조가 잘 정리되어 있습니다필요한 의존성들이 적절히 import되어 있고, 패키지 구조도 도메인별로 잘 분리되어 있네요. 다만 16번 라인에 불필요한 빈 줄이 있어 보입니다.
17-21: 컨트롤러 클래스 설정이 적절합니다
@RestController,@RequestMapping,@RequiredArgsConstructor,@Slf4j어노테이션이 올바르게 사용되었습니다. REST API 컨트롤러로서 필요한 설정들이 잘 갖춰져 있네요.
23-33:Details
❌ Incorrect review comment
사용자 정보 조회 엔드포인트의 안전성을 검증해주세요
현재 구현에서
customOAuth2User.getUser()와user.getRole().getName()호출 시 null 체크가 없어 NPE 위험이 있습니다. 인증된 사용자라도 예외적인 상황에서 null 값이 반환될 수 있습니다.다음과 같이 null 안전성을 개선해보시는 것이 좋겠습니다:
@GetMapping("/me") public ResponseEntity<UserResponseDto> getMyInfo(@AuthenticationPrincipal CustomOAuth2User customOAuth2User){ + if (customOAuth2User == null) { + throw new IllegalStateException("인증된 사용자 정보를 찾을 수 없습니다"); + } + User user = customOAuth2User.getUser(); + if (user == null || user.getRole() == null) { + throw new IllegalStateException("사용자 또는 역할 정보가 없습니다"); + } UserResponseDto userResponseDto = UserResponseDto.builder() .userId(user.getId()) .role(user.getRole().getName()) .build(); return ResponseEntity.ok(userResponseDto); }
🏁 Script executed:
#!/bin/bash # CustomOAuth2User와 User 엔티티에서 null 처리가 어떻게 되어 있는지 확인 ast-grep --pattern $'public User getUser() { $$$ }' # Role 엔티티의 getName() 메서드 구현 확인 ast-grep --pattern $'public String getName() { $$$ }'Length of output: 409
🏁 Script executed:
#!/bin/bash # Display CustomOAuth2User implementation (constructor and getUser) sed -n '1,200p' src/main/java/com/example/gtable/global/security/oauth2/dto/CustomOAuth2User.java # Locate Role entity and show its fields/getters rg "class Role" -A 50 src/main/java/com/example/gtableLength of output: 1329
🏁 Script executed:
#!/bin/bash # Role.java 위치 및 내용 확인 ROLE_FILE=$(find src -type f -name Role.java | head -n1) if [ -z "$ROLE_FILE" ]; then echo "Role.java를 찾을 수 없습니다" exit 1 fi echo "Found Role.java at $ROLE_FILE" sed -n '1,200p' "$ROLE_FILE"Length of output: 907
🏁 Script executed:
#!/bin/bash # User 엔티티에서 role 필드와 관련된 어노테이션/Getter 확인 USER_FILE=$(find src -type f -name User.java | head -n1) if [ -z "$USER_FILE" ]; then echo "User.java를 찾을 수 없습니다" exit 1 fi echo "Found User.java at $USER_FILE" sed -n '1,200p' "$USER_FILE"Length of output: 2242
null 검사 중복 방어 불필요
현재 스프링 시큐리티 설정 하에서
@AuthenticationPrincipal로 주입되는CustomOAuth2User는 인증된 요청에서만 메서드가 호출되므로 null 이 될 수 없으며,CustomOAuth2User생성자에서 반드시User를 전달하고,User엔티티는 빌더 및 팩토리 메서드로 생성 시 항상Role을 설정하도록 되어 있어user.getRole()도 null 이 될 가능성이 없습니다.따라서 아래와 같은 추가 null 체크는 실제로 필요한 방어 로직이 아니며,
인증/인가 실패 시에는 스프링 시큐리티의 필터 체인(401/403)으로 제어하는 편이 더 깔끔합니다.Likely an incorrect or invalid review comment.
src/main/java/com/example/gtable/global/security/oauth2/dto/KaKaoResponse.java (1)
7-15: 클래스 구조와 getProvider 메서드가 깔끔합니다클래스 구조가 잘 설계되어 있고,
getProvider()메서드가 명확하게 "kakao"를 반환하고 있어 좋습니다.src/main/java/com/example/gtable/global/config/SecurityConfig.java (3)
19-28: Spring Security 설정이 잘 구성되어 있습니다!JWT 기반 인증과 OAuth2 로그인을 위한 설정이 적절히 구성되었습니다. 필요한 컴포넌트들이 모두 주입되어 있고 구조가 깔끔합니다.
31-49: 보안 설정이 JWT 기반 인증에 최적화되어 있습니다CSRF, 폼 로그인, HTTP Basic 인증을 비활성화하고 stateless 세션 관리를 설정한 것이 JWT 기반 인증 시스템에 매우 적합합니다. OAuth2 로그인 설정도 커스텀 서비스와 핸들러를 잘 연결했네요.
50-59: 인가 규칙과 JWT 필터 설정이 적절합니다OAuth2 관련 엔드포인트와 토큰 갱신 엔드포인트를 적절히 허용하고, JWT 인증 필터를 올바른 위치에 배치했습니다.
UsernamePasswordAuthenticationFilter이전에 JWT 필터를 배치한 것이 좋은 선택이네요.src/main/java/com/example/gtable/user/entity/User.java (1)
61-64: 비즈니스 로직 메서드가 잘 구현되어 있습니다!
updateNickname메서드가 도메인 주도 설계(DDD) 원칙에 따라 잘 구현되어 있네요. 엔티티 내부에서 상태 변경을 관리하는 것이 좋은 설계입니다.향후 필요에 따라 닉네임 유효성 검증 로직도 추가하실 수 있을 것 같습니다:
public void updateNickname(String nickname){ + if (nickname == null || nickname.trim().isEmpty()) { + throw new IllegalArgumentException("닉네임은 비어있을 수 없습니다."); + } + if (nickname.length() > 50) { + throw new IllegalArgumentException("닉네임은 50자를 초과할 수 없습니다."); + } this.nickname = nickname; }src/main/java/com/example/gtable/global/security/jwt/JwtUtil.java (2)
68-74: 만료 확인 로직이 올바르게 구현되었습니다.
isExpired메서드는verifyWith와parseSignedClaims를 적절히 사용하여 보안적으로 안전하게 구현되었습니다. 좋은 구현입니다!
47-52:⚠️ Potential issue토큰 파싱 시 보안 검증을 강화해주세요.
현재
getTokenCategory메서드에서 토큰 만료 여부를 확인하지 않고 있습니다. 만료된 토큰도 파싱되어 정보가 노출될 수 있습니다.다음과 같이 보안을 강화해주시면 좋을 것 같습니다:
public String getTokenCategory(String token) { - return Jwts.parser().setSigningKey(secretKey).build() + return Jwts.parser().verifyWith(secretKey).build() .parseClaimsJws(token) .getBody() .get("tokenCategory", String.class); }Likely an incorrect or invalid review comment.
src/main/java/com/example/gtable/global/security/exception/GlobalExceptionHandler.java (2)
27-34: OAuth2 예외 처리가 잘 구현되었습니다.OAuth2 인증 실패 시 적절한 상태 코드와 메시지로 처리하고 있어 좋습니다. 사용자에게 명확한 피드백을 제공할 수 있겠네요.
43-49: validation 예외 처리에서 필드별 오류 정보 제공이 훌륭합니다.필드별 validation 오류를 맵으로 구성하여 클라이언트에게 상세한 정보를 제공하는 것이 매우 좋은 접근입니다. 사용자 경험을 크게 개선할 수 있을 것 같습니다.
| public class TokenBadRequestException extends BusinessException { | ||
| public TokenBadRequestException() { | ||
| super(ErrorMessage.DOES_NOT_MATCH_REFRESH_TOKEN); | ||
| } | ||
|
|
||
| } |
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.
💡 Verification agent
🧩 Analysis chain
예외 클래스가 적절하게 구현되었습니다.
토큰 검증 실패를 위한 전용 예외 클래스를 만든 것은 좋은 설계입니다. 명명도 명확하고 목적이 분명합니다.
부모 클래스인 BusinessException과 ErrorMessage.DOES_NOT_MATCH_REFRESH_TOKEN 상수가 제대로 정의되어 있는지 확인이 필요합니다.
다음 스크립트를 실행하여 의존 클래스들이 존재하는지 확인해 주세요:
추가로 더 유연한 예외 처리를 위해 사용자 정의 메시지를 받는 생성자를 추가하는 것도 고려해 보세요:
public class TokenBadRequestException extends BusinessException {
public TokenBadRequestException() {
super(ErrorMessage.DOES_NOT_MATCH_REFRESH_TOKEN);
}
+
+ public TokenBadRequestException(String message) {
+ super(message);
+ }
}🏁 Script executed:
#!/bin/bash
# BusinessException 클래스와 ErrorMessage 상수 확인
echo "=== BusinessException 클래스 확인 ==="
fd -e java -x grep -l "class BusinessException"
echo "=== ErrorMessage 상수 확인 ==="
fd -e java -x grep -l "DOES_NOT_MATCH_REFRESH_TOKEN"Length of output: 494
BusinessException 및 ErrorMessage 존재 확인 완료
검증 결과 BusinessException 클래스와 ErrorMessage.DOES_NOT_MATCH_REFRESH_TOKEN 상수가 정상적으로 정의되어 있습니다. 전용 예외 구조는 적절하며 명명 규칙도 잘 지켜졌습니다.
추가로 예외 처리 유연성을 높이기 위해 사용자 지정 메시지를 받는 생성자 추가를 제안드립니다.
- 검증된 파일 위치
• src/main/java/com/example/gtable/global/security/exception/BusinessException.java
• src/main/java/com/example/gtable/global/security/exception/ErrorMessage.java
제안 드리는 코드 예시:
public class TokenBadRequestException extends BusinessException {
public TokenBadRequestException() {
super(ErrorMessage.DOES_NOT_MATCH_REFRESH_TOKEN);
}
+
+ public TokenBadRequestException(String message) {
+ super(message);
+ }
}📝 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.
| public class TokenBadRequestException extends BusinessException { | |
| public TokenBadRequestException() { | |
| super(ErrorMessage.DOES_NOT_MATCH_REFRESH_TOKEN); | |
| } | |
| } | |
| public class TokenBadRequestException extends BusinessException { | |
| public TokenBadRequestException() { | |
| super(ErrorMessage.DOES_NOT_MATCH_REFRESH_TOKEN); | |
| } | |
| public TokenBadRequestException(String message) { | |
| super(message); | |
| } | |
| } |
🤖 Prompt for AI Agents
In
src/main/java/com/example/gtable/global/security/exception/TokenBadRequestException.java
around lines 3 to 8, the class currently only has a default constructor with a
fixed error message. To improve flexibility in exception handling, add an
additional constructor that accepts a custom error message string and passes it
to the superclass constructor. This allows callers to specify different messages
when throwing this exception.
src/main/java/com/example/gtable/token/dto/AuthenticationResponse.java
Outdated
Show resolved
Hide resolved
| // userId와 role 값 추출 | ||
| Long userId = jwtUtil.getUserId(accessToken); | ||
| String roleString = jwtUtil.getRole(accessToken); | ||
|
|
||
| User user = User.createUserWithId(userId, "sampleEmail", "sampleNickname", "sampleProfileImg" | ||
| , SocialType.KAKAO, Role.fromString(roleString)); | ||
|
|
||
| CustomOAuth2User customOAuth2User = new CustomOAuth2User(user); |
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.
DB 조회 없이 더미 User 생성은 권한·정보 불일치 위험
토큰에 포함된 정보만으로 sampleEmail 등의 임시 값을 넣어 User 를 생성하면, 컨트롤러 단에서 이메일·닉네임을 사용하는 경우 실제 데이터와 달라 혼란을 초래할 수 있습니다. UserRepository 로 조회하거나 토큰에 필요한 정보를 전부 담아 두는 방식으로 수정해 주세요.
🤖 Prompt for AI Agents
In
src/main/java/com/example/gtable/global/security/jwt/JwtAuthorizationFilter.java
around lines 71 to 78, avoid creating a dummy User with placeholder values like
"sampleEmail" without database verification. Instead, fetch the full User entity
from the database using UserRepository based on the userId extracted from the
token, or ensure the token contains all necessary user information to construct
an accurate User object. This prevents inconsistencies between token data and
actual user details used in the application.
| try { | ||
| jwtUtil.isExpired(accessToken); | ||
| } catch (ExpiredJwtException e) { | ||
|
|
||
| // response status code + msg | ||
| response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); | ||
| response.getWriter().print("access token expired"); | ||
|
|
||
| log.info("JwtAuthorizationFilter 2 "); | ||
| return; | ||
| } |
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.
🛠️ Refactor suggestion
만료 응답 포맷과 헤더 세팅 필요
401 응답 시 Content-Type, 캐시 제어 헤더를 지정하지 않고 평문 메시지를 출력하고 있습니다. 프런트엔드에서 JSON 을 기대할 가능성이 높으므로 일관된 에러 포맷(JSON/UTF-8)으로 반환하고 response.flushBuffer() 호출을 잊지 마세요.
🤖 Prompt for AI Agents
In
src/main/java/com/example/gtable/global/security/jwt/JwtAuthorizationFilter.java
lines 47 to 57, when catching ExpiredJwtException and returning a 401 response,
you need to set the response headers properly. Set the Content-Type header to
"application/json;charset=UTF-8" and add cache control headers to prevent
caching. Return a JSON-formatted error message instead of plain text, and call
response.flushBuffer() after writing the response to ensure it is sent
immediately.
src/main/java/com/example/gtable/global/security/exception/GlobalExceptionHandler.java
Outdated
Show resolved
Hide resolved
- 중복 메서드명 변경 - DTO 안정성 추가
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
♻️ Duplicate comments (1)
src/main/java/com/example/gtable/token/dto/AuthenticationResponse.java (1)
1-17: 이전 리뷰 피드백이 완벽하게 반영된 우수한 구현입니다! 🎉이전 리뷰에서 제안되었던 모든 보안 개선사항들이 세심하게 적용되어 있어 정말 훌륭합니다:
✅ 불변성 보장:
final키워드로 토큰 필드의 변경을 방지
✅ 로깅 보안:@ToString(exclude = {...})로 민감한 토큰 정보가 로그에 노출되는 것을 방지
✅ API 표준 준수:@JsonProperty로 snake_case JSON 필드명 사용
✅ 생성자 최적화:@RequiredArgsConstructor로 final 필드만을 위한 생성자 생성특히 토큰과 같은 민감한 정보를 다루는 DTO에서 보안을 최우선으로 고려한 설계가 매우 인상적입니다. 이런 세심한 보안 고려사항들이 전체 애플리케이션의 보안 수준을 크게 향상시킬 것입니다.
🧹 Nitpick comments (1)
src/main/java/com/example/gtable/user/service/UserService.java (1)
9-14: UserService 클래스 구조가 잘 설정되어 있지만, 비즈니스 로직이 필요합니다.Spring 서비스 어노테이션과 Lombok을 사용한 의존성 주입이 올바르게 구성되어 있어 좋습니다. 하지만 현재 클래스가 비어있어서 실제 사용자 관리 기능이 구현되지 않은 상태입니다.
카카오 로그인 구현을 위해서는 다음과 같은 메서드들이 필요할 것 같습니다:
- 이메일로 사용자 조회
- 새 사용자 생성 (OAuth2 첫 로그인 시)
- 사용자 정보 업데이트
- 사용자 인증 정보 처리
사용자 서비스의 핵심 비즈니스 로직을 구현하는 데 도움이 필요하시면 말씀해 주세요. 카카오 OAuth2 인증 플로우에 맞는 메서드들을 제안해드릴 수 있습니다.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (9)
build.gradle(1 hunks)src/main/java/com/example/gtable/global/security/exception/GlobalExceptionHandler.java(1 hunks)src/main/java/com/example/gtable/global/security/jwt/JwtUtil.java(1 hunks)src/main/java/com/example/gtable/token/dto/AuthenticationResponse.java(1 hunks)src/main/java/com/example/gtable/token/dto/RefreshTokenRequest.java(1 hunks)src/main/java/com/example/gtable/user/dto/UserResponseDto.java(1 hunks)src/main/java/com/example/gtable/user/entity/Role.java(1 hunks)src/main/java/com/example/gtable/user/entity/User.java(1 hunks)src/main/java/com/example/gtable/user/service/UserService.java(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- src/main/java/com/example/gtable/user/dto/UserResponseDto.java
🚧 Files skipped from review as they are similar to previous changes (6)
- src/main/java/com/example/gtable/token/dto/RefreshTokenRequest.java
- src/main/java/com/example/gtable/user/entity/Role.java
- src/main/java/com/example/gtable/user/entity/User.java
- build.gradle
- src/main/java/com/example/gtable/global/security/exception/GlobalExceptionHandler.java
- src/main/java/com/example/gtable/global/security/jwt/JwtUtil.java
작업 요약
Issue Link
#10
문제점 및 어려움
해결 방안
Reference
Summary by CodeRabbit
신규 기능
기능 제거
스타일
문서화