728x90
반응형
public static String format(String format, Object... args);
public static String format(Locale l, String format, Object... args);

String 의 static 메서드인 format 메서드는 문자열의 형식을 설정하는 메서드입니다.

 

  1. %d (10진수 형식)
  2. %s (문자열 형식)
  3. %f (실수형 형식)
  4. Locale 설정
  5. %t (날짜시간 형식)
  6. %c (유니코드 문자 형식)
  7. %o, %x(8진수, 16진수 형식)

1. %d (= Integer Formatting)

10진수 integer의 형식을 설정할 때 이용합니다.

int i = 23;

System.out.println(String.format("%d_", i));
System.out.println(String.format("%5d_", i));
System.out.println(String.format("%-5d_", i));
System.out.println(String.format("%05d_", i));
23_
   23_
23   _
00023_

%5d 와 같이 %와 d 사이에 정수를 설정하면, 글자 길이를 설정할 수 있습니다.
기본적으로 오른쪽 정렬이고, -를 붙일 경우 왼쪽정렬입니다.(ln 4~5)
표현할 숫자인 i의 길이가 5보다 작을 경우 0을 붙입니다.(leading 0s) (ln 6)

※ %d 와 %-5d의 구분을 위해 맨 마지막에 _ 을 포함시켰습니다.

 

int i = 123456789;

System.out.println(String.format("%,d_", i));
System.out.println(String.format("%,15d_", i));
System.out.println(String.format("%,-15d_", i));
System.out.println(String.format("%,015d_", i));
123,456,789_
    123,456,789_
123,456,789    _
0000123,456,789_

% 바로 뒤에 , 를 붙이면 3자리 단위로 쉼표를 찍어줍니다.


2. %s (= String Formatting)

문자열의 형식을 설정할 때 이용합니다.

String str = "tete";

System.out.println(String.format("%s_", str));
System.out.println(String.format("%12s_", str));
System.out.println(String.format("%-12s_", str));
System.out.println(String.format("%.2s_", str));
System.out.println(String.format("%-12.2s_", str));
System.out.println(String.format("%12.2s_", str));
tete_
        tete_
tete        _
te_
te          _
          te_

%s는 문자열을 그대로 출력하고,
%s 앞에 숫자(N)를 설정할 경우, str.length()가 N보다 작을 경우 공백을 추가합니다. (ln 4~5)
- 를 붙일 경우, 왼쪽 정렬. (default는 오른쪽 정렬) (ln 5)
.숫자(N)를 설정할 경우, 최대 N길이 만큼만 출력 (ln 7~8)


3. %f (= Floating point Formatting)

실수형 숫자 형식을 설정할 때 이용합니다.

double n = 123.45678;

System.out.println(3.4);
System.out.println(n);
System.out.println();

System.out.println(String.format("%f_", 3.4));
System.out.println(String.format("%f_", n));
System.out.println(String.format("%.6f_", n));
System.out.println(String.format("%15f_", n));
System.out.println(String.format("%-15f_", n));
System.out.println(String.format("%.3f_", n));
System.out.println(String.format("%.2f_", n));
System.out.println(String.format("%15.2f_", n));
System.out.println(String.format("%-15.2f_", n));
System.out.println(String.format("%015f_", n));
System.out.println(String.format("%015.2f_", n));
3.4
123.45678

3.400000_
123.456780_
123.456780_
     123.456780_
123.456780     _
123.457_
123.46_
         123.46_
123.46         _
00000123.456780_
000000000123.46_

floating point 표현법의 기본 설정인 %f는 %.6f 와 같습니다.(ln 7~9)
%15.2f 는 글자 길이 15, 소수점 아래 2자로 나타내라는 의미로, .도 글자길이에 포함됩니다.(ln 12~15)
소수점 아래는 반올림하여 출력됩니다.
%d와 마찬가지로, 숫자 %뒤의 정수부 앞에 0을 붙이면, 왼쪽에 공백으로 표시될 부분을 0으로 채워줍니다(ln 16~17)


4. Locale 설정

String.format(포맷, 값);
위에서는 String.format 메서드에 2개의 인자값을 넣어 실습을 해보았습니다.

같은 메서드명의 overloading 된 String.format(Locale, 포맷, 값); 메서드를 이용하면 국가별 포맷 설정이 가능합니다.
아래의 예시를 봅시다.

int money = 35000;
Date today = new Date();

