1 분 소요

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 조회 결과를 객체에 자동으로 매핑할 때, 가장 표준적인 동작방식은 다음과 같다.

  1. 기본 생성자로 텅 빈 객체를 먼저 만든다.
  2. 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 클래스에는 기본 생성자를 만들어주는 것이 좋다. 이는 프레임워크가 리플렉션을 통해 객체를 생성하고 값을 주입하는 가장 표준적인 방식을 지원하기 위함이다.

댓글남기기