private String jsonString = 

"[\n" +

            "        {\n" +

            "                \"id\": \"c200\",\n" +

            "                \"name\": \"Ravi Tamada\",\n" +

            "                \"email\": \"ravi@gmail.com\",\n" +

            "                \"address\": \"xx-xx-xxxx,x - street, x - country\",\n" +

            "                \"gender\" : \"male\",\n" +

            "                \"phone\": {\n" +

            "                    \"mobile\": \"+91 0000000000\",\n" +

            "                    \"home\": \"00 000000\",\n" +

            "                    \"office\": \"00 000000\"\n" +

            "                }\n" +

            "        },\n" +

            "        {\n" +

            "                \"id\": \"c201\",\n" +

            "                \"name\": \"Johnny Depp\",\n" +

            "                \"email\": \"johnny_depp@gmail.com\",\n" +

            "                \"address\": \"xx-xx-xxxx,x - street, x - country\",\n" +

            "                \"gender\" : \"male\",\n" +

            "                \"phone\": {\n" +

            "                    \"mobile\": \"+91 0000000000\",\n" +

            "                    \"home\": \"00 000000\",\n" +

            "                    \"office\": \"00 000000\"\n" +

            "                }\n" +

            "        }\n" +

"    ]";




public class ContactModel {

     public String id;

     public String name;

     public String email;

}




// Top of file

import java.lang.reflect.Type;


// ...


private void parseJSON() {

    Gson gson = new Gson();

    Type type = new TypeToken<List<ContactModel>>(){}.getType();

    List<ContactModel> contactList = gson.fromJson(jsonString, type);

    for (ContactModel contact : contactList){

        Log.i("Contact Details", contact.id + "-" + contact.name + "-" + contact.email);

    }

}


출처:https://stackoverflow.com/questions/17037340/converting-jsonarray-to-arraylist

Exception in thread "main" java.lang.IllegalAccessError: tried to access method org.apache.poi.util.POILogger.log(ILjava/lang/Object;)V from class org.apache.poi.openxml4j.opc.PackageRelationshipCollection
    at org.apache.poi.openxml4j.opc.PackageRelationshipCollection.parseRelationshipsPart(PackageRelationshipCollection.java:313)
    at org.apache.poi.openxml4j.opc.PackageRelationshipCollection.<init>(PackageRelationshipCollection.java:163)
    at org.apache.poi.openxml4j.opc.PackageRelationshipCollection.<init>(PackageRelationshipCollection.java:131)
    at org.apache.poi.openxml4j.opc.PackagePart.loadRelationships(PackagePart.java:561)
    at org.apache.poi.openxml4j.opc.PackagePart.<init>(PackagePart.java:109)
    at org.apache.poi.openxml4j.opc.PackagePart.<init>(PackagePart.java:80)
    at org.apache.poi.openxml4j.opc.PackagePart.<init>(PackagePart.java:125)
    at org.apache.poi.openxml4j.opc.ZipPackagePart.<init>(ZipPackagePart.java:78)
    at org.apache.poi.openxml4j.opc.ZipPackage.getPartsImpl(ZipPackage.java:243)
    at org.apache.poi.openxml4j.opc.OPCPackage.getParts(OPCPackage.java:684)
    at org.apache.poi.openxml4j.opc.OPCPackage.open(OPCPackage.java:275)
    at org.apache.poi.util.PackageHelper.open(PackageHelper.java:37)
    at org.apache.poi.xssf.usermodel.XSSFWorkbook.<init>(XSSFWorkbook.java:266)


Excel Download 해당 오류 경우

Dependency poi version을 맞춰주면 에러가 해결된다.



<properties>

<poi.version>3.13</poi.version>

</properties>


<dependencies>

<dependency>

<groupId>org.apache.poi</groupId>

<artifactId>poi</artifactId>

<version>${poi.version}</version>

</dependency>

<dependency>

<groupId>org.apache.poi</groupId>

<artifactId>poi-scratchpad</artifactId>

<version>${poi.version}</version>

</dependency>

<dependency>

<groupId>org.apache.poi</groupId>

<artifactId>poi-ooxml</artifactId>

<version>${poi.version}</version>

</dependency>

<!-- etc as needed -->

...

</dependencies>

JSON 파일에 RAW성 데이터를 담아 두고,(참고로 데이터는 가짜 데이터입니다.)

이 데이터들을 조작(필터링과 그룹핑)하여 차트에 그려준 후(column, line, pie차트), 테이블에 출력하는 예제입니다.

