필독 개발자 온보딩 가이드 4장 -(2)
Last updated
Last updated
이 글은 책을 읽고 공부하고 기억하기 위한 기록용으로 문제가 생기면 삭제하겠습니다.
단순히 다시 시도하는 것이다. 작업을 다시 시도한다는 것은 쉬운 일처럼 보일 수 있다. 하지만 현실에서 재시도의 시기와 빈도를 판단하려면 약간의 노하우가 필요하다.
여러 차례 재시도하려는 시도는 주로 원격 시스템에 호출할 경우 나타난다.
문제는 재시도 후 또 실패하면 속도만 느려지고 시스템 복구하기는 점점 어려워진다.
백오프란 비선형으로 대기 시간을 늘리는 방법으로 이 방법을 사용할 때 백오프 시간의 상한선을 정해서 대기 시간이 너무 길어지지 않게 하자.
백오프 전략(Backoff Strategy)은 주로 컴퓨터 네트워크에서 사용되는 전략 중 하나로, 일시적인 문제 또는 혼잡 상황이 발생했을 때 어떻게 대응할지 결정하는 방법론으로 이 전략은 주로 재시도(retry) 메커니즘과 관련이 있다.
동작 과정
일시적 문제 발생: 데이터 패킷 전송이 실패하거나 응답이 없는 등의 일시적인 문제가 발생합니다.
재시도 결정: 문제가 발생했을 때 바로 다음 시도를 하지 않고, 잠시 기다린 후에 다시 시도할지를 결정합니다.
기다림(백오프)
재시도를 위해 기다리는 시간을 정합니다.
이때 기다리는 시간은 여러 방식으로 결정될 수 있는데, 일반적으로 지수적인 방식이 사용됩니다. - 즉, 처음에는 짧은 시간 동안 기다리다가, 실패할 경우 점차 더 긴 시간을 기다리는 방식입니다. - 이로써 네트워크 혼잡을 피하고, 문제를 일시적으로 해결할 기회를 늘릴 수 있습니다.
재시도: 일정 시간 동안 기다린 후에 다시 시도합니다.
백오프 전략의 장점
네트워크 통신에서 패킷 손실, 지연, 혼잡 등의 문제에 유용하게 적용
예를 들어, 데이터 전송 중에 패킷 손실이 발생했을 때 즉시 재시도하지 않고, 잠시 기다렸다가 재시도함으로써 일시적인 문제를 해결할 수 있다. 이는 데이터 전송의 안정성과 신뢰성을 높이는 데에 기여
백오프 예시 코드가 뭐가 있어?
백오프 전략은 주로 네트워크 통신에서 사용되는 개념이며, 코드로 구현될 때는 다양한 상황에 맞게 적용될 수 있습니다. 아래는 간단한 예시 코드로, 일정 시간 동안 기다린 후에 재시도하는 간단한 백오프 전략을 보여줍니다.
이 예시 코드에서는 sendDataToServer() 함수가 데이터 전송을 시뮬레이션하며, 랜덤한 성공률로 데이터 전송의 성공 여부를 반환합니다. 데이터 전송이 실패한 경우 백오프 전략을 적용하여 재시도하고, 일정 시간 동안 기다린 후에 다시 시도합니다. 이렇게 재시도와 기다림을 반복하며 데이터 전송을 시도하게 됩니다.
지터란
임의의 지연시간, 시간의 불규칙한 변동이나 불규칙한 간격을 나타내는 용어로 주로 네트워크나 타이밍과 관련된 상황에서 사용된다.
천둥떼 현상으로 복구 중이던 서비스 다운을 막기 위해 지터를 추가하면 클라이언트들은 특정 범위에서 임의의 값을 백오프 시간에 더한다.
지터는 예상된 시간 간격과 실제 발생한 시간 간격 사이의 차이를 나타내며, 주로 네트워크 패킷의 전송 간격이나 디지털 신호의 타이밍 등에서 발생한다.
예를 들어, 음성 또는 영상 데이터를 실시간으로 전송하는 경우, 일정한 간격으로 데이터를 전송해야 한다. 그러나 네트워크 지연이나 데이터 처리 속도 등으로 인해 실제 전송 간격이 예상과 다를 수 있습니다. 이러한 시간적인 불규칙성을 지터라고 한다.
지터는 통신 시스템에서 중요한 개념으로, 지터가 크면 신호의 도착 시간이 불안정하게 되어 음성이나 영상 데이터에서 소리나 영상이 끊기거나 불안정해질 수 있다.
따라서 통신 시스템 설계나 관리에서 지터를 최소화하여 신호의 안정성과 품질을 유지하는 것이 중요
천둥떼 현상이란
네트워크 서버에 일시적인 문제가 생겨 모든 클라이언트가 동시적으로 장애를 겪는 상황에서 모든 클라이언트가 동일한 백오프 알고리즘을 사용한다면 모두가 동시에 요청을 다시 보내는 현상
가정
A와 B라는 두 대의 컴퓨터가 네트워크를 통해 데이터를 주고받는 상황을 가정합니다.
A가 데이터를 B에게 보내려고 할 때, 네트워크 지연과 패킷 손실이 발생할 수 있습니다.
백오프 전략
A가 데이터를 보낼 때 충돌이 발생하면 일정 시간(백오프 타임) 동안 기다린 후 재전송을 시도하는 전략입니다.
충돌이 발생하면 백오프 타임을 지정하고 그 시간 동안 기다린 후 데이터를 다시 전송합니다.
지터 추가
지터를 추가하여 백오프 타임에 불규칙성을 부여합니다. 이로 인해 동일한 백오프 타임을 사용하는 모든 재전송이 동시에 발생하는 것을 방지하고 네트워크 상황을 더 혼잡하지 않게 만듭니다.
예시
아래는 백오프 전략과 지터를 함께 적용한 예시 코드. 코드는 단순한 예시이므로 실제 상황에 맞게 조정이 필요할 수 있다.
설계 시점에서 처리를 염두에 두지 않은 에러가 발생하면 차라리 애플리케이션에 크래시하도록 놔두는 편이 낫다. 이런 방법을 빨리 실패하기라고 한다.
애플리케이션 크래시
애플리케이션이 "크래시"한다는 것은 해당 애플리케이션이 예상치 못한 오류 또는 예외 상황으로 인해 정상적으로 작동하지 않고 비정상적으로 종료되는 상황
빨리 실패하면 사람이 올바르게 대처할 방법을 찾을 수 있다.
실패 상황을 전파하는 방안을 생각해야 하며, 쉽게 디버깅할 수 있도록 에러 관련 정보는 반드시 확인이 가능해야 한다.
예를 들어, 어떤 웹 서비스의 주문 생성 기능을 가정해봅시다. 이 기능은 주문을 생성하고 데이터베이스에 저장하는 작업을 수행 이 때 멱등성의 원칙을 지키면 아래와 같은 상황에서도 동일한 결과를 보장해야 한다
예시
주문 생성 시도 1회: 사용자 A가 상품 X를 주문하려고 함. 주문이 생성되어 데이터베이스에 저장됨.
주문 생성 시도 2회: 사용자 A가 동일한 상품 X를 다시 주문하려고 함.
멱등성을 지킨다면 이 작업은 이전에 주문이 이미 생성되었음을 감지하고 동일한 주문을 중복 생성하지 않아야 함.
주문은 이미 존재하므로 새로운 주문이 생성되지 않아야 함.
이렇게 멱등성을 지키는 것은 데이터의 일관성과 중복 생성을 방지하는 데 도움이 된다. 이와 유사하게 다양한 작업이나 API 호출에서 멱등성을 고려하여 중복 작업이나 데이터 중복을 방지할 수 있다.
멱등성이란?
동일한 작업을 여러 번 실행해도 항상 같은 결과가 출력됨을 말한다. 모든 작업을 멱등 작업으로 구현하면 시스템 상호작용이 훨씬 편해지며 에러도 현저히 줄어든다,
예를 들어 어떤 값을 해시셋에 추가하는 것은 멱등 작업이다.
이유는 몇 개의 값을 집어놓던지 하나만 존재하기 때문이다.
원격 API도 클라이언트가 각 요청마다 유일한 ID를 지정하게 하면 멱등성 구현 가능
클라이언트가 재시도할 때 실패한 것과 동일한 요청 ID를 전달하면 된다.
그러면 서버는 이미 해당 요청이 처리된 경우에는 해당 작업 실행하지 않는다.
더 이상 필요로 하지 않는 메모리, 데이터 구조, 네트워크 소켓, 파일 핸들 모두 해제하자.
운영체제는 파일 핸들과 네트워크 소켓을 위한 공간이 정해졌는데 가득 차면, 새로 핸들, 소켓 모든 작업이 실패한다.
네트워크 소켓이 누수되면 불필요한 연결에 계속 남아있어 연결 풀이 가득 차게 된다.
f.close를 실행하기 전 코스 실행 실패로 파일 포인터를 닫지 못하기 떄문에 개발 언어가 자동해제를 지원하지 않는다면 try/finally
로 파일 핸들 안전하게 닫게 해줘야 함
현대 개발 언어는 자동리소스 해제 지원, rust는 소멸자 메소드
로, 파이썬은 with 구문
코드를 쉽게 운영하고 디버그할 수 있도록 로깅 프레임워크를 활용하자. 로그 레벨을 설정해서 운영자가 애플리케이션의 로그 양 조정할 수 있게 하자. 로그는 원자적이고 빠르며 안전하게 다뤄야 한다.
개발 언어는 복잡한 애플리케이션을 위해 언제 어떤 것을 로그에 기록할 지 제어하는 연산자를 제공하는 정교한 로깅 라이브러리
를 갖추고 있다.
이러한 연산자를 이용해 로그 레벨을 이용해 로그 양 조절하거나 로그 형식 제어 가능
이 외에도 다양한 로깅 라이브러리가 있으며, 선택할 때 프로젝트의 요구 사항과 개발 환경을 고려하여 적절한 로깅 라이브러리를 선택하는 것이 중요
1. Log4j 2
Apache Log4j 2는 Java의 로깅 프레임워크로 매우 유연하고 강력한 로깅 기능을 제공한다.
로그 메시지를 다양한 출력 대상으로 라우팅하고, 로깅 레벨 및 로그 형식 설정 등이 가능하다.
2. SLF4J (Simple Logging Facade for Java)
SLF4J는 로깅 프레임워크의 추상 계층으로, 다양한 로깅 라이브러리와 결합하여 사용할 수 있다.
주로 로그 API의 추상화를 제공하며, 로깅 라이브러리를 변경해도 코드 수정을 최소화할 수 있으며, 실제로 SLF4J를 구현하는 로깅 라이브러리로 Logback을 많이 사용한다.
**3. Logback **
Logback은 SLF4J의 구현체로, Log4j의 후속 제품으로 개발된 것으로 Logback은 기본적인 설정으로도 간단하게 사용할 수 있지만, 복잡한 설정 및 유연한 로깅 기능을 제공한다.
4. java.util.logging (JUL)
자바 표준 라이브러리에 포함된 로깅 프레임워크로, JUL을 사용하면 별도의 라이브러리를 추가로 필요하지 않는다.
기능은 간단하지만 가볍고 표준화된 로깅을 위한 선택지로 사용할 수 있습니다.
5. Log4j 1
Apache Log4j 1은 로깅 프레임워크로 유명하며 예전부터 많이 사용되어왔으나 현재는 Log4j 2와 비교하여 유지보수 및 보안 이슈로 인해 사용이 권장되지 않는다.
로깅 프레임워크는 운영자가 중요도에 따라 메세지를 필터링할 수 있도록 로그 레벨을 지원한다.
운영자가 로그 레벨을 설정하면 설정한 레벨보다 상위 레벨의 로그는 모두 기록되는 반면** 그보다 낮은 레벨의 로그는 기록되지 않는다.**
로그 레벨을 사용하면 매우 상세한 디버깅 로그부터 정상적인 운영 상황에서 주기적으로 기록되는 로그까지, 주어진 상황에 맞춰서 로그의 양 조절 가능
자바의 log4j.properties 파일의 일부로서, 루트에는 ERROR 레벨의 상세한 로그 지정하고 com.foo.bar 패키지 내의 코드에서는 INFO 레벨의 로그 지정
TRACE
정리하자면,
예시: 특정 메소드나 함수의 호출과 반환 값을 로깅하여 디버깅 시 호출 흐름을 확인할 때 사용.
적용 방법: 로그 라이브러리에서 제공하는 TRACE 레벨 메소드를 사용하여 특정 작업이나 메소드의 세부 정보를 로깅
특정 패키지나 클래스에만 켜지며 최대한 상세한 내용을 출력하는 레벨
개발 환경 이외에서는 사용하는 경우는 거의 없다.
줄 단위 로그나 데이터 구조를 확인하는 데 사용
TRACE 로그 레벨을 자주 사용하는 편이라면 로그 출력 대신, 디버거
를 이용해 코드의 실행 과정을 확인하는 것이 좋다
DEBUG
정리하자면,
예시: 사용자의 입력 데이터를 처리하는 중간 과정을 로깅하여 디버깅 시 데이터 처리 과정을 확인할 때 사용.
적용 방법: 로그 라이브러리에서 제공하는 DEBUG 레벨 메소드를 사용하여 중요한 상태 변경 또는 중간 과정을 로깅
프로덕션 상황에서 문제가 발생했을 때 적합한 레벨이다.
디버그 레벨 로깅을 너무 많이 사용 시 디버깅 할 때 필요한 정보를 찾기 어려워진다.
이런 메세지는 TRACE 레벨로 지정하자.
INFO
정리하자면,
예시: 애플리케이션의 시작과 종료 시점, 서비스 포트 설정 등 애플리케이션의 기본 정보를 로깅할 때 사용.
적용 방법: 로그 라이브러리에서 제공하는 INFO 레벨 메소드를 사용하여 애플리케이션의 상태 정보를 로깅
애플리케이션 상태에 대해 알아두면 좋을 만한 정보를 위한 레벨이다.
즉 문제점을 파악하기 위한 용도가 아니다.
보통 서비스 시작
이나 5050번 포트 사용
과 같은 애플리케이션 상태 메세지를 이 INFO 레벨로 출력한다.
INFO는 기본 로그 레벨이니, 시시한 내용을 기록하지 말자.
만약을 위한
로그는 TRACE나 DEBUG레벨로 출력하자
INFO 레벨 로그는 정상적인 운영 상황에서 유용한 정보를 제공해야 한다.
러스트에서 INFO 레벨 출력하는 예시
요청이 실패한 원인을 유발한 에러도 포함되어 있는데 그런데도 info 레벨을 쓴 이유는 애플리케이션이 자동으로 재시도 하므로 추가 대응할 필요가 없기 때문이다.
WARN
정리하자면,
예시: 서비스 리소스가 한계치에 다다른 상황이나 예상치 못한 동작을 로깅하여 경고할 때 사용.
적용 방법: 로그 라이브러리에서 제공하는 WARN 레벨 메소드를 사용하여 경고할 만한 상황을 로깅
잠재적으로 문제가 될 만한 상황에 대한 메세지 출력을 위한 레벨
어떤 리소스가 한계치에 다다르고 있다면
경고 메세지 출력하기 적합
WARN 레벨 로그를 출력 할 때는 그 메세지를 확인한 사람이 취해야 할 구체적인 대안이 있어야 하며, 없다면 INFO레벨로 옮기자
ERROR
정리하자면,
예시: 예외 발생, 데이터베이스 작업 실패 등 오류 상황에 대한 정보를 로깅할 때 사용.
적용 방법: 로그 라이브러리에서 제공하는 ERROR 레벨 메소드를 사용하여 오류 상황에 대한 정보 및 스택 트레이스 등을 로깅합니다.
살펴봐야 할 에러가 발생했을 때 위한 레벨
데이터 베이스 기록 작업이 실패하면 대체로 ERROR 레벨 로그에 기록
문제를 분석하기에 충분한 정보 제공
관련 스택 추가적과 소프트웨어가 실행된 결과 등 상세한 내용 명확하게 기록
FATAL
가장 위험한 수준의 메세지 출력하기 위한 레벨 -** 프로그램이 심각한 상황에 맞닥뜨려 즉시 종료시켜야 한다면** 그 문제를 유발한 원인을 FATAL 레벨 로그 기록
복구 지점이나 분석 관련 데이터 프로그램 상태 관련된 컨텍스트도 반드시 기록
1. 메소드 호출을 통한 로그 레벨 지정
SLF4J와 Logback을 사용할 때는 로그 레벨을 지정하는 메소드를 사용한다. SLF4J의 로거(Logger) 인스턴스를 가져온 후 해당 인스턴스의 메소드를 호출하여 로그를 남길 수 있다. 메소드에는 로그 레벨을 지정하는 파라미터가 있다.
2. 설정 파일을 통한 로그 레벨 지정
Logback을 사용할 경우에는 logback.xml 또는 logback.groovy 설정 파일을 통해 로그 레벨을 지정할 수 있다. 설정 파일에서는 다양한 로그 레벨을 각각의 로그 출력 대상에 지정할 수 있다.
원자적으로 작성하자
는 프로그래밍 및 데이터 관리에서 매우 중요한 원칙 중 하나를 나타냅니다. 이 원칙은 작업이 더 작은 조각으로 분해되거나 중간에 중단되지 않고 완전히 실행되도록 보장하는 것을 의미한다.
여기서 원자적
이란 작업이 더 이상 나눌 수 없는 최소 단위
이 단위에서 작업은 더 이상 분해되거나 중단될 수 없어야 한다. 원자적 작업은 다음 두 가지 특성
분해 불가능(Indivisible)
: 원자적 작업은 더 작은 단위로 나눌 수 없어야 한다. 작업을 구성하는 각 단계는 분해되거나 재정의되지 않고 실행되어야 한다.
중단 불가능(Undivisible)
: 원자적 작업은 중간에 중단되지 않고 완전히 실행되어야 한다. 만약 작업의 일부만 실행되고 중단되면 일관성과 정합성 문제가 발생할 수 있다.
로그를 원자적으로 작성한다는 것은 로그 메시지가 한 번 작성되면 분해되거나 중단되지 않고 완전히 기록되어야 한다는 의미
예를 들어, 여러 로그 메시지를 연속적으로 작성할 때 각 로그 메시지가 순서대로 기록되는 것을 보장하고, 중간에 작업이 중단되거나 누락되지 않도록 해야 한다. 이를 통해 로그를 통해 시스템 상태나 이벤트에 대한 정확하고 일관된 정보를 얻을 수 있다.
한 메세지에 모든 정보를 원자적으로 저장하자.
로그 수집기는 관련 정보를 한 줄에 표현하는 로그를 더 잘 처리
but, 특정 순서대로 보이지 않을 수 있으며, 로그 정렬 시 시스템 시간에 의존하지말자. 시스템 시간은 리셋되거나 호스트 마다 조금씩 다를 수 있음
로그 메세지에 줄바꿈 문자도 피하자 => 특히 WARNING 로그에 경우, 다른 메세지와 혼합이 되기 때문에 한 줄로 출력 불가하다면 고유한 ID를 포함시켜 나중에 연결할 수 있게 하자.
로그를 너무 기록하면 성능에 영향을 미친다. 로그는 디스크나 콘솔, 원격 시스템 등 어딘가에 반드시 기록되어야 하며, 기록되기 전 한 문자열로 결합해야 한다.
매우 느리게 진행되며, 성능이 중요한 루프에 악영향을 미친다,
결합을 시도하는 문자열이 로그 메소드에 전달되면 로그 레벨과 상관 없이 결합 실행 이유 : 소드의 인수는 메소드에 전달기 앞서 평가가 이뤄지기 때문
프레임워크가 지원한다면, 파라미터화 로깅을 사용하자
JAVA의 로그 호출 시 문자열 결합 방법 3가지
로그 메시지의 내용을 동적으로 생성하고 로깅하는 기법,** 이를 통해 로그 메시지에 변수나 데이터 값을 쉽게 포함시킬 수 있다.** 파라미터화 로깅은 로그 메시지에 변수를 직접 결합하는 대신, 변수 값을 포함할 위치를 지정하고 실제 값은 로깅 함수의 파라미터로 전달하여 로그 메시지를 생성한다.
장점
가독성 향상: 로그 메시지 내에 변수 값을 직접 결합하는 대신 변수 값을 파라미터로 전달하므로 로그 메시지가 더 읽기 쉽고 명확해진다.
동적 로깅: 로그 메시지에 동적으로 변경되는 변수 값을 포함할 수 있다. 이를 통해 특정 이벤트의 상황에 맞는 변수 값을 로그로 남길 수 있다.
보안 강화: 민감한 정보를 로그로 남길 때, 값을 직접 노출하는 대신 파라미터로 전달하면 민감한 정보가 로그 파일에 남을 가능성을 줄일 수 있다.
어펜더는 로깅 시스템에서 로그 메시지를 어디에 기록할지를 결정하는 역할을 하는 요소로 로그 메시지는 어펜더를 통해 특정한 출력 대상에 기록되게 된다. 보통 어펜더는 로그를 콘솔에 출력하거나 파일에 기록하는 등의 역할을 수행한다.
로깅 시스템에서 어펜더는 로그 출력의 대상과 형식을 지정하는 역할을 하며, 기본적으로 탑재된 로그 어펜더는 print 함수와 마찬가지로 호출자의 스레드에서 실행
비동기 어펜더
현재 실행 중인 어펜더는 현재 실행 중인 스레드를 블록하지 않고 메세지를 기록한다.
비동기 어펜더의 작동원리
로그 메시지는 메인 스레드에서 비동기 어펜더에게 전달
비동기 어펜더
는 로그 메시지를 로그 큐(또는 버퍼)에 저장한다. 이때 큐는 별도의 백그라운드 스레드에서 관리된다.
백그라운드 스레드
는 로그 큐에 저장된 메시지를 실제 출력 대상(콘솔, 파일, 데이터베이스 등)으로 전달하며, 이 작업은 메인 스레드의 작업과 별개로 처리된다.
이를 통해 메인 스레드는 로그 출력 작업에 대한 지연을 겪지 않고 다른 작업을 계속 수행할 수 있다.
장점
로그 출력 작업의 지연이 발생해도 메인 스레드의 성능에 영향을 덜 준다.
백그라운드 스레드에서 로그 출력 작업을 관리하므로, 로깅 작업으로 인한 애플리케이션의 멈춤 현상을 방지할 수 있다.
로그 출력 작업이 비동기적으로 처리되므로 전체 애플리케이션의 성능을 향상시킬 수 있다.
일괄 어펜더
로그 메시지를 디스크에 기록하기 앞서 우선 메모리에 보관한다. 쓰기 처리량 역시 증갈한다.
운영체제의 페이지 캐시 역시 버퍼처럼 동작해서 로그 처리량을 향상하는데 도움이 된다.
비동기와 일괄쓰기는 성능을 향상시킬 수 있지만 애플리케이션에 크래시가 발생하몀 로그 메세지가 기록되지 않는 일도 생긴다.
당연한 얘기같지만 흔히 실수 하는 부분으로 URL이나 HTTP 응답을 아무 생각 없이 로그에 기록하면 안전 장치가 없는 로그 숫집기는 자칫 개인정보 노출 될 수 있다.