열심히 프로젝트 에러 수정을 하며 포스트맨으로 테스트를 하던 도중 문제가 발생하였다.
갑자기 로그인 후 마이페이지 조회가 안 되는 것이다 😱 (계속 테스트 하던 도중 안 됐다)
분명 방금 발급받은 토큰으로 동일한 memberId의 회원을 조회하고 있는데 401 에러( Unauthorized User )가 계속 떴다. 🫠
🚨 에러 발생 / ⚠️ Unauthorized error happened: JWT expired at 2023-10-12T11:35:37Z. Current time: 2023-10-12T11:35:44Z, a difference of 7897 milliseconds. Allowed clock skew: 0 milliseconds.
계속해서 이런 메세지가 떴는데 이 때 나는 회원 마이페이지에 Board List 가 안 뜬다고 해서 그걸 수정 하고 확인 하기 위한 작업을 하고 있었고, 다른 백엔드 팀원은 서버에 DB 연결을 다시 하기 위해 인스턴스를 중지 시켰다가 다시 연결했다. 그리고 프론트에서는 시간 표현방법을 변경하였다.
✔️ 내가 만진 파일은 Member Entity에서 cascade 부분과 Board Entity에서의 @JsonIgnore 삭제 뿐이었다.
무엇이 문제를 발생시켰는지 원인은 찾지 못했지만 계속해서 만료 시간이 현재 시간과 9시간 차이 나게 나타나고 있었다.
9시간 차이가 나는 것에 대해 검색 해보니 UTC 시간 보다 한국 시간이 9시간 빠르게 나오고 있었고 이것 때문에 생기는 문제가 아닐까 생각이 들었다.
🛠️ 문제 해결을 위한 노력
1. 서버, 클라이언트 시간 확인
서버의 시간을 확인해보기 위해 팀원들에게 문제 사항을 곧바로 공유했고 팀원들은 바로 응답하여 서버 및 클라이언트 시간을 확인 해주었다. AWS 환경에서 서버 시간을 확인 후 한국 시간으로 변경해주었다.
- 시간 확인
$ date
- Asia/Seoul로 시간 변경
$ sudo rm /etc/localtime
$ sudo ln -s /usr/share/zoneinfo/Asia/Seoul /etc/localtime
이후 모두 한국 시간으로 변경 된 것을 확인했고 테스트를 다시 진행해보았다. 하지만 결과는 실패!
2. System.currentTimeMillis() - 현재 시간 밀리초로 변경
구글링을 해보니 나와 동일한 에러가 나타난 분이 계셨고 그 방법을 따라 메서드 수정을 해보았다.
`System.currentTimeMillis()` 는 타임존에 관계 없이 일관된 기준을 가지고 토큰 만료 시간을 계산할 수 있는 방법이다.
// AccessToken 만료 시간 설정
Date expiration = new Date(System.currentTimeMillis() + (jwtTokenizer.getAccessTokenExpirationMinutes() * 1000L)); // 만료시간 설정
// RefreshToken 만료 시간 설정
Date refreshTokenExpiration = new Date(System.currentTimeMillis() + (jwtTokenizer.getRefreshTokenExpirationMinutes() * 1000L));
하지만 결과는 여전히 실패였다 🥹
🔗https://okky.kr/questions/1364006
3. 토큰 만료 기간 늘리기
실패로 떴을 때 토큰의 만료 기간을 6시간으로 설정해두었을 때였는데 9시간 이전에 만료되었다고 하니 더 길게 늘려보기로 하였다.
토큰의 시간을 24시간으로 늘려보았는데 여전히 토큰의 유효기간은 9시간 전에 끝났다고 떴다🥲
4. Calendar.getInstance().getTime() - UTC 시간 사용하기
로컬 시간이 아닌 UTC 시간으로 토큰 생성을 해보았다.
구글링을 해도 에러에 관련한 해결 방법이 많이 나오지 않아 결국 GPT에게 물어보았다.
GPT에는 에러 메세지와 내 jwt 생성 코드 Calendar, Date 만 복사하여 문제가 있는지 물어보았는데 아래와 같이 답변이 와서 수정을 해보았다. ( * 이 때 2번에서 진행한 것은 실패 후 지운 상태였다.)
그리고 결과는 또 실패 ...
오후 세시부터였던가... 저녁도 안 먹고 열두시까지 앉아서 이것만 보고 있었는데 해결도 안 되고 정말 방법은 안 나오고 뭘 해도 답은 안 나오니 너무 답답했다 😖 (오전에 스터디가 있어서 CS 스터디 과제도 해야하는 상태였는데 내가 이걸 고치지 않으면 조원들이 아무것도 못 한다는 생각에 어떻게든 끝 내려고 했다...)
어떻게든 끝내고 싶었고 그래서 AWS 배포를 맡은 팀원에게 따로 연락하여 아이디와 비밀번호를 받았다.
5. 인스턴스 중지 후 다시 연결
인스턴스 중지, 다시 연결 이 과정을 세 번 정도 반복했던가... .....
갑자기 로그인 후 모든 과정들이 해결됐다. 토큰도 잘 받아졌고 토큰이 유효하지 않다는 이야기도 나오지 않았다. 토큰의 시간도 다시 짧게 변경해보았는데 문제 없었다! 야호~~! 🙌
진짜 기쁜 와중에 내가 서버 재연결 문제 때문에 오늘 9시간을 꼼짝없이 화장실 가는 것도 참으면서 앉아있었던 걸까? 하는 생각이 들어서 웃음이 나왔다 😹😹😹 haha funny...
그렇다면 이 문제는 동기화 문제가 맞았을까?! 하고 궁금하기도 한데 답을 알 수 없어 정말 답답한 부분이긴 하다. 답을 물을 수 있는 곳이 있다면 좋을 텐데 🧐 아무튼 이렇게 문제 해결 후 다른 오류들도 다 금방 수정할 수 있었고 테스트를 잘 마친 후 push 하고 잠들었다.
하지만!
이렇게 끝이어야 했을 문제였는데 다음 날 또 발생되었다. 다음 날이 되니 서버는 또 먹통이 되어 있었다.... 그리고 동일한 문제가 또 발생하고 있었다.
6. 4번 코드 수정 - Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
이 때 코드는 4번을 유지하고 있었는데 다음날 되니 계속 에러가 났고 서버에서 시간이 GMT 시간으로 나온다길래 TimeZone 의 타입을 GMT로 변경해보았다.
당연히 결과는 실패하였고 아예 처음의 코드로 돌려놓았다.
🔑 문제 해결 방법
@PostConstruct 어노테이션을 활용한 필드 초기화
시간과 관련된 어노테이션을 살펴보다 보니 내가 작성한 accessToken의 만료 기간 설정과 문제가 있지는 않을까? 하고 의문이 생겼다. (물론 이전에 잘 작동했고 테스트도 잘 진행했다)
@Value("${jwt.access-token-expiration-minutes}")
private int originMinutes;
@Getter
private final int accessTokenExpirationMinutes = originMinutes * 6;
이렇게 accessToken의 만료 기간을 설정해주고 있었다.
*(accessToken)의 만료기간을 처음에는 1시간으로 정해뒀는데 프론트에서 길게 수정을 요청하여 유동적으로 변경하기 위해 위와 같이 코드를 수정하였다.
이렇게 수정하면서 originMinutes 필드가 초기화되지 않아 필드로 인식되지 않고 있을 수 있다는 것이 문제를 일으킬 수 있겠다 생각하였다. 그래서 필드를 초기화 시켜주는 변수인 @PostContruct 어노테이션을 사용하였다.
@Value("${jwt.access-token-expiration-minutes}")
private int originMinutes;
@Getter
private int accessTokenExpirationMinutes;
@PostConstruct
public void init() {
this.accessTokenExpirationMinutes = this.originMinutes * 24;
}
@PostConstruct 어노테이션이 붙은 init 메소드는 Spring이 모든 의존성을 주입한 후에 실행된다. 이를 통해 accessTokenExpirationMinutes 필드가 올바르게 초기화될 수 있다.
이후 다시 테스트 해보니 어떻게 해도 성공!! 이었다. 진짜 성공!! 🙂
💬 느낀 점
문제는 spirng의 원칙을 지키지 못 한데 있었고 다시 한 번 더 Spring의 기본 특징들에 대해 공부하고 생각할 수 있는 기회가 되었다.
하지만, 사실 중간에 잠깐 됐던 것도 그렇고 테스트도 잘 하고 있다가 갑자기 안 되던 것이 조금 찝찝하다.😶 만약 또 서버가 정지되는 순간에 동일한 에러가 발생한다면 어떻게 해야할까?
그리고 서버와의 문제는 정말 찾기가 쉽지 않다는 것이 너무 어려운 부분인 듯 하다.