JOOQ가 제네릭을 활용하여 타입 안전성을 확보한 방법 #4
sangjun121
started this conversation in
인사이트
Replies: 1 comment
-
|
해당 글을 이해하기 위해 다음 순서로 생각해보면 좋습니다!
|
Beta Was this translation helpful? Give feedback.
0 replies
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
제네릭을 이용하여 해결한 문제(with Jooq)
도입
현재, 오픈소스 프로젝트인 JOOQ의 구현체를 학습하며, JAVA가 내부적으로 어떻게 활용되고 있는지 학습하고 있다. 이번 포스트는 JOOQ가 제네릭을 어떻게 활용하고 있는지 살펴본다.
0. 제네릭의 등장 배경
제네릭이 왜 등장하게 되었는지는, 제네릭의 가장 중요한 목적이 컴파일 타임 체크임을 이해하면 온전히 이해할 수 있을 것이다. 차근차근 살펴보자.
제네릭은 정적 타입 언어인 JAVA의 타입 안정성과 코드 재사용성 문제를 해결하기 위해 등장한 개념이다. 타입 안정성이 없는 경우, 서로 다른 타입 간의 연산, 비교, 대입시에 컴파일 시점에 오류를 잡아줄 수 없었다. 이와 마찬가지로 정적 타입 언어이기 때문에 타입 변환시에 강제 형변환(케스팅)이 수반되야 한다. 또한 컬렉션을 사용하는 경우 특정 타입 지정을 누락하게 되기 때문에, 다른 협업자에게 의도가 드러나지 않는 문제가 발생한다.
문제 상황의 코드를 살펴보자.
이에 따라 제네릭을 도입하여, 기존 Object 기반 컬렉션에서 발생하던 타입 안정성 문제와 캐스팅 오류를 해결하였다. 위의 코드를 제네릭을 도입한 현재 시점에서 올바르게 수정해보자.
또한, 코드의 재사용성을 높일 수 있다. Box라는 클래스의 값 타입을 제네릭의 전후로 표현해보자.
제네릭을 도입함으로서, Box의 값 프로퍼티의 타입이 지정되며, 이에 따라 Box는 T타입의 전용 박스로 해석되며, 다른 타입 전용 박스들과 비교, 대입, 연산로직을 수행할 경우 컴파일 단계에서 제지될 수 있다.
1. 타입 소거의 등장 배경
해당 제네릭은 java5에서 도입된 개념이다. 즉 java5이전에는 제네릭이 존재하지 않았다. 이에 따라 기존 코드인 List와 ArrayList와 같은 컬렉션 객체에 영향을 끼치면 안되었다. 즉, 하위 호환성이 필요했다. 이에 따라 “컴파일 때만 타입을 체크하고, 런타임에는 지우는 방향”으로 타입 소거라는 개념이 등장하게 되었다.
타입 소거는 제네릭 코드가 컴파일 될 때, 지정한 제네릭 타입이 사라지는 것을 의미한다. 제네릭 컴파일 과정을 살펴보자.
2. 제네릭 타입 소거 과정
컴파일러가 클래스를 바이트코드로 변환할 때, 제네릭 타입 T를 Object로 치환한다. 위의 박스 코드를 예시로 다시 살펴보자.
그렇다면 우리는 어떻게 외부에서 Box.get을 호출한 경우에 String을 반환 받을 수 있을까? 제네릭 타입을 제거한 후 타입이 일치하지 않는 곳은 컴파일러가 자동으로 캐스팅(형 변환)을 넣어준다.
위의 예시는 제네릭 클래스의 타입소거를 살펴 보았지만, 제네릭 메서드의 타입소거도 동일하게 Object로 대치되며, 필요한 경우 타입 캐스팅이 진행된다.
사실상 타입 제거로 런타임에서는 해당 타입이 무엇인지가 Object로 대체된다. 그렇다면 사용하는 이유가 무엇인가? 앞서 말했듯이 컴파일 타임의 타입 체크용의 용도라고 이해하면 편하다. 그렇기에 런타임에 제거되는 것이다. (그렇다고 런타임이 제네릭 타입과 아에 무관하지는 않다. 제한적으로 일부 타입 정보는 런타임 도중에도 제거되지 않고 살아있고, 리플렉션에서도 메타데이터로서는 일부 확인 가능하다. 해당 내용은 추후에 다룰 예정이다. 해당 포스트에서는 제네릭이 컴파일 시점의 타입 안정성 보장 용도임만 이해하면 충분하다.)
1. JOOQ에서 제네릭이 필요한 이유
데이터베이스의 타입을 JAVA 어플리케이션의 타입으로 변환하는 과정에서, 타입 안정성을 위해 JOOQ는 제네릭을 적극 활용한다.
1. 여기서 타입 안정성이란 무엇일까?
타입 안정성이란, 잘못된 타입 사용을 컴파일 단계에서 막는 것을 의미한다. 즉, 올바르지 않은 타입이 런타임 도중 예상치 못한 문제를 발생할 위험성을 줄여주기 위해, 컴파일 단계에서 미리 차단하는 것이다.
먼저 JAVA는 정적 타입 언어이기 때문에 각 변수의 타입을 명확히 정의하고, 서로 다른 타입간의 연산이나 대입이 컴파일 단계에서 오류로 사전 방지가 가능하다. 하지만, 컬렉션에서 발생하는 캐스팅 오류는 어떠할까? 아래 예시 코드를 살펴보자.
즉, DB의 각 컬럼,테이블, 레코드의 타입을 JAVA에서 Object로 열어두지 않고, 제네릭을 활용하여 타입 안정성을 보장하는 것이다. 컬럼은 Field, 테이블은 Table, 레코드는 TableRecord처럼 표현되고, 이 덕분에 잘못된 타입 사용을 런타임이 아니라 컴파일 타임에 잡을 수 있다.
2. Field로 살펴보는 타입 변환
jOOQ에서 Field는 컬럼을 단순 이름이 아니라 값 타입까지 아는 SQL 표현식으로 모델링한 핵심 인터페이스 이다.
Field의 T는 컬럼 표현식이 만들어내는 값의 자바 타입이다. 즉, 컬럼을 그냥 이름이 있는 객체로만 다루지 않고, JAVA 내부의 타입도 아는 객체로 다룬다.
즉, Field의 체이닝의 기반으로 sql문을 JAVA의 객체에 맞게 표현할 수 있는데, 그렇기 때문에 JOOQ는 SQL을 문자열이 아닌 타입이 있는 자바 코드로 다룰 수 있는 것이다.
또한, Field는 “DB 컬럼”보다 더 넓은 개념입니다. 정확히는 “SQL에서 하나의 값으로 평가되는 표현식”을 의미한다.(공식 문서에서는 column expression) 예를 들어, 상수값이나 서브쿼리 결과 자체는 컬럼은 아니지만, JOOQ에서 이를 표현할 때도 Field를 사용할 수 있다.
3. Record 내부의 get(Field)이 제공하는 타입 안정성
jOOQ는 Record의 T get(Field)를 통해 필드가 가진 타입 정보를 반환값 타입에 그대로 연결함으로써, 레코드 접근을 캐스팅 없이 타입 안전하게 만든다. 심지어 Record는 제네릭이 아닌데 이것이 어떻게 가능할까? 해당 구현을 살펴보면, 제네릭과 유사한 점이 있음을 알 수 있다.
즉, Record는 제네릭과 유사하게 내부의 타입을 개발자가 직접 캐스팅하지 않도록, 해당 메서드로 고정된 타입을 강제한다는 뜻이다. 그렇다면 왜 Record를 제네릭으로 구현하지 않은 것일까? Record로 하면 되는 것 아닌가? Record에 하나의 필드만 담기지 않기 때문이다. DB 레코드는 보통 여러 타입의 컬럼을 동시에 담기 때문에 이렇게 제네릭으로 타입을 지정하는 것은 불가능하다. 그래서 메서드 레벨 제네릭을 사용한다.
get 파라미터가 Field이라면, 해당 레코드에 속한 필드 타입 역시 String이기 때문에, 이를 반환하도록, 메서드 시그니처 단계에서 강제한 것이다.
해당 메서드 시그니처로 강제하지 않았다면, 개발자들은 get으로 반환받은 Object를 T 타입에 맞게 캐스팅해줘야 할 것이다. 이는 휴먼에러를 초래할 수 있고, 앞서 언급한대로 컴파일 단계에서 에러를 발견하기 힘들다. 이에 따라 메서드 시그니처로 반환 타입을 강제한 것이다.
그렇다면 JOOQ는 이를 내부적으로 어떻게 구현하였을까? 해당 코드는 Record의 구현체인 AbstractRecord에 작성된 get메서드이다.
먼저 Record(DB 행으로 이해하자)는 내부 필드로 각 컬럼에 해당하는 값들을, Object[] 배열에 인덱스 기반으로 저장한다. 그리고 이를 인자로 받은 T로 캐스팅하여 반환한다. 제네릭이 바이트 코드로 컴파일 되는 과정과 유사하지 않은가?
즉, 내부는 Object타입의 배열을 이용하여 범용적으로 저장하는 구조이지만, 외부에 공개하는 API(여기서는 get 메서드)는 타입 안정적인 것으로 해석될 수 있다. 이는 파라미터로 주입받은 field의 T 덕분에 구현가능하다. 제네릭이 컴파일 시점에 T의 타입을 읽어 캐스팅 처리를 한다면, 해당 메서드는 메서드 인자의 Field의 T타입을 읽어 캐스팅 처리를 하는 방식이다.
JOOQ는 Record.get(Field)를 통해 필드가 가진 타입 정보를 값 반환 타입에 그대로 연결함으로써, 레코드 접근을 캐스팅 없이 타입 안전하게 만드는 방식을 채택하고 있다.
Beta Was this translation helpful? Give feedback.
All reactions