kotlin으로 반복되는 권한 체크 로직 중복 제거하기

2018-11-11 12:08

애플리케이션을 개발하다보면 자신이 생성한 컨텐츠인지 여부를 체크해 자신이 생성한 컨텐츠에 한해 수정, 삭제 등이 가능한지를 체크하는 중복 로직이 많이 발생한다.

예를 들어 kotlin으로 구현한다면 다음과 같이 구현할 수 있다.

    fun addLecture(loginUser: NsUser, lecture: Lecture): CourseLecture {
        if (!isOwner(loginUser)) {
            throwPermissionException()
        }
        return courseLectures.addLecture(this, lecture)
    }

    fun moveLecture(loginUser: NsUser, lectureId: String, position: Int): CourseLecture {
        if (!isOwner(loginUser)) {
            throwPermissionException()
        }
        return courseLectures.move(lectureId, position)
    }

위와 같이 단순 반복적으로 권한 체크하는 부분을 어떻게 제거하면 좋을까? AOP를 적용해도 되겠지만 굳이 AOP까지 적용하고 싶지 않아 다음과 같이 구현해 봤다.

권한 체크를 위해 메소드 인자로 전달하는 NsUser 객체에 다음 메소드를 추가했다.

@Entity
open class NsUser(
    fun isSameUser(actor: NsUser) = actor == this

    fun <T:Any> letHasOwnerRole(actor: NsUser, f: () -> T): T {
        if (!isSameUser(actor)) {
            throw HasNotPermissionException("권한이 없습니다.")
        }
        return f()
    }
}

위와 같이 함수를 인자로 받는 메소드를 추가한 후 앞의 권한 체크 로직을 다음과 같이 구현해 봤다.

    fun addLecture(loginUser: NsUser, lecture: Lecture): CourseLecture {
        return loginUser.letHasOwnerRole(creator) {
            courseLectures.addLecture(this, lecture)
        }
    }

    fun moveLecture(loginUser: NsUser, lectureId: String, position: Int): CourseLecture {
        return loginUser.letHasOwnerRole(creator) {
            courseLectures.move(lectureId, position)
        }
    }

kotlin 기반으로 구현하니 이와 같은 구현이 가능해 나름 괜찮다 생각하는데 또 다른 좋은 접근 방식이 있을까?

2개의 의견 from SLiPP

2018-11-11 14:54

외부 구현 없이 Kotlin standards 로는 이렇게도 가능할것 같긴 합니다만 결국 if 가 없을순 없네요. 다른 분들이 더 좋은 아이디어를 남겨 주시길 바라며 전 이만 텨텨텨

fun addLecture(loginUser: NsUser, lecture: Lecture): CourseLecture =
    lecture.takeIf { hasRole(loginUser, role) }?.let {
        courseLectures.addLecture(this, it)
    } ?: throwPermissionException()

hasRole(NsUser, Role) 이라는 함수는 있다고 가정했습니다. extension function 을 이용하면 loginUser.hasRole(role) 처럼 좀더 자연스럽게 쓸 수 있겠네요.

2018-11-14 01:18

개인적으로 현재 테스트하기 힘든 레거시 코드를 많이 다루고 있다보니 다른 형태의 접근이 먼저 떠올랐습니다. 코드가 잘 짜여있고 테스트가 가능한 구조라면 상관없겠지만 추가할 체크 로직 또한 테스트가 필요하고 로직을 추가할 클래스가 굉장히 복잡해서 테스트하기 힘든 경우에는 인터페이스로 분리하는 것이 테스트하기 좋습니다.

data class UntestableUser(
    val dependency1: Dependency1,
    val dependency2: Dependency2,
    val dependency3: Dependency3,
    val dependency4: Dependency4,
    val dependency5: Dependency5,

    val email: String,
    val nickname: String,
    val roles: List<String> = listOf()
) {
    init {
        dependency2.doSomething(dependency1)
        dependency3.doSomething(dependency2)
        dependency4.doSomething(dependency3)
        dependency5.doSomething(dependency4)
    }
}

위와 같은 테스트하기 힘든 클래스 UntestableUser가 있고 여기서 email, nickname, roles에 대해서 유효성 검사가 필요한 상황이라고 가정하겠습니다.

아래와 같이 default interface method를 통해 구현한다던가 코틀린이라면 추가적으로 확장함수로 구현할 수도 있습니다.

interface EmailChecker {
    fun checkEmail(email: String): Boolean = email.contains("@")
}

interface RoleChecker {
    fun <T : Collection<String>> T.checkRole(role: String): Boolean = contains(role)
}

그리고 유효성 검사를 담당해야 할 클래스들에게 해당 인터페이스를 상속받도록 명시해주면 끝입니다. 테스트 코드에서도 마찬가지입니다.

class UserService : EmailChecker, RoleChecker

class CheckersTest : EmailChecker, RoleChecker

여기서부터는 코틀린에만 해당되는 내용입니다.

현실의 코드가 대부분 그렇듯이 인터페이스에서도 내부적으로 쓰여야 할 의존성이 생길 수 있습니다. 이런 경우 자바에서는 추상 클래스 또는 delegation 패턴을 구현하던가 눈물을 흘리며 메서드의 매개변수로 추가하기도 합니다.

코틀린에서는 인터페이스에도 필드를 추가할 수 있습니다. 다만 그렇게 되면 아래와 같이 상속받는 클래스에서 인터페이스에서만 쓰이고 클래스의 다른 곳에서는 필요가 없는 의미없는 멤버 필드가 생길 수 있습니다.

interface NicknameChecker {
    // 3rd party 의존성들
    val firstValidator: FirstValidator
    val secondValidator: SecondValidator
    val thirdValidator: ThirdValidator

    fun checkNickname(nickname: String): Boolean =
        firstValidator.validate(nickname) &&
                secondValidator.validate(nickname) &&
                thirdValidator.validate(nickname)
}

class UserService(
    override val firstValidator: FirstValidator,
    override val secondValidator: SecondValidator,
    override val thirdValidator: ThirdValidator
) : NicknameChecker

이런 경우를 피하기 위해서는 미리 인터페이스의 구현체를 만들고 생성자로 구현체를 넣어준 뒤에 by 키워드를 통해서 클래스 위임을 하면 됩니다.

class NicknameCheckerImpl(
    override val firstValidator: FirstValidator,
    override val secondValidator: SecondValidator,
    override val thirdValidator: ThirdValidator
) : NicknameChecker

class UserService(
    private val nicknameChecker: NicknameChecker
) : EmailChecker, RoleChecker, NicknameChecker by nicknameChecker

테스트 코드를 포함한 모든 샘플 코드는 아래 링크에 올려두었습니다. https://gist.github.com/galcyurio/4f0433b535b146cef327a268b15e3486

의견 추가하기

연관태그

← 목록으로