업무에서 제네릭을 잘못 쓰고 있는 상황 분석
안녕하세요 🐸
분명 제네릭 공식 문서를 보고 내용도 정리하면서 수박 겉핥기 식이라도 얼핏 이해한 시늉정도는 했다 생각했는데 저는 한낱 범부였음을 깨달은 오늘입니다.
문제 환경
여기 2개의 VO 클래스가 있습니다.
1
2
3
vo
|-MyVO
|-YourVO
각각의 클래스 구조는 다음과 같습니다.
1
2
3
4
5
6
7
8
// File : MyVO
public class MyVO{
private List<YourVO> yourVos;
/*
setter/getter 생략
*/
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// File : YourVO
public class YourVO{
private String name;
private String gender;
private int age;
public void setName(String name){
this.name = name;
}
public void setGender(String gender){
if("M".equals(gender)){
this.gender = "남자";
} else if ("F".equals(gender)){
this.gender = "여자";
} else {
this.gender = "미공개";
}
}
public void setAge(int age){
this.age = age;
}
// getter 생략
}
문제 상황
List<Object>
타입인 변수를 List<YourVO>
로 캐스팅 할 때 List 로의 캐스팅은 됐지만 제네릭 타입을 사용한 MyVO 로의 변환이 되지 않았음.
문제 상황 코드)
1
2
3
4
5
6
7
8
9
10
11
12
List<Object> objects = new ArrayList<>();
objects = apiRequest(); // 외부 API를 호출 후 응답을 받는 메서드
MyVO myVO = new MyVO();
List<YourVO> yourVOs = (List<YourVO>) objects;
myVO.setMyVO(yourVOs);
return myVO;
여기서 objects 라는 데이터의 값은 아래의 형태 입니다.
1
2
3
4
5
6
7
8
9
10
11
12
[
{
"name":"A",
"gender":"M",
"age":"20"
},
{
"name":"B",
"gender":"F",
"age":"30"
}
]
형태만 보면 List<YourVO>
의 형식과 같습니다 YourVO 에서 필요로 하는 필드도 있고 List
형태로 있기 때문입니다. 여기서 사실 YourVO 의 형식과 같은지는 중요하지 않고 List 인지가 중요했을 뿐입니다.
문제 발생
이제까지의 코드를 봤을 때 gender
필드의 값이 남자
, 여자
,미공개
로 저장되어있을 것이라는 착각을 했습니다. 하지만 실제로는 M
,F
라는 원시 값이 저장되어 있습니다.
문제 원인
원인은 제네릭의 타입 소거입니다.
MyVO의 필드인 List<YourVOs> yourVOs
라는 필드에 대해 런타임 수준에서는 타입 변수가 YourVO 임을 알 수 없습니다.
그렇기 때문에 런타임 단계에서 setYourVos()
가 동작했을 때에는 List 인지를 확인하고 값을 저장했을 뿐 실제 그 값의 타입 변수가 무엇인지는 확신할 수 없습니다.
문제 원인 탐구
이 문제를 보다 이해하기 쉽게 하기 위해 로그를 남겨봤습니다.
1
2
3
4
5
6
7
System.print.out("List<YourVO> 의 타입 : "+myVO.getYourVOs().getClass().getName());
//
for(Object vo : myVO.getYourVOs()){
System.print.our("제네릭 타입 : "+vo.getClass().getName());
}
위와 같이 로그를 남겨봤을 때 출력 결과는 아래와 같았습니다.
1
2
3
List<YourVO> 의 타입 : java.util.ArrayList
제네릭 타입 : java.util.LinkedHashMap
제네릭 타입 : java.util.LinkedHashMap
여기서 ArrayList 라고 나오는 것은 제네릭을 몰라도 천천히 보면 알 수 있겠지만 갑자기 LinkedHashMap ? 이건 제네릭에 대한 이해가 없다면 모를 수 있습니다.
여기서 제네릭의 타입 소거에 대해 다시 한번 복기하고자 간단하게 요약해보겠습니다.
- 제네릭의 타입소거(Generic Type Erasure)란?
- 컴파일러가 제네릭의 타입 변수를 제거하는 것
제네릭 공식 문서에서 타입 소거에 대해 알아봤던 글 입니다.
결과적으로 컴파일 된 MyVO는 List<YourVO>
타입이 아니고 List
타입이 되는 것 입니다.
여기서 한 가지 의문점이 생겼습니다.
Q. 타입 소거로 인해 의도한 타입으로 캐스팅 되지 않은 것은 알겠지만 어째서 런타임 에러가 발생하지 않은 것이지?🤔
A. List
타입으로 캐스팅은 되었지만 실제로 내부 요소에 접근하기 전까지는 타입 확인을 하지 않기 때문에 예외가 발생하지 않음.
만약 get()
을 통해 내부 요소에 접근한다면 이 시점에는 실제 타입이 필요해지기 때문에 이 때에 에러가 발생.