Spring @Transactional 주의점

안녕하세요. 오늘은 Spring@Transactional 어노테이션을 사용할 때, 주의점에 대해 포스팅합니다.

 

Spring 에서 JPA 기술을 쓸 때 빼놓을 수 없는 기능중 하나는 @Transactional 입니다.

아래와 같이 많은 편리성을 우리에게 제공해주기 때문이지요.

  • transaction begin, commit을 자동 수행해준다.
  • 예외를 발생시키면, rollback 처리를 자동 수행해준다.

 

그렇기에, 많은 보일러코드를 줄 일 수 있기 때문에, 아래와 같이 편리하게 코드를 작성할 수 있습니다.

Note. 문제 상황을 억지로 만들기 위해 코드 효율성이 좋지 않을 수 있습니다.

 

문제는 예시에서 쓴 addBook 메서드를 Books 클래스 내부에서 사용할 때 발생 할 수 있습니다.

 

혹시, addBooks 메서드를 사용할 때, 어떤것이 문제가 될 수 있는지 예상이 가시나요?

문제는 addBooks 메서드 내부에서 호출하는 addBook 메서드의 @Transactional 어노테이션이 적용되지 않는 것입니다.

 

그렇기에, 해당 코드를 실행하더라도 Database에는 저장된 Book 정보에 Flag 컬럼에 정상적으로 업데이트 되지 않습니다.

bookRepositorysave 메서드는 자체적으로 @Transactional 어노테이션이 붙어있어, 정상적으로 코드가 수행이 되어 Database에 insert 를 수행합니다.

하지만, update 의 역할을 하는 book 객체의 변경감지가 동작하지 않아, Flag 컬럼에 업데이트가 되지 않습니다.

 

@Transactional 이 동작하지 않았는지는 Spring@Transactional 기능을 제공하는 방식에 대해 살펴볼 필요가 있습니다.

 

Spring @Transactonal 기능제공 방식

JPA 의 객체 변경감지는 transactoncommit 될 때, 작동합니다.

그렇기에 Spring@Transactonal 어노테이션을 선언한 메서드가 실행되기전, transaction begin 코드를 삽입하며

메서드가 실행된 후, transaction commit 코드를 삽입하여, 객체 변경감지를 수행하게 유도합니다.

 

Spring 의 코드 삽입 방법은 크게 2가지 방법이 있습니다.

  • 바이트 코드 생성 (CGLIB 사용)
  • 프록시 객체 사용

2가지 방법중 Spring 은 기본적으로 프록시 객체 사용 이 선택됩니다. 그렇기에 interface 가 반드시 필요합니다.

SpringBoot 는 기본적으로 바이트 코드 생성 이 선택됩니다. 그렇기에, 굳이 interface 가 필요없습니다.

만약 개발환경이 SpringBoot 라면 Books 인터페이스 없이 봐도 무방합니다.

 

원리는 이렇습니다. 프록시 객체로 우리가 만든 메서드를 한번 감싸서, 메서드 위 아래로 코드를 삽입 해줍니다.

 

그렇기에, 우리는 BooksImpl 자료형을 사용 할 때, 스프링이 제공하는 BooksProxy 객체를 사용하게 되며,

BooksProxy 객체가 제공하는 addBook 메서드를 사용해야만 transaction 처리가 수행되는것입니다.

 

@Transactonal 이 수행되지 않은 이유.

다시 아래의 예제를 살펴봅니다.

 

BooksProxyaddBooks 메서드를 수행하면, 아래와 같은 순서로 작동됩니다.

BooksProxy::addBooks -> BooksImpl::Book

 

즉, BooksImpl 내부의 코드(addBook) 가 수행 되기 때문에 해당 메서드는 프록시로 감싸진 메서드가 아니라는 점에서

@Transactonal 어노테이션 기능이 수행되지 않는다는 것입니다.

 

해결방법

사실 @Transactional 메서드를 내부적으로 사용하지 않는것이 근본적인 해결책입니다.

하지만 만약 굳이 사용해야 겠다면, 의존성 주입을 이용하여 Proxy 인스턴스를 자체적으로 가져와 사용하는 방법이 있습니다.

 

