개발중에 작은 오류 하나에 ... 헛방을 키다 기록용으로 남겨 놓습니다.

mybatis 를 이용한 DB 쿼리 조회에서 

select 안에 join 한 다른 테이블의 리스트 형을 처리 하고자 mybatis의 collection을 이용 하고 있습니다

 

<resultMap type="testVo" id="Map">
    <id property="seq" column="seq"/>
    <result property="userId" column="userId"/>
    <result property="userPw" column="userPw" />
    <collection property="info" javaType="List" ofType="myVo">
        <result property="nameSeq" column="nameSeq"/>
        <result property="testId" column="testId"/>
    </collection>
</resultMap>

resultMap 을 사용 해야 하고 컬렉션 안에는 조인 결과가 많은 리스트로 생성 됩니다

 

실행 결과는 argument type mismatch.....

 

이는 VO 와 DB테이블과 무언가의 변수차이가 난다는 에러 이므로 저와 같은 상황에서는 

myVo 에 info 라는 변수가 존재 해야 하고 info 는 List 타입이어야 해결 됬습니다.

 

반응형

강준한입니다.

스트리밍 버전으로 메모리를 적게 사용 된다고 하는 SXSSF를 이용하여 대용량 엑셀 다운로드 를 사용 했습니다

 

SXSSFWorkbook wb = null;
FileOutputStream fos = null;
SXSSFSheet sheet;
SXSSFRow row;
SXSSFCell cell;
try {
    //크롬 인코딩
    fileName = new String(fileName.getBytes("UTF-8"),"ISO-8859-1");
    wb = new SXSSFWorkbook(100); //메모리 누적 해가면 DISK 에 저장 하는 기준 사이즈
    fos = new FileOutputStream("경로");
    wb.setCompressTempFiles(true);

    sheet = wb.createSheet("Sheet1");

    int position = 0;
    row = sheet.createRow(position);

//          컬럼명
    for (int i = 0; i < columns.length; ++i) {
        cell = row.createCell(i);
        cell.setCellValue(columns[i]);
    }

//          데이터
    for (int i = 0; i < tmpData.size(); i++) {
        position++;
        row = sheet.createRow(position);
        int k = 0;
        for (int column = 0; column < columns.length; ++column) {
            cell = row.createCell(k);
            if (tmpData.get(i).get(columns[k]) != null) {
                cell.setCellValue(tmpData.get(i).get(columns[k]).toString());
            }
            k++;
        }
    }
    res.setContentType("application/vnd.ms-excel");
    // 엑셀 파일명 설정
    res.setHeader("Content-Disposition", "attachment;filename="+파일이름+".xlsx");
    wb.write(res.getOutputStream());

} catch (Exception e) {
    log.error(e.getMessage(), e);

} finally {
    try {
        wb.close();
        wb.dispose();
        if (fos != null) {
            fos.close();
        }
    } catch (IOException e) {
        log.error(e.getMessage(), e);
    }
}

이 방식은 그저 엑셀을 생성 하는 부분이라는 점임니다.

 

주로 엑셀을 다운로드 할 상황은 쿼리를 보내 ROW 를 가져와 엑셀을 만드는 형태입니다.

엑셀의 생성에는 문제 없으나 쿼리가 몇십만 ROW 이다 보니 거기에서 서버가 힘을 내지 못하는 상황이 었습니다.

 

쿼리를 조금씩 가져와 엑셀에 쌓는 방법은 없을까 고민 하다가

org.apache.ibatis 에 괜찮은 기능을 찾았습니다.

 

ResultRowDataHandler

 

주로 mybatis 에서 쿼리를 조회 해오면 리스트에 담아 오는 형태인데 핸들러라는 말이 들어간거 보니 ROW 단위로 DATA를 핸들링 하는 느낌이 들어 심도있게 파봤습니다.

 

public class  ResultRowDataHandler implements ResultHandler {
    private final SXSSFWorkbook workbook;
    private final SXSSFSheet sheet;
    SXSSFRow row;
    SXSSFCell cell;
    FileOutputStream fileOutputStream;
    private final String[] columns;

