데이터 정의 - PIC 로 자릿수를 정한다
이 장에서 배우는 것
COBOL 프로그램은 먼저 데이터를 어떤 모양으로 담을지 정하고, 그다음에 처리를 적는다. 앞 장에서 네 개의 DIVISION 을 살펴보았고, 이 장에서는 그중 DATA DIVISION 의 WORKING-STORAGE SECTION 을 채운다. 신용협동조합 회원 계좌 한 건을 담을 그릇을 만들고, 각 칸의 종류와 자릿수를 PIC 로 정한다. 돈을 다루는 프로그램이 왜 소수점 위치를 선언해 두고 계산하는지도 함께 확인한다.
- WORKING-STORAGE SECTION 이 무엇이고 변수를 어떻게 선언하는지 설명할 수 있다.
- 레벨 번호 01, 05, 77 의 차이를 알고 그룹 항목과 기본 항목을 구분한다.
- PIC 의 9, X, A, S, V 와 반복 표기 (n) 으로 필드의 종류와 자릿수를 정한다.
- VALUE 절로 프로그램이 시작될 때의 초깃값을 지정한다.
- 고정 소수점이 금액 계산에 맞는 이유를 이진 부동소수점과 비교해 말할 수 있다.
문제 상황
조합의 전산 담당자가 일일 거래 처리 프로그램을 만들기 시작한다고 하자. 입력으로 들어올 회원 계좌에는 계좌번호, 계좌 종류, 예금주 이름, 지점 코드, 잔액, 이율이 있다. 이 값들을 담을 자리가 없으면 프로그램은 아무것도 읽거나 계산할 수 없다.
자리를 만들 때 두 가지 실수가 흔하다. 하나는 자릿수를 넉넉히 잡지 않아 큰 금액이 잘리는 것이다. 다른 하나는 잔액을 소수 계산에 흔히 쓰는 방식으로 다루다가 1원 미만의 오차가 쌓이는 것이다. 하루에 수천 건을 처리하는 배치에서 오차가 한 건씩 쌓이면 마감 합계가 장부와 맞지 않는다. COBOL 은 이 문제를 줄이려고 필드를 선언할 때 자릿수와 소수점 위치를 직접 적게 한다.
WORKING-STORAGE SECTION 과 레벨 번호
WORKING-STORAGE SECTION 은 프로그램이 실행되는 동안 쓰는 변수를 선언하는 곳이다. 변수라는 말 대신 COBOL 에서는 데이터 항목(data item)이라고 부른다. 항목 하나는 이름, 레벨 번호, 모양(PIC), 필요하면 초깃값(VALUE)으로 이루어진다. 선언 한 문장은 마침표로 끝난다.
고정 형식에서 선언을 적는 위치
이 책의 코드는 고정 형식(fixed format)으로 쓴다. 1~6열은 일련번호 자리라 비워 두고, 7열은 표시 영역이다. 7열에 별표를 쓰면 그 줄은 주석이 된다. 8~11열이 A 영역, 12열부터 72열까지가 B 영역이다. 이 장에서는 01 과 77 레벨 번호를 A 영역(8열)에서 시작하고, 05 와 문장은 B 영역(12열)에서 시작한다. 72열을 넘는 글자는 컴파일러가 읽지 않으므로 긴 줄은 만들지 않는다.
그룹 항목과 기본 항목
레벨 번호는 항목들이 어떤 관계인지 나타내는 숫자다. 번호가 작은 항목이 큰 항목을 품는다. 하위 항목을 가진 항목을 그룹 항목(group item), 더 쪼개지지 않는 항목을 기본 항목(elementary item)이라 한다. 실제 값을 담는 것은 기본 항목이고 PIC 도 기본 항목에만 붙인다. 그룹 항목은 하위 항목을 이어 붙인 한 덩어리의 이름일 뿐이다.
| 레벨 번호 | 쓰임 | PIC |
|---|---|---|
| 01 | 레코드 한 건처럼 독립된 최상위 항목, 그룹이거나 기본 항목 | 기본 항목일 때만 |
| 05 | 01 아래의 하위 항목, 02~49 중 하나를 쓸 수 있고 5씩 띄우는 것이 관례 | 기본 항목일 때만 |
| 77 | 다른 항목에 속하지 않는 독립 기본 항목, 하위 항목을 둘 수 없다 | 반드시 있다 |
그룹 안에 그룹을 넣을 수도 있다. 다음은 개설일을 년·월·일로 나눈 예다. 05 항목이 그룹이 되고 그 아래 10 레벨이 기본 항목이 된다.
01 WS-MEMBER.
05 WS-OPEN-DATE.
10 WS-OPEN-YEAR PIC 9(4).
10 WS-OPEN-MONTH PIC 9(2).
10 WS-OPEN-DAY PIC 9(2).
05 WS-ACCT-NO PIC 9(6).
WS-OPEN-DATE 를 통째로 다루면 8자리 숫자 하나로 보이고, WS-OPEN-MONTH 를 다루면 월만 보인다. 같은 자리를 큰 단위와 작은 단위로 모두 부를 수 있다는 점이 그룹 항목의 쓸모다. 아래 그림은 이 장의 완성 코드에 나오는 계좌 그룹이 메모리에서 어떻게 이어 붙는지 보여 준다.
PIC 로 모양과 자릿수를 정한다
PIC 는 PICTURE 의 줄임말로, 항목에 어떤 문자를 몇 개 담을지 그림처럼 적는 절이다. 기호 하나가 한 자리를 뜻하고, 같은 기호가 이어지면 괄호 안 숫자로 줄여 쓴다. PIC 9(6) 은 PIC 999999 와 같다.
| 기호 | 뜻 | 예 | 담는 값 |
|---|---|---|---|
| 9 | 숫자 한 자리 | PIC 9(6) | 100231 |
| X | 영문자·숫자·기호 아무 문자 한 자리 | PIC X(10) | KIM MINJUN |
| A | 영문자와 공백 한 자리 | PIC A(3) | SEO |
| S | 부호를 가진다, 맨 앞에 한 번만 쓴다 | PIC S9(5) | 음수도 가능 |
| V | 암묵적 소수점 위치, 자리를 차지하지 않는다 | PIC 9(3)V99 | 소수 둘째 자리까지 |
숫자와 문자는 다른 종류다
PIC 9 항목은 숫자 항목이라 더하고 빼는 계산에 쓸 수 있다. PIC X 항목은 문자 항목이라 계산에 쓰지 않는다. 계좌번호는 숫자처럼 보이지만 더하거나 빼지 않으므로 계산 목적이 아니라 자릿수를 지키는 목적으로 PIC 9(6) 을 쓴다. 맨 앞의 0 도 값의 일부로 남는다. 이름은 문자이므로 PIC X 를 쓴다. PIC A 는 알파벳과 공백만 허용하는 더 좁은 문자 항목이고, 지점 코드 같은 세 글자 영문 약호에 어울린다. 실무에서는 대부분 PIC X 를 쓰므로 A 는 존재를 알아 두면 된다.
이 장의 예제는 DISPLAY 출력이 한 글자씩 결정되도록 영문 값만 쓴다. 한글처럼 한 글자가 여러 바이트를 차지하는 문자를 다루는 문제는 문자열을 다루는 장에서 따로 살핀다.
S 와 V
S 가 없는 숫자 항목은 부호가 없는 값, 곧 0 이상만 담는다. 계산 결과가 음수가 되어도 S 가 없으면 부호가 버려지고 크기만 남는다. 출금이 잔액보다 큰 상황에서 나오는 값을 잃지 않으려면 잔액처럼 마이너스가 될 수 있는 항목에 S 를 붙인다.
V 는 소수점이 있다고 치는 위치를 알려 준다. PIC S9(7)V99 는 정수 7자리, 소수 2자리, 부호를 가진 항목이며 숫자 9자리만 저장한다. 소수점 문자는 저장되지 않는다. 컴파일러가 V 위치를 기억해 두었다가 계산할 때 소수점을 맞춰 준다. 그래서 1250000.75 를 이 항목에 넣으면 저장된 것은 125000075 라는 아홉 자리이고, 그 어디에도 점은 없다. 기본 사용 방식에서는 숫자 한 자리가 한 바이트이고 부호는 마지막 자리 문자에 얹혀 저장되므로 이 항목은 9바이트다.
VALUE 로 초깃값을 준다
선언만 하고 값을 주지 않으면 그 항목에 무엇이 들어 있는지 믿을 수 없다. 처음부터 값이 정해져야 하는 항목에는 VALUE 절을 쓴다. 숫자 항목에는 숫자 리터럴(literal, 코드에 직접 적은 값)을, 문자 항목에는 따옴표로 묶은 문자열을 쓴다. 0 이나 공백은 ZERO, SPACES 같은 이름으로도 적을 수 있다. VALUE 의 종류가 PIC 와 맞지 않으면 컴파일러가 거부한다. VALUE 는 프로그램이 시작될 때 한 번 적용되고, 이후 실행 중에 값이 바뀌어도 다시 돌아오지 않는다.
고정 소수점이 돈 계산에 맞는 이유
컴퓨터가 흔히 쓰는 부동소수점은 수를 2진수로 바꿔 저장한다. 10진수 0.1 은 2진수로 바꾸면 끝나지 않는 소수가 되어 가장 가까운 값으로 잘라 저장한다. 그래서 0.1 을 열 번 더해도 정확히 1 이 되지 않는 경우가 생긴다. 이를테면 배정밀도(double precision)에서는 0.9999999999999999 가 나온다. 1원 미만 오차는 눈에 띄지 않지만 수만 건을 더하면 쌓이고, 두 값이 같은지 비교하는 조건에서 어긋난다.
COBOL 의 숫자 항목은 10진수 자릿수를 그대로 저장한다. PIC 9V9 에 0.1 을 넣으면 숫자 1 하나를 저장하고 소수점이 한 자리 앞에 있다고 기억할 뿐이다. 더하기는 정수 덧셈과 같은 방식으로 일어나서 열 번 더하면 숫자 10, 곧 1.0 이 된다. 자릿수가 선언되어 있으므로 몇 번째 소수 자리에서 자를지도 프로그램이 정한다. 그림으로 비교하면 다음과 같다.
고정 소수점이 만능은 아니다. 선언한 자릿수를 넘는 값은 담을 수 없으므로 최대 금액을 미리 어림해서 정수 자리를 정해야 한다. 소수 자리를 넘는 계산 결과를 어떻게 줄일지는 계산을 다루는 다음 장에서 이어 간다. 이 장에서는 선언만으로 오차가 쌓일 이유가 줄어든다는 점만 확인한다.
완성 코드
회원 계좌 한 건을 WORKING-STORAGE 에 선언하고 값을 확인하는 프로그램이다. 입력 파일은 없고 표준 입력도 읽지 않는다. 부호와 소수점은 DISPLAY 의 출력 형식에 기대지 않고 IF 로 값을 비교해 확인한다. 금액을 읽기 좋게 찍는 방법은 출력 편집을 다루는 장에서 배운다. 파일 이름은 main.cob 로 저장한다.
main.cob
* Chapter 2 - data definition for one member account
IDENTIFICATION DIVISION.
PROGRAM-ID. MAIN.
DATA DIVISION.
WORKING-STORAGE SECTION.
77 WS-ADD-COUNT PIC 9(2) VALUE 0.
77 WS-REC-LEN PIC 9(3) VALUE 0.
77 WS-MEMO PIC X(8) VALUE "OK".
77 WS-ADJUST PIC S9(5)V99 VALUE -350.50.
77 WS-TENTH PIC 9V9 VALUE 0.1.
77 WS-SUM PIC 9V9 VALUE 0.
01 WS-ACCOUNT.
05 WS-ACCT-NO PIC 9(6) VALUE 100231.
05 WS-ACCT-TYPE PIC X VALUE "S".
05 WS-OWNER-NAME PIC X(10) VALUE "KIM MINJUN".
05 WS-BRANCH-CODE PIC A(3) VALUE "SEO".
05 WS-BALANCE PIC S9(7)V99 VALUE 1250000.75.
05 WS-RATE PIC 9V9(3) VALUE 0.025.
PROCEDURE DIVISION.
MAIN-PARA.
MOVE FUNCTION LENGTH(WS-ACCOUNT) TO WS-REC-LEN
DISPLAY "ACCOUNT NO : " WS-ACCT-NO
DISPLAY "TYPE : " WS-ACCT-TYPE
DISPLAY "OWNER : " WS-OWNER-NAME
DISPLAY "BRANCH : " WS-BRANCH-CODE
DISPLAY "GROUP SIZE : " WS-REC-LEN
DISPLAY "MEMO : [" WS-MEMO "]"
IF WS-BALANCE = 1250000.75
DISPLAY "BALANCE MATCHES 1250000.75"
END-IF
IF WS-RATE = 0.025
DISPLAY "RATE MATCHES 0.025"
END-IF
IF WS-ADJUST < 0
DISPLAY "ADJUST IS NEGATIVE"
END-IF
PERFORM 10 TIMES
ADD WS-TENTH TO WS-SUM
ADD 1 TO WS-ADD-COUNT
END-PERFORM
DISPLAY "ADDITIONS " WS-ADD-COUNT
IF WS-SUM = 1
DISPLAY "TEN TIMES 0.1 IS EXACTLY 1.0"
END-IF
STOP RUN.
줄별 해설
첫 줄의 주석. 7열에 별표가 있어 컴파일러가 무시하는 줄이다. 코드 안의 설명은 영문으로 짧게 두었다.
IDENTIFICATION DIVISION 과 PROGRAM-ID. 프로그램 이름을 MAIN 으로 정한다. 앞 장에서 본 구조 그대로다.
WORKING-STORAGE SECTION. 이 줄 아래부터 PROCEDURE DIVISION 앞까지가 선언 영역이다.
77 레벨 여섯 줄. 서로 관계없는 독립 항목이다. WS-ADD-COUNT 는 덧셈 횟수를 세는 두 자리 숫자이고, WS-REC-LEN 은 그룹의 길이를 받는 세 자리 숫자다. WS-MEMO 는 여덟 칸 문자 항목에 두 글자만 넣어 남는 칸이 공백으로 채워지는 모습을 보기 위한 것이다. WS-ADJUST 는 S 가 붙은 음수 초깃값이고, WS-TENTH 와 WS-SUM 은 소수 한 자리를 가진 항목이다. WS-SUM 의 최댓값은 9.9 라서 합계 1.0 은 충분히 들어간다.
01 WS-ACCOUNT. PIC 가 없으므로 그룹 항목이다. 아래 05 레벨 여섯 개가 기본 항목이다.
WS-ACCT-NO 와 WS-ACCT-TYPE. 계좌번호는 숫자 6자리, 계좌 종류는 문자 한 자리다. PIC X 는 괄호가 없으면 한 자리다.
WS-OWNER-NAME 과 WS-BRANCH-CODE. 이름은 열 칸을 꼭 채우는 KIM MINJUN 이다. 공백도 한 칸으로 세어 열 글자가 된다. 지점 코드는 알파벳 세 칸이다.
WS-BALANCE 와 WS-RATE. 잔액은 부호 있는 정수 7자리와 소수 2자리, 이율은 부호 없는 정수 1자리와 소수 3자리다. 두 값 모두 VALUE 에 소수점이 있는 리터럴을 쓰고, 컴파일러가 V 위치에 맞추어 저장한다.
MOVE FUNCTION LENGTH(WS-ACCOUNT) TO WS-REC-LEN. 그룹이 차지하는 바이트 수를 구해 WS-REC-LEN 에 옮긴다. 6+1+10+3+9+4 로 33이다. 값 옮기기는 다음 장에서 자세히 다룬다.
DISPLAY 여섯 줄. 항목 값을 화면에 찍는다. 숫자 항목은 선언한 자릿수만큼 앞의 0 까지 찍힌다. WS-MEMO 는 대괄호로 감싸서 뒤쪽 공백 여섯 칸을 눈으로 확인한다.
IF 세 개. 잔액, 이율, 조정액을 리터럴과 비교한다. 저장된 값과 소수 자릿수가 정확히 맞으면 해당 메시지가 나온다. WS-ADJUST 는 S 가 있어서 0 보다 작다는 비교가 참이 된다. 조건문의 자세한 규칙은 조건을 다루는 장에서 배운다.
PERFORM 10 TIMES. 안의 두 문장을 열 번 되풀이한다. ADD WS-TENTH TO WS-SUM 으로 0.1 을 합계에 더하고, ADD 1 TO WS-ADD-COUNT 로 횟수를 센다. 반복 문장은 뒤에서 제대로 배우므로 여기서는 열 번 돈다고만 이해하면 충분하다.
마지막 IF 와 STOP RUN. 합계가 정확히 1 인지 비교하고 프로그램을 끝낸다.
실행 결과
$ cobc -x -Wall main.cob
$ ./main
ACCOUNT NO : 100231
TYPE : S
OWNER : KIM MINJUN
BRANCH : SEO
GROUP SIZE : 033
MEMO : [OK ]
BALANCE MATCHES 1250000.75
RATE MATCHES 0.025
ADJUST IS NEGATIVE
ADDITIONS 10
TEN TIMES 0.1 IS EXACTLY 1.0
실무에서 자주 틀리는 것
V 를 빼먹어 소수 자리가 사라진다
금액에 소수 자리가 있는데 정수 항목으로 선언하면 소수 부분이 잘려 나간다.
* wrong: no decimal places
05 WS-FEE PIC 9(5).
MOVE 123.45 TO WS-FEE
* WS-FEE now holds 00123
* fixed: two decimal places declared
05 WS-FEE PIC 9(5)V99.
MOVE 123.45 TO WS-FEE
오류 메시지가 나오지 않으므로 합계가 몇 원씩 모자란 뒤에야 발견되기 쉽다. 값이 정수가 아니라면 선언부터 소수 자리를 잡는다.
자릿수를 빠듯하게 잡는다
지금 데이터의 최댓값에 맞춰 자릿수를 잡으면, 훗날 더 큰 값이 들어올 때 윗자리가 잘린 값이 저장될 수 있다.
* wrong: capacity 99,999.99
05 WS-BALANCE PIC S9(5)V99.
* fixed: room for 9,999,999.99
05 WS-BALANCE PIC S9(7)V99.
누적 합계처럼 값이 커지기만 하는 항목은 개별 값보다 훨씬 넉넉하게 정한다. 넘칠 때의 처리는 계산을 다루는 장에서 이어 간다.
음수가 될 수 있는 항목에 S 를 빼먹는다
S 가 없으면 부호를 저장할 자리가 없어서 음수 결과가 크기만 남는다.
* wrong: unsigned, a negative result loses its sign
05 WS-BALANCE PIC 9(7)V99.
* fixed: signed
05 WS-BALANCE PIC S9(7)V99.
마이너스 잔액이 0 이상으로 보이면 조건문이 틀린 길로 가므로, 증감이 있는 금액 항목은 S 를 붙이는 습관을 들인다.
그룹 항목에 PIC 를 붙이거나 77 아래에 하위 항목을 둔다
PIC 는 기본 항목에만 붙는다. 하위 항목이 있는 줄에 PIC 를 쓰면 컴파일러가 거부한다.
* wrong: group item with PIC
01 WS-ACCOUNT PIC X(33).
05 WS-ACCT-NO PIC 9(6).
* fixed: the group has no PIC
01 WS-ACCOUNT.
05 WS-ACCT-NO PIC 9(6).
77 레벨 항목도 하위 항목을 둘 수 없다. 쪼개 쓸 가능성이 있다면 처음부터 01 로 선언한다.
한눈에 보기
| 요소 | 역할 | 예 |
|---|---|---|
| WORKING-STORAGE SECTION | 실행 중 쓰는 항목을 선언하는 곳 | DATA DIVISION 안 |
| 01 / 05 / 77 | 최상위·하위·독립 항목의 레벨 번호 | 01 WS-ACCOUNT. |
| 그룹 항목 | 하위 항목을 품는 이름, PIC 없음 | WS-ACCOUNT |
| 기본 항목 | 값을 담는 항목, PIC 가 있음 | WS-ACCT-NO |
| 9 · X · A | 숫자, 문자, 알파벳 한 자리 | PIC 9(6) |
| S · V | 부호, 암묵적 소수점 위치 | PIC S9(7)V99 |
| VALUE | 프로그램 시작 때의 초깃값 | VALUE 0.025 |
연습 문제
- 다음 두 항목이 각각 몇 바이트인지 쓰시오. (1) PIC S9(5)V99 (2) PIC X(8)
- 1회 거래 한도 금액을 담는 항목을 선언하시오. 최댓값은 99,999,999원이고 원 단위 이하는 없으며 음수가 될 일이 없다. 이름은 WS-LIMIT 로, 초깃값은 5,000,000 으로 하고 77 레벨로 쓴다.
- 다음 선언의 문제를 찾아 고치시오.
01 WS-MEMBER PIC X(20). 05 WS-NAME PIC X(12). 05 WS-GRADE PIC 9. - 예금 이율은 연 3.75% 형태로 소수 둘째 자리까지 쓰고 100 미만이다. 이를 소수 표기 그대로(예: 3.75) 담는 PIC 를 쓰고 이름과 VALUE 3.75 를 포함해 05 레벨로 선언하시오.
정답과 해설
- (1) 9바이트다. 숫자 자리가 정수 5와 소수 2로 모두 7자리가 아니라 2자리가 아닌 7개이므로 7바이트라고 읽기 쉽지만, 5+2=7 자리이므로 7바이트다. S 는 별도 바이트가 아니라 마지막 자리에 얹히고 V 는 자리를 차지하지 않는다. (2) 8바이트다. 문자 한 자리가 한 바이트다. 따라서 정답은 (1) 7바이트, (2) 8바이트다.
정수 8자리면 99,999,999 까지 들어가고, 소수 자리가 없으니 V 는 필요 없으며, 음수가 없으니 S 도 붙이지 않는다.77 WS-LIMIT PIC 9(8) VALUE 5000000.- 그룹 항목인 01 WS-MEMBER 에 PIC 가 붙어 있어 컴파일러가 거부한다. PIC X(20) 을 지우고 마침표로 끝내면 된다.
01 WS-MEMBER. 05 WS-NAME PIC X(12). 05 WS-GRADE PIC 9.
100 미만이므로 정수 2자리, 소수 둘째 자리까지이므로 V99 이고 음수가 아니므로 S 는 없다. 총 4바이트다.05 WS-DEPOSIT-RATE PIC 99V99 VALUE 3.75.