SpringBoot JUnit Test – 파라미터 반복 테스트

> SpringBoot – JUnit 파라미터 반복 테스트

  JUnit 테스트 기동을 위한 Spring 관련 환경설정 어노테이션은 어려가지가 있다.  그러나 환경 셋팅의 심플함을 주무기로 하는 SpringBoot의 강점 활용을 위해 여러 어노테이션들에 대한 학습 보다는 가능한 간단하게 설정 후 테스트를 진행할 수 있는 방법을 알아보려 한다.   동일한 Request에 대한 테스트라 할지라도 고려해야하는 Input 항목이 많을 수록, 테스트 케이스는 승수적으로 늘어날 가능성이 있다.  바로 Input 값들의 조합 (파라미터)을 변경해가며 반복 테스트를 수행해야 하는 경우이다. 금융사들의 업무시스템의 경우 더욱 그러한 특징이 도드라지지 않나 싶다.  SpringBoot – JUnit을 통해 해당 테스트를 수행하는 방법을 알아보고자 한다.

  사족이지만, TDD를 정착시켜 개발문화 및 효율을 한단계 앞으로 나아가게 하려는 움직임이 국내에도 많이 있는 것 같다.  구태여 TDD를 꺼내지 않더라도, JUnit 등의 테스트 프레임웍의 활용은 금융 외의 다른 도메인에서는 그 중요성에 집중하는 개발자들이 많다.  그러나 금융사들의 IT부서 대부분에게 Java 환경으로의 전환은 단순한 개발언어의 변경 외의 의미가 없어 보인다. (특히 계정계)  내가 처음 금융개발을 시작하게된 시점과 테스트방법은 전혀 나아가지 못 한 것 같다.

  일면 이해하는 면이 있다. 컴플라이언스 관련 개발이 많은 금융산업의 특성상 시간은 흘러가고 릴리즈 일자는 미룰 수 없다.   비즈니스 담당부서(고객)들은 IT 의존도가 높은 경우가 많고 또 미룰수 없는 릴리즈의 특징을 교묘히 악용하는 경우도 있다.(결국에는 그 날짜에 릴리즈하게 되니까)  이에 개발자들은 조금은 피해의식을 가지고 있다.  그렇기에 요구사항을 정제하고 테스트케이스를 미리 만들려는 의지를 얻기가 쉽지않다. 

  금융사의 IT개발, 운영 부서는 테스트의 중요성도 나름은 인식은 한다.  다만 더 나은 방법의 테스트에 대한 연구가 조금은 부족하고 도메인 지식 (금융업무 지식)을 가장 강조하면서도 테스트케이스의 작성은 결국 고객만의 역할이라는 인식, 그리고 마지막으로 변경 적용의 엄격함 / 비효율도 소스품질의 저하를 불러오는 악순환의 고리가 아닌가 싶다.  타국 이긴하지만, 역시 금융환경에서 우리와 비슷한 악조건을 이겨나가 결국 애자일, TDD를 정착시켰던 개발자의 [소프트웨어 장인]이란 책이 떠오른다.


> Dependency (Maven)

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>

위와 같이 [spring-boot-starter-test] 의존성 추가 하나만으로 JUnit, Mockito, Hamcrest 등의 테스트를 위한 lib의 사용이 SpringBoot 안에서 가능해진다.

운영 환경에서는 활용되지 않기에, 배포제외 및 테스트 클래스 패스에만 적용을 위해 <scope>test</scopt> Maven Option을 적용하였다.


> JUnit 반복 테스트 Code

@RunWith(Parameterized.class)
@SpringBootTest
public class EventCacheDAOSearchTest {
  
    @ClassRule
    public static final SpringClassRule springClassRule = new SpringClassRule();

    @Rule
    public final SpringMethodRule springMethodRule = new SpringMethodRule();    
    
    private String subjcode;
    private String searchText;
    private int pageNo;
    private boolean freeOnly;
    
    @Autowired
    private EventCacheDAO eventCacheDAO;
    
    public EventCacheDAOSearchTest(String subjcode, String searchText, int pageNo, boolean freeOnly) {
        this.subjcode = subjcode;
        this.searchText = searchText;
        this.pageNo = pageNo;
        this.freeOnly = freeOnly;
     }    
    
