[Java] 제네릭(Generic) 기본

LDB·2024년 12월 27일
1

Java

목록 보기
5/6
post-thumbnail

수치스러운 행위

지금까지 Spring Boot를 하면서 제네릭을 사용할 일이 거의 없다고 생각하여 거의 신경을 쓰지 않고 있었다. 회사다니던 시절에도 제네릭은 사용할일이 사실상 없었고 또한 구글링을 해도 제너릭을 사용하는 코드가 거의 없었다. 나가 죽어라 과거의 나 하지만 세상에나 내가 DB와 통신을 할 때 사용하는 Spring Data JPA의 JpaRepository가 제네릭으로 이루어져 있었다. 이것만으로도 나는 제네릭을 알아야 했다. 그렇기에 지금이라도 제네릭에 대해 공부하기로 마음먹었다.

제네릭 이란?

제네릭은 기존의 데이터형식에 의존하지 않고, 하나의 값이 여러 다른 데이터 타입들을 가질 수 있도록 하는 방법이다. 우리들은 흔히 List,ArrayList등 리스트 및 배열을 생성한다면 흔히 다음과 같이 사용할 것이다.

// 객체<타입> = new 객체<타입>();
List<String> strList = new ArrayList<String>();  
ArrayList<String> strArrList = new ArrayList<String>();

하지만 기능이 똑같지만 입력받는 자료형이 다르다는 이유로 자료형만 다르지만 기능이 똑같은 메서드를 만든다면 너무 비효율적일 것이다. 그렇기에 이러한 문제를 해결하기위해 세상에 나온 것이 제네릭이다.

제네릭은 클래스 내부에서 지정하는 것이 아닌 외부에서 사용자에 의해 지정되는 것을 의미한다. 쉽게 말하면 어느 메서드는 반드시 String을 써야하지만 메서드가 제네릭이면 필요에 따라 지정할 수있기 때문에 코드의 재사용성이 높아지는 장점이 있다.


제네릭의 장점

  1. 제너릭을 사용하면 잘못된 타입이 들어올 수 있는 것을 컴파일 단계에서 방지할 수 있다.
  2. 클래스 외부에서 타입을 지정하기 때문에 별도로 타입을 체크할 필요가 없다. 즉 관리가 편리하다.
  3. 비슷한 기능을 지원한다면 코드의 재사용성이 높아진다.

제네릭 사용방법


위의 이미지는 JpaRepository의 내부인데 제네릭으로 받는 것을 볼 수 있다. 만약에 제네릭으로 받지 않았다면 JpaRepository는 존재하지도 않았을 것이라고 생각한다.

제네릭은 보통 표기를 이렇게 한다.

타입설명
<T>타입
<E>속성
<K>
<V>
<N>숫자

물론 단일 문자일 필요는 없다. 위의 JpaRepository에서도 T는 아마도 Entity타입을 의미하는 것이도 ID는 키값을 의미한다고 생각할 수 있다.

제네릭 클래스 및 인터페이스 1

interface genericInterface <T> {
    void printer1();
}

class genericClass <T> implements genericInterface <T>{ 
  
    @Override
    public void printer1(){
      System.out.println("genericClass_1!!!");
    }
    
    void printer2(){
      System.out.println("genericClass_2!!!");
    }
    
}

public class Main {
    public static void main(String[] args) {
      genericClass<String> a = new genericClass<String>();
      
      System.out.println(a);
      a.printer1(); // genericClass_1!!!
      a.printer2(); // genericClass_2!!!
  }
}

위의 예시처럼 작성하는 것이 가장 기본적인 방법이다. 하지만 위의 코드는 제너릭으로 선언한 부분을 빼고 main에서 genericClass옆의 <String>을 제외하고 실행해도 결과 값은 같기 때문에 사실상 의미가 없다. 그냥 이렇게도 쓸 수 있다는 것을 알고 싶었다.

제네릭 클래스 및 인터페이스 2

그리고 제너릭은 HashMap처럼 타입인자를 2개로 설정이 가능하다. 대표적으로 JpaRepository를 예로 들을 수 있겠다.

class genericClass <T, K> {  }
 
public class Main {
	public static void main(String[] args) {
		genericClass<String, Integer> a = new genericClass<String, Integer>();
	}
}

위의 방식대로 작성한다면 T는 String타입, K는 Integer타입이 된다. 여기서 신경쓰이는 점은 제너릭을 선언하고 타입을 지정하는데 참조 타입(Reference Type)이 온다는 건데 아마도 내생각에는 이러한 이유인거 같다.

배열, 열겨형, 클래스, 인터페이스는 JVM Heap영역에 저장되는데 Int, double, char형처럼 primitive type은 JVM Stack 영역에 저장된다. JVM 저장되는 구역이 다르기 때문에 저장이 안된다고 생각한다. 이를 뒷받침 하는 이유로 Integer, String, Double과 같은 wrapper클래스는 JVM Stack 영역에 저장된다.


제네릭 클래스 및 인터페이스 3

class genericClass <T, ID> { 
    
    private T type; // T 타입 
    private ID idnum; // ID 타입
    
