포스트

업무에서 제네릭을 잘못 쓰고 있는 상황 분석

안녕하세요 🐸

분명 제네릭 공식 문서를 보고 내용도 정리하면서 수박 겉핥기 식이라도 얼핏 이해한 시늉정도는 했다 생각했는데 저는 한낱 범부였음을 깨달은 오늘입니다.

문제 환경

여기 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() 을 통해 내부 요소에 접근한다면 이 시점에는 실제 타입이 필요해지기 때문에 이 때에 에러가 발생.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.