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

개요



 MySQL 쿼리 옵티마이저는 쿼리를 실행할때 최적의 계획을 세운다. 그 계획을 Database용어로 '실행계획'(Query Plan)이라고 하는데, MySQL에서는 'EXPLAIN' 키워드를 이용해 실행계획에 대한 정보를 살펴 볼 수 있다.

 이는 이슈가 발생하는 문제의 쿼리를 이해하고, 어떻게 최적화 할지에대한 insight를 제공하는 매우 강력한 도구가 될 수 있는데, 불행하게도 이를 잘 사용하는 개발자는 별로 없는것 같다. (나 포함 ㅠ)


Understanding EXPLAIN's Output



'EXPLAIN' 을 사용하면 쿼리를 실행하기전에 실행계획을 분석하여 출력한다. 아래와 같은 예를 들어보자

EXPLAIN SELECT * FROM categories
********************** 1. row **********************
           id: 1
  select_type: SIMPLE
        table: categories
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 4
        Extra: 
1 row in set (0.00 sec)

이제부터 해당 항목들에대해 자세히 살펴보도록 한다


  • id : 쿼리 안에 있는 각 select 문에 대한 순차 식별자이다. 이순서대로 select문이 실행된다고 생각하면 된다.
  • select_type : select 문의 유형을 말한다. 각 유형은 아래와 같다
    • SIMPLE : 서브쿼리나 'union'이 없는 가장 단순한 select문을 말한다
    • PRIMARY : 가장 바깥에 있는 select 문을 말한다
    • DERIVED : from 문 안에있는 서브쿼리의 select 문이다.
    • SUBQUERY : 가장 바깥의 select 문에 있는 서브쿼리이다.
    • DEPENDENT SUBQUERY : 기본적으로 SUBQUERY와 같은 유형이며, 가장 바깥의 select문에 '의존성'을 가진 서브쿼리의 select문이다.
    • UNCACHEABLE SUBQUERY
    • UNION : union 문의 두번째 select 문을 말한다
    • DEPENDENT UNION : 바깥 쿼리에 의존성을 가진 union문의 두번째 select문을 말한다
  • table : 참조되는 테이블을 말한다
  • type : MySQL이 어떤식으로 테이블들을 조인하는지를 나타내는 항목이다. 이는 매우 중요한데, 이유는 이 타입을 분석함으로써 어떤 인덱스가 사용되고 사용되지 않았는지를 알 수 있고, 이를통해 어떤식으로 쿼리가 튜닝되어야하는지에 대한 insight를 제공하기 때문이다. 각 유형은 아래와 같다
    • system : 0개 또는 하나의 row를 가진 테이블이다.
    • const : 테이블에 조건을 만족하는 레코드가 하나일 때, 상수 취급
    • eq_ref : primary key나 unique not null column으로 생성된 인덱스를 사용해 조인을 하는 경우이다. const 방식 다음으로 빠른 방법이다.
    • ref : 인덱스로 지정된 컬럼끼리의 '=' , '<=>' 와 같은 연산자를 통한 비교로 수행되는 조인이다
    • index_merge 
    • unique_subquery : 오직 하나의 결과만을 반환하는 'IN'이 포함된 서브쿼리의 경우이다.
    • index_subquery : unique_subquery와 비슷하지만 여러개의 결과를 반환한다
    • range : 특정한 범위의 rows들을 매칭시키는데 인덱스가 사용된 경우이다. BETWEEN이나 IN, '>', '>=' 등이 사용될 때이다.
    • all : 조인시에 모든 테이블의 모든 row를 스캔하는경우이다. 물론 성능이 가장 좋지 않다.
  • possible_keys : 테이블에서 row를 매핑시키기 위해 사용 가능한 (사용하지 않더라도) 키를 보여준다.  
  • key : 실제적으로 쿼리 실행에 사용된 key의 목록이다. 이 항목에는 possible_keys 목록에 나타지 않은 인덱스도 포함 될 수 있다.
  • ref : key column에 지정된 인덱스와 비교되는 column 또는 constants를 보여준다.
  • rows : 결과 산출에 있어서 접근되는 record의 숫자이다. 조인문이나 서브쿼리 최적화에 있어서 중요한 항목이다.
  • Extra : 실행계획에 있어서 부가적인 정보를 보여준다.
distinct : 조건을 만족하는 레코드를 찾았을 때 같은 조건을 만족하는 또 다른 레코드가 있는지 검사하지 않음.
not exist : left join 조건을 만족하는 하나의 레코드를 찾았을 때 다른 레코드의 조합은 더 이상 검사하지 않는다.
range checked for each record : 최적의 인덱스가 없는 차선의 인덱스를 사용한다는 의미.
using filesort : mysql이 정렬을 빠르게 하기 위해 부가적인 일을 한다.
using index : select 할때 인덱스 파일만 사용
using temporary : 임시 테이블을 사용한다. order by 나 group by 할때 주로 사용
using where : 조건을 사용한다는 의미.

    • http://dev.mysql.com/doc/refman/5.6/en/explain-output.html#explain-extra-information


+ Recent posts