<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="kr.co.logosai.assistant.api.admin.document.repository.mapper.DocumentMapper">
...
<select id="findReferenceDocumentByConversationUid" resultType="kr.co.logosai.assistant.api.admin.document.repository.dto.DocumentDto$RagReferenceDto">
SELECT
lrs.embedding_score,
rd.content,
FROM
LOG_RAG_SEARCH_RESULTS lrs
JOIN
RAG_DOCUMENT rd ON lrs.document_uid = rd.uid
WHERE
lrs.bot_uid = #{botUid}
AND lrs.parent_chat_uid = #{conversationUid}
AND lrs.document_uid != 0
ORDER BY
lrs.embedding_score DESC
</select>
</mapper>
import lombok.Builder;
import lombok.Getter;
import java.time.LocalDateTime;
public class DocumentDto {
...
@Getter
@Builder
public static class RagReferenceDto {
private String content;
private Double embeddingScore;
}
}
1번째로 가져오는 값이 embedding_score이기 때문에 → DTO에서 content 필드에 매핑되어 버린다.
16:14:12.235 ERROR k.c.l.a.a.c.r.ApiExceptionAdvice - Unhandled runtime exception: null
Caused by: org.apache.ibatis.executor.result.ResultMapException: Error attempting to get column 'content' from result set. Cause: java.lang.NumberFormatException: For input string: "
String 타입인 content에 Number 타입을 매핑하려 하기 때문에 에러가 발생한다.
해결 방법은 간단하다. DTO 클래스에 붙어 있는
@Builder
어노테이션을 제거함으로써 해결할 수 있다.
import lombok.Getter;
import java.time.LocalDateTime;
public class DocumentDto {
...
@Getter
public static class RagReferenceDto {
private String content;
private Double embeddingScore;
}
}
{
"timestamp": "2025-08-27T16:18:27.335429",
"code": "SUCCESS",
"message": "정상 처리되었습니다.",
"data": [
{
"content": " ... "
"similarityScore": 67.34
}
]
}
그렇다면 왜
@Builder
를 사용했을 때는 되지 않았던 것일까?
- @Builder를 사용하면, 빌더에서 사용하기 위한 생성자를 만들어 낸다.
- 이는 모든 필드를 인자로 받는
private
생성자이다.- Java에서는, 클래스에 생성자가 1개도 없는 경우에는 인자가 없는 ‘기본 생성자’를 만들어 준다.
- 이 때문에 생성자를 따로 만들지 않아도, MyBatis에서
@Setter
를 호출해 DB에서 가져온 값을 DTO에 넣어줄 수가 있다.- 하지만 개발자가 생성자를 1개라도 만드는 순간, 기본 생성자는 만들어지지 않는다.
- 즉,
@Builder
로 인해서 기본 생성자는 만들어 지지 않았고,private
모든 필드 생성자만 만들어 지기에 순서를 맞추어 넣어줘야 했던 것이다.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="kr.co.logosai.assistant.api.admin.document.repository.mapper.DocumentMapper">
...
<select id="findReferenceDocumentByConversationUid" resultType="kr.co.logosai.assistant.api.admin.document.repository.dto.DocumentDto$RagReferenceDto">
SELECT
lrs.embedding_score
FROM
LOG_RAG_SEARCH_RESULTS lrs
JOIN
RAG_DOCUMENT rd ON lrs.document_uid = rd.uid
WHERE
lrs.bot_uid = #{botUid}
AND lrs.parent_chat_uid = #{conversationUid}
AND lrs.document_uid != 0
ORDER BY
lrs.embedding_score DESC
</select>
</mapper>
import lombok.Builder;
import lombok.Getter;
import java.time.LocalDateTime;
public class DocumentDto {
...
@Getter
@Builder
public static class RagReferenceDto {
private String content;
private Double embeddingScore;
}
}
16:30:38.645 ERROR k.c.l.a.a.c.r.ApiExceptionAdvice - Unhandled runtime exception: null
Caused by: java.lang.IndexOutOfBoundsException: Index 1 out of bounds for length 1
at java.base/jdk.internal.util.Preconditions.outOfBounds(Preconditions.java:64)
at java.base/jdk.internal.util.Preconditions.outOfBoundsCheckIndex(Preconditions.java:70)
at java.base/jdk.internal.util.Preconditions.checkIndex(Preconditions.java:266)
at java.base/java.util.Objects.checkIndex(Objects.java:361)
at java.base/java.util.ArrayList.get(ArrayList.java:427)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.applyColumnOrderBasedConstructorAutomapping(DefaultResultSetHandler.java:787)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.applyConstructorAutomapping(DefaultResultSetHandler.java:776)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createByConstructorSignature(DefaultResultSetHandler.java:727)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createResultObject(DefaultResultSetHandler.java:689)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.createResultObject(DefaultResultSetHandler.java:659)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.getRowValue(DefaultResultSetHandler.java:411)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValuesForSimpleResultMap(DefaultResultSetHandler.java:366)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleRowValues(DefaultResultSetHandler.java:337)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSet(DefaultResultSetHandler.java:310)
at org.apache.ibatis.executor.resultset.DefaultResultSetHandler.handleResultSets(DefaultResultSetHandler.java:202)
at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:66)
at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:80)
at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:65)
at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:336)
at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:158)
at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:110)
at com.github.pagehelper.PageInterceptor.intercept(PageInterceptor.java:169)
at org.apache.ibatis.plugin.Plugin.invoke(Plugin.java:59)
at jdk.proxy2/jdk.proxy2.$Proxy158.query(Unknown Source)
at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:154)
... 174 common frames omitted
16:30:38.656 WARN o.s.w.s.m.m.a.ExceptionHandlerExceptionResolver - Resolved [org.mybatis.spring.MyBatisSystemException]
Index 1 out of bounds for length 1
에러가 발생한다.import lombok.Builder;
import lombok.Getter;
import java.time.LocalDateTime;
public class DocumentDto {
...
@Getter
// @Builder
public static class RagReferenceDto {
private String content;
private Double embeddingScore;
}
}
{
"timestamp": "2025-08-27T17:16:24.127897",
"code": "SUCCESS",
"message": "정상 처리되었습니다.",
"data": [
{
"content": null,
"similarityScore": 63.42
}
]
}
MyBatis에서 기본 생성자에 setter를 활용해 값을 할당한다고 설명하였다.
하지만 한 가지 이상한 점은, 현재@Setter
가 없는데도 불구하고 기능이 잘 돌아간다는 것이다.
- 이는 리플렉션이라는 기능 때문이다.
Reflector
클래스가 resultType 의 클래스에 대한 필드정보와 getter/setter의 매핑 정보를 만들어 두고 이를 통해서 바인딩 시 해당 필드를 찾게 된다.- 자세한 내용은 아래 블로그에 잘 정리되어 있다.
Spring + MyBatis에서 쿼리의 결과와 객체가 매핑이 되는 과정
import lombok.Getter;
import java.time.LocalDateTime;
public class DocumentDto {
...
@Getter
public static class RagReferenceDto {
private String content;
private Double embeddingScore;
}
}
@Getter
로 충분하다고 판단했다.@Setter
의 경우에는 리플렉션 기능 덕분에 생략이 가능하다. @Getter
@NoArgsConstructor
public static class RagReferenceDto {
private String content;
private Double embeddingScore;
@Builder
public RagReferenceDto(String content, Double embeddingScore) {
this.content = content;
this.embeddingScore = embeddingScore;
}
}
@NoArgsConstructor
를 붙여 주어야, 기본 생성자가 생겨 순서에 상관 없이 MyBatis가 매핑해 줄 수 있다. @Getter
@Builder
@NoArgsConstructor
public static class RagReferenceDto {
private String content;
private Double embeddingScore;
}
@Builder
와 @NoArgsConstructor
이 충돌하여 컴파일 에러가 발생한다. @Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class RagReferenceDto {
private String content;
private Double embeddingScore;
}
@AllArgsConstructor
를 추가로 붙여서 해결할 수 있으나, 불필요한 어노테이션이 늘어나기 때문에 필요한 생성자를 임의로 만들고 @Builder
를 붙이는 방식이 더 좋다.🧑🏻💻 생각을 해 보니, 함께 일하는 개발자가