System.out.println(String.format("₩ %,d", money));
System.out.println(String.format(Locale.GERMANY, "%,d €", money));
System.out.println(String.format("%tp", today));
System.out.println(String.format(Locale.ENGLISH, "%tp", today));
₩ 35,000
35.000 €
오후
pm

Locale을 설정하지 않을 경우, 기본적으로는 OS에 설정되어있는 값이 default로 적용됩니다.
GERMANY에서 사용하는 금액 단위인 유로화는 3자리 구분자를 .을 이용하기 때문에 3자리수 단위로 .이 표기되며,

 


오전 오후를 표시하는 포맷인 %tp의 경우에도 기본값으로는 '오후'를, Locale.ENGLISH에서는 'pm'을 출력합니다.


5. %t (= DateTime Formatting)
  • y: 연, year
  • M: 월, month
  • d: 일, day of month
  • H: 시, 24-hour
  • h: 시, 12-hour
  • M: 분, minute
  • s: 초, second
Date n = new Date();
System.out.println(n +"\n");
System.out.println(String.format("%%tF(yyyy-MM-dd): %tF", n));
System.out.println(String.format("%%tT(02H:02m:02s): %tT, %%tR(02H:02m): %tR", n, n));
System.out.println(String.format("%%ty(2y): %ty, %%tY(4y): %tY", n, n));		
System.out.println(String.format("%%tm(02M): %tm", n));		
System.out.println(String.format("%%td(02d): %td, %%te(d): %te", n, n));

System.out.println(String.format("%%tH(02H): %tH", n));
System.out.println(String.format("%%tM(02m): %tM", n));
System.out.println(String.format("%%tS(02s): %tS", n));

System.out.println(String.format("%%tZ(time zone): %tZ, %%tz(time zone offset): %tz", n, n));
Thu Mar 05 14:07:09 KST 2020

%tF(yyyy-MM-dd): 2020-03-05
%tT(02H:02m:02s): 14:07:09, %tR(02H:02m): 14:07
%ty(2y): 20, %tY(4y): 2020
%tm(02M): 03
%td(02d): 05, %te(d): 5
%tH(02H): 14
%tM(02m): 07
%tS(02s): 09
%tZ(time zone): KST, %tz(time zone offset): +0900

기본적으로 시간 및 날짜 형식에는 leading-0s를 붙입니다.
가장 많이 이용될 포맷 형식은 %tF %tT로, 날짜와 시각을 연-월-일 시:분:초 로 나타냅니다.
연월일을 yyMMdd 형태로 출력(leading-0s 포함)하고 싶을 때엔 %ty%tm%d
시분초를 HH:mm:ss 형태로 출력(leading-0s 포함)하고 싶을 때엔 %tH%tM%tS

※ 포맷에 %문자를 쓰고 싶다면 %% 와 같이 % 문자를 2번 쓰면 됩니다.

 

++ 더보기

그 밖에 다양한 포맷형식을 제공하는데, 아래를 참고하여 원하는 포맷을 이용하면 됩니다.

System.out.println(String.format("%%tA(day of Week, Full name): %tA, %%ta: %ta", n, n));
System.out.println(String.format("%%tB(month, Full name): %tB, %%tb: %tb", n, n));
System.out.println(String.format(Locale.ENGLISH, "%%tB(month, Full name): %tB, %%tb: %tb", n, n));
System.out.println(String.format("%%tc(= %%ta %%tb %%td %%tT %%tZ %%tY): %tc", n));
System.out.println(String.format("%%tD(MM/dd/yy): %tD", n));
System.out.println(String.format("%%td(02d): %td, %%te(d): %te", n, n));
System.out.println(String.format("%%tF(yyyy-02M-02d): %tF", n));
System.out.println(String.format("%%tH(02H, 00-23): %tH, %%tk(H, 0-23): %tk", n, n));
System.out.println(String.format("%%tI(02h, 01-12): %tI, %%tl(h, 1-12): %tl", n, n));
System.out.println(String.format("%%tj(day of Year, 001-366): %tj", n));
System.out.println(String.format("%%tp(오전 또는 오후): %tp", n));
%tA(day of Week, Full name): 목요일, %ta: 목
%tB(month, Full name): 3월, %tb: 3월
%tB(month, Full name): March, %tb: Mar
%tc(= %ta %tb %td %tT %tZ %tY): 목 3월 05 14:07:09 KST 2020
%tD(MM/dd/yy): 03/05/20
%td(02d): 05, %te(d): 5
%tF(yyyy-02M-02d): 2020-03-05
%tH(02H, 00-23): 14, %tk(H, 0-23): 14
%tI(02h, 01-12): 02, %tl(h, 1-12): 2
%tj(day of Year, 001-366): 065
%tp(오전 또는 오후): 오후

