refactor: 카카오 로그인·회원가입 로직 및 리프레시 토큰 관리 방법 리팩토링(#70)#71
Conversation
95f8a04 to
7bb241b
Compare
7bb241b to
a96af29
Compare
chaiminwoo0223
left a comment
There was a problem hiding this comment.
수고 많으셨습니다. 보안을 세심히 고려하신 점이 인상 깊었습니다. 몇가지 궁금한 내용이 있어서 코드 리뷰를 남겼습니다. 확인 부탁드려요~^^
| public record TokenInfo(String accessToken, String refreshToken, long refreshTokenExpiresIn) { | ||
| public static TokenInfo of( | ||
| String accessToken, String refreshToken, long refreshTokenExpiresIn) { | ||
| return new TokenInfo(accessToken, refreshToken, refreshTokenExpiresIn); |
There was a problem hiding this comment.
refreshTokenExpiresIn(리프레시토큰 만료시간)을 DTO에 같이 담아서 전달하고 있네요. 같이 전달한 이유가 궁금합니다.
There was a problem hiding this comment.
쿠키 관리는 HTTP와 관련된 로직이므로 Presentation 계층에서 전담하도록 설정해두었습니다.
리프레시 토큰과 쿠키의 만료시간을 동일하게 관리하기 위해 Application 계층의 TokenInfo DTO에서 발급된 리프레시 토큰의 만료시간을 함께 반환하고,
Presentation 계층에서 리프레시 쿠키를 생성할 때 토큰의 실제 만료시간을 기준으로 쿠키의 생명주기를 정확하게 관리하고자 refreshTokenExpiresIn(리프레시토큰 만료시간)을 함께 반환하도록 구성했습니다.
| private final String value; | ||
| public static final String AUTH_REISSUE_TOKEN_PREFIX = "auth::reissue::token:"; | ||
| public static final String AUTH_LOGOUT_TOKEN_PREFIX = "auth::logout::token:"; | ||
| public static final String OAUTH_SIGNUP_PROFILE_PREFIX = "%s::signup::profile:"; |
There was a problem hiding this comment.
%s::signup::profile:에서 %s는 어떻게 사용이 되는지 궁금합니다.
There was a problem hiding this comment.
다양한 OAuth 가입 프로필을 저장할 때 OAuth Provider별 고유한 키로 관리하고자 %s 네임스페이스 플레이스홀더를 사용했습니다.
이 방식을 사용하면 kakao::signup::profile, google::signup::profile 같은 제공자별 상수를 별도로 관리하지 않고, formatted() 메서드로 런타임에 동적으로 상황에 맞는 키를 생성할 수 있습니다.
%s에는 kakao, google, naver 같은 OAuth 제공자 식별자가 들어가도록 설정했고, 최종적인 키의 형태는 <provider>::signup::profile:<signupKey> 형태로 키가 저장됩니다.
문자열은 +나 StringBuilder로 이어 붙일 수 있지만, 가독성 측면에서 %s포맷을 우선 선택했습니다.
다만 %s/formatted()는 내부적으로 매 호출마다 Formatter 객체 생성, 포맷 문자열 파싱, Object 배열 할당 작업이 일어나 +/StringBuilder보다 확실히 무겁고 성능적으로 떨어집니다.
일반적인 트래픽에서는 체감이 미미하다고는 하나 대량/대규모 처리 시에는 체감이 크게 날 수도 있습니다.
가독성을 위해 %s포맷을 사용하는 방법과 성능적인 부분을 챙겨가는 방법 중에 어떤 방법이 더 좋을까요?
There was a problem hiding this comment.
운영 환경을 고려했을 때 성능적인 부분도 분명 중요하지만, 현재 방식으로도 충분히 안정적으로 동작할 수 있을 것 같아요. 지금 방식을 유지해도 괜찮을 것 같아요.
| memberRepository.findBySocialProviderAndSocialId(socialProvider, socialId); | ||
|
|
||
| MemberPolicy.validateNotDeleted(member); | ||
| if (member.isEmpty()) return Optional.empty(); |
There was a problem hiding this comment.
if(member.isEmpty) ...도 다른 검증 로직처럼 MemebrPolicy에 두면, 재사용성과 유지보수 측면에서 더 좋을 것 같아요.
There was a problem hiding this comment.
if (member.isEmpty()) ... 검증을 MemberPolicy에 두면 분기 처리하는데 있어 좀 애매해질 수 있을 것 같습니다.
Optional<Member>가 비어 있을 경우 예외로 처리를 해야하는 상황이라면 MemberPolicy로 두는게 적절해보이지만, 지금 같은 경우는 예외 없이 케이스별 다른 분기를 가져야합니다.
MemberPolicy에 boolean validateIsEmpty(Optional<Member> member) 메서드를 생성해 검증을 할 수 있지만, 결국 MemberService에서 다시 if (MemberPolicy.validateIsEmpty(member)) return Optional.empty()처럼 다시 분기 처리를 해줘야하는 상황이 발생할 수 있습니다.
대신 if (member.isEmpty())와 MemberPolicy.validateNotDeleted() 로직을 하나의 Optional 흐름으로 합쳐 map으로 처리하도록 수정할 수 있을 것 같습니다.
There was a problem hiding this comment.
확인했습니다. 그럼 MemberService에 if(member.isEmpty) ...를 그대로 유지하는게 좋겠네요.
chaiminwoo0223
left a comment
There was a problem hiding this comment.
고생하셨습니다. 커밋 내용 병합 후 머지 부탁드립니다!
* feat: 인증 관련 쿠키를 관리하는 AuthCookieHelper 구현 * feat: 쿠키 관련 상수를 관리하는 CookieConstants final class 추가 * refactor: 카카오 로그인 로직 리팩토링 * refactor: 카카오 회원가입 로직 리팩토링 * refactor: 토큰 재발급, 로그아웃 로직 리팩토링 * chore: global.common.constants 상수 클래스를 enum -> final class로 변경 * refactor: /.well-known 정적 리소스 경로 추가 * test: KakaoSignupProfileService 단위 테스트 추가 * test: AuthController 통합 테스트 코드 리팩토링 * test: 카카오 로그인, 회원가입, 로그아웃 Fixture 클래스 리팩토링경 * test: KakaoLoginService 단위 테스트 코드 리팩토링 * test: MemberService 단위 테스트 코드 리팩토링
a96af29 to
2b8fac2
Compare
📌 작업 내용 및 특이사항
✅ 인증 관련 쿠키 관리
AuthCookieHelper를 구현했습니다.setOAuthSignupKeyCookie(),setRefreshTokenCookie()와 쿠키 이름을 기반으로 쿠키를 삭제하는 메서드cleareCookie()를 구현했습니다.HttpOnly,Secure를 함께 적용하고 브라우저와 전송 경로 모두에서 보호되도록 보안을 강화했습니다.CookieConstantsfinal class를 추가했습니다.✅ 카카오 로그인 & 카카오 회원가입 로직 개선
위와 같이 흐름을 개선하면서, 기존엔 클라이언트와 서버 모두 로그인/회원가입에서 각각 카카오 OAuth 서버를 호출했지만 이제는 로그인 시 한 번만 호출하도록 단순화되었습니다.
또한, 쿠키를 활용하기 때문에 회원가입 화면에서 새로고침이 되더라도 인가 코드가 사라지는 문제를 해결했습니다.
카카오 회원가입 로직을 담당하는
KakaoSignupProfileService클래스를 구현했습니다.카카오 가입 프로필을 Redis에 저장·관리 및 키 발급을 담당하는
KakaoSignupProfileRedisRepository와 구현체Adapter클래스를 구현하고,KakaoSignupProfile모델을 구현했습니다.카카오 가입 키 관련 에러코드를 추가했습니다. (
INVALID_KAKAO_SIGNUP_KEY,MISSING_KAKAO_SIGNUP_KEY)✅ 리프레시 토큰 관리 방법 개선
AccessToken과RefreshToken을 응답 Body에 담아 응답하고, 리프레시 토큰을 클라이언트LocalStorage에 보관하던 방식을, 보안 강화를 위해 서버가HttpOnly + Secure쿠키로 발급·관리하는 방식으로 개선했습니다. ->AccessToken만 응답 Body에 담아 응답✅ 관련 테스트 코드 수정
AuthController통합 테스트,KakaoLoginService단위 테스트 코드를 개선된 로직에 맞게 수정했습니다. (쿠키 적용)KakaoSignupProfileService단위 테스트를 추가했습니다.MemberService의 소셜 정보로 멤버를 조회하는 로직의 반환 값을Optinal<Member>로 수정하면서 테스트 코드도 함께 수정했습니다.✅ 기타 사항
AuthFacade의 카카오 로그인·회원가입, 토큰 재발급 흐름에서Application계층의 반환값과Presentation계층의 응답 값을 분리했습니다.로그인은 가입 여부에 따라 다르게 분기 처리가 필요하기 때문에 Application에서는 결과를 표현하는
OAuthLoginOutcomeDTO를 두어 반환하고,기존 토큰 정보를 담은
TokenResponse를TokenInfo로 바꿔Application계층의 DTO로 두고, 실제 클라이언트 응답은 역할과 의미가 명확하도록LoginResponse,ReissueTokenResponsePresentationDTO로 새로 구성했습니다.OAuthLoginOutcome))는OAuthprefix를 사용해 명명했습니다.global.common.constants의 상수 클래스들을enum class->final class로 변경했습니다.CookieConstants에서 쿠키 이름과 TTL을 함께 관리하는데, 두 필드의 타입이 달라enum으로 관리하면 모든 상수에 두 필드를 강제로 두어야 하고, 사용하지 않는 필드는null로 채워야합니다.사용하지 않는 필드를
null값으로 대체하는 방법보다 다양한 타입을 안전하게 다룰 수 있는final class기반 상수 클래스로 전환하고,UrlConstants,CacheConstansts도final class로 변경해 일관되도록 했습니다.🌱 관련 이슈
🔍 참고사항(선택)
[브라우저 쿠키 자동 전송 테스트(크롬 기준)]
Cookie에auth_refresh쿠키가 저장되어 있는 상태입니다.HTTP 요청 시


Request Header에 발급받은auth_refresh쿠키가 없음을 확인할 수 있습니다.Request Header에 리프레시 쿠키가 없어 에러가 발생한 모습입니다.HTTPS 요청 시


Request Header.Cookie에auth_refresh=<발급받은 리프레시 토큰>이 있는 걸 확인할 수 있습니다.Postman/Talend API Tester등 API 테스트 툴은 브라우저 보안 정책을 그대로 적용하지 않아HTTP에서도Secure여부와 무관하게 쿠키가 전송되는 것처럼 동작했습니다.크롬은
localhost/127.0.0.1를 안전한 컨텍스트로 특례 취급해HTTP에서도Secure쿠키가 전송되는 현상이 있었습니다.hosts에api.studytrip.local같은 새로운 호스트를 추가해 테스트 도메인을 분리했고,HTTP에서는Secure쿠키가 전송되지 않고HTTPS에서만 전송됨을 확인했습니다.📚 기타(선택)