<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Kotlin on 얼쑤 · Team Edo</title><link>https://team-edo.github.io/tags/kotlin/</link><description>Recent content in Kotlin on 얼쑤 · Team Edo</description><generator>Hugo</generator><language>ko-kr</language><lastBuildDate>Mon, 09 Nov 2026 00:00:00 +0900</lastBuildDate><atom:link href="https://team-edo.github.io/tags/kotlin/index.xml" rel="self" type="application/rss+xml"/><item><title>악기 이름을 못 불러와도 내 점수는 볼 수 있어야 한다</title><link>https://team-edo.github.io/posts/2026/11/practice-result-optional-data/</link><pubDate>Mon, 09 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/practice-result-optional-data/</guid><description>&lt;p>같은 곡을 두 번 연주한 뒤 첫 번째 기록을 열었다고 해봅시다. 화면에 곡명이 맞게 표시돼도 점수와 틀린 위치가 두 번째 연주의 것이라면, 사용자는 첫 번째 연주에서 무엇을 고쳐야 할지 판단할 수 없습니다. 반대로 악기 이름만 늦게 도착한 경우라면, 점수와 악보부터 확인할 수 있습니다.&lt;/p>
&lt;p>얼쑤의 이전 연주 결과 화면은 이 두 상황을 서로 다른 상태로 처리합니다. 선택한 기록의 결과인지 먼저 확인한 뒤, 점수를 읽는 데 필요한 데이터와 나중에 도착해도 되는 데이터를 나눴습니다.&lt;/p></description></item><item><title>한글을 쳤는데 Enter가 동작하지 않았다</title><link>https://team-edo.github.io/posts/2026/11/compose-web-korean-ime-enter/</link><pubDate>Mon, 09 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/compose-web-korean-ime-enter/</guid><description>&lt;p>정간보 편집기에서 칸을 고르고 &lt;code>청태&lt;/code>를 입력한 뒤 Enter를 눌렀습니다. 입력칸에 글자는 보였지만 악보에 음이 적히지 않았습니다. 미리보기까지 사라져 있었으니, 처음 쓰는 사람이라면 입력이 아직 진행 중인지, 이 음을 쓸 수 없는지, Enter가 동작하지 않은 건지 구별하기 어렵습니다.&lt;/p>
&lt;h2 id="글자가-보이는-시점과-조합이-끝나는-시점">글자가 보이는 시점과 조합이 끝나는 시점&lt;/h2>
&lt;p>한글 입력기는 자모를 모아 글자를 만들고, 조합 중인 범위도 입력 상태에 남깁니다. Compose의 &lt;code>TextFieldValue&lt;/code>에는 현재 문자열과 조합 범위가 함께 들어 있습니다. 브라우저에서는 글자 조합 이벤트와 키 이벤트를 따로 다룹니다. Enter를 눌러도 그 키 이벤트가 앱의 완료 콜백으로 이어지는지는 입력기와 UI가 이벤트를 처리하는 방식에 달려 있습니다. &lt;a href="https://www.w3.org/TR/uievents/#keys-during-composition">W3C UI Events의 조합 중 키 이벤트&lt;/a>&lt;/p></description></item><item><title>Jenkins Wasm 최적화 956초에서 237초로</title><link>https://team-edo.github.io/posts/2026/11/jenkins-wasm-build-bottleneck/</link><pubDate>Wed, 04 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/jenkins-wasm-build-bottleneck/</guid><description>&lt;p>얼쑤의 Jenkins 빌드가 길어졌을 때, 저는 완료 화면의 전체 시간부터 단계별로 쪼개 봤습니다. 백엔드 빌드에서는 시작 기록이 남은 뒤 첫 단계까지 14분 넘게 걸렸고, 별도의 웹 빌드에서는 실제로 실행기에 올라간 뒤 Wasm 최적화 도중 시간 제한에 걸렸습니다. 두 경우 모두 빌드가 오래 걸렸지만, 손봐야 할 곳은 달랐습니다.&lt;/p>
&lt;h2 id="첫-단계가-시작되기까지-얼마나-걸렸나">첫 단계까지의 대기 시간&lt;/h2>
&lt;p>빌드 기록의 시작·종료 시각을 Jenkins의 단계별 시작·종료 시각과 맞춰 봤습니다. 빌드 시작부터 첫 단계까지, 첫 단계부터 마지막 단계가 끝날 때까지, 마지막 단계 이후로 나누면 어느 구간이 길었는지 보입니다.&lt;/p></description></item><item><title>고치기 전에 실패해야 증거가 되는 테스트</title><link>https://team-edo.github.io/posts/2026/11/test-fails-first/</link><pubDate>Mon, 02 Nov 2026 00:00:00 +0000</pubDate><guid>https://team-edo.github.io/posts/2026/11/test-fails-first/</guid><description>&lt;p>버그를 고치고 테스트를 추가했더니 통과했습니다. 그럼 버그가 고쳐졌다고 말할 수 있을까요?&lt;/p>
&lt;p>얼쑤의 연주 비교 기능을 고치면서, 답은 &lt;strong>고치기 전에 그 테스트가 실패했는지 봐야 안다&lt;/strong>는 걸 배웠습니다.&lt;/p>
&lt;h2 id="다시-연주한-구간을-비교하는-기능">다시 연주한 구간을 비교하는 기능&lt;/h2>
&lt;p>&lt;strong>얼쑤&lt;/strong>는 국악기 연주를 녹음하면 음표마다 맞게 연주했는지 알려 주는 앱입니다. 틀린 구간만 골라 다시 연주하면, 앱이 &lt;strong>이전 연주와 이번 연주를 비교&lt;/strong>해 어떤 음을 고쳤고 어떤 음이 아직 틀렸는지 보여 줍니다.&lt;/p></description></item><item><title>Jackson 3에서 JSON 배열과 누락 필드 읽기</title><link>https://team-edo.github.io/posts/2026/11/read-stored-json-trees-with-jackson/</link><pubDate>Mon, 02 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/read-stored-json-trees-with-jackson/</guid><description>&lt;p>학습 단계의 설명과 채점에 쓸 악보는 DB에 JSON으로 저장합니다. 학습 단계에서 잘못된 항목을 건너뛰면 화면에 설명이 빠진 채 나타납니다. 악보에서 정수를 잘못 읽으면 음표의 길이나 시작 박을 다르게 해석할 수 있습니다. 두 파서에서 저장값을 읽는 지점에 입력 계약을 드러냈습니다.&lt;/p>
&lt;h2 id="배열-노드에서-원소를-꺼낸-뒤-변환했다">배열 원소를 꺼내 변환&lt;/h2>
&lt;p>학습 단계에는 할 일과 운지 설명처럼 배열로 저장하는 항목이 있습니다. Jackson 3의 &lt;code>JsonNode.map&lt;/code>은 노드 자체에 함수를 적용합니다. 배열의 각 원소를 변환하려면 노드가 배열인지 확인하고 &lt;code>values()&lt;/code>로 원소를 꺼낸 뒤 Kotlin의 &lt;code>map&lt;/code>을 적용합니다.&lt;/p></description></item><item><title>없는 행과 NULL 값 구분하기</title><link>https://team-edo.github.io/posts/2026/11/preserve-empty-record-and-cache-results/</link><pubDate>Mon, 02 Nov 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/11/preserve-empty-record-and-cache-results/</guid><description>&lt;p>얼쑤는 녹음 파일이 없어도 연습 기록을 저장할 수 있습니다. 또 저장 뒤 악보 버전이 바뀌면 기록은 남지만 현재 악보에서 어느 칸에 해당하는지 표시하지 않습니다. 연습 기록 API를 만들면서 이렇게 값이 비는 경우를 두 곳에서 따로 처리했습니다. SQL을 읽는 매퍼와 한 요청 안에서 조회를 줄이는 캐시였습니다.&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>구분할 곳&lt;/th>
 &lt;th>항목이 없음&lt;/th>
 &lt;th>항목이 있고 값이 비어 있음&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>DB 조회&lt;/td>
 &lt;td>지울 세션이 없습니다.&lt;/td>
 &lt;td>세션은 지울 수 있고 녹음 파일은 없습니다.&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>요청 안의 캐시&lt;/td>
 &lt;td>악보 위치를 아직 조회하지 않았습니다.&lt;/td>
 &lt;td>이미 조회했고 현재 악보에서 표시할 위치가 없습니다.&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="녹음-파일이-없어도-지울-기록은-있다">녹음 없이도 남는 삭제 대상&lt;/h2>