    @Parameterized.Parameters
    public static List parametersForSearch() {
       return Arrays.asList(new Object[][] {
           {"ALL","서초구", 1, false }
          ,{"ALL","서초구", 2, false }
          ,{"ALL","서초구", 1, true  }
          ,{"ALL","연주회", 1, false  }
          ,{"ALL","바이올린", 1, false }   
          ,{"1","강남", 1, true  }
          ,{"1","강남", 2, true  }
          ,{"18","", 1, true  }
          ,{"19","교육", 1, true  }        
       });
    }
    
    //SQL SearchText, EhCache Search간 결과비교
    @Test
    public void compareSearchTextMethod(){        
        
        List<Seoulevent> allEventList = eventCacheDAO.getEventsBySearchText(this.subjcode, this.searchText, this.pageNo, this.freeOnly);                 
        List<Seoulevent> allEventListCache = eventCacheDAO.getCacheEventsBySearchText(this.subjcode, this.searchText, this.pageNo, this.freeOnly);             
        assertTrue(allEventList.size() == allEventListCache.size());   
    
    }
    
}
  1. @RunWith(Parameterized.class) – 파라미터 반복테스트를 위한 어노테이션 사용이다.  테스트 Class의 생성자 인자를 통해 파라미터를 받아 인스턴스 변수를 이용한 테스트가 가능하게 해준다.  

  2. @SpringBootTest – 해당 Test ClassPath가 속한 Project의 SpringBoot Application Config를 자동으로 Load하여 주며(application.properties 등), 테스트를 위한 일부 어노테이션 사용을 가능하게 해준다.  웹서버가 포함된 SpringBoot의 특징을 활용해 웹서버 기동 환경을 만들어주기도 한다. [@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)]
    SpringBoot가 아닌 Spring에서의 @ContextConfiguration, @SpringApplicationConfiguration 를 대체한다.  

  3. @ClassRule, @Rule (선언된 멤버) – Test Class의 기준을 정하는 JUnit 어노테이션이다.  위의 선언된 문장은 2문장을 함께 사용할 경우 @RunWith(SpringRunner.class)를 대체 한다.  @RunWith(SpringRunner.class or SpringJUnit4ClassRunner.class) 는 @RunWith는 JUnit 내장된 runner 대신 SpringJUnit4ClassRunner.class 클래스를 참조하여 테스트를 실행하도록 해준다.  Bean 등을 위한 Spring의 여러 Container를 일반적인 Load 시점과 마찬가지로 기동하는데 그 목적이 있다.   @RunWith는 다중 인자를 허용하지 않는다.  그렇기에 부득이 하게 Parameterized.class를 사용하기 위하여 위와 같이 선언하였다.  

  4. @Parameterized.Parameters – 파라미터 정의를 위한 어노테이션이다.  해당 어노테이션이 선언된 메서드는 List형으로 파라미터를 반환한다.  List item의 멤버 순서대로 해당 파라미터들은 본 Class 생성자에 주입된다.  그러므로 생성자를 그에 맞게 선언하여 파라미터를 해당 클래스의 인스턴스 멤버에 입력하는 등에 처리가 필요하다.   List의 멤버 Size (갯수) 만큼 @Test 어노테이션이 붙은 메소드들이 반복 수행된다.  

  5. 예로 보여진 위의 코드는 결과가 같다고 추정되는 2가지의 메소드를, 입력값 변화 반복테스트를 통해 결과를 비교하여 검증하는 테스트이다.

SpringBoot – Transaction (트랜잭션)

> Transaction (트랜잭션)

너무 기본적인 이야기 일수도 있지만, 트랜잭션은 처리의 원자성을 보장하기 위한 개념이다.   처리의 원자성이란, 한번에 처리되기 원하는 액션들의 그룹핑을 보장하는 것으로 표현하면 어떨까 싶다.  Cache에도 분명 적용가능하긴하나 보통은 액션이란 Database – DML(Insert/Update/Delete)이며, 결국 Commit/Rollback, All or Nothing 으로 그 원자성을 보장한다.  몇가지 예외 적인 경우를 제외하고 나면 트랜잭션 관리가 없는 형태의 Business System 개발은 감히 있을 수 없다고 말해도 무방하지 않을까 생각된다.

새로 시작하는 회사이긴 했지만, 주 System에 Spring Framework를 사용하는 어떤 금융사(핀테크)의 경우 트랜잭션 처리에 대한 표준을 정하지 않고 개발이 거의 완료단계까지 진행된 경우도 보았다.  거의 모든 Request가 Non-transaction으로 처리되고 있었다.   꽤나 복잡한 형태의 거래 형태도 존재 했기 때문에, 트랜잭션 관리 부재에 대한 문제는 불거져 나오기 시작했다.  Spring의 강점인 선언적 Transaction을 사용하여 어느 정도 문제는 커버할 수 있었으나, 많은 부분을 위험요소로 남겨둔채로 Release까지 진행됬다.

