QR코드 판독값을 그대로 사용해서는 안 된다 ── 오류 정정이 통과해도 값은 보장되지 않는다

· · QR코드, 바코드, 오류 정정, 입력 검증, 데이터 품질, 업무 시스템, C#, 설계, 현장 운용

창고 검품 단말기가 ‘삐’하고 울립니다. 전표의 QR코드가 읽혔다는 신호입니다. 읽힌 문자열은 그대로 재고 시스템으로 전달되고, 전표가 인계되어 출하 지시가 확정됩니다. QR코드에는 오류 정정이 있으니 다소 손상되어 있어도 올바른 값이 반환된다 ── 이런 전제로 만들어진 시스템은 결코 드물지 않습니다.

앞부분은 맞는 이야기입니다. QR코드는 오염이나 손상이 있어도 데이터를 복원할 수 있도록 설계되어 있으며, 오류 정정 레벨 L부터 H까지 부호어의 약 7%에서 약 30%까지 복원할 수 있습니다.1 문제는 뒷부분, 즉 ‘그러므로 반환된 값은 올바르다’는 추론 쪽입니다.

이 글에서는 실제로 생성한 QR코드 샘플 이미지와 두 종류의 디코더를 이용한 실측을 바탕으로, 오류 정정이 통과해도 값이 보장되지 않는 이유와 업무 시스템 측에서 무엇을 검증해야 하는지를 정리합니다. 이 글에 실린 QR코드는 모두 실물입니다. 읽을 수 있는 것은 손에 있는 QR 리더로 그대로 확인할 수 있습니다(‘읽을 수 없음’을 보여주기 위한 견본도 포함되어 있으며, 해당 부분에서는 명시합니다). 브라우저에서 jsQR과 OpenCV.js를 전환하며 시험해 볼 수 있는 QR코드 읽기 비교 도구도 준비했습니다. 이 글의 실측은 그쪽에서 재현할 수 있습니다.

1. 먼저 결론

  • 오류 정정은 ‘복원’이지 ‘검증’이 아닙니다. 규격 스스로가, 손상된 모듈은 ‘명백히 타당하지만 다른 부호어로 오복호한다’고 적고 있습니다.2
  • 무작위 손상이라면 대부분 ‘읽을 수 없음’으로 귀결됩니다. 실측 9,700회에서 잘못된 값이 반환된 사례는 0건이었습니다.
  • 그러나 손상이 한곳에 몰리면 확실하게 다른 값이 됩니다. 26개의 부호어 중 7개만 깨져도 004873104873으로 읽히며, 독립된 두 디코더가 나란히 같은 오류를 반환했습니다.
  • 오류 정정의 바깥쪽에는 더 단순한 함정이 있습니다. 분할 QR, 문자 인코딩, 화면 안의 다른 코드. 모두 오류가 나지 않은 채로 일어날 수 있습니다. 디코더에 따라서는 경고나 예외가 나오기도 하지만, 나오는 방식은 구현에 따라 다르며 나오지 않는 쪽으로 귀결되는 조합이 실제로 존재합니다.
  • 그러므로 읽힌 값은 미검증 입력으로 다룹니다. 형식 검사 → 체크 디지트 → 업무 검증, 이 3단계로 받아들이는 것이 기본형입니다.

2. 샘플 ── 거의 똑같아 보이는 QR코드 두 장

먼저 실물을 봐 주십시오. 다음 두 장은 어느 쪽이든 오류 없이 읽힙니다.

위는 정상적인 QR코드, 아래는 세로로 한 줄 띠 모양의 손상이 들어간 QR코드. 겉보기에는 거의 같음

시험해 보기 전에 한마디: iOS 표준 카메라에서는 반응하지 않는 경우가 있습니다. 읽히지 않으면 QR 리더 앱으로 시도해 보십시오(이유는 바로 아래 참고에 적었습니다).

  • A(위)NO:20260725-004873
  • B(아래)NO:20260725-104873

실물이므로 손에 있는 리더로 그대로 시험해 볼 수 있습니다. 이 전표 번호는 ‘수주일 8자리 + 일련번호 5자리 + 체크 디지트 1자리’라는 체계이므로, 바뀐 것은 일련번호 부분 ── 0048710487이 되어 1만 건 어긋난 다른 전표를 가리키게 됩니다.

iOS 표준 카메라에서는 반응하지 않는 경우가 있습니다. 표준 카메라는 URL처럼 ‘열기’ 동작이 가능한 내용을 우선하도록 만들어져 있어서, 이 글처럼 문자열만 있는 QR에서는 아무것도 나오지 않는 경우가 있습니다. QR 리더 앱을 사용하면 읽을 수 있습니다. 같은 이미지인데도 읽는 쪽의 구현에 따라 동작이 달라진다 ── 이 글의 주제 그 자체를 첫 번째 샘플에서부터 체험하게 되는 셈입니다.

B가 A와 다른 부분은 왼쪽에서 10~14번째 열, 세로 한 줄의 띠 안에 있는 31개 모듈뿐입니다. 부호어 영역 208모듈 중 15%에 해당합니다.

B 중 A에서 바뀐 31개 모듈을 빨간 테두리로 표시한 그림. 세로 방향으로 가늘고 길게 분포함

그리고 중요한 것은 디코더가 양쪽 모두에 대해 오류를 반환하지 않는다는 점입니다. ‘정정했습니다’라는 알림조차 없습니다. 애플리케이션 입장에서는 둘 다 똑같이 성공한 읽기입니다.

한 가지 미리 밝혀 두면, 이 손상은 의도적으로 구성한 것이며, 우연히 이렇게 될 확률은 높지 않습니다. 만드는 방법과 실제 오염과의 거리는 4절에서 다룹니다.

3. 왜 일어나는가 ── 오류 정정의 내부

QR코드의 오류 정정은 리드-솔로몬 부호이며, 부호어(8비트 단위) 단위로 작동합니다. 이번에 사용한 버전 1・오류 정정 레벨 M(21×21모듈)의 내역은 다음과 같습니다.2

총 부호어 수 데이터 부호어 오류 정정 부호어 규격상 정정 능력
26 16 10 4부호어

눈길을 끄는 것은 마지막 열입니다. 오류 정정 부호어가 10개 있으면 리드-솔로몬 부호로서는 5개까지 정정할 수 있습니다. 그런데 규격이 정하는 정정 능력은 4입니다. 차이인 1개분은 오독 방지용 부호어 p로서 의도적으로 남겨 둔 것입니다(버전 1-M에서는 p = 2). 규격의 표 13에도 각주로 ‘오독 확률을 낮추기 위해 정정 능력을 오류 정정 부호어 수의 절반 미만으로 하고 있다’고 명시되어 있습니다.2