참고로, %tB와 %tb는 한글로는 똑같이 출력되는데 영어에서는 February, Feb 이렇게 표현됩니다.


6. %c (= Unicode char Formatting)

숫자를 유니코드로 변환해줍니다.

System.out.println("Unicode 코드 → 문자");
System.out.println(String.format("48 → %c, 57 → %c", 48, 57));
System.out.println(String.format("65 → %c, 90 → %c", 65, 90));
System.out.println(String.format("97 → %c, 122 → %c", 97, 122));
System.out.println(String.format("44032 → %c, 55203 → %c", 44032, 55203)); //  U+AC00, U+D7A3
Unicode 코드 → 문자
48 → 0, 57 → 9
65 → A, 90 → Z
97 → a, 122 → z
44032 → 가, 55203 → 힣

U+AC00 = (163 * 10)+(162 * 12) = 44032
U+D7A3 = (163 * 13)+(162 * 7)+(16 * 10)+3 = 55203


7. %o, %x (= Octal/Hex Formatting)
int n = 100;
System.out.println(String.format("10진수(%d) : 2진수(%s), 8진수(%o), 16진수(%x)", n, Integer.toBinaryString(n), n, n));
10진수(100) : 2진수(1100100), 8진수(144), 16진수(64)

1100100(2) = 1/100/100 → 1/22/22 = 144(8)
1100100(2) = 110/0100 → 22+21/22 = 64(16)

728x90
반응형
728x90
반응형

parameter에 대한 적절한 설정은 좋은 performance를 내는 데 중요한 요인이 된다. 

 

이들 parameter에 대한 자세한 이해를 통해 효과적인 lob segment를 생성할 수 있다. 

참고로 lob에 대한 자세한 정보를 보려면 dba/all/user_lobs를 조회하면 알 수 있다.

 

/***************************************************************************************************** 

    (column list)
    [physical attributes]
    [storage clause]
    [LOB (<lobcol1> [, <lobcol2>...])
        STORE AS
            [<lob_segment_name>]
            (
                [TABLESPACE <tablespace_name>]
                [{ENABLE | DISABLE} STORAGE IN ROW]
                [CHUNK <chunk_size>]
                [PCTVERSION <version_number>]
                [ { CACHE | NO CACHE [{LOGGING | NOLOGGING}]
                          | CACHE READS [{LOGGING | NOLOGGING}]
                  }
                ]
                [<storage_clause_for_LOB_segment>]
                [INDEX [<lob_index_name>] [physical attributes] [<storage_for_LOB_index>] ]
            )
    ]
    [LOB (<lobcol1> [, <lobcol2>...]) ... ]

*****************************************************************************************************/

 

CREATE TABLE demolob ( A NUMBER, B CLOB )
    STORAGE (INITIAL 256 NEXT 256) 
    TABLESPACE user_data 
    LOB(b) STORE AS demolob_seg ( 
        TABLESPACE lob_tb
        STORAGE (INITIAL 6144 NEXT 6144) 
        CHUNK 4
        PCTVERSION 20
        NOCACHE LOGGING
        ENABLE STORAGE IN ROW 
        INDEX demolob_idx (
            TABLESPACE lob_tb
            STORAGE ( INITIAL 256 NEXT 256 ) 
            ) 
        );
 


1) TABLESPACE와 storage parameter

- lob, lob index에 대한 tablespace를 지정하지 않는 경우, 해당 table이 저장되는 tablespace에 같이 저장되게 된다. 

  lob 컬럼, lob index, table 에 대해 tablespace를 각기 지정하는 것이 contention을 줄일 수 있어 보다 효과적이다. (최소한      lob 컬럼과 다른 컬럼들을 구분하여 별개의 tablespace에 저장하도록 지정하는 것이 바람직하다.)

- lob index는 lob 컬럼의 내부적 저장 위치를 연결시켜주는 indicator를 저장한 index이다. 

  default로 제공받는 index명은 이해하기 어렵기 때문에 lob index명을 지정하여 사용하는 것이 편하다.

