학습 단계의 설명과 채점에 쓸 악보는 DB에 JSON으로 저장합니다. 학습 단계에서 잘못된 항목을 건너뛰면 화면에 설명이 빠진 채 나타납니다. 악보에서 정수를 잘못 읽으면 음표의 길이나 시작 박을 다르게 해석할 수 있습니다. 두 파서에서 저장값을 읽는 지점에 입력 계약을 드러냈습니다.

배열 원소를 꺼내 변환

학습 단계에는 할 일과 운지 설명처럼 배열로 저장하는 항목이 있습니다. Jackson 3의 JsonNode.map은 노드 자체에 함수를 적용합니다. 배열의 각 원소를 변환하려면 노드가 배열인지 확인하고 values()로 원소를 꺼낸 뒤 Kotlin의 map을 적용합니다.

node.get(field)
    ?.takeIf { it.isArray }
    ?.values()
    ?.map(readItem)
    ?: throw LessonContentException(lessonId, "expected array")

할 일이 없는 단계의 빈 배열은 그대로 읽고, 필수 배열이 빠졌거나 null이면 오류로 처리합니다. 통과 조건처럼 계약상 선택인 객체에는 누락과 null을 허용했습니다. 이 구분이 있어야 원래 내용이 없는 단계와 시드에 잘못 들어간 단계를 가려낼 수 있습니다.

누락된 정수 필드 검증

악보 파서에서 path()로 없는 필드를 읽으면 누락을 나타내는 노드가 돌아옵니다. 이 노드에 asInt()를 바로 호출하면 0이 나올 수 있습니다. 시작 틱 0은 실제로 유효한 값이라, 필드가 빠진 경우와 같게 처리하면 오류를 놓칩니다. 그래서 시작 틱·길이처럼 정수인 필드는 isInt를 확인한 다음 값을 읽고, 이벤트 목록은 isArray로 검사했습니다.

DB에서 악보와 율명 정보를 가져오는 코드와 변환 코드를 분리했습니다. 변환에는 악보 JSON, 연습 범위, 율명 맵만 전달합니다. 단위 테스트는 작은 악보에서 쉼표 뒤 음의 시작 박과 원래 번호가 유지되는지 확인했고, 등록되지 않은 율명이나 읽을 수 없는 악보는 거절했습니다.

학습 단계 파서의 단위 테스트는 정상 입력의 할 일 2개와 선택 항목을 확인했습니다. 필수 문구 누락, 배열 대신 null, 문자열 목록에 들어간 숫자, JSON이 아닌 문자열 4종은 예외가 나야 통과합니다. 이 검사는 두 파서에 입력한 JSON과 변환 결과를 확인했습니다. 운영 DB에 저장된 모든 악보와 학습 단계를 읽어 본 결과는 아닙니다.