COBOL · 기본
데이터 처리로 배우는 COBOL
순차 파일 읽기 - 레코드 단위 처리
SELECT·ASSIGN, FILE SECTION 과 FD, 고정 길이 레코드 배치, OPEN INPUT·READ AT END·CLOSE, FILE STATUS 확인, 읽기 루프의 표준 모양(선행 읽기)
개발자KR · 원고 갱신
이 장에서 배우는 것
앞 장까지 만든 프로그램은 숫자와 문자를 프로그램 안에 직접 적어 두고 처리했다. 실제 일괄 처리는 그렇지 않다. 그날의 거래는 파일에 담겨 도착하고, 프로그램은 그 파일을 처음부터 끝까지 한 건씩 읽는다. 이 장에서는 거래 파일 한 개를 열어 레코드(record, 파일에서 한 건을 이루는 데이터 묶음)를 하나씩 읽고, 입금과 출금을 나누어 합계를 내는 프로그램을 만든다.
- SELECT 와 ASSIGN 으로 프로그램 안의 파일 이름을 실제 파일에 연결한다.
- FILE SECTION 의 FD 아래에 고정 길이 레코드의 칸 배치를 정의한다.
- OPEN INPUT, READ ... AT END, CLOSE 를 올바른 순서로 쓴다.
- FILE STATUS 값을 확인해 파일이 없거나 읽기가 실패한 경우를 알아챈다.
- 선행 읽기(priming read)를 쓰는 읽기 루프의 표준 모양을 익힌다.
문제 상황
작은 신용협동조합의 전산 담당자가 되었다고 하자. 지점 창구에서 받은 입금과 출금은 하루 동안 쌓였다가, 마감 후 trans.dat 라는 파일 하나로 모인다. 한 줄이 거래 한 건이다. 맡은 일은 이 파일을 읽어 입금 건수와 출금 건수, 각 합계를 확인하는 것이다.
이 일에는 세 가지 어려움이 있다. 첫째, 파일에는 글자만 이어져 있을 뿐이라 어디까지가 계좌 번호이고 어디부터가 금액인지는 프로그램이 알려 주어야 한다. 둘째, 거래가 몇 건인지 미리 알 수 없으므로 끝에 닿을 때까지 읽어야 한다. 셋째, 파일이 없거나 이름이 틀린 경우에도 프로그램은 조용히 잘못된 결과를 내면 안 된다. 이 장의 내용은 이 세 가지를 차례로 해결한다.
파일을 프로그램에 연결하기
SELECT 와 ASSIGN
COBOL 프로그램은 파일을 두 단계로 다룬다. 먼저 ENVIRONMENT DIVISION 에서 "프로그램 안에서 부를 이름"과 "실제 파일"을 연결하고, 다음으로 DATA DIVISION 에서 그 파일 한 줄의 모양을 정의한다. 연결은 INPUT-OUTPUT SECTION 의 FILE-CONTROL 문단에 쓴다.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT TRANS-FILE ASSIGN TO "trans.dat"
ORGANIZATION IS LINE SEQUENTIAL
FILE STATUS IS WS-FS.
TRANS-FILE 은 프로그램 안에서만 쓰는 이름이다. ASSIGN TO "trans.dat" 는 디스크의 실제 파일 이름이며, 상대 경로이므로 프로그램을 실행하는 폴더를 기준으로 찾는다. ORGANIZATION IS LINE SEQUENTIAL 은 파일이 줄 단위의 텍스트이고 앞에서부터 차례로만 읽을 수 있다는 뜻이다. 이 책의 데이터 파일은 모든 줄의 길이가 같으므로 고정 길이 레코드로 다룬다. FILE STATUS IS WS-FS 는 파일 작업이 끝날 때마다 결과 코드 두 글자를 WS-FS 에 넣어 달라는 요청이다. WS-FS 는 WORKING-STORAGE 에서 PIC XX 로 직접 정의해야 한다.
FILE SECTION 과 FD
FILE SECTION 에는 파일마다 FD(File Description, 파일 기술)를 하나씩 쓰고, 그 아래에 레코드 구조를 둔다. 앞 장에서 배운 그룹 항목과 PIC 를 그대로 쓰면 된다.
FILE SECTION.
FD TRANS-FILE.
01 TRANS-REC.
05 TR-ACCT PIC 9(6).
05 TR-TYPE PIC X.
05 TR-AMOUNT PIC 9(9).
05 TR-DATE PIC 9(8).
05 TR-BRANCH PIC X(4).
TRANS-REC 는 READ 가 성공할 때마다 새 줄의 내용으로 채워지는 입력 영역이다. 별도로 MOVE 하지 않아도 READ 가 한 줄을 이 영역에 펼쳐 놓는다.
고정 길이 레코드 배치
고정 길이 레코드에서는 각 필드가 몇 번째 글자에서 시작해 몇 글자를 차지하는지가 곧 파일의 규칙이다. 필드의 PIC 길이를 앞에서부터 더한 값이 한 줄의 길이이고, 이 합이 데이터 파일의 실제 줄 길이와 같아야 한다. 이 장의 거래 레코드는 다음과 같다.
| 필드 | PIC | 위치 | 뜻 |
|---|---|---|---|
| TR-ACCT | 9(6) | 1~6 | 계좌 번호 |
| TR-TYPE | X | 7 | D 는 입금, W 는 출금 |
| TR-AMOUNT | 9(9) | 8~16 | 금액(원 단위, 앞을 0으로 채움) |
| TR-DATE | 9(8) | 17~24 | 거래 일자(YYYYMMDD) |
| TR-BRANCH | X(4) | 25~28 | 지점 코드 |
6+1+9+8+4 는 28이다. 아래 그림은 같은 배치를 폭에 비례하게 그리고, 실제 한 줄이 어떻게 나뉘는지 보여 준다.
읽기 문장 - OPEN, READ, CLOSE
세 문장의 순서
파일을 읽는 일은 항상 열기, 읽기, 닫기의 순서로 진행된다.
OPEN INPUT TRANS-FILE은 파일을 읽기 전용으로 연다. 열기 전에는 READ 를 할 수 없다.READ TRANS-FILE은 다음 한 줄을TRANS-REC로 가져온다. 더 읽을 줄이 없으면AT END아래의 문장이 실행된다. 읽기에 성공했을 때만 실행할 문장은NOT AT END아래에 쓴다.CLOSE TRANS-FILE은 파일을 닫는다. 작업이 끝나면 반드시 닫는다.
READ 는 매번 파일의 다음 줄로 한 칸씩 나아갈 뿐 되돌아가지 못한다. 순차 파일(sequential file)이라는 이름이 여기서 나왔다.
FILE STATUS 확인
FILE STATUS 를 지정하면 열기, 읽기, 닫기의 결과가 두 글자 코드로 남는다. 이 코드를 확인하지 않으면 실패해도 프로그램은 계속 진행한다. 자주 만나는 값은 다음과 같다.
| 값 | 뜻 | 보통의 대응 |
|---|---|---|
| 00 | 작업 성공 | 계속 진행 |
| 10 | 파일 끝에 닿음 | 읽기 루프 종료 |
| 35 | 열려는 파일이 없음 | 이름과 실행 폴더 확인 후 중단 |
| 46 | 끝에 닿은 뒤 다시 READ | 루프 구조 점검 |
이 장의 프로그램은 OPEN 직후에 00 이 아니면 중단하고, READ 직후에는 00 과 10 이외의 값이면 중단한다. 파일 끝(10)은 오류가 아니라 정상적인 종료 신호이므로 걸러 내면 안 된다.
읽기 루프의 표준 모양
끝까지 읽는 반복을 쓰는 방법은 여러 가지처럼 보이지만, 안전한 모양은 하나다. 루프에 들어가기 전에 READ 를 한 번 먼저 하고(선행 읽기), 루프 안에서는 처리를 먼저 한 뒤 맨 끝에서 다음 READ 를 한다. 루프의 조건은 "끝에 닿았는가"이다.
PERFORM READ-TRANS
PERFORM UNTIL WS-EOF
PERFORM PROCESS-TRANS
PERFORM READ-TRANS
END-PERFORM
이렇게 하면 루프 몸체가 실행되는 시점에는 항상 유효한 레코드가 입력 영역에 들어 있다. 빈 파일이어도 선행 읽기에서 곧바로 끝이 확인되어 몸체가 한 번도 실행되지 않는다. READ 문장이 프로그램 안에 한 군데뿐이므로 AT END 처리와 상태 검사도 한 군데에 모을 수 있다. 아래 그림은 이 흐름을 나타낸다.
끝 여부는 앞 장에서 다룬 조건 이름(88 레벨)으로 표현했다. WS-EOF-FLAG 에 "Y"가 들어 있으면 WS-EOF 가 참이 되고, SET WS-EOF TO TRUE 가 그 값을 넣는다.
완성 코드
아래 두 파일을 같은 폴더에 둔다. 데이터 파일의 각 줄은 28글자이며, 줄 끝에 개행 문자가 하나씩 있어야 한다.
trans.dat
100001D00005000020261001SEOL
100002W00001200020261001BUSN
100001W00002000020261001SEOL
100003D00012000020261001DAEG
100002D00000300020261001BUSN
main.cob
IDENTIFICATION DIVISION.
PROGRAM-ID. MAIN.
ENVIRONMENT DIVISION.
INPUT-OUTPUT SECTION.
FILE-CONTROL.
SELECT TRANS-FILE ASSIGN TO "trans.dat"
ORGANIZATION IS LINE SEQUENTIAL
FILE STATUS IS WS-FS.
DATA DIVISION.
FILE SECTION.
FD TRANS-FILE.
01 TRANS-REC.
05 TR-ACCT PIC 9(6).
05 TR-TYPE PIC X.
05 TR-AMOUNT PIC 9(9).
05 TR-DATE PIC 9(8).
05 TR-BRANCH PIC X(4).
WORKING-STORAGE SECTION.
01 WS-FS PIC XX.
01 WS-EOF-FLAG PIC X VALUE "N".
88 WS-EOF VALUE "Y".
01 WS-COUNTERS.
05 WS-READ-CNT PIC 9(3) VALUE 0.
05 WS-DEP-CNT PIC 9(3) VALUE 0.
05 WS-WDR-CNT PIC 9(3) VALUE 0.
05 WS-BAD-CNT PIC 9(3) VALUE 0.
01 WS-TOTALS.
05 WS-DEP-TOT PIC 9(9) VALUE 0.
05 WS-WDR-TOT PIC 9(9) VALUE 0.
01 WS-AMT-EDIT PIC ZZZ,ZZZ,ZZ9.
01 WS-CNT-EDIT PIC ZZ9.
PROCEDURE DIVISION.
MAIN-PARA.
OPEN INPUT TRANS-FILE
IF WS-FS NOT = "00"
DISPLAY "OPEN FAILED, STATUS=" WS-FS
STOP RUN
END-IF
DISPLAY "--- DAILY TRANSACTIONS ---"
PERFORM READ-TRANS
PERFORM UNTIL WS-EOF
PERFORM PROCESS-TRANS
PERFORM READ-TRANS
END-PERFORM
DISPLAY "LAST STATUS : " WS-FS
CLOSE TRANS-FILE
DISPLAY "--- SUMMARY ---"
MOVE WS-READ-CNT TO WS-CNT-EDIT
DISPLAY "RECORDS READ : " WS-CNT-EDIT
MOVE WS-DEP-CNT TO WS-CNT-EDIT
DISPLAY "DEPOSITS : " WS-CNT-EDIT
MOVE WS-WDR-CNT TO WS-CNT-EDIT
DISPLAY "WITHDRAWALS : " WS-CNT-EDIT
MOVE WS-BAD-CNT TO WS-CNT-EDIT
DISPLAY "UNKNOWN TYPE : " WS-CNT-EDIT
MOVE WS-DEP-TOT TO WS-AMT-EDIT
DISPLAY "DEPOSIT TOTAL : " WS-AMT-EDIT
MOVE WS-WDR-TOT TO WS-AMT-EDIT
DISPLAY "WITHDRAW TOTAL: " WS-AMT-EDIT
STOP RUN.
READ-TRANS.
READ TRANS-FILE
AT END
SET WS-EOF TO TRUE
NOT AT END
ADD 1 TO WS-READ-CNT
END-READ
IF WS-FS NOT = "00" AND WS-FS NOT = "10"
DISPLAY "READ FAILED, STATUS=" WS-FS
CLOSE TRANS-FILE
STOP RUN
END-IF.
PROCESS-TRANS.
EVALUATE TR-TYPE
WHEN "D"
ADD 1 TO WS-DEP-CNT
ADD TR-AMOUNT TO WS-DEP-TOT
WHEN "W"
ADD 1 TO WS-WDR-CNT
ADD TR-AMOUNT TO WS-WDR-TOT
WHEN OTHER
ADD 1 TO WS-BAD-CNT
END-EVALUATE
MOVE TR-AMOUNT TO WS-AMT-EDIT
DISPLAY TR-ACCT " " TR-TYPE " " WS-AMT-EDIT " " TR-BRANCH.
줄별 해설
- SELECT ... FILE STATUS IS WS-FS: 프로그램 안의 이름
TRANS-FILE을trans.dat에 연결하고, 작업 결과를WS-FS에 받는다. - FD TRANS-FILE 과 TRANS-REC: 한 줄을 5개 필드로 나눈다. 길이의 합 28이 데이터 파일의 줄 길이와 같다.
- WS-EOF-FLAG 와 88 WS-EOF: 끝에 닿았는지를 기억하는 표지다. 처음에는 "N"이다.
- WS-COUNTERS, WS-TOTALS: 읽은 건수, 입금과 출금 건수, 알 수 없는 구분 건수, 입금과 출금 합계를 담는다. 모두 0으로 시작한다.
- WS-AMT-EDIT, WS-CNT-EDIT: 앞 장에서 배운 숫자 편집 항목이다. 출력 직전에 MOVE 로 값을 옮겨 천 단위 쉼표를 넣고 앞의 0을 지운다.
- OPEN INPUT 과 첫 IF: 열기에 실패하면 상태 코드를 보여 주고 곧바로 끝낸다. 열리지 않은 파일을 읽는 일을 막는다.
- PERFORM READ-TRANS (루프 앞): 선행 읽기다. 첫 번째 거래가 입력 영역에 들어온다.
- PERFORM UNTIL WS-EOF: 끝에 닿을 때까지 처리와 읽기를 번갈아 한다.
- DISPLAY "LAST STATUS": 루프를 빠져나온 직후의 상태 코드를 보여 준다. 끝에 닿았으므로
10이다. - CLOSE: 사용을 마친 파일을 닫는다.
- READ-TRANS 문단: READ 가 이 문단에만 있다.
AT END에서 표지를 켜고,NOT AT END에서 읽은 건수를 센다. 뒤의 IF 는00과10이 아닌 코드를 오류로 보고 파일을 닫고 중단한다. - PROCESS-TRANS 문단: EVALUATE 로 구분 코드를 나눈다. "D"는 입금, "W"는 출금으로 세고 합산하며, 그 밖의 코드는 알 수 없는 구분으로 센다. 마지막에 금액을 편집해 한 줄로 출력한다.
실행 결과
trans.dat 가 있는 폴더에서 컴파일하고 실행한다.
$ cobc -x -Wall main.cob
$ ./main
--- DAILY TRANSACTIONS ---
100001 D 50,000 SEOL
100002 W 12,000 BUSN
100001 W 20,000 SEOL
100003 D 120,000 DAEG
100002 D 3,000 BUSN
LAST STATUS : 10
--- SUMMARY ---
RECORDS READ : 5
DEPOSITS : 3
WITHDRAWALS : 2
UNKNOWN TYPE : 0
DEPOSIT TOTAL : 173,000
WITHDRAW TOTAL: 32,000
입금은 50,000 + 120,000 + 3,000 으로 173,000 원이고, 출금은 12,000 + 20,000 으로 32,000 원이다. 마지막 상태 코드가 10 이라는 점도 확인해 둔다.
실무에서 자주 틀리는 것
1. 선행 읽기 없이 루프 맨 위에서 읽는다
틀린 코드는 루프 안에서 READ 직후 곧바로 처리한다.
PERFORM UNTIL WS-EOF
READ TRANS-FILE
AT END SET WS-EOF TO TRUE
END-READ
PERFORM PROCESS-TRANS
END-PERFORM
마지막 READ 에서 끝이 확인되어도 같은 반복 안에서 PROCESS-TRANS 가 실행된다. 이때 입력 영역의 내용은 믿을 수 없고, 보통은 직전 레코드가 그대로 남아 있어 마지막 거래가 두 번 처리된 것처럼 보인다. 고친 코드는 앞에서 본 표준 모양이다. 루프 앞에서 한 번 읽고, 처리 뒤에 읽는다.
PERFORM READ-TRANS
PERFORM UNTIL WS-EOF
PERFORM PROCESS-TRANS
PERFORM READ-TRANS
END-PERFORM
2. OPEN 결과를 확인하지 않는다
파일 이름이 틀렸는데도 열기 결과를 보지 않는 코드다.
OPEN INPUT TRANS-FILE
PERFORM READ-TRANS
PERFORM UNTIL WS-EOF
PERFORM PROCESS-TRANS
PERFORM READ-TRANS
END-PERFORM
열기는 상태 코드 35 로 실패하지만 프로그램은 계속 진행한다. 열리지 않은 파일을 READ 하면 AT END 조건이 아니라 다른 오류가 나므로 WS-EOF 가 영영 켜지지 않아 반복이 끝나지 않을 수 있다. 열기 직후와 읽기 직후에 상태를 확인한다.
OPEN INPUT TRANS-FILE
IF WS-FS NOT = "00"
DISPLAY "OPEN FAILED, STATUS=" WS-FS
STOP RUN
END-IF
3. 레코드 길이의 합이 데이터와 다르다
금액을 9자리가 아니라 8자리로 잘못 정의한 경우다.
01 TRANS-REC.
05 TR-ACCT PIC 9(6).
05 TR-TYPE PIC X.
05 TR-AMOUNT PIC 9(8).
05 TR-DATE PIC 9(8).
05 TR-BRANCH PIC X(4).
금액은 한 글자가 모자라 앞 8자리만 읽히고, 일자와 지점은 한 칸씩 앞으로 당겨져 엉뚱한 값이 된다. 컴파일러는 데이터 파일을 보지 않으므로 경고도 나오지 않는다. 필드 길이를 더해 줄 길이와 맞는지 확인하고, 고친 코드는 PIC 9(9) 로 되돌린다.
05 TR-AMOUNT PIC 9(9).
4. 다른 폴더에서 실행한다
ASSIGN TO "trans.dat" 는 현재 폴더를 기준으로 파일을 찾는다. 파일이 있는 폴더의 바깥에서 실행하면 열리지 않는다.
$ ./work/main
OPEN FAILED, STATUS=35
프로그램과 데이터 파일이 있는 폴더로 이동한 뒤 실행한다.
$ cd work
$ ./main
한눈에 보기
| 구문 | 놓는 곳 | 역할 | 주의 |
|---|---|---|---|
| SELECT ... ASSIGN TO | FILE-CONTROL | 내부 이름과 실제 파일 연결 | 상대 경로는 실행 폴더 기준 |
| FILE STATUS IS | SELECT 안 | 결과 코드 두 글자를 받음 | PIC XX 로 직접 정의 |
| FD 와 01 레코드 | FILE SECTION | 한 줄의 칸 배치 정의 | 길이의 합이 줄 길이와 같아야 함 |
| OPEN INPUT | PROCEDURE DIVISION | 읽기용으로 열기 | 직후에 00 확인 |
| READ ... AT END | PROCEDURE DIVISION | 다음 레코드 가져오기 | 끝은 상태 10, 오류가 아님 |
| CLOSE | PROCEDURE DIVISION | 파일 닫기 | 종료 전에 반드시 실행 |
| 단계 | 위치 | 하는 일 |
|---|---|---|
| 선행 읽기 | 루프 앞 | 첫 레코드를 가져오고 빈 파일이면 끝을 알아챔 |
| 처리 | 루프 몸체 앞부분 | 입력 영역에 있는 레코드를 계산하고 출력 |
| 다음 읽기 | 루프 몸체 끝 | 다음 레코드를 가져오거나 끝 표지를 켬 |
연습 문제
- 지점 코드가 "SEOL" 인 거래가 몇 건인지 세어, 요약 끝에
SEOL COUNT :형태로 출력하도록 고쳐 보라. 어떤 문장을 어디에 추가해야 하는가. - 입금 거래 중 가장 큰 금액을 구해 요약에 출력하려 한다. 필요한 변수와 PROCESS-TRANS 의 변경 부분을 써 보라.
- 선행 읽기를 지우고 루프 안에서 PROCESS-TRANS 를 먼저 실행한 뒤 READ-TRANS 를 하도록 순서만 바꾸지 않고, 첫 줄의
PERFORM READ-TRANS만 삭제하면 처음 반복에서 무슨 일이 일어나는지 설명하라. ASSIGN TO "trans.dta"로 오타를 냈을 때 실행 결과는 어떻게 되는가. 그리고 프로그램이 왜 끝까지 안전하게 종료되는지 설명하라.
정답과 해설
- WORKING-STORAGE 에
01 WS-SEOL-CNT PIC 9(3) VALUE 0.을 추가하고, PROCESS-TRANS 안에IF TR-BRANCH = "SEOL"아래ADD 1 TO WS-SEOL-CNT를 둔다. 요약에서는MOVE WS-SEOL-CNT TO WS-CNT-EDIT후DISPLAY "SEOL COUNT : " WS-CNT-EDIT로 출력한다. 이 데이터에서는 2건이다. 조건 검사는 읽은 레코드마다 해야 하므로 처리 문단 안에 둔다. 01 WS-MAX-DEP PIC 9(9) VALUE 0.을 추가하고, EVALUATE 의 "D" 가지에IF TR-AMOUNT > WS-MAX-DEP아래MOVE TR-AMOUNT TO WS-MAX-DEP를 넣는다. 요약에서는WS-AMT-EDIT로 옮겨 출력한다. 이 데이터의 결과는 120,000 이다. 초깃값을 0으로 두면 첫 입금이 반드시 최댓값으로 바뀐다.- 선행 읽기가 없으면 첫 반복에서 아직 한 줄도 읽히지 않은 입력 영역을 처리하게 된다. 영역의 내용은 정해지지 않았으므로 구분 코드는 "D"도 "W"도 아닐 가능성이 크고, 알 수 없는 구분으로 세어진다. 그 뒤 READ 가 첫 거래를 가져오지만, 반복 순서가 어긋나 있으면 마지막 거래 처리 시점도 함께 어긋난다. 읽은 값을 처리하기 전에 반드시 READ 가 한 번 있어야 한다는 것이 선행 읽기의 이유다.
- 열기에서 상태 코드
35가 나오고WS-FS NOT = "00"이 참이 되어OPEN FAILED, STATUS=35를 출력한 뒤STOP RUN으로 끝난다. READ 를 한 번도 실행하지 않으므로 끝나지 않는 반복도 생기지 않는다. 열기 직후의 상태 확인이 이 안전장치의 전부다.
READER FEEDBACK
질문·의견
내용에 관한 질문이나 더 나은 설명을 위한 의견을 남겨 주세요. 오탈자는 위의 제보 양식이 더 빨리 반영됩니다. 이 댓글은 원래 게시글과 같은 자리에 쌓입니다.
댓글 0
아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.