Post

2025년 6월 회고

2025년 6월 회고

쌓인 TIL 을 정리하기

그동안 쌓였던 TIL 들을 정리하는 시간을 가졌다.

https://github.com/happy-nut/happy-nut.github.io/tree/main/til

예전에 TIL 을 정리하곤 했었는데, 찾아보니 남아있긴 하나 그냥 처음부터 다시 작성하는 게 더 낫겠다 싶었다. 노션에서 마크다운으로 옮겨오는게 여간 불편했기 때문이다.

요즘은 트러블슈팅의 중요성에 대해 실감하고 있다. 어떤 문제들을 해결해보았느냐가 중요하다는 걸 최근에서야 깨달았다. AI가 발달할 수록 cs 지식들은 획득하기가 너무 쉬워졌고, 결국 남는 것은 경험이다.

앞으로는 트러블슈팅 기록을 남기는 한 편, 새로운 til 은 그대로 토막글 형태로 작성하되, 주말마다 정리하는 시간을 가져야 할 것 같다. 지하철 출퇴근 시간은 궁금한 걸 AI를 통해 채우기에 충분한 시간이다.

6월 동안 쓴 TIL

Lock Striping

  • lock striping 은 전체 자원을 bucket 으로 나누어 해당 bucket 단위로 Lock 을 거는 기법을 의미함
  • 예를 들어 ConcurrentHashMap 에서는 lock striping 기법을 활용해 동시성을 제어하는데, hash map 의 버킷의 첫 node 를 단위로 synchronized 를 걸어 동시성을 보장함
    • 참고로, ConcurrentHashMap은 단일 연산(get, put, remove)에 대해서는 thread-safe 함을 보장하지만, 복합 연산(size, forEach, clear) 에 대해서는 thread-safe 를 보장하지 않음
    • ConcurrentHashMap의 복합 연산에 대해 동시성을 확보하고 싶다면 전체 자원(Map 객체)을 대상으로 lock 을 걸어주어야 함
  • lock striping은 이 외에도, 여러가지 상황에서 사용됨
    • 리눅스 커널이 파일, 디렉토리 등의 inode를 다룰 때 inode_hashtable 을 여러 bucket으로 만들어 lock striping 을 함
    • Netty 에서 자주 쓰이는 SO_REUSEPORT는 여러 소켓이 하나의 포트에 바인딩하는 옵션인데, 소켓 단위로 lock striping 기법이 쓰임
    • JVM GC에서도 Lock striping 은 쓰임. concurrent 하게 region 을 sweep 하기 위해 region 별 lock 을 둠.

ReentrantReadWriteLock

  • Reentrant 라는 이름은 같은 스레드가 한 번 사용한 lock을 또 획득하고서 재진입(re-enter)할 수 있다는 데에서 유래함
  • ReentrantReadWriteLock에는 read lock 과 write lock 이 있음
  • read lock 은 여러 스레드가 동시에 획득 가능함, 단 write lock 이 걸려 있지 않은 경우에만 획득 가능.
  • write lock 은 다른 스레드의 모든 read/write lock 이 해제되어야 획득 가능
  • 만약 read lock 을 잡고 unlock 하지 않은 스레드가 write lock 을 잡으면 데드락이 발생함
  • 만약 write lock 을 잡고 unlock 하지 않은 스레드가 read lock 을 잡으면 그건 가능함
  • 대기하고 있는 스레드 중에서 어떤 걸 먼저 집어넣느냐는 공정성(fairness) 설정에 따름.
  • Non-Fair 일 때는 잡히는 놈부터 들여보내서 성능이 좋음. 대부분이 이 설정을 쓰는 경우가 많으나, 최악의 경우 Thread starvation 이 일어남
  • Fair 일 때는 FIFO를 보장함. 대신 어떤 스레드를 들여보낼 지 연산이 들어가기 때문에 성능이 안좋아짐

