다연이네

[days16] AOP와 트랜잭션 본문

Spring

[days16] AOP와 트랜잭션

 다연  2021. 2. 24. 13:10
반응형

AOP

AOP는 '관심사의 분리'를 추구한다. 예를 들어 나눗셈을 구현한다고 하면 '핵심 로직'은 두 개의 숫자를 나누는 것이지만, '주변 로직'은 0을 나누는 것ㅇ이 아닌지 등을 체크하는 것이다.

 

AOP는 과거에 개발자가 작성했던 '관심사 + 비즈니스 로직'을 분리해서 별도의 코드로 작성하도록 하고, 실행할 때 이를 결합하는 방식으로 접근한다. 실제 실행은 결합된 상태의 코드가 실행되므로 개발자들은 핵심 비즈니스 로직에만 근거하여 코드를 작성하고, 나머지는 어떤 관심사들과 결합할 것인지를 설정하는 것 만으로 모든 개발을 마칠 수 있다.

 

 

예제

1. pom.xml에 스프링 버전과 AOP 버전을 수정, 라이브러리 추가

  - 스프링AOP는 AspectJ라는 라이브러리의 도움을 많이 받음

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd">
   <modelVersion>4.0.0</modelVersion>
   <groupId>org.zerock</groupId>
   <artifactId>controller</artifactId>
   <name>ex04</name>
   <packaging>war</packaging>
   <version>1.0.0-BUILD-SNAPSHOT</version>
   <properties>
      <java-version>1.8</java-version>
      <org.springframework-version>5.0.7.RELEASE</org.springframework-version>
      <org.aspectj-version>1.9.0</org.aspectj-version>
      <org.slf4j-version>1.7.25</org.slf4j-version>
   </properties>
   <dependencies>
      <!-- Spring -->
      <dependency>
         <groupId>org.springframework</groupId>
         <artifactId>spring-context</artifactId>
         <version>${org.springframework-version}</version>
         <exclusions>
            <!-- Exclude Commons Logging in favor of SLF4j -->
            <exclusion>
               <groupId>commons-logging</groupId>
               <artifactId>commons-logging</artifactId>
            </exclusion>
         </exclusions>
      </dependency>
      <dependency>
         <groupId>org.springframework</groupId>
         <artifactId>spring-webmvc</artifactId>
         <version>${org.springframework-version}</version>
      </dependency>

      <!-- AspectJ -->
      <dependency>
         <groupId>org.aspectj</groupId>
         <artifactId>aspectjrt</artifactId>
         <version>${org.aspectj-version}</version>
      </dependency>

      <!-- Logging -->
      <dependency>
         <groupId>org.slf4j</groupId>
         <artifactId>slf4j-api</artifactId>
         <version>${org.slf4j-version}</version>
      </dependency>
      <dependency>
         <groupId>org.slf4j</groupId>
         <artifactId>jcl-over-slf4j</artifactId>
         <version>${org.slf4j-version}</version>
         <scope>runtime</scope>
      </dependency>
      <dependency>
         <groupId>org.slf4j</groupId>
         <artifactId>slf4j-log4j12</artifactId>
         <version>${org.slf4j-version}</version>
         <scope>runtime</scope>
      </dependency>
      <!-- p.54 <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> 
         <version>1.2.15</version> <exclusions> <exclusion> <groupId>javax.mail</groupId> 
         <artifactId>mail</artifactId> </exclusion> <exclusion> <groupId>javax.jms</groupId> 
         <artifactId>jms</artifactId> </exclusion> <exclusion> <groupId>com.sun.jdmk</groupId> 
         <artifactId>jmxtools</artifactId> </exclusion> <exclusion> <groupId>com.sun.jmx</groupId> 
         <artifactId>jmxri</artifactId> </exclusion> </exclusions> <scope>runtime</scope> 
         </dependency> -->
      <!-- @Inject -->
      <dependency>
         <groupId>javax.inject</groupId>
         <artifactId>javax.inject</artifactId>
         <version>1</version>
      </dependency>

      <!-- Servlet -->
      <!-- <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> 
         <version>2.5</version> <scope>provided</scope> </dependency> -->
      <!-- p.111 -->
      <!-- https://mvnrepository.com/artifact/javax.servlet/javax.servlet-api -->
      <dependency>
         <groupId>javax.servlet</groupId>
         <artifactId>javax.servlet-api</artifactId>
         <version>3.1.0</version>
         <scope>provided</scope>
      </dependency>

      <dependency>
         <groupId>javax.servlet.jsp</groupId>
         <artifactId>jsp-api</artifactId>
         <version>2.1</version>
         <scope>provided</scope>
      </dependency>
      <dependency>
         <groupId>javax.servlet</groupId>
         <artifactId>jstl</artifactId>
         <version>1.2</version>
      </dependency>

      <!-- Test p.54 -->
      <dependency>
         <groupId>junit</groupId>
         <artifactId>junit</artifactId>
         <version>4.12</version>
         <scope>test</scope>
      </dependency>

      <!-- p.54 -->
      <dependency>
         <groupId>org.springframework</groupId>
         <artifactId>spring-test</artifactId>
         <version>${org.springframework-version}</version>
      </dependency>
      <dependency>
         <groupId>org.projectlombok</groupId>
         <artifactId>lombok</artifactId>
         <version>1.18.18</version>
         <scope>provided</scope>
      </dependency>
      <dependency><!-- log4j.xml 설정 필요(자동) -->
         <groupId>log4j</groupId>
         <artifactId>log4j</artifactId>
         <version>1.2.17</version>
      </dependency>

      <!-- p.83 https://github.com/brettwooldridge/HiKariCP -->
      <dependency>
         <groupId>com.zaxxer</groupId>
         <artifactId>HikariCP</artifactId>
         <version>4.0.2</version>
      </dependency>

      <!-- p 90 -->
      <!-- https://mvnrepository.com/artifact/org.mybatis/mybatis -->
      <dependency>
         <groupId>org.mybatis</groupId>
         <artifactId>mybatis</artifactId>
         <!-- <version>3.4.6</version> -->
         <version>3.5.6</version>
      </dependency>
      <dependency>
         <groupId>org.mybatis</groupId>
         <artifactId>mybatis-spring</artifactId>
         <!-- <version>1.3.2</version> -->
         <version>2.0.6</version>
      </dependency>
      <dependency>
         <groupId>org.springframework</groupId>
         <artifactId>spring-tx</artifactId>
         <version>${org.springframework-version}</version>
      </dependency>
      <dependency>
         <groupId>org.springframework</groupId>
         <artifactId>spring-jdbc</artifactId>
         <version>${org.springframework-version}</version>
      </dependency>

      <!-- p 101 -->
      <dependency>
         <groupId>org.bgee.log4jdbc-log4j2</groupId>
         <artifactId>log4jdbc-log4j2-jdbc4</artifactId>
         <version>1.16</version>
      </dependency>

      <!-- p 147 jackson-databind -->
      <dependency>
         <groupId>com.fasterxml.jackson.core</groupId>
         <artifactId>jackson-databind</artifactId>
         <version>2.12.1</version>
      </dependency>

      <!-- p 149 -->
      <!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
      <dependency>
         <groupId>commons-fileupload</groupId>
         <artifactId>commons-fileupload</artifactId>
         <version>1.4</version>
      </dependency>

      <!-- p 357 -->
      <!-- https://mvnrepository.com/artifact/com.fasterxml.jackson.dataformat/jackson-dataformat-xml -->
      <dependency>
         <groupId>com.fasterxml.jackson.dataformat</groupId>
         <artifactId>jackson-dataformat-xml</artifactId>
         <version>2.12.1</version>
      </dependency>

      <!-- p 357 Java 인스턴스를 JSON 타입의 문자열로 변환 -->
      <!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
      <dependency>
         <groupId>com.google.code.gson</groupId>
         <artifactId>gson</artifactId>
         <version>2.8.6</version>
      </dependency>

