sealed interface class를 이용한 확장성 설계

단순하게 사용자 로그인을 구현하여 ID와 Password로만 이루어진 요청을 보내고 있다가 갑작스럽게 요구 사항이 증가하여 일반 관리자와 슈퍼 관리자의 로그인을 갑작스럽게 처리해야 하는 상황이 생긴다면 당황스러울 것이다.

예를 들어서

interface class LoginService {
  fun login(request: RequestLoginDTO): JwtToken
}
data class RequestUserLoginDTO (
  val loginId: String,
  val loginPw: String
)

위와 같이 인터페이스를 구현했을 경우에는 해당 인터페이스를 상속 받아 구현해야 하는 구현체 RequestLoginDTO 라는 객체만을 매개 변수로 받아야 한다. 하지만, 만약 슈퍼 관리자와 일반 관리자가 각각 별도의 추가 파라미터 값을 요구하고 있다면 확장성에 불리한 요구 사항이 될 것이다. 이를 해결하기 위해서 Kotlin에서 sealed interface를 사용할 수 있다.

sealed interface LoginDTO 

위와 같이 sealed interface를 생성하고 인터페이스에 매개 변수로 생성해준다 그리고 DTO에 sealed interface를 상속 받아 구현해주면 아래와 같이 사용할 수 있다.

data class RequestUserLoginDTO(
  val loginId: String,
  val loginPw: String
): LoginDTO

이렇게 되면

interface LoginService {
  fun login(request: LoginDTO): JwtToken
} 

위와 같이 interface를 수정해주고, 위의 인터페이스를 상속 받은 서비스 구현체에 when을 사용하여 exhaustive check가 가능하다

그렇다면 일반 관리자와 슈퍼 관리자가 추가되었다고 가정 했을 경우,

data class RequestAdminLoginDTO(
  val loginId: String,
  val loginPw: String,
  val adminKey: String
): LoginDTO
data class RequestSuperAdminLoginDTO(
  val loginId: String,
  val loginPw: String,
  val loginIp: String
): LoginDTO

이렇게 LoginDTO를 통해서 타입의 확장성과 안정성을 확보할 수 있게 됩니다. 그리고 interfaceLoginDTO를 강제할 수 있게 interface에 매개 변수로 설정하게 되면

interface LoginService {
  fun login(request: LoginDTO): JwtToken
}

과 같이 LoginDTO를 상속해야만 가능한 구조로 만들 수 있고

class LoginServiceImpl: LoginService {
  override fun login(request: LoginDTO): JwtToken {
    when (request) {
      is RequestUserLoginDTO -> { TODO("일반 사용자 로그인") }
      is RequestAdminLoginDTO -> { TODO("일반 관리자 로그인") }
      is RequestSuperAdminLoginDTO -> { TODO("슈퍼 관리자 로그인") }
    }
  }
}

이렇게 처리하여 타입 안정성을 확보할 수 있으며, 만약 구현되지 않은 케이스가 있다면 런타임에 오류를 발생하여 exhaustive check가 가능한 설계가 됩니다.

마무리

서비스 초반에는 ID/PW만 받는 간단한 구조로 개발해도 괜찮습니다. 하지만 서비스가 커질수록 각 역할별로 인증 파라미터가 달라지는 요구 사항은 반드시 발생합니다. 이러한 추가 요구 사항이 생기거나, 제공하는 서비스가 점점 커질수록 sealed interface를 활용한다면 구조를 깨지 않고, 확장성 있게 타입 안정성까지 챙길 수 있어 실무에서 강력한 무기가 될 것 같습니다.

추가 사항

sealed class의 경우에는 다중 상속이 안 되기 때문에 기능을 확장하기에는 불편하지만 sealed interface의 경우에는 역할 분리도 가능해서 전략 패턴, 로깅 기능 추가에도 유리할 수 있습니다.