메모리 페이지와 디스크 스왑

  • 운영체제는 가상메모리를 관리할 때 Page 단위(4KB)로 관리함
  • CPU는 실제 물리 주소는 모르고 가상 주소만 다룸
  • 운영체제는 페이지 테이블(Page Table)을 통해 가상 주소 <-> 물리 주소(RAM)를 매핑함
  • 프로그램을 실행하면 코드, 데이터, 스택 등이 각각 메모리 페이지 단위로 로딩됨
  • OS는 현재 사용 중이지 않은 페이지를 swap 영역으로 이동시켜 RAM을 확보함
  • 페이지 안에 원하는 데이터가 없는 경우도 있는데, 이를 Page fault 라고 하고, 이 땐 디스크에서 원하는 데이터를 가져옴
  • 그럼 swat out 발생 시 페이지 테이블 안에는 RAM의 물리 주소가 아니라 디스크의 물리 주소가 적히는 건가?
  • 아니었음. swap 영역은 운영체제 내부적으로 slot 기반으로 추상화됨
  • page table 은 이 슬롯의 인덱스만 가지고 있음
  • Page table 은 구조체를 정의하는데 이 중 present 를 나타내는 비트로 page fault 여부를 검사하고, 0이면 swap 인덱스로 디스크에서 데이터를 가져오고 1로 마킹함

카프카의 auto commit 옵션과 Spring-kafka 의 AckMode

  • 카프카의 enable.auto.commit 옵션은 자동으로 커밋하도록 설정하는 옵션임
  • poll() 을 통해 메시지들을 가져오고 auto.commit.interval.ms 주기가 되었을 때 이전 poll() 에서 받은 메시지의 offset을 커밋함
  • poll() 을 한 번 하고 나서 메시지를 처리하다가 죽으면 오프셋은 커밋이 안되어서, 다시 살아났을 때 중복 처리가 발생할 수 있음
  • 오프셋은 polling loop 안에서만 커밋이 이루어지기 때문에, poll()이 호출되지 않으면 커밋도 되지 않음
  • spring kafka의 경우에는 에러가 발생하더라도 errorHandler 에서 캐치해버리고 마치 에러가 발생하지 않은 것처럼 다음 poll() 때 커밋함
  • 게다가, enable.auto.commit 이 false 더라도, 설정된 AckMode에 따라 manual commit 하지 않아도 알아서 커밋해줌
  • 오토 커밋이 켜져 있는 상태에서 파티션 리벨런싱이 일어나면?
  • 리벨런싱이 일어나면 메시지를 다른 파티션으로 옮기진 않고, 새로 추가된 메시지들만 키에 따라 분산해서 파티션에 들어감
  • 다만 컨슈머가 revoke 될 때 이미 poll() 해서 가져온 메시지들을 처리중이었다면 커밋되지 않은 오프셋은 그대로 사라지고 중복 컨슘이 발생할 수 있음

캐시 관련 문제 상황 별 대응 방안

  • Cache stampede: 동시에 대량의 키가 만료되면 DB에 요청이 몰려 과부하가 발생함
    • 이를 막기 위해 Jitter 로 만료 시간을 분산함
  • Cache penetration: DB에 없는 값을 조회할 경우 보통, 캐시를 갱신하지 않는데 이 요청이 너무 많으면 DB에 과부하가 발생함
    • 이를 막기 위해 Null object pattern을 사용해 ‘값이 없음’ 자체를 캐싱함
  • Cache system 장애: redis장애 시 DB로 요청이 몰려 과부하가 발생함
    • 반드시 동작해야 하는 핵심 기능을 제외하고 부가 기능은 운영을 잠시 중단하는 게 나음
    • 캐시 시스템이 복구되는 동안 DB 로 버티는 수밖에 없음
  • Hot key expiration: 많은 요청이 집중되는 hot key가 만료되는 경우, DB로 요청이 몰려 과부하가 발생함
    • 분산 락을 사용해 DB에 접근하는 요청 스레드를 제한하거나
    • 만료 없이 캐시 값을 다시 계산하여 덮어쓰기 함
  • Cache query 가 몰리는 경우: Redis 과부하로 이어질 수 있음
    • Local Cache 에 cache 데이터를 가져와 저장 후 cache miss 시 redis 조회
    • 이벤트 기반 (Redis pub/sub) 을 통해 최신 데이터 반영
  • 캐시를 쓰는 데도 너무 값이 자주 바뀌어 DB 요청량 자체가 너무 많은 경우 DB 과부하가 발생함
    • Write-behind 캐싱을 사용함(먼저 redis 에 기록 후 백그라운드에서 DB로 동기화)