해당 코드는 Books 인터페이스를 이용하여 BooksProxy 인스턴스를 주입할 수 있도록 유도합니다.

생성자 주입을 사용하면, 순환 에러가 발생합니다. @Autowired 로만 해주세요. Autowired 싫다.

 

그 후, 자기 자신 즉 this 가 가지고 있는 순수한 addBook 메서드가 아니라 proxy 로 감싸진 addBook 메서드를 통해@Transactional 어노테이션 기능을 사용할 수 있게 됩니다.

 

정리하자면

  • @Transactonal 어노테이션 메서드는 클래스 내부적으로 사용하지 말고, 밖에서 사용하자. (프록시 객체 때문에)
  • 굳이 내부적으로 사용하려면, 자기 자신의 Proxy 객체를 사용하여 처리하자.

 

마무으리

혹시 틀린 내용이 있어서, 알려주신다면 감사하게 듣겠습니다. (_ _)

긴글 읽어주셔서 감사합니다!

 

포스팅이 도움 되셨다면, 커피 한잔 후원해주세요!
더 좋은 포스팅 작성에 큰 힘이 됩니다.

Buy me a coffeeBuy me a coffee
  1. GOD IS GOOD ALWAYS 2020.03.10 18:27

    오 제가 방금전에 transaction 에 대해 질문하고 왔는데

    제가 사용하고 있는게 @Transactional 어노테이션이었거든요

  2. JEONG_AMATEUR 2020.06.08 21:01 신고

    @Async도 그렇고 @Transactional도 그렇고 프록시로 동작하는 annotation에서는 항상 이 문제를 유의해야겠군요!

    정리 감사합니다~

  3. kingkk31 2021.01.29 14:23 신고

    내용 큰 도움되었습니다 감사합니다!

  4. hpotter1993 2021.05.25 21:52 신고

    혹시 BooksProxy가 addBooks메소드가 아닌, addBook 메소드를 실행하는 것 아닌가요?? BooksProxy에는 addProxy만 작성되어 있습니다!...

  5. tomato 2021.06.15 14:49

    좋은 글 정말 감사합니다.

    스프링을 공부하고 있는 학생인데 스프링 내부 동작 원리와 같은 이런 정보들은 스프링 공식 문서에서 얻을 수 있을까요?

    • Mommoo 2021.07.12 02:16 신고

      답변이 늦었습니다. ^^

      공식 문서를 전부 읽어본건 아니지만, Proxy 내용은 있었던거 같습니다.

      저는 공식문서는 필요할때만 읽는편이고... 정보가 필요할때는 먼저 구글 서칭과 서적으로 시작하는 편입니다.

  6. 리키타카 2021.06.27 16:14 신고

    안녕하세요 궁금한 점 있습니다!
    1] 클래스에 @Transactional 달아주면 안되나요?

    2] void addBooks(List<String> bookNames) 메소드에 트랜잭션을 달아주면 안되나요?

    저는 보통 클래스에 트랜잭션 어노테이션 달고 / 조회 api 인 경우에는 메소드에 (readOnly = true) 달고 사용해서요.
    (아니면 별도로 조회 전용 컨트롤러를 분리해서 만들던가)

    글 잘 봤습니다.

    • Mommoo 2021.07.12 02:15 신고

      안녕하세요^^
      답변이 늦었습니다.

      1) 클래스에 해당 어노테이션 명시해줄수 있습니다.! 그렇기 되면 클래스에서 사용하는 모든 메서드가 @Transaction 처리를 받게 됩니다.

      2) 붙여줘도 됩니다 ^^
      다만, 붙이게 되면 하나의 트랜잭션으로 취합이 되구요 개별 트랜잭션 N개로 수행이 되지 않습니다.

      예시는 addBooks 에 트랜잭션을 붙여버리면, 어쨋거나 통합 Transaction이 수행되기 때문에, addBook 트랜잭션이 수행되지 않는다는걸 알려드릴 수 없었습니다.

퀵정렬 포스팅

Note.

해당 글은, 퀵정렬의 기본적인 개념은 알고 있다는 전제로 진행합니다.

또한 틀린 내용이 있다면, 가감없이 알려주시면 감사하겠습니다. (_ _)

 

