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)이란❓

전략 패턴이란 객체 지향 디자인 패턴 중 하나로, 동일한 작업을 수행하는 다양한 전략을 정의하고 이를 동적으로 선택하여 사용하는 방식입니다. 이렇게 글로만 보면 감이 전혀 오지 않으니 간단한 예시를 살펴보도록 하겠습니다. 

 

우선 전략 패턴에서는 세 가지 요소가 필요합니다.

  1. 전략 (전략 메서드를 가진 전략 객체)
  2. 컨텍스트 (전략 객체를 사용하는 컨텍스트)
  3. 클라이언트 (전략 객체를 생성해 컨텍스트에 주입하는 클라이언트)

위 세 가지 요소를 머리에 담아두고 코드 예시를 살펴보겠습니다.

 

우선 구현하고 싶은 상황은 다음과 같습니다. 로봇을 제어하는 프로그램을 만들고 싶은데 로봇의 이동 방식(전략)이 여러 개 있을 수 있는 상황이라면 전략패턴을 사용해 구현해 볼 수 있습니다.

// 전략 인터페이스
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)
    }
}

위와 같이 전략 패턴을 적용하여 리팩토링하였고, 이제는 소셜 로그인 플랫폼을 추가하려면 아래와 같은 작업만 해주면 됩니다.

  1. 소셜 플랫폼에 맞는 로그인 전략 객체 생성
  2. OAuthService에 socialLoginStrategyMap에 이를 포함시키기

즉, 클라이언트 함수인 OAuthService의 socialLogin은 건드리지 않아도 되는 것이죠!


4. 나가며💨

소셜 로그인에 전략 패턴을 도입함으로써 새로운 소셜 로그인 플랫폼 추가 및 기존 로그인 기능 수정을 클라이언트 코드 수정 없이 이룰 수 있게 되었고, 이를 통해 확장성과 유지 보수성을 얻을 수 있었습니다.

 

소셜로그인의 확장을 위한 리팩토링을 하고 난 뒤, 이번 리팩토링에 꼭 전략 패턴을 사용해야했을까? 라는 생각이 들었습니다.  변하는 부분과 변하지 않는 부분을 분리하기 위해 전략 패턴을 많이 사용하곤 하는데 제 상황에서는 변하지 않는 부분이 따로 없었기 때문에 Context 클래스라는 불필요한 클래스를 만드는 오버헤드가 생기기도 했습니다. 좀 더 효율적인 코드를 작성하고자 했다면 나만의 방법을 통해 코드를 작성했겠지만, 나만 아는 코드를 작성하기보다는 조금 불필요한 오버헤드가 생기더라도 모두가 알고 있는 디자인 패턴으로 작성한다면 추후 협업을 할 때 남들이 보기 편한 코드를 만들 수 있을 것이라는 생각이 들었습니다. 이는 곧 코드의 유지 보수성을 높일 수 있는 효과를 낼 수 있죠. 따라서 전략 패턴을 사용하는 것이 과하지 않았을 수 있겠다라고 스스로 결론내렸습니다.

 

끝으로..

디자인 패턴을 눈으로만 익힐 때는 객체지향적인 코드를 쓰는 방법들이므로 딱히 알아두지 않아도 경험이 쌓이면 알아서 비슷하게 쓰지 않을까라는 생각을 하기도 했는데, 이번에 실제로 도입해 보면서 이것이 큰 오해였음을 깨달았습니다. 개발자라면 마주치게 되는 문제 상황을 선대 개발자들이 고민하여 정리해 놓은 만큼 디자인 패턴을 하나하나 내 것으로 만들어 나가면 큰 도움이 될 것 같습니다.

 

2023.08.23 수정

지금 와서 다시보니 전략 객체들을 List로 하여 빈주입을 받으면 좀 더 좋은 코드가 될 것 같습니다. 현재는 전략 객체를 생성할때마다 map에 전략을 추가해주어야하는데 List<SocialLoginStrategy> 같은 느낌으로 빈을 주입받고 support 함수를 사용한다면 좀 더 객체지향적으로 코드를 작성할 수 있을 것 같습니다.

반응형

'Spring > Project' 카테고리의 다른 글

[Spring & Project] 채팅 읽음 확인 기능 구현하기  (1) 2023.02.01
복사했습니다!