규격 자체가 ‘오류 정정은 잘못된 값을 낼 수 있다’는 전제로 설계되어 있는 셈입니다. 같은 절에는 이렇게도 쓰여 있습니다.

QR코드는 매트릭스 심볼이므로, 모듈을 어두운색에서 밝은색으로(또는 그 반대로) 바꾸는 결함은 해당 심볼 캐릭터가 명백히 타당하지만 다른 부호어로 오복호하는 결과를 초래한다.2

이유는 정정의 원리 자체에 있습니다. 리드-솔로몬 복호가 하는 일은 받은 패턴으로부터 일정한 거리(정정 능력) 안에 부호어가 있는지 찾는 것입니다. 발견되면 그것을 답으로 반환하고, 발견되지 않으면 ‘읽을 수 없음’으로 끝납니다. 아무리 손상되어도 가장 가까운 부호어를 찾아내는 동작이 아닙니다.

이러한 성질 때문에 결과는 둘로 나뉩니다. 무작위 손상은 어느 부호어에서도 먼 곳으로 흩어지므로 대체로 ‘찾을 수 없음 = 읽을 수 없음’으로 귀결됩니다. 위험한 것은 손상이 우연히 다른 부호어의 근방으로 들어갔을 때입니다. 그때 디코더는 그 부호어야말로 정답이라고 판단해 반환합니다. 반환된 부호어는 그 자체로 완전히 정합적이므로 오류라는 사실을 알 수 없습니다.

4. 어디까지 정정되고, 어디부터 위험한가

여기서부터가 실측입니다. 결론부터 말하면 위험한 것은 손상의 ‘양’이 아니라 ‘위치’였습니다.

수치를 보기 전에 이 장의 실험 조건을 정리해 둡니다. 직접 재현 실험을 할 경우의 전제입니다.

항목 내용
대상 심볼 버전 1-M / 21×21(26부호어 = 데이터 16 + 오류 정정 10)
생성 segno 1.6.6 / Python 3.11
디코더 1 OpenCV 5.0.0 cv2.QRCodeDetector(Python 3.11)
디코더 2 jsQR 1.4.0(Node.js 22)
손상을 주는 방법(4.1절) 부호어 영역 208모듈에서 무작위로 골라 반전 / 부호어 단위로 무작위 파괴 / 심볼 전체에 균일한 블러・노이즈・대비 저하
손상을 주는 방법(4.2절) 목표값 B로 근접시키는 부호어 조합을 전수 시행
시행 횟수 모듈 반전 3,900회(각 수준 300회), 부호어 파괴 1,800회(각 수준 200회) + 정정 능력 초과 추가 시행 4,000회, 화질 저하 1,000회(각 조건 200회). 4.2절은 792가지・495가지 전수
판정 디코더의 반환값을, 기대값과 일치하면 ‘정확히 읽힘’, 값을 얻지 못하면 ‘읽을 수 없음’, 비어 있지 않은 다른 값이 반환되면 ‘잘못된 값’으로 분류

5절의 샘플을 포함한 이 글 전체의 환경은 글 말미의 ‘검증 환경’에 정리해 두었습니다.

4.1. 무작위 손상은 ‘읽을 수 없음’으로 귀결된다

버전 1-M 심볼에 대해 부호어 영역 208모듈 중 몇 개를 무작위로 반전시켜 결과를 분류했습니다(각 수준 300회, 총 3,900회).

6모듈 반전과 9모듈 반전 QR코드의 비교. 위는 읽히고 아래는 읽히지 않음

위는 6개, 아래는 9개를 반전시킨 것입니다. 위는 정확히 읽히고, 아래는 전혀 읽히지 않습니다. 3모듈의 차이는 사람의 눈으로는 거의 알아볼 수 없습니다. 경계는 겉모습에 나타나지 않는 곳에 있습니다.

반전시킨 모듈 수 정확히 읽힘 읽을 수 없음 잘못된 값
0~5 1,799 1 0
6 131 169 0
7 32 268 0
8 11 289 0
9~12 0 1,200 0

읽히는지 여부는 5개에서 6개 사이에서 무너지고, 9개 이상은 전멸합니다. 그리고 잘못된 값은 단 한 건도 나오지 않았습니다. 다 정정하지 못하는 손상은 ‘읽을 수 없음’으로 귀결된다는 것이 솔직히 좋은 소식입니다. jsQR도 수치는 거의 같았습니다(0~5는 1,800건 전부 정답, 6 이상은 OpenCV와 1건 이내의 차이).

같은 현상을 부호어 단위로 보면 경계가 더 뚜렷해집니다(각 수준 200회, 총 1,800회).

파괴한 부호어 수 정확히 읽힘 읽을 수 없음 잘못된 값
0~5 1,194 6 0
6~8 0 600 0

5개 부호어까지 정정되고 있습니다. 앞 절에서 본 대로 규격상 정정 능력은 4이며, 나머지는 ‘정정하지 않고 검출만 하기’ 위한 여유분이었습니다. jsQR도 0~5의 1,200건 전부를 정확히 읽었으며, 두 구현 모두 그 여유분을 정정에 다 써 버리고 있습니다. 규격이 오독 대책으로 마련해 둔 여유는 구현 수준에서는 믿을 수 없습니다.

정정 능력을 명백히 넘는 파괴(6부호어와 8부호어)도 각각 2,000회씩 추가 시행했지만, 잘못된 값은 역시 0건으로 모두 ‘읽을 수 없음’으로 끝났습니다.

카메라에서 비롯되는 열화도 같은 경향입니다. 심볼 전체에 균일한 블러·노이즈·대비 저하를 적용한 결과입니다(각 200회, 총 1,000회).

블러 σ 노이즈 σ 대비 정확히 읽힘 읽을 수 없음 잘못된 값
0 0 1.00 200 0 0
1.5 10 0.90 183 17 0
3.0 20 0.70 1 199 0
4.5 30 0.50 0 200 0
6.0 40 0.35 0 200 0

결과는 ‘정확히 읽힌다’와 ‘읽을 수 없다’ 둘 중 하나이며, 그 중간은 없습니다. 열화가 진행되면 정답률은 떨어지지만, 줄어든 만큼은 모두 읽기 실패로 바뀝니다.

