이 글은 책을 읽고 공부하고 기억하기 위한 기록용으로 문제가 생기면 삭제하겠습니다.
단순히 다시 시도하는 것이다. 작업을 다시 시도한다는 것은 쉬운 일처럼 보일 수 있다. 하지만 현실에서 재시도의 시기와 빈도를 판단하려면 약간의 노하우가 필요하다.
백오프란 비선형으로 대기 시간을 늘리는 방법으로 이 방법을 사용할 때 백오프 시간의 상한선을 정해서 대기 시간이 너무 길어지지 않게 하자.
백오프 전략(Backoff Strategy)은 주로 컴퓨터 네트워크에서 사용되는 전략 중 하나로, 일시적인 문제 또는 혼잡 상황이 발생했을 때 어떻게 대응할지 결정하는 방법론으로 이 전략은 주로 재시도(retry) 메커니즘과 관련이 있다.
일시적 문제 발생: 데이터 패킷 전송이 실패하거나 응답이 없는 등의 일시적인 문제가 발생합니다.
재시도 결정: 문제가 발생했을 때 바로 다음 시도를 하지 않고, 잠시 기다린 후에 다시 시도할지를 결정합니다.
기다림(백오프)
백오프 전략은 주로 네트워크 통신에서 사용되는 개념이며, 코드로 구현될 때는 다양한 상황에 맞게 적용될 수 있습니다. 아래는 간단한 예시 코드로, 일정 시간 동안 기다린 후에 재시도하는 간단한 백오프 전략을 보여줍니다.
이 예시 코드에서는 sendDataToServer() 함수가 데이터 전송을 시뮬레이션하며, 랜덤한 성공률로 데이터 전송의 성공 여부를 반환합니다. 데이터 전송이 실패한 경우 백오프 전략을 적용하여 재시도하고, 일정 시간 동안 기다린 후에 다시 시도합니다. 이렇게 재시도와 기다림을 반복하며 데이터 전송을 시도하게 됩니다.
import java.util.Random;
public class BackoffExample {
public static void main(String[] args) {
int maxRetries = 3;
int retryDelay = 1000; // 1000 milliseconds = 1 second
int currentRetry = 0;
while (currentRetry < maxRetries) {
boolean success = sendDataToServer();
if (success) {
System.out.println("Data sent successfully!");
break;
} else {
System.out.println("Data sending failed. Retrying...");
currentRetry++;
if (currentRetry < maxRetries) {
System.out.println("Waiting before retrying...");
try {
Thread.sleep(retryDelay); // Wait before retrying
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("Max retries reached. Giving up.");
}
}
}
}
public static boolean sendDataToServer() {
// Simulate data sending with a random success rate
Random random = new Random();
double successRate = 0.7; // Simulate 70% success rate
return random.nextDouble() < successRate;
}
}
임의의 지연시간, 시간의 불규칙한 변동이나 불규칙한 간격을 나타내는 용어로 주로 네트워크나 타이밍과 관련된 상황에서 사용된다.
천둥떼 현상으로 복구 중이던 서비스 다운을 막기 위해 지터를 추가하면 클라이언트들은 특정 범위에서 임의의 값을 백오프 시간에 더한다.
지터는 예상된 시간 간격과 실제 발생한 시간 간격 사이의 차이를 나타내며, 주로 네트워크 패킷의 전송 간격이나 디지털 신호의 타이밍 등에서 발생한다.
예를 들어, 음성 또는 영상 데이터를 실시간으로 전송하는 경우, 일정한 간격으로 데이터를 전송해야 한다. 그러나 네트워크 지연이나 데이터 처리 속도 등으로 인해 실제 전송 간격이 예상과 다를 수 있습니다. 이러한 시간적인 불규칙성을 지터라고 한다.
지터는 통신 시스템에서 중요한 개념으로, 지터가 크면 신호의 도착 시간이 불안정하게 되어 음성이나 영상 데이터에서 소리나 영상이 끊기거나 불안정해질 수 있다.
따라서 통신 시스템 설계나 관리에서 지터를 최소화하여 신호의 안정성과 품질을 유지하는 것이 중요
네트워크 서버에 일시적인 문제가 생겨 모든 클라이언트가 동시적으로 장애를 겪는 상황에서 모든 클라이언트가 동일한 백오프 알고리즘을 사용한다면 모두가 동시에 요청을 다시 보내는 현상
아래는 백오프 전략과 지터를 함께 적용한 예시 코드. 코드는 단순한 예시이므로 실제 상황에 맞게 조정이 필요할 수 있다.
import java.util.Random;
public class BackoffWithJitterExample {
public static void main(String[] args) {
int backoffTime = 1000; // 초기 백오프 타임 (1초)
Random random = new Random();
for (int attempt = 1; attempt <= 5; attempt++) {
System.out.println("Attempt #" + attempt);
// Simulate network collision
if (random.nextInt(100) < 50) {
System.out.println("Collision occurred");
// Apply backoff with jitter
int jitter = random.nextInt(backoffTime);
int waitTime = backoffTime + jitter;
System.out.println("Applying backoff with jitter: " + waitTime + " ms");
try {
Thread.sleep(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
System.out.println("Data sent successfully");
break;
}
// Exponential backoff: double the backoff time for next attempt
backoffTime *= 2;
}
}
}
설계 시점에서 처리를 염두에 두지 않은 에러가 발생하면 차라리 애플리케이션에 크래시하도록 놔두는 편이 낫다. 이런 방법을 빨리 실패하기라고 한다.
애플리케이션이 "크래시"한다는 것은 해당 애플리케이션이 예상치 못한 오류 또는 예외 상황으로 인해 정상적으로 작동하지 않고 비정상적으로 종료되는 상황
빨리 실패하면 사람이 올바르게 대처할 방법을 찾을 수 있다.
실패 상황을 전파하는 방안을 생각해야 하며, 쉽게 디버깅할 수 있도록 에러 관련 정보는 반드시 확인이 가능해야 한다.
예를 들어, 어떤 웹 서비스의 주문 생성 기능을 가정해봅시다. 이 기능은 주문을 생성하고 데이터베이스에 저장하는 작업을 수행 이 때 멱등성의 원칙을 지키면 아래와 같은 상황에서도 동일한 결과를 보장해야 한다
주문 생성 시도 1회: 사용자 A가 상품 X를 주문하려고 함.
주문이 생성되어 데이터베이스에 저장됨.
주문 생성 시도 2회: 사용자 A가 동일한 상품 X를 다시 주문하려고 함.
멱등성을 지킨다면 이 작업은 이전에 주문이 이미 생성되었음을 감지하고 동일한 주문을 중복 생성하지 않아야 함.
주문은 이미 존재하므로 새로운 주문이 생성되지 않아야 함.
이렇게 멱등성을 지키는 것은 데이터의 일관성과 중복 생성을 방지하는 데 도움이 된다. 이와 유사하게 다양한 작업이나 API 호출에서 멱등성을 고려하여 중복 작업이나 데이터 중복을 방지할 수 있다.
동일한 작업을 여러 번 실행해도 항상 같은 결과가 출력됨을 말한다. 모든 작업을 멱등 작업으로 구현하면 시스템 상호작용이 훨씬 편해지며 에러도 현저히 줄어든다,
더 이상 필요로 하지 않는 메모리, 데이터 구조, 네트워크 소켓, 파일 핸들 모두 해제하자.
운영체제는 파일 핸들과 네트워크 소켓을 위한 공간이 정해졌는데 가득 차면, 새로 핸들, 소켓 모든 작업이 실패한다.
네트워크 소켓이 누수되면 불필요한 연결에 계속 남아있어 연결 풀이 가득 차게 된다.
f.close를 실행하기 전 코스 실행 실패로 파일 포인터를 닫지 못하기 떄문에 개발 언어가 자동해제를 지원하지 않는다면 try/finally
로 파일 핸들 안전하게 닫게 해줘야 함
f = open(`foo.txt`, 'w')
#...
f.close()
소멸자 메소드
로, 파이썬은 with 구문
with open('foo.txt') as f:
#...
코드를 쉽게 운영하고 디버그할 수 있도록 로깅 프레임워크를 활용하자. 로그 레벨을 설정해서 운영자가 애플리케이션의 로그 양 조정할 수 있게 하자. 로그는 원자적이고 빠르며 안전하게 다뤄야 한다.
로깅 라이브러리
를 갖추고 있다.이 외에도 다양한 로깅 라이브러리가 있으며, 선택할 때 프로젝트의 요구 사항과 개발 환경을 고려하여 적절한 로깅 라이브러리를 선택하는 것이 중요
1. Log4j 2
2. SLF4J (Simple Logging Facade for Java)
3. Logback
4. java.util.logging (JUL)
5. Log4j 1
로깅 프레임워크는 운영자가 중요도에 따라 메세지를 필터링할 수 있도록 로그 레벨을 지원한다.
자바의 log4j.properties 파일의 일부로서, 루트에는 ERROR 레벨의 상세한 로그 지정하고 com.foo.bar 패키지 내의 코드에서는 INFO 레벨의 로그 지정
# 루트 로거에는 ERROR **텍스트**레벨을 지정하고 fout라는 이름의 FileAppender를 사용한다,
log4j.rootLogger = Error, fout
# com.foo.bar 패키지는 INFO 레벨 지정
log4j.logger.com.foo.bar = INFO
정리하자면,
- 예시: 특정 메소드나 함수의 호출과 반환 값을 로깅하여 디버깅 시 호출 흐름을 확인할 때 사용.
- 적용 방법: 로그 라이브러리에서 제공하는 TRACE 레벨 메소드를 사용하여 특정 작업이나 메소드의 세부 정보를 로깅
디버거
를 이용해 코드의 실행 과정을 확인하는 것이 좋다정리하자면,
- 예시: 사용자의 입력 데이터를 처리하는 중간 과정을 로깅하여 디버깅 시 데이터 처리 과정을 확인할 때 사용.
- 적용 방법: 로그 라이브러리에서 제공하는 DEBUG 레벨 메소드를 사용하여 중요한 상태 변경 또는 중간 과정을 로깅
정리하자면,
- 예시: 애플리케이션의 시작과 종료 시점, 서비스 포트 설정 등 애플리케이션의 기본 정보를 로깅할 때 사용.
- 적용 방법: 로그 라이브러리에서 제공하는 INFO 레벨 메소드를 사용하여 애플리케이션의 상태 정보를 로깅
서비스 시작
이나 5050번 포트 사용
과 같은 애플리케이션 상태 메세지를 이 INFO 레벨로 출력한다.만약을 위한
로그는 TRACE나 DEBUG레벨로 출력하자요청이 실패한 원인을 유발한 에러도 포함되어 있는데 그런데도 info 레벨을 쓴 이유는 애플리케이션이 자동으로 재시도 하므로 추가 대응할 필요가 없기 때문이다.
info!("Failed request : {}, retrying", e);
정리하자면,
- 예시: 서비스 리소스가 한계치에 다다른 상황이나 예상치 못한 동작을 로깅하여 경고할 때 사용.
- 적용 방법: 로그 라이브러리에서 제공하는 WARN 레벨 메소드를 사용하여 경고할 만한 상황을 로깅
리소스가 한계치에 다다르고 있다면
경고 메세지 출력하기 적합정리하자면,
- 예시: 예외 발생, 데이터베이스 작업 실패 등 오류 상황에 대한 정보를 로깅할 때 사용.
- 적용 방법: 로그 라이브러리에서 제공하는 ERROR 레벨 메소드를 사용하여 오류 상황에 대한 정보 및 스택 트레이스 등을 로깅합니다.
SLF4J와 Logback을 사용할 때는 로그 레벨을 지정하는 메소드를 사용한다. SLF4J의 로거(Logger) 인스턴스를 가져온 후 해당 인스턴스의 메소드를 호출하여 로그를 남길 수 있다. 메소드에는 로그 레벨을 지정하는 파라미터가 있다.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class MyClass {
private static final Logger logger = LoggerFactory.getLogger(MyClass.class);
public void myMethod() {
logger.trace("This is a trace level log.");
logger.debug("This is a debug level log.");
logger.info("This is an info level log.");
logger.warn("This is a warn level log.");
logger.error("This is an error level log.");
}
}
Logback을 사용할 경우에는 logback.xml 또는 logback.groovy 설정 파일을 통해 로그 레벨을 지정할 수 있다. 설정 파일에서는 다양한 로그 레벨을 각각의 로그 출력 대상에 지정할 수 있다.
<!-- logback.xml -->
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE" />
</root>
</configuration>
원자적으로 작성하자
는 프로그래밍 및 데이터 관리에서 매우 중요한 원칙 중 하나를 나타냅니다. 이 원칙은 작업이 더 작은 조각으로 분해되거나 중간에 중단되지 않고 완전히 실행되도록 보장하는 것을 의미한다.
원자적
이란 작업이 더 이상 나눌 수 없는 최소 단위분해 불가능(Indivisible)
: 원자적 작업은 더 작은 단위로 나눌 수 없어야 한다. 작업을 구성하는 각 단계는 분해되거나 재정의되지 않고 실행되어야 한다.중단 불가능(Undivisible)
: 원자적 작업은 중간에 중단되지 않고 완전히 실행되어야 한다. 만약 작업의 일부만 실행되고 중단되면 일관성과 정합성 문제가 발생할 수 있다.로그를 원자적으로 작성한다는 것은 로그 메시지가 한 번 작성되면 분해되거나 중단되지 않고 완전히 기록되어야 한다는 의미
한 메세지에 모든 정보를 원자적으로 저장하자.
로그 수집기는 관련 정보를 한 줄에 표현하는 로그를 더 잘 처리
but, 특정 순서대로 보이지 않을 수 있으며, 로그 정렬 시 시스템 시간에 의존하지말자. 시스템 시간은 리셋되거나 호스트 마다 조금씩 다를 수 있음
로그 메세지에 줄바꿈 문자도 피하자 => 특히 WARNING 로그에 경우, 다른 메세지와 혼합이 되기 때문에 한 줄로 출력 불가하다면 고유한 ID를 포함시켜 나중에 연결할 수 있게 하자.
로그를 너무 기록하면 성능에 영향을 미친다. 로그는 디스크나 콘솔, 원격 시스템 등 어딘가에 반드시 기록되어야 하며, 기록되기 전 한 문자열로 결합해야 한다.
매우 느리게 진행되며, 성능이 중요한 루프에 악영향을 미친다,
결합을 시도하는 문자열이 로그 메소드에 전달되면 로그 레벨과 상관 없이 결합 실행
이유 : 소드의 인수는 메소드에 전달기 앞서 평가가 이뤄지기 때문
프레임워크가 지원한다면, 파라미터화 로깅을 사용하자
JAVA의 로그 호출 시 문자열 결합 방법 3가지
while(message.size() > 0){
Message m = message.poll();
// 이 문자열은 trace 레벨이 비활성화돼 있어도 결합이 실행
log.trace("got message: "+m);
// 이 문자열 역시 trace 레벨이 비활성화돼 있어도 결합이 실행
log.trace("got message : {}".format(m));
// 이 문자열은 trace 레벨이 활성화된 경우에만 결합을 실행하므로 더 빠르다. => 파라미터화 // 로깅
log.trace("got message : {}", m);
}
로그 메시지의 내용을 동적으로 생성하고 로깅하는 기법, 이를 통해 로그 메시지에 변수나 데이터 값을 쉽게 포함시킬 수 있다. 파라미터화 로깅은 로그 메시지에 변수를 직접 결합하는 대신, 변수 값을 포함할 위치를 지정하고 실제 값은 로깅 함수의 파라미터로 전달하여 로그 메시지를 생성한다.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class LoggingExample {
private static final Logger logger = LoggerFactory.getLogger(LoggingExample.class);
public static void main(String[] args) {
String username = "john_doe";
int userId = 123;
// 파라미터화 로깅
logger.info("User {} with ID {} logged in", username, userId);
}
}
어펜더는 로깅 시스템에서 로그 메시지를 어디에 기록할지를 결정하는 역할을 하는 요소로 로그 메시지는 어펜더를 통해 특정한 출력 대상에 기록되게 된다. 보통 어펜더는 로그를 콘솔에 출력하거나 파일에 기록하는 등의 역할을 수행한다.
로깅 시스템에서 어펜더는 로그 출력의 대상과 형식을 지정하는 역할을 하며, 기본적으로 탑재된 로그 어펜더는 print 함수와 마찬가지로 호출자의 스레드에서 실행
현재 실행 중인 어펜더는 현재 실행 중인 스레드를 블록하지 않고 메세지를 기록한다.
비동기 어펜더
는 로그 메시지를 로그 큐(또는 버퍼)에 저장한다. 이때 큐는 별도의 백그라운드 스레드에서 관리된다.백그라운드 스레드
는 로그 큐에 저장된 메시지를 실제 출력 대상(콘솔, 파일, 데이터베이스 등)으로 전달하며, 이 작업은 메인 스레드의 작업과 별개로 처리된다.로그 메시지를 디스크에 기록하기 앞서 우선 메모리에 보관한다. 쓰기 처리량 역시 증갈한다.
운영체제의 페이지 캐시 역시 버퍼처럼 동작해서 로그 처리량을 향상하는데 도움이 된다.
비동기와 일괄쓰기는 성능을 향상시킬 수 있지만 애플리케이션에 크래시가 발생하몀 로그 메세지가 기록되지 않는 일도 생긴다.
당연한 얘기같지만 흔히 실수 하는 부분으로 URL이나 HTTP 응답을 아무 생각 없이 로그에 기록하면 안전 장치가 없는 로그 숫집기는 자칫 개인정보 노출 될 수 있다.