<!-- AOP 중요 -->
      <!-- https://mvnrepository.com/artifact/org.aspectj/aspectjweaver -->
      <dependency>
         <groupId>org.aspectj</groupId>
         <artifactId>aspectjweaver</artifactId>
         <version>${org.aspectj-version}</version>
      </dependency>

      <!-- https://mvnrepository.com/artifact/net.coobird/thumbnailator -->
      <dependency>
         <groupId>net.coobird</groupId>
         <artifactId>thumbnailator</artifactId>
         <version>0.4.8</version>
      </dependency>

      <!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz -->
      <dependency>
         <groupId>org.quartz-scheduler</groupId>
         <artifactId>quartz</artifactId>
         <version>2.3.0</version>
      </dependency>

      <!-- https://mvnrepository.com/artifact/org.quartz-scheduler/quartz-jobs -->
      <dependency>
         <groupId>org.quartz-scheduler</groupId>
         <artifactId>quartz-jobs</artifactId>
         <version>2.3.0</version>
      </dependency>

      <dependency>
         <groupId>org.springframework.security</groupId>
         <artifactId>spring-security-web</artifactId>
         <version>${org.springframework-version}</version>
      </dependency>

      <dependency>
         <groupId>org.springframework.security</groupId>
         <artifactId>spring-security-config</artifactId>
         <version>${org.springframework-version}</version>
      </dependency>

      <dependency>
         <groupId>org.springframework.security</groupId>
         <artifactId>spring-security-core</artifactId>
         <version>${org.springframework-version}</version>
      </dependency>

      <!-- https://mvnrepository.com/artifact/org.springframework.security/spring-security-taglibs -->
      <dependency>
         <groupId>org.springframework.security</groupId>
         <artifactId>spring-security-taglibs</artifactId>
         <version>${org.springframework-version}</version>
      </dependency>
   </dependencies>
   <build>
      <plugins>
         <plugin>
            <artifactId>maven-eclipse-plugin</artifactId>
            <version>2.9</version>
            <configuration>
               <additionalProjectnatures>
                  <projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>
               </additionalProjectnatures>
               <additionalBuildcommands>
                  <buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand>
               </additionalBuildcommands>
               <downloadSources>true</downloadSources>
               <downloadJavadocs>true</downloadJavadocs>
            </configuration>
         </plugin>
         <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.5.1</version>
            <configuration>
               <source>1.8</source>
               <target>1.8</target>
               <compilerArgument>-Xlint:all</compilerArgument>
               <showWarnings>true</showWarnings>
               <showDeprecation>true</showDeprecation>
            </configuration>
         </plugin>
         <plugin>
            <groupId>org.codehaus.mojo</groupId>
            <artifactId>exec-maven-plugin</artifactId>
            <version>1.2.1</version>
            <configuration>
               <mainClass>org.test.int1.Main</mainClass>
            </configuration>
         </plugin>
      </plugins>
   </build>
