티스토리 뷰

시작하기 앞서...

Application을 개발하다 보면 기능이 점점 복잡해지고 데이터가 쌓이면서, 처음과 다른 성능 저하가 발생하게 된다.

서버의 사양을 올리거나, 서버의 댓수를 추가하면 전체적인 성능은 올라가지만 그만큼의 비용이 발생하게 된다.

 

자본이 무한하다면 상관없지만, 물리적 증설은 최후의 보루로 남겨두고 Application 적인 측면에서 성능개선을 꾀할 수 있는 두 가지 방법을 소개하도록 하겠다.

 

Cache의 정의

Cache란? - feat Wikipedia

캐시(cache, 문화어: 캐쉬, 고속 완충기, 고속 완충 기억기)는컴퓨터 과학에서 데이터나 값을 미리 복사해 놓는 임시 장소를 가리킨다. 캐시는 캐시의 접근 시간에 비해 원래 데이터를 접근하는 시간이 오래 걸리는 경우나 값을 다시 계산하는 시간을 절약하고 싶은 경우에 사용한다. 캐시에 데이터를 미리 복사해 놓으면 계산이나 접근 시간 없이 더 빠른 속도로 데이터에 접근할 수 있다.

 

쉽게 말해 어디엔가 데이터를 복사해두고 빠르게 접근할 수 있는 저장소를 말한다.

 

Caching의 기법에는 Local Cache와 Cache Server를 이용한 Global Cache로 나뉘는데, 각각 아래의 특징을 가지고 있다.

 

Local Cache

장점

  • WAS의 인스턴스 메모리에 데이터를 저장하기 때문에 접근 속도가 매우 빠르다.
  • 별도의 Infrastructure를 필요로 하지 않는다.

단점

  • 캐싱되는 데이터가 커지면 WAS 인스턴스의 메모리 사용량도 증가한다.
  • 외부에서 참조하기가 힘들기 때문에 인스턴스가 여러 개인 경우 캐싱된 데이터의 원본 데이터가 바뀌면 정합성을 보장하지 못한다.

Cache Server (aka Global Cache)

장점

  • 여러 인스턴스에서 동일한 값을 바라본다. 그렇기 때문에 서버의 대수가 많아지면 많아질수록 Local Cache에 비해 유리하다.
  • Shading과 Replication으로 분산 저장이 가능하다.

단점

  • 별도의 Infrastructure가 필요하다.
  • 네트워크 비용이 발생하기 때문에 상대적으로 Local Cache보다는 느리다. 

요새는 대부분 AWS나 AZURE 같은 Cloud 기반으로 서비스를 많이 하다 보니, Cache Server의 구축이 매우 쉽고 노력 대비 얻을 수 있는 성능이 크기 때문에 Global Cache를 대부분 사용하고 있지만, 몇몇 상황에서는 Local Cache로도 대체가 가능하기 때문에 오늘 이 글에서는 Local Cache를 다뤄보도록 하겠다.

 

 

무조건 캐시 사용이 좋다고 라고 말할 수는 없다. 모든 상황에서 쓸 수 없을뿐더러, 캐시를 사용하기 위한 비용이 필요하기 때문이다. 

 

Cache Server의 경우 들어가는 비용이 상대적으로 높은 만큼 다양한 곳에 사용할 수 있겠지만, 우리가 사용할 Local Cache의 경우 아래와 같은 데이터에 사용하기 적절하다.

 

  • 변화의 주기가 매우 길거나, 한번 지정하면 변화가 없는 고정 값(ex. 회사의 휴일, 로그인한 유저의 그룹명 등)
  • 동일한 데이터를 매우 빈번하게 조회를 하는 경우 (ex. 할인 정책 조회, 결제수단 목록 조회 등)

 

 

Application에 적용해보자 With EhCache

 

과거 Spring Framework에서 많이 쓰이던 EhCache를 사용하여 Localcache를 적용해보도록 하겠다.

 

Dependency 추가

//Maven
<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-cache</artifactId>
 </dependency>
 
<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
</dependency>
<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.9.0</version>
</dependency>
//Gradle
dependencies {
      compile('org.springframework.boot:spring-boot-starter-cache')
    
      compile group: 'org.ehcache', name: 'ehcache', version: '3.9.0' 
      compile group: 'javax.cache', name: 'cache-api', version: '1.1.1'    

}

ehcache.xml 설정

ehcache.xml 은 캐시의 정의와 설정이라고 보면 된다.

cache attribute를 여러 개 정의할 수 있다.

(지원하는 attribute는 ehcache.org 를 참고하자)

<config xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns='http://www.ehcache.org/v3' xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core.xsd">
    <cache alias="cacheName">
	<!-- key와 value는 Class면 상관없다. java에서 제공하던, 사용자가 만들었던 무관-->
        <key-type>key의 자료형(ex java.lang.String)</key-type>
        <value-type>value의 자료형(ex com.test.a.b.Class)</value-type>
        <expiry>
	<!-- 캐시 만료 시간 unit 역시 바꿀 수 있다. -->
            <ttl unit="hours">24</ttl>
        </expiry>
        <resources>
	<!-- JVM의 heap 메모리에 몇개나 저장할 것인가 (LRU) -->
            <heap unit="entries">1024</heap>
        </resources>
    </cache>
</config>

@EnableCaching

Cache를 사용하고자 하는 Class에 EnableCaching을 선언해준다. 

(대체적으로 Main이나 Config Bean에 선언하면 해당 애플리케이션에서 모두 쓸 수 있어서 많이 쓰는 편) 

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

@Cacheable

caching 하고자 하는 Method에 선언해준다.

@Cacheable(value = "캐시명(ehcache.xml에 명시한 name)", key = "#캐시의 키값") 
public int method(key) { 
	return ....; 
}	

 