오늘은 퀵정렬 알고리즘에 대해 포스팅합니다.

퀵정렬 알고리즘은 매우 유명해서 검색하면 많은 자료를 찾아볼 수 있을텐데요.

저도 오랜만에 퀵정렬을 공부하려고, 많은 자료를 찾아봤는데 비슷 비슷한 코드를 볼 수 있었습니다.

그런데 말입니다.(김상중 아찌톤) 코드를 왜 그렇게 구성했는지 한번에 이해가지 않았으며, 후에 분석해본 결과 틀린 코드들도 정말 많았습니다.

어쩌면, 퀵정렬 구현이 공식처럼 알려진게 아닌가 싶고 제대로 된 코드 이해없이 비슷하게만 따라하다가 틀린 코드를 작성하는 경우가 많을거 같다는 생각을 했습니다.

그렇기에, 퀵정렬 코드를 단번에 이해못하는 멍청한 저를 위해, 나름대로 개념을 뽀개 봤습니다.

 

해당 포스팅은 아래와 같이 쓸때없는(?) 것을 연구하고 도출한 결과를 설명 해볼까합니다.

  • 왜 많은 자료들의 코드는 pivot index를 항상 끝 방향에 두고 코딩을 하는걸까? 가운데다가 두고 하면 안돼는가?

  • 왜 어떤 코드는 탐색 할 때, pivot 의 값만 비교하고, 어떤 코드는 pivot 값과 왼쪽 index 와 오른쪽 index 크기 비교까지 하는 걸까?

  • 왜 다들 오른쪽 부터, 탐색하는 코드만 작성 하는가? 왼쪽부터 탐색하면 안되는가?

  • SWAP 작업을 마치고, 마지막에 pivot index를 옮길 때, 주의해야할점

 

Pivot을 끝방향(왼쪽 끝 또는 오른쪽 끝)에 두는 이유

저는 이상한 고집이 있어서, 남들이 pivot 을 끝에다가 두면 굳이 가운데다가 해보고싶은 욕구가 있습니다. 지식을 습득할 때, 당연하게 외우기 보다 왜 그렇게만 해야하는지에 대해 아는게 중요하다고 생각하기 때문이지요.


결론적으로, pivot 을 가운데다가 배치를 하게 되면, 코드를 작성하기가 매우 까다로워 집니다.

퀵정렬의 기본개념은, 왼쪽방향과 끝쪽방향에서 서로 접근하며 바꿀 수 있는 수가 나올때 SWAP 을 시도하는것과, SWAP 작업을 마친 후, pivot 자리를 찾아주는 것이 중요합니다. 그래야 pivot 기준으로 왼쪽, 오른쪽 배열로 쪼개서 분할정복 을 시도할 수 있기 때문이지요.


문제는 pivot 이 가운데에 끼여 있으면, pivotSWAP 될수도 있다는것 입니다. 그럴경우, pivot index를 업데이트를 꼭 해주어야 합니다.


그렇지 않으면, 마지막에 pivot 자리를 찾아주는 과정에서 pivot 이 아닌 다른 값을 배치하기 때문이지요.

정리하자면, pivot을 가운데 잡아서 SWAP 작업을 진행하느니, 맨끝쪽에 pivot을 배치하여 SWAP 작업으로부터 안전하게 빼면 index 업데이트를 할 필요가 없습니다.

 

탐색할 때 비교해야할 요소

코드는 짧고 명료하고 간단한것이 매력적입니다. 멍청해서, 어려운건 이해하가 힘들거든요. (나도 똑똑해지고 싶다!!!)

그래서 그런지, 아래 코드가 끌리긴 했습니다. 굳이 왼쪽 index 와 오른쪽 index 를 비교해야 하나 싶었지요.


결론적으로는, 비교를 해야 하는게 맞습니다.

만약 하지 않는다면, index 가 배열 밖으로 벗어날 위험이 있습니다.

아래와 같은 배열이 있다고 가정해봅시다. (pivot 은 맨 왼쪽입니다.)

위 코드를 실행하면, beginIndex 는 결국 배열 크기 밖으로 빠지면서, OutOfIndex 에러가 발생하게 됩니다.

