이 글에서는 짧은 bash shell script 프로그램을 작성하여 실행해보도록 하겠다.

이 프로그램은 두 정수를 입력받아 두 정수 사이에 속하는 모든 소수의 합을 구하는 프로그램이다. 코드는 다음과 같다.

#!/bin/bash
let rst=0
echo -n "type two integers : "
read x y
if [ $x == $y ] #만약 두 숫자가 같다면 x, y사이에 값이 없으므로 0을 출력하고 종료
then
    echo 0
    exit 1
fi
if [ $x -gt $y ] //x보다 y가 작다면 둘이 숫자를 swap
then
    let tmp=$x
    x=$y
    y=$tmp
fi
let x++
while (( $x != $y )) #x가 y보다 작을 때까지
do
    if [ $x -eq 2 ] #2인 경우 추가
    then
        let rst+=2
        let x++
        continue;
    fi
    tmp=2 #나누는 수 2부터 시작
    until (( $tmp == $x )) #나누는 수가 x보다 작을 동안만 반복
    do
        let rest=$x%$tmp #나머지 계산
        if [ $rest -eq 0 ] #나누어 떨어지는 경우
        then
            break;
        fi
        let tmp++ #나누어 떨어지지 않는 경우
    done
    if [ $tmp -eq $x ] #2부터 x-1까지 x와 나누어 떨어지는 수가 하나도 없는 경우
    then
        let rst+=$x #rst에 x 더함
    fi
    let x++ #x 1 증가
done
echo "result : $rst"

큰 숫자가 먼저 나오는 경우도 올바르게 출력된다.

이 코드는 다음과 같이 실행된다.

1. x와 y가 같은지 확인한다. 같을 경우 0출력하고 종료

2. y가 x보다 큰지 확인한다. 그럴 경우 두 변수를 바꾼다.

3. x를 1씩 증가하며 y보다 작을 때까지 계산을 반복한다.

  3-1. x가 2인경우 rst에 x를 더한다.

  3-2. x가 소수인지 판별할 tmp변수를 2부터 x-1까지 1씩 증가하며 x와 나누어 떨어지는지 확인한다.

  3-3. 만약 나누어 떨어지는 수가 있는 경우 rst에 아무것도 더하지 않고 x를 1 증가시킨다.

  3-4. 만약 2부터 x-1까지 모든 수가 x와 나누어 떨어지지 않는 경우 rst에 x를 더한다.

 

위 실행 결과는 wsl2 ubuntu-20.04버전에서 실행한 결과이다.

    어떤 터미널 창을 열었을 때 우리는 쉘을 보게 된다. 이 쉘은 명령어를 입력하여 컴퓨터와 대화하는 프로그램이다. 이 쉘은 프로그램이기 때문에 어떤 터미널 창을 띄울 경우 하나의 프로세스라는 말이다. 리눅스, 맥 OS X에서 기본으로 사용되는 쉘은 Bash 쉘이다 이 Bash 쉘은 유닉스에서 사용되던 본 쉘(Bourne shell)을 GNU에서 확장하여 개발하였다. 이 글에서는 bash 쉘에서 사용하는 문법을 이용해 짧은 쉘 스크립트 프로그램을 짜는 방법을 알아볼 것이다.

    우선 쉘에서는 환경변수와 지역변수가 존재한다. 환경변수는 자식 프로세스에게 상속되는 변수이고 지역변수는 상속되지 않는 변수이다. bash shell창에 $ env를 통해 미리 정해진 환경 변수들을 살펴볼 수 있다. bash shell에서 다음과 같이 변수를 설정할 수 있다.

$ VAR=STRING
$ export VAR

첫 번째 줄과 같이 [변수명]=[문자열]로 지역변수를 설정할 수 있고 두 번째 줄과 같이 export [변수명]을 통해 설정한 지역변수를 환경변수로 만들 수 있다. 이때 주의해야할 점은 '=' 앞뒤에 공백이 있으면 안된다는 것이다.

또 리스트 변수를 선언할 수 있다. 리스트 변수는 다음과 같이 선언할 수 있다.