다만 이것은 균일한 열화에 국한된 이야기입니다. 실제 손떨림에는 방향이 있고, 비스듬한 촬영이나 조명 얼룩은 이미지의 일부만 무너뜨립니다. 다음에 살펴보듯 위험한 것은 손상이 한쪽으로 쏠리는 것이므로, ‘화질 문제라면 오독하지 않는다’고 일반화하지 말아 주십시오.

4.2. 위치가 나쁘면 확실히 다른 값이 된다

2절의 샘플 B는 이 ‘치우친 손상’을 노려 구성한 것입니다.

NO:20260725-004873(A)와 NO:20260725-104873(B)의 26개 부호어를 나란히 놓으면 차이가 있는 곳은 12군데였습니다. 데이터 부호어 2개와, 그에 연동해 바뀐 오류 정정 부호어 10개입니다.

이 12군데 중 7군데를 B의 값에 맞추면, 완성된 패턴은 A로부터는 7부호어만큼 떨어지고 B로부터는 5부호어만큼의 거리에 오게 됩니다. 정정 능력이 5라면 디코더는 이것을 ‘B가 5군데 손상된 것’으로 해석해 B로 정정합니다.

조건 시도한 조합 B로 오독한 수
7부호어를 B로 맞춤(B와의 거리 5) 792가지 792가지(100%)
8부호어를 B로 맞춤(B와의 거리 4) 495가지 495가지(100%)

어느 조합에서든 예외 없이 오독했습니다. 아래 행은 B와의 거리가 4, 즉 규격이 정하는 정정 능력 안쪽입니다. 오독 방지용 부호어 p를 존중하는 구현이라 하더라도 손상이 1부호어만큼 늘어나면 같은 결과가 됩니다. p는 확률을 낮출 뿐 막지는 못합니다.

2절에 실은 B는 이 중에서 손상이 세로 한 줄로 모이는 조합(31모듈)입니다. 최소화하면 23모듈까지 줄일 수 있었습니다. OpenCV 5.0.0과 jsQR 1.4.0이라는 서로 무관한 두 구현이 모두 NO:20260725-104873을 반환합니다.

4.3. 이 차이를 어떻게 받아들일 것인가

솔직히 말하면 이 손상은 무작위로는 잘 일어나지 않는 것입니다. 무작위 손상으로는 9,700회를 시도해 오독 0건이었습니다. ‘당장 내일이라도 일어난다’는 이야기는 아닙니다.

그래도 무시할 수 없는 이유가 세 가지 있습니다.

  • 실제 손상은 무작위가 아닙니다. 접힘 자국은 직선으로 나고, 운반 중 마찰은 같은 변에 집중되며, 인쇄 헤드의 막힘은 세로줄이 됩니다. 이번의 띠 모양 손상은 이러한 ‘위치에 치우친 손상’의 한 예입니다. 다만 B는 흰색→검은색 17모듈, 검은색→흰색 14모듈로 양방향 변화를 포함하고 있어서, 잉크가 줄어드는 것만으로 생기는 불량으로는 재현할 수 없습니다. 양방향이 동시에 일어나는 경우는 접힘 자국의 그림자로 이진화 판정이 어긋난다거나, 오염과 마모가 겹친다거나, 위에서 다른 스티커가 부분적으로 붙는다거나 하는 상황입니다.
  • 스캔 횟수가 자릿수부터 다릅니다. 한 번당으로는 무시할 수 있는 확률이라도, 하루에 수만 번 읽는 현장이라면 이야기가 달라집니다. 게다가 오독은 오류를 내지 않으므로 기록에 남지 않고, 원인 불명의 재고 조사 차이로 처리되고 끝나 버립니다.
  • 의도적으로 만들 수 있다는 것은 다른 사람도 만들 수 있다는 뜻입니다. 이번 패턴은 노리는 값을 정해 놓고 기계적으로 구성한 것입니다. 가격표나 쿠폰처럼 값을 바꿔치기할 동기가 있는 QR에서는 이것이 공격 수법이 됩니다.

5. 오류 정정과 관계없는 ‘읽혔는데 다르다’

실무에서 더 자주 마주치는 것은 이쪽입니다. 오류 정정이 완벽하게 작동해도 일어나며, 확률의 문제조차 아닙니다.

5.1. 분할 QR의 첫 번째 조각만 읽기

QR코드에는 긴 데이터를 여러 개의 심볼로 나누어 읽는 쪽에서 연결하는 구조(Structured Append)가 있습니다. 다음은 NO:20260725-004873/LOT:AB-77/QTY:120/EXP:20270131을 3장으로 분할한 그 첫 번째 장입니다.

분할 QR 3장 중 첫 번째 장. 단독으로 읽으면 끝부분이 빠진 전표 번호가 반환됨

겉보기에는 평범한 QR코드로, 3장 중 1장이라는 단서는 없습니다. 이것을 단독으로 읽으면 OpenCV는 다음과 같이 반환합니다.

NO:20260725-00487

오류 없음, 경고 없음. 끝의 체크 디지트 3만 빠진, 완전히 그럴듯한 전표 번호입니다. 두 번째・세 번째 장은 각각 3/LOT:AB-77/QTY:, 120/EXP:20270131이 됩니다. 같은 이미지를 jsQR에 전달하면 빈 문자열이 반환되었습니다. 분할 QR을 상정하지 않은 앱이 우연히 첫 번째 장을 스캔했을 때 무슨 일이 일어나는지는 디코더에 따라 달라집니다.

업무에서는 이렇게 나타납니다. 전표 번호를 전방 일치나 LIKE로 검색하는 화면이라면, 끝자리 1자리가 빠진 NO:20260725-00487이 그대로 원래 전표에 매치되어 버려서, ‘읽혔고 전표도 나왔으니’ 아무도 이상을 알아차리지 못합니다. 자릿수를 고정으로 검사하지 않는 한 이 조각은 끝까지 정상적인 읽기로 흘러갑니다.

5.2. 문자 인코딩과 ECI

다음은 部品番号 東-004873을 Shift_JIS로, ECI 지정 없이 넣은 QR코드입니다.

Shift_JIS로 ECI 지정 없이 생성한 QR코드. 읽으면 문자가 깨짐

이것도 실물입니다. 손에 있는 리더로 읽으면 部品番号 東-004873이 나오거나, 문자가 깨진 문자열이 나오거나, 아무것도 반환되지 않거나 ── 사용 중인 리더가 어떤 해석을 하고 있는지 알 수 있습니다.