</project>

 

2. 서비스 계층 설계

SampleService 인터페이스

package org.zerock.service;

public interface SampleService {

  public Integer doAdd(String str1, String str2)throws Exception;

}

SampleServiceImpl

package org.zerock.service;

import org.springframework.stereotype.Service;

@Service
public class SampleServiceImpl implements SampleService {

	@Override
	public Integer doAdd(String str1, String str2) throws Exception {

		return Integer.parseInt(str1) + Integer.parseInt(str2);

	}
}

 

3. Advice 작성

위의 SampleServiceImpl 코드를 보면 기존에 항상 하던 log.info()가 빠져있다. 지금까지 해왔던, 로그를 기록해 오던 일은 '반복적이면서 핵심 로직도 아니고, 필요하기는 한 기능'이기 때문에 '관심사'라고 볼 수 있다.

로그를 기록해주는 LogAdvice를 설계해보자

org.zerock.aop에 LogAdvice.java 생성

package org.zerock.aop;

import java.util.Arrays;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

import lombok.extern.log4j.Log4j;

@Aspect //보조 기능을 하는 구현 클래스
@Log4j  //로그 기록 
@Component // 컴포넌트 스캔
public class LogAdvice {

  @Before( "execution(* org.zerock.service.SampleService*.*(..))") //AspectJ 문법
  public void logBefore() {

    log.info("========================");
  }
  