$ HERO=(LEESOONSHIN KIMYOUSHIN AHNJUNGKEUN YOUKWANSOON)
${HERO[i]} #리스트의 i번째 원소
${HERO[*]} #리스트의 모든 원소
${HERO[@]} #동일
${#HERO[*]} #리스트의 원소 개수
${#HERO[@]} #동일

첫 번째 줄과 같이 괄호( ) 안에 공백으로 원소를 구분하며 나열하여 선언할 수 있다. 그 아래와 같이 C언어와 비슷하게 대괄호 안에 원하는 인덱스의 번호를 주어 해당 원소에 접근할 수 있다. 만약 모든 원소를 사용하고 싶은 경우 인덱스 번호 대신 '*'나 '@'를 주면 된다. 마지막으로 원소의 개수는 모든 원소를 접근하는 방법에다가 리스트 변수명 앞에 '#'을 붙여 나타낼 수 있다. 실제 실행결과는 다음과 같다.

실제 실행 결과

echo라는 명령어는 뒤에 나오는 인자를 화면에 출력해주는 명령어이다. 따라서 변수를 줬을 때 변수의 값을 출력해준다. 변수를 접근하고자할 때는 변수명 앞에 '$'를 붙여줘야한다. 그렇지 않으면 문자열로 인식하여 다음과 같이 출력될 수 있다.

변수명 앞에 '$'를 붙이지 않을 경우

위 사진은 wsl2 Ubuntu-20.04에서 실행한 결과다. 다음 시간에는 이런 변수를 통해 짧은 shell script program을 짜보겠다.

    이번에는 이전 글(https://shyeon.tistory.com/13)에서 만든 레코드파일을 관리하는 simple index 파일을 생성하고 생성한 simple index파일을 활용하여 이진탐색 방식을 이용해 원하는 레코드의 정보를 불러오는 기능을 구현해 보았다.

simple index파일에서의 key 값으로는 14바이트짜리 id가 사용된다 그리고 원하는 주소에 접근하기 위해서 페이지번호와 페이지에서의 레코드 번호가 주소값으로 사용된다. 그렇기 때문에 뒤에 널문자를 제외한 13바이트의 id, 페이지 번호로 4바이트, 레코드 번호로 4바이트 총 21 바이트가 simple index 파일의 하나의 레코드를 구성한다. 레코드 파일을 하나 넘겨줄 경우 record.idx라는 이름의 simple index 파일이 생성된다. 이때 생성된 레코드는 id에 대해 오름차순으로 정렬된다. 이 id는 정수형으로 정렬되기 때문에 자리수가 클 수록 큰 수로 판단된다. 레코드파일은 다음과 같은 명령어로 실행된다.

$ a.out i records.dat records.idx

이렇게 두 번째 인자로 i를 줄 경우 records.dat의 레코드파일로부터 records.idx라는 이름의 index 파일을 생성한다.

    만약 두 번째 인자로 b 옵션을 줄 경우 정렬된 index 파일로부터 원하는 키 값을 주었을 때 원하는 레코드의 정보를 출력한다. 실행할 경우 다음과 같이 나타나게 된다.

$ a.out b records.dat records.idx "9800001111111"

#reads:3

id=9800001111111

name=shyeon

age=24

addr=Seoul

phone=010-0000-0000

email=shyeon@shyeon.com

이렇게 records.idx에서 9800001111111이라는 키를 이진 탐색으로 찾는데 3번의 읽기를 했다는 의미로 #reads:3을 출력하고 records.idx에서 찾은 주소값으로 records.dat이라는 레코드파일에서 해당 레코드를 읽은 결과를 밑에 출력하게 된다. 만약 레코드가 존재하지 않는 경우 "no persons"를 출력한다.

https://github.com/Seo-sang/simple_index_record_file

이전 글에서 설명한 가변길이 레코드 방식을 실제로 구현해보았다. 레코드파일 person.c의 구조는 다음과 같다.

 - 레코드 파일

    1. 레코드 파일은 헤더 영역과 데이터 영역으로 이루어져있다.

    2. 헤더 영역에는 헤더 레코드가 존재하고 데이터 영역에는 여러 개의 데이터 페이지가 존재한다.

 - 헤더 레코드

    1. 헤더 레코드는 전체 데이터 페이지 수 4바이트, 레코드 파일에 존재하는 모든 레코드 수 4바이트, 마지막 8바이트에는 4바이트씩 삭제 레코드를 관리하기 위해 가장 최근에 삭제된 레코드의 페이지 번호와 레코드 번호를 가지고 있다.      2. 만약 삭제 레코드가 존재하지 않는다면 헤더 레코드의 마지막 8바이트의 값은 모두 -1이다.

    3. 레코드가 삭제되었을 때 헤더 레코드의 페이지 수나 모든 레코드의 수는 수정하지 않는다.

    4. 삭제 레코드는 연결리스트 방식으로 관리된다.

 - 데이터 페이지

    1. 데이터 페이지는 헤더 영역과 데이터 영역으로 나누어져있다.

    2. 헤더 영역에는 페이지에 저장된 레코드의 수, 각 레코드의 주소, 길이(offset, length)가 있으며 데이터 영역에는 실제 저장하려는 레코드가 저장된다.

    3. 레코드의 수, 주소, 길이는 각각 4바이트이며 첫 번째 레코드의 주소는 0이다.

    4. 데이터 페이지의 번호는 0부터 부여한다.

 - 레코드

    1. 레코드는 가변길이 방식으로 저장한다.

    2. 레코드의 필드는 구분자(delimiter)를 이용한다. 이때 구분자로 '#'을 사용한다. 

    3. 새로운 레코드는 파일의 마지막 페이지의 마지막 레코드 다음 위치에 저장되고 공간이 부족할 경우 데이터 페이지를 새로 할당하여 저장한다.

    4. 만약 삭제레코드에 새로운 레코드를 저장할 수 있는 경우 삭제 레코드에 저장한다. 이때 first-fit 방식을 이용한다.

    5. 삭제 레코드에 추가할 경우 연결리스트의 앞과 뒤를 연결해주어야한다.

    6. 삭제 레코드 중에 추가할 수 없는 경우 3번과 같이 추가한다.

    7. 레코드를 삭제할 경우 delete mark로 '*'를 사용하며 delete mark 뒤에 연결리스트에 사용될 다음 삭제 레코드의 페이지 번호와 레코드 번호가 4바이트씩 binary integer 형식으로 저장된다.

    8. internal fragmentation은 고려하지 않는다.

 

레코드는 다음과 같이 이루어져있다.

#define MAX_RECORD_SIZE	100 // including 6 delimeters
#define PAGE_SIZE		256 // 임의로 지정
#define HEADER_AREA_SIZE	(PAGE_SIZE/10)
#define DATA_AREA_SIZE	(PAGE_SIZE - HEADER_AREA_SIZE)

typedef struct _Person
{
	char id[14];		
	char name[18];		
	char age[4];		
	char addr[22];	   
	char phone[16];		
	char email[26];		
} Person;

#endif

위의 6개의 정보로 이루어진 구조체가 하나의 레코드로 저장된다.

완성된 코드는 아래 링크에서 확인할 수 있다.

https://github.com/Seo-sang/variable_length_record

 

Seo-sang/variable_length_record

Contribute to Seo-sang/variable_length_record development by creating an account on GitHub.

github.com

개발 환경은

OS Linux Ubuntu 20.04

gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0

이다.

리눅스 system call 함수를 이용하여 ls, chmod, touch명령어의 일부 기능을 수행하는 myls, mychmod, mytouch 명령어를 직접 만들어보았다. 명령어의 옵션이 상당히 많기 때문에 일부의 옵션만 구현하였다. 모든 프로그램은 system()이라는 system call함수를 사용하지 않았다.

소스코드는 글 맨 아래에 링크를 달아두었다.

    우선 ls의 명령어는 굉장히 많기 때문에 일부옵션인 l, i, t, a옵션만 구현하였다. 모든 옵션은 동시에 가능하다 예를 들면 myls -lit <filename|pathname>과 같이 사용할 수 있다. 만약 매개변수가 경로명일 경우 해당 경로에 있는 모든 파일들을 보여주고 매개변수가 파일명일 경우 해당 파일에 대한 ls명령어 결과를 보여준다.

'l' 옵션을 준 경우 파일에 대한 자세한 사항을 보여준다.

'a' 옵션을 준 경우 '.'으로 시작하는 파일을 포함하여 디렉토리에 있는 숨겨진 파일 전부를 보여준다.

't' 옵션을 준 경우 경로에 있는 파일을 시간순으로 정렬하여 출력한다.

'i' 옵션을 준 경우 파일의 i-node 번호를 앞에 출력해준다.

아래는 실행 결과이다.

ls명령어와 같이 myls를 실행했을 때 현재 디렉토리의 모든 파일을 보여준다.
-l 옵션을 주어 실제 ls명령어와 결과 비교
-lit 옵션을 주었을 때 ls명령어와 결과 비교
-l 옵션을 주었을 경우 symbolic 디렉토리일 경우  실제로 가리키는 디렉토리가 -> 뒤에 나타난다.
setuid bit가 설정된 파일의 경우 x대신 s가 나타난다.

chmod 명령어는 숫자, 알파벳 모두 옵션으로 사용할 수 있도록 구현하였다. 따라서 다음과 같이 사용할 수 있다.

mychmod g+r <filename>

mychmod 644 <filename>

프로그램 내부에서는 chmod()라는 system call함수를 이용하여 파일의 접근권한을 수정한다. mychmod를 실행하기 전과 후에 ls명령어를 이용하여 파일 정보를 확인한다면 결과가 접근권한이 바뀌어있는 것을 확인할 수 있을 것이다.

아래는 실행 결과이다.

mychmod로 접근권한을 751로 바뀌게 했을 경우 실행 이전과 비교
뒤에 3자리 접근권한 앞에 1일 경우 stickybit를, 2일 경우 setgid bit, 4일 경우 setuid비트를 설정할 수 있다.
문자를 이용하여 접근권한을 바꿀 경우 결과이다.

    마지막으로 touch명령어는 굉장히 간단한 프로그램이다. 파일이 없을 경우 생성만 하면 되고 'c' 옵션의 경우 파일이 없을 경우 파일을 생성하지 않으면 된다. 만약 파일이 있을 경우 파일의 access, modify, change 시간을 현재 시간으로 갱신하기만 하면 된다. 이때 시간 변경은 utime()이라는 system call 함수를 이용하여 두 번째 인자로 NULL을 주어 자동적으로 현재 시간으로 수정하게 하면 된다. utime()함수는 access와 modify시간만 변경 가능하다. 따라서 현재 시간을 받아 그 시간 변수를 인자로 넘길 경우 각 줄을 실행하는 딜레이로 인해 change시간이 access와 modify의 시간과 다르게 갱신될 수 있다.

https://github.com/Seo-sang/LinuxSystemProgramming_ls_chmod_touch.git

 

Seo-sang/LinuxSystemProgramming_ls_chmod_touch

Contribute to Seo-sang/LinuxSystemProgramming_ls_chmod_touch development by creating an account on GitHub.

github.com

 

https://programmers.co.kr/learn/courses/30/lessons/42579

 

코딩테스트 연습 - 베스트앨범

스트리밍 사이트에서 장르 별로 가장 많이 재생된 노래를 두 개씩 모아 베스트 앨범을 출시하려 합니다. 노래는 고유 번호로 구분하며, 노래를 수록하는 기준은 다음과 같습니다. 속한 노래가

programmers.co.kr

이 문제는 프로그래머스의 코딩테스트 연습 - 해시 - 베스트앨범이라는 문제이다.

위 링크를 타고 문제를 읽어보면 생각보다 단순하지 않게 느껴질 것이다.

우선 아래는 직접 풀은 코드이다.

#include <string>
#include <vector>
#include <iostream>
#include <map>
#include <algorithm>

using namespace std;

bool cmp(pair<int,int> a, pair<int,int> b) { //map에 있는 vector 정렬
    return a.second > b.second;
}

bool cmp2(pair<string,int> a, pair<string,int> b) { //vector정렬
    return a.second > b.second;
}

vector<int> solution(vector<string> genres, vector<int> plays) {
    vector<int> answer;
    map<string,vector<pair<int,int>>> m;
    vector<pair<string,int>> v;
    for(int i = 0; i < genres.size(); i++) {
        if(m.find(genres[i]) == m.end()) { //처음 보는 장르인 경우
            vector<pair<int,int>> tmp; //벡터를 생성하여 번호와 재생 수를 저장
            tmp.push_back({i, plays[i]});
            m.insert({genres[i], tmp});
            v.push_back({genres[i], plays[i]}); //벡터에 추가
        }
        else { //이미 있는 장르인 경우
            m[genres[i]].push_back({i, plays[i]}); //map에 번호와 재생 수를 추가
            for(int j = 0; j < v.size(); j++) { //vector에서 같은 장르를 찾음
                if(genres[i] == v[j].first) {
                    v[j].second += plays[i]; //장르의 전체 재생 수 추가
                    break;
                }
            }
        }
    }
    sort(v.begin(), v.end(), cmp2); //장르별 전체 재생 수 정렬
    for(auto it = m.begin(); it != m.end(); it++) { //같은 장르의 각각 재생 수 정렬
        sort((*it).second.begin(),(*it).second.end(), cmp);
    }
    for(auto it = v.begin(); it != v.end(); it++) { //장르별 재생 수가 많은 순서대로 
        answer.push_back(m[(*it).first][0].first); //첫 번째 많은 재생 수 추가
        if(m[(*it).first].size() > 1) { //만약 재생 수가 2개 이상인 경우 추가
            answer.push_back(m[(*it).first][1].first);
        }
    }
    
    
    
    return answer;
}

이 문제는 map과 vector를 이용하여 풀었다.

우선 이 문제에서는 장르별 전체 곡 수를 저장할 vector와 각 장르에서의 곡 번호와 재생 수를 저장할 map을 선언하였다.

map에는 첫 번째 인자로 string을 주어 장르 이름을 통해 접근할 수 있게하고 두 번째 인자로 vector<pair<int,int>>를 주어 곡 번호와 재생 수를 저장할 수 있게한다.

주어진 입력을 모두 돌며 map과 vector에 저장한 다음 sort함수를 이용해 장르별 재생 수와 각 장르에서의 곡별 재생수를 내림차순으로 정렬한다.

그 다음 정렬된 vector를 처음부터 돌며 가장 많은 곡을 순서대로 answer에 넣는다. 이때 한 장르의 전체 곡 수가 1개일 수 있으므로 if문을 이용해 장르의 곡 수가 2개 이상인지 확인하고 추가한다.

이 문제를 풀며 코드도 깔끔하게 작성하지 못하였고 풀이 방식도 간결하지 못하다고 느꼈지만 효용성을 채점하는 부분이 없고 정확성 채점만 존재했기 때문에 vector, map을 모두 이용하여 푸는 방법 밖에 떠올리지 못했다.

이 문제에서 중요했던 스킬은 iterator를 사용하는 것과 vmap에서 type으로 또 다른 vector를 넣어 이를 이용하는 방식이 관건이었다. 직접 작성하면서도 헷갈리는 부분이 굉장히 많았고 풀이는 처음에 맞았지만 i와 j를 잘 못 쓰는 바람에 시간이 조금 더 걸렸었다. c++을 활용한 코딩테스트 문제를 풀 때 vector는 기본이고 set과 map을 활용해야하는 문제가 굉장히 많다고 느끼기 때문에 c++의 STL을 잘 활용할 수 있는 공부가 필요하다고 생각한다. 문제를 풀고 다른 사람들의 풀이를 보고 더 짧은 코드는 어떻게 돌아가는지 이해하는 것도 중요한 것 같다.

    이전 글에서 파일에서 레코드 저장하는 고정길이와 가변길이 방식을 설명하였다. 이번 글에서는 저장된 레코드를 삭제했을 때의 관리 방법에 대해 알아보겠다.

    레코드를 삭제한 경우 가장 단순한 방법은 삭제된 레코드가 차지한 공간을 없애는 방법이다. 즉 삭제된 레코드 오른쪽에 있는 모든 데이터들을 이동(shift)시키는 방법이다. 이 방식을 Storage Compaction이라고 한다. 레코드가 삭제되었을 때 표시를 해놓고 그 레코드를 제외하고 다른 파일에 그대로 옮겨서 저장하는 방식이다. 이 방법은 파일 크기를 줄이는 장점이 있지만 파일의 크기가 클 경우 굉장히 많은 비용이 들기 때문에 삭제가 될 때마다 할 수 없다. 그렇기 때문에 얼마나 자주 compaction을 할지는 삭제 레코드의 수나 시간 등을 정해놓고 수행하는 것이 좋다. 다른 파일에 쓰는 경우 out-of-place 방식이지만 시간이 더 걸리고 복잡한 방식으로 in-place 방식으로 같은 파일에 쓸 수 있는 방법도 있다.

    Storage Compaction방식은 현실적으로 어려우니 삭제된 공간을 재사용하는 방법이 이상적이다. 이 경우 고정길이 방식과 가변길이 방식에 큰 차이점이 있다.

    먼저 고정길이 레코드 방식의 경우 레코드가 삭제되었다고 생각해보자. 이 경우 삭제된 레코드에 특수문자인 delete mark를 맨 앞에 써놓아 이 레코드는 삭제되었다는 것을 표시한다. 고정길이 레코드는 모든 레코드의 길이가 동일하기 때문에 어떤 레코드든 삭제된 레코드에 쓸 수 있다. 이때 레코드를 새로 추가할 때 삭제된 레코드의 위치를 빠르게 찾는 것이 중요하다. 가장 좋은 방법으로 링크드리스트 방법이 있다. 삭제된 레코드의 위치를 링크드리스트 방법으로 관리하는 방법이다. 아래의 그림과 같이 표현할 수 있다. 

헤더영역에 삭제된 레코드의 첫번째 주소(RRN : Relative Record Number)를 쓴다. 그 다음 4번레코드에는 2가 있고 2번에는 5번, 5번에는 -1이 들어있다. 링크드리스트의 마지막에는 -1을 넣어 끝임을 알린다. 이렇게 삭제된 레코드의 번호를 링크드리스트 방식으로 다룰 수 있다. 만약 새로 레코드를 추가할 겨우 4번 레코드에 쓰게 되며 헤더의 레코드번호는 그 다음 주소인 2번이 존재하게 된다. 이때 추가로 삭제되는 레코드를 가장 연결리스트의 첫번째 노드에 오게 할 지 맨 마지막에 오게 할 지는 스스로 결졍하면 된다. 이 글에서는 가장 최근에 삭제된 레코드를 헤더에 쓰겠다. 이렇게 고정길이 방식에서의 삭제 레코드 관리를 알아보았다. 그 다음 가변길이 레코드 방식에서의 삭제 레코드 관리방법에 대해 알아보겠다.

    가변길이 레코드 방식은 고정길이보다 더 복잡하다. 고정길이 방식은 레코드의 길이가 고정되어있으므로 어떤 삭제레코드든 삽입할 수 있었다. 하지만 가변길이는 모든 레코드의 길이가 다르므로 레코드의 길이를 판별하는 과정이 추가된다. 또한 레코드의 길이가 다르기 때문에 RRN이 아닌 레코드의 주소인 offset을 사용한다. 따라서 delete mark를 쓰고 레코드의 RRN이 아닌 레코드의 길이와 다음 연결리스트 노드의 주소를 쓴다. 따라서 레코드를 삽입하고자 할 때 처음 연결리스트의 노드부터 길이를 비교하여 추가하는 레코드의 길이가 삭제된 레코드의 길이보다 작거나 같은지 확인하고 맞지 않을 경우 다음 노드의 offset으로 이동하여 비교하는 과정을 거친다. 모든 연결리스트의 레코드 길이가 맞지 않은 경우 맨 마지막에 추가해야한다.

    만약 추가하려는 레코드의 길이가 60이고 삭제된 레코드의 길이가 100이라고 가정하자. 추가 레코드의 길이가 더 작으므로 삭제 레코드에 쓸 수 있다. 쓴 경우 40의 길이가 남게 된다. 이렇게 낭비되는 공간을 internal fragmentation이라고 한다. 이 공간은 다시 재사용할 수 있게된다. 그 이후 길이 35의 레코드를 추가한다고해보자 이 경우 40의 공간을 또 재사용하여 35길이의 레코드를 추가했다. 남은 공간은 5가 된다. 크기가 5인 공간은 또 다른 레코드를 할당하기에 너무 작아 재사용되지 못하고 낭비하게 된다. 이 공간을 external fragmentation이라고 한다. 그렇다면 internal fragmentation과 external fragmentation을 관리하려면 어떻게 해야할까? 삭제레코드를 관리하는 연결리스트에서 레코드를 추가할 때의 순서에 영향을 받는다. 이 방법에 세 가지가 존재한다.

    첫 번째로 first-fit이다. 말 그대로 첫 번째로 발견하는 삭제 레코드를 할당하겠다는 말이다. 만약 40 -> 72 -> 68 -> 57 크기의 연결리스트로 연결된 삭제 레코드가 있다고 하자 이때 추가하고자 하는 레코드의 길이가 55라면 55보다 크거나 같은 가장 첫 번째인 72에 할당한다는 말이다.

    두 번째로 best-fit이 있다. 이 방식은 크기가 가장 가까운 공간에 할당하는 방법이다. 위의 예시에서 55의 길이와 가장 가까운 57에 할당하게 된다. 이 방법은 external fragmentation이 발생할 확률이 늘어나게 된다.

    마지막으로 worst-fit이 있다. 이 방식은 크기의 차이가 가장 많이나는 공간에 할당하는 방법이다. 위의 예시에서 55와 가장 차이가 많이나는 72에 할당한다. 이 방식은 internal fragmentation을 발생시켜 재사용할 수 있는 가능성을 높이는 방법이다.

    지금까지 파일에서 레코드를 삭제할 경우 삭제된 레코드를 관리하는 방법에 대해 알아보았다. 실제에서 적용할 경우 레코드의 특징과 길이, 파일의 크기 등을 고려하여 가장 적절한 방법을 찾아 적용해야할 것이다.

같은 형식을 가진 데이터들을 파일에 저장하고자할 때 어떤 방식을 이용할 수 있을까?

예를들어 사람들의 이름, 나이, 거주지, 직업의 필드를 가진 레코드들을 파일에 저장한다고 해보자.

그렇다면 이 네 가지의 정보가 들어있는 구조체를 선언하여 한 사람마다 저장한 다음 구조체단위로 파일에 써야할 것이다. 그렇다면 아래와 같은 구조체를 만들 수 있을 것이다.

struct Person {
    char name[10];
    char age[4];
    char address[10];
    char job[10];
};

이렇게 만든 구조체에 한 사람의 정보를 담고 파일에 쓴다고 생각해보자. 그렇다면 각 필드의 크기를 모두 합친 10+4+10+10 = 34byte를 하나의 레코드가 차지하는 고정길이필드 방식을 사용할 수 있다. 각 필드를 일렬로 줄세워 파일에 쓰는 것이다. 이러한 방식은 굉장히 쉽고 단순하다는 장점이 있지만 만약 이름이 4byte, 나이가 1byte, 주소가 5byte, 직업이 5byte라고 한다면 낭비되는 공간이 훨씬 많아진다는 치명적인 단점이 존재한다. 그렇기 때문에 우리는 가변길이 필드 방식을 사용할 수 있다. 가변길이 방식에는 3가지 방식이 있다.

    첫 번째로 길이 지시자(length indicator)가 있다. 이 방식은 각 필드 앞에 필드의 길이를 쓰는 것이다. 예를들어 다음과 같이 쓸 수 있다.

5James2255Seoul10Programmer

이렇게 James는 5바이트이므로 앞에 5를, 25살을 뜻하는 25는 2바이트이므로 앞에 2를 쓰는 방법을 이용한다. 하지만 실제 구현하였을 때 앞의 길이지시자를 읽고 그 다음 해당 길이만큼 각 필드를 읽어야하기 때문에 번거롭다는 단점이 있다.

    두 번째로 구분자(Delimiter)가 있다. 이 방식은 필드와 필드 사이에 구분을 지을 수 있는 문자를 넣는 방법이다. 다음과 같이 사용할 수 있다.

James|25|Seoul|Programmer|

이렇게 필드와 필드 사이에 '|' 문자를 넣어 필드를 구분할 수 있게 하는 방법이다. 이 방법은 가변길이기 때문에 그 delimiter를 만나기 전까지 읽어야하므로 길이를 알 수 없다는 단점이 있다.

    세 번째로 Keyword = Value 방식이 있다. 이 방식은 어떤 필드인지를 앞에 써주는 방법이다.

NAME=James|AGE=25|ADDRESS=Seoul|JOB=Programmer|

이 방식을 사용하였을 때 사라진 필드가 어떤 필드인지를 알 수 있는 장점이 있다. 예를 들어 ADDRESS필드가 사라졌다고 생각해보자. 그렇다면 Keyword를 보고 빠진 필드가 무엇인지 알 수 있고 Seoul이 없을 대 직업인 Programmer가 거주지라고 착각할 일이 생기지 않는다. 하지만 이 방법은 Keyword가 너무 많은 공간을 차지하기 때문에 실질적으로 사용이 어렵다.

    지금까지 필드를 구분하는 방법을 알아보았으니 필드로 이루어진 레코드를 구분하는 방법을 알아보자. 레코드 구분 방식에도 고정길이와 가변길이 방식이 있다. 고정길이의 경우 레코드의 길이를 일정하게 지정해놓는 방법이다. 다음은 고정길이 레코드 방식에 고정길이방식 필드와 가변길이방식 필드를 나타낸 것이다. 남은 공간은 '_' 로 나타내었다.

고정길이필드 : James_____25__Seoul_____Programmer

가변길이필드 : James|25|Seoul|Programmer|__________

고정길이 레코드 방식일 경우 위와 같이 나타날 수 있다. 두 방법 모두 낭비되는 공간이 생기게 된다. 이 남은 공간이 생기지 않기 위해서는 가변길이 레코드 방식을 사용하여야한다. 가변길이 레코드 방식은 4가지가 있다.

    첫 번째로 헤더 영역에 메타데이터를 저장하는 것이다. 파일의 맨 첫 부분에 레코드를 구성하는 필드의 수, 수정 날짜, 삭제된 레코드 정보 등을 저장해놓아 레코드를 구분할 수 있는 방식이다. 예를 들어 우리의 구조체는 4개의 필드를 가지고 있으므로 헤더영역에 4를 저장해놓아 하나의 레코드는 4개의 필드로 이루어져 있음을 인지하고 레코드를 읽는 방식이다.

    두 번째로 길이 지시자(length indicator)가 있다. 가변길이 필드 방식에서와 마찬가지로 각 레코드의 맨 앞에 레코드의 길이를 쓰는 방식이다. 아래와 같이 표현할 수 있다.

26James|25|Seoul|Programmer|

다음 예시는 필드 구분을 구분자를 사용하였다. 이 경우 구분자의 byte 수까지 포함하여 앞에 써준다. 이 방식에서는 앞에 26의 숫자를 ASCII 문자인 '2'와'6'으로 쓸지 26을 binary integer방식으로 십진수 26의 ASCII문자를 저장할 지 결정해야한다. 이때 길이지시자로 2바이트를 사용한다고 할때 ASCII문자 방식은 0~99까지의 100개의 숫자만 나타낼 수 있지만 binary integer방식은 2^16의 굉장히 많은 길이를 표현할 수있는 차이점이 있다.

    세 번째로 Index file을 사용하는 방식이 있다. 레코드들이 저장된 데이터파일에 저장하는 것이 아닌 각 레코드의 시작 주소(offset)을 저장하는 index file을 따로 만드는 방식을 사용할 수 있다. 만약 하나의 레코드당 길이를 2바이트를 사용하였다면 1000번째 레코드를 읽고자할 때 offset만 2*1000으로 이동하여 2바이트를 읽으면 되는 방식이다.

    마지막으로 구분자(Delimiter)방식이 있다. 필드 구분 방식과 같이 레코드를 구분할 수 있는 문자를 레코드 사이에 넣어 구분할 수 있는 방식이다. 이 경우 가변길이 필드 방식의 구분자와 같아서는 안되며 1000번째 레코드를 가져오고자 할 때 굉장히 많은 시간이 걸리고 레코드를 읽기에 힘들다는 단점이 있다.

 

    지금까지 고정길이와 가변길이 필드, 레코드 방식에 대하여 알아보았다. 실제 파일을 처리할 때 이전 글에서 말했듯이 디스크 접근 비용을 줄이는 것이 가장 중요하므로 위 방식을 적절히 변형하거나 조합하여 사용하는 것이 중요하다.

+ Recent posts