MyBatis 매핑 시 기본생성자 문제
MyBatis 매핑 문제 발생 …
RefreshTokenMapper를 테스트하는 과정에서 다음과 같은 문제가 발생했다. 데이터베이스에 토큰 정보를 저장(save)한 뒤, 해당 데이터를 다시 조회(findByMemberId)하는 간단한 로직이었다.
Cause: org.h2.jdbc.JdbcSQLDataException: Cannot parse “TIMESTAMP” constant “refresh-token”
마이바티스가 TOKEN 매핑해야 할 문자열 타입의 정보인 “refresh-token”을 엉뚱한 컬럼인 TIMESTAMP에 매핑을 시도하고 있었던 것이다.
아니, TOKEN컬럼은 VARCHAR(512)타입인데, 어떻게 DATETIME 타입 컬럼에 매핑을 시도한다는 것인가?
문제를 해결하기 위해 관련된 코드들을 모두 찾아 올라갔다.
RefreshToken.java
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class RefreshToken {
private Long id;
private Long memberId;
private String token;
private LocalDateTime issuedAt;
private LocalDateTime expiresAt;
public RefreshToken(Long memberId, String token, LocalDateTime issuedAt, LocalDateTime expiresAt) {
this.memberId = memberId;
this.token = token;
this.issuedAt = issuedAt;
this.expiresAt = expiresAt;
}
}
RefreshTokenMapper.xml
<mapper namespace="bapfriendbe.auth.RefreshTokenMapper">
<select id="findByMemberId" parameterType="java.lang.Long" resultType="bapfriendbe.auth.RefreshToken">
SELECT *
FROM refresh_token
WHERE member_id = #{memberId}
</select>
</mapper>
SELECT *
구문이 컬럼 순서를 보장하지 않아 문제가 될 수 있다는 생각에 <resultMap>
을 도입하여 수동으로 매핑 규칙을 정해 보았다.
RefreshTokenMapper.xml
<resultMap id="RefreshTokenMap" type="bapfriendbe.auth.RefreshToken">
<id column="id" property="id"/>
<result column="member_id" property="memberId"/>
<result column="token" property="token"/>
<result column="issued_at" property="issuedAt"/>
<result column="expires_at" property="expiresAt"/>
</resultMap>
<select id="findByMemberId" parameterType="java.lang.Long" resultMap="RefreshTokenMap">
SELECT id, member_id, token, issued_at, expires_at
FROM refresh_token
WHERE member_id = #{memberId}
</select>
resultMap을 도입해도 문제는 해결되지 않았다. 그런데 원인은 뜻밖에도 다른 곳에 있었다.
원인: RefreshToken 객체의 기본 생성자 부재
우선 마이바티스가 resultType을 통해 DB 조회 결과를 객체에 자동으로 매핑할 때, 가장 표준적인 동작방식은 다음과 같다.
- 기본 생성자로 텅 빈 객체를 먼저 만든다.
- ResultMap에 정의된 규칙을 따라, 각 컬럼의 값을 가져와서 해당하는 Setter 메서드를 호출하여 빈 객체의 필드를 채워나간다.
그런데, 나의 RefreshToken 클래스에는 내가 정의한 생성자 때문에 컴파일러가 눈에 보이지 않는 기본 생성자를 만들지 않았던 것이였다. 따라서 기본 생성자를 Lombok을 활용해 간단하게 만들어 주었다.
@Data
@NoArgsConstructor // <<< 추가
public class RefreshToken {
private Long id;
private Long memberId;
private String token;
private LocalDateTime issuedAt; // 리프레시토큰 발급 시간
private LocalDateTime expiresAt; // 리프레시토큰 만료 시간
public RefreshToken(Long memberId, String token, LocalDateTime issuedAt, LocalDateTime expiresAt) {
this.memberId = memberId;
this.token = token;
this.issuedAt = issuedAt;
this.expiresAt = expiresAt;
}
}
기본 생성자를 명시적으로 넣어 주니 드디어 테스트가 정상적으로 동작하게 되었다.
결론
MyBatis와 같은 프레임워크와 연동하는 DTO/VO 클래스에는 기본 생성자를 만들어주는 것이 좋다. 이는 프레임워크가 리플렉션을 통해 객체를 생성하고 값을 주입하는 가장 표준적인 방식을 지원하기 위함이다.
댓글남기기