일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 게시판프로젝트
- application영역
- page영역
- 스프링시큐리티
- ResponseEntity
- session영역
- 스프링회원정보수정
- RPTLANFTN
- 회원정보수정
- 내장객체
- 스프링
- Security
- Spring
- jsp
- 게시판댓글
- SCOPE
- 댓글수처리
- 시큐리티
- request영역
- 회원정보
- 게시판댓글수
- Today
- Total
코코무의 코딩캔버스
[Spring] AOP와 트랜잭션(@Transactional) 본문
※ 본 글은 교재 [코드로 배우는 스프링 웹 프로젝트 - 구멍가게 코딩단]을 바탕으로 작성되었습니다.
AOP 기능은 주로 일반적인 Java API를 이용하는 클래스들에 적용합니다.
Controller에는 인터셉터나 필터 등을 이용하고, 서비스 계층에 AOP를 적용할 것입니다.
서비스 계층의 메서드 호출 시 모든 파라미터들의 로그를 기록하고
메서드들의 실행 시간을 기록하겠습니다.
실습
1. 예제 프로젝트 생성 및 설정
2. 서비스 계층 설계
Service 인터페이스와 ServiceImpl를 생성합니다.
ServiceImpl는 문자열을 변환해서 더하기 연산을 하는 단순 작업으로 작성합니다.
작성 시에는 반드시 @Service라는 어노테이션을 추가해 스프링에서 빈으로 사용될 수 있도록 설정합니다.
3. Advice 작성(로그 기록용)
지금까지 해왔던 로그를 기록하는 일은 '관심사'로 간주할 수 있습니다.
이제는 아예 LogAdvice 클래스로 로그 기록을 빼줄 것입니다.
AOP 기능 설정은 일단 어노테이션만 사용하겠습니다.
@Aspect //aop 담당
@Log4j2
@Component //aop와 관계는 없지만 스프링에서 빈으로 인식시키게 함
public class LogAdvice {
@Before( "execution(* org.zerock.service.SampleService*.*(..))") //* 접근 제한자, *.* 클래스.메서드
public void logBefore() {
//BeforeAdvice 구현 메서드
log.info("===================aop===================");
}
}
@Aspect
해당 클래스의 객체가 Aspect를 구현한 것임을 나타내기 위해 사용함
@Component
AOP와 무관하지만 스프링에서 빈으로 인식하기 위해 사용함
@Before
BeforeAdvice를 구현한 메서드에 추가함(@After, @AfterReturning, @AfterThrowing, @Around도 동일한 방식)
execution....
AspectJ의 표현식으로 접근제한자와 특정 클래스의 메서드를 지정할 수 있음
맨 앞의 *는 접근제한자를 의미하고 맨 뒤의 *는 클래스의 이름과 메서드의 이름을 의미
AOP 설정
프로젝트의 root-context.xml을 선택해 NameSpace 탭에서 aop와 context를 추가합니다.
그리고 <context:component-scan base-package>를 사용해 패키지를 스프링에서 스캔하도록 하고
<aop:aspectj-autoproxy>를 추가합니다.
이 과정에서 ServiceImpl 클래스와 LogAdvice는 스프링의 빈으로 등록되며 LogAdvice에 설정한 @Before가 동작합니다.
AOP 테스트
ServiceImpl와 LogAdvice가 함께 묶여 자동으로 Proxy 객체가 생성됩니다.
*proxy: 대리자 라는 뜻으로, 클라이언트가 사용하려고 하는 실제 대상인 것처럼 위장해서 클라이언트의 요청을 받아주는 역할, 이를 사용하는 클라이언트는 구체 클래스를 알 필요가 없어짐, 접근을 제어하고 싶거나, 부가 기능을 추가하고 싶을 때 주로 사용
이를 확인하기 위해 테스트 파일을 만들어 줍니다.
@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration({"file:src/main/webapp/WEB-INF/spring/root-context.xml", "file:src/main/webapp/WEB-INF/spring/appServlet/servlet-context.xml"})
@Log4j2
public class SampleServiceTests {
@Setter(onMethod_ = @Autowired)
private SampleService service;
@Test
public void testClass() {
log.info(service);
log.info(service.getClass().getName());
}
}
AOP 설정을 한 Target에 대해 Proxy 객체가 정상적으로 만들어져 있는지 확인하는 것이 우선입니다.
문제가 없다면 service 변수의 클래스는 ServiceImple의 인터페이스가 아니라 Proxy 클래스의 인스턴스가 됩니다.
@Test
public void testAdd() throws Exception{
log.info(service.doAdd("123", "123"));
}
이제 ServiceImpl에 있는 doAdd메서드를 실행하는 테스트를 해봅니다.
LogAdvice의 설정이 같이 적용된 콘솔이 떠야 합니다.
1. args를 이용한 파라미터 추적
LogAdvice가 Service의 doAdd( )를 실행하기 직전 간단한 로그를 기록하지만
그외에도 해당 메서드에 전달되는 파라미터가 무엇인지 기록하거나 예외가 발생했을 때 어떤 파라미터에 문제가 있는지 알고 싶은 경우도 많습니다.
LogAdvice에 적용된 @Before~은 어떤 위치에 Advice를 적용할 것인지를 결정하는 Pointcut인데 설정 시 args를 이용하면 간단히 파라미터를 구할 수 있습니다.
2. @AfterThrowing
지정된 대상이 예외를 발생한 후에 동작하면서 문제를 찾는 것을 도와줍니다.
logException( )에 적용된 @AferThrowing은 'pointcut'과 'throwing' 속성을 지정하고 변수 이름을 'exception'으로 지정합니다.
doAdd( )는 숫자로 변환이 가능한 문자열을 파라미터로 지정해야 하는데 고의적으로 'ABC'와 같은 문자를 지정하면 예외가 발생합니다.
@Around와 ProceedingJoinPoint
AOP를 통해 좀 더 구체적인 처리를 할 때 사용합니다.
@Around는 직접 대상 메서드를 실행할 수 있는 권한을 가지고 있고 메서드의 실행 전과 실행 후에 처리가 가능합니다.
트랜잭션 관리
트랜잭션(Transaction)이란
쪼개질 수 없는 하나의 작업 단어를 말합니다.
트랜잭션의 성격을 ACID 원칙으로 설명할 수 있습니다.
원자성(Atomicity) | 하나의 트랜잭션은 모두 하나의 단위로 처리되어야 함 어떤 트랜잭션이 A와 B로 구성된다면 항상 A, B의 처리 결과는 동일한 결과여야 함 어떤 작업이 잘못되는 경우 모든 것이 다시 원점으로 돌아가야만 함 |
일관성(Consistency) | 트랜잭션이 성공했다면 데이터베이스의 모든 데이터는 일관성을 유지해야만 함 트랜잭션으로 처리된 데이터와 일반 데이터 사이에는 전혀 차이가 없어야만 함 |
격리(Isolation) | 트랜잭션으로 처리되는 중간에 외부에서의 간섭은 없어야만 함 |
영속성(Durability) | 트랜잭션이 성공적으로 처리되면 그 결과는 영속적으로 보관되어야 함 |
가장 흔한 예는 '계좌 이체'인데요,
이 행위가 내부적으로는 하나의 계좌에서는 출금이 이루어져야 하고 이체의 대상 계좌에서는 입금이 이루어져야 합니다.
쉽게 말하면 '출금'과 '입금'이 하나의 단위를 이루게 되는 것이지요.
계좌 이체를 가지고 더 자세히 예를 들어보도록 하겠습니다.
계좌 이체를 bankTransfer( ),
입금과 출금은 각각 deposiot( ), withdraw( )라는 메서드로 정의해봅시다.
deposit( )과 withdraw는 각자 고유한 데이터베이스와 커넥션을 맺고 작업을 처리합니다.
문제는 withdraw( )는 정상 처리되었는데 deposit( )에서 예외가 발생하는 경우입니다.
트랜잭션으로 관리 또는 묶는다는 표현은 AND 연산과 의미가 유사합니다(NOT OR).
withdraw( )와 deposit( )은 영속 계층에서 각각 데이터베이스와 연결을 맺고 처리하는데 하나의 트랜잭션으로 처리해야 할 경우
한쪽이 잘못되는 경우 이미 성공한 작업까지 다시 원상복귀 되어야 합니다.
그래서 bankTransfer( )를 작성할 때는 어느 한 쪽이 실패할 것을 염두에 두는 코드를 복잡하게 만들어야 합니다.
그런데 스프링은 이러한 트랜잭션 처리를 간단하게 XML 설정을 이용하거나 어노테이션 처리만으로 해버립니다.
😲...!
1. 데이터베이스 설계와 트랜잭션
DB의 저장 구조를 효율적으로 관리하기 위해 흔히 '정규화' 작업을 합니다.
정규화의 가장 기본은 중복된 데이터를 제거해서 저장 효율을 높이는 것입니다.
정규화를 진행하면 테이블은 늘어나고 각 테이블의 데이터 양은 줄어듭니다.
예를 들어,
시간이 흐르면 변경되는 데이터(나이 등)는 칼럼으로 기록하지 않습니다.
계산이 가능한 데이터를 칼럼으로 기록하지 않습니다.
누구에게나 정해진 값(요일 등)은 DB에서 취급하지 않습니다.
정규화가 잘 되었거나, 위의 규칙들이 반영된 데이터베이스의 설계에서는 트랜잭션이 많이 일어나지 않습니다.
정규화가 진행될수록 테이블은 점점 더 순수한 형태가 되어가는데 그럴수록 트랜잭션 처리의 대상에서 멀어집니다.
(튜닝의 끝은 순정이라는 말도 있습니다)
테이블은 더 간결해지겠지만 사실 쿼리를 쓰는 입장에서는 여러 개로 늘어난 테이블을 조인하거나 서브쿼리를 사용해야 하기 때문에 불편해지기는 합니다.
2. 트랜잭션 설정 실습
스프링 트랜잭션 설정은 XML과 어노테이션을 이용합니다.
트랜잭션을 이용하기 위해 Transaction Manager라는 것이 필요합니다.
*Transaction Manager: Service에서 예외 발생 시 예외 발생 전 실행된 모든 DAO 메서드 명령을 ROLLBACK 처리함
pom.xml에 spring-jdbc, spring-tx 라이브러리를 추가하고
MyBatis, Mybatis-spring, hikari 등의 라이브러리를 추가합니다.
Maven Repository 참고
root-context.xml에서는 NameSpace 탭의 tx 항목을 체크하고
트랜잭션을 관리하는 빈(객체)을 등록, 어노테이션 기반으로 트랜잭션을 설정하기 위해 <tx:annotation-driven> 태그를 등록합니다.
이제 어노테이션을 사용할 준비가 완료되었습니다.
1) 예제 테이블 생성
create table tbl_sample1(col1 varchar2(500));
create table tbl_sample2(col2 varchar2(50));
tbl_sample1의 col1은 varchar2(500)으로 설정된 반면 tbl_sample2의 col2는 varchar2(50)으로 설정되었습니다.50바이트 이상의 데이터를 넣는 상황이라면 tbl_sample1에는 정상적으로 insert 되지만,tbl_sample2에는 칼럼의 최대 길이보다 크기 때문에 insert 되지 않습니다.
데이터베이스와의 연결을 위해 Mapper 인터페이스를 작성합니다.
public interface Sample1Mapper {
@Insert("insert into tbl_sample1(col1) values (#{data}) ")
public int insertCol1(String data);
}
publicc interface Sample2Mapper {
@Insert("insert into tbl_sample2 (col2) values (#{data}) ")
public int insertCol2(String data);
}
2) 비즈니스 계층과 트랜잭션 설정
트랜잭션은 비즈니스 계층에서 이루어집니다.
먼저 트랜잭션이 적용되지 않았을 때 어떻게 처리되는지 보겠습니다.
public interface SampleTxService {
public void addData(String value);
}
@Service
@Log4j
public class SampleTxServiceImpl implements SampleTxService {
@Setter(onMethod_ = @Autowired)
private Sample1Mapper mapper1;
@Setter(onMethod_ = @Autowired)
private Sample2Mapper mapper2;
@Override
public void addData(String value) {
log.info("mapper1....");
mapper1.insertCol1(value);
log.info("mapper2....");
mapper2.insertCol2(value);
log.info("end....");
}
}
addData( ) 메서드로 데이터가 추가되도록 코드를 작성합니다.
mapper1과 mapper2의 insertCol 메서드가 호출되면 파라미터인 value를 통해
Mapper 인터페이스에 작성한 쿼리문이 실행되면서 데이터가 추가되는 루트로 이해하면 됩니다.
잘 추가되는지 확인하기 위해 서비스 테스트 클래스를 만들어줍니다.
@RunWith(SpringJUnit4ClassRunner.class)
@Log4j2
@ContextConfiguration({"file:src/main/webapp/WEB-INF/spring/root-context.xml"})
public class SampleTxServiceTests {
@Setter(onMethod_ = @Autowired)
private SampleTxService service;
@Test
public void testing() {
String str = "Starry\r\n Starry night\r\n" +
"Paint your palette blue and grey\r\n" +
"Look out on a summer's day";
log.info(str.getBytes().length);
service.addData(str);
}
}
testLong( )에서 50bytes 초과 500bytes 미만의 예문을 통해 tbl_sample1, tbl_sample2 테이블에 insert를 시도합니다.
테이블을 생성할 때 설정했던 칼럼의 크기 때문에
tbl_sample1에는 데이터가 추가되지만tbl_sample2에는 데이터가 추가되지 않았습니다.
콘솔창에서는 'value too large for column " 계정 이름"."TBL_SAMPLE2"."COL2"...이런 식으로 표시 됩니다.
트랜잭션 처리를 아직 하지 않았기 때문에 하나의 테이블에만 insert가 성공되었습니다.
3) @Transactional 어노테이션
그렇다면 트랜잭션 처리를 한다면 어떻게 처리될지 우리는 예상할 수 있습니다.
트랜잭션은 쪼개질 수 없는 하나의 묶여있는 작업이기 때문에
한 쪽에서 insert가 실패했다면 다른 한 쪽도 insert에 실패해야 합니다.
마치 공동체와도 같습니다.
한몸이 되어 움직이는 공동체 생활을 코딩에서도 배울 수 있습니다.
예제를 통해 이를 확인해 보도록 하겠습니다.
@Transactional
@Override
public void addData(String value) {
log.info("mapper1....");
mapper1.insertCol1(value);
log.info("mapper2....");
mapper2.insertCol2(value);
log.info("end....");
}
이전에 insert에 성공한 tbl_sample1의 데이터를 삭제하고 다시 테스트를 실행합니다.
이제 콘솔에는 rollback( )이 표시됩니다.
데이터베이스에서도 두 테이블 모두 아무 데이터가 들어가지 않는 것이 확인됩니다.
기존에서 달라진 것이라고는 @Transactional 어노테이션을 추가한 것 밖에는 없습니다.
이 어노테이션 하나로 동작끼리 하나로 묶여 작업을 수행하는 것이 정말 신기하지 않습니까?
4) @Transactional 어노테이션 속성
이 어노테이션을 사용하려면 적어도 어떤 친구인지 알고 써야하겠습니다.
경우에 따라 이 속성들을 조정해서 사용할 필요가 있습니다.
- 전파(Propagation) 속성
Propation_Madatory | 작업은 반드시 특정한 트랜잭션이 존재하는 상태에서만 가능 |
Propation_Nested | 기존에 트랜잭션이 존재하는 경우 포함되어 실행 |
Propation_Never | 트랜잭션 상황 하에 실행되면 예외 발생 |
Propation_Not_Supported | 트랜잭션이 있는 경우에는 트랜잭션이 끝날 때까지 보류된 후 실행 |
Propation_Required | 트랜잭션이 있으면 그 상황에서 실행, 없으면 새로운 트랜잭션 실행(default) |
Propation_Required_New | 대상은 자신만의 고유한 트랜잭션으로 실행 |
Propation_Supports | 트랜잭션을 필요로 하지 않으나 트랜잭션 상황 하에 있다면 포함되어 실행 |
- 격리(Isolation) 레벨
Default | DB 설정, 기본 격리 수준(기본 설정) |
Serializable | 가장 높은 격리, 성능 저하의 우려있음 |
Read_Uncommited | 커밋되지 않은 데이터에 대한 읽기 허용 |
Read_Commited | 커밋된 데이터에 대해 읽기 허용 |
Repeateable_Read | 동일 필드에 대해 다중 접근 시 모두 동일한 결과를 보장 |
- Read-only 속성: true인 경우 insert, update, delete 실행 시 예외 발생, 기본 설정은 false
- Rollback-for-예외: 특정 예외가 발생 시 강제로 Rollback
- No-rollback-for-예외: 특정 예외의 발생 시에는 Rollback 처리가 되지 않음
5) @Transactional 어노테이션 적용 순서
@Transactional을 메서드에 설정하는 것도 가능하지만 클래스나 인터페이스에 선언하는 것도 가능합니다.
1순위: 메서드
2순위: 클래스
3순위: 인터페이스
인터페이스에는 가장 기준이 되는 @Transactional과 같은 설정을 지정하고,
클래스나 메서드에는 필요하는 어노테이션을 처리하는 것이 좋습니다.
이번 포스팅에서는 AOP와 트랜잭션에 대해 다루어 보았습니다.
총총🐰
'Spring' 카테고리의 다른 글
[Spring] ResponseEntity의 개념 및 사용법 (0) | 2024.03.15 |
---|---|
[Spring] 댓글과 댓글 수에 대한 처리 (0) | 2024.03.08 |
[Spring] AOP에 대하여 (0) | 2024.03.07 |
[Spring] 댓글 처리(REST와 Ajax...스압주의) (0) | 2024.03.07 |
[Spring] MyBatis의 페이징 처리 (0) | 2024.03.05 |