오늘도 새로운 것을 공부하기 위해 기술블로그를 구경하던 도중 좋은 글이 있어 해당 내용을 공유하고자 합니다. 원문을 보고싶으신 분들은 A Deep Dive Into Java Wildcards — Covariance를 참고하시길 바랍니다.
처음 Wildcard를 접했을 때 <T>
,<U>
,<S>
와 같이 오는 이 놈들은 무엇이고 <T extends Number>
vs <? extends Number>
를 어느 경우에 사용해야 하는지 굉장히 혼란스러웠다.
오늘 우리는 <? extends Bla>
에 대해 이해하는 시간을 가질 것이다. 단순히 암기하는 것이 아닌 핵심에 대해 다룰 예정이니 천천히 읽어보도록 하자.
깊게 들어가기 전에 사전 배경부터 핵심까지 다룰 예정이니 꽉 붙잡아라.
우리는 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이 도입되었을 때, 그들은 <T>
개념을 가지고 있지 않아 유형 정보없이 코드를 작성할 수 있었다. 따라서 다음과 같은 코드를 작성해도 오류가 발생하지 않았다.
List s = new ArrayList();
s.add("hi");
s.add("hello");
s.add(2) // ??
이는 Java 5의 컬렉션 개선사항으로 반영되어 그 이후부터는 컴파일러에서 잘못된 할당을 탐지할 수 있도록 개선되었다.
이제 한 번 실습을 통해 이를 경험해보자.
public class BaseWork {
public void run() {
System.out.println("BaseWork Start!");
}
}
public class DetailWork extends BaseWork{
@Override
public void run() {
System.out.println("BaseWork Start!");
}
}
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를 적용할 경우 개발자들은 다음과 같은 이점을 얻을 수 있다.
그렇다면 Wildcard가 어떤 오류들을 컴파일 과정에서 제공해주는지 확인해보자.
Wildcard를 만든 개발자들은 참으로 똑똑하다.
Covariance를 안전하게 제공하는 것을 보장하기 위해 다음과 같은 추가적인 정보들을 관리한다.
그럼 이제 Wildcard가 관리하는 오류들을 만들러 가보자.
다음과 같은 내용들을 추가로 관리하는 이유는 앞서 말씀드린 런타임시에 발생하는 오류들에 대해 사전에 방지하고자 다음과 같은 제한을 둔 것이다.