이를 방지하기 위해, beginIndex 의 조건을 배열 크기 보다 작은 경우로 줄 수도 있겠습니다.


하지만 우리가 원하는 것은 beginIndexendIndex 가 만나는 순간이고, endIndex는 배열 크기 보다 무조건 작거나 같으므로 아래와 같이 더 적은 횟수를 수행하는 조건으로 작성할 수 있습니다.

생각보다 많은 자료의 코드들이 OutOfIndex 에러가 발생하는것을 목격할 수 있었습니다.

 

 

탐색 순서가 중요한 이유

아까도 말씀드렸다 시피, 저는 이상한 고집이 있어서... 오른쪽 부터 탐색하는 코드가 많으면, 굳이 왼쪽으로 해보고 싶어집니다.(삐뚤게 살거야ㅋㅋㅋ)

해봤더니, 코드가 제대로 동작하지 않았습니다.

이상합니다. 어떤 코드는 왼쪽부터 동작하게 짰었거든요. 그렇다면 탐색 순서가 중요하긴 한가 봅니다.

고민해본결과, 왼쪽 부터 먼저 탐색하는것과 오른쪽 부터 탐색하는 결과의 차이점은 다음과 같습니다.

 

다음 그림에서, pivot을 맨 왼쪽 으로 잡았습니다.

탐색하는 코드는 다음과 같이 왼쪽부터 수행한다고 가정 한다면,

beginIndex8endIndex 도 마찬가지로 8 에 위치하게 됩니다. (index 가 아니라 입니다.!!! )

beginIndexendIndex 가 만났으니 pivot SWAP 작업을 거쳐야 하는데, 자세히 살펴보면 해당 작업을 수행하면 정렬 이 깨져 버립니다.

85 를 바꾼 후, 살펴보면 pivot 값인 5를 기준으로 왼쪽에 작은수만 있어야 하는 8 이 있어버리는 바람에 정렬이 깨져버립니다.

 

그렇다면 오른쪽부터 수행하면 어떻게 될까요?

해보시면 알겠지만, beginIndex 도, endIndex5 에 도달하게 됩니다.

pivot SWAP 작업을 거치더라도, 정렬이 깨지지 않는걸 알 수 있죠.

 

정리하자면 이렇습니다.

왼쪽부터 탐색을 수행하면, pivot 값 보다 큰 값 으로 beginIndexendIndex 가 만나게 되며,

오른쪽 부터 탐색을 수행하면, pivot 값 보다 작은 값 으로 beginIndexendIndex 가 만나게 됩니다.

 

이를 통해, 아래와 같이 생각 할 수 있습니다.

이는, 만약 맨-왼쪽으로 pivot 값 을 잡는다면, SWAP 대상이 pivot 값 보다 작은 값 이여야 하며,

맨-오른쪽으로 pivot 값 을 잡는다면, SWAP 대상이 pivot 값 보다 큰 값 이여야 함을 알 수 있습니다.

즉... 왼쪽부터냐 오른쪽 부터냐는 취향대로 하면 안되고,

pivot의 위치에 따라 반대방향으로 탐색 순서를 잡아야 함을 알 수 있습니다.

 

 

pivot swap 할 때, 주의 해야할점.

SWAP 작업의 마지막은 pivot SWAP 으로 pivot 자리를 찾아주는 것으로, 이는 매우 중요한 작업입니다.

많은 자료의 코드들이 단순하게 beginIndexendIndex 가 만나는 지점의 indexSWAP 을 시켜주는데, 이는 잘못된 코드가 될 수 있습니다.

아래와 같은 배열이 있다고 가정해봅시다. (pivot 은 맨 왼쪽입니다.)

배웠드시, 맨 왼쪽 pivot 이므로 오른쪽 부터 탐색을 수행해야 합니다.

그렇게 되면 beginIndex 와, endIndex5 에 도달하게 됩니다.

 

이때, pivot SWAP 을 진행하고 살펴보면 정렬이 깨지는걸 알 수 있습니다.