Grafana + Prometheus 와 actuator

  • prometheus 는 주기적으로 시스템의 매트릭을 조회해서 prometheus 내부의 TSDB(Time Series Database)에 저장함
  • Grafana 는 promql 문법으로 매트릭을 조회하고 시각화함(시계열 매트릭을 시각화하는 데 최적화도미)
  • Spring 의 actuator 는 애플리케이션의 내부 메트릭을 Micrometer가 수집하여 backend에 맞게 포멧화하여 api 로 제공하는 역할을 담당함
  • Micrometer는 Spring의 메트릭 라이브러리이며 Prometheus 뿐만 아니라 Datadog, CloudWatch 등 다양한 backend를 지원함
  • 이를 통해 prometheus 는 host/actuator/prometheus 로 메트릭을 수집해서 저장할 수 있음
  • 메트릭을 가져갈 땐 pull 방식으로 가져가고, 관련된 scrape_interval 설정을 비롯해 여러 설정은 여기서 더 알아볼 수 있음
  • 메트릭을 수집하는 람다에서 너무 무거운 IO를 하지 않도록 주의해야 하고, 어쩔 수 없는 경우라면 캐싱을 고려해보아야 함

CAP의 partition tolerance

  • 네트워크 분할 내성으로 분산 시스템에선 포기할 수 없는 속성임
  • 여러 서버 중 한 대가 죽었다고 해서 모든 서비스 장애로 이어지면 안되기 때문
  • 또한 단순히 서버 한 대가 죽는 것을 넘어 네트워크 자체가 분할되는 상황(DC간 분리 등)에서 분리된 시스템(파티션)들은 독립적으로 잘 동작해야함을 의미함
  • 그래서 모든 속성을 동시에 만족할 수 없다는 CAP 이론에 따라 보통 가용성(A)이나 일관성(C) 중 하나를 포기함
  • CP시스템들은 일관성이 깨질 바에 응답을 포기함. 예를 들어 MongoDB는 세컨더리가 승격될 때까지 쓰기를 차단함
  • AP시스템들은 응답을 못할 바에 틀린데이터를 보여줌. DynamoDB는 네트워크 분할시에 일시적 불일치를 허용해서 복제가 지연되더라도 일단 해당 값을 응답함

g1gc 에서 pause time을 조절할 수 있는 이유

  • g1gc 는 CMS와 다르게 STW 시간을 조절할 수 있음
  • 이게 가능한 이유는 CMS와 달리 Eden, Survivor, Old가 각기 다른 사이즈를 같는 것이 아니라 heap 을 바둑판 형태로 쪼개 동일한 사이즈의 Region 조각으로 메모리 공간을 나누었기 때문임
  • 각 region 별로 Eden, Survivor, Old 역할을 할당해주고, 이 할당된 역할은 GC 사이클 중 동적으로 역할이 바뀔 수 있음
  • Eden 에는 새로 할당되는 객체들이, Survivor에는 Minor GC 이후 eden에서 살아남은 객체들이, Old GC는 여러번의 minor GC 이후 살아남은 객체들이 저장됨(단, Region 크기의 50%를 넘어가는 Humongous 객체는 바로 old 취급임), 이런 old region은 Mixed GC나 Full GC 에서 정리됨
  • Mixed GC란 young 과 old 일부만을 청소하는 GC로, 고정된 크기를 처리하기 때문에 걸리는 데 시간을 예측할 수 있음
  • 이렇게 선택적으로 청소를 할 수 있기 때문에 -XX:MaxGCPauseMillis 설정이 가능한 것
  • Full GC는 Humongous 객체 할당을 실패하거나, mixed gc로도 계속 쌓이는 등 여러 상황에서 발생할 수 있는데, full gc 가 발생하면 g1gc의 철학인 ‘점진적이고 예측가능한 GC’ 가 깨져버리기 때문에 full gc가 발생하지 않도록 주의해야 함
  • Region 사이즈를 조절하거나 mixed가 old를 좀 더 공격적으로 수거하도록 설정을 튜닝하는 것외에도 humongous 가 생기지 않도록 주의해야 함