QR코드 읽기 비교 도구에 이 이미지를 읽히면, jsQR이 추출한 원본 바이트열과 이를 UTF-8 / Shift_JIS / EUC-JP 등으로 다시 해석한 결과가 나란히 표시됩니다. 같은 바이트열이 문자 인코딩에 따라 전혀 다른 것이 되는 모습을 그 자리에서 확인할 수 있습니다.

같은 내용을 문자 인코딩과 ECI 지정을 바꿔 가며 생성해, 두 디코더로 읽힌 결과가 이것입니다. 이 4가지 조건의 심볼은 모두 도구의 샘플에 넣어 두었습니다. 다만 표의 값은 Python의 cv2로 측정한 것이며, 3번째 행만 브라우저 버전과 결과가 다릅니다(이 표 바로 다음에 자세히 설명합니다). 도구에서 확인할 수 있는 것은 브라우저 버전의 동작이므로, 3번째 행의 ‘예외로 실패’를 재현하려면 Python의 cv2가 필요합니다.

생성 조건 OpenCV 5.0.0 jsQR 1.4.0
Shift_JIS / ECI 없음 문자가 깨진 문자열을 성공으로 반환 빈 문자열
UTF-8 / ECI 없음 部品番号 東-004873 部品番号 東-004873
Shift_JIS / ECI 있음 경고를 내고 복호 실패 빈 문자열
UTF-8 / ECI 있음 部品番号 東-004873 部品番号 東-004873

1번째 행이 최악입니다. OpenCV는 바이트열을 Latin-1로 해석해 \x95\x94\x95i...라는 깨진 문자열을 오류 없이 반환했습니다. 애플리케이션 입장에서는 정상적인 읽기이며, 그대로 데이터베이스에 들어가면 문자가 깨진 레코드가 1건 생기게 됩니다.

업무에서는 이렇게 나타납니다. 입고 실적의 품명란에 깨진 문자열이 들어가 등록되고, 다음 날 ‘그 품번으로 검색해도 실적이 나오지 않는다’는 문의로 이어집니다. 같은 라벨을 다시 읽어도 같은 값이 들어가므로, 현장은 ‘시스템 검색이 이상하다’고 보고하고, 원인이 읽는 쪽의 문자 인코딩이라는 것을 알게 될 때까지 왔다 갔다 하는 일이 이어집니다.

이것은 OpenCV의 버그가 아니라 현행 규격이 정하는 기본 해석(ISO/IEC 8859-1)대로의 동작입니다.3 규격에서 벗어난 것은 Shift_JIS를 ECI 없이 넣은 쪽입니다.

3번째 행도 놓칠 수 없습니다. ECI(문자 인코딩을 명시하는 구조)를 올바르게 지정했는데도, OpenCV는 QR: ECI is not supported properly라는 경고를 내고 반환값을 UTF-8로 해석하지 못한 채 예외로 실패했습니다. 규격에 충실하게 만든 QR 쪽이 오히려 읽히지 않는다는 역전이 실제로 일어납니다.

게다가 이 3번째 행은 같은 OpenCV라도 언어 바인딩에 따라 결과가 달라집니다. 위 표는 Python의 cv2에서의 결과이지만, 브라우저 버전(opencv.js 5.0.0)에 같은 이미지를 전달하면 예외는 발생하지 않고 ���i��� ��-004873이라는 대체 문자투성이 문자열을 성공으로 반환합니다. Emscripten이 std::string을 UTF-8로 변환할 때 잘못된 바이트를 예외가 아니라 U+FFFD로 처리하기 때문입니다. 버전도 이미지도 같은데 달라진 것은 호출하는 쪽의 언어뿐입니다. 예외로 알아차릴 수 있는 Python 쪽이 그나마 나은 편이고, 브라우저 버전은 ‘읽혔다’고 말하면서 깨진 값을 반환합니다. 도구의 샘플에서 실제로 확인할 수 있습니다.

5.3. 화면 안에 여러 개의 QR이 있는 경우

전표에 여러 개의 QR이 인쇄되어 있다, 옆 상자의 라벨이 시야에 들어온다 ── 흔히 있는 상황입니다. 3개의 QR을 가로로 나란히 놓고 시험했습니다.

3개의 QR코드를 가로로 나란히 놓은 이미지. 왼쪽부터 NO:, ITEM:, LOT: 순서

먼저, 이 이미지에서는 OpenCV의 단일 읽기 API가 아무것도 반환하지 않았습니다. 다만 이것이 ‘여러 개가 있으면 걸러 준다’는 보장은 아닙니다. 단일 읽기 API는 하나의 QR을 검출해 복호하는 것이라고 문서화되어 있을 뿐, 여러 장을 거부한다고 쓰여 있지는 않으며, 배치에 따라서는 그중 하나를 반환할 수도 있습니다. 여러 코드의 검출을 단일 읽기 API의 반환값 유무로 대체하지 마십시오.

그렇다면 다중 읽기 API라면 어떨까 ── 여기가 까다로웠습니다.

위 그림(왼쪽부터 NO: / ITEM: / LOT:)을 그대로 사용해 화소 수만 바꿔서 읽힌 결과입니다. 실제 스캐너에서 대상과의 거리나 카메라 해상도가 달라지는 상황에 해당합니다.

이미지 너비 반환 순서
1,001px NO: / LOT: / ITEM:
1,502px 아무것도 반환되지 않음
2,002px NO: / LOT: / ITEM:
3,003px NO: / ITEM: / LOT:
4,004px LOT: / NO: / ITEM:

같은 이미지인데도 해상도만 바뀌면 순서가 달라집니다. 왼쪽부터 순서도 아니고, 큰 순서도 아닙니다. 읽히지 않는 해상도조차 있습니다. 순서는 검출 알고리즘의 내부 사정으로 정해지는 것이라서, 규정되어 있지 않은 이상 이렇게 됩니다.

즉 ‘첫 번째가 전표 번호일 것’이라며 인덱스 0을 사용하는 코드는, 오늘은 우연히 동작하고 있어도 내일 카메라를 가까이 대는 것만으로 다른 코드를 잡게 됩니다. 재현하기 어렵고 원인도 추적하기 어려운 종류의 결함입니다.

업무에서는 이렇게 나타납니다. 검품대 위에서 목적하는 전표와 옆 상자의 라벨이 동시에 시야에 들어옵니다. 인덱스 0을 채택한 앱은 옆 전표를 인계해, 출하 지시가 그쪽으로 확정됩니다. 작업자는 올바른 라벨에 카메라를 향했다고 생각하므로, 출하 후 ‘다른 상품이 도착했다’는 말을 듣기 전까지는 아무도 알아차리지 못합니다.

