Gunyoung-Kim / Touch-My-Body

웨이트 트레이닝 운동 기록 서비스 및 웨이트 트레이닝 관련 정보, 커뮤니티 제공 서비스
https://touch-my-body.com
0 stars 1 forks source link

파라미터 유효성 검사를 위한 Preconditions 클래스 추가 #39

Closed Gunyoung-Kim closed 3 years ago

Gunyoung-Kim commented 3 years ago

메소에서 파라미터 유효성 검사를 위한 코드는 메소드의 시작 부분에 위치해야한다.

이에 대한 근거는 크게 2가지라 본다.

  1. 사전에 잘못된 파라미터를 차단하여 잘못된 실행을 방지할 수 있다.
  2. 잘못된 파라미터로 인해 발생하는 에러를 디버깅하는 과정을 편하게 해준다.

예시 코드 조각을 통해 알아보자


    public User addUserExercise(User user, UserExercise userExercise) {
        setRelationBetweenUserAndUserExercise(user, userExercise);
        userExerciseService.save(userExercise);
        return user;
    }

    private void setRelationBetweenUserAndUserExercise(User user, UserExercise userExercise) {
        user.getUserExercises().add(userExercise);
        userExercise.setUser(user);
    }

위 코드에서는 메소드 시작부분에서 따로 파라미터의 유효성 검사를 하는 처리를 하지 않았다. 만약 userExercise로 null 값이 들어오면 setRelationBetweenUserAndUserExercise의 userExercise.setUser(user); 부분에서 NullPointerException을 던질 것이다. 하지만 그 전의 코드 user.getUserExercises().add(userExercise); 는 그대로 실행될것이다. 결론적으로 메소드가 NullPointerException을 던지긴 했다만 유저의 userExercises 리스트에는 null 값이 들어가게 될 것이다. 그리고 NullPointerException 발생 시 어디서 발생했는지 호출 스택을 보며 찾아봐야 한다.

-> 메소드 로직 수행 전 파라미터 유효성 검사는 필요하다!

위 내용은 TouchMyBody 모든 클래스에 해당되는 부분이기에 따로 파라미터 유효성 검사를 위한 클래스를 추가하기로 결정했다. 바로 Preconditions 해당 클래스에 대한 영감은 Junit의 platform-commons 패키지에서 얻었다.

Gunyoung-Kim commented 3 years ago

아래 코드는 Precondtions 클래스의 구조를 파악할 수 있는 간단한 코드이다.

    public final class Preconditions {

    private Preconditions() {
        throw new AssertionError();
    }

    public static <T> T notNull(T object, String message) throws PreconditionViolationException {
        condition(object != null, message);
        return object;
    }

    private static void condition(boolean predicate, String message) {
        if(!predicate) 
            throw new PreconditionViolationException(message);
    }
}

notNull 메소드는 object의 null 값 여부를 확인하여 null 값인 경우 PreconditionViolationException(Preconditions 를 위해 추가한 예외 클래스, 비검사 예외)를 던지고 아닌 경우에는 object를 그대로 반환하도록 했다.

Preconditions에 추가될 메소드는 내부적으로 모두 condition 메소드를 실행할 것이다. 해당 메소드의 predicate에는 유효성 검사를 하는 로직을 전달하고 message에는 유효성 검사 실패했을 때 예외에 담길 에러 메시지가 담긴다.

아래 코드는 Preconditions.notNull 을 적용한 코드이다.


    public User addUserExercise(User user, UserExercise userExercise) {
        Preconditions.notNull(user, "Given user must be not null");
        Preconditions.notNull(userExercise, "Given userExercise must be not null");

        setRelationBetweenUserAndUserExercise(user, userExercise);
        userExerciseService.save(userExercise);
        return user;
    }

    private void setRelationBetweenUserAndUserExercise(User user, UserExercise userExercise) {
        user.getUserExercises().add(userExercise);
        userExercise.setUser(user);
    }

반영 commit : https://github.com/Gunyoung-Kim/Touch-My-Body/commit/32f2662985d15d3d6b966d767ba13d3414848133

Gunyoung-Kim commented 3 years ago

파라미터 유효성 검사 항목에는 null 값 여부외에도 특정 값보다 크거나 작은지의 여부도 있다.


        public Page<User> findAllByNickNameOrNameInPage(String keyword, Integer pageNumber) {
        PageRequest pageRequest = PageRequest.of(pageNumber-1, PageSize.BY_NICKNAME_NAME_PAGE_SIZE.getSize());
        return userRepository.findAllByNickNameOrName(keyword, pageRequest);
    }