- lob index에 대한 parameter 변경은 alter index문을 이용하지 않고, alter table문을 이용하여야 한다. 단, index명을 바꿀 

  수는 없다.


2) PCTVERSION


- 데이타를 변경할때는 read consistency를 위해 undo 정보를 저장할 필요가 있다. 

  그러나 LOB 데이타인 경우, 그 크기가 크기때문에 undo 정보 유지하기에는 많은 어려움이 따르기 때문에, 

  대신에 old version 데이타를 유지하는 방법으로 read consistency를 제공하고 있다. 

  pctversion은 old version lob data가 차지하는 percentage를 의미한다. 

 

  예를들어 default value가(10) 적용되었다면, 새로운 lob data가 old version의 10%가 저장될때 까지는 old version을 간직  

  하고 있다가, 이 이상 크기가 되면 바로 old version data를 reclaim하고, 이 space를 재사용 즉, overwrite 하게 된다. 

- pctversion을 큰 값을 지정한 경우, old version을 저장하기 위해 보다 많은 space가 필요하게 된다. 

  하지만 update가 많은 작업인 경우에는 이 값을 높게 잡아 다음과 같은 에러를 피할 수 있을 것이다.

    ORA-01555: snapshot too old: rollback segment number with name "" too small
    ORA-22924: snapshot too old

- 만약 lob data가 read-only인 경우라면, pctversion은 0으로 설정할 수 있다.
  * 읽기 요청이 많으면서 동시에 LOB 변경(20% 이상)
  * 읽기 요청이 대부분이며 변경이 거의 없음(5% 이하)


- pctversion 변경 

SQL> ALTER TABLE demolob MODIFY LOB(b) (PCTVERSION 10);

3) CACHE/NOCACHE

- 자주 access되는 경우라면, cache를 선택하여 사용한다. default는 nocache이다.

- in-line lob은 영향을 받지 않는다. 즉, in-line lob은 다른 데이타와 마찬가지로 buffer cache에서 바로 읽혀지기 때문이다.

- CACHE_SIZE_THRESHOLD limit이 적용되지 않기 때문에 cache할 때는 주의해야 한다. 

- cache/nocache 변경 
    

SQL> ALTER TABLE demolob MODIFY LOB (b) ( CACHE/NOCACHE );

4) CHUNK(디폴트 block size)


- lob data를 access하는 단위로써, db_block_size의 배수로 설정한다.
  lob 데이타가 저장될 initial extent, next extent는 chunk의 배수로 설정하는 것이 좋다. 

  만약 db_block_buffer가 2K이고, chunk를 3K로 설정했다면 chunk는 4K로 조정 되어 적용된다.


- chunk는 in-line lob에는 영향을 주지 않고, out-line lob에만 영향을 준다. 

  예를들어 chuk를 32K로 설정하고, disable storage in row를 설정했다면 1K의 데이타를 저장할때도 32K가 lob segment에 

  할당된다. 


- lob table이 생성된 이후에는 변경할 수 없다. 


5) LOGGING/NO LOGGING


- redo 정보를 생성할 것인지 여부를 결정하는 parameter이다. 


- cache option을 사용하는 경우는 무조건 logging을 의미한다.


- logging, nologging에 상관 없이 undo 정보는 lob index에 대해서만 생성되고고, lob 데이타에 대해서는 생성하지 않는다.


- logging인 경우는 redo 정보를 생성하고, bulk load나 대량의 insert를 하는 경우 nologging을 설정하여 redo 정보를 생성 

  하지 않도록 할 수 있다.


- logging/no logging 변경
    

SQL> ALTER TABLE demolob MODIFY LOB(b) (NOCACHE NOLOGGING);

6) ENABLE/DISABLE STORAGE IN ROW

- 4k 이하의 data를 in-line에 저장할 지 여부를 결정한다.


- enable인 경우 (default)

  4k 이하의 lob은 in-line으로, 즉 테이블에 저장하고, 4k 보다 큰 경우에는 out-line 즉, lob segment에 저장된다. 

  이때 4K는 control 정보를 포함한 크기로써, 실제 in-line으로 저장할 수 있는 최대 크기는 3964 byte이다. 

  4K 이상의 데이타는 lob segment에 저장되지만, 36 - 84 bytes의 information 정보는 in-line에 남게 된다. 


- disable인 경우
  모든 datas는 out-line으로 저장된다. 20 byte lob locator만 in-line으로 저장되어 lob index에서 해당 lob block을 찾을 수
  있도록 해준다.