  @Before("execution(* org.zerock.service.SampleService*.doAdd(String, String)) && args(str1, str2)")
  public void logBeforeWithParam(String str1, String str2) {

    log.info("str1: " + str1);
    log.info("str2: " + str2);
  }  
  //before두개면 다 실행

  @AfterThrowing(pointcut = "execution(* org.zerock.service.SampleService*.*(..))", throwing="exception")
  public void logException(Exception exception) {
    
    log.info("Exception....!!!!");
    log.info("exception: "+ exception);
  
  }
  
  
  @Around("execution(* org.zerock.service.SampleService*.*(..))")
  public Object logTime( ProceedingJoinPoint pjp) {
    
    long start = System.currentTimeMillis();
    
    log.info("Target: " + pjp.getTarget());
    log.info("Param: " + Arrays.toString(pjp.getArgs()));
    
    
    //invoke method 
    Object result = null;
    
    try {
      result = pjp.proceed();
    } catch (Throwable e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
    
    long end = System.currentTimeMillis();
    
    log.info("TIME: "  + (end - start));
    
    return result;
  }
}

 

4. AOP 설정

스프링 프로젝트에 AOP를 설정하는 것은 스프링 2버전 이후에는 간단히 자동으로 Proxy 객체를 만들어주는 설정을 추가해주면 된다.

프로젝트의 root-context.xml에서 Namespaces에 aop와 context를 추가하고 아래 코딩을 추가하자

   <!-- p.455 -->
   <context:annotation-config></context:annotation-config>   
   <context:component-scan base-package="org.zerock.service" />
   <context:component-scan base-package="org.zerock.aop" />   
   <aop:aspectj-autoproxy></aop:aspectj-autoproxy>

컴포넌트 스캔을 통해 패키지들을 스캔하고, 이 과정에서 SampleServiceImpl 클래스와 LogAdvice가 스프링의 빈으로 등록된다. <aop:aspectj-autoproxy>를 이용해 LogAdvice에 설정한 @들이 동작하게 된다.

-Java 설정을 이용하는 경우 p456

 

5. AOP 테스트

정상적인 상황이라면 SampleServiceImpl, LogAdvice는 같이 묶여서 자동으로 Proxy 객체가 생성된다. 

테스트 폴더에 SampleServiceTests 추가

package org.zerock.service;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import lombok.Setter;
import lombok.extern.log4j.Log4j;

@RunWith(SpringJUnit4ClassRunner.class)
@Log4j
@ContextConfiguration({ "file:src/main/webapp/WEB-INF/spring/root-context.xml" })
//Java설정의 경우 
//@ContextConfiguration(classes= {RootConfig.class})
public class SampleServiceTests {

  //@Setter(onMethod = @__({ @Autowired }))
  @Setter(onMethod_ = @Autowired)
  private SampleService service;
  
  
  @Test
  public void testClass() {
    //보조기능 출력
    log.info(service);
    log.info(service.getClass().getName());     
    
  }
  
  @Test
  public void testAdd() throws Exception {
    
    log.info(service.doAdd("123", "456"));
    
  }
  
