PostgreSQL alias는 소문자로 나오는데 DTO 매핑은 잘 될까?
TL;DR
PostgreSQL은 따옴표 없는 alias를 전부 소문자로 변환하지만,
JDBC 드라이버의 findColumn()은 대소문자를 구분하지 않고 컬럼을 탐색하며,
SimplePropertyRowMapper는 DTO 필드명 그대로→snake_case 순으로 컬럼을 찾는다.
덕분에 AS blogId로 alias를 줘도 DTO의 blogId 필드로 정확히 매핑된다.
예를 들어 다음과 같이 Spring의JdbcClient 를 사용해 PostgreSQL 쿼리 결과를 Kotlin data class로 매핑한다고 해봅시다
val sql = """
SELECT t.parent_id AS blogId,
t.id AS tagId,
t.name AS tagName
FROM tags t
""".trimIndent()
jdbcClient.sql(sql)
.query(TagByBlogDto::class.java)
.list()
그리고 매핑 대상은 아래처럼 단순한 data class입니다
data class TagByBlogDto(
val blogId: Long,
val tagId: Long,
val tagName: String,
)
PostgreSQL은 AS blogId를 내부적으로 소문자(blogid) 로 변환하지만,
JdbcClient는 이 컬럼을 TagByBlogDto.blogId에 정확히 매핑합니다.
어떻게 이런 일이 가능한 걸까요?
PostgreSQL alias 규칙
PostgreSQL 공식 문서에 따르면 👇
“Quoting an identifier also makes it case-sensitive, whereas unquoted names are always folded to lower case.
For example, the identifiers FOO, foo, and "foo" are considered the same by PostgreSQL,
but "Foo" and "FOO" are different from these three and each other.”
즉,
AS blogId→ 내부적으로blogidAS "blogId"→ 대소문자 그대로 유지
작성 형태 실제 인식되는 이름 AS blogId blogid AS “blogId” blogId AS BLOGID blogid
JdbcClient는 어떤 매퍼를 사용할까?
JdbcClient는 .query(Class<T>) 호출 시 내부적으로
SimplePropertyRowMapper 를 사용합니다 👇
public <T> MappedQuerySpec<T> query(Class<T> mappedClass) {
RowMapper<?> rowMapper = rowMapperCache.computeIfAbsent(mappedClass, key ->
BeanUtils.isSimpleProperty(mappedClass)
? new SingleColumnRowMapper<>(mappedClass)
: new SimplePropertyRowMapper<>(mappedClass)
);
return query((RowMapper<T>) rowMapper);
}
즉, TagByBlogDto처럼 DTO를 넘기면
new SimplePropertyRowMapper<>(TagByBlogDto.class)가 생성되어 필드 단위로 매핑됩니다.
SimplePropertyRowMapper의 동작 원리
SimplePropertyRowMapper는 생성자 파라미터명을 기준으로 ResultSet에서 컬럼을 찾습니다.
핵심 부분은 아래와 같습니다 👇
for (int i = 0; i < args.length; i++) {
String name = this.constructorParameterNames[i];
int index;
try {
// ① 필드명 그대로 컬럼 탐색 (예: blogId)
index = rs.findColumn(name);
}
catch (SQLException ex) {
// ② 실패 시 snake_case 변환 (예: blog_id)
index = rs.findColumn(JdbcUtils.convertPropertyNameToUnderscoreName(name));
}
Object value = JdbcUtils.getResultSetValue(rs, index, td.getType());
args[i] = this.conversionService.convert(value, td);
}
즉, 매퍼는 다음 순서로 필드 매칭을 시도합니다:
findColumn("blogId")실패 시
findColumn("blog_id")
PostgreSQL JDBC 드라이버(PgResultSet)의 findColumn 로직
이제 핵심입니다.
PostgreSQL JDBC 드라이버는 ResultSet.findColumn() 호출 시 대소문자를 모두 무시하고 컬럼을 찾습니다.
아래는 실제 PgResultSet 코드 일부입니다
private int findColumnIndex(String columnName) {
Integer index = columnNameIndexMap.get(columnName);
if (index != null) return index;
// 1️⃣ 소문자 비교
index = columnNameIndexMap.get(columnName.toLowerCase(Locale.US));
if (index != null) {
columnNameIndexMap.put(columnName, index);
return index;
}
// 2️⃣ 대문자 비교
index = columnNameIndexMap.get(columnName.toUpperCase(Locale.US));
if (index != null) {
columnNameIndexMap.put(columnName, index);
return index;
}
return 0;
}
즉, rs.findColumn("blogId")를 호출하면 드라이버가 내부적으로
blogId, blogid, BLOGID를 모두 비교하기 때문에 일치하게 됩니다.
댓글을 작성해보세요.