영주의 개발노트

🏦 기술 부채 정산 | 제네릭 톺아보기(1): 유연한 코딩을 위한 한 걸음 본문

STUDY 📖/JAVA

🏦 기술 부채 정산 | 제네릭 톺아보기(1): 유연한 코딩을 위한 한 걸음

0JUUU 2024. 1. 4. 22:06
반응형

 김영한님의 '스프링 핵심 원리 - 고급편' 강의 예제 코드에서 제네릭이 등장해 순간 멈칫하여 해당 글을 작성하고자 한다. 필자는 자바를 약 4년간 사용하였음에도 제네릭에 대해 잘 모른다. 지금 이 글을 읽고 있는 당신! 이런 사람도 있으니 민망해하지마시길... 해당 글은 시리즈로 작성할 것이며, 이번 글에서는 제네릭에 대한 전반적인 기본 지식을 다루고자 한다. 

예상 독자

  • 기본적인 자바는 알지만, 자바 제네릭에 대한 지식이 전무한 사람
  • 제네릭을 어디선가 봤지만, 명확하게 알지 못하는 사람

해당 글을 통해 얻어갈 수 있는 내용

  • 제네릭에 대한 기본 지식
  • 제네릭 등장 배경
  • 제네릭 이점
  • 제네릭 간단한 소개

경각심을 줬던 코드는 아래와 같다. 

public abstract class AbstractTemplate<T> {

    private final LogTrace trace;

    public AbstractTemplate(LogTrace trace) {
        this.trace = trace;
    }

    public T execute(String message) {
        TraceStatus status = null;
        try {
            status = trace.begin(message);

            // 로직 호출
            T result = call();

            trace.end(status);
            return result;
        } catch (Exception e) {
            trace.exception(status, e);
            throw e;
        }
    }

    protected abstract T call();
}

 

public class TraceTemplate {

    private final LogTrace trace;

    public TraceTemplate(LogTrace trace) {
        this.trace = trace;
    }

    public <T> T execute(String message, TraceCallback<T> callback) {
        TraceStatus status = null;
        try {
            status = trace.begin(message);

            // 로직 호출
            T result = callback.call();

            trace.end(status);
            return result;
        } catch (Exception e) {
            trace.exception(status, e);
            throw e;
        }
    }
}

여기서 들었던 의문점 2가지가 있다. 

  1. 클래스 선언 시 <T> 의 유무는 무엇에 의해 결정되는 거지? <T>는 뭐지?
  2. public <T> T 라고 명시한 이유가 뭘까?

 위 의문점을 해결하기 전 우리는 제네릭(generics)에 대해 알아야 한다.

 

제네릭이란

 제네릭의 사전 정의를 살펴보면 "일반적인"이라는 의미를 갖고 있다. 제네릭은 자바 5부터 등장한 개념으로, 여러 타입을 처리할 수 있는 코드를 만드는 기술이다. 클래스, 인터페이스, 메서드를 정의할 때 타입을 파라미터로 사용할 수 있도록 하는 기능이다. 라이브러리 개발 시 많이 쓰인다. 

 여기서 타입을 파라미터로 사용할 수 있다는 말이 무슨 뜻일까? 잠시 제네릭이 적용된 코드를 살펴보겠다. List 인터페이스 선언 관련된 코드이다. 

 

우리는 이 List를 사용할 때, 아래와 같이 사용한다. 이런 식으로 List 인터페이스의 인스턴스를 생성할 때, Integer, String 등과 같은 타입을 넘겨주는 것을 '타입을 파라미터로 사용한다~'라고 할 수 있다. 

세상은 제네릭 등장 이전과 이후로 나뉜다 👼

 제네릭이 등장하기 전 상황, 한계, 등장 후의 상황을 비교해보며 제네릭 이점에 대해 이야기하겠다. 우리는 아무 타입이나 저장할 수 있는 data를 가진 Box를 개발하는 사람이라고 가정하자. 클라이언트의 요구에 따라 String이라면 String을 data에 저장할 Box, Integer 라면 Integer를 data에 저장할 Box를 제공해야 한다.

 물론 String만 저장할 수 있는 StringBox, Integer만 저장할 수 있는 IntegerBox를 만들어 제공하면 될 것이다. 하지만, 우리는 요구사항이 늘어날수록 계속해서 반복해야 하는 작업을 참을 수 없는 개발자이다. 만약 클라이언트가 100개의 다른 타입을 요구한다면, 100개의 각기 다른 클래스를 생성해야 할 것이다. 더 좋은 해결 방법이 없을까? 🤔

 사실 제네릭이 등장하기 전에도 모든 종류의 타입을 받을 수 있는 클래스를 작성할 수 있었다. 우리에겐 모든 타입의 최상위 부모 Object가 있다. 다형성을 활용해 객체를 Object 타입으로 받아 처리하면 된다. 

