OAuth 왜 이렇게 개발했나요?
AuthFacadeService와 AuthService는 모두 로그인 또는 회원가입을 처리합니다.
하지만, 한 클래스에서 진행하지 않고 별도의 클래스로 분리하였습니다.
그 이유는 transaction과 관련있습니다.
로그인 또는 회원가입을 하기 위해서는 외부 API를 호출해야 합니다.
하지만, 외부 API는 우리가 제어할 수 있는 것이 아닙니다.
즉, 외부 API(슬랙)에서 요청에 대한 처리가 오래 걸린다면 우리 서비스는 무턱대고 기다릴 수 밖에 없습니다.
하지만, service 클래스는 transactinon(즉 데이터베이스와의 커넥션)을 처리하는 게 주 책임입니다.
하지만, 데이터베이스의 커넥션은 한정된 개수
를 가지고 있습니다.
따라서 외부 API 응답 대기 시간이 길어질수록 우리 서비스에서는 데이터베이스 커넥션을 확보하지 못 해서 새로운 유저의 요청을 받을 수 없을 수 있습니다.
자세한 설명은 아래 글을 참고해주세요.
트랜잭션 내에 외부 리소스 요청이 담기게 되면 어떤 문제가 발생할까?
Q1. 한 클래스에서 private로 분리해서 @transaction 을 하면 되지 않나?
하지만, @transaction은 AOP 기반으로 동작합니다.
즉, 프록시 객체를 생성하여 프록시 객체에서 @transaction이 붙은 메서드를 호출하는 방식입니다.
하지만, private로 접근제어자를 가지고 있으면 외부 클래스에서 호출할 수 없기 때문에 private 메서드에만@transaction을 적용할 수 없습니다.
위 내용을 정리하겠습니다.
Oauth 서버의 토큰을 그대로 사용하는 것은 우리 서비스가 외부 서비스(슬랙)에 너무 의존하게 된다고 판단하였습니다.
토큰은 우리 서비스에 대한 인증/인가를 하기 위함입니다.
따라서 토큰을 직접 컨트롤 할 수 있어야 하며, 토큰 전략을 결정할 수 있어야 한다고 생각합니다.
결과적으로 Slack에서 제공받은 토큰은 표시이름과 같은 Slack에서 필요한 정보를 빼오는 데만 사용하고
우리 서비스를 이용(인가)하는데는 우리 서비스에서 발급한 토큰을 이용합니다.
새로운 회원인지 기존 회원인지 판단하기 위해 OAuthId를 가지고 있어야 합니다.
OAuthId 를 가지고 있다면 ⇒ 기존 회원이므로 로그인을 진행합니다.
OAuthId 를 가지고 있지 않다면 ⇒ 신규 회원이므로 회원가입을 진행합니다.
OauthClientService
에서 진행합니다.JWT를 사용한다면 데이터베이스로터 사용자 상태를 조회하지 않아도 됩니다.
따라서 JWT와 관련된 로직을 작성할 때는 꼭 이점을 생각하시면 좋을 것 같습니다.
JWT를 사용함으로써 DB와 connection을 줄일 수 있다.
refresh token(이하 RT)의 경우 유효시간이 깁니다.
만약, 유저가 로그아웃이나 회원 탈퇴를 하기 전에
해커가 RT토큰을 탈취한다면 해커가 해당 RT토큰으로 재발급을 진행하여 계속해서 작업을 처리할 수 있습니다.
따라서 유저가 로그아웃이나 회원 탈퇴를 했다면 해당 RT 토큰을 블랙 리스트 토큰으로 지정하여
이후 해당 RT 토큰으로 재발 요청이 들어왔을 경우 유효하지 않은 토큰으로 예외를 터트려 재발급을 진행하지 못 하게 막습니다.
Q1. 그럼 왜 acceess token(이하 AT)의 경우 블랙 리스트 토큰을 따로 지정하지 않나요?
AT 토큰의 경우 굉장히 유효기간이 짧습니다.
그래도 AT 토큰도 블랙 리스트 토큰으로 지정한다면 오히려 보안에 좋은 게 아닌가요?
맞습니다.
하지만 이는 JWT 토큰의 장점(stateless)을 잃어버립니다.
인가 작업 시에 매번 DB에 커넥션 하여 블랙 리스트에 등록된 토큰인지 확인해야 하기에 JWT 토큰을 사용하는 의미가 없어집니다.(즉 이 경우는 JWT를 stateful하게 사용하게 된다.)
따라서 AT토큰은 따로 블랙리스트로 지정하지 않고 짧은 유효기간을 가도록 하였습니다.
Access 토큰 재발급 시 사용한 Refresh 토큰이 만료되지 않았을 지라도
현재는 Access , Refresh Token을 모두 재발급해주고 있습니다.
(이하 RTR 방식)
이 방법을 사용한 이유는 다음과 같습니다.
유효 시간이 긴 Refresh Token이 탈취가 되었을 지라도 재발급을 한다면 탈취된 토큰은 무효화가 된다.
즉, 보안상의 이유로 RTR 방식을 사용했습니다.
stateful하게 사용한 경우도 있지만
회원 탈퇴 시 다음과 같은 작업을 처리해야 합니다.
다음과 같은 작업을 처리해야 합니다.
이중 첫 번째 작업(사용자 정보 제거)는 주 작업이지만
2-3번째 작업은 회원 탈퇴를 진행하면서 처리해야 하는 후처리 작업입니다.
따라서 서비스 의존성을 분리하기 위해서 이벤트 발행을 사용하였습니다.
Q1. 이벤트 발행
으로 인한 장점
서비스 의존성 분리
후처리 작업이 늘어났을 때(예를 들어, 탈퇴 하는 회원이 생성한 프로그램을 제거해야 한다면)
이벤트 발행 전 : 의존해야 하는 서비스가 늘어남
이벤트 발행 후 : 이벤트만 발행하면 이벤트 리스너가 처리하면 됩니다.
Q2. 별도의 트랜젝션으로 처리한 이유
@Transactional(propagation = Propagation.REQUIRES_NEW)
와 관련
사용자 정보는 제거가 되었는데 후처리 작업에서 예외가 발생하여 롤백이 된다면 사용자 정보 제거 작업도 롤백이 되어야 할까요?
2-3번 작업은 정말로 후처리 작업입니다.
따라서 2-3번 작업에서 예외가 발생하여 롤백이 되었다고 해도 메인작업(사용자 정보 제거)이 롤백이 발생하지 않고 정상적으로 커밋되어야 한다고 생각했습니다.
따라서 별도의 트랜젝션으로 작업을 처리했습니다.
Q3. 비동기로 처리한 이유
@Async
와 관련
유저가 후처리 작업까지 기다릴 필요가 없다고 생각했습니다.
즉, 메인 작업이 처리되었으면 유저는 정상적인 응답을 받게 됩니다.
Q4. 만약 메인작업에서 롤백이 발생한다면? 비동기로 처리했기 때문에 메인작업이 정상적으로 처리되지 않았음에도 후처리 작업을 실행하는 거 아닌가요?
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
와 관련
예외가 발생할 경우를 고려하여 트랜젝션이 commit된 이후에 후처리 작업을 실행할 수 있게 처리합니다.
아래 문서를 참고해주세요
2~4번과 관련있는 코드입니다.(DeletedMemberEventListener.class)