차트는 HighChart 라이브러리를 사용하였고, 

IE 8이상에서는 html파일을 실행하면 바로 출력되지만 크롬에서는 AJAX 통신을 하므로 압축을 푼 후 웹서버에 올려서 실행하면 됩니다.

자세한 내용은 소스코드의 주석을 참고하시기 바랍니다.


highcharts_hw.zip



출처: http://egloos.zum.com/tit99hds/v/3441566

자바 Split 함수 사용시에 "|" 로 자르는 경우 

엉뚱하게 잘리는 현상이 있다.


"\\|" 로 자르면 제대로 잘려진다.

SELECT 

DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01')

, DATE_FORMAT(CURDATE(), '%Y-%m-%d')

FROM DUAL



// 비교

, CASE WHEN

DATE_FORMAT(B.DEL_DATE, '%Y-%m-01') <= DATE_FORMAT(CURDATE(), '%Y-%m-%d') AND DATE_FORMAT(DATE_ADD(B.DEL_DATE, INTERVAL 1 MONTH), '%Y-%m-01') >= DATE_FORMAT(CURDATE(), '%Y-%m-%d') THEN 'Y'

ELSE 'N'

MyBatis 사용 시에 param이 1개 혹은 여러개 혹은 전부 사용 시

prefix에 AND 혹은 , 항목이 필요 할 때 

trim 구문을 사용하면 유용하다




<update id="updateTable "  parameterType="map">

UPDATE TABLE

<trim prefix="SET" prefixOverrides=",">

<if test="aFlag!= null and aFlag!= ''.toString()">

, A_FLAG = #{aFlag}

</if>

<if test="bFlag!= null and bFlag!= ''.toString()">

, B_FLAG = #{bFlag}

</if>

<if test="cFlag!= null and cFlag!= ''.toString()">

, C_FLAG = #{cFlag}

</if>

</trim>

WHERE KEY = #{key}

</update>

json 파싱하기


이런 json 포맷을 이해할 수 있는 자료형으로 파싱하기 위한 다양한 라이브러리들이 있습니다.


기본적으로 java의 JsonObject와 JsonArray로 json을 파싱할 수 있습니다.


{

    "name": "hello!",

    "data": {

        "name": "jspiner",

        "age": 8,

        "birth": 1996

    },

    "friends": [

        "john",

        "smith",

        "sam"

    ],

    "books": [

        {

            "name": "book1",

            "price": 10000

        },

        {

            "name": "book2",

            "price": 15000

        },

        {

            "name": "book3",

            "price": 7000

        }

    ]

}




JsonParser jsonParser = new JsonParser();


JsonObject jsonObject = (JsonObject) jsonParser.parse(json);

JsonObject dataObject = (JsonObject) jsonObject.get("data");


System.out.print("name : " + dataObject.get("name"));

System.out.print("age : " + dataObject.get("age"));

System.out.print("birth : " + dataObject.get("birth"));







직렬화(Serialization) 이용하기


직렬화란 객체를 직렬화하여 전송 가능한 형태로 만드는것을 의미한다. 객체들의 데이터를 특정한 포맷의 연속적인 데이터로 변형하여 데이터를 읽도록 하는 것이다.


java에서는 직렬화를 지원하는 Gson Jackson등의 라이브러리가 있다.


JsonObject로 파싱하던 위 코드를 Gson으로 파싱하기 위해선 우선 객체를 담을 클래스를 구현한다.




class DataJson {


    @SerializedName("name")

    public String name;


    @SerializedName("data")

    public Data data;


    @SerializedName("books")

    public List<Book> books;


    class Data{

        @SerializedName("name")

        public String name;


        @SerializedName("age")

        public int age;


        @SerializedName("birth")

        public int birth;

    }


    class Book{

        @SerializedName("name")

        public String name;


        @SerializedName("price")

        public int price;

    }


}

// @SerializedName은 파싱할때 사용될 key 이름이다.




DataJson dataJson= new Gson().fromJson(json, DataJson.class);


System.out.println("name : " + dataJson.name);


for(DataJson.Book book : dataJson.books){

    System.out.println("book name : " + book.name);

    System.out.println("book price : " + book.price);

}



출처:https://calyfactory.github.io/%EC%A0%9C%EC%9D%B4%EC%8A%A8%ED%8C%8C%EC%8B%B1/

권장사항이다. 이것을 이해하면 당신의 어플리케이션이 더 나은 성능을 발휘할 것이다.