위 코드에서 인자로 전달된 pageNumber은 1 이상의 Integer 값이여야한다. pageNumber이 1 미만이면 PageRequest의 of 정적 메소드에서 IllegalArgumnetException 예외를 던진다. 이러한 상황에서 디버깅을 좀 더 편하기 위해(PreconditionViolationException 이라면 메시지 확인과 더불어 해당 인자만 확인하기 때문에) 메소드 로직 수행 전 미리 1이상의 값인지 확인하려는 코드를 추가하고자했다.

아래는 위의 요구사항을 위해 Preconditions에 추가한 메소드이다.

public final class Preconditions {

    private Preconditions() {
        throw new AssertionError();
    }

    // (중략) 

    public static <T extends Comparable<T>> T notLessThan(T object, T another, String message) throws PreconditionViolationException {
        condition(object.compareTo(another) >= 0, message);
        return object;
    }

    public static <T extends Comparable<T>> T notMoreThan(T object, T another, String message) throws PreconditionViolationException {
        condition(object.compareTo(another) <= 0, message);
        return object;
    }

    private static void condition(boolean predicate, String message) {
        if(!predicate) 
            throw new PreconditionViolationException(message);
    }
}

notLessThan은 object가 another 이상인지 확인하는 메소드이고 notMoreThan은 object가 another 이하인지 확인하는 메소드이다. 각각 이상인지 이하인지 여부는 Comparable.comparTo 를 통해 판단한다. 그래서 전달되는 인자는 모두 Comparable 인터페이스를 구현한 클래스여야 한다.

아래는 위 메소드를 적용한 서비스 클래스 코드 조각이다.

        public Page<User> findAllByNickNameOrNameInPage(String keyword, Integer pageNumber) {
        Preconditions.notLessThan(pageNumber, 1, "pageNumber should be equal or greater than 1");

        PageRequest pageRequest = PageRequest.of(pageNumber-1, PageSize.BY_NICKNAME_NAME_PAGE_SIZE.getSize());
        return userRepository.findAllByNickNameOrName(keyword, pageRequest);
    }

반영 commit : https://github.com/Gunyoung-Kim/Touch-My-Body/commit/2628a787889451fcf7c855ab9b6dbce1f2865c49 , https://github.com/Gunyoung-Kim/Touch-My-Body/commit/dc34ad692ce70f12a28f066337e5881a4e180902

Gunyoung-Kim commented 3 years ago

위의 notMoreThan, notLessThan 에는 한가지 성능상의 문제가 있다. 유효성 검사 대상이 기본 자료형(int, long, double)인 경우 Comparable.comparTo 를 사용하기 위해 오토 박싱이 일어난다. 무분별한 오토 박싱/언박싱은 성능에 좋지 않은 현상이다.

그래서 이를 해결하기 위해 기본 자료형을 위한, 오토 박싱을 하지 않는 메소드를 추가하기로 했다.

추가한 메소드이다.

  public final class Preconditions {

    // (중략)

    public static int notLessThanInt(int object, int another, String message) throws PreconditionViolationException {
        condition(Integer.compare(object, another) >= 0, message);
        return object;
    }

    public static int notMoreThanInt(int object, int another, String message) throws PreconditionViolationException {
        condition(Integer.compare(object, another) <= 0, message);
        return object;
    }

    private static void condition(boolean predicate, String message) {
        if(!predicate) 
            throw new PreconditionViolationException(message);
    }
}

기존의 notLessThan, notMoreThan 메소드에 해당 기본 자료형의 이름(위의 경우엔 Int)을 추가하는 방식으로 메소드 이름을 정했다. 내부적으로 Integer.compare 정적 메소드를 호출했다. Integer.compare 정적 메소드는 전달된 int의 값 자체만 비교하기 때문에 오토 박싱이 따로 일어나지 않는다.

long, double 같은 자료형에 대한 메소드는 아직 추가하지 않았다. TouchMyBody에서는 아직 필요하지 않기 때문이다.(ver 0.0.13 기준) 만약 추가한다면 notLessThanLong, notMoreThanDouble 이런식으로 메소드를 정의하고 Long.compare, Double.compare 을 호출하면 될 것이다.

반영 commit : https://github.com/Gunyoung-Kim/Touch-My-Body/commit/4b776f696bcb5e3a1bec7b99c7b2b7a4267b7692 , https://github.com/Gunyoung-Kim/Touch-My-Body/commit/fae7d12f675b69cf34892ecd06942810c05e843d