새로운 내용을 공부할 때
새로운 내용의 공부를 시작할 때 용어의 정의를 이해하지 못하거나 정확하게 알지 못한다면 그 용어가 포함된 문장을 이해하지 못합니다.
작은 단어 하나가 내용을 이해하지 못하게 하기 때문에 용어를 정확하게 이해하는 것이 중요합니다.

8 분 소요

오늘 학습은 지연 평가와 Redis에 대해서 학습합니다.

학습 목표

  • 지연 평가
  • Redis

Lazy Evaluation(지연 평가)

Lazy Evaluation는 단순히 연산을 지연시키는 것 이상의 의미를 갖습니다.

주로 함수형 프로그래밍에서 사용되는 Lazy Evaluation는 자바 8의 함수형 프로그래밍이 접목되면서 기능을 손쉽게 사용할 수 있게 되었습니다.

키워드

  1. 지연 초기화 : 실제로 필요할 때까지 객체나 변수를 초기화 하지 않는 방법
  2. 단축 평가 : 논리 연산에서 불필요한 계산을 피하기 위해 표현식의 결과가 결정되는 즉시 나머지 계산을 생략하는 기법
  3. 캐싱 : 한 번 계산된 결과 값을 저장하여 동일한 계산을 피하는 방법
  4. 성능 최적화 : 지연 평가를 통해 불필요한 계산을 피하고 메모리 사용을 최소화하여 성능을 향상 시키는 방법

Lazy Evaluation 주요 특징

  1. 불필요한 계산 회피: 필요하지 않은 값이나 표현식(expression)은 평가되지 않는다.
  2. 무한 데이터 구조 지원: Lazy Evaluation을 사용하면 무한한 데이터 구조를 처리할 수 있습니다. 자바 8 무한 스트림을 다룰 때 유용합니다.
  3. 메모리 효율성: 값이 필요할 때마다 계산되기 때문에 메모리 사용량을 줄일 수 있습니다. 필요한 데이터만 메모리에 로드되므로 큰 데이터 셋을 다룰 때 효율적입니다.
  4. 단축 평가: 조건이 충족되면 나머지 계산을 건너뛸 수 있습니다. 예를 들면 논리 연산자 &&||는 왼쪽 표현식만으로 결과가 결정되면 오른쪽 표현식은 평가를 하지 않습니다.

Optional의 orElse 와 orElseGet

자바 8에 추가된 기능인 Lambda와 함수형 인터페이스를 통해 Lazy Evaluation을 제공합니다.

Optional과 Stream는 Lazy Evaluation을 활용하여 불필요한 평가나 실행을 하지 않아 메모리를 효율적으로 사용도록 도와줍니다.

Optional 클래스의 메서드 orElseorElseGet 는 데이터의 값이 없을 경우 인수를 Evaluation(평가) 하여 반환합니다.

똑같은 역할을 하는 두 메서드는 Lazy Evaluation 방식의 차이가 있습니다.

orElse는 Optional 객체의 값을 확인하기 전에 미리 평가를 합니다.

orElse 예제

public static void main(String[] args) {
        // Optional 값이 가지고 있을 때
        Optional<String> optionalString = Optional.of("Hello");

        // Optional 비어있을 때
        Optional<String> empty = Optional.empty();

        System.out.println("값이 있을 경우 연산이 되는가?");
        String result1 = optionalString.orElse(getDefault());
        System.out.println("result1 = " + result1);

        System.out.println("값이 없는 경우 연산이 되는가?");
        String result2 = empty.orElse(getDefault());
        System.out.println("result2 = " + result2);

    }

    public static String getDefault() {
        System.out.println("========= 평가 =========");
        return "Default";
    }
}

결과코드

값이 있을 경우 연산이 되는가?
========= 평가 =========
result1 = Hello

값이 없는 경우 연산이 되는가?
========= 평가 =========
result2 = Default

orElse가 null 인지 아닌지 확인하기 전에 인수를 평가하는 것을 확인할 수 있습니다.

orElseGet 예제

public class OrElseGetEvaluation {
    public static void main(String[] args) {
        // Optional 값이 가지고 있을 때
        Optional<String> optionalString = Optional.of("Hello");
        Optional<String> empty = Optional.empty();

        System.out.println("값이 있을 경우 연산이 되는가?");
        String result1 = optionalString.orElseGet(OrElseGetEvaluation::getDefault);
        System.out.println("result1 = " + result1);

        System.out.println("값이 없는 경우 연산이 되는가?");
        String result2 = empty.orElse(getDefault());
        System.out.println("result2 = " + result2);
    }