    public ResultRowDataHandler(Map<String, Object> params, String[] col, Map<String, Object> getConfig) throws FileNotFoundException {
        // 현재 날짜 구하기
        LocalDate now = LocalDate.now();
        // 포맷 정의
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
        // 포맷 적용
        String formatedNow = now.format(formatter);

        workbook = new SXSSFWorkbook(10000);
        workbook.setCompressTempFiles(true);
        sheet = workbook.createSheet("Sheet1");

        row = sheet.createRow(0);
        columns = col;
        //컬럼명
        for (int i = 0; i < columns.length; ++i) {
            cell = row.createCell(i);
            cell.setCellValue(columns[i]);
           
        }

        String savePath = params.get("path").toString() + "/" + formatedNow+"_"+params.get("SaveFileName").toString() + ".xlsx";
        fileOutputStream = new FileOutputStream(savePath);

    }

    @Override
    public void handleResult(ResultContext resultContext) {
        Map<String, Object> result = (Map<String, Object>) resultContext.getResultObject();
        //데이터
        row = sheet.createRow(resultContext.getResultCount());
        int k = 0;
        for (int column = 0; column < columns.length; ++column) {
            cell = row.createCell(k);
            if (result.get(columns[k]) != null) {
                cell.setCellValue(result.get(columns[k]).toString());
            }
            k++;
        }
    }

    public void close() {
        try{
            workbook.write(fileOutputStream);
            workbook.dispose();
            workbook.close();
            fileOutputStream.close();
        }catch (Exception e){
            log.error("ERROR : ", e);
        }
    }
}

우선 핸드러를 여기저기 구글링 하여 여차 저차 작성 했습니다.

가져온 데이터를 앞서 작성해본 엑셀에 차차 쌓는 형식의 헨들링 파일을 작성하고

 

핸들러를 호출 합니다 

다오에서 매퍼로 넘길때까지 핸들러를 같이 넘겨 쿼리를 실행하며 엑셀을 생성 합니다.

30만 ROW 까지는 끄떡없지만 ... 많이 느리네요

반응형

한때 아메리칸 드림을 꿈꾸며 해외 사업에 잠시 몸 담았던 적이 있다

프로젝트를 진행하며 수많은 고난과 역경을 겪었지만
그중 가장 곤란했던 문제는 달러 상품 가격을 설정하는 과정에서 발생했다.

소수점 단위의 숫자들을 연산하는 과정에서 미세한 수치지만 오차가 발생했고,

해결 방법을 열심히 찾아본 결과 이 오차는 부동소수점을 사용하는 double 자료형의 문제점인 것으로 확인했다.


오차가 왜 생겼을까?

컴퓨터는 0과 1로 이루어진 2진법을 사용하여 연산을 수행한다.
그렇기 때문에 10진법으로 이루어진 수를 2진법으로 변환하는 과정에서 오차가 발생할 확률이 존재한다.

0.2 를 2진법으로 변환할 경우
0.001100110011... 과 같이 0011 이 무한으로 반복되는 수가 된다.
0.3 을 2진법으로 변환할 경우도 마찬가지로
0.01001100110011... 과 같이 0011 이 무한으로 반복되어
컴퓨터는 0.2 0.3 과 같은 숫자들을 정확한 값으로 저장하지 않고 근사치로 저장하게 된다.

이때 double을 이용하여 이 둘을 연산할 경우

double salePrice = 0.3;
double discountPrice = 0.2;
System.out.println(salePrice - discountPrice);

 

 

위와 같은 오차가 발생하게 된다.

그렇다면 우리는 이러한 오차를 감수해야할까?


해결 방법은?

오차를 제거할 수 있는 방법 2가지를 찾았다.

가장 간단한 해결 방법은 소수점 연산을 하지 않는 것이다.
소수점 자리만큼 10^n 값을 곱해주고 나눠주는 연산을 통해 간단한 문제는 해결할 수 있다.

double salePrice = 0.3;
double discountPrice = 0.2;
System.out.println( ((salePrice*10) - (discountPrice*10)) /10 );

 


그러나 좀 더 정확한 연산을 위해서는 부동소수점인 float나 double 자료형이 아닌 BigDecimal 자료형을 사용해야 한다.

 

BigDecimal

BigDecimal의 초기화 방법은 다음과 같다.

BigDecimal salePrice = new BigDecimal("0.3");

초기화 과정에서 파라미터를 String 형태로 넘겨주는 것이 중요한데,

다른 숫자 형태의 파라미터로도 BigDecimal 초기화는 가능하지만 이때는 앞선 double 형태의 연산과 마찬가지로 오차가 발생할 수 있다.

따라서, 파라미터를 String 형태로 넘겨주는 것이 중요하다.

 

BigDecimal 연산

연산하는 방법은 다음과 같다

double salePrice = 0.3;
double discountPrice = 0.2;