이런 현상이 왜 발생하냐면, beginIndex 가 수행할 기회 없이, 일방적으로 endIndex 가 끝으로 와버렸기 때문입니다. 즉 합의에 의한 만남이 아니라 일방적인 만남인것이지요. (말이좀 이상하네)

이런 경우가 있을 수 있기 때문에, pivot SWAP 을 진행할 때는, 데이터 크기 비교 후 진행해야 합니다.

 

정리하자면,

  • pivot 은 가운데다가 둘 수 있지만, SWAP 작업에 영향을 받을 수 있으니, 안전한 장소인 양끝에 두는걸로 하자.
  • index 탐색 작업을 수행 할 때, 배열 크기를 넘을 수 있으니 반드시 left < right 조건과 같이 탐색을 진행하자.
  • pivot 을 오른쪽 끝에 둘거면, 탐색 순서를 왼쪽부터! pivot 을 왼쪽 끝에 둘거면, 탐색 순서를 오른쪽 부터 진행하자.
  • pivot SWAP 을 진행할 때는, 해당 요소의 크기 조건이 안맞는 경우도 있으니 크기 조건을 반드시 추가하자.

 

마무으리

위에 진행한 코드는 아래에 경로에서 볼 수 있습니다.

https://github.com/Mommoo/Programmers/blob/master/src/com/mommoo/practice/sort/QuickSort.java

코드를 봐도 이해가 안간다는건 참 속상한 일입니다.

이렇게 분석을 나름 열심히 했는데도 틀린 분석이라면 더 속상할 거 같습니다.

만약에 이 게시글을 보고 틀린점이 있다면, 꼭 알려주셨으면 합니다.

읽어주셔서 감사합니다.

 


포스팅이 도움 되셨다면, 커피 한잔 후원해주세요!
더 좋은 포스팅 작성에 큰 힘이 됩니다.

Buy me a coffeeBuy me a coffee


터미널에서 Git commit 을 진행 하면 마지막에 commit message 를 작성하기 위한 vi 화면이 나옵니다.


문제는 # 문자로 시작하여 커밋 메시지를 작성하고 싶을 때 가 있습니다.


하지만 위 설명과 같이 # 으로 시작하는 문장은 Git 은 주석으로 인식하기 때문에 커밋 메시지가 정상적으로 적용되지 않습니다.


이를 해결 하기 위한 방법중 하나는 주석을 의미하는 힌트 문자를 바꾸는 방법이 있습니다.


아래와 같이 본인이 메시지 작성에 쓰이지 않는, 문자 하나를 주석 힌트 문자로 설정해주면 됩니다.


저는 * 로 하여, 주석의 시작이 * 로 바뀌었고, 무사히 # 문자를 commit message 에 작성할 수 있었습니다.

git config core.commentChar '*' (개인 프로젝트)
git config --global core.commentChar '*' (전체 프로젝트)


포스팅이 도움 되셨다면, 커피 한잔 후원해주세요!
더 좋은 포스팅 작성에 큰 힘이 됩니다.

Buy me a coffeeBuy me a coffee


오늘 포스팅 할 내용은 IntelliJ IDEA 의 내장된 DB Client를 사용하는 방법에 대해 기술합니다.

사용한 환경은 대략적으로 아래와 같습니다.

  • Mac OS

  • IntelliJ IDEA Ultimate

  • H2 데이터베이스

  • Maven Project

H2 데이터베이스 실행하기

http://www.h2database.com/html/download.html

위 경로에서 H2 데이터베이스를 설치합니다.

다운받은 후, bin 경로의 h2.sh 스크립트를 실행합니다. (윈도우 사용자라면, h2.bat 을 실행하면 됩니다.)

실행하면, 아래 이미지와 같이 기본 설정된 브라우저로 데이터베이스 접속화면이 뜹니다.

Note. 브라우저의 접속화면이 계속 로딩 화면으로 유지 된다면, url을 localhost 로 변경 해보세요.

화면 상단 언어설정에서 한국어로 바꾼후,

위 이미지 처럼 설정 하되, 세부 설정은 아래의 설명을 참고해주세요.

JDBC URL 맨 끝에는 원하시는 데이터베이스 명을 넣으시면 됩니다.

ex) Database 이름을 Person으로 -> jdbc:h2:tcp://localhost/~/Person

