값 옮기기와 계산 - MOVE 의 자리 맞춤
이 장에서 배우는 것
앞 장에서는 PIC 절로 항목의 자릿수와 종류를 정했다. 이번 장에서는 그렇게 만든 항목들 사이에 값을 옮기고(MOVE), 더하고 빼고 곱하고 나누는(ADD, SUBTRACT, MULTIPLY, DIVIDE, COMPUTE) 방법을 배운다. 금액을 다루는 프로그램에서는 값이 어디로 옮겨지고 어디에서 잘리는지가 곧 결과의 정확성이다. 그래서 이 장은 문법보다 "자리"에 무게를 둔다.
- MOVE 가 숫자 항목과 문자 항목을 각각 어떤 기준으로 맞추는지 설명할 수 있다.
- ADD, SUBTRACT, MULTIPLY, DIVIDE 의 GIVING 형식과 GIVING 이 없는 형식의 차이를 구분한다.
- COMPUTE 로 수식을 한 문장에 쓰고 연산자 우선순위를 적용한다.
- ROUNDED 로 반올림하고, 쓰지 않으면 버림이 일어남을 확인한다.
- ON SIZE ERROR 로 자릿수 넘침과 0 으로 나누기를 감지한다.
문제 상황
작은 신용협동조합의 야간 배치가 하루 거래를 반영한다고 하자. 어느 회원의 잔액이 1,250,000.50 원이고, 오늘 300,000.25 원이 입금되고 450,000.00 원이 출금되었다. 반영한 잔액에 연 2.75% 이자율을 적용해 연 이자를 구하고, 이를 365 로 나누어 하루치 이자를 계산한다. 그리고 이자소득세 15.4% 를 뺀 세후 이자까지 구한다.
계산 자체는 초등학교 산수다. 어려운 곳은 컴퓨터의 칸이 고정되어 있다는 점이다. 하루치 이자는 82.876767… 처럼 소수가 끝없이 이어지지만 받는 칸은 소수 둘째 자리까지만 있다. 잔액 칸이 정수 5자리인데 합계가 6자리가 되면 어떻게 되는가. 이런 일은 오류 메시지 없이 조용히 일어나서, 모르고 지나가면 장부가 어긋난다. 이 장은 그 규칙을 하나씩 확인한다.
MOVE 의 자리 맞춤
MOVE 는 보내는 항목의 값을 받는 항목에 복사한다. 이때 값을 "그대로" 붙이는 것이 아니라, 받는 항목의 PIC 에 맞춰 자리를 다시 배치한다. 규칙은 받는 항목의 종류에 따라 갈린다.
숫자 항목: 소수점을 기준으로 맞춘다
숫자 항목끼리의 MOVE 는 가상의 소수점(PIC 의 V 위치)을 맞춰 놓고 자리별로 옮긴다. 받는 쪽에 자리가 남으면 0 으로 채운다. 받는 쪽에 자리가 모자라면 넘친 자리를 버린다. 정수부는 높은 자리, 곧 왼쪽이 잘리고 소수부는 낮은 자리, 곧 오른쪽이 잘린다. 반올림은 하지 않는다.
그림에서 원본 12345.67 은 소수점을 기준으로 놓인다. 넓은 항목에는 앞뒤가 0 으로 채워지고, 좁은 항목에서는 왼쪽의 1, 2 와 오른쪽의 7 이 사라진다. 사라진 자리에 대한 경고나 오류는 없다.
문자 항목: 왼쪽 정렬, 남으면 공백
문자 항목(PIC X)은 소수점이 없으므로 왼쪽 끝을 맞춘다. 받는 쪽이 더 길면 오른쪽 남은 칸을 공백으로 채우고, 더 짧으면 오른쪽이 잘린다. 숫자와는 잘리는 방향이 반대다. 숫자는 낮은 자리 쪽을 살리려고 왼쪽을 자르고, 문자는 앞글자를 살리려고 오른쪽을 자른다고 기억하면 쉽다.
| 받는 항목 | 맞추는 기준 | 남는 칸 | 모자랄 때 |
|---|---|---|---|
| 숫자 (PIC 9, S9) | 소수점 위치 | 앞뒤를 0 으로 채움 | 정수부는 왼쪽, 소수부는 오른쪽을 버림 |
| 문자 (PIC X) | 왼쪽 끝 | 오른쪽을 공백으로 채움 | 오른쪽을 버림 |
한 가지 더 알아 둘 점이 있다. 부호 있는 항목(PIC S9)의 값을 부호 없는 항목(PIC 9)으로 옮기면 부호가 사라지고 크기만 남는다. -300 이 300 이 되는 것이다. 받는 항목이 부호를 담을 수 없기 때문이다.
사칙연산 문장과 COMPUTE
GIVING 이 있는 형식과 없는 형식
COBOL 의 사칙연산 동사는 영어 문장처럼 읽힌다. 핵심은 결과가 어느 항목에 저장되는가다. GIVING 이 없으면 마지막에 적은 항목의 값이 바뀐다. GIVING 이 있으면 GIVING 뒤의 항목에 결과를 저장하고 피연산자는 그대로 둔다. 원래 값을 남겨야 하는 장부 계산에서는 GIVING 형식이 안전하다.
| 문장 | 계산 | 결과가 들어가는 항목 |
|---|---|---|
| ADD A TO B | B + A | B |
| ADD A B GIVING C | A + B | C |
| SUBTRACT A FROM B | B - A | B |
| SUBTRACT A FROM B GIVING C | B - A | C |
| MULTIPLY A BY B GIVING C | A × B | C |
| DIVIDE A BY B GIVING C | A ÷ B | C |
| DIVIDE A INTO B GIVING C | B ÷ A | C |
SUBTRACT 는 "A 를 B 에서 뺀다"이므로 FROM 뒤가 먼저 오는 수다. DIVIDE 는 BY 와 INTO 에서 나뉘는 수와 나누는 수의 위치가 서로 반대다. BY 앞이 나뉘는 수, INTO 앞이 나누는 수다. 이 순서는 뒤의 "자주 틀리는 것"에서 다시 다룬다.
COMPUTE: 수식 한 줄
COMPUTE 는 일반 수식을 쓸 수 있다. 연산자는 + - * / 와 거듭제곱 ** 이고, 곱셈·나눗셈이 덧셈·뺄셈보다 먼저 계산되며 괄호로 순서를 바꿀 수 있다. 등호 양옆, 연산자 양옆에는 반드시 공백을 둔다. 공백 없이 쓰면 A-B 가 하나의 이름으로 읽힐 수 있기 때문이다. 이 장의 예제는 세후 이자를 이렇게 구한다.
COMPUTE WS-NET-INT ROUNDED
= WS-YEAR-INT - WS-YEAR-INT * 0.154
곱셈이 먼저 계산되어 이자소득세가 나오고, 거기서 연 이자를 뺀다. 여러 단계를 거치는 계산은 중간 결과를 별도 항목에 담지 말고 COMPUTE 한 문장에 쓰는 편이 정확하다. 중간 항목에 담을 때마다 자릿수가 잘리기 때문이다.
ROUNDED: 버림 대신 반올림
결과를 받는 항목의 소수 자리가 모자라면 기본 동작은 버림이다. 항목 이름 뒤에 ROUNDED 를 붙이면 마지막 남는 자리에서 반올림한다. 하루 이자 82.876767… 은 기본으로 82.87, ROUNDED 를 붙이면 82.88 이 된다. 반올림 규칙은 0.5 이상을 올리는 일반적인 방식이다.
ON SIZE ERROR: 넘침을 알아채기
계산 결과의 정수부가 받는 항목보다 길거나 0 으로 나누면 크기 오류(size error)가 된다. 연산 문장에 ON SIZE ERROR 를 쓰지 않으면 넘친 높은 자리는 조용히 버려진다. 99,000.00 에 2,000.00 을 더해 101,000.00 이 되었는데 받는 항목이 정수 5자리라면 01000.00 이 저장된다. ON SIZE ERROR 를 쓰면 받는 항목의 값은 바뀌지 않고 그 아래 문장이 실행된다. 정상일 때 실행할 문장은 NOT ON SIZE ERROR 에 쓴다. 문장의 끝은 END-ADD, END-DIVIDE 같은 끝맺음 낱말로 표시한다.
MOVE 에는 ON SIZE ERROR 가 없다. 자릿수가 모자란 MOVE 는 감지할 방법이 없으므로, 받는 항목의 PIC 를 처음부터 충분히 크게 설계하는 일이 먼저다.
완성 코드
아래 프로그램은 앞에서 설명한 규칙을 차례로 실행해 본다. GnuCOBOL 에서 숫자 항목을 DISPLAY 하면 V 자리에 점이 찍혀 나오지만, 기억 장소에 점이 저장되는 것은 아니다. 점과 쉼표를 넣어 읽기 좋게 만드는 방법은 다음 장에서 다루므로, 여기서는 출력의 끝 두 자리나 세 자리가 소수부라고 읽으면 된다.
main.cob
IDENTIFICATION DIVISION.
PROGRAM-ID. MAIN.
DATA DIVISION.
WORKING-STORAGE SECTION.
* MOVE 실험용 항목
01 WS-SRC-AMT PIC 9(5)V99 VALUE 12345.67.
01 WS-WIDE PIC 9(7)V9(3).
01 WS-NARROW PIC 9(3)V9.
01 WS-INT-ONLY PIC 9(4).
01 WS-SRC-NAME PIC X(10) VALUE "HAN SOYEON".
01 WS-LONG-NAME PIC X(15).
01 WS-SHORT-NAME PIC X(6).
* 잔액 계산용 항목
01 WS-BALANCE PIC 9(7)V99 VALUE 1250000.50.
01 WS-DEPOSIT PIC 9(7)V99 VALUE 300000.25.
01 WS-WITHDRAW PIC 9(7)V99 VALUE 450000.00.
01 WS-NEW-BAL PIC 9(7)V99.
01 WS-AFTER-BAL PIC 9(7)V99.
* 이자 계산용 항목
01 WS-RATE PIC 9V9999 VALUE 0.0275.
01 WS-DAYS PIC 999 VALUE 365.
01 WS-YEAR-INT PIC 9(7)V99.
01 WS-DAY-TRUNC PIC 9(5)V99.
01 WS-DAY-ROUND PIC 9(5)V99.
01 WS-NET-INT PIC 9(7)V99.
* 크기 오류 실험용 항목
01 WS-LIMIT PIC 9(5)V99 VALUE 99000.00.
01 WS-ADD-AMT PIC 9(5)V99 VALUE 2000.00.
01 WS-WRAP PIC 9(5)V99.
01 WS-GUARD PIC 9(5)V99.
01 WS-ZERO PIC 999 VALUE 0.
PROCEDURE DIVISION.
MAIN-PARA.
* 1) MOVE 의 자리 맞춤
MOVE WS-SRC-AMT TO WS-WIDE
MOVE WS-SRC-AMT TO WS-NARROW
MOVE WS-SRC-AMT TO WS-INT-ONLY
DISPLAY "SRC: " WS-SRC-AMT
DISPLAY "WIDE: " WS-WIDE
DISPLAY "NARROW: " WS-NARROW
DISPLAY "INT-ONLY: " WS-INT-ONLY
MOVE WS-SRC-NAME TO WS-LONG-NAME
MOVE WS-SRC-NAME TO WS-SHORT-NAME
DISPLAY "LONG: [" WS-LONG-NAME "]"
DISPLAY "SHORT: [" WS-SHORT-NAME "]"
* 2) 입금과 출금 반영
ADD WS-BALANCE WS-DEPOSIT GIVING WS-NEW-BAL
SUBTRACT WS-WITHDRAW FROM WS-NEW-BAL
GIVING WS-AFTER-BAL
DISPLAY "NEW-BAL: " WS-NEW-BAL
DISPLAY "AFTER: " WS-AFTER-BAL
* 3) 이자 계산: 곱하기, 나누기, ROUNDED
MULTIPLY WS-AFTER-BAL BY WS-RATE GIVING WS-YEAR-INT
DIVIDE WS-YEAR-INT BY WS-DAYS GIVING WS-DAY-TRUNC
DIVIDE WS-YEAR-INT BY WS-DAYS GIVING WS-DAY-ROUND
ROUNDED
DISPLAY "YEAR-INT: " WS-YEAR-INT
DISPLAY "DAY-TRUNC: " WS-DAY-TRUNC
DISPLAY "DAY-ROUND: " WS-DAY-ROUND
* 4) COMPUTE 로 세후 이자
COMPUTE WS-NET-INT ROUNDED
= WS-YEAR-INT - WS-YEAR-INT * 0.154
DISPLAY "NET-INT: " WS-NET-INT
* 5) 크기 오류
MOVE WS-LIMIT TO WS-WRAP
ADD WS-ADD-AMT TO WS-WRAP
DISPLAY "WRAP: " WS-WRAP
MOVE WS-LIMIT TO WS-GUARD
ADD WS-ADD-AMT TO WS-GUARD
ON SIZE ERROR
DISPLAY "GUARD: SIZE ERROR"
NOT ON SIZE ERROR
DISPLAY "GUARD: OK"
END-ADD
DISPLAY "GUARD-VAL: " WS-GUARD
DIVIDE WS-YEAR-INT BY WS-ZERO GIVING WS-DAY-ROUND
ON SIZE ERROR
DISPLAY "ZERO: DIVISOR IS ZERO"
END-DIVIDE
DISPLAY "DAY-ROUND AFTER: " WS-DAY-ROUND
STOP RUN.
줄별 해설
DATA DIVISION 의 항목들. 금액 항목은 PIC 9(7)V99 로 통일했다. 정수 7자리에 소수 2자리라서 9,999,999.99 까지 담는다. 이자율 WS-RATE 는 PIC 9V9999 이므로 0.0275 처럼 소수 4자리까지 표현한다. 일수 WS-DAYS 는 소수 없는 3자리다. 이름 항목은 PIC X(10), X(15), X(6) 세 개로, 보내는 쪽이 받는 쪽보다 짧거나 긴 경우를 모두 만든다.
1) MOVE. WS-SRC-AMT 의 값 12345.67 을 세 항목에 옮긴다. 소수 3자리가 있는 WS-WIDE 에서는 0012345.670, 정수 3자리에 소수 1자리인 WS-NARROW 에서는 345.6 이다. 이 6 은 6.7 을 반올림한 7 이 아니라 7 을 버린 결과다. WS-INT-ONLY 는 소수부가 없고 정수 4자리라 2345 가 된다. 이름은 WS-LONG-NAME 에서 오른쪽에 공백 5칸이 붙고, WS-SHORT-NAME 에서는 앞 6글자만 남는다. 공백이 눈에 보이도록 DISPLAY 에서 대괄호로 감쌌다.
2) 입금과 출금. ADD WS-BALANCE WS-DEPOSIT GIVING WS-NEW-BAL 은 두 값을 더해 WS-NEW-BAL 에 담고 WS-BALANCE, WS-DEPOSIT 는 건드리지 않는다. 이어지는 SUBTRACT 는 WS-NEW-BAL 에서 출금액을 뺀 값을 WS-AFTER-BAL 에 따로 담는다. 문장이 두 줄에 걸치는 것은 72열을 넘지 않으려는 것이고, 문법상 줄바꿈은 아무 곳에나 해도 된다.
3) 이자. MULTIPLY 는 1,100,000.75 × 0.0275 = 30,250.020625 를 계산하지만 WS-YEAR-INT 는 소수 2자리라 30250.02 가 저장된다. 이어서 같은 나눗셈을 두 번 한다. 첫 번째는 ROUNDED 가 없어 82.87 이 되고, 두 번째는 ROUNDED 가 있어 82.88 이 된다. 정확한 값은 82.87676…이다.
4) COMPUTE. 30250.02 × 0.154 = 4658.50308 이고 30250.02 에서 이를 빼면 25591.51692 다. 소수 셋째 자리 이하는 6 이상이 이어지므로 ROUNDED 로 25591.52 가 된다. 이 계산이 한 문장 안에서 정확하게 이뤄지기 때문에 중간값 4658.50308 이 그대로 쓰인다. ROUNDED 가 없었다면 25591.51 이다.
5) 크기 오류. WS-WRAP 은 ON SIZE ERROR 없이 99000.00 + 2000.00 = 101000.00 을 받는다. 정수 5자리에 담기지 않으므로 맨 앞의 1 이 버려져 01000.00 이 저장된다. WS-GUARD 는 같은 계산에 ON SIZE ERROR 를 달았다. 넘침이 감지되어 첫 번째 DISPLAY 가 실행되고 WS-GUARD 는 원래 값 99000.00 으로 남는다. 마지막 DIVIDE 는 나누는 수가 0 이어서 크기 오류가 되고, WS-DAY-ROUND 는 앞의 값 82.88 을 유지한다.
실행 결과
$ cobc -x -Wall main.cob
$ ./main
SRC: 12345.67
WIDE: 0012345.670
NARROW: 345.6
INT-ONLY: 2345
LONG: [HAN SOYEON ]
SHORT: [HAN SO]
NEW-BAL: 1550000.75
AFTER: 1100000.75
YEAR-INT: 0030250.02
DAY-TRUNC: 00082.87
DAY-ROUND: 00082.88
NET-INT: 0025591.52
WRAP: 01000.00
GUARD: SIZE ERROR
GUARD-VAL: 99000.00
ZERO: DIVISOR IS ZERO
DAY-ROUND AFTER: 00082.88
GnuCOBOL 은 숫자 항목을 DISPLAY 할 때 V 자리에 점을 찍어 보여 준다. 예를 들어 NEW-BAL 은 정수 7자리와 소수 2자리라서 1550000.75 로 나온다. IBM 메인프레임 COBOL 은 같은 값을 점 없이 155000075 처럼 자릿수만 이어서 내보내므로, 출력 모양은 컴파일러에 따라 다르다. 어느 쪽이든 기억 장소에 점이 저장되지는 않는다.
실무에서 자주 틀리는 것
DIVIDE 의 BY 와 INTO 를 거꾸로 쓴다
연 이자를 일수로 나누려고 했는데 순서가 뒤집혔다.
DIVIDE WS-DAYS BY WS-YEAR-INT GIVING WS-DAY-ROUND
이 문장은 365 ÷ 30250.02 를 계산해 0.01 이 된다. 오류도 경고도 없다. BY 앞이 나뉘는 수이므로 순서를 바꾼다.
DIVIDE WS-YEAR-INT BY WS-DAYS GIVING WS-DAY-ROUND
받는 항목에 소수 자리를 주지 않는다
이자를 PIC 9(7) 로 선언하면 소수가 전부 버려진다.
01 WS-YEAR-INT PIC 9(7).
...
MULTIPLY WS-AFTER-BAL BY WS-RATE GIVING WS-YEAR-INT
결과는 30250 이고 0.02 원이 사라진다. 이 오차는 회원 수만큼 쌓인다. 소수 자리를 설계 단계에서 정해 둔다.
01 WS-YEAR-INT PIC 9(7)V99.
크기 오류를 감지하지 않는다
합계 항목이 합계를 담을 만큼 크지 않은데 ON SIZE ERROR 도 없으면, 높은 자리가 조용히 사라진다.
ADD WS-ADD-AMT TO WS-WRAP
99000.00 에 2000.00 을 더하면 01000.00 이 된다. 넘칠 수 있는 합계에는 항목을 한 자리 넓히고, 한계가 있는 곳에는 ON SIZE ERROR 로 처리 경로를 둔다.
ADD WS-ADD-AMT TO WS-GUARD
ON SIZE ERROR
DISPLAY "GUARD: SIZE ERROR"
END-ADD
부호 있는 값을 부호 없는 항목으로 옮긴다
출금이 입금보다 큰 날의 차액을 담으려는 항목에 부호가 없다.
01 WS-DIFF PIC 9(5)V99.
...
SUBTRACT WS-WITHDRAW FROM WS-DEPOSIT GIVING WS-DIFF
입금 1,000 에서 출금 1,500 을 빼면 -500 이어야 하지만 부호 없는 항목에는 500.00 이 저장된다. 음수가 나올 수 있는 항목은 PIC S9(5)V99 처럼 S 를 붙여 선언한다.
01 WS-DIFF PIC S9(5)V99.
한눈에 보기
| 주제 | 규칙 | 예 | 주의 |
|---|---|---|---|
| 숫자 MOVE | 소수점 기준 정렬 | 12345.67 → 9(3)V9 는 345.6 | 반올림 없이 버림 |
| 문자 MOVE | 왼쪽 정렬, 오른쪽 공백 | HAN SOYEON → X(6) 은 HAN SO | 짧으면 오른쪽이 잘림 |
| GIVING | 결과를 GIVING 항목에 저장 | ADD A B GIVING C | A, B 는 그대로 |
| DIVIDE | BY 앞이 나뉘는 수 | DIVIDE A BY B | INTO 는 반대 순서 |
| COMPUTE | 수식 한 줄, 곱셈 먼저 | X = A - A * 0.154 | 연산자 양옆 공백 |
| ROUNDED | 마지막 자리에서 반올림 | 82.8767 → 82.88 | 없으면 82.87 |
| ON SIZE ERROR | 넘침과 0 나누기 감지 | 99000 + 2000 → 5자리 초과 | 없으면 높은 자리 소실, MOVE 에는 쓸 수 없음 |
연습 문제
- WS-A 가 PIC 9(4)V99 이고 값이 8765.43 이다. 이를 PIC 9(3)V9 와 PIC 9(6)V9(3) 항목에 각각 MOVE 하면 DISPLAY 결과는 어떻게 나오는가.
- PIC X(8) 항목에 SAVINGS 가 들어 있다. PIC X(4) 와 PIC X(10) 항목에 MOVE 한 결과를 대괄호를 붙여 적어 보라.
- 출금액 WS-WITHDRAW 가 1999.99 이고 수수료율 WS-FEE-RATE 가 0.0015 다. 수수료를 PIC 9(5)V99 인 WS-FEE 에 구하되 소수 셋째 자리에서 반올림하는 COMPUTE 문장을 쓰고, 결과를 적어 보라. ROUNDED 를 빼면 결과는 어떻게 되는가.
- PIC 9(3)V99 항목 WS-X 에 999.99 가 들어 있다. 여기에 0.01 을 더하는 ADD 를 ON SIZE ERROR 없이 썼을 때와 썼을 때의 WS-X 값을 비교하라.
정답과 해설
- 9(3)V9 는 정수부 8765 의 높은 자리 8 이 잘리고 소수부 43 의 3 이 버려져 7654 가 나온다. 9(6)V9(3) 은 정수부가 008765, 소수부가 430 이므로 008765430 이다. 정수부는 앞에서, 소수부는 뒤에서 0 이 채워지고 소수점 위치가 기준이다.
- PIC X(4) 는 [SAVI], PIC X(10) 은 [SAVINGS ] 다. 문자는 왼쪽 정렬이므로 짧으면 오른쪽이 잘리고 길면 오른쪽에 공백 3칸이 붙는다.
- COMPUTE WS-FEE ROUNDED = WS-WITHDRAW * WS-FEE-RATE 이다. 정확한 값은 2.999985 이므로 ROUNDED 로 3.00 이고 DISPLAY 하면 0000300 이다. ROUNDED 를 빼면 소수 셋째 자리 이하가 버려져 2.99 가 된다.
- ON SIZE ERROR 가 없으면 결과 1000.00 이 정수 3자리에 담기지 않아 높은 자리가 버려지고 WS-X 는 000.00 이 된다. ON SIZE ERROR 가 있으면 WS-X 는 999.99 로 바뀌지 않고 크기 오류 처리 문장이 실행된다. 조용히 0 이 되는 쪽이 장부에서는 훨씬 위험하다.
PIC, MOVE, 사칙연산의 동작은 GnuCOBOL 프로그래머 안내서에서 다시 확인할 수 있다: GnuCOBOL 공식 사이트.