조금은 반대의 경우이긴 한데 몇 몇 금융회사를 다니며 느낀점을 또 잠시 이야기 하자면, 트랜잭션의 원자성 보장에 대해 금융IT 개발자의 대부분이 무감각하며 특별히 신경쓰지 않았다.  EJB, Spring등의 트랜잭션 관리가 비교적 손쉬운 Framework를 사용중인 회사도 마찬가지 였다.  그도 그럴 것이 금융시스템의 경우 위에서 이야기한 트랜잭션 원자성 보장이 필수적이며, 또 그렇기에 당연한 개념으로 인지하는 경향 때문이 아닌가 싶다.   차세대와 같은 프로젝트 초기에 Request 처리 방법이 확정되면 그 당연한것에 대한 구현기법에 의문을 갖는 사람이 없었다.  All or Nothing 이라는 개념은 분명 중요하다.  그러나 궁금해 하지 않는 이러한 성향은  뒤집어 이야기하면 트랜잭션의 분리, 전파를 조정하여 원하는 형태의 세세한 트랜잭션을 처리하는 기법에 대해 친숙하지 않다고 말할 수도 있을 것 같다.

이 포스트에서는 SpringBoot에서의 Transaction 여러 적용 방법 중 하나를 알아보며,  Transaction 경계 및 일부 특징에 대해서도 알아보고자 한다.

 