- in-line lob인 경우에는 다른 데이타 타입처럼 REDO, UNDO 정보가 기록된다. 그러나 out-line인 경우에는 column locator

  와 LOB INDEX가 변경되는 경우에만 UNDO 정보를 기록한다. 즉, lob segment에 대해서는 undo 정보를 만들지 않는다.


- lob 컬럼에 대한 access가 많지 않은 경우는 disable을 설정하는 것이 바람직하다. High Water Mark를 작게 유지될 수 있기    때문에 특히, full table scan을 자주 하는 table인 경우 유용하다.


- lob table이 생성된 이후에는 변경할 수 없다.

 

 

 

 

<LOB 생성시 주의사항>


LOB은 데이터의 속성상, 다른 데이터타입에는 없는 다양한 옵션들이 존재한다. 

부주의하게 사용할 경우 많은 성능문제를 야기할 수 있다. LOB 생성시 다음과 같은 사항들에 주의해야 한다.

 

1. enable storage in row 옵션을 사용하는 경우 4000 bytes 보다 작은 LOB 데이터는 로우와 같은 블록에 저장된다. 
    따라서 Row chaining을 유발할 가능성이 높다. 
    4000 bytes보다 큰 LOB 데이터는 disable storage in row 옵션을 사용한 것과 같은 효과가 있다. 
    LOB 데이터의 크기를 고려하여 만일 Row chaining이 발생할 가능성이 높다고 판단되면 disable storage in row 옵션을 

    사용하는 것이 좋다. 

    로우와 같은 블록에 저장되는 LOB 데이터를 In-line LOB이라고 부르며, LOB 세그먼트에 저장되는 경우에는 Out-of-line 

    LOB이라고 부다.

 

2. disable storage in row 옵션을 사용하는 경우 LOB 데이터는 별도의 LOB 세그먼트에 저장되며 Row 내에는 20 bytes의 
    LOB Locator 정보만 저장된다. 이 경우 언두 데이터는 LOB Locator에 대해서만 생성된다. LOB 데이터에 대한 실제적인 
    언두 정보는 언두 테이블스페이스에 저장되지 않고 같은 LOB 세그먼트 내에 저장됨에 유의해야 한다. 

    LOB 세그먼트의 언두는 PCTVERSION 옵션에 의해 제어되는데, 가령 PCTVERSION이 50이면 LOB Segment의 공간 중          50%가 언두 정보를 저장하는데 사용된다. 

    따라서 PCTVERSION을 낮게 주는 경우 예기치 않은 ORA-01555 : snapshot too old 에러가 생길 수 있다. 
    LOB 세그먼트의 언두 영역 부족에 따른 snapshot too old 에러의 경우 rollback segment name이 공백으로 나온다. 
    PCTVERSION이 너무 작으면 ORA-01555 에러가 발생할 확률이 높아지고, PTCVERSION이 너무 높으면 공간의 낭비가 

    심해진다. 
    이 문제를 해결하는 방법은 일반 롤백 세그먼트에서의 ORA-01555 에러의 경우와 동일하다. 

    PCTVERSION을 적절히 유지하고 불필요한 커밋을 줄인다. LOB 세그먼트의 확장공간을 적절히 확보해주는 것도 중요하다.

 

3. CACHE / NOCACHE, LOGGING / NOLOGGING : 만일 LOB 데이터의 크기가 크고 자주 액세스되지 않거나, 무작위적으로 
    액세스된다면 NOCACHE 옵션을 사용하는 것이 바람직하다. NOCACHE 옵션을 사용하는 경우 리두를 생성할 지의 여부도      지정할 수 있다. 

    만일 반드시 복구가 될 필요가 없는 데이터라면 Nologging 옵션을 사용함으로써 성능개선효과를 얻을 수 있다. CACHE 

    옵션을 사용하는 경우에는 반드시 Logging 속성을 지니게 된다. NOCACHE 옵션을 사용하는 경우 버퍼 캐시를 경유하지

    않기 때문에 direct path read(lob), direct path write(lob) 이벤트를 대기하게 된다. 하지만 CACHE 옵션을 사용하는 경우에

    는 버퍼 캐시를 경유하기 때문에 db file sequential read 와 latch: cache buffers chains 대기를 유발할 수 있다.

 