BigDecimal salePrice2 = new BigDecimal(String.valueOf(salePrice));
BigDecimal discountPrice2 = new BigDecimal(String.valueOf(discountPrice));

// 덧셈 연산
System.out.println(salePrice2.add(discountPrice2));
// 뺄셈 연산
System.out.println(salePrice2.subtract(discountPrice2));
// 곱셈 연산
System.out.println(salePrice2.multiply(discountPrice2));

나눗셈의 경우는 추가적으로 옵션을 설정해주어야 하는데

BigDecimal a = new BigDecimal("0.3");
BigDecimal b = new BigDecimal("0.2");

System.out.println(b.divide(a));

위와같이 나누어지지 않는 수를 나누려 할 경우에 다음과 같은 에러가 발생한다.

Non-terminating decimal expansion; no exact representable decimal result.
java.lang.ArithmeticException: Non-terminating decimal expansion; no exact representable decimal result.

 

따라서 나눗셈에는 다음과 같은 옵션이 추가된다.

System.out.println(b.divide(a, 2, RoundingMode.UP));

2번째 매개변수는 표현할 소수점 자리수, 3번째 매개변수는 처리방식을 나타낸다.

상황에 따라 적절히 옵션을 이용하여 사용하면 될 것이다.

 

BigDecimal 비교

compareTo 메서드를 이용하여 BigDecimal 간의 비교를 할 수 있다.

비교하려는 수가 비교되는 수 보다 클 경우 1, 같을 경우 0, 작을 경우 -1 을 리턴한다.

BigDecimal a = new BigDecimal("2");
BigDecimal b = new BigDecimal("1");
BigDecimal c = new BigDecimal("1");

System.out.println(a.compareTo(b));
System.out.println(b.compareTo(a));
System.out.println(b.compareTo(c));


 

글을 작성하다보니 옛 생각이 새록새록 떠오릅니다.

부족한 글이지만 도움이 되길 바랍니다.

 

감사합니다.

반응형

강준한 입니다.

감동을 줄 뻔한 이야기로 시작해보죠.

아들이 치매에 걸린 아버지와 같이 있습니다.

아버지는 멀리 있는 새를 보고 아들에게 물었습니다,

저게 무어냐?

 아들이 말하죠...

까마귀요...

잠시 뒤, 치매에 걸린 아버지가 다시 물었습니다,

저게 무어냐?

아들이 다시 말하죠...

까마귀 라니까요

아버지는 잠시 뒤 다시 물었습니다

 저게 뭐라고?

아들은 짜증이 났습니다.

 까마귀라고요. 까!!마!!귀!!

아버지는 잠시 뒤 다시 물었습니다

저게 무어냐?

아들은 화를내며 큰소리로 말합니다.

까마귀라고요! 까마귀. 왜 자꾸 같은 SELECT를 반복하세요!!

 

그렇습니다 ... 아들에게는 cache가 적용되어 있지 않았던 것입니다. 

그래서 화(부하)가 발생했던 것입니다.

 

cache는 이런 간단하고 반복된 답을 줘야 하는 내용을 어느 한켠에 저장 했다가

바로 꺼내어 줄수 있는 방법입니다.

 

스프링에서는 아주 간단하게 사용이 가능 합니다.

implementation group: 'org.springframework.boot', name: 'spring-boot-starter-cache', version: '2.7.9'

종속성을 먼저 추가 한 후에 사용 하시면 됩니다.

물론 예시라서 따라 치면 오류납니다.

이 캐시의 어노테이션의 종류는 5가지 정도가 있습니다.

 

@Cacheable : 동일한 파리 미터로 메서드를 호출 이력과 반환 결과가 캐시에 저장되어 있으면

@CacheEvict : 캐시에 저장된 호출 이력 및 결과 값을 삭제

@CachePut : 캐시에 저장된 결괏값을 업데이트

@Caching : 여러 개의 캐시 어노테이션 옵션을 동시에 활성화하고자 할 때 사용

@CacheConfig : 캐시 옵션을 메서드 단위가 아닌 클래스 단위로 설정하고자 할 때 사용

 

그림에서 사용한 @Cacheable 키 벨류로 데이터를 저장하고 사용 되겠죠 마치 맵과 같네요

키를 안쓰면 뭐 그 안의 알고리즘으로 자동 생성 된다고 합니다.

 

선언후에 삭제 하거나 변경하는 작업을 주로 쓰겠죠? 사용 시점에 잘 분배해서 사용 하시면 될 것같습니다.

