일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 | 28 | 29 | 30 |
- 트랜잭션
- 제네릭
- JDK14
- B+TREE
- 재고 시스템
- CaffeineCache
- Redis
- 리팩터링
- ci/cd
- Spring Data Redis
- 공변
- springboot
- nonclustered index
- JPA
- 웹캐시
- 상태패턴
- 지연로딩
- 카카오 화재
- Metaspace
- method area
- lazyloading
- backend
- java
- Jenkins
- Ehcache
- 부하테스트
- 주문
- 동시성
- GithubActions
- JAVA8
- Today
- Total
NDM
[Java] Java Generic은 어떻게 동작할까? 본문
목차
- Java Generic이란?
- Java Generic을 왜 쓸까?
- 공변과 불공변
- Generic의 동작 방식
- 왜 Primitive type은 Generic에서 사용할 수 없는가
** 기본 문법에 대해서는 포스팅 하지 않습니다 **
Java Generic이란?
- Java 프로그래밍을 하다 보면 ArrayList<T> , HashMap<K, V> 등 <>안에 들어간 의문의 문자들을 볼 수 있는데
이를 제네릭(Generic)이라 한다. - Data type을 특정한 type하나로 정하지 않고 사용할때마다 바뀔 수 있게 범용적이고 포괄적으로 지정한다 라는
의미이며 - 클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해 지정되는 것을 의미
왜 Generic을 사용하는 걸까?
- 비슷한 기능의 경우 재사용성이 높다.
Spring을 이용해 개발을 하다 보면 JSON형식으로 데이터를 반환해 줘야 하는 경우가 많다.
만약 현재 장바구니 목록과 장바구니에 담은 물품의 개수를 담은 Dto를 만들어보자
Json 형식은 다음과 같을 것이다.
{
count : 장바구니 개수
datas : [{
Category : 의류,
productName : 후드티1
},
{
Category : 과일
productName : 사과1
}]
}
그러면 Dto코드는 이렇게 만들어야 한다.
class Dto {
private int count;
private List<장바구니> datas
}
이번엔 장바구니가 아닌, 주문한 상품의 개수와 주문한 상품 목록을 불러와 보자
{
count : 주문한 상품 개수
data : [{
Category : 의류,
productName : 후드티1
},
{
Category : 과일
productName : 사과1
}]
}
Json 형식이 똑같다. 그러면 Dto도 똑같이 쓸 수 있지 않을까? 했는데 이전에 만들어둔 Dto는 장바구니라는 타입이 이미 들어가 있어 장바구니가 아닌 상황에서는 사용할 수 없게 되었다. 사용할 경우는 많지만, 유연성이 매우 떨어진다.
그래서 Dto를 Generic을 이용해 이렇게 고쳐보았다.
class Dto<T> {
private int count;
private List<T> datas
}
장바구니든, 주문한 상품이든 어떤 경우에도 "데이터의 개수와 목록이 필요한 상황" 에서는 쓸 수 있는 Dto가 완성되었다. 이런식으로 Generic을 사용하면 비슷한 경우에 있어서 재사용성을 크게 높일 수 있다.
- 형 변환이 필요 없으며, 컴파일 시점에 Type Checking이 가능하다
여기서 공변과 불공변의 개념에 대해 정리해보자
- 공변 : 자기 자신과 자식 객체로 타입 변환을 허용 ( ex) Array )
- 불공변 : 자기 자신과 자식 객체로 타입 변환을 허용하지 않음. 두 개의 타입은 전혀 상관 없음. ( ex) Generic )
// Long보다 상위 객체인 Object가 Long으로 자유롭게 형 변환을 할 수 있다.
Object[] array = new Long[1];
Long temp = (Long) array[0];
// Object과 String은 부모-자식 관계이나 List<Object>와 List<String>은 전혀 상관없다.
public static void test(List<Object> list) {
}
List<String> list = new ArrayList<>();
list.add("Gyunny");
test(list); // 컴파일 에러
Generic은 이러한 불공변의 특성을 띄고 있다.
- 위 코드에서 Long temp = (Long) array[0] 은 형 변환이 되니 간편해 보이지만, 이러한 데이터가 수천 수만건 있다고 생각해 보면, 수천 수만번 형 변환을 해줘야 한다.
- 때문에 이러한 코드 대신 Generic을 사용하면 형 변환도 필요 없고, 컴파일 시점에 컴파일 에러를 잡아낼 수 있다는 효과가 생긴다.
- 그렇다면 Generic은 Type Casting을 정말로 하지 않는 것일까??
- Type Casting을 하지 않고 <T>를 어떻게 상황에 맞게 형변환 하는 것일까??
- 또한 컴파일 에러를 어떻게 잡아내고 있는 것일까??
- 답은 바로 Generic의 동작 방식인 Type Erasure에 있다.
Generic의 동작 방식
먼저, 구체화(reify)와 비 구체화(non-reify)의 개념에 대해 알아보자
- 구체화(reify) : 자신의 타입 정보를 런타임에도 알고 있는 것 ( ex) Array )
- 비 구체화(non-reify) : 런타임에는 소거가 되기 때문에 컴파일 타임보다 정보를 적게 가지는 것( ex) Generic )
이를 기반으로, Generic은 Type Erasure이라는 동작 방식을 취한다.
Type Erasure은 원소 타입을 컴파일 시점에만 검사하고 런타임에는 해당 타입 정보를 알 수 없는 것을 뜻한다.
즉, 컴파일 타임에만 타입 제약 조건을 정의하고, 런타임에는 타입을 제거한다는 것이다.
여기에는 몇가지 규칙이 있다.
1. unbounded Type(<?>, <T>)는 Object로 변환
2. bound type(<E extends Comparable>)의 경우는 Object가 아닌 Comprarable로 변환
3. 확장된 제네릭 타입에서 다형성을 보존하기 위해 bridge method를 생성
Generic에서 Primitive Type을 사용할 수 없는 이유
바로 Type Erasure 동작 방식 때문이었다.
런타임 시점에 Type Erasure가 이루어져 Object로 변환이 되어야 하는데, Primitive Type은 부모로 Object를 갖지 않으니 변환이 되지 않아 Generic에 Primitive Type을 사용할 수 없는 것이었다.
출처 https://st-lab.tistory.com/153
https://devlog-wjdrbs96.tistory.com/263
'Java' 카테고리의 다른 글
[Java] Java Exception과 Spring Transaction (0) | 2022.08.06 |
---|---|
[Java] Java8 MetaSpace (0) | 2022.08.05 |
[Java] GC와 Java Reference Type 1편 : Java Reference Type (0) | 2022.05.01 |
[Java] 자바 동작 원리와 JVM 2편 : Runtime Data Area (0) | 2022.04.28 |
[Java] 자바 동작 원리와 JVM 1편 : Class Loader (0) | 2022.04.28 |