    public static String getDefault(){
        System.out.println("========= 평가 =========");
        return "Default";
    }
}

결과 코드

값이 있을 경우 연산이 되는가?
result1 = Hello

값이 없는 경우 연산이 되는가?
========= 평가 =========
result2 = Default

orElseGet은 결과를 확인한 뒤 평가를 확인하는 것을 볼 수 있습니다.

단축평가(short-circuit evaluation)

논리 표현식에서 일부 서브 표현식의 실행 또는 평가를 컴파일러가 건너뛰는 프로그래밍 개념입니다.

표현식의 값이 결정되는 즉시 컴파일러는 추가 서브 표현식의 평가를 중단합니다.

자바 처음 배울 때 &&|| 의 차이를 말합니다.

  • && 에서 false가 자주 발생하는 것을 앞에 두면 뒤에 연산은 할 필요가 없어지고
  • ||에서 true가 자주 발생하는 표현식을 앞에두면 뒤에 있는 연산은 할 필요가 없어집니다.

예를 들면 || 연산에서 a == b 일 경우 뒤에 있는 표현식은 평가하지 않습니다.

if (a == b || c == d || e == f) {
    // do_something
}

예상치 못한 동작을 회피

두 번째 인수로 인해 예상치 못한 동장을 예방 할 수 있습니다.

public class ShortCircuitAvoidUnexpectedBehavior {
    public static void main(String[] args) {
        String str = "Hello";
        String anotherStr = null;

        // 단축평가를 사용하여 str과 anotherStr을 비교
        if (str != null && executeLongTime(str, anotherStr)) {
            System.out.println("두 문자열은 같습니다.");
        } else {
            System.out.println("두 문자열은 다릅니다.");
        }

    }
    private static boolean executeLongTime(String str, String anotherStr) {
        return str.equals(anotherStr);
    }
}

여기서 str과 anotherStr의 값을 비교할 때 NPE가 발생하지 않도록 동작을 회피할 수 있습니다.

비싼 연산 회피

특정 조건에서만 실행되어야 하는 비싼 연산을 피하는 데 유용할 수 있습니다. 아래 코드를 통해 이를 설명할 수 있습니다.

public class ShortCircuitAvoidExpensiveComputation {
    public static void main(String[] args) {
        int a = 0;

        // veryVeryExpensive()는 호출되지 않음
        if (a != 0 && veryVeryExpensive()) {
            // do_something();
        }
    }

    public static boolean veryVeryExpensive() {
        // 비싼 연산
        System.out.println("비싼 연산 수행 중...");
        return true;
    }
}

위 예제 처럼 비싼 연산을 바로 실행하여 내부에서 취소되는 것이 아니라 특정 조건에서만 실행 될 수 있도록 하여 비싼 연산을 매번 피할 수 있습니다.

파일 준비 상태 확인

파일이 이미 준비된 상태라면 파일을 매번 준비하는 준비 과정을 피하는 방식에도 응용할 수 있습니다.

public class ShortCircuitFileHandling {
    public static void main(String[] args) {
        if (isFileReady() || getFileReady()) {
            System.out.println("파일이 준비되었습니다.");
        }
    }

    public static boolean isFileReady() {
        // 파일 준비 상태 확인
        System.out.println("파일이 이미 준비되었습니다.");
        return true;
    }

    public static boolean getFileReady() {
        // 파일 준비 작업 수행
        System.out.println("파일 준비 작업 수행 중...");
        return true;
    }
}

캐싱

캐싱은 지연 평가 방식과 독립적으로 사용할 수 있는 기술이지만, 지연 평가 방식은 실제로 값이 필요할 때 계산을 수행하는 방식입니다. 둘은 독립적이지만 함께 사용한다면 강력한 성능 최적화 기법으로도 사용이 가능합니다.

public class LazyEvaluationWithCachingExample {

    public static void main(String[] args) {
        Supplier<Integer> lazyCachedValue = createLazyCachedSupplier(() -> {
            System.out.println("Computing value...");
            return 42;
        });

        System.out.println(lazyCachedValue.get()); // 처음 호출 시 계산 수행
        System.out.println(lazyCachedValue.get()); // 두 번째 호출 시 캐시된 값 반환
    }

