데이터는 null인데, jdbcTemplate getLong, getInt에서 default값이 나와요

김재연·2024년 11월 30일
1
post-thumbnail

배경

회사에서 jdbcTemplate 을 이용하여 save, read 메서드를 작성하고 테스트를 했는데, 데이터 타입을 Long? 으로 선언했고 데이터를 null로 넣었는데, 테스트에서 계속 null이 아닌 0으로 값이 나와서 실패를 했다. DB까지 조회해서 값을 확인해봤는데, DB에도 null인데 객체 조회할때는 0으로 나왔다.
이번 포스팅에서 왜 null이 아닌 0이 나왔는지에 대해 살펴보고 추가로 Long 타입 말고 다른 타입에서도 유사한 문제가 있는지 + JPA에서는 어떻게 동작하는지 살펴보고자 한다.

Long? 이란 무엇인가요?

코틀린문법으로 데이터 타입은 Long인데, nullable할 수 있는 Long

// 예시
data class User(

val notNullLong : Long, // 항상 not null을 보장

val nullableLong : Long?, // null일수도 있고, not null 일 수도 있다.

)

원인

getLong

원인을 살펴보니 JdbcTemplate 을 통해 데이터를 읽으면 ResultSet 을 통해 entity 값을 매핑하는데, 사진과 같이 getLong 메서드 내부에서 값이 null일때 default값으로 0L 을 매핑하는 로직이 존재했다.

default값이 존재하는 이유는 long 과 같이 Primitive 타입은 null을 가질 수 없기 때문에 default값이 존재한다. 반면 String은 Primitive이 아니기 때문에 null을 가질 수 있다.

다른 타입은 이슈 없나?

Long 타입 말고 다른 타입도 이상이 없는지 체크해보기 아래와 같이 각 데이터 타입을 들고 있는 entity를 선언하고 jdbcTemplate을 이용하여 저장 및 조회 메서드를 작성하고 테스트코드를 작성해보았다.

결론적으로 jdbcTemplate을 사용할때 getBoolean, getInt, getLong, getDouble, getFloat, getByte, getShort 메서드는 조회한 값이 null일때 default값을 넣우주고 DB와 entity 사이 값이 다르기 때문에 주의 해서 사용해야한다.

// entity
@Entity
@Table(name = "blog_test")
data class BlogTestEntity(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null,
    val testString: String?,
    val testBoolean: Boolean?,
    val testInt: Int?,
    val testLong: Long?,
    val testBigDecimal: BigDecimal?,
    val testDouble: Double?,
    val testFloat: Float?,

    // etc type
    val testByte: Byte?,
    val testShort: Short?,
)

// jdbc Mapper
@Component
internal class BlogTestMapper: RowMapper<BlogTestEntity> {
    override fun mapRow(rs: ResultSet, rowNum: Int) = BlogTestEntity(
        id = rs.getLong("id"),
        testString = rs.getString("test_string"),
        testBoolean = rs.getBoolean("test_boolean"),
        testInt = rs.getInt("test_int"),
        testLong = rs.getLong("test_long"),
        testBigDecimal = rs.getBigDecimal("test_big_decimal"),
        testDouble = rs.getDouble("test_double"),
        testFloat = rs.getFloat("test_float"),
        testByte = rs.getByte("test_byte"),
        testShort = rs.getShort("test_short")
    )
}

// test
@Test
    fun saveTest() {
        val entity = BlogTestEntity(
            testString = null,
            testBoolean = true,
            testInt = null,
            testLong = null,
            testBigDecimal = null,
            testDouble = null,
            testFloat = null,
            testByte = null,
            testShort = null
        )

        assertDoesNotThrow { sut.save(entity) }

        // then
        val resultList = assertDoesNotThrow { sut.findAll() }
        assertTrue(resultList.isNotEmpty())
        val result = resultList.first()

		// id 필드를 제외하고 모든 필드가 같은지 검증
        ComparisonUtils.isEqualsTo(
            result,
            BlogTestEntity(
                testString = null,
                testBoolean = false,
                testInt = 0,
                testLong = 0L,
                testBigDecimal = null,
                testDouble = 0.0,
                testFloat = 0F,
                testByte = 0,
                testShort = 0
            ),
            ignoreFields = listOf("id")
        )
    }

jpa에서는 어떻게 동작하지?

jdbcTemplate에서는 default값이 들어가는것을 확인했으니 jpa에서는 어떻게 동작하는지 궁금해졌다. jpa 엔티티를 선언하고, 저장 + 조회하는 테스트를 작성해보았는데, jpa에서는 entity에 null 값들이 잘 들어왔다.

jpa에서는 하이버네이트 내부에서 리플렉션을 이용하여 객체를 만드는데, db와 유사한값으로 매핑하기 때문에 jdbcTemplate의 getLong, getInt 처럼 default값이 없었다.

jpa에서는 어떻게 값을 매핑하는지 궁금해서 디버깅을 많이 들어가봤는데, 정확한 로직을 파악하지 못해서 이 부분은 기회되면 다른 포스팅에서 작성하고자 한다.

@Test
    fun `jpaSaveTest`() {
        val entity = BlogTestEntity(
            testString = null,
            testBoolean = null,
            testInt = null,
            testLong = null,
            testBigDecimal = null,
            testDouble = null,
            testFloat = null,
            testByte = null,
            testShort = null
        )

        val savedEntity = assertDoesNotThrow { sut.save(entity) }

        val result = sut.findByIdOrNull(savedEntity.id)

        ComparisonUtils.isEqualsTo(
            result,
            entity
        )

        sut.deleteById(savedEntity.id)
    }

결론

ResultSet의getLong, getInt, getFloat, getDouble, getBoolean, getBytes, getShort 메서드에서는 Integer, Long 과 같이 Wrapper 객체를 반환하는 것이 아닌 int, long처럼 Primitive값을 반환하기 때문에 값이 null이면 default값이 존재하기 때문에 사용할때 주의하자.

느낀점

필자는 정산팀에 근무하고 있는데, 문제됬던 Long? 컬럼에서 값이 null 이냐 default=0 이냐에 따라 비즈니스 로직이 많이 다른데, 이거를 잘 모르고 사용했으면 비즈니스로직에서 크리티컬한 문제가 됬을텐데 미리 방지해서 다행이였다.

스스로한테 아쉬웠던 부분은 내가 사용하는 기술에 대해서 깊이 이해하지 못했던 부분이 아쉬웠다. 기회되는 대로 조금씩 내가 사용하는 기술에 대해 동작원리를 이해해야겠다.

관련 코드

profile
이제 블로그 좀 쓰자

0개의 댓글