public class Box {
    private Object data;
    
    public void setData(Object data) {
    	this.data = data;
    }
    
    public Object getData() {
    	return data;
    }
}

 

 이렇게 되면, Box에는 String도 Integer도 모두 저장할 수 있다. 

Box b = new Box();

b.setData("before generics");
String sData = (String) b.getData();

b.setData(new Integer(0));
Integer iData = (Integer) b.getData();

 

 만능인 것처럼 보이는 Object에는 단점이 존재한다. data를 꺼내올 때마다 형변환을 해야한다는 것이다. 더 큰 문제는 만약 클라이언트가 String을 넣었던 것을 깜빡하고 Integer로 형변환하여 data를 접근한다면 런타임 오류가 발생한다.

Box box = new Box();
box.setData("STRING!!!!!");
Intger i = (Integer) box.getData();	// 런타임 오류 발생

 

 개발자에게 가장 좋지 않은 오류는 런타임 오류이다. 해당 오류는 Box 개발자인 내가 먼저 접하기보다는 나에게 돈을 주고 Box를 사간 클라이언트가 먼저 마주하게 된다. 매우 좋지 않은 상황이다. 

 

😎 제네릭 등장

 앞서 살펴본 문제들을 제네릭을 사용하면 말끔하게 해결할 수 있다. 제네릭을 사용한 클래스(일명, 제네릭 클래스)에서는 타입을 변수로 표시한다. 타입을 변수로 한다는 뜻에서 타입 매개변수(type parameter)라고 한다. 타입 매개변수는 객체 생성 시에 코드 작성자에 의해 결정된다. 제네릭을 사용해 클라이언트에게 제공해 보자. 

public class Box<T> {
    private T data;
    
    public void setData(T data) {
    	this.data = data;
    }
	
    public T getData() {
    	return data;
    }
}

 

일반적으로 타입을 변수로 표시할 때, T 처럼 대문자로 표시한다. 이 Box를 제공받은 클라이언트는 다음과 같이 사용할 수 있다.

Box<String> box = new Box<>();
b.setData("STRING");
String sData = b.getData();

 이전과 달리 형변환을 명시적으로 작성할 필요가 없어졌다! 추가로 위 box에 Integer 값을 넣으려고 시도한다거나, box의 data를 접근할 때 Integer로 한다면 컴파일 오류가 발생하게 된다. 컴파일 단계에서 오류를 확인할 수 있으므로 안전하게 프로그래밍할 수 있게 되었다.

Box<String> box = new Box<>();
box.setData(new Integer(0));	// 컴파일 오류
Integer iData = box.getData();	// 컴파일 오류

 

 정리해보면, 제네릭 등장 이전에는 런타임 시점이 되어서야 확인할 수 있는 형변환 관련 오류들이 많이 발생했었다. 이를 보완하기 위해 제네릭이 등장하였다. 이를 통해 명시적으로 형변환을 작성하던 귀찮음을 없앨 수 있었으며 컴파일 시점에 형변환 관련 오류를 확인할 수 있게 되었다. 자바의 제네릭 기본 지식, 클래스, 인터페이스 단에서 사용하는 제네릭에 대해서는 이 정도면 된 것 같다. 

 

글을 마무리하며

당신... 낚이셨어요...

 그래서 위에 서술했던 2가지의 의문점에 대한 답이 뭔데? 라고 궁금할 것이다. 해당 내용은 다음 편에서 다뤄보도록 하겠다. 😎

 제네릭에 대해 공부하며 얻었던 지식들을 정리해보았다. 자바 5에서 처음 등장한 기술이라는 것에 크게 놀랐다. 현재 글을 쓰고 있는 시점에는 자바 2N 까지 나왔기 때문이다. 내가 미뤄온 기술부채가 어마어마하다는 사실에 민망했다. 해당 글을 읽는 독자들도 이 글을 통해 제네릭에 대해 알아가고 으레 써왔던 기술을 다시 한번 체득해 볼 수 있는 기회가 되었으면 한다. 

 

참고