    // setter
    void set(T type, ID idnum){
      this.type = type;
      this.idnum = idnum;
    }
    
    // getter
    T getType(){
      return type;
    }
    
    // getter
    ID getIdnum(){
      return idnum;
    }
    
}

public class Main {
    public static void main(String[] args) {
        genericClass<String, Integer> test = new genericClass<String, Integer>();
        
        test.set("100",200);
        
        // 값 확인
        System.out.println("T data = " + test.getType());
        System.out.println("ID data = " + test.getIdnum());
        
        // 자료형 확인
        System.out.println("T type = " + test.getType().getClass().getName()); // String
        System.out.println("ID type = " + test.getIdnum().getClass().getName()); // Integer
        
  }
}

이런식으로 타입을 지정하여 사용할 수 있다, 실제로 출력을 해보면 T의 타입은 String으로 나오고 ID의 타입은 Integer로 나오는 것을 확인 할 수 있다. 이런식으로 <>안에 타입을 파라미터로 보내 제네릭 타입을 지정해 주는 것이 제네릭 프로그래밍이라고 한다.

제네릭 메서드

지난글에서는 제네릭을 이용하여 클래스를 작성하는 것을 했는데 메서드에도 제네릭을 선언하여 사용할 수 있다.

// 작성법
[접근 제어자] <제네릭타입> [반환타입] [메소드명]([제네릭타입] [파라미터]) {
	// 텍스트
}

// 예시
// public 접근제어로 제네릭 타입은 T 리턴타입도 T타입이고 파라미터는 T타입의 type을 가져온다. 
public <T> T genericMethod(T type) {

}

그렇다면 이제 class와 함께 예시를 보여주겠다.

class genericClass <T, ID> { 
    
    <T> T genericMethod(T o) {	// 제네릭 메소드
		  return o;
	  }
    
}

public class Main {
    public static void main(String[] args) {
        genericClass<String, Integer> test = new genericClass<String, Integer>();
        
        // 제네릭 메소드 Integer
        System.out.println("<T> returnType : " + test.genericMethod(3).getClass().getName());
	    // 제네릭 메소드 String
		System.out.println("<T> returnType : " + test.genericMethod("ABCD").getClass().getName());
		// 제네릭 메소드 ClassName genericClass
		System.out.println("<T> returnType : " + test.genericMethod(test).getClass().getName());
        
  }
}

이렇게 사용할 수 있겠다. 여기서 알 수 있는점은 클래스에서 지정한 제네릭유형과는 별도로 메서드에서는 독립적으로 제네릭을 사용해도 문제가 없다는 점이다.

그렇다면 이렇게 사용하는 이유는 무엇인가? 바로 정적 메서드로 선언할 때 필요하기 때문이다. static이 붙은 변수, 함수는 프로그램 실행시 메모리에 올라가 있기 때문에 클래스 이름을 통해 바로 접근이 가능하다.

그렇다면 일반적으로 static은 자료형 하나로 리턴타입이 지정되어있는데 제네릭은 어떻게 하는가? 바로 이런식으로 처리한다.

class generic<T> {
    
    static <T> T genericMethod(T o) { // 제네릭 static 메서드
        return o;
    }
    
    static int normalMethod(int i) { // 일반 static 메서드
        return i;
    }
}

public class main {
    public static void main(String[] args) {

        System.out.println(generic.normalMethod(11)); 
        System.out.println(generic.genericMethod("11"));
        System.out.println(generic.genericMethod("11").getClass().getName());
    }
}

제네릭은 개발자가 지정한 타입이기 때문에 만약 제네릭타입을 없애고 리턴타입만 남긴다면 T라는 유형을 클래스로부터 얻어올 방법이 없기 때문에 에러가 난다.

그렇기에 제네릭을 사용하는 메서드를 정적메서드로 만들고 싶은 경우에는 제네릭 클래스와 별도로 제네릭을 사용해야한다.

결론

제네릭이 이렇게 보니 쉽게 생각하면 기존의 클래스를 선언하고 변수를 선언하는 과정에 정해진 자료형에서 개발자가 임의로 지정한 자료형으로 받겠다는의미로 해석하면 되겠다. 내가 항상사용하는 JpaRepository와 비교하면서 보니 이해가 더욱 쉽게 이해할 수 있었다. 하지만 아직 어려운부분도 있다, 그래도 비록 느리더라도 조금씩 한발씩 나아가다 보면 언젠가 이해할 수 있는 날이 올 것이라고 믿는다.

참고 사이트

https://st-lab.tistory.com/153

https://inpa.tistory.com/entry/JAVA-%E2%98%95-%EB%B3%80%EC%88%98%EC%9D%98-%EA%B8%B0%EB%B3%B8%ED%98%95-%EC%B0%B8%EC%A1%B0%ED%98%95-%ED%83%80%EC%9E%85

https://velog.io/@kimdy0915/기본형primitive-vs.-래퍼-클래스wrapper-class

(항상 감사합니다.)

profile
가끔은 정신줄 놓고 멍 때리는 것도 필요하다.

0개의 댓글