🧐 캐싱 방식 사용 시에 메모리 사용량을 최소화하는 방법은 무엇일까?
@Test
@DisplayName("메모리 사용량을 최소화하는 방법은 무엇일까?")
void 메모리_사용량을_최소화하는_방법은_무엇일까() {
// TODO: 메모리 사용량을 최소화하는 방법을 고민 후 개선해보세요.
record Position(int value) {
private static final Map<Integer, Position> CACHE = new ConcurrentHashMap<>();
public static Position startingPoint() {
return valueOf(0);
}
public static Position valueOf(final int value) {
return CACHE.computeIfAbsent(value, Position::new);
}
우테코의 좋은 코드, 좋은 예외 처리 수업에서 위와 같이 Map을 통해 데이터를 캐싱하는 상황에서 메모리의 사용량을 최소화하는 방법이 무엇일까에 대한 문제를 받게 되었다.
캐싱을 하게 된 배경
record Position(int value) {
Position() {
this(0);
}
public Position increase() {
return new Position(value + 1);
}
}
우선 캐싱을 하는 이유는 Position을 불변상태로 만들어서 예측하지 못한 값 변동을 바꾸기 위해 다음과 같은 코드를 차용했기 때문이다.
매번 Position이 증가될 때마다, 사이드 이펙트를 방지하기 위해 새로운 Position 객체를 만들어서 return하고 있는 형태이다.
물론 문제 해결에는 도움이 되겠지만 이렇게 관리하게 되면 계속해서 Position 객체를 생성하게 되기 때문에 이에 대한 해결책으로 캐싱이라는 방법을 도입하게 된 상황이다.
💡 LinkedHashMap의 removeEldestEntry
나는 이 문제에 대해 고민하는 과정에서 LinkedHashMap의 removeEldestEntry() 라는 메서드를 알게 되었다.
LinkedHashMap은 저장된 데이터의 순서를 기억하는 Map인데,
이러한 특징을 이용하여 Map에 설정한 최대 캐싱 수가 N개보다 많아졌을 오래된 데이터를 삭제하도록 만들 수 있는 메서드였다.
나는 메모리 사용량 최소화라는 주제에 적용해볼 수 있는 방식 중 하나가 바로 이 LinkedHashMap이라고 생각되어 바로 적용해보았다.
@Test
@DisplayName("메모리 사용량을 최소화하는 방법은 무엇일까?")
void 메모리_사용량을_최소화하는_방법은_무엇일까() {
// TODO: 메모리 사용량을 최소화하는 방법을 고민 후 개선해보세요.
record Position(int value) {
private static final Map<Integer, Position> CACHE = new LinkedHashMap<>() {
static final int CACHE_MAX = 5;
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Position> eldest) {
return CACHE.size() >= CACHE_MAX;
}
};
public static Position valueOf(final int value) {
return CACHE.computeIfAbsent(value, Position::new);
}
- removeEldestEntry를 Override 하여 CACHE_MAX개 이상의 데이터가 캐싱되면, 캐싱된지 오래된 순서대로 값을 삭제하도록 만들어서 CACHE_MAX 보다 많은 데이터가 캐싱되지 않도록 조절하였다.
이 방식을 활용하면 현재 가지고 있는 리소스에 따라 CACHE_MAX를 적절하게 조절하여 메모리 사용량을 최소화할 수 있도록 구현할 수 있을 것이다.
물론 단점으로는 자주 사용되는 것을 구분하는 방식이 아닌 오래된 순서대로 삭제한다는 점이 있다.
때문에 자주 사용되는 것을 구분해서 캐싱해야하는 상황이라면 이 방법을 선택하는 것이 그렇게 효율적이진 않을 수도 있다.
적용 결과
위 방식을 적용하고 나니, 위와 같이 캐싱 사이즈가 CACHE_MAX 를 넘어가면 오래된 데이터를 제거하도록 동작하는 것을 확인할 수 있었다.
문제점
캐시의 최대 한계치를 설정할 수 있다는 점은 알겠는데, 자주 사용되는 것을 구분하는 방식이 아니라 Insert된 지 오래된 순서대로 삭제한다는 큰 문제점은 어떻게 해결할 수 있을까?
이에 대한 해결책으로 LinkedHashMap이 제공하는 Ordering Mode 중 Access-Order 방식 을 활용해 볼 수 있겠다.
💡 LinkedHashMap의 Ordering Mode
LinkedHashMap은 Ordering Mode라는 것을 통해 오래된 데이터를 판단하는 기준을 2가지 기준으로 구분할 수 있다.
Insertion-Order (삽입 기준 정렬)
- Insertion-Order란 데이터가 Insert 된 순서를 기준으로 정렬한다는 것이다.
- LHM을 기본 생성자로 생성하게 되면 Ordering Mode 설정을 담당하는 accessOrder가 false로 설정되어 자동으로 Insertion-order 모드로 설정된다.
Access-Order (접근 기준 정렬)
- Access-Order 란 데이터가 최근 접근된 순서를 기준으로 정렬한다는 것이다.
- LHM이 가장 오랫동안 사용하지 않은 페이지를 교체하는 기법인 LRU(Least Recently Used) 알고리즘을 구현할 수 있도록 제공하는 기능이다.
- initialCapacity, loadFactor, accessOrder를 입력받는 생성자를 통해 사용 가능하다.
🔑 initialCapacity
- 초기 Map의 용량에 대한 값이다.
- 기본 생성자의 Default 값은 16이다.
🔑 loadFactor
- Map의 용량이 어느정도 찼을 때 capacity를 확장할 지에 대한 값이다.
- 기본 생성자의 Default 값은 0.75f이다.
🔑 accessOrder
- Ordering Mode 설정을 담당하는 값으로 true로 선택 시, Access-Order 모드를 사용할 수 있다.
💡 Access-Order 모드 적용
record Position(int value) {
private static final Map<Integer, Position> CACHE = new LinkedHashMap<>(16, 0.75f, true) {
static final int CACHE_MAX = 5;
@Override
protected boolean removeEldestEntry(Map.Entry<Integer, Position> eldest) {
System.out.println(CACHE);
return CACHE.size() >= CACHE_MAX;
}
};
- LHM을 Access-Order 모드 적용을 통해 최근에 접근한 순서대로 Map의 요소들을 정렬하여,
가장 오랫동안 조회되지 않은 값을 오래되었다고 판단하도록 설정 - 이를 통해 사용되지 않는 값들은 자연스레 캐시 메모리에서 제거하고 자주 사용되는 값들은 오랫동안 유지
적용 결과
위 코드 적용을 통해 사진 마지막 줄에서 볼 수 있듯이 1,2,3,4,5를 순차적으로 생성한 후, 1번에 접근 하였을 때
맨 뒤에 있던 1번이 가장 맨 앞으로 오는 것을 확인할 수 있었다.
마무리
2주간 우테코에서 학습하며 느낀 우테코의 장점은
좋은 코드를 만들기 위해 더 깊이 있게 학습하고, 더 깊이있게 고민할 수 있는 환경을 조성해준다는 점인 것 같다.
우테코에서 체계적인 미션과 수업 그리고 수준 높은 리뷰를 받아보며 내가 부족한 것이 무엇인지에 대해 직관적으로 파악할 수 있게 되었고,
페어 프로그래밍을 통해 페어와 코드에 대한 고민을 생각이 아닌 대화로 주고 받으면서 머릿속에 어질러져있던 생각과 개념들이 가지런히 정리되어 간다는 것을 느꼈다.
또, 페어 뿐만 아니라 크루원들과 대화하는 과정에서도 많은 인사이트와 동기부여를 얻고 있는 요즘이다.
포스팅의 내용 또한 크루원들과 대화 중 WeakHashMap 이라는 것을 알게 되어, 이에 대해 학습하는 과정에서 새롭게 배운 내용이다.
역시 개발은 혼자가 아닌 함께할 때 가장 빠르게 성장해나가는 것 같다🔥
Reference
'부트캠프 > 우아한테크코스 6기' 카테고리의 다른 글
테스트를 위한 객체, 테스트 더블(Test Double) (0) | 2024.04.04 |
---|---|
[Java] Fluent API란? (feat. JDBC에 적용해보기) (0) | 2024.04.04 |
[Java] 생성자 체이닝(Constructor Chaining) 기법 (0) | 2024.04.02 |
[Java] 자바에서 라인을 ‘잘’ 개행하는 방법 (2) | 2024.03.01 |
[Java] 클래스 멤버는 각자의 위치가 존재한다 (0) | 2024.02.28 |