4. 청크(chunk)의 크기 : Out-of-line LOB인 경우 청크 단위로 LOB 데이터를 저장하게 된다. 

    큰 청크 사이즈의 문제는 공간이 낭비될 가능성이 높다는 것이다. 만일 청크가 8K로 되어있는데, 1K 크기의 LOB 데이터가      삽입된다면 나머지 7K의 공간은 사용할 수 없게 된다. 따라서 청크 사이즈를 결정할 때는 대상이 되는 LOB 데이터의 크기      를 고려해서 결정해야 다. 

    청크의 크기 단위는 기본적으로 블록 사이즈의 배수가 된다. 

    만일 블록 사이즈가 8K인 경우 청크의 크기를 5000 bytes로 주면 오라클은 암묵적으로 8192 bytes로 변환한다. 청크의 

    크기를 지나치게 작게 하는 경우에는 연속적으로 청크를 할당받는 과정에서 오버헤드가 발생하게 된다.

728x90
반응형
728x90
반응형

Chunk 지향 프로그래밍

Chunk란 아이템이 트랜잭션에 commit되는 수를 말한다.
즉, 청크 지향 처리란 한 번에 하나씩 데이터를 읽어 Chunk라는 덩어리를 만든 뒤, Chunk 단위로 트랜잭션을 다루는 것을 의미한다. Chunk 단위로 트랜잭션을 수행하기 때문에, 수행이 실패한 경우 해당 Chunk 만큼만 롤백이 되고, 이전에 커밋된 트랜잭션 범위까지는 반영된다는 것을 의미한다.
Chunk 지향 프로세싱은 1000개의 데이터에 대해 배치 로직을 실행한다고 가정하면, Chunk 단위로 나누지 않았을 경우에는 한개만 실패해도 성공한 999개의 데이터가 롤백된다. Chunk 단위를 10으로 한다면, 작업 중에 다른 Chunk는 영향을 받지 않는다.
 
여기선 Reader, Processor에서는 1건씩 다뤄지고, Writer에서는 Chunk 단위로 처리된다는 것을 기억하면 된다.
ChunkOrientedTasklet
 
public class ChunkOrientedTasklet<I> implements Tasklet {
 
private static final String INPUTS_KEY = "INPUTS";
 
private final ChunkProcessor<I> chunkProcessor;
 
private final ChunkProvider<I> chunkProvider;
 
private boolean buffering = true;
 
private static Log logger = LogFactory.getLog(ChunkOrientedTasklet.class);
 
 
public ChunkOrientedTasklet(ChunkProvider<I> chunkProvider, ChunkProcessor<I> chunkProcessor) {
 
this.chunkProvider = chunkProvider;
 
this.chunkProcessor = chunkProcessor;
 
}
 
 
public void setBuffering(boolean buffering) {
 
this.buffering = buffering;
 
}
 
 
@Nullable
 
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
 
Chunk<I> inputs = (Chunk)chunkContext.getAttribute("INPUTS");
 
if (inputs == null) {
 
// Reader에서 데이터 가져오기
 
inputs = this.chunkProvider.provide(contribution);
 
if (this.buffering) {
 
chunkContext.setAttribute("INPUTS", inputs);
 
}
 
}
 
 
 
this.chunkProcessor.process(contribution, inputs); // Processor & Writer 처리
 
this.chunkProvider.postProcess(contribution, inputs);
 
if (inputs.isBusy()) {
 
logger.debug("Inputs still busy");
 
return RepeatStatus.CONTINUABLE;
 
} else {
 
chunkContext.removeAttribute("INPUTS");
 
chunkContext.setComplete();
 
if (logger.isDebugEnabled()) {
 
logger.debug("Inputs not busy, ended: " + inputs.isEnd());
 
}
 
 
return RepeatStatus.continueIf(!inputs.isEnd());
 
}
 
}
 
}
ChunkOrientedTasklet에서 Chunk 단위로 작업하기 위한 코드는 execute()에 있다.
 
SimpleChunkProcessor
ChunkProcessor 는 Processor와 Writer의 로직을 구현하고 있다.
 
public interface ChunkProcessor<I> {
 
void process(StepContribution var1, Chunk<I> var2) throws Exception;
 
}
ChunkProcessor는 인터페이스이기 때문에 실제 구현체가 있어야하며, 기본적으로 SimpleChunkProcessor가 사용된다.
 
//
 
// Source code recreated from a .class file by IntelliJ IDEA
 
// (powered by FernFlower decompiler)
 
