본문 바로가기

java

제네릭이란 무엇인가? ( Generics )

동적 타이핑이 가능한 Javascript 나 python 을 사용하다보면 항상 하는 생각이 있다.

이 값이 문자인가? 아니면 이 값이 숫자 인가? 정말 헷갈릴 때는 결국 타입 체크를 통해서 해당 타입을 추론하며 코드를 작성했었다. 동적이기 때문에 타입 지정을 하지 않고 막 쓸 수 있어서 편한 점은 있지만, 사용이 편하기 때문에 타입 에러나 런타임 에러 등 다른 문제가 발생하게 된다. ( 세상 이치 인 것 같다 )

 

아무튼 이번에 자바를 공부하면서 제네릭이 나오는데 JS 를 많이 하다 보니 타입 스크립트를 공부할 때 도움이 많이 될 것 같다. 보다 보니 언어가 굉장히 많은 부분 비슷하다고 느껴 지고 확실히 좋은 점은 서로 닮아가려는 점 때문에 비슷해지는 것 같다.

 

( 예를 들면 자바 최신 문법 중 var 동적 타입이 사실은 JS 에서 사용되는 것과 유사 한 것 )

 

자바 같이 정적 타입 언어 같은 경우, JS 같은 언어의 자유로움을 부러워하는 반면, JS 는 동적 타입 언어라 타입 스크립트 같은 정적 타입 ( Generics 나 인터페이스 등 ) 으로 타입 추론이 없고 지정해서 사용하고 싶어하는 것 같다.

 

제네릭

타입 안정성 ( Type Safe )

 

객체 지향 프로그램 개발에 있어서 타입의 중요성은 정말 중요하다.

고정된 코드에서는 크게 문제가 되지 않지만, 실행 과정에서 동적으로 전달되는 객체를 참조해야하는 경우

잘못된 타입이 전달되면 문제가 된다.

 

예를 들면 다음 코드에서

 

Color color = new Color("red");

 

Color 객체를 생성하는데 "red" 라는 문자열 데이터가 사용되었다.

컴파일 상에서는 전혀 문제가 없지만, 만일 문자열 값이 잘못된 것이면 실행 중 런타임 에러가 발생한다.

 

이 경우에는,

 

Color color = new Color(Color.RED);

 

객체 타입으로 지정을 해버리면 컴파일시 잘못된 색상을 사용할 가능성이 차단된다. 마찬가지로 다양한 타입으로

데이터가 구성될 수 있는 클래스를 설계할 때 제네릭을 사용하면 이와 같은 타입 문제를 해결할 수 있다.

 

 

ArrayList 는 배열과 유사한 자료구조를 제공하는 클래스로 Object 타입의 데이터를 저장할 수 있다.

 

하지만, Object 클래스는 모든 자바 클래스의 슈퍼 ( 부모 ) 클래스 이므로 실제로는 모든 자바 클래스가 원소로 들어 갈 수 있다는 의미이다.

 

매우 편한 구조이긴 하지만, ArrayList 로 부터 참조 원소들을 꺼내 사용할 때 타입들이 서로 다를 수 있기 때문에

메서드의 사용 등이 차이가 있어 타입 비교를 해야하는 문제가 발생한다.

 

이와 같이 다양한 타입의 객체들을 다루는 메서드나 컬렉션 클래스에 컴파일 과정에 타입 체크를 해주는 기능을 

제네릭 이라고 한다. ( compile-time type check )

 

제네릭 사용의 장점은 다음과 같다.

 

  • 제네릭 클래스 타입의 객체를 생성할때 개발자가 원하는 타입을 지정할 수 있음.
  • 타입 안정성을 제공.
  • 의도하지 않은 타입의 객체가 저장되는 것을 막아 잘못 형변환 되는 오류를 방지.
  • 형변환의 번거로움을 줄여줌. -> 간결한 코드 유지 가능
class Storage<T> {
	T item;
    // getter, setter 생략
}

class App {
	public static void main(String[] args) {
    	Storage<String> storage = new Storage<>();
    }
}
  • T 는 타입파라미터를 의미하며 임의의 객체타입 지정이 가능.
  • 타입파라미터는 원시형은 안되고 객체타입만 가능하므로 int 의 경우 Integer 랩퍼클래스를 사용.
  • new 에서는 <> 타입 명시 하지 않아도 됨.(타입 추론 가능)