받는 쪽은 순서가 아니라 내용으로 선별할 필요가 있습니다. 다중 읽기 API로 전부 수집하고, 접두사와 형식이 일치하는 것만 채택하며, 해당하는 것이 0개 또는 2개 이상이면 오류로 처리한다 ── 이것이 안전한 작성 방식입니다.

5.4. 애초에 내용이 올바르다는 보장은 없다

이미지 처리로는 원리적으로 검출할 수 없는 층이 있습니다. 인쇄 원본 데이터가 잘못되어 있다, 교체 전의 오래된 라벨이 상자에 남아 있다, 다른 거래처의 라벨이 섞여 있다, 라벨이 복사되어 있다.

QR코드는 ‘그곳에 무엇이 쓰여 있는가’만 알려 줍니다. ‘그것이 올바른가’, ‘자사가 발행한 것인가’는 받는 쪽이 확인할 수밖에 없습니다.

업무에서는 이렇게 나타납니다. 재사용한 상자의 옆면에 이전 라벨이 남아 있어서, 윗면의 새 라벨이 아니라 그쪽을 읽습니다. 값 자체는 완전히 올바른 QR이므로 형식 검사도 체크 디지트도 마스터 대조도 전부 통과해, 지난번 출하처로 화물이 향하게 됩니다. 이 층만큼은 읽은 값에 대한 검사로는 전혀 잡아낼 수 없습니다.

6. 받은 값을 어떻게 다룰 것인가

대책은 오류 정정의 바깥쪽에 자체 검증을 쌓는 것으로 귀결됩니다.

단계 검증 내용 잡아낼 수 있는 것
1. 형식 검사 길이・문자 종류・구분자・접두사의 완전 일치 다른 코드의 읽기, 분할 QR의 조각, 문자 깨짐
2. 자기 검증 체크 디지트 한 글자의 변화는 확실히 검출. 여러 글자는 놓치는 경우 있음
3. 업무 검증 마스터 대조와 지금 처리해야 할 대상과 일치하는가 오래된 라벨, 타사 라벨, 다른 전표와의 혼동

샘플 전표 번호는 NO: + 수주일 8자리 + 일련번호 5자리 + 체크 디지트 1자리이며, 끝자리는 GS1과 같은 모듈러스 10・가중치 3 방식입니다. 2절의 오독 NO:20260725-104873은 여기서 멈춥니다. 2026072510487의 올바른 체크 디지트는 0이며, 라벨상의 3과 일치하지 않기 때문입니다.

코드를 작성하기 전에 문자 인코딩의 전제를 먼저 정해 두십시오. 아래 구현은 전표 번호가 ASCII 숫자만으로 구성된다는 것을 전제로, 체크 디지트를 ‘문자 − '0'‘으로 계산하고 있습니다. 전각 숫자나 5.2절에서 본 문자가 깨진 바이트열이 이 계산까지 도달하면 결과는 의미가 없어집니다. 그래서 형식 검사의 정규 표현식은 \d가 아니라 [0-9]로 작성해, ASCII 이외의 숫자가 체크 디지트 계산에 도달하지 않도록 순서를 만들어 두었습니다. 전제를 정하는 것이 먼저, 그것을 형식 검사로 담보하는 것이 다음, 계산은 그 뒤입니다. 이 순서가 무너지면 이후 단계의 검사는 전부 헛돌게 됩니다.

using System.Linq;
using System.Text.RegularExpressions;

public sealed record ScanOutcome(bool Accepted, string? SlipNo, string Reason);

public static class SlipScanValidator
{
    // NO: + 수주일 8자리 + '-' + 일련번호 5자리 + 체크 디지트 1자리
    // 종료는 $가 아니라 \z. .NET의 $는 끝의 개행 직전에도 매치되므로,
    // "NO:20260725-004873\n"을 통과시켜 버린다.
    // 숫자는 \d가 아니라 [0-9]. .NET의 \d는 전각 숫자 등 유니코드의 숫자
    // 전반에 매치되지만, 후단의 체크 디지트 계산은 ASCII를 전제로 한다
    private static readonly Regex Format =
        new(@"\ANO:(?<date>[0-9]{8})-(?<seq>[0-9]{5})(?<cd>[0-9])\z", RegexOptions.Compiled);

    public static ScanOutcome Validate(string? raw, ISlipRepository repo)
    {
        // 1. '읽지 못함'을 '빈 성공'으로 처리하지 않는다.
        //    디코더가 실패를 null / 빈 문자열 / 예외 중 무엇으로 반환할지는 구현마다 다르다
        if (string.IsNullOrEmpty(raw))
            return new(false, null, "읽을 수 없었습니다. 다시 스캔해 주세요");

        // 2. 형식 검사. 전방 일치가 아니라 길이까지 포함한 완전 일치로 본다.
        //    분할 QR의 조각 "NO:20260725-00487"은 여기서 걸러진다
        var m = Format.Match(raw);
        if (!m.Success)
            return new(false, null, $"전표 QR 형식이 아닙니다 ({Describe(raw)})");

        // 3. 자기 검증. 오정정으로 숫자가 깨져 있으면 여기서 잡힌다
        var body = m.Groups["date"].Value + m.Groups["seq"].Value;
        if (Modulus10Weight3(body) != m.Groups["cd"].Value[0] - '0')
            return new(false, null, "체크 디지트가 일치하지 않습니다. 라벨의 오염을 확인해 주세요");

        // 4. 업무 검증. 실재하는지, 그리고 지금 처리해도 되는 상태인지
        var slip = repo.Find(raw);
        if (slip is null)
            return new(false, null, "해당하는 전표가 없습니다");
        if (slip.Status != SlipStatus.WaitingForShipment)
            return new(false, null, $"이 전표는 「{slip.Status}」입니다. 출하 대상이 아닙니다");

        return new(true, raw, "OK");
    }

    // GS1과 같은 모듈러스 10・가중치 3. 오른쪽 끝부터 3,1,3,1... 의 가중치를 곱한다
    private static int Modulus10Weight3(string body)
    {
        var sum = 0;
        for (var i = 0; i < body.Length; i++)
        {
            var weight = (body.Length - i) % 2 == 1 ? 3 : 1;
            sum += (body[i] - '0') * weight;
        }
        return (10 - sum % 10) % 10;
    }