String.format 성능이 구린 이유

  • format 안에서 내부적으로 포맷 지시자들을 정규식을 통해 찾음
  • 이 때문에 단순 concat 대비해 불필요한 객체들(Formatter 등)이 많이 생성되고, 성능도 안좋아짐
  • 이런 중간 객체들이 많이 생성되면 GC 압력이 증가하기 때문임
  • StringBuilder 나 + (concat) 연산은 JIT 컴파일러가 aggresive 하게 인라인 최적화할 수 있음
  • 반면 String.format 은 내부 동작이 복잡하기 때문에 JIT 최적화가 어렵고 느림
  • 따라서 성능이 중요하다면 StringBuilder를 쓰는게 훨씬 나음

HTTP 와 TCP 의 keep alive 설정

  • 설정 이름이 똑같아서 얼핏 비슷해 보이지만 목적이 서로 다름 (계층도 각각 L7, L4로 다른 것처럼)
  • http 의 keep alive 는 매 자료 요청마다 3 way handshake를 하지 않기 위해 설정함(성능 최적화)
  • tcp 의 keep alive 는 소켓을 재사용하기 위해 사용하고 죽은 소켓을 탐지하기 위함임(네트워크 안정성)
  • tcp 의 keep alive 설정은 일정시간마다 probe 패킷을 전송하여 이를 탐지함
  • tcp 의 keep alive 보다 http keep alive 가 더 클 수는 없음
  • 그 전에 연결이 끊기기 때문에 초과값은 의미가 없어지기 때문임
  • 실무에선 대개의 경우 tcp 의 keep alive 설정이 훨씬 길어서 TCP keep alive 가 작동하기 전에 http 커넥션은 정리됨
  • 단, 웹소켓이나 RPC 처럼 긴 연결을 맺고서 하는 통신의 경우 TCP 설정 튜닝이 들어가서 달라질 수 있음

Java Flight Recorder

  • JAVA 7 버전 이상에서 Java 애플리케이션의 성능과 동작을 분석하고 디버깅하는 데 사용되는 프로파일링 도구임
  • JVM 내부에서 발생하는 다양한 이벤트(예: GC, 메서드 실행 시간, 스레드 상태 변화 등)를 추적함
  • java -XX:StartFlightRecording=duration=60s,filename=recording.jfr -jar myapp.jar 옵션으로 리코딩을 시작할 수 있음
  • JDK Mission Control (JMC)는 GUI 기반 JFR 분석 도구로, JVM 내부 동작을 정밀하게 시각화하여 볼 수 있음
  • .jfr파일은 인텔리제이에서도 열 수 있음

Augmented Coding(증강 코딩): 바이브를 넘어

  • 증강 코딩이란 프로그래머가 AI와 협력하여 코드를 생성하는 방식이고, 코드 품질에 신경쓰지 않는 바이브 코딩과는 차이점이 있음
  • 켄트백이 증강 코딩을 통해 B+tree를 구현했고, 그 과정에서 다음 철칙을 지키도록 함
    • TDD 사이클(red, green, refactor)을 따르도록 함, 테스트가 통과해야지만 커밋하라고 지시
    • Tidy First (구조 변경과 기능 추가를 분리하여 수행하도록 함)
  • 그 과정에서 AI가 무언가 잘못되어간다는 경고 신호를 발견함
    • 무한 루프
    • 요청하지 않은 기능 추가
    • 테스트 비활성화 또는 삭제(cheating)
  • 또, AI가 rust 로 코드를 잘 작성하지 못하자, 파이썬으로 먼저 구현시킨 후 그것을 rust 로 번역하도록 하니 잘 작성하였음
  • 최종 결과물은 정확성과 성능 면에서는 만족스러웠으나, 코드 품질 면에서는 덜 만족스러웠다고 함
    • AI 가 단순한 코드를 유지하게끔 하는데 어려움을 겪음
This post is licensed under CC BY 4.0 by the author.