//
 
 
package org.springframework.batch.core.step.item;
 
 
import io.micrometer.core.instrument.Tag;
 
import io.micrometer.core.instrument.Timer.Sample;
 
import java.util.Iterator;
 
import java.util.List;
 
import org.springframework.batch.core.StepContribution;
 
import org.springframework.batch.core.StepExecution;
 
import org.springframework.batch.core.StepListener;
 
import org.springframework.batch.core.listener.MulticasterBatchListener;
 
import org.springframework.batch.core.metrics.BatchMetrics;
 
import org.springframework.batch.core.step.item.Chunk.ChunkIterator;
 
import org.springframework.batch.item.ItemProcessor;
 
import org.springframework.batch.item.ItemWriter;
 
import org.springframework.beans.factory.InitializingBean;
 
import org.springframework.lang.Nullable;
 
import org.springframework.util.Assert;
 
 
public class SimpleChunkProcessor<I, O> implements ChunkProcessor<I>, InitializingBean {
 
private ItemProcessor<? super I, ? extends O> itemProcessor;
 
private ItemWriter<? super O> itemWriter;
 
private final MulticasterBatchListener<I, O> listener;
 
 
// ...
 
protected final O doProcess(I item) throws Exception {
 
if (this.itemProcessor == null) {
 
return item;
 
} else {
 
try {
 
this.listener.beforeProcess(item);
 
// ItemProcessor의 process()로 가공
 
O result = this.itemProcessor.process(item);
 
this.listener.afterProcess(item, result);
 
return result;
 
} catch (Exception var3) {
 
this.listener.onProcessError(item, var3);
 
throw var3;
 
}
 
}
 
}
 
 
protected final void doWrite(List<O> items) throws Exception {
 
if (this.itemWriter != null) {
 
try {
 
this.listener.beforeWrite(items);
 
// 가공된 데이터들을 doWirte()로 일괄 처리
 
this.writeItems(items);
 
this.doAfterWrite(items);
 
} catch (Exception var3) {
 
this.doOnWriteError(var3, items);
 
throw var3;
 
}
 
}
 
}
 
 
public final void process(StepContribution contribution, Chunk<I> inputs) throws Exception {
 
this.initializeUserData(inputs);
 
if (!this.isComplete(inputs)) {
 
// inputs는 이전에 `chunkProvider.privide()에서 받은 ChunkSize만큼 쌓인 item
 
Chunk<O> outputs = this.transform(contribution, inputs);
 
contribution.incrementFilterCount(this.getFilterCount(inputs, outputs));
 
// transform()을 통해 가공된 대량 데이터는 write()를 통해 일괄 저장된다.
 
// 이때 wirte()는 저장이 될 수도 있고, API 전송이 될 수도 있다. (ItemWriter 구현방식에 따라 다름)
 
this.write(contribution, inputs, this.getAdjustedOutputs(inputs, outputs));
 
}
 
}
 
 
// ...
 
 
protected void write(StepContribution contribution, Chunk<I> inputs, Chunk<O> outputs) throws Exception {
 
Sample sample = BatchMetrics.createTimerSample();
 
String status = "SUCCESS";
 
 
try {
 
this.doWrite(outputs.getItems());
 
} catch (Exception var10) {
 
inputs.clear();
 
status = "FAILURE";
 
throw var10;
 
} finally {
 
this.stopTimer(sample, contribution.getStepExecution(), "chunk.write", status, "Chunk writing");
 
}
 
 
contribution.incrementWriteCount(outputs.size());
 
}
 
 
// 전달받은 input을 doProcess()로 전달하고 변환 값을 받는다.
 
protected Chunk<O> transform(StepContribution contribution, Chunk<I> inputs) throws Exception {
 
Chunk<O> outputs = new Chunk();
 
ChunkIterator iterator = inputs.iterator();
 
 
while(iterator.hasNext()) {
 
I item = iterator.next();
 
Sample sample = BatchMetrics.createTimerSample();
 
String status = "SUCCESS";
 
 
Object output;
 
try {
 
output = this.doProcess(item);
 
} catch (Exception var13) {
 
inputs.clear();
 
status = "FAILURE";
 
throw var13;
 
} finally {
 
this.stopTimer(sample, contribution.getStepExecution(), "item.process", status, "Item processing");
 
}
 
 
if (output != null) {
 
outputs.add(output);
 
} else {
 
iterator.remove();
 
}
 
}
 
 
return outputs;
 
}
 
 
}
여기서 Chunk 단위 처리를 담당하는 핵심 로직은 process()에 있다.
Page Size vs Chunk Size
Chunk Size는 한번에 처리될 트랜잭션 단위를 의미하며, Page Size는 한번에 조회할 Item의 양을 의미한다.
AbstractPagingItemReader
 