    // 읽은 값은 외부 입력이다. 화면이나 로그에 출력하기 전에 인쇄 가능한 ASCII만 남긴다.
    // char.IsControl은 유니코드의 Control(Cc)만 걸러내고, U+202E(오른쪽에서 왼쪽
    // 쓰기로 덮어쓰기) 등의 Format(Cf)은 통과시켜 버린다. 제외 목록이 아니라 허용 목록으로 작성한다
    private static string Describe(string raw)
    {
        var kept = raw.Where(c => c >= ' ' && c <= '~').Take(40).ToArray();
        if (kept.Length == 0) return "(표시할 수 없는 문자열)";
        var safe = new string(kept);
        return kept.Length < raw.Length ? safe + "…(일부 문자를 제거)" : safe;
    }
}

체크 디지트 설계 자체는 업무 시스템의 코드 설계와 체크 디지트에서 산식과 선택법을 정리해 두었습니다. 이 글의 맥락에서 그 검사가 지니는 가치는, 사람의 오타뿐 아니라 기계의 오독에도 효과가 있다는 점에 있습니다.

이 3단계로 지킬 수 없는 것

3단계를 다 갖추어도 빠져나가는 구멍이 3가지 있습니다. 어느 것이든 ‘검증 설계를 한 단계 더 깊게 하는’ 것으로 막을 수 있으므로, 함께 짚어 두시기 바랍니다.

체크 디지트는 여러 글자의 변화를 놓칩니다. 모듈러스 10・가중치 3이 확실히 잡아내는 것은 한 글자의 오류입니다. 실제로 20260725004872026072517487은 둘 다 체크 디지트가 3이 되기 때문에, NO:20260725-174873은 2단계를 그대로 통과합니다. 오정정이 바꾸는 것이 반드시 한 글자만은 아니므로 3단계를 생략할 수 없습니다.

마스터 대조를 ‘존재 확인’으로 끝내지 마십시오. 위 코드의 repo.Find()와 상태 확인은 ‘사용 가능한 전표가 어딘가에 존재한다’는 것만 확인하고 있습니다. 작업자가 옆 상자의 라벨을 읽은 경우, 그 라벨도 형식・체크 디지트・출하 대기 상태를 전부 만족하므로 그대로 통과합니다. 읽은 값은 지금 처리하고 있어야 할 대상과 대조할 필요가 있습니다 ── 피킹 리스트의 다음 1건과 일치하는가, 스캔이 끝난 컨테이너 ID에 연결되어 있는가, 출하처가 작업 중인 배차와 같은가. 무엇과 대조할 것인가는 업무마다 다르므로 이 부분만큼은 범용 코드가 될 수 없습니다.

이중 처리는 검증만으로는 막을 수 없습니다. 두 대의 단말기가 같은 라벨을 거의 동시에 읽으면, 양쪽 모두 ‘출하 대기’를 확인한 뒤 상태를 갱신하기 때문에 둘 다 통과합니다. 이를 막는 것은 실행 쪽의 역할로, UPDATE ... WHERE status = '출하 대기'와 같은 조건부 상태 전이를 하나의 원자적 연산으로 만들거나, 멱등 키로 재실행을 흡수합니다. 검증은 입구에서의 판단일 뿐, 배타 제어를 대신하지는 못합니다.

‘사고’와 ‘공격’은 별개의 문제

여기까지의 3단계가 지키는 것은 사고입니다. 손상에 의한 오독, 분할 QR의 누락, 문자 깨짐, 오래된 라벨의 혼입 ── 악의 없는 오류는 이것으로 막을 수 있습니다.

반면 값을 바꿀 동기가 있는 용도(가격표, 쿠폰, 입장권, 결제)에서는 방어가 되지 않습니다. 공격자는 형식을 충족시키고 체크 디지트를 다시 계산해, 실재하는 다른 번호를 가리키는 QR을 자유롭게 만들 수 있습니다. 마스터 대조는 존재 여부만 보므로 그대로 통과합니다.

QR을 소지하고 있다는 것이 가치나 권한을 의미한다면, 값 자체에 진정성을 부여할 필요가 있습니다. 서버가 발급하는 추측 불가능한 토큰(충분한 길이의 난수)으로 만들어 번호에서 번호를 추측할 수 없게 하거나, 페이로드에 키 있는 MAC이나 전자 서명을 붙여 받는 쪽이 키로 검증하는 방법이 있습니다. 어느 쪽이든 사용 완료 상태를 서버 쪽에서 관리해 복제본의 이중 사용을 막습니다.

다만 진정성만으로는 ‘바꿔치기’를 막을 수 없습니다. 저렴한 상품에서 정규 QR을 떼어 내 비싼 상품에 붙이면 토큰도 서명도 진짜인 채로 남습니다. 다시 붙일 수 있는 가격표처럼 매체가 재사용되는 상황에서는 사용 완료 판정도 통하지 않습니다. 여기서도 통하는 것은 3단계와 같은 발상으로, 그 QR이 눈앞의 대상의 것인가를 값과는 다른 경로로 확인하는 것입니다 ── 상품 쪽의 식별을 다른 수단으로 취득해 대조한다, 거래 문맥(계산대의 명세, 입장 시간대)과 대조한다, 떼어 내면 찢어지는 라벨로 물리적으로 결합한다 같은 방법이 있습니다.

체크 디지트도 마스터 대조도 진정성에 대해서는 아무것도 보장하지 않습니다. 그리고 진정성 그 자체도 그 값이 눈앞의 대상의 것이라는 사실까지는 보장하지 않습니다. ‘오독 대책’, ‘위조 대책’, ‘바꿔치기 대책’을 같은 구조로 해결하려 하지 않는 것이 핵심입니다.

7. 운영 측에서 정해 둘 것