다만 이것이 사람의 실력을 판단하는 척도로 사용되서는 안 될 것이다.

 

작게 생각하기

- 조만간 규모가 커질거라면 MySQL ecosystem을 봐야된다.
- 그리고 캐싱 빡시게 안 하는 메이저 웹사이트는 없다.
- develooper.com의 Hansen PT랑 Ilia 튜토리얼 볼 것
- 처음부터 확장 가능하게 아키텍처 잘 쪼개놔야된다.
- 복제랑 파티셔닝 어떻게 할지 미리 계획 세워놔라.
- 파일 기반 세션 좀 쓰지마 -_-
- 그렇다고 너무 쓸데없이 크게 생각하지도 말 것
- 특히 성능하고 확장성 구분 못 하면 난감함

 

EXPLAIN 안 써보기

- SELECT 앞에 EXPLAIN 이라고 붙이기만 하면 되는 것을 (..)
- 실행 계획 확인
- 타입 컬럼에 index 써있는거랑 Extra 컬럼에 index 써있는거랑 “매우 큰” 차이 있음
* 타입에 있으면 Full 인덱스 스캔 (안 좋다.)
* Extra 컬럼에 있으면 Covering 인덱스 찾았다는 의미임 (좋다!)
- 5.0 이후부터는 index_merge 최적화도 한다.

 

잘못된 데이터 타입 선택

- 한 메모리 블럭 단위에 인덱스 레코드가 많이 들어갈수록 쿼리가 빨리 실행될 것이다. (중요)
- 아.. 정규화 좀 해 -_-… (이거 정말 충격과 공포인 듯)
- 가장 작은 데이터 타입을 써.. (진짜 BIGINT가 필요하냐고..)
- 인덱스 걸리는 필드는 정말 최소한으로 데이터 크기를 써야된다고.
- IP는 INT UNSIGNED로 저장해!! (아주 공감)
* 이럴 때 쓰라고 INET_ATON 함수가 아예 내장되어 있음.

 

PHP에서 pconnect 쓰는 짓

- 아파치에서 좀비 프로세스라도 생기면 그 커넥션은 그냥 증발하는거야..
- 어차피 MySQL 접속 속도는 Oracle이나 PostgreSQL 보다 10~100배 빠르다고.

너무 과도한 DB 추상화 계층을 두는 것
- 어디 포팅 열심히 할 거 아니면 추상화 계층 쓰지마 (ADODB, MDB2, PearDB 등)
- scale out 가능한걸 쓰라고.

 

스토리지 엔진 이해 못 하는 것

- 단일 엔진만으로 전체 아키텍처를 결정했다면 대부분 최적이 아님
- 엔진 별 장단점을 공부할 것
- ARCHIVE : zlib으로 압축해주고 UPDATE 안 되고 로그 Bulk Insert에 유용함.
- MEMORY : 서버 재시작하면 증발. 인덱스가 HASH나 BTREE로 가능함. 임시, 요약 데이터에 사용.
* 주간 top X 테이블 같은 것.
* 하여튼 메모리에 박아넣고 싶은 데이터 있으면..

 

인덱스 레이아웃 이해 못 하는 것

- 제대로 인덱스랑 스토리지 엔진 선택하려면 공부 좀 해
- 엔진은 데이터와 인덱스 레코드를 메모리나 디스크에 레이아웃하는 걸 구현한 것
- clustered 구성은 데이터를 PK 순서에 따라 저장함.
- non-clustered 구성은 인덱스만 순서대로 저장하고 데이터는 순서 가정하지 않음.
- clustered에서는 인덱스만 타면 추가적인 조회 없이 바로 데이터 가져오는 것임.
- 그래서 clustered PK는 작은 놈으로 할 필요가 있다는거
* 다른 인덱스는 각 레코드마다 PK를 앞에 더 붙이게 되니까.
* PK 지정 안 하면 아무렇게나 해버림

 

쿼리 캐시 이해 못 하는 것

- 어플리케이션 read/write 비율은 알고 있어야지
- 쿼리 캐시 설계는 CPU 사용과 읽기 성능 간의 타협
- 쿼리 캐시 크기를 늘린다고 읽기 성능이 좋아지는게 아님. heavy read라도 마찬가지.
- 과도한 CPU 사용을 막기 위해 무효화 할 때는 캐시 항목들을 뭉텅이로 날려버림
- 한마디로 SELECT가 참조하는 테이블 데이터 하나라도 변경되면 그 테이블 캐시는 다 날라간다는 얘기임
- 수직 테이블 파티셔닝으로 처방
* Product와 ProductCount를 쪼갠다든지..
* 자주 변하는 것과 변하지 않는 것을 쪼개는게 중요하다 이 말임.

 

