본 글은 2021년 12월 19일 에 기록되었다.
지네릭스에 대한 이론적인 공부가 전반되어 있지 않은 상태에서
그 특성 및 사용 예시에 대한 공부를 진행하였기 때문에 결점이 많다고 생각되어
이를 다시 공부하고 재 작성하게 되었다.
본 글의 이전 버전 작성일인 2021년 12월 13일 에
Effective Java 4 | Generics 를 배우고 이에 대한 내용의 필요성을 체감하고 배우게 되었다.
Generics 란
다양한 타입의 객체들을 다루는 메서드나 컬랙션 클래스에
컴파일 시의 타입 체크를 해주는 기능이다.
객체의 타입을 컴파일 시에 체크하기 때문에
다음과 같은 이점을 가지게 된다.
class Box <T> {}
아래의 경우는 Generics 를 사용할 수 없다.
그 아래 줄에는 자세한 설명이 써져있으니,
이해가 되지 않는다면 참고하도록 하자.
원제목 | 지네릭스 타입 정적 매개변수
generics는 특정한 클래스 혹은 매개변수에 넣을 어떤 값(=객체)를
컴파일 시에 결정 지음으로 인해서 타입 안정성 및 유연성 을 확보하는 것이다. 이러한 특징으로 generics는 인스턴스 변수로 참조된다.
그리고 static member variables 는 인스턴스 변수 를 참조할 수 없기 때문에, generics와 공존할 수 없다.
원제목 | 지네릭스 배열
결론부터 말하면 원칙적으로 불가 하지만 동적 객체 생성 혹은 Object 배열의 지네릭스 배열로 형번환 의 방법을 사용해야 한다.
원칙적으로 불가능한 이유는 new 연산자 떄문.
T[] itemArr; // 가능
T[] itemArr=new T[10]; // 불가능 | new 연산자는 컴파일 시 T 를 알아야 함.
동적 객체 생성
Reflection API 의 newInstance() 참고
단, Spring 을 공부하고 있거나 Java 의 전반을 공부한 사람에게 추천한다.
Object 배열의 지네릭스 배열로 형변환
T[] itemArr=(T[]) new Object[10]; // 가능
지네릭스 를 처음 배우고 가장 혼란스러운 점은
다음과 같은 부분을 생각하지 않고 공부하기 때문에 발생한다고 생각한다.
또한 생각보다 지네릭스와 관련된 내용이 많다는 점이다.
Effective Java 4 | Generics 의 포스트를 작성하며 간단하게 작성했지만,
여기서 그 예를 다시 한 번 볼테니 이를 기준 삼아 이어나가면 편할 것 같다.
원제목 | 타입 변수
타입변수란,
기호의 종류만 다를 뿐, 임의의 참조형 타입 을 의미한다.
Essentials of java 와 Effective Java 뿐만 아니라 수많은 곳에서 이러한 타입 변수라는 명칭을 쓰고 있었다. 하지만 나는 이것을 추상적 지네릭스 라고 이해하는 것이 직관적이라고 생각할 뿐이다.
이 시점에서는 어떠한 구체적인 타입을 받아들일지 결정하지 않았다.
아래의 코드 예제를 참고해보자
class Box<T> {
T item;
T getItem() {
return item;
}
void setItem(T item) {
this.item=item;
}
}
원제목 | 실제 타입
위 추상적 지네릭스를 제대로 읽고 이해했다면 바로 이해했을 것이라 생각한다.
말 그대로 어떠한 설계 도면을 기반으로 만든 객체(인스턴스)에 실제 타입을 넣어주는 순간을 의미한다.
바로 아래 코드를 보자
class Box<T> {
// 추상적 지네릭스 예제 코드와 동일
}
class Main {
Box<String> box1=new Box<String>(); // jdk 1.5 이후 가능
Box<String> box2=new Box<>(); // jdk 1.7 이후 가능
}
원제목 | 제한된 지네릭스
위 추상적 지네릭스와 구체적 지네릭스 를 제대로 읽었다면 아래를 알게 되었을 것이다.
위의 두 내용이 클래스 작성 + 객체 생성과 닮았다는 점을 말이다.
제한된 지네릭스 는 extends 에 대한 내용이고
이 또한 객체의 상속 닮았다.
지금까지 공부한 내용을 기반으로,
제한된 지네릭스에 대해서 전술한 내용을 살펴보자.
표현식에 추상적 지네릭스가 들어가 있는 것을 보면,
본능적으로 아 이 문법은 클래스 작성 시점에 사용하는 것이구나 라고 알 수 있다.
그리고 상속이라는 본질을 생각해보면,
객체 생성 시점 에 추상적 지네릭스가 구체적 지네릭스로 바뀌게 되는데,
이 때, extends 뒤의 부분 String 혹은 String&Boolean 혹은 Object 등 만을 받아들일 수 있게 하는 구나 라고 알 수 있다.
원제목 | 와일드 카드
추상적, 구체적, 제한적 지네릭스를 통해서
우리는 지네릭스 클래스, 객체 그리고 그 중간 사이의 무언가에 대한 것을 배웠다.
그러나 이 모든 것들은 한 가지 타입에 대한 것에 국한된 유연성 임을 알 수 있다.
위 문제를 실감하기 위해 다음과 같은 비즈니스 로직이 있다고 해보자.
우리는 2개의 Dto 를 가지고 있다.
그리고 그 Dto 는 모두 objectCode 라는 기본키 필드(멤버 변수) 를 가지고 있다.
이는 SQL 의 object_code SEIRAL PRIMARY KEY 와 대응된다.
아직 JDBC, SQL 을 배우지 않은 사람을 위해 풀어 쓰자면,
Java 와 DB 가 2개의 데이터 모델을 가지고 있고 각 모델은 objectCode 라는 유니크한 고유값을 가지고 있다고 이해하면 될 것같다.
우리는 Java와 DB 를 연결하기 위해 한개의 Dao 를 만들 것인데,
그 Dao 안에는 하나의 출력 메서드가 있는데 objectCode 와 targetTableName 을 넣어주면
targetTableName에 따라 2개의 데이터 모델 중 하나로 연결을 해주고
objectCode에 따라 2개의 데이터 모델 중 하나로 객체 생성을 하고
이를 리턴한다고 쳐보자.
위 내용을 코드로 작성해보면 다음과 같이 될 것이다.
그렇다면 저 <어떠한 지네릭스 문법> 안에 어떠한 내용이 들어가야 할 것이냐 라는 문제점이 생긴다. 왜냐하면 해당 Dao 는 2개의 Dto 에 대한 처리를 다루고 있기 때문이다.
public UtilityDao<어떠한 지네릭스 문법>{
public void search (int objectCode, String targetTableName) {
T dto=null;
if(targetTableName.equals("모델 1의 이름")){
// 프로세스 : 결과값들을 받음
dto=new T(...args); // 결과값들을 기반으로 T 클래스를 만듬
}else if(targetTableName.eqauls("모델 2의 이름")){
// 프로세스 : 결과값들을 받음
dto=new T(...args); // 결과값들을 기반으로 T 클래스를 만듬
}
return dto;
}
}
그러한 문제를 해결하기 위해 우리는 아래와 같은 방법들을 사용할 수 있다.
따라서 UserDto 와 PostDto 가 있다고 가정하고 이 둘의 공통 요소인 objectCode 를 가지고 있는 부모 클래스인 ObjectDto 를 만들어서 아래와 같이 코드를 개선할 수 있다.
class UtilityDao<? extends ObjectDto>{
public void search(int objectCode, String targetTableName) {
T dto=null;
if(targetTableName.equals("UserDto")){
dto=new T(...args);
}else if(targetTableName.eqauls("PostDto")){
dto=new T(...args);
}
return dto;
}
}
public Main{
public static void main(String[] args){
// 부모 타입 사용가능
UtilityDao<ObjectDto> utilityDao1=new UtiltiyDao<>();
// 자식 타입 사용가능(모두)
UtilityDao<PostDto> utilityDao2=new UtiltiyDao<>();
UtilityDao<UserDto> utilityDao3=new UtiltiyDao<>();
}
}
이것으로 이론 기본의 설명은 끝이 났다고 생각한다.
지네릭스를 사용해보고 감을 잡을 수 있는 방법은 다음과 같다.
나는 1,2 번을 하면서 3번을 통해 최종적으로 지식 검증을 하는 것이 좋은 것 같다.
활용에서는 다음과 같은 내용을 포함하고 있다.
지네릭스 타입의 형변환에서는 다음과 같은 케이스를 분석하고 있다.
Box box=null;
Box<Object> objBox=null;
box=(Box) objBox; // 지네릭스 타입 > 원시타입 | **경고발생**
objBox=(Box<Object> box; // 원시타입 > 지네릭스 타입 | **경고발생**
Box<Object> objBox=null;
Box<String> strBox=null;
objBox=(Box<Object>) strBox; // 타입 매개변수 String > Object | **에러**
strBox=(Box<String>) objBox; // 타입 매개변수 Object > String | **에러**
Box<? extends Object> objBox=null;
Box<String> strBox=null;
objBox=(Box<? exends Object>) strBox; // **가능**
strBox=(Box<String>) objBox; // **가능** | **경고발생** | 확인되지 않은 형변환
컴파일러는 지네릭 타입을 이용해서 소스파일을 체크하고
필요한 곳에 형변환을 넣어주며 지네릭 타입을 제거한다.
즉, 컴파일된 파일(*.class) 에는 지네릭 타입에 대한 정보가 없는 것이다.
지네릭 타입의 제거는 다음과 같은 프로세스로 진행된다.
본 글은 2021년 12월 13일 에 기록되었다.
지네릭스에 대한 이론적인 공부가 전반되어 있지 않은 상태에서
그 특성 및 사용 예시에 대한 공부를 진행하였기 때문에 결점이 많다고 생각되어
이를 다시 공부하고 재 작성하게 되었다.
클래스 인스턴스화에서, 해당 인스턴스의 자료형 타입을 제한하는 기능
특정한 타입을 매개변수로 받을지 모르는 상황에서,
어떠한 타입을 매개변수로 받더라도 자동으로 형변환하여 에러를 방지해주는 기술
그 과정에서 제한하는 효과가 발생할 뿐이다.class Numb { int i=0; } public class Main{ public static void main(String[] args){ Numb<Integer> a=new Numb<Integer>; } }
학습 과정에 사용한 npm propTypes 이랑 약간 비슷한 것같다.
node.js 기반으로 나는 prop-types 를 다음과 같이 사용했는데,
1. 클래스 안에 들어갈 Data Validation 을 하기 위하여
2. 객체 들을 주고받을 때 Data Validation 을 하기 위하여
위 Generics 는 아예 클레스 및 객체 단위로 사용된다는 점이 특이한 것 같다.
하지만, prop-types 에도 Object 단위로 Validation 하는 기능이 있으니,
결과적으로는 동일한 역할을 수행하는 것으로 대체할 수 있지 않을까 싶다.
용어, Term 정의
class Class<Type>{ // syntax }
Class< Type > : Generics Class : 지네릭 클래스
Class : Origin Type : 원시타입
Type : Type Variables or Type Parameter : 타입 파라미터
다형성, Polymorphism 은 Class 에서 처음 언급된 용어이다.
Generics 는 기본적으로 객체 참조형인 Integer String Bool 등으로 사용하지만,
Class 나 Interface 도 사용할 수 있다.
관련 문서를 보면 알겠지만,
static 은 전역변수 키워드로 한 번 생성되면 메모리 종료 시까지 존재하게 된다.
이러한 특징 때문에,
클래스나 객체참조 시 static 을 붙이게 되면 해당 클래스의 모든 속성값이 동일한 공간에 존재하게 되고 구분이 불가능해 진다. 그러한 원리로 모든 객체가 같은 속성값을 공유하게 된다.
그런데, Generics 를 통해서 Type Parameter 를 설정하게 되면,
객체들이 각각 다른 값을 가지게 되는 모순이 생기게 된다.Hello<AA> A=new Hello<AA>(); Hello<BB> B=new Hello<BB>(); A.props; B.props;
A를 Type Parameter로 하는 Hello.props 와
B를 Type Parameter로 하는 Hello.props 가 같을 수 없기 때문이다.
Generics Array 는 기본적으로 생성이 불가능 하다.
class Test<A> { A[] Array; // Ok A[] Array2=new A[Array.length]; // Error A[] Array3=new A[3]; // Error }
관련 문서를 보면 알겠지만,
new 연산자는 컴파일 시점에 A 가 뭔지 정확히 알아야 한다.
그런데,
Test< A > 를 컴파일 하는 시점에는 A 가 뭔지 예상할 수 없기 때문이다.
물론 생성하는 방법도 있다
- Reflection API 의 newInstance()와 같이 동적 인스턴스 생성 도구를 이용
- Object 배열로 생성해서 복사한 후 T[]로 형변환
제한된 지네릭, Limited Generics Class 는
객체 생성 당시의 Generics 를 미리 지정한 Type 만 사용할 수 있게 제한하는 것이다.
1. Fruits 으로만 제한 시에는 extends
2. Eatable 으로만 제한 시에도 extends (not Implement)
3. 둘 다 받아들이는 것으로 제한 시에는 그 사이에 & 연산자 사용interface Eatable { ... } class Fruits { ... } class Apple extends Fruits{ ... } class Grape extends Fruits{ ... } class FruitsBox<T extends Fruits & Eatable> { ... } class FruitsOnly<T extends Fruits> {... } class EatBax<T extends Eatable>{ ... }
아래와 같이 제한된다.
FruitsBox.add(new Apple()); //ok FruitsBox.add(new Grape()); // ok FruitsBos.add(new Toy()); // 에러
재료를 넣으면 해당 재료로 만들어진 쥬스를 return 하는 클래스를 만들기로 했다.
class Juicer { static Juice makeJuicer(FruitBox<T> box){ sTirng tmp=""; // 임시 변수 생성 for(Fuirt f:box.getList()) tmp +=f+" "; // 형변환 return new Juice(tmp); // 생성된 쥬스를 반환 } }
- 문제점
- Juicer 클래스가 Generics 클래스가 아니다.
- Juicer 클래스가 Generics 클래스여도, static 키워드는 Type Parameter 로 T를 가질 수 없고 구체적인 Type을 지정해주어야 한다.
class Juicer { static Juice makeJuicer(FruitBox<Fruit> box){ sTirng tmp=""; // 임시 변수 생성 for(Fuirt f:box.getList()) tmp +=f+" "; // 형변환 return new Juice(tmp); // 생성된 쥬스를 반환 } }
- 문제점
- 위와 같이 작성하면, 새로운 Type Parameter 를 받아들이기 위해 overlaoding 해야한다.
-> 그런데, Type Parameter 가 다른 것으로는 overloading 조건이 불충족된다.
-> 메서드 중복 정의 에러가 발생된다.
- 해결방안
WildCard 사용
가능
1. < ? extends T > T와 그 자녀들만 가능
2. < ? super T > T와 그 조상들만 가능
3. < ? >, < ? extends Objects > 와 동일
불가
1. < ? extends T & E>
지네릭 타입이 설정된 클래스
지네릭 타입이 설정된 메서드