설 연수
하하호홓
설 연수
전체 방문자
오늘
어제
  • 분류 전체보기 (231)
    • Back-End (2)
      • Java (20)
      • JSP (13)
      • Spring (18)
      • Kotlin (0)
      • node.js (0)
    • Front-End (68)
      • JavaScript (19)
      • jQuery (39)
      • Angular (4)
      • HTML (5)
    • Dev-Ops (12)
      • Linux, Cloud (5)
      • docker, k8s (5)
      • ElasticSeach (2)
    • Other (33)
      • OOP (3)
      • 알고리즘 (2)
      • DB (12)
      • Git (1)
      • Swift (4)
    • Backup (65)

블로그 메뉴

    공지사항

    인기 글

    태그

    • flex
    • MYSQL
    • jOOQ
    • angular 콜백
    • INVALID
    • docker
    • page not found
    • jquery invalid
    • angular4
    • mongodb
    • angular2
    • Redis
    • RESTful
    • Kafka
    • 404 error
    • CORS
    • angular callback
    • Angular
    • 패스트캠퍼스
    • 크로스도메인

    최근 댓글

    최근 글

    티스토리

    hELLO · Designed By 정상우.
    설 연수

    하하호홓

    Back-End/Java

    MySQL-jOOQ BigDecimal 캐스팅 다루기

    2022. 3. 26. 16:50

    오류

    오류 사례

    적정금액일때는 잘 계산되던 프로그램이 숫자범위가 커지자 합계금액이 틀어지는 오류 발생.
    (숫자 한계범위 초과)

    오류 원인파악

    형변환을 위해 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);
    }
    • https://www.jooq.org/doc/latest/manual/sql-building/column-expressions/datatype-coercions/

    그러니까.. 형변환을 안하면 되는것 아니냐고.. 왜 해야되는데?: 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으로 변경해야 함.
            1. BigDecimal.ZERO → BigInteger.ZERO
            2. BigInteger와 BigDecimal 연산 시(+-*/), 타입으로인한 컴파일오류가 발생
              1. coerce활용 필요.
          • 응답GQL, DTO까지 변환해야 완전한 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
      'Back-End/Java' 카테고리의 다른 글
      • ThreadLocal
      • 인텔리J Entity Class에 @Table(name), @Column(name) 빨간줄 끄는방법
      • [TDD] Mockito
      • JAVA 자소 분리된 단어 합치기
      설 연수
      설 연수

      티스토리툴바