에러 기록 - 'NaN' is not a valid numeric or approximate numeric value
서비스 막바지 개발 중 테스트 시에 어이없는 에러가 터졌다.
2024-12-18T22:56:05.609+09:00 WARN 4860 --- [nio-8084-exec-6] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 0, SQLState: S1009
2024-12-18T22:56:05.609+09:00 ERROR 4860 --- [nio-8084-exec-6] o.h.engine.jdbc.spi.SqlExceptionHelper : 'NaN' is not a valid numeric or approximate numeric value
2024-12-18T22:56:05.627+09:00 INFO 4860 --- [nio-8084-exec-6] p6spy :
2024-12-18T22:56:05.636+09:00 ERROR 4860 --- [nio-8084-exec-6] c.j.j.c.e.h.GlobalExceptionHandler : RuntimeException : Unable to bind parameter #9 - NaN ['NaN' is not a valid numeric or approximate numeric value] [n/a]
OD로 작성된 시음노트에 대한 값이 삭제되려고 할 때 발생하는 에러였기 때문에,
// 우리 서비스는 쉽게 말해 우리가 갖고 있는 데이터를 활용한 게시글과 사용자가 직접 입력한 데이터를 활용한 게시글 2개로 나뉜다.
실제 삭제 로직을 보면 아래와 같아서 수정할 부분이 명확했다.
@Transactional
public DeleteTastingNoteResponse deleteTastingNote(Member member, Long tastingNoteId) {
TastingNote tastingNote = getTastingNote(tastingNoteId);
validateTastingNoteWriter(member, tastingNote);
if (!Objects.isNull(tastingNote.getAlcoholicDrinks())) {
AlcoholicDrinks alcoholicDrinks = alcoholicDrinksReader.getById(tastingNote.getAlcoholicDrinks().getId());
alcoholicDrinks.removeRating(tastingNote.getRating());
}
tastingNote.delete();
return new DeleteTastingNoteResponse(tastingNote.getId());
}
물론 우리 서비스 코드를 처음 보는 사람들은 모르겠지만..
if문 안에 있는 removeRating이 문제였다.
해당 서비스 로직을 보면,
public void removeRating(Double existingRating) {
double currentRating = this.rating;
int count = this.tastingNoteCount;
double totalScore = count * currentRating - existingRating;
this.tastingNoteCount = count - 1;
this.rating = totalScore / this.tastingNoteCount;
}
위 로직은 alcoholicDrinks 엔티티 안에 있는 평점 업데이트 메소드이다.
해당 alcoholicDrinks로 작성된 게시글이라면, 우리는 각 alcoholicDrinks마다 평점이 있기 때문에 이렇게 평점을 업데이트 해줘야 한다.
그런데 여기서 에러가 발생한 이유는 뭘까.
문제는 tastingNoteCount가 0일 때다.
this.rating = totalScore / this.tastingNoteCount;
여기서 this.tastingNoteCount가 0이면, 0으로 나누기가 발생해 NaN이 저장된다.
아주 간단한 에러였다 나름..?
그래서 아래와 같이 방어 로직을 추가했다.
public void removeRating(Double existingRating) {
double currentRating = this.rating;
int count = this.tastingNoteCount;
// 방어 로직 추가
if (count <= 1) {
this.rating = 0.0; // 기본값으로 초기화
this.tastingNoteCount = 0;
return;
}
double totalScore = count * currentRating - existingRating;
this.tastingNoteCount = count - 1;
this.rating = totalScore / this.tastingNoteCount;
}
만약 tastingNoteCount가 1보다 작거나 같다면, 현재 데이터의 rating과 tastingNoteCount를 0(기본값)으로 초기화한다.
NaN이 저장되는 것을 방지하기 위해서다.
에러 해결!!
추가 정보
NaN은 "Not a Number"의 약자로, 숫자 연산에서 발생할 수 있는 특수한 상태입니다.
Java에서 NaN은 double이나 float 타입의 값으로, 특정 연산 결과가 수학적으로 정의되지 않을 때 나타납니다.
NaN은 다음과 같은 상황에서 발생한다.
1. 0을 0으로 나눌 때
double result = 0.0 / 0.0; // NaN
2. 음수의 제곱근을 계산할 때
double result = Math.sqrt(-1); // NaN
3. NaN 값과 연산을 할 때
double result = Double.NaN + 1; // 여전히 NaN
이제 NaN이랑은 다시 만나고 싶지 않다..!