  @Test
  public void testAddError() throws Exception {
    
    log.info(service.doAdd("123", "ABC"));
    
  }  
}

 

 

스프링에서 트랜잭션 관리

 

스프링 트랜잭션 설정은 AOP와 같이 XML을 이용해서 설정하거나 어노테이션을 이용해서 설정이 가능하다.

 

1. pom.xml에 라이브러리 추가 - spring-jdbc, spring-tx, mybatis, mybatis-spring, hikari 등

(위 pom.xml과 동일) 

 

2. root-context.xml에는 Namespaces 탭에서 tx 항목 체크 아래 태그 등록

<tx:annotation-driven />

: 어노테이션 기반으로 트랜잭션을 설정할 수 있도록 함

-Java 설정을 이용한 트랜잭션 설정 p470

 

3. 예제 테이블 생성

create table tbl_sample1(col1 varchar2(500));
create table tbl_sample2(col2 varchar2(50));

Sample1MApper - tbl_sample1 테이블에 데이터 삽입하는 메소드

package org.zerock.mapper;

import org.apache.ibatis.annotations.Insert;

public interface Sample1Mapper {

	@Insert("insert into tbl_sample1 (col1) values (#{data}) ")
	public int insertCol1(String data);

}

Sample2Mapper - tbl_sample2 테이블에 데이터 삽입하는 메소드

package org.zerock.mapper;

import org.apache.ibatis.annotations.Insert;

public interface Sample2Mapper {

	@Insert("insert into tbl_sample2 (col2) values (#{data}) ")
	public int insertCol2(String data);

}

 

4. 비즈니스 계층과 트랜잭션 설정

트랜잭션은 비즈니스 계층에서 이루어지므로, org.zerock.service 계층에서 Sample1Mapper, Sample2Mapper를 사용하는 SampleTxService, SampleTxServiceImpl 클래스 설계

SampleTxService

package org.zerock.service;

public interface SampleTxService {

	public void addData(String value);
}

SampleTxServiceImpl 

package org.zerock.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.zerock.mapper.Sample1Mapper;
import org.zerock.mapper.Sample2Mapper;

import lombok.Setter;
import lombok.extern.log4j.Log4j;

@Service
@Log4j
public class SampleTxServiceImpl implements SampleTxService {

  @Setter(onMethod_ = { @Autowired })
  private Sample1Mapper mapper1;
  
  @Setter(onMethod_ = { @Autowired })
  private Sample2Mapper mapper2;
  
  @Transactional
  @Override
  public void addData(String value) { //마크: 내부적으로 AOP 처리된다는 의미
    
    log.info("mapper1....................");
    mapper1.insertCol1(value);
    
    log.info("mapper2.....................");
    mapper2.insertCol2(value);
    
    log.info("end..........................");
    //insert 작업을 2번 하는 동안 @트랜잭션 걸었음
    // 하나라도 잘못되면 전부 롤백되는지 확인 (O)
  }

}

 

5. Test

package org.zerock.service;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;

import lombok.Setter;
import lombok.extern.log4j.Log4j;

@RunWith(SpringJUnit4ClassRunner.class)
@Log4j
@ContextConfiguration({ "file:src/main/webapp/WEB-INF/spring/root-context.xml" })
//Java설정의 경우 
//@ContextConfiguration(classes= {RootConfig.class})

public class SampleTxServiceTests {

  @Setter(onMethod_ = { @Autowired })
  private SampleTxService service;
  
  
  

  @Test
  public void testLong() {
/*    
    String str ="Starry\r\n" + 
        "Starry night\r\n" + 
        "Paint your palette blue and grey\r\n" + 
        "Look out on a summer's day";
 */
	  
	    String str ="Starry";
    log.info("길이: "+str.getBytes().length); 
    
    service.addData(str);    
    //위 글이 50바이트보다 크면 샘플2에 안들어가져서 트랜잭션에 의해 둘 다 롤백
    //작으면 커밋 (두 테이블 모두 잘 들어감)
  }
  
  
}

@Transaction 어노테이션에 의해 트랜잭션 처리가 잘 된다.

반응형
Comments