Spring에서 Transaction 관리를 위해 사용되는 AOP의 프록시 매커니즘은 두가지이다.

  • JDK Dynamic Proxy 
    JDK Dynamic Proxy는 인터페이스에 대한 Proxy만을 지원하며, 클래스에 대한 Proxy를 지원할 수 없다.  JDK Proxy가 가지는 또 하나의 단점은 Target 클래스에 Proxy를 적용할 때 PointCut에 정보에 따라 Advice되는 메써드와 그렇지 않은 메써드가 존재한다.  그러나 JDK Proxy를 사용할 경우 Target 클래스에 대한 모든 메써드 호출이 일단 JVM에 Intercept한 다음 Advice의 invoke 메써드를 호출하게 된다. 그 후에 이 메써드가 Advice되는 메써드인지 그렇지 않은지를 판단하게 된다.  이 과정에서 JVM에 의하여 Intercept한 다음 invoke 메써드를 호출할 때 JDK의 reflection을 이용하여 호출하게 되는것이다.  이는 Proxy를 사용할 때 실행속도를 저하시키는 원인이 된다.
     
  • CGLIB Proxy
    CGLIB Proxy 또한 JDK Proxy처럼 Runtime시에 Target 메써드가 호출될 때 해당 메써드의 Advice적용 여부를 결정하게 된다. 그러나 CGLIB는 메써드가 처음 호출 되었을때 동적으로 bytecode를 생성하여 이후 호출에서는 재사용하는 과정을 거치게 된다.  이 같은 과정을 통하여 두번째 호출이후부터는 실행속도의 향상을 가져올 수 있는 방법을 사용하고 있다.  또한 CGLIB Proxy는 클래스에 대한 Proxy가 가능하다.

    (토비의스프링(1권) 참조 – CGLIB Proxy는 final 클래스에는 적용할 수 없으며, 상속을 통해 프록시를 만들기 때문에 해당 클래스의 생성자가 2번 호출된다.  또한 Class단위로 적용되기 때문에 불필요한 public 메서드에까지 트랜잭션이 적용된다.   클래스프록시는 코드를 함부로 손댈 수 없는 레거시 코드나, 여타제한 때문에 인터페이스를 만들기 어려울 경우에만 사용해야한다.)

    -> (SpringBoot Ver 1.5.3,  Spring Ver 4.3.7 기준) 현재 해당 문제는 모두 해결 된것으로 확인 됐다.  이제 CGLIB Proxy는 final클래스에도 적용가능하며 생성자가 2번 호출되지 않는다.  또한 메서드 단위로도 트랜잭션 설정이 가능하다.   오히려 JDK Dynamic Proxy가 주는 문제나 성능 이슈로 SpringBoot의 기본 Default는 CGLIB 으로 되어 있다.


  1. SpringBoot에서의 설정 및 Transaction 속성

    SpringBoot를 이용하면 전통적인 Spring의 방식과는 다르게 기본적으로 Transaction 관리를 위한 별도 설정(XML, Javaconfig) 없이 트랜잭션 어노테이션사용이 바로 가능하다.  (CGLIB Proxy Default)
    아래는 일반적인 트랜잭션의 속성이다.  

    • 트랜잭션의 속성 1 (전파 – Propagation)

      트랜잭션의 경계를 정의하며 시작 방법을 결정하는 속성이다.   실제 Business, 혹은 비기능적 요구사항에 대한 개발을 진행하다보면 트랜잭션 경계를 가져가야 할 경우가 있다.  이를 태면, 메인 로직이 진행되고 메인 로직은 트랜잭션으로 묶어야 할 여러 Action을 포함한다고 하자.  그리고 이와 동시에 어떤 Repository(DB)에 메인 로직이 진행을 알리는 처리LOG를 적재(Insert)하고자 한다면,  이 처리LOG는 메인 로직이 중간에 어떤 이유에 있어서든(의도한 Exception 이거나 아니더라도) 중단 되더라도 메인 로직의 commit or rollback과 관계 없이 적재가 필요할 수 있다.  그럴경우 메인 로직과는 별개의 트랜잭션이 시작되어야 할 것이다.  (새로운 트랜잭션 경계의 시작)

      > REQUIRED  :  Defualt 속성이다.  트랜잭션의 시작 시점에 이미 진행중인 트랜잭션이 있으면 해당 트랜잭션에 참여하며 없을 경우 새로운 트랜잭션을 시작한다.
      > REQUIRES_NEW  :  항상 새로운 트랜잭션을 시작한다.   이미 진행중인 트랜잭션이 있다면 잠깐 보류되고 해당 트랜잭션 경계가 종료 된 후 다시 시작된다. 
      > SUPPORT  :  이미 진행중인 트랜잭션이 있으면 참여하고, 없을 경우 Non-transaction으로 시작한다.
      > MANDATORY  :  이미 진행중인 트랜잭션이 반드시 있어야만 해당 경계를 넘어 시작할 수 있다.  없을 경우 Exception을 발생시킨다.
      > NOT_SUPPORT  :  Non-transaction으로 시작하며, 이미 진행중인 트랜잭션이 있으면 잠시 보류시킨다.
      > NEVER  :  Non-transaction 상태에서만 해당 경계를 넘어갈 수 있다.  이미 진행중인 트랜잭션(parent)가 있으면 예외를 발생시킨다.  
      > NESTED  :  이미 진행중인 트랜잭션(parent)이 있을 경우 중첩트랜잭션을 생성하여 시작한다.  생성된 중첩트랜잭션은 (parent)가 rollback되면 함께 되지만, 해당 트랜잭션안에서의 Commit/Rollback은 (parent)에 영향을 주지 않는다.  이미 진행중인 트랜잭션이 없을 경우 새로운 트랜잭션을 만든다.

    • 트랜잭션의 속성 2(격리수준-Isolation)

      동시에 여러 Thread 별로 여러 트랜잭션이 진행될 때, 결과를 타 트랜잭션에 어떻게 노출할 것인지를 결정한다.

      > DEFAULT  : 사용하는 데이터 액세스 기술 또는 DB드라이버의 디폴트 설정을 따른다.  대부분의 DB는 READ_COMMITTED를 기본으로 한다.
      > READ_UNCOMMITTED  :  가장 낮은 격리수준.   트랜잭션의 종료에 따른 Commit / Rollback이 이루어지지 않아도 해당 변화가 다른 트랜잭션에 노출된다. (성능은 가장 좋음)
      > READ_COMMITTED  :  Commit / Rollback 되지 않은 정보는 다른 트랜잭션에서 읽을 수 없다.  서로 다른 트랜잭션이 동시에 Row를 핸들링하는 경우 의도치 않은 결과가 올 수 있다.
      > REPEATABLE_READ  :  하나의 트랜잭션이 읽은 로우를 다른 트랜잭션이 수정하는 것을 막아준다.  하지만 New 로우 추가는 제한 않는다. SELECT 후 타 트랜잭션에서 추가로 결과가 달라질수도 있다.
      > SERIALIZABLE  :  가장 강력한 격리수준이다.  순차적으로 진행해서 여러 트랜잭션이 동시에 같은 테이블 정보를 엑세스하지 못하게 한다.  (성능은 가장 낮다)

    • 트랜잭션의 속성 3(Rollback, Commit 관련)

      선언적 트랜잭션에서는 트랜잭션 경계지점(Method)을 기점으로 다시 제어가 돌아왔을때 (해당 Method가 끝날때), 아무 Exception 발생없이 끝나면 Commit, RuntimeException이 throw 되면 Rollback 한다.   일반 Exception(checked)의 경우 commit 이  DEFAULT 이다.  

      > rollback 조건-Exception 변경 (rollbackFor) : rollback이 이루어지는 Exception(throw된)을 변경 한다.
      > commit 조건-Exception 변경 (noRollbackFor) : 반대로 commit (noRollback) 이 이루어지는 Exception을 추가 해줄수도 있다.

    • 트랜잭션의 속성 4(읽기전용 – readOnly)

      트랜잭션을 읽기전용으로 설정한다.  성능최적화, 혹은 의도적으로 조회만을 위한 트랜잭션을 만들기위해 사용할 수 있다.  
       

  2. 선언적 트랜잭션 사용 (Annotation)

    선언적 트랜잭션이란 실사용 코드에 트랜잭션에 대한 구문개입 없이 설정파일 (XML, javaconfig)이나 어노테이션만으로 트랜잭션을 사용하는 것이다.   선언적 트랜잭션 경계설정이 사용 가능하게 된 것은 스프링 AOP 사용의 일예이며 TransactionProxy Bean 덕분이다.   선언적 트랜잭션 사용은 2가지 방법이 주로 사용되나, 여기서는 어노테이션을 이용하는 방법만을 다룬다.  어노테이션을 이용하는 방법은 일면 번거러운면이 분명있다.  그래도 설정파일(XML) 없이 환경구성이 가능한 SpringBoot에서  진행을 해보려 하니 딱히 XML을 따로 만들지 않으려 한다.  그러나 모든 메스드에 일괄적으로 적용하기 위해서는 Springboot에서도 설정파일(XML)에 TX네임스페이스를 이용할 수 밖에 없어보인다.

    • @Transactional
      @Transactional
      public void batchEvent(){
      	
      	try{		
      		apiCallAndAddEvents();
      		
      		apiCallAndAddSubjcode();
          	
      		commService.addBatchJobLog("OK");
          	
      	}catch(Exception e){
      		commService.addBatchJobLog("FAIL");
      		throw new RuntimeException(e);
      	}
      }	

      경계로 가져가고 싶은 클래스, 인터페이스, 메서드에 위와 같이 지정하여 사용할 수 있다.   또한 트랜잭션 에트리뷰트 설정에 따라 해당 위치가 트랜잭션의 경계로 적용되어진다고 볼 수 있다.  위의 경우에는 메스드에 사용하였다.   어노테이션이 중복으로 적용될 경우 메소드 < 클래스 < 인터페이스 순으로 우선순위가 정해진다.  
      트랜잭션은 또한 별도의 에트리뷰트(파라메터)를 갖을 수 있다.  @Transactional(propagation=Propagation.REQUIRES_NEW)  와 같은 형태로 작성되며 1장에서 이야기한 트랜잭션의 속성들을 지정하여 사용할 수 있다.  
      위의 코드를 예를 들자면 2작업 (apiCallAndAddEvents(), apiCallAndAddSubjcode()) 이 트랜잭션으로 묶여 처리되고 정상적으로 종료되면 정상 로그 (OK)를 적재하며, Exception이 발생할경우 오류 로그 (FAIL)을 남기는 코드의 예이다.  위의 코드에는 명시하지 않았지만, commService.addBatchJobLog() 메서드에는 트랜잭션 경계를 다시 부여하여 2작업 결과와 관계없이 로그가 적재되도록 구성되었다.  에러 로그를 적재한 후에는 메인 로직의 Rollback을 위해 다시 RuntimeException을 throw한다.
       

  3. 기타

    • Rollback대상 Exception 발생 후 Rollback 절차  (JPA : Transaction marked as rollbackOnly)

      위에서도 설명한 것처럼 선언적 트랜잭션이 설정된 하나의 그룹이 끝날때 Transactional 전파 옵션에 따라 Rollback or Commit 처리가 이루어진다.  JPA를 사용중일때 다만 주의 할 점은 Rollback대상으로 인지된 Exception (DEFAULT : RuntimeException을 상속한)이 발생하면, 해당 오류의 Try ~ Catch여부와 관계 없이 해당 트랜잭션은 rollbackOnly로 Marked 된다. 이런 경우 Try ~ Catch를 통해 해당 Exception을 Catch하고 다시 Rollback을 위한 Exception을 throw하지 않으면 에러가 발생한다.  (javax.persistence.RollbackException: Transaction marked as rollbackOnly)
      그러나 때로는 Exception Catch를 해당 메서드 밖으로 보내고 싶지 않을때도 분명있다.  이럴 경우, 아래와 같이 처리하여 Exception throw없이 rollback을 수행하게 할 수도 있다. 

      TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
       

    • SpringBoot 의 AOP Proxy 기본방식인 CGLIB 및 JDK Dynamic Proxy 방식 사용에 대하여

      구글링을 진행하며 몇 몇 SpringBoot의 AOP Proxy기본 방식이 CGLIB로 확정된 이유에 대해 궁금해 하는 포스트를 보았다.  나도 해당 내용이 의문이 들어 찾아본 결과 github의 SpringBoot Project에서 owner 및 SpringGroup 멤버로 부터 의문에 답을 찾을 수 있었다.  또한 SpringBoot에서 JDK Dynamic Proxy 방식으로 전환하는 것에 대해서도 약간의 문제가 있음을 알 수 있었다.  (spring.aop.proxy-target-class=false)

      > We’ve generally found cglib proxies less likely to cause unexpected cast exceptions. (LINK)

      >  Unfortunately, it’s not as simple as that. The problem is that we’re fighting against three different ways in the Framework that the proxy type can be configured:

      1. @EnableAspectJAutoProxy
      2. @EnableCaching
      3. @EnableTransactionManagement
        ....... 
        (LINK)

