본문 바로가기
삽질일기/트러블 슈팅

Mybatis 환경에서 lombok 어노테이션 남용이 부른 참사

by 권성호 2021. 10. 18.

1. 서론

회사에서 Mybatis를 사용해 게시글에 대한 페이징 처리를 구현하던 중, Mybatis mapper에서 디비로 요청한 쿼리의 결과가 Entity 객체에 제대로 메핑 되지 않는 문제를 겪었다.  

 

2. 본론

 

문제를 설명하기 앞서 우선 내가 만든 Entity 객체와 mybatis mapper가 어떻게 구현되어 있었는지 보면 아래와 같다.

@Data
@Builder
public class FileInfoEntity {
	private String deviceCode;
	private String userId;
	private String deptCode;
	private String reqUserId;

	private String fileName;
	private String filePath;
	private Long fileSize;

	private String fileUuid;
	private FileStatus fileStatus;
	private ReqMode reqMode;
	private Long reqTime;
	private Long updateTime;
	private String serverFilePath;
	
}

<FileInfoEntity(java 클래스)>

 

<!-- 이하 생략 -->

	<resultMap id="SearchResult" type="FileInfoEntity">
	    <result property="deviceCode" column="device_code" />
	    <result property="userId" column="user_id" />
	    <result property="deptCode" column="dept_code" />
	    <result property="reqUserId" column="req_user_id" />
	    <result property="fileName" column="file_name" />
	    <result property="filePath" column="file_path" />
	    <result property="fileSize" column="file_size" />
	    <result property="fileUuid" column="file_uuid" />
	    <result property="fileStatus" column="file_status" />
	    <result property="reqMode" column="req_mode" />
	    <result property="reqTime" column="req_time" />
	    <result property="updateTime" column="update_time" />
	    <result property="serverFilePath" column="server_file_path" />
	</resultMap>
    
	<select id="getFileInfoList" parameterType="map" resultMap="SearchResult">
		SELECT 
			PAGE.RNUM, PAGE.* 
		FROM 
			( 
			SELECT 
			ROW_NUMBER() OVER(ORDER BY req_time DESC) AS RNUM, 
			f.device_code, 
			f.user_id, 
			f.dept_code, 
			f.req_user_id, 
			f.file_name, 
			f.file_path, 
			f.file_size, 
			f.file_uuid, 
			f.file_status, 
			f.req_mode, 
			f.req_time,
			f.update_time,
			f.server_file_path
			FROM 
			fileinfo f WITH (NOLOCK) 
			WHERE 
			req_time BETWEEN #{startTime} AND #{endTime} 
			) PAGE 
		WHERE PAGE.RNUM BETWEEN #{offset} AND #{limits} 
	</select>
    
    
    <!-- 이하 생략 -->

<mybatis mapper 일부>

 

mybatis mapper는 fileinfo에서 요청 시간이 [startTime ~ endTime]인 레코드를 요청 시간으로 정렬 후 상위 offset ~ limits 까지를 가져오는 페이징 쿼리로 구현되어 있다. 

 

쿼리의 결과는 id가  SearchResult인 Result map으로 인해 FileInfoEntity와 메핑되도록 해 두었다.

 

이제 모든것이 완벽하게 동작할 것 같았지만 오류가 발생했다.

 

file_name은 nvarchar(문자열) 데이터인데 bigint(정수형)으로 메핑 하려 해서 오류가 났다...?

 

페이징 쿼리의 결과가 단순히 FileInfoEntity의 필드가 아니라 RowNum값도 추가되기 때문에 resultMap을 사용해 쿼리의 결과를 메핑 했는데 왜 메핑이 안 되는 거지?

라는 의문이 들었고 오랜 시간 삽질을 시작했다.

 

쿼리를 실제로 실행하는 dao 함수 부분에서 디버깅을 찍어보니 쿼리 결과를 메핑 하는 과정에서 생성자를 기반으로 메핑이 이루어 짐을 발견했다.

resultMap의 경우 생성자 기반이 아니라 key-value쌍인 map을 기반으로 메핑이 이루어지는데 이는 분명 뭔가 이상했다.

그렇게 고민을 반복하던 중 시니어 분께까지 질문을 드리게 되었고 드디어 해답을 얻을 수 있었다!

 

"FileInfoEntity에서 @Builder만 사용하면 모든 메게변수를 받는 생성자 하나만 생성돼요."

이것이 해답이었다.

 

resultMap을 사용하면 쿼리의 결과와 java 객체를 메핑 하기 위해 Mybatis는 아래와 같은 과정을 거친다.

  1. java 객체를 디폴트 생성자를 기반으로 생성한다.
  2. resultMap을 기반으로 쿼리 결과 값을  생성된 java 객체의 필드 값에 적절하게 대입해 준다.

그런데 내 코드의 경우 Entity 클래스의 생성자는 모든 메게변수를 받는 것 하나뿐이어서 어쩔 수 없이 생성자 기반 mapping을 mybaits에서 수행하려 했고 그래서 위와 같은 오류가 발생했던 것이다.

 

실제로 FileInfoEntity 클래스를 아래와 같이 바꾼 후에는 메핑이 잘 되었다.

@Data
@Builder
public class FileInfoEntity {
	private String deviceCode;
	private String userId;
	private String deptCode;
	private String reqUserId;

	private String fileName;
	private String filePath;
	private Long fileSize;

	private String fileUuid;
	private FileStatus fileStatus;
	private ReqMode reqMode;
	private Long reqTime;
	private Long updateTime;
	private String serverFilePath;
	
	public FileInfoEntity() {}
	
	public FileInfoEntity(String deviceCode, String userId, String deptCode, String reqUserId, 
			String fileName, String filePath, Long fileSize,
			String fileUuid, FileStatus fileStatus, ReqMode reqMode, Long reqTime, Long updateTime, String serverFilePath) {
		
		this.deviceCode = deviceCode;
		this.userId = userId;
		this.deptCode = deptCode;
		this.reqUserId = reqUserId;
		this.fileName = fileName;
		this.filePath = filePath;
		this.fileSize = fileSize;
		this.fileUuid = fileUuid;
		this.fileStatus = fileStatus;
		this.reqMode = reqMode;
		this.reqTime = reqTime;
		this.updateTime = updateTime;
		this.serverFilePath = serverFilePath;
	}
}

 

3. 결론

 

  • mybatis의 resultMap에서 쿼리 결과와 java 객체를 메핑 할 때는 디폴트 생성자를 우선 생성한다.
  • mybatis 뿐 아니라 특정 데이터와 java 객체를 메핑할 때, 디폴트 생성자가 필요한 경우가 꽤 많으니 이점도 주의하자
    • 예를 들면, Spring Controller단에서 클라이언트의 json body요청을 바로 java 객체로 메핑 할 때
  • lombok 어노테이션은 만능이 아니다.
    • 편리함 속에 너무 추상화되어 있기 때문에, 버그가 발생할 가능성이 높고 발생 시에 발견하기도 어렵다.
    • 쓸 거면 정확하게 알고 써야 한다.

댓글