일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- spring webflux
- design pattern
- JPA
- 컴퓨터구조
- IT
- JavaScript
- Algorithm
- 파이썬
- MSA
- react
- OS
- Heap
- Proxy
- 운영체제
- 자료구조
- MySQL
- 알고리즘
- Spring
- C
- mongoDB
- 디자인 패턴
- 백준
- 네트워크
- Data Structure
- Java
- Galera Cluster
- 자바
- c언어
- redis
- Kafka
- Today
- Total
시냅스
Java 로 구현하는 In-Memory Cache 본문
Java 로 구현하는 In-Memory Cache
캐시는 성능을 위해 매우 중요한 장치입니다.
디스크보다 빠른 접근시간을 가지며, 일시적으로 어떤 값을 저장/접근하기에 용이합니다.
자바에서 연산을 하는 것 또한 메모리 위에서 이뤄지는 것이므로,
애플리케이션에 사용할 캐시를 직접 만들어 볼 수 있겠습니다.
(이를테면 Caffeine 같은 라이브러리를 저희가 만드는 것입니다!)
이를 위해 자바의 참조 유형과 캐시에 대해서 알고 계시면 진행이 수월할 것입니다.
https://liltdevs.tistory.com/182
https://liltdevs.tistory.com/126
구현 조건
저희가 만들 자료구조는 캐싱을 위해 사용하므로,
각 요소가 Out Of Memory Error 가 발생하기 이전까지만 유지하고
메모리가 부족하다면 gc로 비워지게 하고 싶습니다.
또한 참조되고 있는 객체는 gc 이후에도 사용하고,
비교적 참조가 적은 객체들은 메모리가 해제되게 하고 싶습니다.
(참조 지역성의 원리를 이용하고 싶습니다!)
이때 저희는 자바의 참조 유형인 Soft Reference 를 사용할 수 있을 것입니다.
구현
구현의 편의를 위해 vm option 으로 힙사이즈를 꽤 작게 설정하였습니다.
우선 차이점을 보기 위해서 강참조를 통한 OOME 를 유도해보겠습니다.
public static void main(String[] args) {
List<Object> cache = new ArrayList<>();
Object o = new Object();
while (true) {
cache.add(o);
}
}
위의 코드는 저희가 잘 알고 있는 list 에 그저 Object 를 넣는 코드입니다.
Object 를 Strong Reference 하고 있습니다.
실제로 실행해본다면 꽤 짧은 시간 안에 OOME 가 발생하는 것을 확인하실 수 있을 것입니다.
다시 돌아와,
저희의 목적에 맞는 Cache 를 구현해보겠습니다.
public class Cache<K, V> {
private final HashMap<K, CacheSoftReference<K, V>> map = new HashMap<>();
// 참조 대상 객체들의 생명 주기를 추척하고 gc 에 의해 수집되는 객체들을 모으는 데 사용
// 참조가 약한 객체들은 여기에 담긴다.
private final ReferenceQueue<V> queue = new ReferenceQueue<>();
public void put(K key, V value) {
cleanUp(); // 참조가 약한 객체들을 먼저 제거하고 넣는다.
CacheSoftReference<K, V> ref = new CacheSoftReference<>(key, value, queue);
map.put(key, ref);
}
public V get(K key) {
CacheSoftReference<K, V> ref = map.get(key);
if (ref == null) { // cache miss
return null;
}
return ref.get(); // cache hit
}
public V remove(K key) {
cleanUp();
CacheSoftReference<K, V> ref = map.remove(key);
if (ref == null) {
return null;
}
return ref.get();
}
private void cleanUp() {
CacheSoftReference<K, V> ref;
// queue 에는 참조가 약한 객체들이 담겨있고 OOM 을 방지하기 위해 제거한다.
// 참조 카운트가 0이 된 객체들을 jvm 이 queue 에 담는다
while ((ref = (CacheSoftReference<K, V>) queue.poll()) != null) {
map.remove(ref.key);
}
}
private static class CacheSoftReference<K, V> extends SoftReference<V> {
private final K key;
public CacheSoftReference(K key, V referent, ReferenceQueue<V> queue) {
super(referent, queue);
this.key = key;
}
@Override
public boolean equals(Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof CacheSoftReference)) {
return false;
}
CacheSoftReference<?, ?> ref = (CacheSoftReference<?, ?>) obj;
return Objects.equals(this.key, ref.key);
}
@Override
public int hashCode() {
return Objects.hashCode(key);
}
}
}
내부적으로는 map 을 사용하여 key 에 대한 접근을 빠르게 하게 되고
Reference queue 는 참조가 약한 객체를 담아두어 필요에 의해
참조가 약한 객체들을 삭제함으로써 캐시의 조건에 부합하게 됩니다.
객체를 넣거나 삭제할 때에는 cleanUp 을 통해 우선 참조가 약한 객체들을 해제합니다.
이는 위에서 설명했듯 참조지역성의 원리나, OOM을 방지하기 위함입니다.
캐시를 구현하였으니, 실제로 실행해보겠습니다.
먼저 일반적인 HashMap 에서 OOM 이 일어날 수 있는 코드를 살펴보겠습니다.
HashMap<Integer, TestClass> cache = new HashMap<>();
int i = 0;
while (true) {
cache.put(i++, new TestClass(1));
}
그저 객체를 단순히 집어넣기만 하고 있으니 당연히 OOM 이 발생할 것으로 예상할 수 있습니다.
Cache<Integer, TestClass> cache = new Cache<>();
int i = 0;
while (true) {
cache.put(i++, new TestClass(1));
}
저희가 생성한 Cache 는 위에서 봤듯, 약한 참조에 대해서는 객체의 참조를 해제하게 됩니다.
그렇게 가용한 공간을 만든 뒤 객체를 새로 생성하여 넣음으로써 OOM 을 방지할 수 있게 됩니다.
실행하며 애플리케이션의 상태를 VisualVM 으로 확인해보겠습니다.
힙이 일정 수준 이상 올라가지 않는 것으로 미루어 보아,
gc 가 적절히 잘 되고 있다는 것으로 생각해볼 수 있겠습니다.
또한 gc 의 현황을 보게되면, Eden Space 에서 대부분 생성되었다가 사라지는 것을 볼 수 있습니다.
Old 영역으로 이동되는 객체 또한 거의 없는 것을 확인할 수 있습니다.
저희의 구현 조건을 만족하는 것을 확인하였습니다.
코드 상으로 구현이 단순하지만, 유용한 자바로 구현하는 Cache 에 대해서 알아보았습니다.
캐시를 구현함으로써 gc 의 작동 방식과 java 의 참조 유형에 대해서 알 수 있게 됐고,
실제로 사용하며 성능상의 이점을 노려볼 수도 있겠습니다.
완성도 있는 제품을 사용하려면 카페인, 구아바, 레디스 등을 고려해볼 수도 있겠습니다.
끝!
'Java, Spring' 카테고리의 다른 글
코드로 살펴보는 Spring Thread Model 과 blocking/non-blocking I/O (0) | 2023.04.02 |
---|---|
HikariCP 101 : 코드로 알아보는 HikariCP (0) | 2023.04.01 |
Java 참조 유형 과 GC (strong, soft, weak, phantom reference) (0) | 2023.03.26 |
Java8 Parallel Stream 과 성능, 동시성 문제에 대해 (0) | 2023.03.25 |
코드로 뜯어보는 System.out.println 대신 logger 를 사용해야 하는 이유 (0) | 2023.03.19 |