SpringBoot – Ehcache 사용

※ 사전지식

  • JSR : (Java Specification Requests) 사양 및 기술적 변경에 대한 정식 제안 문서.  개인 및 조직은 JCP (Java Community Process)의 회원이 될 수 있으며 JSR에 언급 된 스펙에 따라 코드를 개발할 수 있다.   개발 된 기술적 변화는 JCP 회원들의 검토를 거쳐 승인된다.
    .
  • JSR-107 : (JCACHE – Java Temporary Caching API) 객체 생성, 공유 액세스, 스풀링, 무효화 및 JVM 전반에 걸친 일관성을 포함하여 Java 객체의 메모리 캐싱에서 사용할 API 에 대한 기준으로 볼 수 있다.    해당 Spec 으로 구현된 cache로는 EhCache가 유명하며, Hazelcast, Infinispan, Couchbase, Redis, Caffeine 등도 해당 기준을 따르는 것으로 알려져 있다.

 

> Cache의 활용

지금까지 경험했던 Site (주로 중대형 금융사)에서는 Cache의 활용이 거의 없었다.   (계정계)라는 꽤나 변화하기 Cache도입이 쉽지 않은 (구)환경(Pro C, Mainframe (Cobol 등))의 원인도 있겠지만, Java Framework(Spring, Struts)을 이미 꽤 오래전에 도입한 중형 금융사들의 환경도 마찬가지 였다.   물론 Data변화가 잦은 Business 특성에는 맞지 않을 수 있지만, 그 가운데도 Cache를 도입할 요소는 분명  많이 있다고 생각된다.  (예 : 코드 및 기준정보와 같은 Access가 잦으나 자주 Update되지 않는)
특히 공통으로 사용하는 코드정보는, 그 규모가 대형이 아닌 중형 금융사임에도 불구하고 하루 동안 Database Access (Select) 횟수가 10만회를 넘어 섰다.  이러한 경우 Database의 Cache가 분명 활용되었겠으나 Database의 Cache 및 리소스 활용을 더 중요한일에 할 수 없었던 아쉬운 경우가 아닌가 생각된다.

 

