오류
오류 사례
적정금액일때는 잘 계산되던 프로그램이 숫자범위가 커지자 합계금액이 틀어지는 오류 발생.
(숫자 한계범위 초과)
오류 원인파악
형변환을 위해 BigDecimal로 타입캐스팅을 하고 있었음
ENTITY.COL1.subtract(ENTITY.COL2))
.cast(BigDecimal.class).as("잔액")
.cast(BigDecimal.class)으로 발생하는 문제: 숫자 범위초과
.cast(BigDecimal.class)코드 SQL문법으로 다음과 같이 렌더링된다.
cast(? as decimal)
decimal의 기본값은 decimal(10)과 같다
cast(? as decimal(10))
즉, 10자리 정수까지만 처리가 가능하다는 의미로 이해하면 된다.
- 100억 미만까지 표현가능
MySQL피셜
- https://dev.mysql.com/doc/refman/8.0/en/precision-math-decimal-characteristics.html
- https://dev.mysql.com/doc/refman/5.7/en/precision-math-decimal-characteristics.html
문제점:
- 10자리를 초과하는 정수의 경우, 9999999999가 리턴된다.
/**
|------------------|------------------|-------------------|-------------------|--------------------------|
|? |? |cast(? as decimal) |cast(? as decimal) |cast(? as decimal(30, 5)) |
|------------------|------------------|-------------------|-------------------|--------------------------|
|10000000000.11111 |10000000000.11111 |9999999999 |9999999999 |10000000000.11111 |
|------------------|------------------|-------------------|-------------------|--------------------------|
* */
@Test
void decimalCast() {
final BigDecimal bigDecimalValue = BigDecimal.valueOf(1_00000_00000.11111); // 11자리 정수 + 5자리 소수점
final double doubleValue = 1_00000_00000.11111;// 11자리 정수 + 5자리 소수점
final var fetchOne = dslContext.select(
DSL.value(bigDecimalValue),
DSL.value(doubleValue).coerce(BigDecimal.class),
DSL.value(bigDecimalValue).cast(BigInteger.class),
DSL.value(bigDecimalValue).cast(BigDecimal.class),
DSL.value(bigDecimalValue).cast(SQLDataType.DECIMAL.precision(30, 5))
).fetchOne();
// 형변환 X
assertThat(fetchOne.component1())
.as("캐스팅 안한 결과: 값이 그대로 출력된다.")
.isEqualByComparingTo(bigDecimalValue);
// decimal의 default: decimal(10)
assertThat(fetchOne.component3())
.as("BigInteger => cast(? as decimal): 소수점이 버려지고, 10자리 정수로 값이 변경된다.")
.isEqualByComparingTo(BigInteger.valueOf((long) 9999999999.0));
assertThat(fetchOne.component4())
.as("BigDecimal => cast(? as decimal): 소수점이 버려지고, 10자리 정수로 값이 변경된다.")
.isEqualByComparingTo(BigDecimal.valueOf((long) 9999999999.0));
}
해결하기
DataType의 정수,소수점 범위 지정해서 캐스팅하기
BigDecimal의 경우, 캐스팅할때 DataType으로 범위를 설정해야 오류를 방지할 수 있다.
// <Z> Field<Z> cast(DataType<Z> type);
DSL.value(bigDecimalValue).cast(SQLDataType.DECIMAL.precision(30, 5))
/**
|------------------|------------------|-------------------|-------------------|--------------------------|
|? |? |cast(? as decimal) |cast(? as decimal) |cast(? as decimal(30, 5)) |
|------------------|------------------|-------------------|-------------------|--------------------------|
|10000000000.11111 |10000000000.11111 |9999999999 |9999999999 |10000000000.11111 |
|------------------|------------------|-------------------|-------------------|--------------------------|
* */
@Test
void decimalCast() {
final BigDecimal bigDecimalValue = BigDecimal.valueOf(1_00000_00000.11111); // 11자리 정수 + 5자리 소수점
final double doubleValue = 1_00000_00000.11111;// 11자리 정수 + 5자리 소수점
final var fetchOne = dslContext.select(
DSL.value(bigDecimalValue),
DSL.value(doubleValue).coerce(BigDecimal.class),
DSL.value(bigDecimalValue).cast(BigInteger.class),
DSL.value(bigDecimalValue).cast(BigDecimal.class),
DSL.value(bigDecimalValue).cast(SQLDataType.DECIMAL.precision(30, 5))
).fetchOne();
// 형변환 X
assertThat(fetchOne.component1())
.as("캐스팅 안한 결과: 값이 그대로 출력된다.")
.isEqualByComparingTo(bigDecimalValue);
// cast할때, 정석
assertThat(fetchOne.component5())
.as("DECIMAL.precision(30, 5) => cast(? as decimal(30, 5)): 자리수를 초과하지 않았기때문에 그대로 출력된다.")
.isEqualByComparingTo(bigDecimalValue);
}
개인의견: 캐스팅하는것이 성능저하에 영향이 조금이나마 영향이 있을것 같아 꺼려졌다.
캐스팅처리를 안하면 되는거아냐?: coerce로 SQL(cast) 렌더링 없이 제네릭 형변환하기
jOOQ에서 JAVA컴파일단 형변환만 수행하는 메서드가 존재했다: coerce(Class<Z> type)
제네릭 타입은 변환하지만, SQL 렌더링(cast)은 하지않음
// Field<Z> coerce(Class<Z> type);
@Test
void decimalCast() {
final BigDecimal bigDecimalValue = BigDecimal.valueOf(1_00000_00000.11111); // 11자리 정수 + 5자리 소수점
final double doubleValue = 1_00000_00000.11111;// 11자리 정수 + 5자리 소수점
final var fetchOne = dslContext.select(
DSL.value(bigDecimalValue),
DSL.value(doubleValue).coerce(BigDecimal.class),
DSL.value(bigDecimalValue).cast(BigInteger.class),
DSL.value(bigDecimalValue).cast(BigDecimal.class),
DSL.value(bigDecimalValue).cast(SQLDataType.DECIMAL.precision(30, 5))
).fetchOne();
// coerce: 타입 강제 형변환(SQL 렌더링 X)
assertThat(fetchOne.component2())
.as("캐스팅 안함")
.isEqualByComparingTo(bigDecimalValue);
}
그러니까.. 형변환을 안하면 되는것 아니냐고.. 왜 해야되는데?: jOOQ클래스 Long필드를 BigDecimal로 바꾸기
- 왜 형변환이 필요했을까?
- 형변환이 필요없었던 jOOQ Class 필드
- BigDecimal을 BigDecimal과 연산할때는 형변환이 필요하지 않았다.
public class ENTITY extends TableImpl<ENTITY_RECORD> {
// ...
public final TableField<ENTITY_RECORD, BigDecimal> SUM_AMT = createField(DSL.name("SUM_AMT"), SQLDataType.DECIMAL(15, 3).defaultValue(DSL.inline("0.000", SQLDataType.DECIMAL)), this, "");
}
형변환이 필요했던 jOOQ Class 필드
- 필드의 제네릭 타입이 BigDecimal이 아님.
Long을 BigDecimal과 연산하려니 형변환이 필요했다.
public class ENTITY extends TableImpl<ENTITY_RECORD> {
// ...
public final TableField<ENTITY_RECORD, Long> DR_AMT = createField(DSL.name("DR_AMT"), SQLDataType.BIGINT.nullable(false).defaultValue(DSL.inline("0", SQLDataType.BIGINT)), this, "");
}
- 결론: decimal(numeric)의 소수점 유무가 jOOQ 필드타입에 영향을 미친것 같다.
(왜 BigInteger가 아니고 Long일까..?) - 다른컬럼들처럼 AMT, QTY 컬럼은 그냥 BigDecimal로 다룰래
- jOOQ Forced types으로 타입을 지정한다.
- jOOQ Forced types Manual: https://www.jooq.org/doc/latest/manual/code-generation/codegen-advanced/codegen-config-database/codegen-database-forced-types/
- gradle.build
jooq {
// ...
project(sourceSets.main) {
// ...
generator {
// ...
database {
// ...
forcedTypes {
forcedType {
// Specify any data type that is supported in your database, or if unsupported,
// a type from org.jooq.impl.SQLDataType
name = 'DECIMAL'
// A Java regex matching data types to be forced to have this type.
includeTypes = '.*'
// A Java regex matching fully-qualified columns, attributes, parameters. Use the pipe to separate several expressions.
includeExpression = '.*AMT|.*PRICE|.*QTY'
// Force a type on ALL or specific object types, including
// ATTRIBUTE, COLUMN, ELEMENT, PARAMETER, SEQUENCE
objectType = 'COLUMN'
}
}
}
// ...
}
}
system(sourceSets.main) {
// ...
generator {
// ...
database {
// ...
forcedTypes {
forcedType {
name = 'DECIMAL'
includeTypes = '.*'
includeExpression = '.*AMT|.*PRICE|.*QTY'
objectType = 'COLUMN'
}
}
}
// ...
}
}
}
- 소수점 없는 decimal타입을 BigDecimal로 다루는게 좋을까?
- 일단, 기존에는 Long이었다.
- BigDecimal보다 BigInteger가 더 적합한것 같다는 생각도 든다.
(일단 생각만 하기로함…)- BigInteger로 다룰 경우,
- 기존 BigDecimal처리를 BigInteger으로 변경해야 함.
- BigDecimal.ZERO → BigInteger.ZERO
- BigInteger와 BigDecimal 연산 시(+-*/), 타입으로인한 컴파일오류가 발생
- coerce활용 필요.
- 응답GQL, DTO까지 변환해야 완전한 BigInteger적용.
- 기존 BigDecimal처리를 BigInteger으로 변경해야 함.
- BigInteger로 다룰 경우,
전체 테스트코드
/**
|------------------|------------------|-------------------|-------------------|--------------------------|
|? |? |cast(? as decimal) |cast(? as decimal) |cast(? as decimal(30, 5)) |
|------------------|------------------|-------------------|-------------------|--------------------------|
|10000000000.11111 |10000000000.11111 |9999999999 |9999999999 |10000000000.11111 |
|------------------|------------------|-------------------|-------------------|--------------------------|
* */
@Test
void decimalCast() {
final BigDecimal bigDecimalValue = BigDecimal.valueOf(1_00000_00000.11111); // 11자리 정수 + 5자리 소수점
final double doubleValue = 1_00000_00000.11111;// 11자리 정수 + 5자리 소수점
final var fetchOne = dslContext.select(
DSL.value(bigDecimalValue),
DSL.value(doubleValue).coerce(BigDecimal.class),
DSL.value(bigDecimalValue).cast(BigInteger.class),
DSL.value(bigDecimalValue).cast(BigDecimal.class),
DSL.value(bigDecimalValue).cast(SQLDataType.DECIMAL.precision(30, 5))
).fetchOne();
// 형변환 X
assertThat(fetchOne.component1())
.as("캐스팅 안한 결과: 값이 그대로 출력된다.")
.isEqualByComparingTo(bigDecimalValue);
// coerce: 타입 강제 형변환(SQL 렌더링 X) https://www.jooq.org/doc/latest/manual/sql-building/column-expressions/datatype-coercions/
assertThat(fetchOne.component2())
.as("캐스팅 안함")
.isEqualByComparingTo(bigDecimalValue);
// decimal의 default: decimal(10)
// https://dev.mysql.com/doc/refman/5.7/en/precision-math-decimal-characteristics.html
assertThat(fetchOne.component3())
.as("BigInteger => cast(? as decimal): 소수점이 버려지고, 10자리 정수로 값이 변경된다.")
.isEqualByComparingTo(BigInteger.valueOf((long) 9999999999.0));
assertThat(fetchOne.component4())
.as("BigDecimal => cast(? as decimal): 소수점이 버려지고, 10자리 정수로 값이 변경된다.")
.isEqualByComparingTo(BigDecimal.valueOf((long) 9999999999.0));
// cast할때, 정석
assertThat(fetchOne.component5())
.as("DECIMAL.precision(30, 5) => cast(? as decimal(30, 5)): 자리수를 초과하지 않았기때문에 그대로 출력된다.")
.isEqualByComparingTo(bigDecimalValue);
}
'Back-End > Java' 카테고리의 다른 글
ThreadLocal (0) | 2022.03.27 |
---|---|
인텔리J Entity Class에 @Table(name), @Column(name) 빨간줄 끄는방법 (0) | 2020.06.12 |
[TDD] Mockito (0) | 2020.05.08 |
JAVA 자소 분리된 단어 합치기 (2) | 2020.04.30 |
직접 작성해보는 java map, filter, reduce, curry (0) | 2019.10.28 |