&lt;p>삭제 API는 먼저 사용자가 지울 수 있는 기록인지 찾고, 녹음 경로가 있으면 파일 삭제도 처리합니다. 이때 &lt;code>recording_url&lt;/code>만 조회하면 MyBatis의 기본 설정에서는 조회한 열이 전부 NULL인 행이 &lt;code>null&lt;/code>로 반환됩니다. 결과만 보면 행이 없었는지, 행은 있는데 녹음 경로만 비었는지 구분할 수 없습니다.&lt;/p></description></item><item><title>상태 조회가 대기 녹음을 지울 수 있던 이유</title><link>https://team-edo.github.io/posts/2026/10/keep-pending-recordings-until-upload/</link><pubDate>Thu, 29 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/keep-pending-recordings-until-upload/</guid><description>&lt;p>연주를 끝낸 직후 연결이 끊기면, 아직 서버에 도착하지 않은 녹음을 다시 보낼 수 있어야 합니다. 얼쑤에서는 녹음 파일을 기기의 보관 위치로 옮긴 다음, 어느 세션에 어떤 파일을 보낼지 기록하고 업로드하도록 만들었습니다. 앱을 다시 열어도 그 기록에서 보낼 녹음을 찾을 수 있습니다.&lt;/p>
&lt;p>리뷰에서 파일을 옮기는 일과 대기 기록을 저장하는 일 사이에 틈이 있다는 걸 발견했습니다. 새 파일은 이미 보관 위치에 있지만, 상태 조회에서 읽을 대기 기록은 아직 없습니다. 기존 조회 함수는 기록이 없으면 보관 폴더에 남은 파일을 정리했습니다. 이 순간에 조회가 끼어들면 방금 옮긴 녹음도 버려도 되는 파일로 보고 지울 수 있었습니다.&lt;/p></description></item><item><title>업무 결과를 HTTP 응답으로 바꾸기</title><link>https://team-edo.github.io/posts/2026/10/map-use-case-results-to-http/</link><pubDate>Thu, 29 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/map-use-case-results-to-http/</guid><description>&lt;p>연주 평가를 조회하는 요청은 평가가 있는지, 다른 회원의 세션인지, 아직 분석 중인지에 따라 답이 달라집니다. 분석을 다시 요청할 때도 아직 작업을 시작할 수 없는 경우가 여러 가지입니다. 두 기능에서 예상되는 업무 결과를 이름 있는 값으로 표현하고, HTTP 상태와 응답 본문은 컨트롤러에서 정했습니다.&lt;/p>
&lt;h2 id="평가-조회는-누가-무엇을-판단하는가">평가 조회의 판단 경계&lt;/h2>
&lt;p>컨트롤러는 인증을 마친 토큰에서 회원 ID를 읽어 세션 ID와 함께 넘깁니다. 유스케이스는 조회 계약을 통해 소유한 세션을 찾고, 완료된 세션에서만 평가와 음표 결과를 읽습니다. 세션과 음표 결과는 같은 읽기 전용 트랜잭션에서 조회합니다. 그 계약을 구현한 MyBatis 어댑터는 조회 행을 업무 코드가 읽을 값으로 바꿉니다.&lt;/p></description></item><item><title>96.7 BPM이 96으로 바뀐 이유</title><link>https://team-edo.github.io/posts/2026/10/strict-json-input-types/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/strict-json-input-types/</guid><description>&lt;p>연습 세션을 시작할 때 앱은 곡과 템포를 서버에 보냅니다. 템포는 JSON 정수만 받기로 했는데, 리뷰 중 &lt;code>&amp;quot;bpm&amp;quot;: 96.7&lt;/code>을 보냈더니 서버는 &lt;strong>96 BPM으로 처리하고 HTTP 201&lt;/strong>을 돌려줬습니다. 숫자가 허용 범위에 드는지는 검사했지만, 그 전에 소수점이 사라졌습니다.&lt;/p>
&lt;h2 id="검증기에-도착한-값은-이미-정수였다">검증 전에 정수로 바뀐 BPM&lt;/h2>
&lt;p>당시 프로젝트의 Jackson 3.1.5 설정으로 입력 모양을 바꿔 봤습니다. 정수 칸에 보낸 문자열 &lt;code>&amp;quot;96&amp;quot;&lt;/code>도 96이 됐고, 문자열이어야 하는 곡 ID에는 숫자 7을 보내도 &lt;code>&amp;quot;7&amp;quot;&lt;/code>이 됐습니다. 불리언 칸에 보낸 1은 &lt;code>true&lt;/code>가 됐습니다.&lt;/p></description></item><item><title>HTTP 테스트에서만 수신 시각 고정하기</title><link>https://team-edo.github.io/posts/2026/10/scope-fixed-clock-to-http-test/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/scope-fixed-clock-to-http-test/</guid><description>&lt;p>악보 화면을 본 시간을 서버에 보낼 때, 서버는 앱이 보낸 시간과 실제로 요청을 받은 시각을 대조합니다. 화면을 본 시간이 수신 시각에서 시작 시각을 뺀 값보다 지나치게 길면 저장하지 않습니다. 이런 API를 고정된 날짜로 테스트하면, 테스트를 실행하는 날짜에 따라 같은 요청이 허용되거나 거절될 수 있습니다.&lt;/p>
&lt;h2 id="실제-http-테스트에만-수신-시계를-고정했다">테스트 설정에 고정 &lt;code>Clock&lt;/code> 주입&lt;/h2>
&lt;p>체류 기록 테스트는 애플리케이션을 띄우고 실제 HTTP로 요청합니다. 서비스만 직접 호출하는 테스트처럼 생성자에 시계를 건네는 것으로는 서버의 수신 시각을 고정할 수 없습니다. 그래서 이 테스트가 가져오는 &lt;code>@TestConfiguration&lt;/code>에 고정된 &lt;code>Clock&lt;/code>을 만들고 &lt;code>@Primary&lt;/code>를 붙였습니다. 이 설정을 가져온 테스트에서는 같은 타입을 주입할 때 고정 시계를 우선합니다.&lt;/p></description></item><item><title>녹음 정보 한 칸이 비어도 CHECK가 통과한 이유</title><link>https://team-edo.github.io/posts/2026/10/mysql-check-unknown-recording-metadata/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/mysql-check-unknown-recording-metadata/</guid><description>&lt;p>연습 세션은 녹음이 올라오기 전에 먼저 만들어집니다. 그래서 파일 크기, 길이, 샘플레이트, 채널 수는 처음에는 모두 비어 있을 수 있습니다. 업로드를 마친 뒤에는 네 값이 모두 양수여야 합니다. 이 두 가지 상태만 허용하도록 MySQL에 &lt;code>CHECK&lt;/code>를 추가했습니다.&lt;/p>
&lt;h2 id="양수-비교는-null을-거짓으로-만들지-않는다">NULL과 양수 비교가 만든 UNKNOWN&lt;/h2>
&lt;p>처음 식은 네 칸이 모두 비었거나, 네 칸 모두 0보다 크면 허용하는 모양이었습니다. 그런데 채널 수만 NULL로 바꾼 테스트 행이 저장됐습니다. 네 칸 모두 비었다는 조건은 거짓이었지만, &lt;code>NULL &amp;gt; 0&lt;/code>은 거짓 대신 SQL의 &lt;code>UNKNOWN&lt;/code>이 됩니다. 그 결과 식 전체도 &lt;code>UNKNOWN&lt;/code>이었고, MySQL의 &lt;code>CHECK&lt;/code>는 평가가 거짓일 때 위반으로 처리합니다.&lt;/p></description></item><item><title>분석 제출에 실패하면 이미 커밋한 시도는?</title><link>https://team-edo.github.io/posts/2026/10/compensate-failed-analysis-handoff/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/compensate-failed-analysis-handoff/</guid><description>&lt;p>얼쑤에서 같은 녹음의 분석을 다시 요청하면 세션을 &lt;code>PENDING&lt;/code>으로 바꾸고 시도 번호를 올립니다. 실행기는 커밋 뒤에 작업을 받습니다. 상태가 반영되기 전 작업이 시작되면 새 시도를 읽을 수 없기 때문입니다.&lt;/p>
&lt;p>커밋과 작업 제출 사이에 실패할 수 있는 지점이 남습니다. 실행기의 대기열이 가득 차면 작업을 넘길 수 없지만, DB에는 이미 진행 중인 시도가 기록돼 있습니다. 이때 호출자에게 제출 실패만 돌려주면 상태를 읽는 쪽에는 아직 시작하지 못한 작업이 진행 중인 것처럼 보입니다.&lt;/p></description></item><item><title>서비스 검사를 꺼도 통과한 테스트 8개</title><link>https://team-edo.github.io/posts/2026/10/fake-repository-masks-service-check/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/fake-repository-masks-service-check/</guid><description>&lt;p>얼쑤에서 분석을 다시 시작할 수 있는 경우는 기존 녹음을 유지한 서버 오류나 시간 초과입니다. 다른 상태에서 요청이 들어오면 서비스는 먼저 거절합니다. 그 검사를 통과한 뒤에도 DB 갱신은 조건부로 실행합니다. 서비스가 상태를 읽은 뒤 다른 요청이 그 행을 바꿀 수 있기 때문입니다.&lt;/p>
&lt;p>단위 테스트의 가짜 저장소도 실제 DB의 조건을 비슷하게 구현했습니다. 서비스의 재시도 가능 여부 검사를 일부러 항상 참으로 바꿔 봤는데, 당시 테스트 &lt;strong>여덟 개&lt;/strong>가 모두 통과했습니다. 서비스는 거절했어야 할 요청을 저장소에 넘겼지만, 가짜 저장소가 대신 거절해서 바깥에서 본 결과와 행 상태는 같았습니다.&lt;/p></description></item><item><title>지난달과 연속 연습일 계산하기</title><link>https://team-edo.github.io/posts/2026/10/practice-calendar-previous-month-and-streak/</link><pubDate>Wed, 28 Oct 2026 00:00:00 +0900</pubDate><guid>https://team-edo.github.io/posts/2026/10/practice-calendar-previous-month-and-streak/</guid><description>&lt;p>얼쑤의 성장 화면에는 선택한 기간을 이전 기간과 비교한 수치와 현재 연속 연습일이 함께 나옵니다. 두 숫자 모두 날짜로 계산하지만 기준이 다릅니다. 비교 기간은 사용자가 고른 구간에 따라 바뀝니다. 연속 연습일은 과거 달을 보고 있어도 오늘을 기준으로 유지하기로 했습니다.&lt;/p>
&lt;p>초안에서는 서버가 주간·월간 구간을 정하려고 했습니다. 팀의 API 계약을 대조한 뒤 화면에서 고른 시작일과 종료일을 앱이 보내고, 서버가 그 범위의 요약과 날짜별 집계를 반환하도록 맞췄습니다. 인정 시간 &lt;strong>1분 이상&lt;/strong>을 연습한 날로 보는 규칙도 합의를 거쳐 계약에 반영했습니다. 이때 정한 시작일·종료일 계약을 기준으로 비교 기간과 연속 일수의 계산을 나눴습니다.&lt;/p></description></item></channel></rss>