사용자명비밀번호 는 개인적으로 맘에드는 것을 작성하면 되지만, 기억하셔야 합니다.

저는 사용자명sa비밀번호는 작성하지 않았습니다.

설정이 끝난 후 연결시험을 눌러 시험 성공 메시지가 뜨는지 확인해주세요.

성공 메시지가 정상적으로 떴다면, 연결 버튼을 눌러 연결을 해주세요.

의존성 준비하기 (FEAT. MAVEN)

필요한 의존성은 H2 Database 프로그램을 Java 프로젝트와 연동할 H2 Database Driver 라이브러리가 필요합니다.

pom.xml 의 dependency 설정부분에 아래와 같이 의존성을 추가해주세요.

<dependency>
 <groupId>com.h2database</groupId>
 <artifactId>h2</artifactId>
 <version>1.4.193</version>
</dependency>

INTELLIJ DB CLIENT 연결하기.

Action 검색으로 Database를 검색합니다. ( shift + shift )

Database 탭이 뜬 후, + 버튼을 눌러 DataSource > H2 를 누릅니다.

설정창이 뜰텐데요, 정보를 기입할때 몇가지 유념해야할 사항이 있습니다.

Connection Type

저희는 H2 Database 프로그램을 서버모드로 작동시켰습니다.

http://www.h2database.com/html/features.html#database_url

대응하는 Type은 Remote 입니다. 해당 Type을 선택해줍니다.

Driver

Maven으로 H2 Database Driver 모듈을 다운받았다면,

H2 로 설정되어 있습니다. 만약 안되어 있다면, 의존성을 다시 확인해주시고, 수동 설정을 해보시길 바랍니다.

Host

H2 Database 가 실행된곳의 서버정보를 입력하면 됩니다. (보통 localhost)

Port

port 는 조금 알아야하는 내용이 있습니다.

기입해야할 port H2 Database 웹 브라우저 접속에서 사용한 8082번 포트가 아닙니다.

8082 번 포트는 H2 Database의 웹 어플리케이션을 구동할때 사용하는 포트번호 입니다.

우리가 실제로 연결해야할 H2 Database의 프로그램 포트번호는 9092 번을 사용합니다.

따라서, 9092 번으로 기입합니다.

User , Password

H2 Database 웹 접속화면에서 작성한 아이디와 비밀번호를 입력하면 됩니다.

Database

우리가 작성한 JDBC URL 포맷은 ~ 를 사용하여 상대경로로 표현했습니다.

아쉽게도 IntelliJ IDEA 가 해당 표현을 인지를 못하는거 같아, 일단 공백으로 둡니다.

아래의 URL을 작성하는 곳에서 Database 명까지 한번에 입력할 예정입니다.

URL

지금 까지 설정대로 잘 작성했다면, 아래와 같은 패턴으로 자동완성되어 있습니다.

jdbc:h2:tcp://{host}:9092/

우리가 사용한 URL 포맷으로 아래와 같이 바꿉니다. (데이터베이스 명까지 한번에 작성합니다.)

jdbc:h2:tcp://{host}:9092/~/{데이터베이스 명}

마지막으로 TEST CONNECTION 버튼으로 연결 테스팅을 진행 후, 설정을 마칩니다.

마무리

IntelliJ IDEADatabase 탭 메뉴중 콘솔을 눌러서 잘되었는지 간단한 명령어로 확인해봅니다.

ex) show tables;

만약 스키마 목록이 보이지 않는다면, Database 탭 메인 화면에서 오른쪽에 조그만한 숫자를 눌러, 메뉴 표시를 하시면 됩니다.

오늘 포스팅은 여기까지 입니다.

읽어주셔서 감사합니다.

'IDE' 카테고리의 다른 글

[IntelliJ] 인텔리J에 이클립스 Tab 기능 설정하기.  (2) 2018.12.23
[Eclipse] 이클립스 폰트 설정  (0) 2016.04.08

포스팅이 도움 되셨다면, 커피 한잔 후원해주세요!
더 좋은 포스팅 작성에 큰 힘이 됩니다.

Buy me a coffeeBuy me a coffee

+ Recent posts