Stored Procedure를 쓰는 것

- 무조건 쓰면 안 된다는게 아니고..
- 컴파일 할 때 무슨 일이 일어나는지 이해 못 하고 쓰면 재앙이 된다 이 말.
- 다른 RDBMS랑 다르게 connection thread에서 실행 계획이 세워짐.
- 이게 뭔 얘기냐 하면 데이터 한 번 가져오고 연결 끊으면 그냥 CPU 낭비 (7~8% 정도)하는 꼴이라는 것.
- 웬만하면 Prepared 구문과 Dynamic SQL을 써라.. 아래 경우를 제외하고
* ETL 타입 프로시저
* 아주아주 복잡하지만 자주 실행되지는 않는 것
* 한 번 요청할 때마다 여러번 실행되는 간단한 것 (연결한 상태로 여러번 써야 된다니까)

 

인덱스 컬럼에 함수 쓰는 것

- 함수에 인덱스 컬럼 넣어 호출하면 당연히 인덱스 못 탄다
- 함수를 먼저 계산해서 상수로 만든 다음에 = 로 연결해야 인덱스 탈 수 있다.
* 여기 실행 계획 보면 LIKE도 range type 인덱스 타는 것 보임

 

인덱스 빼먹거나 쓸모없는 인덱스 만들어 놓는 것

