제네릭을 활용하여 범위를 구하는 'Range' util class 만들기

다음의 예시처럼 특정 숫자가 정해진 숫자 범위 안의 숫자에 해당하는지 확인하는 조건문이 있다.

if (age >= 10 && age <= 20) {
    ...
}

위의 로직을 공통으로 분리하여 범위를 구하는 util class 을 만들어 보겠다.

 

로직 분리하기

age >= 10 && age <= 20 해당 조건을 어떻게 코드를 분리해낼 수 있을까?

먼저 ‘범위'라는 객체를 생성해볼 수 있을 것 같다.

조건문 속 ‘범위'에 해당하는 부분을 분리해낼 수 있다.

10과 20은 범위에 해당하므로 ‘범위'라는 객체와 시작과 끝을 의미하는 start, end 로 구성해볼 수 있다.

class Range {
    private int start; // 범위의 시작
    private int end; // 범위의 끝

    public Range(int start, int end) {
        this.start = start;
        this.end = end;
    }
}

범위를 비교하는 로직은 어떻게 구현하면 좋을까?

‘범위' 라는 객체에게 ‘범위 안의 숫자인지’ 물어보면 좋을 것 같다.

그럼 다음과 같이 ‘Range’ 객체에게 물어볼 수 있다.

class Range {
    ...중략...
    public boolean contain(int target) { // 범위 확인 메소드 생성
        return start <= target && target <= end;
    }
}

정적 팩토리 메소드를 적용하여 객체를 생성하는 로직까지 분리해보았다.

// 생성자를 private 으로 변경하여 객체 내 에서만 호출할 수 있도록 변경
private Range(int start, int end) {
        ...중략...
}

// 외부에서 호출될 객체생성을 위한 정적 팩토리 메소드
public static Range of(int start, int end) {
    return new Range(start, end);
}

그럼 처음에 숫자를 비교했던 ‘age >= 10 && age <= 20’ 이 코드는 다음과 같이 변경된다.

if (Range.of(10, 20).contain(age)) {
    ...
}

 

여러 숫자 타입을 허용하도록 변경해보기

int 타입의 범위 확인이 가능한 ‘Range’를 활용하여 double타입의 범위도 확인하고 싶다.

어떻게하면 ‘Range’ 객체가 여러 숫자 타입을 허용할 수 있을까?

 

Number 추상 클래스와 제네릭 활용하기

모든 숫자 자료형은 Number 추상 클래스를 상속하고 있다.

따라서 다음과 같이 ‘Number’를 상속하는 클래스만 Range으로 확인할 수 있도록 적용할 수 있다.

public class Range<N extends Number> {
    ...중략...
}

그리고 객체 내 범위를 의미하는 start 와 end 그리고 확인할 숫자인 target 또한 타입을 특정하지 않고 Number의 구현체로 허용하기 위해 제네릭으로 바꾸어줄 수 있다.

그럼 코드는 다음과 같아진다.

public class Range<N extends Number> {
    private final N start;
    private final N end;

    private Range(N start, N end) {
        this.start = start;
        this.end = end;
    }

    public static <N extends Number> Range<?> of(N start, N end) {
        return new Range<>(start, end);
    }
    ...중략...
}

그럼 Number를 상속받는 제네릭 값으로 범위 비교를 어떻게 할 수 있을까?

Number 추상 클래스로 접근할 수 있기 때문에 Number의 메소드를 확인해본다.

doubleValue() 추상 메소드는 모든 구현체들이 오버라이딩하여 메소드 구현을 해야하기 때문에, 해당 메소드를 활용하여 모든 타입을 double로 변경하여 범위 비교를 할 수 있다.

public <N extends Number> boolean contain(N target) {
    return start.doubleValue() <= target.doubleValue()
        && target.doubleValue() <= end.doubleValue();
}

 

모든 숫자 타입을 받아 범위를 비교할 수 있는 Range util 클래스를 만들어 보았다.

전체 코드

public class Range<N extends Number> {
    private final N start;
    private final N end;

    private Range(N start, N end) {
        this.start = start;
        this.end = end;
    }

    public static <N extends Number> Range<?> of(N start, N end) {
        return new Range<>(start, end);
    }

    public <N extends Number> boolean contain(N target) {
        return start.doubleValue() <= target.doubleValue()
            && target.doubleValue() <= end.doubleValue();
    }
}