실전 API 연습 2. Member Controller

변현섭·2023년 7월 4일
0
post-thumbnail

2. logout()

1) getExpiration()


① getMemberIdx() 메서드와 비슷한 원리로 Jwt를 파싱하여 만료시간에 대한 정보를 추출하고 있다.

② 만료시간에 대한 정보를 추출하는 getExpiration() 메서드의 반환타입은 Date이다.

③ Date를 Long타입으로 변환하기 위해 사용할 수 있는 메서드는 getTime() 메서드이다. 즉, getTime() 메서드를 통해 현재 날짜와 시간에 대한 정보를 Long형으로 변환한다.

④ System.currentTimeMills()는 현재 날짜와 시간에 대한 정보를 밀리초 단위로 반환하는 메서드로, 반환타입은 Long이다.

※ 날짜와 시간을 Long으로 바꾸는 원리
특정 날짜와 시간은 1970년 1월 1일부터 경과한 시간을 밀리초 단위로 환산하여 반환하면 현재의 시간을 Long형 정수로 나타낼 수 있다.

⑤ 토큰의 만료시간과 현재 시간 사이의 차이를 반환한다. 이미 만료된 상태라면 음수를 반환한다.

⑥ 메서드의 이름에서 착각하면 안되는 게 토큰의 만료시간을 반환하는 메서드가 아니라, 토큰의 만료까지 남은 시간을 반환하는 메서드이다.

2) Redis 데이터베이스

Redis가 무엇인지 모른다면 아래의 설명을 보기 전에 반드시 아래의 링크를 읽어보기 바란다.
>> Redis 데이터베이스

① redisTemplate

  • Redis 데이터베이스에 액세스하기 위해 사용되는 RedisTemplate 객체이다.
  • 이 객체를 통해 Redis 연결 및 데이터 조작에 필요한 기능을 사용할 수 있다.
  • 먼저는 Service에 아래와 같이 객체를 생성해주어야한다.

② opsForValue()

  • RedisTemplate 객체의 메서드 중 값(Value)에 대한 작업을 수행하기 위한 메서드를 호출하기 위해 사용한다.
  • 이를 통해 Redis의 문자열 값에 접근할 수 있다.

③ set(accessToken, "logout", expiration, TimeUnit.MILLISECONDS)

  • Redis에 값을 저장하는 메서드이다.
  • 여기서 accessToken은 Redis의 키(Key)로 사용될 값이다.
  • "logout"은 해당 키에 저장될 값이다. 일반적으로 logout이 key이고, accessToken이 value일거라 생각하는데 그렇지 않다. 이는 key가 사용자마다 고유해야 하기 때문이다.
  • Redis는 key-value쌍의 NoSQL 기반 데이터베이스이므로, key 값으로 데이터를 찾는다. 따라서 access token이 Redis 데이터베이스에 있는지 검사하기 위해선 access token이 key가 되어야 한다.
  • expiration은 해당 키의 유효기간을 나타내며, TimeUnit.MILLISECONDS는 유효기간의 단위를 밀리초로 지정한다.

이제 저번에 미처 설명하지 못한 checkBlackToken 메서드를 다시보자.

  • accessToken을 key로 갖는 value를 찾는다.
  • value(=logout)가 있으면, 이는 블랙토큰이라는 뜻이므로 true를 반환한다.
  • 반대로 key(=accessToken)에 대한 value가 없다는 건, Redis에 accessToken이 없다는 것이므로, false를 반환한다.

Ⅴ. Member Controller "update" 요청

1. getMemberIdx()


1) getLogoutMemberIdx()와의 유사점

  • getMemberIdx()는 이전 포스팅에서 설명한 getLogoutMemberIdx()와 거의 유사하다.
  • 다만, ExpiredJwtException이 발생했을 때의 처리로직만 다르다.
  • 이는 로그아웃 요청을 제외한 요청에 대해선 Access Token이 만료되었더라도, Refresh Token이 유효하다면 재발급을 통해 요청을 처리할 수 있어야하기 때문이다.
  • 즉, 사용자 인증이 필요한 모든 API(로그아웃 제외)에 대해 getMemberIdx() 메서드를 사용해야 한다.

2) ExpiredJwtException

JWT가 만료되었을 때 발생하는 예외이다. 만약 요청 헤더에 포함된 JWT가 만료된 JWT일 경우, 이 예외에 해당하는 catch문이 실행된다. accessToken에 해당하는 멤버를 찾고, refreshAccessToken()에 그 멤버의 refreshToken 값을 입력해 새로운 accessToken을 재발급한다.

