작지만 꾸준한 반복

비관리 의존성을 트랜잭션에서 제외하기(feat 파사드 패턴) 본문

카테고리 없음

비관리 의존성을 트랜잭션에서 제외하기(feat 파사드 패턴)

iamjooon2 2023. 10. 9. 20:52

집사의고민 프로젝트에서는 소셜 로그인을 사용하고는데요

사용자 경험 측면에서 회원가입 과정이 간소해지고, 우리 사이트에서 관리해야할 사용자 정보 역시 줄어들어, 보안 측면에서도 이점이 있다고 생각했기 때문이었습니다

 

하지만 이 소셜로그인 과정에서 문제가 되는 점이 하나 있었는데요

 

코드와 함께 보자면

@Transactional
public TokenDto login(String authCode) {
    // 외부 인프라 호출
    String accessToken = oAuthClient.getAccessToken(authCode);
    OAuthMemberResponse oAuthMemberResponse = oAuthClient.getMember(accessToken);

    Member member = memberRepository.findByEmail(oAuthMemberResponse.getEmail())
            .orElseGet(() -> memberRepository.save(oAuthMemberResponse.toMember()));

    return createTokens(member.getId());
}

 

바로 oAuthClinet 객체와 협력하는 두 부분입니다.

oAuthClinet는 외부 인증서버 + 자원서버를 추상화한 객체로, 해당 객체를 통해 외부 자원서버의 엑세스 토큰을 발급한 다음

다시 받아온 엑세스 토큰을 통해 사용자의 정보를 받아오는 역할을 합니다

이 부분이 우리 트랜잭션 안에 묶여있다는 점이 고민이었습니다

 

RealMySQL에는 이런 내용이 나옵니다

메일전송이나 FTP 파일 전송 작업 또는 네트워크를 통해 원격 서버와 통신하는 등과 같은 작업은 어떻게 해서든 DBMS의 트랜잭션 내에서 제거하는 것이 좋다. 프로그램이 실행되는 동안 메일 서버와 통신할 수 없는 상황이 발생한다면 웹 서버뿐 아니라 DBMS서버까지 위험해지는 상황이 발생할 것이다 [Real MySQL 8.0, p159]

 

여기서 말하는 원격 서버와 통신하는 일 == 카카오 API 호출이었습니다

 

해당 카카오 API를 호출하는 부분은 우리 트랜잭션을 내부에 잡아둘 필요가 없습니다

외부 API와 통신하는 동안 불필요하게 우리 트랜잭션을 불필요하게 점유할 뿐더러,

혹여라도 외부 API에 장애가 발생하여 호출 시간이 길어진다면 불필요한 성능 저하를 일으킬 수 있습니다.

 

팀원과 고민을 나누었는데, HTTP 요청의 영역이라고 볼 수도 있으니.. Controller 계층으로 옮기는 것은 어떻냐는 의견을 받았지만

제가 생각한 Controller의 역할은 사용자의 입출력을 처리하고, 위는 로그인에 대한 비즈니스로직이라 Serivce의 영역이라 생각했습니다

 

결국 파사드 패턴을 적용하여 해결하였습니다

 

파사드 패턴은, 하위 객체를 복잡도를 숨기기위해

사용처에서 인터페이스를 하나 두는 디자인 패턴입니다

 

사용 전후 의존관계는 다음과 같아요

 

 

 

사실 처음에는 실제 외부 RESTApi를 사용하는 RestTemplate에 i/o timeout를 거는 방식을 사용했습니다

하지만 현실적으로 외부 인프라에 timeout을 얼마나 걸어야할지.. 잘 감이 오지 않았고

임의로 시간을 정해 timeout을 설정하였습니다

 

불확실한 기준에 의존하던 이전 방법보다, 확실하게 트랜잭션에서 제외시킨 지금이 더 좋은 방법이라는 확신이 듭니다

 

적용하고 난 코드는 다음과 같습니다

public class AuthServiceFacade {

    private final AuthService authService;
    private final OAuthClient oAuthClient;

    public TokenDto login(String authCode) {
        String accessToken = oAuthClient.getAccessToken(authCode);
        OAuthMemberResponse oAuthMemberResponse = oAuthClient.getMember(accessToken);
        return authService.login(oAuthMemberResponse);
    }
    
 }
 
@Transactional 
public class AuthService {
   
    private final JwtProvider jwtProvider;
    private final MemberRepository memberRepository;
    private final RefreshTokenRepository refreshTokenRepository;
  
    public TokenDto login(OAuthMemberResponse oAuthMemberResponse) {
        Member member = memberRepository.findByEmail(oAuthMemberResponse.getEmail())
                .orElseGet(() -> memberRepository.save(oAuthMemberResponse.toMember()));
        return createTokens(member.getId());
    }
    
    private TokenDto createTokens(Long memberId) {
        String accessToken = jwtProvider.createAccessToken(memberId);
        String refreshToken = jwtProvider.createRefreshToken();

        refreshTokenRepository.deleteByMemberId(memberId);
        refreshTokenRepository.save(new RefreshToken(memberId, refreshToken));

        return TokenDto.of(accessToken, refreshToken);
    }

}

 

파사드 패턴의 본래 사용 목적과는 다르게 사용하긴 했다..만

트랜잭션을 필요한 부분에서만 적용되도록 하는 데에 성공했기에 만족합니다!