0. 들어가며🏃🏻♂️
진행했던 프로젝트에서 구글과 카카오 소셜로그인 기능을 구현했었는데 해당 코드를 다시 살펴보며 리팩토링하면 좋겠다 싶은 부분을 발견했습니다. 이번 글에서는 디자인 패턴 중 하나인 전략 패턴(Strategy Pattern)을 적용해 리팩토링해본 경험을 정리해보려 합니다.
1. 기존의 코드는..🤔
기존 코드에서는 구글 로그인과 카카오 로그인을 각각 다른 API로 처리하였습니다. 간략하게 아래와 같은 형태였죠.
@GetMapping("/google/login")
fun googleLogin(...) { 구글 로그인... }
@GetMapping("/kakao/login")
fun kakaoLogin(...) { 카카오 로그인... }
기능은 잘 동작했지만, 거의 같은 기능을 하는 API가 두 개로 나눠져있는 점이 거슬렸습니다. 또한 더 큰 문제는 네이버, 페이스북 등의 새로운 소셜 플랫폼을 통한 로그인 기능을 추가하고 싶을 때마다 새로운 API를 만들어야 하는 점이었습니다. 이러한 확장은 일어날 가능성이 높으므로 이에 대응할 수 있는 리팩토링의 우선순위가 높아 보였죠.
위와 같은 문제로 처음에는 안일하게.. 아래와 같이 리팩토링해보았습니다.
// 컨트롤러 Layer
@GetMapping("/{socialProvider}/login")
fun socialLogin(...) { 서비스 Layer의 socialProvider 로그인 호출... }
// 서비스 Layer
fun socialLogin(socialProvider: String) {
if (socialProvider == "kakao") { 카카오 로그인... }
else if (socialProvider == "google") { 구글 로그인... }
...
}
위와 같은 방식으로 코드를 구현하게 되면 소셜 로그인 플랫폼을 추가할 때마다 조건 분기를 추가해야 합니다. 또한 어떤 플랫폼의 로그인 기능을 수정해야 할 경우가 생기면 if문을 뒤적거리며 수정해야 합니다. 이러한 경우들 모두 서비스 Layer의 socialLogin이라는 클라이언트 함수를 건드리는 작업이 될 텐데 클라이언트 함수를 만지는 것 자체가 유지 보수성 측면에서 좋지 않죠.
이러한 문제를 해결하기 위해 고민하던 중 적용해 볼 만한 디자인 패턴은 없을까 살펴보았고, 전략 패턴(Strategy Pattern)을 적절히 활용해 보면 클라이언트 함수를 만지지 않고 해결할 수 있겠다라고 생각했습니다.
2. 전략 패턴(Strategy Pattern)이란❓
전략 패턴이란 객체 지향 디자인 패턴 중 하나로, 동일한 작업을 수행하는 다양한 전략을 정의하고 이를 동적으로 선택하여 사용하는 방식입니다. 이렇게 글로만 보면 감이 전혀 오지 않으니 간단한 예시를 살펴보도록 하겠습니다.
우선 전략 패턴에서는 세 가지 요소가 필요합니다.
- 전략 (전략 메서드를 가진 전략 객체)
- 컨텍스트 (전략 객체를 사용하는 컨텍스트)
- 클라이언트 (전략 객체를 생성해 컨텍스트에 주입하는 클라이언트)
위 세 가지 요소를 머리에 담아두고 코드 예시를 살펴보겠습니다.
우선 구현하고 싶은 상황은 다음과 같습니다. 로봇을 제어하는 프로그램을 만들고 싶은데 로봇의 이동 방식(전략)이 여러 개 있을 수 있는 상황이라면 전략패턴을 사용해 구현해 볼 수 있습니다.
// 전략 인터페이스
public interface MovementStrategy {
void move();
}
// 전략1: 걷기
public class WalkStrategy implements MovementStrategy {
@Override
public void move() {
System.out.println("Walking");
}
}
// 전략2: 달리기
public class RunStrategy implements MovementStrategy {
@Override
public void move() {
System.out.println("Running");
}
}
// 컨텍스트
public class Robot {
private MovementStrategy movementStrategy; // 전략 객체
public Robot(MovementStrategy movementStrategy) {
this.movementStrategy = movementStrategy;
}
public void setMovementStrategy(MovementStrategy movementStrategy) {
this.movementStrategy = movementStrategy;
}
public void move() {
movementStrategy.move();
}
}
// 클라이언트
public static void main(String[] args) {
// 걷는 로봇 만들기 -> 전략 객체를 컨텍스트에 주입
Robot walkingRobot = new Robot(new WalkStrategy());
walkingRobot.move(); // 출력: Walking
// 날아다니는 로봇 만들기 -> 전략 객체를 컨텍스트에 주입
Robot flyingRobot = new Robot(new FlyStrategy());
flyingRobot.move(); // 출력: Flying
}
위 코드를 천천히 살펴보면 전략 패턴의 세 가지 요소가 각각 무슨 일을 하는지 이해하실 수 있을 것 같습니다.
이렇게 전략패턴을 사용하게 되면 위 예시에서는 다양한 전략을 확장성 있게 정의할 수 있게 되고, 전략을 동적으로 선택함으로써 코드의 유연성이 좋아집니다. 이를 통해 코드 재사용성과 유지 보수성을 향상 시킬 수 있는 것이죠.
3. 전략 패턴을 활용해 소셜로그인 코드 리팩토링하기😎
기존 코드를 전략 패턴을 적용해 리팩토링한 코드를 간략하게 나타내면 아래와 같습니다.
// 전략 인터페이스
interface SocialLoginStrategy {
fun login(authValue: String): LoginResponse
}
// 전략1: 구글 로그인
class GoogleLoginStrategy: SocialLoginStrategy {
override fun login(authValue: String): LoginResponse {
// 구글 로그인 기능 구현
}
}
// 전략2: 카카오 로그인
class KakaoLoginStrategy: SocialLoginStrategy {
override fun login(authValue: String): LoginResponse {
// 카카오 로그인 기능 구현
}
}
// 컨텍스트
class SocialLoginContext {
fun doLogin(socialLoginStrategy: SocialLoginStrategy, authValue: String): LoginResponse {
return socialLoginStrategy.login(authValue)
}
}
class OAuthService(
private val googleLoginStrategy: GoogleLoginStrategy,
private val kakaoLoginStrategy: KakaoLoginStrategy,
) {
private val socialLoginStrategyMap = mapOf(
"google" to googleLoginStrategy,
"kakao" to kakaoLoginStrategy,
)
// 리팩토링한 기존 클라이언트 함수
fun socialLogin(socialProvider: String, authValue: String): LoginResponse {
val strategy: SocialLoginStrategy = socialLoginStrategyMap[socialProvider]
?: let{ throw Exception400("지원하지 않는 소셜 로그인 플랫폼입니다.") }
return SocialLoginContext().doLogin(strategy, authValue)
}
}
위와 같이 전략 패턴을 적용하여 리팩토링하였고, 이제는 소셜 로그인 플랫폼을 추가하려면 아래와 같은 작업만 해주면 됩니다.
- 소셜 플랫폼에 맞는 로그인 전략 객체 생성
- OAuthService에 socialLoginStrategyMap에 이를 포함시키기
즉, 클라이언트 함수인 OAuthService의 socialLogin은 건드리지 않아도 되는 것이죠!
4. 나가며💨
소셜 로그인에 전략 패턴을 도입함으로써 새로운 소셜 로그인 플랫폼 추가 및 기존 로그인 기능 수정을 클라이언트 코드 수정 없이 이룰 수 있게 되었고, 이를 통해 확장성과 유지 보수성을 얻을 수 있었습니다.
소셜로그인의 확장을 위한 리팩토링을 하고 난 뒤, 이번 리팩토링에 꼭 전략 패턴을 사용해야했을까? 라는 생각이 들었습니다. 변하는 부분과 변하지 않는 부분을 분리하기 위해 전략 패턴을 많이 사용하곤 하는데 제 상황에서는 변하지 않는 부분이 따로 없었기 때문에 Context 클래스라는 불필요한 클래스를 만드는 오버헤드가 생기기도 했습니다. 좀 더 효율적인 코드를 작성하고자 했다면 나만의 방법을 통해 코드를 작성했겠지만, 나만 아는 코드를 작성하기보다는 조금 불필요한 오버헤드가 생기더라도 모두가 알고 있는 디자인 패턴으로 작성한다면 추후 협업을 할 때 남들이 보기 편한 코드를 만들 수 있을 것이라는 생각이 들었습니다. 이는 곧 코드의 유지 보수성을 높일 수 있는 효과를 낼 수 있죠. 따라서 전략 패턴을 사용하는 것이 과하지 않았을 수 있겠다라고 스스로 결론내렸습니다.
끝으로..
디자인 패턴을 눈으로만 익힐 때는 객체지향적인 코드를 쓰는 방법들이므로 딱히 알아두지 않아도 경험이 쌓이면 알아서 비슷하게 쓰지 않을까라는 생각을 하기도 했는데, 이번에 실제로 도입해 보면서 이것이 큰 오해였음을 깨달았습니다. 개발자라면 마주치게 되는 문제 상황을 선대 개발자들이 고민하여 정리해 놓은 만큼 디자인 패턴을 하나하나 내 것으로 만들어 나가면 큰 도움이 될 것 같습니다.
2023.08.23 수정
지금 와서 다시보니 전략 객체들을 List로 하여 빈주입을 받으면 좀 더 좋은 코드가 될 것 같습니다. 현재는 전략 객체를 생성할때마다 map에 전략을 추가해주어야하는데 List<SocialLoginStrategy> 같은 느낌으로 빈을 주입받고 support 함수를 사용한다면 좀 더 객체지향적으로 코드를 작성할 수 있을 것 같습니다.
'Spring > Project' 카테고리의 다른 글
[Spring & Project] 채팅 읽음 확인 기능 구현하기 (1) | 2023.02.01 |
---|