> SpringBoot에서의  Cache

기본적으로 (JSR-107) 에 따르는 Cache들을 지원한다.   또한 Spring은 각 Cache API들의 기술 및 변화에 관계 없이 일관된 사용을 위해 추상화를 지원한다(CacheManager) .  Auto Detect 기능으로 인해 EhCache나 Redis등 별도 Cache library가 추가되면 해당 라이브러리를 자동적으로 이용하게 된다.

 

> Dependency 추가 (Maven)

SpringBoot Cache 및 사용할 Ehcache를 추가한다.

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-cache</artifactId>
 </dependency>
 
 <dependency>
   <groupId>net.sf.ehcache</groupId>
   <artifactId>ehcache</artifactId>
   <version>2.10.3</version>
 </dependency>

 

> Ehcache config (XML) 파일 생성 (ehcache.xml)

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="http://www.ehcache.org/ehcache.xsd"
         updateCheck="true"
         monitoring="autodetect"
         dynamicConfig="true">

    <cache name="사용할CacheName"
           maxElementsInMemory="1000"
           eternal="true"
           overflowToDisk="false"
           timeToLiveSeconds="300"
           timeToIdleSeconds="0"
           memoryStoreEvictionPolicy="LFU"
           transactionalMode="off">
    </cache>

</ehcache>