놀랍게도 끝이다.

(물론 캐싱된 데이터의 원본이 변경될 경우를 위한 Eviction도 추가해야 하지만, 데이터 변경이 즉시 적용되지 않아도 된다면, TTL 타임을 짧게 가져가도 성능 향상의 효과를 얻을 수 있다. 또한 데이터가 변경되는 Action이 같은 인스턴스가 아니면 Eviction을 할 수 없다.)

 

@Cacheable을 적용한 메서드는 아래와 같이 동작한다.

  1. 해당 메서드의 인자값, 혹은 그 중 일부를 key로 하여 캐시에 해당 key가 존재한다면 해당 key에 해당하는 value를 리턴한다.
  2. key가 존재하지 않는다면, 메소드 안의 내용을 수행하고, key와 그 return값을 value로 캐시에 저장한다.

적용 예시

예를 들어 아래와 같은 HoliDay 테이블이 있다고 가정하자. 이 테이블에는 한국의 공휴일뿐만 아니라, 회사의 휴일 또는 매년 바뀔 수 있는 임시공휴일 데이터가 들어있다.

holiday name
2021-01-01 신정
2021-02-11 설연휴
2021-02-12 설연휴
2021-02-13 설연휴
2021-03-01 삼일절

만약 특정일자를 입력값으로 주고, 이 날짜의 +1 영업일을 구해야 한다면, 재귀적으로 해당 테이블을 조회하여 휴일인지 아닌지 구할 수밖에 없다. 연휴가 길면 길수록, 입력값의 개수가 많으면 많을수록 테이블의 조회수는 압도적으로 증가한다.

그런데 만약 위와 같은 캐시를 적용한다면 캐시의 유효시간(TTL) 안에 각 일자별로 한 번씩만 조회하면 입력값이 얼마든지 늘어나도 db를 거치지 않기 때문에 빠른 응답속도를 가질 수 있게 된다.

 

사실 이러한 내용은 LocalCache에 국한되지 않고, 캐싱의 장점에 대해 서술한 것이다.

 

이러한 동일한 데이터의 반복 조회가 빈번하다면 캐시 적용을 고민해보도록 하자.

 

얼마나 빨라질까? - 실전

아래 코드는 입력받은 시간이 일요일인지 체크하는 메소드이다. Sleep을 걸었기때문에 결과는 1초가 걸릴 것이다.

@Service
public class CacheService {

    public boolean isHoliday(LocalDate localDate) {

        try {

            Thread.sleep(1000);

            return (localDate.getDayOfWeek() == DayOfWeek.SUNDAY);

        } catch (Exception e) {
            e.printStackTrace();
        }

        return false;

    }

}

아래 코드는 위 작성한 메소드를 실행하기 위한 코드이다. 10번 반복하여 휴일 Count의 합을 리턴한다.

@SpringBootTest
class EhcacheApplicationTests {

	@Autowired
	CacheService cacheService;

	@Test
	void testIsHoliday() {
		int holidaySum = 0;
		Instant startTime = Instant.now();
		System.out.println(String.format("시작시간 : %s",
				LocalDateTime.ofInstant(startTime,ZoneId.of("Asia/Seoul")).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))));


		for (int i = 0; i < 10; i++) {
			if (cacheService.isHoliday(LocalDate.ofInstant(startTime,ZoneId.of("Asia/Seoul")))) {
				holidaySum++;
			}
		}

		Instant endTime = Instant.now();
		System.out.println(String.format("종료시간 : %s",
				LocalDateTime.ofInstant(endTime,ZoneId.of("Asia/Seoul")).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))));

		Duration diff = Duration.between(startTime, endTime);

		System.out.println(String.format("소요시간 : %d초, Result : %d",diff.getSeconds(), holidaySum));
	}

}

기대값은 1초 * 10회 이므로 10초이다.

 

당연히 10초이다.

 

이제 캐시를 적용해보자.

 

아래와 같이 ehcache.xml 을 설정해줬다. premitive 타입은 클래스가 아니기때문에 저런형태로 써주면되고, 

LocalDate 와 같은 Class 는 Clase 풀 네임을 써주도록 하자.

<config xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xmlns='http://www.ehcache.org/v3' xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core.xsd">
    <cache alias="cacheHoliday">      
        <key-type>java.time.LocalDate</key-type>
        <value-type>boolean</value-type>
        <expiry>           
            <ttl unit="minute">1</ttl>
        </expiry>
        <resources>          
            <heap unit="entries">5</heap>
        </resources>
    </cache>
</config>

isHoliday 메소드에도 Cache를 사용할 수 있도록 Annotation을 추가했다. value값은 ehcache.xml에 기재한값과 동일해야 한다.

 @Cacheable(value = "cacheHoliday", key = "#localDate")
    public boolean isHoliday(LocalDate localDate) {
		....

    }

수정했으면 다시 실행해보자.

무려 1/10로 단축되었다.

캐시가 적용이 되어서 동일한 파라메터에 대해서는 메소드 내부로직을 타지 않게되어, Sleep을 타지 않아 매우 짧은 응답속도를 보일 수 있다. 루프 카운터를 바꿔도 결과는 동일하다.

 

1억번을 돌리던 1초가 나올것이다.

 

이렇게 동일한 파라메터를 반복적으로 호출 할 경우에는 Cache가 매우 유용함을 알 수 있게 되었다.

 

성능을 고민하고 있다면, 캐시 적용을 할 수 있는 부분이 있는지 한번 살펴보도록 하자. 놀라운 성능향상효과를 얻을 수 있을 것이다.

 

 

참고

ko.wikipedia.org/wiki/캐시

ehcache.org

댓글