코드만으로는 완전히 막을 수 없는 부분이 있습니다.

  • QR 아래에 사람이 읽을 수 있는 문자열을 반드시 병기한다. GS1이 정하는 HRI(Human Readable Interpretation)의 사고방식과 같습니다.4 오독이 의심될 때 사람이 대조할 수 있는 수단이 남습니다. 실제 운영에서는 이것이 유일한 발견 수단이 되는 경우도 드물지 않습니다. 바코드 전반의 현장 운용은 GS1 바코드 규격의 기본과 현장 운용 시 주의점에 정리해 두었습니다.
  • ‘읽을 수 없음’일 때의 절차를 정해 둔다. 재스캔 횟수의 상한, 수기 입력으로의 대체, 그 승인자. 이 부분이 애매하면 현장은 ‘읽힐 때까지 각도를 바꿔 가며 몇 번이고 시도한다’는, 오독 확률을 높이는 방향으로 움직입니다.
  • 거부된 값을 로그에 남긴다. 특정 라벨이나 단말기에 편중되어 있다면 인쇄기나 스캐너의 이상을 조기에 발견할 수 있습니다. 다만 원본 문자열을 행 지향 로그에 그대로 적지는 마십시오. 개행이나 제어 문자를 포함한 값은 로그의 행을 위장하거나 표시를 망가뜨립니다. 원본은 길이를 제한하고 이스케이프해 구조화 로그의 필드나 데이터베이스의 컬럼에 저장하고, 사람이 읽는 행에는 무해화한 표현만 출력한다 ── 이 분리를 처음부터 넣어 둡니다. 가능하면 읽은 이미지도 남겨 둡니다.
  • 분할 QR을 사용하지 않는다. 데이터가 다 들어가지 않는다면 버전을 올리거나, QR에는 식별자만 넣고 나머지는 마스터에서 가져옵니다. 후자는 라벨을 작게 만들 수 있고, 내용 수정을 라벨 재발행 없이 할 수 있다는 이점도 있습니다.
  • 문자 인코딩은 ‘넣지 않는’ 것으로 해결한다. 업무용 QR은 ASCII 범위 안으로 한정합니다. 바이트 모드는 ECI 지정이 없으면 문자 인코딩을 선언하지 않은 상태이며, 기본 해석은 규격의 버전에 따라 달라집니다.3 UTF-8로 작성하는 것만으로는 상호 운용성을 얻을 수 없고, 다른 해석을 하는 스캐너에서는 문자가 깨집니다. 아무래도 비ASCII를 넣어야 한다면 UTF-8을 ECI 지정과 함께 선언하는 것이 규격상의 정답이지만, 5.2절에서 본 대로 ECI 처리가 미덥지 않은 구현도 실제로 존재하므로 예상 기종에서의 실기 확인을 피할 수는 없습니다.
  • 되돌릴 수 없는 조작 전에 확인을 넣는다. 출하 확정・재고 차감・입금 소진 처리처럼 취소 부담이 큰 조작에서는, 읽은 값에서 조회한 품명이나 금액을 화면에 표시해 사람에게 보여 줍니다. 오독은 값으로서는 타당해 보여도 업무 문맥에서는 부자연스러워 보이는 경우가 많기 때문입니다.

검증을 어디까지 할 것인가는 틀렸을 때의 손해로 정해집니다.

용도 형식 검사 체크 디지트 마스터 대조 사람에 의한 확인
사내의 장소・선반 번호 필수 임의 권장 불필요
입출고・재고 조사 필수 권장 필수 불필요
출하 확정・재고 차감 필수 필수 필수 권장
청구・입금 소진 처리 필수 필수 필수 필수
의약품・위험물 혼동 방지 필수 필수 필수 필수

8. 정리

QR코드의 오류 정정은 인쇄된 패턴으로부터 원래의 부호어를 복원하기 위한 구조입니다. 이 역할은 확실히 해냅니다. 실측에서도 무작위 손상이나 균일한 화질 열화에 대해서는, 정정할 수 있는 범위에서는 정확히 읽고, 정정할 수 없는 범위에서는 순순히 읽기를 포기했습니다.

그러나 그것은 애플리케이션이 받은 문자열이 업무적으로 올바르다는 것과는 별개의 이야기입니다. 손상의 위치에 따라서는 정정이 작동한 결과로 다른 타당한 값이 나옵니다. 분할 QR이나 문자 인코딩, 화면 안의 다른 코드에 이르면 오류 정정 바깥쪽의 문제이며, 확률조차 아닙니다.

규격 스스로가 ‘명백히 타당하지만 다른 부호어로 오복호한다’고 적고, 오독 방지용 부호어를 일부러 확보해 두고 있다는 사실이 이 구조를 단적으로 보여 줍니다. 그렇게까지 해도 확률을 낮출 수 있을 뿐 ── 그것이 규격이 도달한 지점입니다. 나머지를 메울 수 있는 것은 값을 받은 애플리케이션뿐입니다.

읽어낸 값은 외부에서 온 미검증 입력입니다. 키보드로 입력된 문자열과 같은 방식으로 다룬다 ── 그것이 QR코드와 올바르게 함께하는 방법이라고 생각합니다.

자신의 QR을 시험해 보는 3단계

여기까지의 이야기는 자사의 라벨로 그대로 확인해 볼 수 있습니다.

  1. 생성한다. 실제로 운영 중인 서식의 값을 하나, 손에 있는 QR 생성 도구로 QR로 만듭니다(이 글의 샘플은 segno 1.6.6으로 생성했습니다). 먼저 손상되지 않은 상태에서 읽히는지 확인합니다.
  2. 손상을 준다. 이미지 편집 소프트웨어로 접힘 자국이나 인쇄 헤드의 막힘에 빗댄 세로 한 줄의 띠를 그립니다. 중요한 것은 양이 아니라 치우침이므로, 전체에 옅게 노이즈를 흩뿌리지 말고 좁은 범위에 집중시켜 주십시오.
  3. 두 디코더로 비교한다. QR코드 읽기 비교 도구에 읽혀, jsQR과 OpenCV.js의 결과를 비교합니다. 한쪽만 값을 반환한다, 양쪽이 같은 값을 반환하는데 원래 값과 다르다 같은 동작을 그 자리에서 확인할 수 있습니다.

결과가 두 디코더에서 갈린다면, 그 조건은 자사의 검증 설계에서 반드시 처리해야 할 조건입니다. ‘우리 현장의 스캐너는 괜찮은가’를 책상 위에서 한 번 시험해 볼 가치가 있습니다.


검증 환경

오류 정정의 동작을 살펴보는 2~4절은 버전 1-M(21×21모듈)로 통일했습니다. 5절의 샘플은 데이터 양에 따라 버전이 달라지므로 절마다 기재합니다.