제네릭을 사용할때 주의 할 점?

  • T는 인스턴스변수로 간주되기 때문에 static멤버에는 타입변수 T를 사용할 수 없음.
  • 제네릭 타입의 배열을 생성하는 것은 불가능.
  • new, instanceof 연산자의 경우 컴파일 시점에 타입T를 명확하게 알아야 하기 때문에 T를 피연산자로 사용할 수 없음.
public class Storage<T> {
	T item;
    
    public T getItem() {
    	return item;
    }
    
    public void setItem(T item) {
    	this.item = item;
    }
}


public class GenericsTest {
	public static void main(String[] args) {
    	Storage<String> storage = new Storage<>();
        storage.setItem("My Item");
        System.out.println(storage.getItem());
        
        Storage<Integer> storage1 = new Storage<>();
        Storage1.setItem(2020202);
        System.out.println(storage1.getItem());
    }
}
  • 동일한 Storage 클래스를 사용하지만 각각 String  Integer 타입으로 선언.
  • 선언된 타입 파라미터에 따라 setItem() 의 값의 타입 일치를 컴파일러가 체크.

제네릭 고급 사용?

위에서 말한 클래스 선언 때가 아닌 메서드 선언에서도 제네릭이 사용되는 형태가 있을 수 있다.

메서드의 인자 혹은 리턴에 제네릭이 사용될 수 있으며 다양한 타입을 처리해야 하는 경우 유용하다.

 

public <T> void print(Storage<T> storage) {

}
// 인자에 제네릭 클래스를 사용한 경우 메서드 앞에 <T> 를 붙여줘야한다.

// 리턴 타입 역시 제네릭 클래스 사용할 수 있다.

public List<String> getList() {

}

// 인자와 리턴이 모두 제네릭을 가지는 경우에는 다소 복잡해질 수 있다.

public <T> List<Character> convert(Storage<T> storage) {

}

와일드 카드 

 

제네릭 타입을 사용할때 발생할 수 있는 문제점으로 특정 제네릭 타입을 인자로 받는 메서드를 구현하는 상황을

예로 들 수 있다.

앞에서 만든 Storage 를 인자로 하는 메서드를 살펴보면,

public void print(Storage<String>) {
    ..
}

인자로 String 타입 파라미터를 가지는 Storage 클래스가 지정되어 있기 때문에 Storage 타입은 해당 메서드를 이용할 수 없습니다. 인자가 다르니 메서드 오버로딩을 사용하면 어떨까요 ? 아쉽게도 제네릭의 경우 클래스 타입 자체는 동일하므로 오버로딩이 적용되지 않습니다. 만약 허용된다고 해도 필요한 타입마다 메서드를 오버로딩하는 것도 바람직한 구조는 아닙니다.

이 경우 와일드 카드를 사용해 사용할 수 있는 타입에 유연성을 부여하는 방법이 있습니다.

 

 

public void print(Storage<? extends Storage) {

}
  • ?는 사실상 Object 클래스라고 볼 수 있음.
  • extends , super 를 통해 올수 있는 타입의 관계를 특정할 수 있음.
  • ? extends T : T와 그 자손만 가능.
  • ? super T : T와 그 부모만 가능.

사용 실습

 

package com.noa.chapter1;

import java.util.ArrayList;
import java.util.List;

class Storage<T> {
    T item;

    public T getItem() {
        return item;
    }

    public void setItem(T item) {
        this.item = item;
    }
}

public class Generics {
	public <T> List<Character> convert(Storage<T> storage) {
		ArrayList<Character> list = new ArrayList<>();
		
		String s = String.valueOf(storage.getItem());
		int size = s.length();
		
		for(int i = 0; i < size; i++) {
			list.add(s.charAt(i));
		}
		
		return list;
	}
	
	public static void main(String[] args) {
		Storage<String> s1 = new Storage<>();
		s1.setItem("MyItem");
		
		Storage<Integer> s2 = new Storage<>();
		s2.setItem(20201920);
		
		Generics gt = new Generics();
		
		System.out.println(gt.convert(s1));
		System.out.println(gt.convert(s2));
	}
}
  • 앞에서 만든 Storage 클래스를 활용.
  • 2개의 서로 다른 타입을 가지는 Storage 인스턴스 생성.
  • convert() 메서드는 Storage 타입을 받아 getItem() 결과를 문자열로 변환.
  • 문자열을 Character List 로 변환해서 리턴.

'java' 카테고리의 다른 글

문자열 다루기?  (0) 2021.01.21
java 예외 처리  (0) 2021.01.20
제어자?  (0) 2021.01.20
오버로딩과 오버라이딩  (0) 2021.01.19
자바 메모리 관리 ?  (0) 2021.01.19