서비스 계층 분리


이전에 다루었던 DAO에서는 CRUD 기능만 가지고 있기 때문에 비즈니스 로직이 들어가기에는 적합하지 않다. 그렇다면 컨트롤러에 이 비즈니스 로직을 담게 될텐데, 이렇게 컨트롤러에 담는 것 또한 적절하지 않다. 따라서 비즈니스 로직을 담당해줄 수 있는 계층을 만들어주고, 이 Business(Service) 계층에서 DAO를 주입 받고 컨트롤러가 이 계층으로부터 주입 받아 사용하는 방식을 따른다. 따라서 이 서비스 계층이 필요한 것이다.

 

ex) RegisterController ⇄ UserService ⇄ UserDao 혹은 UserHistoryDao ⇄ DB

 

Controller - @Controller, Service - @Service, DAO - @Repository   => 세 어노테이션 모두 @Component 어노테이션을 포함

 

 

 

TransactionManager


DAO의 각 메서드는 개별 Connection을 사용(selectUser() → DB, insertUser() → DB)한다. 그리고 Tx은 1개의 Connection에서 이루어진다. 예를 들어 deleteUser()라는 메서드가 두 번 호출되면 이 두 메서드는 별도의 Connection에서 실행되는 것이다. 따라서, 하나의 Tx로 묶기 위해서는 같은 Connection을 사용해야 하는데 이를 위해 필요한 것이 바로 TransactionManager 이다.

 

DAO에서 Connection을 얻거나 반환할 때 DataSourceUtils를 사용해야 한다. 그래야 TransactionManager를 사용 가능하다. 

ex) conn = DataSourceUtils.getConnection(ds);

      DataSourceUtils.releaseConnection(conn, ds);

 

 

다음은 TransactionManager로 Transaction을 적용하는 예제 코드이다.

PlatformTransactionManager tm = new DataSourceTransactionManager(ds);
TransactionStatus status = tm.getTransaction(new DefaultTransactionDefinition());

// Tx 시작
try {
    a1Dao.insert(1, 100);
    a1Dao.insert(1, 200);
    tm.commit(status); // Tx 끝 - 성공(커밋)
} catch(Exception ex) {
    tm.rollback(status); // Tx 끝 - 실패(롤백)
}

 

new DataSourceTransactionManager()를 통해 TxManager를 생성한다. 이후 new DefaultTransacctionDefinition()을 통해 Tx의 속성을 정의해준다. try-catch문은 Tx을 시작하고 하나라도 예외가 발생하면 둘다 모두 롤백되는 구조이다.

 

 

※ TxManager를 빈으로 등록해줄 수도 있다. <tx:annotation-driven/> 태그를 통해 @Transactional 어노테이션을 사용 가능하다.

 

 

 

@Transactional


AOP를 이용해 핵심 기능과 부가 기능을 분리해줄 수 있다. 바로 @Transactional 어노테이션을 통해 위에서 TransactionManager를 통해 Tx을 적용하는 예제 코드처럼 핵심 기능을 수행하는 두 메서드 a1Dao.insert(1, 100), a1Dao.insert(1, 200)에만 집중할 수 있도록 고정된 위, 아래의 부가적인 코드들을 자동으로 넣어주는 것이다. 

 

@Transactional은 메서드 뿐만 아니라 클래스나 인터페이스에도 붙일 수 있고, 클래스나 인터페이스에 붙이게 되면 클래스 혹은 인터페이스 내의 모든 메서드에 적용된다.

 

 

 

@Transactional의 속성은 다음과 같다.

 

속성 설명
propagation Tx의 경계(boundary)를 설정하는 방법을 지정
isolation Tx의 isolation level을 지정(READ UNCOMMITED, READ COMMITED, REPEATABLE READ, SERIALIZABLE)
readonly Tx이 데이터를 읽기만 하는 경우, true로 지정하면 성능 향상
rollbackFor 지정된 예외가 발생하면, Tx을 rollback. RuntimeException과 Error는 자동 rollback.
norollbackFor 지정된 예외가 발생해도 Tx을 rollback 하지 않음
timeout 지정된 시간(초) 내에 Tx이 종료되지 않으면, Tx을 강제 종료

 

propagation 속성의 값은 다음과 같다.

 

설명
REQUIRED Tx이 진행 중이면 참여하고, 없으면 새로운 Tx 시작(디폴트)
REQUIRED_NEW Tx이 진행 중이건 아니건, 새로 Tx 시작
NESTED Tx이 진행 중이면, Tx의 내부 Tx으로 실행
MANDATORY 반드시 진행 중인 Tx 내에서만 실행 가능. 아니면 예외 발생
SUPPORTS Tx이 진행 중이건 아니건 상관없이 실행
NOT_SUPPORTS Tx 없이 처리. Tx이 진행 중이면 잠시 중단(suspend)
NEVER Tx 없이 처리. Tx이 진행 중이면 예외 발생

 

여기서 두 가지 비교 포인트가 있다.

 

REQUIRED_NEW vs NESTED ?

 

=> REQUIRED_NEW의 경우 Tx 안에 다른 Tx를 의미하고, NESTED의 경우 같은 Tx 안에 있는 SubTx(save point)를 의미한다. 여기서 save point는 Tx 작업의 단위가 길어졌을 때 예외가 발생하면 롤백하여 처음으로 돌아가는 것은 비효율적일 수 있기 때문에 중간에 롤백 해줄 수 지점을 말한다.

 

 

REQUIRED vs REQUIRED_NEW ?

 

=> REQUIRED의 경우 Tx가 기존에 있으면 새로 Tx를 만들지 않고 이 기존에 있는 Tx에 참여하는 것이고, REQUIRED_NEW의 경우 Tx 내에 다른 Tx가 새로 만들어지는 것이다.

 

 

다음은 REQUIRED에 대한 예제 코드 및 그림이다.

@Transactional(propagation = Propagation.REQUIRED)
public void insertA1WithTx() throws Exception {
    a1Dao.insert(1,100);
    insertB1WithTx();
    a1Dao.insert(1,200);
}

@Transactional(propagation = Propagation.REQUIRED)
public void insertB1WithTx() throws Exception {
    b1Dao.insert(1,100);
    b1Dao.insert(1,200);
}

 

 

insertA1WithTx() 메서드를 통해 Tx1이 이미 시작되어 있고, insertB1WithTx() 메서드를 호출하여도 Tx1 내에서 새로운 Tx을 만드는 것이 아닌 기존에 시작돼 있던 Tx1에 참여한다. 따라서 네 개 중 하나라도 예외가 발생하면 Tx1 시작부분으로 롤백된다.

 

 

다음은 REQUIRED_NEW에 대한 예제 코드와 그림이다.

@Transactional(propagation = Propagation.REQUIRED)
public void insertA1WithTx() throws Exception {
    a1Dao.insert(1,100);
    insertB1WithTx();
    a1Dao.insert(1,200);
}

@Transactional(propagation = Propagation.REQUIRED_NEW)
public void insertB1WithTx() throws Exception {
    b1Dao.insert(1,100);
    b1Dao.insert(1,200);
}

insertA1WithTx() 메서드를 통해 Tx1이 이미 시작되어 있지만 insertB1WithTx() 메서드를 호출하면 Tx1 내에서 새로운 Tx인 Tx2가 만들어진다. 따라서 B1과 B2가 모두 예외가 발생하지 않고 성공하여 커밋되면 A2에서 예외가 발생하여도 A1과 A2가 롤백되는 것이지, 다른 Tx인 Tx2의 B1과 B2는 이미 커밋되어 영향 받지 않는다는 것에 유의한다. 

+ Recent posts