Java Wildcard 파헤치기 - 출생의 비밀

겔로그·2022년 9월 17일
0

Java

목록 보기
5/10

개요

오늘도 새로운 것을 공부하기 위해 기술블로그를 구경하던 도중 좋은 글이 있어 해당 내용을 공유하고자 합니다. 원문을 보고싶으신 분들은 A Deep Dive Into Java Wildcards — Covariance를 참고하시길 바랍니다.

처음 Wildcard를 접했을 때 <T>,<U>,<S>와 같이 오는 이 놈들은 무엇이고 <T extends Number> vs <? extends Number>를 어느 경우에 사용해야 하는지 굉장히 혼란스러웠다.

오늘 우리는 <? extends Bla> 에 대해 이해하는 시간을 가질 것이다. 단순히 암기하는 것이 아닌 핵심에 대해 다룰 예정이니 천천히 읽어보도록 하자.

깊게 들어가기 전에 사전 배경부터 핵심까지 다룰 예정이니 꽉 붙잡아라.

Covariance and Contravariance — Important Concepts!

Covariance Rule

우리는 String 객체가 Object를 상속해 자식 객체라는 것을 안다. Java의 룰에 따르면 우리는 다음과 같이 부모 객체로 자식 객체를 참조할 수 있다.

String s = "Hello World!";
Object o = s; //부모객체 Object로 자식객체 String 참조

이를 Covariance라 한다.

Covariance rules에 따르면 이것 또한 가능하다.

String[] sArray = { "Hello World!" };
Object[] o = sArray; // Valid in Java.

우리는String[]Object[]를 HT(holder type)이라 부른다. HT에는 무언가를 잡고 있을 수 있는 객체들이 포함된다.(ex) List<>, Set<>, Box<> 등)

따라서 Covariance에 따를 경우 Object 객체가 String 객체의 부모일 경우 HT<Object> 또한 HT<String>의 부모이다.

List<String> s = new ArrayList<>();
List<Object> o = new ArrayList<>();

// 이건 컴파일되지 않지만 covariance가 무슨 개념인지 알려줍니다.
o = s; 
// 우리는 List가 covariant 하다는 것을 다음 예시로 알 수 있지만 List는 단순히 List만으로는 covariant하지 않다는 것을 알 수 있습니다.

배열 또한 Covariant하기 때문에 다음 코드 또한 유효합니다.

String[] sArray = { "Wildcards" };
Number[] nArray = { 2, 3 };
Object[] o = sArray; 
o = nArray; // Object는 Number의 부모이기도 하기에 가능하다.

배열을 covariant하게 만든 것은 자바의 디자인 결정이었다. 그러나, 배열을 covariant하게 만듬으로서 수많은 다형적 행위가 허용되었다. 개발자는 'Object[] 내부에 비즈니스 객체를 넣어서 개발'과 같은 제네릭 코드를 이용한 개발을 할 수 있었다. 그러나 이것은 버그를 불러일으켰고 이것은 오로지 실행시에만 발견이 가능했다.

다음 예시를 보자

Number[] nArray = { 2, 3 };
Object[] o = nArray;
o[0] = "s"; // ArrayStoreException으로 인한 충돌 발생.

큰 어플리케이션이나 라이브러리에서 이러한 종류의 에러는 충돌의 가능성을 높였고 위험성을 유발하였다.

Collections의 시대

collections이 도입되었을 때, 그들은 <T> 개념을 가지고 있지 않아 유형 정보없이 코드를 작성할 수 있었다. 따라서 다음과 같은 코드를 작성해도 오류가 발생하지 않았다.

List s = new ArrayList();
s.add("hi");
s.add("hello");
s.add(2) // ??

이는 Java 5의 컬렉션 개선사항으로 반영되어 그 이후부터는 컴파일러에서 잘못된 할당을 탐지할 수 있도록 개선되었다.

이제 한 번 실습을 통해 이를 경험해보자.

실습

  • BaseWork.java
public class BaseWork {
    public void run() {
        System.out.println("BaseWork Start!");
    }
}
  • DetailWork.java
