포트폴리오

다시 시도할수록 쌓이던 producer

Pulsar에서 메시지를 암호화하는 producer는 만들 때 암호화 키를 불러옵니다. 키를 불러오지 못하면 producer를 만들지 못했습니다. 하지만 그사이 broker에 연결을 걸어 둔 producer는 닫히지 않고 남았습니다. 그래서 애플리케이션이 다시 만들려고 할 때마다 broker에 producer가 하나씩 쌓였습니다.

키를 불러오지 못했을 때도 다른 생성 실패와 똑같이 producer를 닫고 클라이언트 목록에서 빼도록 고쳤습니다.

용량 제한에 걸린 업로드가 남긴 임시 파일

Spring은 업로드된 파일 조각이 메모리 한도를 넘으면 임시 파일로 옮겨 담습니다. 그런데 옮기는 순간 이미 디스크 한도까지 넘어 있으면 파일을 먼저 만든 뒤에 한도 오류가 났습니다. 그래서 그 파일을 지우는 정리 단계가 돌지 않았습니다. 이런 요청이 실패할 때마다 임시 폴더에 파일이 하나씩 남았습니다.

파일을 만들기 전에 디스크 한도부터 확인하도록 고쳤습니다.

세미콜론 없이도 통과하던 MIME 타입

MIME 타입 뒤에는 ;로 구분해서 charset 같은 매개변수를 붙입니다. 7.1에서 바뀐 해석기는 ; 없이 공백만 있어도 매개변수로 받아들여서 text/plain charset=ISO-8859-1처럼 잘못 쓴 값을 올바른 값처럼 처리했습니다.

매개변수 앞에 ;가 있을 때만 받아들이도록 고쳤습니다.

시간 단위를 초로 바꾸면 터지던 @Retryable

Spring의 @Retryable은 실패한 메서드를 일정 간격으로 다시 실행해 줍니다. 최대 대기 시간을 따로 정하지 않으면 아주 큰 값이 기본으로 들어가는데 시간 단위를 초나 분으로 바꾸면 이 값을 변환하다 숫자가 넘쳤습니다. 그래서 그 메서드를 부를 때마다 오류가 났습니다.

넘치지 않는 변환 방식으로 바꿔서 아주 큰 값은 그대로 “제한 없음"으로 다루도록 고쳤습니다.

모든 열이 있는데도 실패하던 검사

Spring의 DataClassRowMapper는 DB 조회 결과 한 줄을 객체 하나로 바꿔 줍니다. 생성자로 채우는 값은 setter로 채울 목록에서 빼 두는데 “모든 값이 채워졌는지” 확인하는 목록에서는 빼지 않았습니다. 그래서 생성자와 setter를 함께 가진 클래스에 이 확인을 켜면, 필요한 열이 모두 있어도 항상 실패했습니다.

생성자로 채우는 값을 두 목록에서 함께 빼도록 고쳤습니다.

두 번째로 읽으면 비어 있던 업로드 파일

Spring은 업로드된 파일 조각(Part)을 크기가 작으면 메모리에, 크면 디스크에 보관합니다. 디스크에 있는 것은 읽을 때마다 처음부터 읽혔습니다. 그런데 메모리에 있는 것은 한 번 읽고 나면 읽는 위치가 끝에 남았습니다. 그래서 두 번째부터는 빈 내용이 나왔습니다. 같은 코드가 파일 크기에 따라 다르게 동작했습니다.

메모리에 있는 것도 읽을 때마다 처음부터 읽히도록 고쳤습니다.

건너뛴 메시지를 기다리며 멈춘 refresh

TableView의 refresh는 “지금까지 들어온 메시지를 다 반영했다"고 확인해 주는 기능입니다. 그런데 처리하다 오류가 난 메시지는 건너뛰었지만 refresh가 기다리는 위치에는 그 메시지가 반영되지 않았습니다. 그래서 refresh는 이미 지나간 그 메시지를 계속 기다렸습니다. 새 메시지가 오지 않는 topic에서는 뷰를 닫을 때까지 끝나지 않았습니다.

처리에 실패한 메시지도 처리한 것으로 보고 그 메시지를 기다리던 refresh를 바로 끝내도록 고쳤습니다.

다시 읽기 전에 잠들던 TableView

TableView는 새 메시지를 읽다가 실패하면 잠깐 쉬었다가 다시 읽습니다. 그런데 이 기다림은 thread를 직접 재우는 식이었습니다. 이 thread는 클라이언트 안의 여러 기능이 함께 씁니다. 그래서 기다리는 동안 그 thread에 맡긴 다른 일이 멈췄습니다. 또 실패가 이어지면 같은 간격으로 끝없이 다시 시도했습니다.

thread를 재우는 대신 타이머에 다음 읽기를 예약하고 실패가 이어질수록 간격을 늘리도록 고쳤습니다. 뷰를 닫으면 예약해 둔 재시도는 읽지 않고 기다리던 refresh는 바로 실패로 끝납니다.