주로 많이 사용 하는 게시판을 예로 들면 리스트를 불러올때 @Cacheable 을 선언할 것이고

insert나 업데이트를 할때 캐시를 업데이트 할테고 ... delete 할때는 캐시를 비웠다가 다시 리스트를 불러올때 선언 하겠죠? ( 맞을까? )

 

하지만 사용 하다보면 각 상황에 맞는 포인트를 찾기 쉽지 않아 제때 알아서 한번씩 비워주고 다시 생성 됬으면 합니다.

물론 자동으로요 

 

여기저기 구글링을 하다 보니 ehcache 라는 녀석이 눈에 띕니다. redis 캐시도 눈에 뛰구요

하지만 과하게 가지 않고 로컬에 저장되고 사용되길 원하니 ehcache 를 사용 해봅니다. 종속성을 추가 해줍니다

 

implementation group: 'org.ehcache', name: 'ehcache', version: '3.10.0'

 

/***
private String soliloquy() throws Exception {

    return "속으론 여러번 고민한게 있었는데 이거는 그냥 쓸데없는거같지만 뭐랄까....
    메모리를 사용하는 레디스 캐시를 사용하면 1유저의 세션을 저장한다고 했을때 용량산정에 
    대한?? 이게 고민 할 문제가 맞을까요 ...? .. 암튼";
}
***/

 

버전은 자유겠지요 ... 저는 https://mvnrepository.com/ 에서 종속성을 검색한 후에 최근 몇개월간 중 많이 사용된 버전을 주로 사용 합니다. 전세계 테스터들이 좋으니까 썻겟죠?

 

ehcache 설정은 많이 검색해봤지만 xml 설정 만 나오는 거같아요

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://www.ehcache.org/ehcache.xsd"
         updateCheck="true"
         monitoring="autodetect"
         dynamicConfig="true">

    <diskStore path="io.dveamer.sample.ehcache"/>

    <defaultCache
            maxElementsInMemory="10000"
            eternal="false"
            timeToIdleSeconds="120"
            timeToLiveSeconds="120"
            overflowToDisk="true"
            maxElementsOnDisk="10000000"
            diskPersistent="false"
            diskExpiryThreadIntervalSeconds="120"
            memoryStoreEvictionPolicy="LRU"
    />

</ehcache>

전부 모르겠고 중요해 보이는 

maxElementsInMemory : 수치는 모르겠지만 찾아보니 바이트 단위 인듯 합니다

timeToIdleSeconds : 120초 동안 재요청이 없으면 비우고
timeToLiveSeconds : 120초 동안 살려 놓는다는 뜻인가 보네요

 

xml 설정은 JAVA CONFIG로 변경 해봅니다

import org.ehcache.config.CacheConfiguration;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.ExpiryPolicyBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.config.units.MemoryUnit;
import org.ehcache.jsr107.Eh107Configuration;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.cache.spi.CachingProvider;
import java.time.Duration;

@EnableCaching
@Configuration
public class CacheConfig {
    @Bean
    public CacheManager EhcacheManager() {

        CacheConfiguration<String, Object> cachecConfig = CacheConfigurationBuilder
                .newCacheConfigurationBuilder(String.class,
                        Object.class,
                        ResourcePoolsBuilder.newResourcePoolsBuilder()
                                .offheap(10, MemoryUnit.MB)
                                .build())
                .withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofSeconds(3600)))
                .build();

        CachingProvider cachingProvider = Caching.getCachingProvider();
        CacheManager cacheManager = cachingProvider.getCacheManager();

        javax.cache.configuration.Configuration<String, Object> configuration = Eh107Configuration.fromEhcacheCacheConfiguration(cachecConfig);
        cacheManager.createCache("test", configuration);
        return cacheManager;
    }
}

여기서는 offheap 에 10 메가로 설정했고

재요청이 3600초 동안 없으면 지우는 걸로 설정 했어요 

.withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofSeconds(3600))));

을 추가 하기 전에 이걸로 테스트 해보려 합니다 . 재요청이 계속 있으면 죽이 않는지 ..

 

그리고 나서 케시 이름을 지정하여 생성 해줍니다 

저는 test를 만들어놓고 사용해봅니다

 

위에

이렇게 사용 벨류에 케시 이름으로 만들어논 캐시 영역을 사용 하시면 됩니다.

 

지금까지 테스트는 잘되고 잘 캐싱이 되고 있는데 

문제가 생기면 같은걸로 다시 포스팅 해보겠습니다.

 

그럼 이만

반응형

+ Recent posts