public class DetailWork extends BaseWork{
    @Override
    public void run() {
        System.out.println("BaseWork Start!");
    }
}
  • main.java
public class main {
    public static void main(String args[]) {
        BaseWork work = new BaseWork();
        DetailWork detailWork = new DetailWork();

        startJob(work); // Valid!
        startJob(detailWork); // Valid, polymorphism magic.

        List<BaseWork> works = new ArrayList<>();
        List<DetailWork> detailWorks = new ArrayList<>();

        startJob(works);
        startJob(detailWorks);
    }
    private static void startJob(BaseWork incomingWork) {
        incomingWork.run();
    }

    private static void startJob(List<BaseWork> incomingWorks) {
        for(BaseWork incomingWork : incomingWorks) {
            incomingWork.run();
        }
    }
}

다음과 같이 코드를 구현하면 실행 이전에 다음과 같은 오류가 발생한다.

오...잉? 뭐지? 이것은 다음과 같은 이유로 발생한다.

DetailWork는 BaseWork의 자식이더라도 List<DetailWork>List<BaseWork>의 자식이 아니다.

음..? 아까는 된다며..?

배열을 covariant하게 만듬으로서 발생한 문제들을 피하기 위해 컬렉션에서는 허용하지 않는 것이다. 이를 허용했을 경우, 이전에 발생했던 runtime 에러가 동일하게 발생했을 것이다.

문제 해결

이제 이러한 해결을 통해 List<>는 더 이상 covariance를 지원하지 않는다는 것을 알 수 있다. 하지만 우리의 개발자들은 이를 사용하고 싶어하기에 List에 covariance를 지원하면서도 convariance를 안전하게 이용할 수 있는 방법 을 고안하였다. 이것이 Wildcard이다.

List<? extends BaseWork> 

이를 이용해서 다시 한 번 main 코드를 수정해보자.

public class main {
    public static void main(String args[]) {
        BaseWork work = new BaseWork();
        DetailWork detailWork = new DetailWork();

        startJob(work); // Valid!
        startJob(detailWork); // Valid, polymorphism magic.

        List<BaseWork> works = new ArrayList<>();
        List<DetailWork> detailWorks = new ArrayList<>();

        startJob(works);
        startJob(detailWorks);
    }
    private static void startJob(BaseWork incomingWork) {
        incomingWork.run();
    }

    private static void startJob(List<? extends BaseWork> incomingWorks) {
        for(BaseWork incomingWork : incomingWorks) {
            incomingWork.run();
        }
    }
}

Wildcard를 적용할 경우 개발자들은 다음과 같은 이점을 얻을 수 있다.

  • 런타임시에 발생하는 오류들을 컴파일 과정에서 확인할 수 있다.
  • Covariance가 없는 객체들에게 Covariance를 지원할 수 있도록 만들어준다.

그렇다면 Wildcard가 어떤 오류들을 컴파일 과정에서 제공해주는지 확인해보자.

제공하는 compile 정보

Wildcard를 만든 개발자들은 참으로 똑똑하다.
Covariance를 안전하게 제공하는 것을 보장하기 위해 다음과 같은 추가적인 정보들을 관리한다.

그럼 이제 Wildcard가 관리하는 오류들을 만들러 가보자.

1. Wildcard 적용 인수에 관련된 객체로 형변환

2. 한 번 참조한 Covariance 객체에 무언가를 추가할 경우

다음과 같은 내용들을 추가로 관리하는 이유는 앞서 말씀드린 런타임시에 발생하는 오류들에 대해 사전에 방지하고자 다음과 같은 제한을 둔 것이다.

결론

  1. Covariance는 HT(홀더 유형)간에 동일한 부모-자식 관계를 설정하는데 사용할 수 있다. 이는 Java에선 기본적으로 제공하지 않는다.
  2. HT를 읽기 전용으로 만드는데 사용할 수 있다.(extends 된 부모 유형만 읽기)
  3. extends 클래스 참조만 가능하도록 HT를 제한한다.
profile
Gelog 나쁜 것만 드려요~

0개의 댓글