도구 호출을 글로 써 버린 모델

OpenClaw는 모델이 “이 도구를 실행해 달라"는 요청을 정해진 형식으로 보내면 그 도구를 실행합니다. 가끔 모델이 형식을 쓰지 않고 요청 내용을 그냥 글로 써 버리면 실행할 도구도, 사용자에게 보일 답도 없어서 “응답을 만들지 못했다"는 경고로 끝났습니다.

이런 경우에는 하던 일을 이어 가라고 모델에게 한 번 더 요청하도록 고쳤습니다.

빌드 진행률이 작업 표시줄에 안 보이던 이유

Gradle에는 빌드가 얼마나 진행됐는지를 터미널에 신호로 보내는 기능이 이미 있었습니다. 다만 신호를 받아 줄 수 있는 터미널 목록에 Windows Terminal이 빠져 있었습니다. 그래서 Windows Terminal에서는 아무것도 보내지 않았습니다.

Windows Terminal이 창마다 남겨 두는 환경 변수를 보고 알아보도록 고쳤습니다. 이제 빌드를 걸어 두고 다른 일을 하다가도 작업 표시줄 아이콘만 보면 진행 상황을 알 수 있습니다.

가끔만 실패하는 테스트의 원인 찾기

이 테스트는 “여기까지 받았다"는 확인을 보낸 다음, 읽는 위치를 앞으로 되감습니다. 확인은 네트워크를 거쳐 가고 되감기는 같은 프로그램 안에서 바로 실행됩니다. 그래서 컴퓨터가 바쁠 때는 나중에 시킨 되감기가 먼저 실행되기도 했습니다.

확인이 실제로 저장된 것을 본 다음에 되감도록 고쳤더니 이제는 테스트가 원래 보려던 순서대로 언제나 동작합니다.

닫히는 중인 클라이언트로 메시지를 보내면

메시지 전송이 실패하면 “실패했다"는 결과를 알려 주는 일은 따로 맡은 일꾼에게 넘깁니다. 그런데 클라이언트가 닫히는 중에는 이 일꾼도 곧 정리됩니다. 넘겨 둔 일을 처리하기 전에 사라지면 보낸 쪽은 결과를 끝없이 기다리게 됩니다.

클라이언트가 닫히는 중이라면 일꾼에게 넘기지 않고 그 자리에서 바로 결과를 알려 주도록 고쳤습니다.

TableView가 메시지 전체를 쓰게 하기

Pulsar의 TableView 기능은 topic을 읽어 key마다 가장 최근 값을 표처럼 들고 있습니다. 그동안은 메시지의 값만 저장할 수 있어서 메시지에 함께 붙어 오는 속성이나 시간 같은 정보는 쓸 수 없었습니다.

메시지 전체를 받아 원하는 값으로 바꿔 저장하는 방법을 새로 만들었습니다. 구현 PR을 먼저 올렸고, 공개 API를 바꾸는 일이라 리뷰에서 요청받아 설계 문서(PIP-445)를 뒤따라 썼습니다. 값으로 바꾸다 실패한 메시지는 건너뛰고 그 사실은 따로 알림으로 받을 수 있습니다.

글머리표가 두 번 찍히던 Markdown

OpenDataLoader PDF는 PDF를 Markdown으로 바꿔 줍니다. 글머리표가 있는 목록을 바꾸면 변환기가 붙이는 - 뒤에 원본 PDF의 •까지 남아서 - • 항목처럼 두 번 찍혔습니다.

원인을 찾은 뒤 Markdown으로 쓸 때 원본 글머리표를 지우도록 고쳐 보냈습니다. 유지관리자는 제 수정을 보고 목록을 찾는 단계에서 글머리표 길이를 계산하는 방식으로 고쳤습니다. 이 방식은 Markdown뿐 아니라 HTML 출력에도 적용됐습니다.

경고 없이 꺼지던 hybrid 모드

OpenDataLoader PDF는 --hybrid를 켜면 PDF 변환을 외부 변환기에 함께 맡깁니다. 그런데 --use-struct-tree를 같이 켜고 구조 정보가 들어 있는 PDF를 넣으면 외부 변환기를 한 번도 부르지 않으면서 아무 경고도 남기지 않았습니다.

두 옵션 중 어느 쪽이 먼저 적용되는지 원인을 찾아 재현 결과와 함께 이슈로 보고했습니다. 유지관리자가 그 이슈를 보고 경고와 도움말 설명을 추가했습니다.

대문자와 소문자 때문에 통과하지 못한 JWT

JWT의 header에는 토큰 종류를 적는 typ 값이 있습니다. 이 값을 검사하는 코드가 대문자와 소문자를 다른 글자로 취급해서 뜻이 같은 값인데도 표기에 따라 검사 결과가 달라졌습니다. 예전에 쓰던 검사 방식은 대문자와 소문자를 구분하지 않았기 때문에 버전을 올리면서 동작이 달라진 셈이었습니다.

글자 크기를 가리지 않고 비교하도록 고쳤습니다.