> name : 캐시 이름 지정
> maxEntriesLocalHeap: 메모리에 생성될 Entry Max값  (0=제한없음)
> maxEntriesLocalDisk: 디스크(DiskStore)에 저장될 Entry Max값 (0=제한없음)
> eternal: 영구 Cache 사용 여부 (true 인경우 timeToIdleSeconds, timeToLiveSeconds 설정은 무시된다.)
> timeToIdleSeconds: 해당 시간 동안 캐쉬가 사용되지 않으면 삭제. (0=삭제되지 않는다)
> timeToLiveSeconds: 해당 시간이 지나면 캐쉬는 삭제된다. (0=삭제되지 않는다)
> diskExpiryThreadIntervalSeconds: DiskStore 캐시 정리 작업 실행 간격 (Default=120초)
> diskSpoolBufferSizeMB: 스풀버퍼에 대한 DiskStore 크기 설정
> clearOnFlush: flush() 메서드 호출 시점에 메모리(MemoryStore) 삭제 여부. (Default=true)
> memoryStoreEvictionPolicy : maxEntriesLocalHeap 설정 값에 도달했을때 설정된 정책에 따라객체가 제거되고 새로 추가된다.
> logging: 로깅 사용 여부를 설정한다.
> maxEntriesInCache: Terracotta의 분산캐시에만 사용가능하며, 클러스터에 저장 할 수 있는 최대 엔트리 수를 설정한다. 0은 제한이 없다. 캐시가 작동하는 동안에 속성을 수정할 수 있다.
> overflowToOffHeap: 이 설정은 Ehcache 엔터프라이즈 버전에서 사용할 수 있다. true 로 설정하며 성능을 향상시킬 수 있는 Off-heap 메모리 스토리지를 활용하여 캐시를 사용할 수 있다. Off-heap 메모리 자바의 GC에 영향을 주지않는 다. (Default=false)
(참고사이트 : http://www.ehcache.org/ehcache.xml)

 

> SpringBoot Property 파일 수정 (application.properties)

...
spring.cache.ehcache.config=classpath:ehcache.xml
...

 

> Cache Enable (@EnableCaching 어노테이션 추가)

@EnableCaching
@SpringBootApplication
public class MyApplication {
	public static void main(String[] args)  {
		SpringApplication.run(MyApplication.class, args);
	}
}

※ 만약 추가된 별도 Cache (Ehcache)가 없을 경우 Cache Enable되면 ConcurrentMap 이용하는 방식으로 Cache가 Enable 된다.

 

> 어노테이션을 통한 Cache 사용

  1. @Cacheable
    @Cacheable(cacheNames = "useCacheNames", key="#pageNo")	
    public List<ObjectA> getEventsByPageNo(int pageNo) {
        .......
    }

    메서드를 기준으로 수행되며, 2가지의 캐쉬기능이 동작한다.  1. 해당 메서드의 Argument, 혹은 그 중 일부를 key로 취하여 캐쉬에 해당 Key가 존재할 경우 메서드는 수행되지 않고 캐쉬 값을 바로 return 값으로 반환 한다.   2. 또한 캐쉬에 해당 값이 존재 하지 않아 메서드가 수행될 경우 Key와 수행결과 (return 값)을 캐쉬에 저장한다.

    – 어노테이션 Parameter

    > cacheNames, value : 사용할 Cache 명을 지정한다.  cache설정 파일(ehcache.xml)에서 생성해준 Cache 명을 지정한다. 배열 형식({A, B, C}) 등으로 명시하여 여러개의 Cache에 적용토록 사용할 수도 있다.

    > key : cache key로 사용할 key를 명시적으로 지정한다.  해당 메서드의 Argument 중 일부를 지정할 수도 있고, keyGenerator를 사용하도록 지정할 수도 있다.  혹은 문자열의 조합을 사용하도록 지정도 가능하다.

    > condition : true나 false가 되는 SpEL 표현식을 받는 conditional 파라미터로 캐시수행 여부를 결정하는 조건을 지정할 수 있다.
    (예: condition=”#name.length < 32″)

    unless  : SpEL표현식, 값이 true이면 반환 값이 캐시에 남지 않게 된다.

    > 어노테이션의 Parameter명 지정 없이 @Cacheable(“cacheNM”) 와 같이 사용할 경우 cacheNames와 같다.

    > Key가 지정되지 않은 상태로 해당 어노테이션이 사용될 경우 메서드가 단일 Argument일 경우 자동으로 해당 값을 취하며, 여러개의 Argument가 존재할 경우 SimpleKey의 형태로 자동으로 복합키를 생성하여 취하도록 수행된다.

  2. @CacheEvict
    @CacheEvict(cacheNames = {"cacheNameA"}, allEntries = true)
        public void clearCache(){}

    저장된 캐쉬를 제거한다.  @Cacheable와는 달리 @CacheEvict 어노테이션은 캐시를 제거(eviction)하는 메서드를 구분하는데 즉, 캐시에서 데이터를 제거하는 트리거로 동작하는 메서드다. 다른 캐시 어노테이션과 마찬가지로 @CacheEvict는 동작할 때 영향을 끼치는 하나 이상의 캐시를 지정해야 한다.

    – 어노테이션 Parameter

    > cacheNames, value, key, condition : @Cacheable 과 동일

    > allEntries : @CacheEvict에서 키나 조건을 지정해야 할 수 있지만 딱 하나의 엔트리(키에 기반을 둔)가 아니라 제거를 할 캐시의 범위를 나타내는 allEntries 파라미터를 추가로 사용할 수 있다. 해당 값을 true로 할 경우 해당 CacheName에 해당하는 모든 캐쉬를 비운다.

    > beforeInvocation : 메서드 실행 이후(기본값)나 이전에 제거를 해야 하는 지를 지정할 수도 있다. false 일 경우 메서드가 실행되지 않거나(캐시 되어서) 예외가 던져지면 제거가 실행되지 않는다.   true로 지정할 경우 메서드 정상 수행 여부와 관계 없이 항상 캐쉬 제거가 수행된다.

  3. @CachePut
    @CachePut(cacheNames = "useCacheNames", key="#pageNo")	
    public List<ObjectA> getEventsByPageNo(int pageNo) {
        .......
    }

    메서드를 수행 후 Return 값을 캐쉬에 저장한다.  @Cacheable과의 차이는 캐싱값을 취하는 (메서드가 수행되지 않는) 방법이 아닌 메서드 수행 후 항상 캐쉬값을 저장 하도록 한다는 것이다.

    – 어노테이션 Parameter : @Cacheable 과 동일

  4. @Caching
    @Caching(evict = { @CacheEvict("primary"), @CacheEvict(value = "secondary", key = "#p0") })
    public Book importBooks(String deposit, Date date)

    @CacheEvict나 @CachePut처럼 같은 계열의 어노테이션을 여러 개 지정해야 하는 경우가 있는데 예를 들어 조건이나 키 표현식이 캐시에 따라 다른 경우이다. 안타깝게도 자바는 이러한 선언을 지원하지 않지만 감싸진(enclosing) 어노테이션을 사용해서(이 경우에는 @Caching) 우회할 수 있다. @Caching에서 중첩된 @Cacheable, @CachePut, @CacheEvict를 같은 메서드에 다수 사용할 수 있다.

 

> ehCacheManager를 통한 Cache 사용

import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.Cache;
.....
@Autowired
private EhCacheCacheManager ehCacheManager;	
.....
public void setCacheByEvents(String stDate) {    	
    	Cache ehCacheAll      = ehCacheManager.getCache("cacheName");
    	ehCacheAll.put("keyA", 56789945);
......
}

자동 Detecting 되어 생성된 Bean – EhCacheCacheManager (혹은 CacheManager)를 주입 받아 해당 Cache를 수동으로 생성 혹은 삭제할 수도 있다.  어노테이션에 의한 방법의 경우 캐쉬값의 생성에 있어서는 최초에 한번 put 해주는 작업이 필요하며 key가 low 레벨일 경우 번거로움을 수반한다.   (예 : Batch 혹은 Bean Construct 후 Cache 값 셋팅 등)

아래의 코드와 같이 현재 생성된 Cache값을 모두 Print 해볼수도 있다.

import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.Cache;
.....
@Autowired
private EhCacheCacheManager ehCacheManager;	
.....
    public void printAllCache() {
    	    		
    	String[] cacheNames = ehCacheManager.getCacheManager().getCacheNames();
    	
    	for(String cacheName : cacheNames){
	    	List strArr = ehCacheManager.getCacheManager().getCache(cacheName).getKeys();
	    	for (Object string : strArr) {
	    		System.out.println(cacheName + "-Member : "+string.toString());
	    	}
    	}
    }