항목 내용
디코더 1 OpenCV 5.0.0 cv2.QRCodeDetector
디코더 2 jsQR 1.4.0(Node.js 22)
생성 segno 1.6.6 / Python 3.11
2~4절의 심볼 버전 1-M / 21×21(26부호어 = 데이터 16 + 오류 정정 10)
5.1절(분할 QR) 3장 모두 버전 1-M / 21×21
5.2절(문자 인코딩과 ECI) Shift_JIS는 버전 2-Q, UTF-8은 버전 2-M(모두 25×25). 일본어 18~23바이트가 버전 1-M의 데이터 16부호어에 들어가지 않기 때문
5.3절(복수 코드) NO:ITEM:은 버전 1-M, LOT:AB-77은 데이터가 짧아 segno가 오류 정정 레벨을 끌어올리기 때문에 버전 1-H(모두 21×21)
  1. 주식회사 덴소웨이브, 오류 정정 기능에 대하여|QR코드닷컴 

  2. ISO/IEC 18004 Information technology — Automatic identification and data capture techniques — QR code bar code symbology specification. 오류 정정 능력의 식 e + 2t ≦ d - p, 오독 방지용 부호어 p의 값, 그리고 ‘명백히 타당하지만 다른 부호어로 오복호한다’는 기술은 8.5.1 Error correction capacity에, 버전 1-M의 (26,16,4)와 각주 ‘오독 확률을 낮추기 위해 정정 능력을 오류 정정 부호어 수의 절반 미만으로 하고 있다’는 Table 13에 있습니다(인용은 2000년판을 기준으로 함). 현행판은 ISO/IEC 18004:2024입니다.  2 3 4

  3. 바이트 모드에서 ECI 지정이 없을 때의 기본 해석은 규격 버전에 따라 달라져 왔습니다. ISO/IEC 18004:2000의 8.3.1은 「The default interpretation for QR Code is ECI 000020 representing the JIS8 and Shift JIS character sets」라고 정하고 있었지만, 2006년판(QR Code 2005) 이후로는 ECI 000003, 즉 ISO/IEC 8859-1이 기본입니다. 선언되지 않은 기본값에 의존하면, 규격의 버전이 바뀌는 것만으로 해석이 달라질 수 있다는 뜻이기도 합니다.  2

  4. GS1, GS1 General Specifications 

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

오류 정정 레벨을 H로 하면 오독을 막을 수 있나요?
막을 수 없습니다. 레벨을 높이면 정정할 수 있는 손상의 양은 늘어나지만, '정정이 작동해 다른 값이 되어버리는' 현상 자체는 사라지지 않습니다. 리드-솔로몬 복호는 '받은 패턴으로부터 정정 능력 범위 안에 부호어가 있는지 찾는' 동작이므로, 손상이 다른 부호어 근방으로 들어가면 그것을 정답으로 판단해 반환합니다(멀리 벗어난 손상은 '읽을 수 없음'으로 끝납니다). ISO/IEC 18004는 이를 '명백히 타당하지만 다른 부호어로 오복호한다'고 명시하고 있으며, 오독 방지용 부호어를 별도로 확보해 두고 있지만, 이 역시 확률을 낮추는 조치일 뿐 보장은 아닙니다. 오독을 잡아낼 수 있는 것은 값을 받은 애플리케이션 쪽뿐입니다.
실제로 QR의 오독은 어느 정도 발생하나요?
무작위 손상이라면 거의 일어나지 않습니다. 이 글의 실측에서는 모듈을 무작위로 반전시킨 3,900회와 부호어를 무작위로 파괴한 5,800회 전부에서 잘못된 값이 반환된 사례는 0건이었습니다. 정정할 수 없는 손상은 '읽을 수 없음'으로 귀결됩니다. 다만 실제 손상은 무작위가 아니라 접힘 자국·마찰·인쇄 헤드의 막힘처럼 위치에 편향을 가집니다. 게다가 분할 QR의 일부 누락이나 여러 코드의 혼동은 확률의 문제조차 아니어서, 조건이 갖춰지면 매번 발생합니다.
QR에는 오류 정정이 있는데도 체크 디지트가 필요한가요?
필요합니다. 지키는 계층이 다릅니다. 오류 정정이 다루는 것은 심볼 내부의 일관성이며, 그마저도 복원을 보장하는 것은 정정 능력 범위 내까지입니다. 그 범위를 넘는 손상에서는 다른 타당한 부호어로 '복원'되어 버리는 경우가 있습니다. 체크 디지트 쪽은 '애플리케이션이 받은 문자열이 코드 체계로서 성립하는가'를 검사합니다. 한 글자의 변화는 확실히 잡아낼 수 있지만, 여러 글자가 바뀐 경우에는 놓치는 경우가 있습니다. 모듈러스 10의 검사값은 10가지밖에 없어서, 본문에서도 두 자리가 바뀌었는데 체크 디지트가 일치하는 예를 들었습니다. 그러므로 체크 디지트는 오독의 대부분을 막아 주는 계층으로 여기고, 최종 판단은 마스터 대조와 업무적 확인에 맡기시기 바랍니다. 수기 입력·전사·다른 경로로부터의 입수도 같은 검사로 지킬 수 있다는 점이 장점입니다.
스마트폰 카메라 앱으로 읽혔으니 값은 올바른 것 아닌가요?
'읽혔다'가 의미하는 것은 '디코더가 비어 있지 않은 문자열을 반환했다'는 것뿐이며, 그 내용이 올바른지는 아무것도 말해 주지 않습니다. 실패를 반환하는 방식도 구현마다 달라서 예외·빈 문자열·null 중 어느 것이든 있을 수 있으므로, 예외가 발생하지 않았다는 것을 성공의 판정 기준으로 삼는 것도 위험합니다. 이 글의 실측에서는 같은 이미지에 대해 OpenCV와 jsQR이 서로 다른 결과를 반환한 사례가 여러 건 있었습니다. Shift_JIS로 일본어를 넣은 QR을, 한쪽은 문자가 깨진 문자열을 성공으로 반환하고, 다른 한쪽은 빈 문자열을 반환했습니다. 분할 QR의 첫 번째 조각에서도 결과가 갈렸습니다. 읽혔는지 여부는 값의 정확성에 대해 아무것도 말해 주지 않습니다.
분할 QR(Structured Append)은 업무에서 사용하지 않는 편이 좋을까요?
특별한 이유가 없다면 피하는 것이 무난합니다. 분할 QR은 여러 심볼로 나눈 데이터를 읽는 쪽에서 모아 연결하는 구조인데, 지원하지 않는 디코더에 첫 번째 조각만 읽혔을 때의 동작은 구현에 따라 다릅니다. 이 글의 실측에서는 OpenCV가 끝부분이 빠진 전표 번호를 오류 없이 반환한 반면, jsQR은 빈 문자열을 반환했습니다. 조각을 그럴듯한 값으로 통과시켜 버리는 구현이 존재하는 이상, 분할 QR을 상정하지 않는다면 불완전한 결과를 걸러내는 검증이 필요합니다. 데이터가 다 들어가지 않는다면 QR의 버전을 올리거나, 코드를 짧게 해서 마스터 참조 방식으로 옮기는 편이 안전합니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기