- 인덱스 분포도(selectivity)가 허접하면 안 쓴다.
- S = d/n
* d = 서로 다른 값의 수 (# of distinct values)
* n = 테이블의 전체 레코드 수
- 쓸모없는 인덱스는 INSERT/UPDATE/DELETE를 느리게 할 뿐..
- FK는 무조건 인덱스 걸어라. (물론 FK 제약 걸면 인덱스 자동으로 생긴다.)
- WHERE나 GROUP BY 표현식에서 쓰이는 컬럼은 인덱스 추가를 고려할 것
- covering index 사용을 고려할 것
- 인덱스 컬럼 순서에 유의할 것!

 

join 안 쓰는 짓

- 서브쿼리는 join으로 재작성해라
- 커서 제거해라
- 좋은 Mysql 성능을 내려면 기본
- 집합 기반으로 생각해야지 루프 돌리는거 생각하면 안 된다.

 

Deep Scan 고려하지 않는 것

- 검색엔진 크러울러가 쓸고 지나갈 수 있다.
- 이 경우 계속해서 전체 집합을 정렬한 다음 LIMIT로 가져와야 하니 무진장 느려진다.
- 어떻게든 집합을 작게 줄인 다음 거기서 LIMIT 걸어 가져올 것

 

InnoDB 테이블에서 WHERE 조건절 없이 SELECT COUNT(*) 하는 짓

- InnoDB 테이블에서는 조건절 없이 COUNT(*) 하는게 느리다.
- 각 레코드의 transaction isolation을 유지하는 MVCC 구현이 복잡해서 그렇다는..
- 트리거 걸어서 메모리 스토리지 엔진 쓰는 테이블에 통계를 별도로 유지하면 된다.

 

프로파일링이나 벤치마킹 안 하는 것

- 프로파일링 : 병목 찾아내기
- 벤치마킹 : 시간에 따른 성능 변화 추이 평가, 부하 견딜 수 있는지 테스트
- 프로파일링 할 때는 실제 데이터를 옮겨와서 할 것
- 어디가 병목이냐~ Memory? Disk I/O? CPU? Network I/O? OS?
- 느린 쿼리 로그로 남기기
* log_slow_queries=/path/to/log
* log_queries_not_using_indexes
- 벤치마킹 시에는 다 고정시키고 변수 하나만 바꿔가면서 해야 함. (쿼리 캐시는 끌 것.)
- 도구를 써라~~
* EXPLAIN
* SHOW PROFILE
* MyTop/innotop
* mysqlslap
* MyBench
* ApacheBench (ab)
* super-smack
* SysBench
* JMeter/Ant
* Slow Query Log

 

AUTO_INCREMENT 안 쓰는 것

- PK를 AUTO_INCREMENT로 쓰는건 무진장 최적화 되어 있음
* 고속 병행 INSERT 가능
* 잠금 안 걸리고 읽으면서 계속 할 수 있다는!
- 새 레코드를 근처에 놓음으로써 디스크와 페이지 단편화를 줄임
- 메모리와 디스크에 핫 스팟을 생성하고 스와핑을 줄임

 

ON DUPLICATE KEY UPDATE를 안 쓰는 것

- 레코드가 있으면 업데이트하고 없으면 인서트하고 이런 코드 필요없다!! 다 날려버려라!!
- 서버에 불필요하게 왔다갔다 할 필요가 없어짐
- 5-6% 정도 빠름
- 데이터 입력이 많다면 더 커질 수 있음


하지 말아야 할 것 총정리
Thinking too small
Not using EXPLAIN
Choosing the wrong data types
Using persistent connections in PHP
Using a heavy DB abstraction layer
Not understanding storage engines
Not understanding index layouts
Not understanding how the query cache works
Using stored procedures improperly
Operating on an indexed column with a function
Having missing or useless indexes
Not being a join-fu master
Not accounting for deep scans
Doing SELECT COUNT(*) without WHERE on an InnoDB table
Not profiling or benchmarking
Not using AUTO_INCREMENT
Not using ON DUPLICATE KEY UPDATE


출처: https://blog.lael.be/post/370

@RequestMapping의 produces 속성을 이용해 Response의 Content-Type을 제어할 수 있다


한국관광공사가 제공하는 TourAPI를 사용하면서 지역코드(area_code)로 지역명(area_name)을 조회하는 간단한 요청에서 encoding 문제에 부딪혔다. 아래와 같이 ajax 요청으로 areaCode를 보내면 서버에서 areaName을 return하는 식이다. 예를 들면, areaCode로 1을 보내면 서버는 "서울"을 응답해야 한다. 그런데 "서울"이 "??"로 출력되는 인코딩 문제가 발생했다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
$(function() {
    $.ajax({
        url: '/travel/getAreaName',
        method: 'get',
        data: {areaCode:'${content.area}'},
        dataType:"text",
        error:function(textStatus) {
            console.log(textStatus);
        },
        success:function(areaName) {
            console.log('선택된 지역 이름 : ' + areaName);
        }
    });
});
cs


이때까지 @ResponseBody를 사용하여 문자열을 return할 때 영어만 사용했어서 인코딩 문제를 겪지 않았었다. @RequestMapping의 produces 속성을 아래와 같이 작성하면 해결할 수 있다.


1
2
3
4
5
6
7
8
9
10
11
12
/**
 * 지역코드(areaCode)로 지역명을 조회한다
 * @param areaCode
 * @param resp
 * @return
 */
@RequestMapping(value="/getAreaName"
                method=RequestMethod.GET, 
                produces="application/text;charset=utf-8")
public @ResponseBody String getAreaName(@RequestParam int areaCode) {
    return tourApiModule.getAreaName(areaCode);
}
cs


아래는 produces 속성을 작성하지 않았을 때 Response Header의 Content-Type이다.



아래는 produces 속성을 작성한 후 Content-Type이 변경되어 응답하는 것을 확인할 수 있다.




출처: http://bigfat.tistory.com/103

모든 트랜잭션이 같은 방식으로 동작하는 건 아니다. 전체가 같이 실패하거나 성공하는 하나의 작업으로 묶인다는 점에서는 다를바 없겠지만, 세밀히 따져보면 몇 가지 차이점이 있다. 스프링은 트랜잭션의 경계를 설정할 때 네 가지 트랜잭션 속성을 지정할 수 있다. 또, 선언적 트랜잭션에서는 롤백과 커밋의 기준을 변경하기 위해 두 가지 추가 속성을 지정할 수 있다. 선언적 트랜잭션 기준으로 보자면 모든 트랜잭션 경계는 여섯 가지 속성을 갖고 있는 셈이다.

트랜잭션 속성의 지정은 tx/aop 스키마의 태그를 이용하는 경우에는 다음과 같이 <tx:method> 태그의 애트리뷰트로 지정할 수 있다. <tx:method> 의 애트리뷰트는 메소드 이름 패턴을 담은 name 애트리뷰트를 제외하면 모두 디폴트 값이 정의되어 있으므로 생략가능하다.


<tx:attributes>

    <tx:method name="..."

               read-only="..."

               isolation="..."

               propatation="..."

               timeout="..."

               rollback-for="..."

               no-rollback-for="..." />

</tx:attributes>


@Transactional 을 이용했을 때는 다음과 같이 애노테이션의 앨리먼트로 트랜잭션 속성을 지정할 수 있다.


@Transactional(readOnly=...,

               isolation=...,

               propagation=...,

               timeout=...,

               rollbackFor=..., rollbackForClassName=...,

               noRollbackFor=..., noRollbackForClassName=...)


모든 앨리먼트는 디폴트 값이 정의되어 있으므로 생략 가능하다. 이제 트랜잭션 속성에 대해 자세히 알아보자.


트랜잭션 전파(propagation)


이제 트랜잭션을 시작하거나 기존 트랜잭션에 참여하는 방법을 결정하는 속성이다.선언적 트랜잭션 경계설정 방식의 장점은 여러 트랜잭션 적용 범위를 묶어서 커다란 트랜잭션 경계를 만들 수 있다는 점이다. 트랜잭션 경계의 시작 지점에서 트랜잭션 전파 속성을 참조해서 해당 범위의 트랜잭션을 어떤 식으로 진행시킬지 결정할 수 있다.


스프링이 지원하는 트랜잭션 전파 속성은 다음 여섯 가지가 있다. 모든 속성이 모든 종류의 트랜잭션 매니저와 데이터 액세스 기술에서 다 지원되진 않음을 주의해야 한다. 각 트랜잭션 매니저의 API문서에는 사용 가능한 트랜잭션 전파 속성이 설명되어 있으니 사용하기 전에 꼭 참고해 봐야 한다.


 <tx:method> 에서는 propagation 애트리뷰트 값으로, @Transactional 에서는 propagation 앨리먼트로 지정한다. propagation 앨리먼트의 이늄 값은 org.springframework.transaction.annotation.Propagation 에 정의된 것을 사용한다.


REQUIRED


디폴트 속성이다. 모든 트랜잭션 매니저가 지원하며, 대개 이속성이면 충분하다. 미리 시작된 트랜잭션이 있으면 참여하고 없으면 새로 시작한다. 자연스럽고 간단한 트랜잭션 전파 방식이지만 사용해보면 매우 강력하고 유용하다는 사실을 알 수 있다. 하나의 트랜잭션이 시작된 후에 다른 트랜잭션 경계가 설정된 메소드를 호출하면 자연스럽게 같은 트랜잭션으로 묶인다.


SUPPORTS


이미 시작된 트랜잭션이 있으면 참여하고 그렇지 않으면 트랜잭션 없이 진행하게 만든다. 트랜잭션이 없긴 하지만 해당 경계 안에서 Connection이나 하이버네이트 Session 등을 공유할 수 있다.


MANDATORY


REQUIRED와 비슷하게 이미 시작된 트랜잭션이 있으면 참여한다. 반면에 트랜잭션이 시작된 것이 없으면 새로 시작하는 대신 예외를 발생시킨다. 혼자서는 독립적으로 트랜잭션을 진행하면 안 되는 경우에 사용한다.


REQUIRES_NEW


항상 새로운 트랜잭션을 시작한다. 이미 진행 중인 트랜잭션이 있으면 트랜잭션을 잠시 보류시킨다. JTA 트랜잭션 매니저를 사용한다면 서버의 트랜잭션 매니저에 트랜잭션 보류가 가능하도록 설정되어 있어야 한다.


NOT_SUPPORTED


트랜잭션을 사용하지 않게 한다. 이미 진행 중인 트랜잭션이 있으면 보류시킨다.


NEVER


트랜잭션을 사용하지 않도록 강제한다. 이미 진행 중인 트랜잭션도 존재하면 안된다 있다면 예외를 발생시킨다.


NESTED


이미 진행중인 트랜잭션이 있으면 중첩 트랜잭션을 시작한다. 중첩 트랜잭션은 트랜잭션 안에 다시 트랜잭션을 만드는 것이다. 하지만 독립적인 트랜잭션을 마드는 REQUIRES_NEW와는 다르다.


중첩된 트랜잭션은 먼저 시작된 부모 트랜잭션의 커밋과 롤백에는 영향을 받지만 자신의 커밋과 롤백은 부모 트랝개션에게 영향을 주지 않는다. 예를 들어 어떤 중요한 작업을 진행하는 중에 작업 로그를 DB에 저장해야 한다고 해보자. 그런데 로그를 저장하는 작업이 실패하더라도 메인 작업의 트랜잭션까지는 롤백해서는 안되는 경우가 있다. 힘들게 처리한 시급한 작업을 단지 로그를 남기는 작업에 문제가 있다고 모두 실패로 만들 수는 없기 때문이다. 반면에 로그를 남긴 후에 핵심 작업에서 예외가 발생한다면 이때는 저장한 로그도 제거해야 한다. 바로 이럴 때 로그 작업을 메인 트랜잭션에서 분리해서 중첩 트랜잭션으로 만들어 두면 된다. 메인 트랜잭션이 롤백되면 중첩된 로그 트랜잭션도 같이 롤백되지만, 반대로 중첩된 로그 트랜잭션이 롤백돼도 메인 작업에 이상이 없다면 메인 트랜잭션은 정상적으로 커밋된다.


중첩 트랜잭션은 JDBC 3.0 스펙의 저장포인트(savepoint)를 지원하는 드라이버와 DataSourceTransactionManager 를 이용할 경우에 적용 가능하다. 또는 중첩 트랜잭션을 지원하는 일부 WAS의 JTA 트랜잭션 매니저를 이용할 때도 적용할 수 있다. 유용한 트랜잭션 전파 방식이지만 모든 트랜잭션 매니저에 다 적용 가능한 건 아니므로, 적용하려면 사용할 트랜잭션 매니저와 드라이버, WAS의 문서를 참조해 보고, 미리 학습 테스트를 만들어서 검증해봐야 한다.


트랜잭션 격리 수준(isolation)


트랜잭션 격리수준은 동시에 여러 트랜잭션이 진행될 때에 트랜잭션의 작업 결과를 여타 트랜잭션에게 어떻게 노출할 것인지를 결정하는 기준이다. 스프링은 다음 다섯 가지 격리수준 속성을 지원한다.


격리수준은 <tx:method>의 isolation 애트리뷰트와 @Transactional 의 isolation 앨리먼트로 지정할 수 있다.


DEFAULT


사용하는 데이터 액세스 기술 또는 DB 드라이버의 디폴트 설정을 따른다. 보통 드라이버의 격리수준은 DB의 격리수준을 따르는게 일반적이다. 대부분의 DB는 READ_COMMITTED를 기본 격리수준으로 갖는다. 하지만 일부 DB는 디폴트 값이 다른 경우도 있으므로 DEFAULT를 사용할 경우에는 드라이버와 DB의 문서를 참고해서 디폴트 격리수준을 확인해야 한다.


READ_UNCOMMITTED


가장 낮은 격리수준이다. 하나의 트랜잭션이 커밋되기 전에 그 변화가 다른 트랜잭션에 그대로 노출되는 문제가 있다. 하지만 가장 빠르기 때문에 데이터의 일관성이 조금 떨어지더라도 성능을 극대화할 때 의도적으로 사용하기도 한다.


READ_COMMITTED


실제로 가장 많이 사용되는 격리수준이다. 물론 스프링에서는 DEFAULT로 설정해둬도 DB의 기본 격리수준을 따라서 READ_COMMITTED로 동작하는 경우가 대부분이므로 명시적으로 설정하지 않기도 한다. READ_UNCOMMITTED와 달리 다른 트랜잭션이 커밋하지 않은 정보는 읽을 수 없다. 대신 하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정할 수 있다. 이 때문에 처음 트랜잭션이 같은 로우를 읽을 경우 다른 내용이 발견될 수 있다.


REPEATABLE_READ


하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정하는 것을 막아준다. 하지만 새로운 로우를 추가하는 것은 제한하지 않는다. 따라서 SELECT로 조건에 맞는 로우를 전부 가져오는 경우 트랜잭션이 끝나기 전에 추가된 로우가 발견될 수 있다.


SERIALIZABLE


가장 강력한 트랜잭션 격리수준이다. 이름 그대로 트랜잭션을 순차적으로 진행시켜 주기 때문에 여러 트랜잭션이 동시에 같은 테이블의 정보를 액세스하지 못한다. 가장 안전한 격리수준이지만 가장 성능이 떨어지기 때문에 극단적인 안전한 작업이 필요한 경우가 아니라면 자주 사용되지 않는다.


트랜잭션 제한시간(timeout)


이 속성을 이용하면 트랜잭션에 제한시간을 지정할 수 있다. 값은 초 단위로 지정한다. 디폴트는 트랜잭션 시스템의 제한시간을 따르는 것이다. 트랜잭션 제한시간을 직접 지정하는 경우 이 기능을 지원하지 못하는 일부 트랜잭션 매니저는 예외를 발생시킬 수 있다.


XML에서는 <tx:method> 의 timeout 애트리뷰트를 이용하고 @Transactional 애노테이션에는 timeout 엘리먼트로 지정할 수 있다.


읽기전용 트랜잭션(read-only, readOnly)


트랜잭션을 읽기 전용으로 설정할 수 있다. 성능을 최적화하기 위해 사용할 수도 있고 특정 트랜잭션 작업 안에서 쓰기 작업이 일어나는 것을 의도적으로 방지하기 위해 사용할 수도 있다. 트랜잭션을 준비하면서 읽기 전용 속성이 트랜잭션 매니저에게 전달된다. 그에 따라 트랜잭션 매니저가 적절한 작업을 수행한다. 그런데 일부 트랜잭션 매니저의 경우 읽기전용 속성을 무시하고 쓰기 작업을 허용할 수도 있기 때문에 주의해야 한다. 일반적으로 읽기 전용 트랜잭션이 시작된 이후 INSERT, UPDATE, DELETE 같은 쓰기 작업이 진행되면 예외가 발생한다. 


aop/tx 스키마로 트랜잭션 선언을 할 때는 이름 패턴을 이용해 읽기 전용 속성으로 만드는 경우가 많다. 보통 get이나 find 같은 이름의 메소드를 모두 읽기전용으로 만들어 사용하면 편리하다. @Transactional 의 경우는 각 메소드에 일일이 읽기 전용 지정을 해줘야 한다.


read-only 애트리뷰트 또는 readOnly 앨리먼트로 지정한다.


트랜잭션 롤백 예외(rollback-for, rollbackFor, rollbackForClassName)


선언적 트랜잭션에서는 런타임 예외가 발생하면 롤백한다. 반면에 예외가 전혀 발생하지 않거나 체크 예외가 발생하면 커밋한다. 체크 예외를 커밋 대상으로 삼은 이유는 체크 예외가 예외적인 상황에서 사용되기보다는 리턴 값을 대신해서 비즈니스적인 의미를 담은 결과를 돌려주는 용도로 많이 사용되기 때문이다. 스프링에서는 데이터 액세스 기술의 예외는 런타임 예외로 전환돼서 던져지므로 런타임 예외만 롤백 대상으로 삼은 것이다.


하지만 원한다면 기본 동작방식을 바꿀 수 있다. 체크 예외지만 롤백 대상으로 삼아야 하는 것이 있다면 XML의 rolback-for 애트리뷰트나 애노테이션의 rollbackFor 또는 rollbackForClassName 앨리먼트를 이용해서 예외를 지정하면 된다. 


rollback-for 나 rollbackForClassName 은 예외 이름을 넣으면 되고, rollbackFor 는 예외 클래스를 직접 넣는다.


<tx:method> 라면 다음과 같이 지정하면 된다.


<tx:method name="get*" read-only="true" rollback-for="NoSuchMemberException" />


@Transactional 에서는 다음과 같이 클래스 이름 대신 클래스를 직접 사용해도 된다.


@Transactional(readOnly=true, rollbackFor=NoSuchMemberException.class)


트랜잭션 커밋 예외


(no-rollback-for, noRollbackFor, noRollbackForClassName)


rollback-for 속성과는 반대로 기본적으로는 롤백 대상인 런타임 예외를 트랜잭션 커밋 대상으로 지정해 준다.


사용 방법은 rollback-for 와 동일하다.


이 여섯가지 트랜잭션 속성은 모든 트랜잭션 경계설정 속성에 사용할 수 있다. 하지만 모든 트랜잭션마다 일일이 트랜잭션 속성을 지정하는 건 매우 번거롭고 불편한 일이다. 세밀하게 튜닝해야 하는 시스템이 아니라면 메소드 이름 패턴을 이용해서 트랜잭션 속성을 한 번에 지정하는 aop/tx 스키마 태그 방식이 편리하다. 보통은 read-only 속성 정도만 사용하고 나머지는 디폴트로 지정하는 경우가 많다. 세밀한 속성은 DB나 WAS의 트랜잭션 매니저의 설정을 이용해도 되기 때문이다.


세밀한 트랜잭션 속성 지정이 필요한 경우에는 @Transactional 을 사용하는 편이 좋다. 대신 트랜잭션 속성이 전체적으로 어떻게 지정되어 있는지 한눈에 보기 힘들다는 단점이 있고, 개발자가 코드를 만들 때 트랜잭션 속성을 실수로 잘못 지정하는 등의 위험이 있기 때문에 사전에 트랜잭션 속성 지정에 관한 정책이나 가이드라인을 잘 만들어 둬야 한다.

+ Recent posts