    public static <T> Supplier<T> createLazyCachedSupplier(Supplier<T> original) {
        return new Supplier<>() {
            private boolean isComputed = false;
            private T value;

            @Override
            public T get() {
                if (!isComputed) {
                    value = original.get(); // 처음 호출될 때 계산 수행
                    isComputed = true;
                }
                return value;
            }
        };
    }
}
// 지연 평가와 캐싱으로 필요할 때 연산
Computing value...
5252
// 캐싱된 결과    
5252

Lazy Evaluation 정리

지연 평가 한줄평

비싼 물건을 살 때 일주일은 고민하고 사라고 하셨다.

지연 평가는 정말 필요할 때 계산하는 방법입니다. 제한된 자원을 효율적으로 사용할 수 있습니다.

많은 데이터 셋 중에서 일부를 사용하거나 뒤늦게 상황에 따라 필요한 정보만 가져와야 한다면 지연 평가가 효율적으로 사용할 수 있는 환경입니다.

하지만 먼저 준비해야하는 리소스 ( 커넥션, 쓰레드풀 )과 같은 경우에는 지연 평가는 주의해서 사용해야합니다.

REDIS

레디스 오늘 학습할 내용

  • AOF vs RDB

Redis의 보험

레디스는 인메모리 데이터베이스로, 모든 데이터를 메모리에 저장합니다.

이러한 특성으로 시스템 오류, 재시작 또는 전원이 꺼지는 경우 모든 데이터가 사라질 수 있습니다.

이를 방지하고 데이터의 영속성을 보장하기 위해 레디스는 AOF(Apeend-Only File) 방식과 RDB(Redis DataBase Backup)을 제공합니다.

Append-Only 의 의미

Append-only는 새 데이터를 저장소에 추가할 수 있지만 기존 데이터는 변경할 수 없는 시스템 데이터 저장소의 속성입니다.

위키백과

데이터 저장 방식중에 하나며, 특히 로그 파일에서 많이 사용됩니다.

로그 파일에 최적화

로그 파일은 수정되지 않고 저장만 하는 방식입니다. 수정도 필요없고, 삭제도 필요없이 쓰기 작업에 최적화된 저장 방식을 사용해야합니다.

Append-only는 쓰기 작업에 최적화가 되어있습니다. 데이터가 파일의 끝에 추가되기만 하므로, 디스크 I/O 작업이 상대적으로 간단하고 빠릅니다.

RDB 방식

레디스에서 데이터를 백업하기 위한 가장 단순한 방법입니다.

원하는 시점에 데이터 자체를 스냅샷 찍듯 저장할 수 잇기 때문에 백업에 적합한 파일 형태라고 할 수 있습니다.

예를 들어 한시간에 한 번씩 RDB(스냅샷) 생성할 수 있으며, 장애 발생시 원하는 시점으로 복구가 가능합니다.

한계

장애가 발생할 경우 데이터 손실 가능성을 최소화해야하는 서비스에는 RDB 파일을 이용한 백업만 사용하는 것은 적절하지 않다.

이유는 사용자가 지정한 시간 단위로 파일이 저장되기 때문에 저장 시점부터 장애 시점까지의 데이터는 손실될 수 있다는 것입니다.

저장 방법

특정 조건에 자동으로 RDB 파일생성

레디스 설정 파일에서 save 옵션을 사용해 원하는 조건의 RDB 파일을 저장하도록 설정할 수 있습니다.

일정 기간(초)동안 변경된 키의 개수가 조건에 맞을 때 레디스는 자동으로 RDB 파일을 생성핣니다.

수동으로 RDB 파일 생성

  • SAVE: 동기 방식으로 파일을 저장한다. 대신 다른 클라이언트가 레디스를 사용할 수 없습니다.

  • BGSAVE: fork를 호출하여 자식 프로세스를 생성하며 자식 프로세스가 백그라운드에서 RDB파일을 생성 후 소멸됩니다.
    다른 클라이언트는 부모 프로세스를 사용하기 때문에 파일 저장에는 영향을 받지 않습니다.

RDB 파일 확인 방법

RDB 파일이 정상적으로 저장되었는지 LASTSAVE 커맨드로 마지막 RDB 파일이 저장된 시점을 확인할 수 있습니다.

AOF 방식

