Rlog

Web 계층에 Port 를 통해 더 자유로운 코드 짜기 본문

Architecture

Web 계층에 Port 를 통해 더 자유로운 코드 짜기

dev_roach 2022. 1. 20. 20:32
728x90

아래 코드를 한번 보자.
Cotroller 에서 Service 계층을 바로 참조하고 있다.
이건 보통 일반적인 비즈니스 로직을 작성할때 많이 작성되는 코드이다.

@RestController
class UserJoinController(
  private val userJoinService: UserJoinService
) {

  @GetMapping("/users")
  fun hello() {
    userJoinService.join()
  }

}

이건 보통 일반적인 코드인데 한가지 문제점이라면 문제점이 있을 수 있다.
만약 User 가 회원가입하는 부분이 MSA 환경으로 분리된다면 어떻게 대응할 수 있을까?
아마도, 기존의 UserService 를 지워야 하거나 수정하는 현상이 발생할 것이다.

우리가 클린코드 / 클린아키텍쳐과 같은 여러 명저들을 읽다보면 변경을 최대한 피하라고 한다.
아마 의문이 있을 수 있다. 당연히 UserService 가 바뀌어야 하는게 아닌가? 기술적인 요구가 바뀌었으니?
다만 변경이 아닌 추가로도 충분히 가능하다.

현재 Controller 의 Dependency 를 diagram 으로 한번 보자.

위와 같은 형태로 Controller 는 Service 에 dependency 를 가지고 있게 된다.
여기서 중요한건 Service Class 에 의존성을 가지고 있다는 것보다.
직접적인 구현체(Class) 에 의존성을 가지고 있는 것에 의의를 두어야 한다.

오브젝트(Object) 구절 중 Interface 는 잘 변하지 않는 설계도와 같다고 표현한다.
우리가 변화에 최소한의 피해를 입기 위해서는 최대한 변하지 않는 것들에 의존해야 한다.
처음에는 UserJoinService 라는 직접적인 클래스에 연관하고 있었기에
직접적인 코드의 변경만이 유일한 해결책이였다.

그렇다면 어떻게 이걸 해결할 수 있을까?
최근에 읽은 "만들면서 배우는 클린 아키텍쳐" 에서는 아래와 같은 방법으로 이를 처리했다.
Web 계층에서는 Port 에만 의존하고 Port 를 UseCase를 구현하는 계층에서 구현한다.
그래서 일단 Port 계층에 아래와 같은 코드를 작성해보자.

그리고 Controller 에서는 이제 UserJoinPort 를 의존하도록 바꿔주어야 할것이다.

@RestController
class UserJoinController(
  private val userJoinPort: UserJoinPort
) {

  @GetMapping("/users")
  fun hello() {
    userJoinPort.join()
  }

}

이제 Service 는 Port 를 구현하도록 코드를 바꿔보자.

@Service
class UserJoinService(
  private val userRepository: UserRepository
) : UserJoinPort {

  override fun join() {
    val user = User(
      email = "roach@naver.com"
    );

    userRepository.save(user)

  }

}

이렇게 됬을때 클래스 도식도를 한번 다시살펴보자.

Controller 는 이제 잘 변하지 않는 UserJoinPort Interface 에 의존하게 되었고,

UserJoinService 는 현재 우리의 Join Usecase 를 직접적으로 구현하고 있는 클래스이다.

나중에 MSA 환경으로 이전하면서 다른 인증서버를 통해 회원가입을 하게될경우

우리는 또하나의 유즈케이스를 추가해주기만 하면된다.

 

이로인해 변경보다는 추가만으로 새로운 변화에 대응할 수 있게 되었다.

아까 위의 도식도와는 다르게 이제는 의존관계의 역전도 일어났다.

이전에는 Controller 가 특정 Service(구현체) 를 선택했지만

이제는 Controller 는 추상계층인 UserJoinPort 만 선택할 수 있고, UserJoinPort 의 구현체는

구현하는 측에서 어떻게 구현할지를 담당한다.

즉, Controller 는 Interface 에만 오로지 의존하게 될 수 있는 것이다.

 

이는 유연한 코드를 적기위해서 아주 중요한 요소이다.

하지만 위와 같이 Port 를 계속 만들기에는 좀 부담일 수 있다.

왜냐면 사실 실무를 하다보면 이게 MSA 로 갈지 안갈지 판단하기도 어렵고 이런데,

그게 과연 이 Service 마다 Port 계층을 만드는게 맞을까? 라고 생각할 수 있다.

 

그건 팀원에서 잘협의를 봐서 정하면 좋을거 같은데,

개인적으로 나는 사이드 프로젝트에서는 UserService 와 같은 권한인증에 관한 부분은

Port 계층을 두고 해보려고 한다. 

여하튼 좋은 방법중 하나인거 같아서 직접 구현해보면서 느낀점을 적어봤다