ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • nest로 oauth 로그인 구현하기(전략패턴, 개방폐쇄원칙)
    카테고리 없음 2024. 6. 14. 22:02

     

     

    nest로 소셜로그인기능을 구현해보았다. 하지만 개발 도중, 생기는 문제가 있었다.

    지금은 소셜로그인 OAuth기능을 구글 서비스로만 구현했지만, OAuth기능을 제공하는 서비스들은 엄청많다.

     

    (네이버, 카카오, 페이스북, 깃허브..등등)

    새로운 소셜로그인 기능을 도입할 여지가 충분히 있는 상황이었다.

    그러면 새로운 소셜로그인기능을 추가할때마다, 기존의 서비스 코드를 수정하고, 새로운 메서드를 추가하거나,

     

    if문을 통해 분기처리를 하는게 맞는걸까?

    이러한 방식으로 하게된다면, 

    if-else if문이 굉장히 길어지게되고, 새로운 기능이 더해질때마다 계속해서 수정해야하는 부분이 생기기때문에 유지보수가 어려워진다

     

    그리고 하나의 서비스 클래스 에 여러가지 로직이 더해지기때문에 이해하기도 어려워진다

    문제의 코드)

    @Injectable()
    export class AuthService {
        
        async socialAuth(provider: string) {
            if (provider.equals("google")) {
                googleAuth()
            } else if (provider.equals("naver")) {
                naverAuth()
            } else if (provider.equals("kakao")) {
                kakaoAuth()
            } else {
                throw new UnsupportedOperationException("Unsupported social login provider");
            }
        }
        
        
        async googleAuth() {
        	// googleAuth로직
        }
        
        async naverAuth() {
        	// NaverAuth로직
        }
        
         async kakaoAuth() {
        	// KakaoAuth로직
        }
        
        
    }

     

    이러한 방식대로 하면, if문이 계속해서 길어진다

     

    하지만 전략패턴을 이용하여 각각의 소셜로그인 전략들을 클래스로 만들어서 

     interface ISocialAuthStategy를 implement하게 만들었다

    그리고 각각의 전략들을 starategy 디렉토리에서 관리할 수 있도록 하였다

     

    동일한 소셜로그인 메서드 socialAuth 를 이용하기때문에

    새로운 소셜로그인 기능이 추가되어도 strategy 디렉토리에 새로운 전략만 추가해주면된다

    개선된코드

    auth.service.ts

    @Injectable()
    export class AuthService {
      private strategy = {};
    
      constructor(
        private readonly userService: UserService,
        private readonly jwtService: JwtService,
        private readonly prismaService: PrismaService,
        private readonly googleStrategy: GoogleStrategy, // 새로운 전략이 추가될때마다 DI추가
      ) {
        this.strategy[SocialLoginProvider.GOOGLE] = googleStrategy; // 새로운 전략 추가될때마다 속성추가
      }
    
      async socialAuth(
        req: Request,
        res: Response,
        provider: SocialLoginProvider,
      ): Promise<{ accessToken: string }> {
        let strategy = this.strategy[provider];
    
        if (!strategy) {
          throw new NotFoundException('Not Found OAuth Service');
        }
    
        return strategy.socialAuth(req, res);
      }
    
    
      async socialAuthCallbck(
        provider: string,
        query: any,
      ): Promise<{ accessToken: string }> {
        let strategy = this.strategy[provider];
    
        if (!strategy) {
          throw new NotFoundException('Not Found OAuth Service');
        }
    
        return strategy.socialAuthCallback(query);
      }
    
      async socialWithdraw(
        loginUser: LoginUser,
        socialWithdrawDto: SocialWithdrawDto,
      ): Promise<void> {
        const strategy = this.strategy[socialWithdrawDto.provider];
    
        if (!strategy) {
          throw new NotFoundException('Not Found OAuth Service');
        }
    
        return strategy.socailWithdraw(loginUser, socialWithdrawDto);
      }

     

     

     

     

     


    interface / social-auth-strategy.interface.ts

    export interface ISocialAuthStrategy {
      socialAuth(
        req: Request,
        res: Response,
        provider: SocialLoginProvider,
        socialAuthDto: SocialAuthDto,
      ): Promise<void>;
    
      socialAuthCallback(query: any): Promise<{ accessToken: string }>;
    
      socialWithdraw(
        loginUser: LoginUser,
        socailwithdrawDto: SocialWithdrawDto,
      ): Promise<void>;
    }

     

     

     

    strategy / google.strategy.ts

     

    > 각 strategy들을 기존에 정의해둔 인터페이스인 ISocialAuthStrategy로 묶어준다

     

    > 때문에 각 strategy 클래스들은 socialAuth / socialAuthCallback / socialWithdraw 3개의 메서드를 가진다

    @Injectable()
    export class GoogleStrategy implements ISocialAuthStrategy {
      constructor(
        private readonly userService: UserService,
        private readonly jwtService: JwtService,
        private readonly httpService: HttpService,
        private readonly configServce: ConfigService,
      ) {}
    
      async socialAuth(req: Request, res: Response): Promise<void> {
        let url = `https://accounts.google.com/o/oauth2/v2/auth`;
        url += `?client_id=${this.configServce.get<string>('GOOGLE_CLIENT_ID')}`;
        url += `&redirect_uri=${this.configServce.get<string>('GOOGLE_REDIRECT_URI')}`;
        url += `&response_type=code`;
        url += `&scope=email profile`;
    
        res.redirect(url);
      }
    
      async socialAuthCallback(
        query: GoogleCallbackDto,
      ): Promise<{ accessToken: string }> {
        const { code, scope, authuser, prompt } = query;
    
        const tokenRequestBody = {
          client_id: this.configServce.get<string>('GOOGLE_CLIENT_ID'),
          client_secret: this.configServce.get<string>('GOOGLE_CLIENT_SECRET'),
          code: code,
          redirect_uri: this.configServce.get<string>('GOOGLE_REDIRECT_URI'),
          grant_type: 'authorization_code',
        };
    
        let tokenUrl = `https://oauth2.googleapis.com/token`;
    
        const { data: tokenData } = await this.httpService.axiosRef.post(
          tokenUrl,
          tokenRequestBody,
        );
    
        const userInfoUrl = `https://www.googleapis.com/oauth2/v2/userinfo`;
    
        const { data: userInfo } = await this.httpService.axiosRef.get(
          userInfoUrl,
          {
            headers: {
              Authorization: `${tokenData.token_type} ${tokenData.access_token}`,
            },
          },
        );
    
        let user = await this.userService.getUser({ email: userInfo.email });
    
        if (!user) {
          user = await this.userService.signUpOAuth({
            email: userInfo.email,
            provider: 'google',
            providerKey: userInfo.id,
          });
        }
    
        const payload = { idx: user.idx };
        const accessToken = await this.jwtService.signAsync(payload);
    
        return { accessToken };
      }
      
      }

     

     

     

     

     

     

    기존의 방식대로 코드를 짰다면 서비스코드에, OAuth기능마다 socialAuth / socialAuthCallback / socialWithdraw 의

     

    메서드를 만들어야했다.

     

    하지만 이제는 깔끔한 형태를 유지할 수 있고, 

     

     새로운 소셜로그인이 추가되어도 기존코드 수정없이  ISocialAuthService를

     

    implement하는 strategy를 하나 추가하기만 하면된다.

     

    때문에 객체지향의 원칙중 개방폐쇄의 원칙을 충족한 확장성이 좋은 코드가 되었다

     

     

     

     

     

     

     

     

     

    이러한 전략패턴 방식은 동일한 기능이 여러개 추가로 확장될 여지가 많을 때 사용하는 것이 좋다

     

    결제 기능을 만들때도 유용하다

     

     

     

     

     

     

     

     

Designed by Tistory.