[모두의 랜덤 디펜스]OCP를 준수하는 OAuth 로그인 팩토리 패턴 구현 (Google, Github, Naver)

2024. 1. 2. 23:20Backend/Spring

 

혹시 코드나 설계에 대해 피드백해 주시면 정말 소중하게 여기고 반영하겠습니다. 

감사합니다. 

개요

 모두의 랜덤 디펜스를 설계하는 데 있어, 서비스의 주 사용자들이 개발자면서, 웹 사용자인 점을 고려하여 구글, 네이버, 깃허브 3개의 OAuth 서비스 도입을 구상했습니다.

 

저희 팀의 계획은 MVP를 빠르게 개발 후 피드백을 통해 서비스를 개선하는 것이기 때문에, 빠르게 구글 OAuth만 우선적으로 도입 후 나머지 OAuth를 추후에 도입하기로 결정했습니다.

 

이때, 이후 다른 회사의 OAuth를 추가하게 되더라도 기존 코드를 수정하는 일이 없도록 하기 위해 많은 고민을 했고,

단순 if문으로 분기하기보다 상속을 통해 객체 지향적인 특성을 이용하고자 팩토리 패턴을 활용하게 되었습니다.

1. OAuth 서비스 분석

OAuth 서비스는 일반적으로 SSO와 인가 코드, Access Token, 사용자 정보를 주고받으며 로그인을 완료합니다.

구글 OAuth가 진행되는 플로우

  1. 사용자가 웹에서 구글 로그인을 완료하여 OAuth 로그인 요청을 보내면
  2. 구글은 사용자에 대한 인가 코드를 발급하여 백엔드 API로 redirect 시킵니다.
  3. 백엔드로 redirect 된 API 요청이 도착하면 인가 코드에 client secret 등을 추가하여 Access Token을 요청합니다.
  4. Access Token을 응답받고, Access Token을 통해 구글에 사용자 정보를 요청합니다.
  5. 응답받은 사용자 정보를 통해 DB에서 사용자를 검색 후 회원가입/로그인합니다.
  6. 회원가입/로그인이 완료되면 백엔드에서는 사용자를 메인 페이지로 redirect 합니다.

 

네이버나 깃허브 또한 위와 같은 플로우를 통해 진행되는데, 여기에서 백엔드에서 수행하는 공통 로직인 3과 4를 추출하여 OAuthService라는 인터페이스를 선언했습니다.

이때 서비스에 알맞은 클래스를 가져오기 위해 getType 메서드와 문자열을 이용했습니다.

 

아쉬운 점

더보기

문자열보다는 enum을 통해 관리하면 더 좋을 것 같습니다. SocialType이라는 enum을 만들어두고 활용하지 않았던 아쉬움이 남습니다.

isDev또한 환경 구성을 통해 관리하는 게 더 좋은 습관이 될 것 같습니다.  

2. 인터페이스 구현체를 담는 OAuthServiceFactory

OAuth 로그인하려는 회사와는 관계없이 하나의 API로 OAuth 작업이 수행되게 설계하려고 했습니다.

회사 타입에 따라 적절한 로그인 서비스가 호출되게 하기 위해 팩토리 패턴을 이용하기로 선택했습니다. Spring의 의존성 주입을 통해 OAuthService 인터페이스를 부모 타입으로 갖는 구현체들은 모두 생성자 호출시점에 OAuthServiceFactory에 주입되어 관리됩니다.

 

이후 OAuth 서비스 종류가 추가되더라도

OAuthService 인터페이스를 상속받는 클래스를 구현하기만 하면 OAuthServiceFactory 생성자 호출 시점에 자동 주입되기 때문에 기존의 팩토리 관련 기존 코드는 변경할 필요가 없게 되는 장점이 있습니다.

 

3. 인터페이스에 의존하는 LoginService

공통 기능을 수행하는 클래스에서는 부모 타입인 OAuthService으로 구현하여 OAuthServiceFactory에서 관리되는 다양한 회사 중 적절한 OAuth 인증에 필요한 인스턴스를 가져오고, 부모의 메서드를 실행시켜  OAuth 서비스를 수행하게 됩니다.

이렇게 함으로써 LoginService는 OAuth 서비스의 구체적인 구현에 대해서는 알 필요 없이 인터페이스를 통해 서비스를 사용할 수 있게 되었습니다.

4. 구글 OAuth 메서드 구현

OAuthService 인터페이스를 상속받는 GoogleService를 구현해 주었습니다.

getAccessToken메서드와 getMemberInfo 메서드에서는 각각 OAuth의 핵심적이고 공통적인 로직인

 

3. 백엔드로 redirect 된 API 요청이 도착하면 인가 코드 api 시크릿 키 등을 추가하여 Access Token을 요청

4. Access Token을 응답받고, Access Token을 통해 구글에 사용자 정보를 요청

을 수행하게 됩니다. 

 

처음 구상할 때에는 이렇게만 설계하면 별 다른 문제가 생기지 않을 것으로 생각했습니다.

 

하지만 4번 Access Token을 통해 사용자 정보를 요청하는 과정에서 다른 OAuth 제공자인 Github의 JSON 응답 형식을 처리하면서 문제에 직면하게 되었습니다.

문제 상황은 다음과 같습니다.

 

응답받은 Access Token으로 GoogleGithub의 이메일 등의 사용자 정보를 응답받을 때 Json 필드 이름이 달랐습니다.

 

 ObjectMapper를 통해 역직렬화하기 위해 DTO를 여러 개 만들게 되면 과정을 공통 메서드에서 처리하기 힘들어진 것입니다.

추상화된 메서드를 실행시키는 것으로 로직을 수행할 수 없기 때문에 다른 방법을 찾아야 했습니다.

 

저는 인터페이스을 상속받는 DTO 를 설계하여 간단하게 해결하고자 하였습니다.

4-1. DTO 인터페이스를 통해 해결

기존 GoogleUserDto로 단독 사용하던 것을 인터페이스를 정의하여 UserDto 인터페이스를 만들었습니다.

좌 : UserDto interface / 우 : UserDto를 상속받는 GoogleUserDto의 구현

인터페이스를 정의할 때, 모두의 랜덤 디펜스에 가입하기 위해 꼭 필요한 필드들에 대해 getter를 직접 구현하도록 강제했습니다.

 

부모타입을 이용하는 메서드

해당 인터페이스 타입으로 파라미터를 정의하고, OAuth 요청에 따라 자식 DTO를 파라미터로 넘겨주면

자식에서 구현한 getter를 통해 공통 처리를 할 수 있게 됩니다.

느낀 점

다른 백엔드 팀원이 타 서비스(네이버)의 OAuth 로그인 구현을 원하는 상황에서

제가 설계해 둔 OAuth 로그인 코드를 바탕으로 기존 코드의 수정 없이 NaverServiceNaverUserDto를 구현하는 것만으로 서비스를 개발할 수 있었습니다.

 

저는 위의 경험으로 체계적인 설계의 중요성에 대해 더욱 실감하게 되는 계기가 되었으며, 효과적인 로그인 코드 개발로 OCP원칙을 준수하는 OAuth 서비스 코드를 작성할 수 있었습니다.

이를 통해 시스템 확장성을 보장하고, 팀원이 새로운 기능을 기존 코드의 변경 없이 손쉽게 추가할 수 있는 유연한 환경을 조성하여 유지보수 비용을 절감했습니다.

 

결과적으로, 팀원이 네이버 OAuth 추가 작업에 소요하는 시간이 예상보다 50% 이상 단축되는 성과를 얻었습니다.