① refreshAccessToken()

  • 회원가입을 한 멤버라면 refreshToken은 적어도 null은 아니다. 값이 존재하거나 ""일 것이다.
  • 따라서 null인지를 검사할 필요는 없고, ""인지만 체크하면 된다.

    ※ ""과 null의 차이
    ""은 길이가 0인 문자열을 의미한다. 즉, 어떠한 문자도 포함하지 않는 문자열입니다. ""은 유효한 값으로 간주되며, 메모리에 실제로 할당되는 값이다.
    반면, null은 아무런 값도 가지지 않음을 나타내는 특별한 상태이다. null은 값이 없음을 의미하며, 메모리상에서 참조가 없기 때문에 유효하지 않은 값으로 간주된다. 따라서, null에서 값을 가져올 경우 NPE가 발생하기 때문에 주의해야 한다.

  • refresh token의 유효성을 검사하여 refresh token이 만료되었거나 유효하지 않은 경우, 예외로 처리한다(이에 대해서는 ②에서 자세히 이야기하기로 하자).
  • 유효한 refresh token이라는 것이 확인되면, refreshedAccessToken이라는 새로운 access token으로 업데이트 된다.

② Exception Handling

  • 위에서 예외가 발생할 경우, BaseException으로 예외가 처리되지 않는다.

  • 그 이유는 예외처리를 담당하는 핸들러 메서드를 따로 정의해두었기 때문이다. 이 예외처리는 ExceptionController에 모아두었다.

  • ExpiredJwtException에 의해 throw 되는 모든 예외는 결국 위의 핸들러 메서드에 의해 일괄적으로 처리된다.

  • @ResponseStatus는 핸들러 메서드가 발생시킨 예외 또는 요청을 처리한 결과에 대한 HTTP 응답 상태 코드를 지정하는 데 사용된다. 위 예시에서는 BAD_REQUEST 즉, 400으로 상태 코드를 설정하고 있다.

  • e.printStackTrace()는 예외가 발생한 위치를 출력하는 메서드로, 주로 디버깅 목적으로 사용된다.

  • 다시 말해 EXPIRED_USER_JWT, INVALID_JWT, FAILED_TO_REFRESH가 호출되지 않고, 그저 {"code" : "403", "msg" : "ExpiredJwtException"}이 Json 형식으로 사용자에게 반환될 것이다.

즉, 이 메서드는 Authorization 필드에 담긴 JWT에서 멤버의 ID를 추출하는 역할을 수행한다. Access Token이 만료된 경우, Refresh Token의 유효성을 평가해 Access Token을 재발급한 후 멤버의 ID를 반환한다.

2. 변경점

지난 "update" 요청은 이메일 필드를 포함시켜 사용자 인증을 처리하였다. 그러나 이제는 JWT만 가지고 사용자 인증을 하고 있다. 닉네임 변경을 요청한 사람의 닉네임을 바꾸기 때문에, 다른 사람의 닉네임을 변경하는 것이 불가능해진다. 이제 사용자로부터 이메일을 입력받지 않아도 되기 때문에 사용자 편의성이 증진되고 코드가 간결해진다.

Ⅵ. Member Controller "update-profile" 요청

S3를 이용하는 자세한 로직에 대해서는 아래의 링크에서 확인할 수 있다.
>> 사진 업로드 API
프로필 변경에는 아래와 같이 4가지 경우가 있다. 이 4가지 모두에 대해 고려하여 분기문을 작성해야 한다.

즉, 기존 프로필이 없는 사용자가 이번에도 사진 없이 수정을 누른다면, 아무 처리도 안하면 되고, 이번에는 사진을 업로드한다면, 이를 S3에 업로드하고 Repository에 저장해야 한다.

또한, 기존 프로필이 있는 사용자가 이번에는 사진 없이 수정을 누른다면, 기존 사진을 S3에서 삭제하고 Repository에서 지워야 할 것이고, 다른 사진으로 업로드하면, 업로드 한 사진을 S3에 업로드하고 Repository에 저장해야 한다. (동일한 사진으로 업데이트하는 경우도 업데이트가 진행된다.)

이로써, Member Controller의 요청에 대한 설명을 모두 마쳤다. Member 이외에도 Board나 Comment 컨트롤러의 요청도 비슷하게 이루어졌다보니, 이 정도만 설명해도 모든 코드를 이해하는 데 부족함이 없을 거 같다. 설명은 생략하더라도 반드시 한번씩 읽어보기 바란다. 다음 포스팅에선 지금까지 구현한 API를 테스트해보고 결과를 확인해보도록 하자.

profile
Java Spring, Android Kotlin, Node.js, ML/DL 개발을 공부하는 인하대학교 정보통신공학과 학생입니다.

0개의 댓글