문제 상황을 이해하기 위해서 다음과 같은 문제를 풀어 보자

interface Fruite {
    Integer getSize();
}
        // apple.java
    class Apple implements Fruit, Comparable<Apple> {
        private final Integer size;

        public Apple(Integer size) {
            this.size = size;
        }

        @Override public Integer getSize() {
            return size;
        }

        @Override public int compareTo(Apple other) {
            return size.compareTo(other.size);
        }
    }
    // orange.java
    class Orange implements Fruit, Comparable<Orange> {
      private final Integer size;

      public Orange(Integer size) {
          this.size = size;
      }

      @Override public Integer getSize() {
          return size;
      }

      @Override public int compareTo(Orange other) {
          return size.compareTo(other.size);
      }
  }

  //main.java
  class Main {
    public static void main(String[] args) {
        Apple apple1 = new Apple(3);
        Apple apple2 = new Apple(4);
        apple1.compareTo(apple2);

        Orange orange1 = new Orange(3);
        Orange orange2 = new Orange(4);
        orange1.compareTo(orange2);

        apple1.compareTo(orange1);  // Error: different types
    }
}

일단 우선 위의 예제에서는 compareTo라는 함수가 중복된다. 이렇게 되면 Fruit를 상속받는 모든 클래스가 이 compareTo를 Implement하는 문제가 생긴다. 이걸 수정하기 위해서 다음과 같이 해보자

  • Common 클래스로 중복 옮기기
    class Fruit implements Comparable<Fruit> {
        private final Integer size;
    
        public Fruit(Integer size) {
            this.size = size;
        }
    
        public Integer getSize() {
            return size;
        }
    
        @Override public int compareTo(Fruit other) {
            return size.compareTo(other.getSize());
        }
    }
    
class Apple extends Fruit {
    public Apple(Integer size) {
        super(size);
    }
}
class Orange extends Fruit {
    public Orange(Integer size) {
        super(size);
    }
}

CompareTo를 Common class로 추출해서 위와 같이 작성하면 쉽게 구현체를 만들 수 있다. 이 코드가 자바라서 class를 사용했지만 java8에서는 default Method가 지원되서 interface를 이용해도 가능하다.

이 때 문제점은 두개의 서로 다른 타입을 비교 했을 때 에러가나지 않는다는 점이다. 우리는 서로 다른 타입을 비교하면 에러가 나기를 원한다.

apple1.compareTo(orange1);    // No error

이 때 사용되는 것이 바로 타입 파라미터이다.

class Fruit<T> implements Comparable<T> {
    private final Integer size;

    public Fruit(Integer size) {
        this.size = size;
    }

    public Integer getSize() {
        return size;
    }

    @Override public int compareTo(T other) {
        return size.compareTo(other.getSize());     // Error: getSize() not available.
    }
}

그런데 Fruit에 타입 파라미터를 추가하니 T의 메서드인 getSize()는 컴파일러가 알 수 없다. 왜냐면 우리의 클래스 Fruit클래스의 타입파라미터 T는 어떠한 Bound도 없기 때문이다. 그래서 T는 어떠한 클래스도 될 수 있으며, 모든 클래스가 getSize()를 가질 수도 없다.

이때 도입되는 것이 Recursive Type Bound이다.

class Fruit<T extends Fruit<T>> implements Comparable<T> {
    private final Integer size;

    public Fruit(Integer size) {
        this.size = size;
    }

    public Integer getSize() {
        return size;
    }

    @Override public int compareTo(T other) {
        return size.compareTo(other.getSize());     // Now getSize() is available.
    }
}
class Apple extends Fruit<Apple> {
    public Apple(Integer size) {
        super(size);
    }
}
class Orange extends Fruit<Orange> {
    public Orange(Integer size) {
        super(size);
    }
}

우선 컴파일러에게 이 T가 Fruit를 상속 받은 T라는 것을 말해주자. 다른 말로 Upper bound가 “T extends Fruit<T>“라는 뜻이 된다.

이것은 오로지 Fruit의 SubType만이 Type Argument로 사용될 수 있음을 뜻한다.

모든 문제가 해결 되었을까?

아니다. 이 패턴에는 함정이 하나 있다. 컴파일러는 다른 서브타입에 대한 Type argument를 막지 못한다. 예를 들어보자.

class Orange extends Fruit<Orange> {}
class Apple extends Fruit<Orange> {} // no compile error

이 문제를 스칼라에서는 self:E라는 것을 통해서 풀어본다고 하는데 어떻게 하는지 나중에 살펴보자