AOF는 레디스 인스턴스에서 수행된 모든 쓰기 작업의 로그를 차례로 기록합니다.

실수로 모든 데이터를 삭제했어도 로그 파일인 AOF 파일 내용중 모든 데이터를 삭제하는 커맨드만 지우고 재시작을 하면됩니다.

AOF 파일 재구성하는 방법

AOF 로그 저장방식은 데이터를 삭제하지 않고 누적하는 방식이기 때문에 주기적으로 압축시키는 재구성(rewrite) 작업이 필요합니다.

예를들어 이런 구조로 데이터가 AOF로 저장되었습니다.

SET key1 value1
SET key1 value2
SET key2 value3
DEL key1
SET key1 value4

주기적으로 압축을 위해 재구성이 되는 경우 아래과 같이 AOF파일이 변경됩니다.

SET key2 value3
SET key1 value4

예시로 확인할 수 있듯이, 중복되거나 불필요한 명령어가 제거되고, 최종 상태를 반영하는 명령어만 남게 됩니다.

자동 AOF 재구성

auto-aof-rewrite-perecntage는 AOF 파일을 다시 쓰기 위한 시점을 정하기 위한 옵션입니다.

마지막으로 재구성된 AOF 파일의 크기와 비교해, 현재 AOF 파일이 지정된 퍼센트 만큼 커질 경우 재구성을 시도하는 방식입니다.

> INFO Persistence
# Persistence
...
aof_current_size: 1866830
aof_base_size: 145802
...

마지막 재구성 AOF파일 크기는 145,802이고, 현재 AOF파일 크기는 186,830입니다.

auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

퍼센트가 100 이므로 aof_base_size의 100% 증가 값인 291,604가 넘어가게 되면 자동 재구성이 됩니다.

auto-aof-rewrite-min-size가 필요한 이유

만약 재구성 이후 AOF 파일 크기가 1KB가 된다면 데이터를 추가할 때마다 재구성이 되는 비효율적인 작업이 발생합니다.

최소한의 크기인 64MB보다 커질 경우에 동작하도록 하여 비효율적인 작업을 최소화 할 수 있습니다.

수동 AOF 재구성

BGREWRITEAOF 커맨드를 사용하면원하는 시점에 직접 AOF 파일을 재구성할 수 있습니다.

자동으로 재구성할 때와 동일하게 동작합니다

정리

기준 AOF (Append Only File) RDB (Redis Database Backup)
데이터 손실 허용 범위 거의 없음 (appendfsync 설정에 따라 다름) 약간의 데이터 손실 가능 (스냅샷 간의 시간 간격)
성능 요구 사항 쓰기 성능 저하 가능 (appendfsync 설정에 따라 다름) 일반적으로 성능에 미치는 영향 적음
데이터 복구 시간 복구 시간이 길 수 있음 (모든 명령어 재생) 빠른 복구 가능 (스냅샷 로드)
파일 크기 관리 시간이 지남에 따라 파일 크기 증가 파일 크기가 상대적으로 작음
디스크 공간 주기적인 AOF Rewrite로 관리 필요 일반적으로 더 적은 디스크 공간 사용
실시간성 실시간 백업 가능 실시간 백업 불가 (주기적인 스냅샷)
설정 및 관리 appendfsync 설정에 따라 다름 SAVE 또는 BGSAVE 명령어 사용
사용 용도 데이터 손실을 최소화하고자 할 때 빠른 복구와 높은 성능이 필요할 때
  • 데이터 손실 허용 범위
    • 데이터 손실을 거의 허용하지 않으려면 AOF 사용.
    • 약간의 데이터 손실을 허용할 수 있다면 RDB 사용.
  • 성능 요구 사항
    • 높은 쓰기 성능이 중요하면 RDB 사용.
      AOF는 쓰기 발생시 매번 로그 파일을 저장하기 때문입니다.
    • AOF는 appendfsync 설정을 조정하여 성능 최적화 가능.
  • 데이터 복구 시간
    • 빠른 복구가 필요하면 RDB 사용.
    • 정확한 데이터 복구가 필요하면 AOF 사용.
  • 파일 크기 관리
    • 디스크 공간이 제한적이면 RDB 사용.
    • 주기적인 관리가 가능하면 AOF와 AOF Rewrite 사용.
  • 혼합 사용
    • RDB와 AOF를 혼합하여 신뢰성과 성능을 균형 있게 유지.

댓글남기기