@Nullable
 
protected T doRead() throws Exception {
 
synchronized(this.lock) {
 
 
if (this.results == null || this.current >= this.pageSize) {
 
if (this.logger.isDebugEnabled()) {
 
this.logger.debug("Reading page " + this.getPage());
 
}
 
 
this.doReadPage();
 
++this.page;
 
if (this.current >= this.pageSize) {
 
this.current = 0;
 
}
 
}
 
 
int next = this.current++;
 
return next < this.results.size() ? this.results.get(next) : null;
 
}
 
}
현재 읽어올 데이터가 없거나, pageSize를 초과한 경우 doReadPage()를 호출하는 것을 볼 수 있다. 즉, Page 단위로 끊어서 호출하는 것을 볼 수 있다.
Page Size와 Chunk Size를 다르게 설정하는 경우의 예를 들어보자. 만약 PageSize가 10, Chunk Size가 50이라면, ItemReader에서 Page조회가 5번 일어나면 1번의 트랜잭션이 발생하여, Chunk가 처리될 것이다.
한번의 트랜잭션의 처리를 위해 5번의 쿼리 조회가 발생하는 것은 성능샹 이슈가 발생할 수 있다. Spring Batch에서는 다음과 같이 설명이 되어있다.
Setting a fairly large page size and using a commit interval that matches the page size should provide better performance. (상당히 큰 페이지 크기를 설정하고 페이지 크기와 일치하는 커미트 간격을 사용하면 성능이 향상됩니다.)
추가적으로 JPA 사용시에도 lazeException 오류가 발생할 수 있다.
 
org.hibernate.LazyInitializationException: failed to lazily initialize a collection of role: com.blogcode.example3.domain.PurchaseOrder.productList, could not initialize proxy - no Session
 
public abstract class AbstractPagingItemReader<T> extends AbstractItemCountingItemStreamItemReader<T> implements InitializingBean {
 
protected Log logger = LogFactory.getLog(this.getClass());
 
private volatile boolean initialized = false;
 
private int pageSize = 10;
AbstractPagingItemReader를 보면 pageSize의 default 크기는 10인 것을 확인할 수 있다.
 
protected void doReadPage() {
 
EntityTransaction tx = null;
 
if (this.transacted) {
 
tx = this.entityManager.getTransaction();
 
tx.begin();
 
this.entityManager.flush();
 
this.entityManager.clear();
 
}
 
 
Query query = this.createQuery().setFirstResult(this.getPage() * this.getPageSize()).setMaxResults(this.getPageSize());
 
if (this.parameterValues != null) {
 
Iterator var3 = this.parameterValues.entrySet().iterator();
 
 
while(var3.hasNext()) {
 
Entry<String, Object> me = (Entry)var3.next();
 
query.setParameter((String)me.getKey(), me.getValue());
 
}
 
}
 
 
if (this.results == null) {
 
this.results = new CopyOnWriteArrayList();
 
} else {
 
this.results.clear();
 
}
 
 
if (!this.transacted) {
 
List<T> queryResult = query.getResultList();
 
Iterator var7 = queryResult.iterator();
 
 
while(var7.hasNext()) {
 
T entity = var7.next();
 
this.entityManager.detach(entity);
 
this.results.add(entity);
 
}
 
} else {
 
this.results.addAll(query.getResultList());
 
tx.commit();
 
}
 
 
}
그리고 JpaPagingItemReaderdoReadPage()를 보면 this.entityManager.flush(), this.entityManager.clear()로 이전 트랜잭션을 초기화 시키기때문에 만약 Chunk Size가 100, Page Size가 10이라면 마지막 조회를 제외한 9번의 조회결과들의 트랜잭션 세션이 전부 종료되어 오류가 발생하는 것을 볼 수 있다.
이 문제 또한, Page Size와 Chunk Size를 일치시키면 해결되는 것을 볼 수 있다.
2개의 값이 의미하는 바는 다르지만, 여러 이슈로 2개 값을 일치 시키는 것이 보편적으로 좋은 방법이며, 2개 값을 일치 시키는 것을 권장한다.
728x90
반응형

+ Recent posts