[문제 해결] JDBC ResultSet이 null을 0으로 읽는다?
전제
우아한테크코스 장바구니 협업 미션에서 우리 팀은 주문에 1개의 쿠폰을 적용할 수 있도록 정책을 정하였다.
사용자는 주문 시 쿠폰을 쓸 수도, 안쓸 수도 있기 때문에 ORDERS 테이블의 coupon_id 컬럼은 null을 허용하도록 했다.
CREATE TABLE IF NOT EXISTS orders
(
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
member_id BIGINT NOT NULL,
coupon_id BIGINT,
delivery_fee BIGINT NOT NULL,
status VARCHAR(10) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
문제 상황
그런데 쿠폰을 적용하지 않은 주문을 조회할 때 문제가 발생했다.
내가 설정해둔 이 예외가 발생했다.
IllegalStateException("illegal data exists in table ORDERS; coupon_id")
coupon_id가 null이 아니어서 쿠폰을 적용한 주문이라고 판단했을 때,
해당 주문의 coupon_id를 가진 쿠폰 데이터가 없으면 던지는 예외이다.
(나는 FK와 DB Join을 쓰지 않았다.
대신 저장 시에는 Repository 계층에서 유효한 참조키인지 검증하여 무결성을 보장하고,
조회 시에는 잘못된 참조키로 조회하려고 하면 잘못된 데이터를 가지고 있다는 걸 알 수 있으므로 예외를 던지도록 구현하였다.)
// OrderRepository.java
public Optional<Order> findById(final Long orderId) {
final Optional<OrderEntity> foundResult = orderDao.findById(orderId);
if (foundResult.isEmpty()) {
return Optional.empty();
}
final OrderEntity foundOrder = foundResult.get();
final List<OrderItemEntity> foundOrderItems = orderItemDao.findByOrderId(orderId);
final Long couponId = foundOrder.getCouponId();
if (Objects.nonNull(couponId)) {
// 이 부분!!
final Coupon foundCoupon = couponRepository.findById(couponId)
.orElseThrow(() -> new IllegalStateException("illegal data exists in table ORDERS; coupon_id"));
return Optional.of(Order.of(foundOrder, foundOrderItems, foundCoupon));
}
return Optional.of(Order.of(foundOrder, foundOrderItems));
}
디버깅해보니, orderDao에서 주문을 조회할 때 coupon_id에 null이 아닌 0이 담기고 있었다.
OrderDao에서 사용한 RowMapper는 다음과 같다.
// OrderDao.java
private static final RowMapper<OrderEntity> ROW_MAPPER = (resultSet, rowNum) ->
new OrderEntity(
resultSet.getLong("id"),
resultSet.getLong("member_id"),
resultSet.getLong("coupon_id"),
resultSet.getLong("delivery_fee"),
resultSet.getString("status"),
resultSet.getTimestamp("created_at")
);
원인 & 해결
원인은 간단했다. resultSet의 getLong() 메서드는, 열의 값이 null이면 0을 반환하기 때문이었다. (대체 왜 그렇게 만든거지..^^)
그럼 다른 getXXX() 메서드는 null값에 대해 어떻게 동작할까?
getInt(), getDouble(), getFloat(), getBoolean() 메서드도 열 값이 null인 경우 각각 0, 0.0, 0.0f, false를 반환한다고 한다.
하지만 getString()은 null인 값을 null로 반환한다.
그럼 어떻게 하는 게 좋을까?
대신, resultSet의 wasNull() 메서드는 직전에 조회한 값이 null인지 알려준다.
그래서 RowMapper 코드를 아래와 같이 수정하여 해결했다.
하지만 해당 변수에 final 키워드를 붙일 수 없는 방식의 코드라 마음에 들지는 않는다.
더 좋은 방법은 뭐가 있을까?
// OrderDao.java
private static final RowMapper<OrderEntity> ROW_MAPPER = (resultSet, rowNum) -> {
Long couponId = resultSet.getLong("coupon_id");
if (resultSet.wasNull()) {
couponId = null;
}
return new OrderEntity(
resultSet.getLong("id"),
resultSet.getLong("member_id"),
couponId,
resultSet.getLong("delivery_fee"),
resultSet.getString("status"),
resultSet.getTimestamp("created_at")
);
};