UI 설계를 마쳤으면 이제 본격적으로 코딩으로 들어갈 차례이다. 이번 글에서는 코딩에 대해서 정리해 보려고 한다.
1. 코딩 작업
소프트웨어의 품질은 결국 원시 코드에 모두 귀결된다. 따라서 코딩 작업은 설계 명세에 나타난 요구사항을 만족할 수 있도록 프로그래밍하는 과정이며, 오류가 적고 품질 좋은 프로그램을 작성하는 것을 목표로 한다.
코딩 작업의 로드맵을 그림으로 나타내면 다음과 같다.

그림을 보면 코딩 작업은 요구사항과 상세 설계 결과를 바탕으로 진행된다.
- 먼저 원시 코드를 같은 스타일로 작성하기 위해 코딩 표준을 정의한다.
- 또한 아키텍처 설계 결과를 바탕으로 적절한 프레임워크 패키지와 응용 패키지를 결정한다.
- 이후 각 클래스에 대해 메소드를 구현한다.
- 구현이 끝난 클래스는 인스팩션을 통해 코드에 결함이 없는지 확인한다.
- 인스팩션 후에는 클래스 단위로 단위 테스트를 수행하여 기능이 올바르게 동작하는지 검증한다.
- 마지막으로 테스트를 마친 클래스나 패키지를 릴리스하여 응용 시스템으로 통합한다.
즉, 코딩 작업은 단순히 코드를 작성하는 단계가 아니라, 코딩 표준을 정하고 구현, 검토, 테스트, 통합을 반복하며 코드 품질을 관리하는 과정이라고 할 수 있다.
1.1. 자주 발생하는 오류
코딩 과정에서 오류는 다양한 방법으로 발견되지만, 자주 발생하는 오류의 유형은 어느 정도 정해져 있다. 이러한 오류들은 프로그램의 안정성과 품질에 큰 영향을 줄 수 있으므로 미리 알고 주의하는 것이 중요하다. 이번에는 코딩 과정에서 흔히 발생할 수 있는 오류들을 차례대로 정리해 보려고 한다.
1.1.1. 메모리 누수
메모리 누수는 메모리가 free 되지 않고 프로그램에 계속 남아있는 현상을 의미한다. 특히 장시간 수행되는 시스템에서는 메모리 누수가 반복될 경우 사용 가능한 메모리가 점점 줄어들기 때문에 치명적인 문제가 될 수 있다.
다음 코드를 살펴보자.
char *foo(int s)
{
char *output;
if(s > 0)
output = (char *)malloc(size);
if (s == 1)
return NULL; // if s ==1 1 then memeory leaked
return output;
}
위 코드에서 s > 0이면 malloc()을 통해 메모리가 할당된다. 그런데 s == 1인 경우에는 할당된 메모리를 free()하지 않고 바로 NULL을 반환한다.
이때 output이 가리키던 메모리는 해제되지 않았고, 함수 밖에서는 그 메모리에 접근할 방법도 사라진다. 따라서 해당 메모리는 프로그램이 종료되기 전까지 계속 할당된 상태로 남게 되며, 이것이 메모리 누수이다.
정리하면 메모리 누수는 동적으로 할당한 메모리를 사용한 뒤, 해제하지 않아서 발생하는 오류이며, 이를 방지하기 위해서는 malloc()과 같은 메모리 할당 이후 더 이상 사용하지 않는 메모리를 반드시 free()해야 한다.
1.1.2. 중복된 프리선언
프로그램 안에서 사용하는 자원은 먼저 할당하고 사용 후에는 프리로 선언해야 한다. 하지만 이미 프리로 선언된 자원을 또 다시 프리로 선언하는 경우 오류가 발생할 수 있다. 이러한 오류를 중복된 free 선언 또는 double free 오류라고 한다.
다음 코드를 살펴보자.
main()
{
char *str;
str = (char *) malloc (10);
if (gloabl == 0)
free(str);
free(str) // *str is already freed
}
위 코드에서 global == 0 이라면 str이 가리키는 메모리는 if문 안에서 한 번 프리된다. 그런데 이후 main() 함수의 마지막에 다시 free(str)을 실행하기 때문에 이미 해제된 메모리를 한 번 더 해제하려는 상황이 발생한다.
이처럼 같은 메모리를 두 번 해제하면 프로그램이 비정상적으로 종료되거나 예측하기 어려운 동작이 발생할 수 있다.
따라서 동적 메모리를 사용할 때는 메모리를 한 번만 해제되도록 주의해야 한다. 일반적으로 free()를 호출한 뒤 포인터를 NULL로 초이고하하면 중복 해제를 방지하는 데 도움이 된다.
1.1.3. NULL의 선언
NULL 오류는 포인터가 아무 유효한 메모리 주소도 가리키고 있지 않은 상태에서 해당 포인터가 가리키는 값을 접근하려고 할 때 발생한다. 즉, NULL을 가리키는 포인터를 역참조하면 오류가 발생할 수 있다.
이러한 오류는 프로그램을 비정상적으로 종료시키거나 시스템을 다운시킬 수 있으므로 주의해야 한다.
다음 코드를 살펴보자.
char *ch = NULL;
if(x > 0)
{
ch = 'c';
}
printf("%c", *ch); // ch may be NULL
*ch = malloc(size);
ch = 'c' // ch will be NULL if malloc returns NULL
위 코드에서 ch는 처음에 NULL로 초기화되어 있다. 만약 x > 0 조건이 만족되지 않으면 ch는 여전히 NULL 상태로 남아 있다. 그런데 이후 printf("%c", *ch);에서 *ch를 사용하면, NULL 포인터가 가리키는 값을 접근하려는 상황이 된다. 이때 오류가 발생할 수 있다.
또한 동적 메모리 할당을 사용할 때도 주의해야 한다. malloc()은 항상 성공하는 것이 아니라, 메모리 할당에 실패하면 NULL을 반환할 수 있다. 따라서 malloc() 이후에는 포인터가 NULL인지 확인한 뒤 사용해야 한다.
정리하면 NULL 오류는 포인터가 유효한 주소를 가리키는지 확인하지 않고 접근할 때 발생한다. 이를 방지하려면 포인터를 사용하기 전에 NULL 여부를 검사하고, 동적 메모리 할당 후에도 할당 성공 여부를 확인해야 한다.
1.1.4. 별칭의 남용
별칭(alias)은 서로 다른 이름의 변수가 같은 메모리 주소나 같은 객체를 참조하는 경우를 의미한다. 별칭을 적절히 사용하면 편리할 수 있지만, 지나치게 많이 사용하면 프로그램의 동작을 예측하기 어려워질 수 있다.
예를 들어 개발자는 두 변수가 서로 다른 데이터를 가리킨다고 생각하고 코드를 작성했지만, 실제로는 두 변수가 같은 주소를 참조하고 있을 수 있다. 이 경우 한 변수를 통해 값을 변경하면 다른 변수에서도 값이 바뀐 것처럼 보이기 때문에 예상하지 못한 오류가 발생할 수 있다.
정리하면 별칭의 남용은 서로 다른 변수라고 생각한 대상이 실제로는 같은 값을 참조하면서 발생하는 오류이다. 따라서 포인터나 객체 참조를 사용할 때는 여러 변수가 같은 대상을 가리키고 있는지 주의해야 한다.
1.1.5. 배열 인덱스 오류
배열 인덱스 오류는 배열의 범위를 벗어난 위치에 접근할 때 발생하는 오류이다. 배열 인덱스가 한도를 벗어나거나 음수 값을 갖는 경우 오류가 발생할 수 있으므로, 배열을 사용할 때는 인덱스 범위에 주의해야 한다.
다음 코드를 살펴보자.
dataArray[80];
for(i = 0; i <= 80; i++)
dataArray[i] = 0;
위 코드에서 dataArray[80]은 크기가 80인 배열을 의미한다. 따라서 사용할 수 있는 인덱스는 0 ~ 79이다.
하지만 반복문의 조건이 i <= 80으로 되어 있기 때문에 i가 80일 때도 반복문이 실행된다. 이때 dataArray[80]에 접근하게 되는데, 이는 배열의 범위를 벗어난 접근이다.
정리하면 배열을 사용할 때는 배열의 크기와 실제 접근 가능한 인덱스 범위를 정확히 구분해야 한다.
1.1.6. 수식 예외 오류
수식 예외 오류는 수식을 계산하는 과정에서 발생하는 오류이다. 대표적인 예로는 0으로 나누는 오류와 변동 소수점 예외 오류가 있다.
1.1.7. 하나 차이에 의한 오류
하나 차이에 의한 오류는 반복문의 시작 값이나 종료 조건을 잘못 설정하여 발생하는 오류이다. 예를 들어 0부터 시작해야 하는 값을 1부터 시작하거나, <=을 <으로 작성하는 경우가 이에 해당한다.
이러한 오류는 코드상으로는 큰 문제가 없어 보일 수 있지만, 실제 실행 결과에서는 특정 데이터가 누락되거나 배열 범위를 잘못 접근하느 문제가 발생할 수 있다. 또한 컴파일러나 테스트 도구에 의해 검출되지 않는 경우가 많기 때문에 개발자가 직접 주의해서 작성해야 한다.
1.1.8. 사용자 정의 자료형 오류
사용자 정의 자료형 오류는 사용자가 직접 정의한 자료형의 값을 다루는 과정에서 발생할 수 있는 오류이다. 사용자 정의 자료형을 사용할 때 값의 범위를 제대로 확인하지 않으면 오버플로우나 언더플로우 오류가 발생할 수 있다. 따라서 사용자 정의 자료형의 값을 다룰 때는 특별한 주의가 필요하다.
다음 코드를 살펴보자.
typedef enum{A, B, C, D} grade;
void foo(grade X)
{
int l, m;
l = GLOBAL_ARRAY[x-1]; // Underflow possible
m = GLOBAL_ARRAY[x+1]; // Overflow possible
}
위 코드에서 grade는 A, B, C, D라는 값을 갖는 열거형 자료형이다. 일반적으로 열거형 값은 내부적으로 정수처럼 처리되며, A = 0, B = 1, C = 2, D = 3과 같이 대응될 수 있다.
문제는 x - 1이나 x + 1처럼 값을 변경하여 배열 인덱스로 사용할 때 발생한다. 만약 X가 A라면 x - 1은 -1이 되어 언더플로우 문제가 발생할 수 있다. 반대로 X가 D라면 x + 1은 열거형의 마지막 범위를 넘어가므로 오버플로우 문제가 발생할 수 있다.
따라서 사용자 정의 자료형을 사용할 때는 해당 자료형이 가질 수 있는 값의 범위를 확인하고 배열 인데긋나 연산에 사용할 경우 범위를 벗어나지 않도록 검사해야 한다.
1.1.9. 스트링 처리 오류
스트링 처리 오류는 문자열을 다루는 과정에서 발생하는 오류이다. C 언어에서는 strcpy, sprintf 등 문자열을 처리하는 함수가 많이 사용되는데, 이러한 함수들을 사용할 때는 문자열의 크기와 종료 조건을 주의해야 한다.
예를 들어 매개변수가 NULL이거나 문자열이 '\0'으로 끝나지 않은 경우 오류가 발생할 수 있다. 또한 source 문자열 크기가 destination 버퍼의 크기보다 큰 경우, destination 영역을 넘어 데이터가 복사될 수 있어 문제가 발생한다.
따라서 문자열을 처리할 때는 입력 문자열이 NULL인지 확인하고, 문자열이 정상적으로 종료되는지 확인해야 한다. 또한 문자열을 복사하거나 출력 형식으로 저장할 때는 destination 버퍼의 크기를 초과하지 않도록 주의해야 한다.
1.1.10. 버퍼 오류
버퍼 오류는 프로그램이 입력 값을 버퍼에 저장하는 과정에서 버퍼의 크기보다 큰 데이터가 들어올 때 발생하는 오류이다. 특히 입력 값을 검사하지 않고 그대로 버퍼에 복사하면 스택 영역의 버퍼에 오버플로우가 발생할 수 있다.
이러한 버퍼 오버플로우는 단순한 프로그램 오류로 끝나지 않고 보안 취약점으로 이어질 수 있다. 공격자가 고의로 매우 큰 입력 값을 전달하면, 버퍼의 범위를 넘어 다른 메모리 영역을 덮어쓸 수 있고, 이를 이용해 악성 코드를 실행시키는 공격도 가능하다.
다음 코드를 살펴보자.
void mygets(char *str) {
int ch;
while (ch = getchar() != '\n' && ch != '\0')
*(str++) == ch;
*str = '\0';
}
main() {
char s2[4];
mygets(s2);
}
위 코드에서 s2는 크기가 4인 문자 배열이다. 그런데 mygets(s2)는 입력 길이를 검사하지 않고 사용자가 입력한 문자를 계속 s2로 저장하게 된다.
따라서 사용자가 4바이트보다 큰 문자열을 입력하면 s2의 범위를 넘어서는 위치에 데이터가 저장될 수 있다. 이때 버퍼 오버플로우가 발생하며, 프로그램이 비정상적으로 종료되거나 예측하지 못한 동작을 할 수 있다.
1.1.11. 동기화 오류
동기화 오류는 공통 자원에 접근하려는 여러 스레드가 있는 병렬 프로그램에서 흔히 발생하는 오류이다. 여러 스레드가 동시에 같은 자원에 접근하면 실행 순서에 따라 결과가 달라지거나, 자원을 제대로 해제하지 못해 프로그램이 멈추는 문제가 발생할 수 있다.
동기화 오류의 대표적인 예는 다음과 같다.
- 데드락(deadlock): 여러 스레드가 서로 필요한 자원을 점유한 상태에서, 상대방이 자원을 해제하기를 기다리며 더 이상 진행하지 못하는 상태
- 경쟁 조건(race condition): 두 개 이상의 스레드가 같은 자원에 동시에 접근하려고 할 때, 스레드들의 실행 순서에 따라 수행 결과가 달라지는 경우
- 모순이 있는 동기화: 공유 변수에 접근할 때 로킹과 언로킹을 일관성 없이 수행하여 오류가 발생하는 경우
2. 코딩 표준
코딩 표준은 코드를 작성할 때 지켜야 하는 규칙이나 기준을 의미한다. 세부적인 코딩 표준은 언어, 개발 조직, 프로젝트에 따라 달라질 수 있지만, 공통적으로 중요한 기준은 간결하고 읽기 쉬운 코드를 작성하는 것이다.
간결한 코드는 복잡하지 않고 명확하여 이해하기 쉬운 코드를 의미한다. 또한 읽기 쉬운 코드는 프로그램을 대충 훑어보아도 구조와 동작을 파악하기 쉬운 코드를 의미한다.
또한 설계 단계에서 모듈화가 잘 이루어져 높은 응집력과 낮은 결합도를 달성했다면, 각 모듈은 비교적 간단하고 이해하기 쉬운 구조가 된다. 이러한 설계 원칙과 코딩 표준이 함께 적용될 때 품질 좋은 코드를 작성할 수 있다.
2.1. 명명 규칙
코딩 표준을 정할 때 먼저 고려할 수 있는 것은 명명 규칙이다. 명명 규칙은 클래스, 변수, 매서드, 상수 등의 이름을 일관된 방식으로 작성하기 위한 기준이다. 이름을 일관성 있게 작성하면 코드의 의미를 더 쉽게 파악할 수 있고, 여러 사람이 함께 개발할 때도 코드 이해가 쉬워진다.
대표적인 명명 규칙의 예시는 다음과 같다.
- 카멜 케이스
- Java 클래스, 필드, 메소드 및 변수 이름에서 주로 사용하는 명명 규칙이다. 여러 단어를 붙여 쓰되, 첫 단어는 소문자로 시작하고 그 다음 단어부터 첫 글자를 대문자로 표기한다.
- ex) int carWeightLimit;
- 상수 이름
- 상수는 일반적으로 모든 글자를 대문자로 작성하고, 단어 사이는 언더스코어(_)로 구분한다. Java에서는 보통 static final을 사용하여 상수를 정의한다.
- ex) public static final CAR_WEIGHT_LIMIT = 3; // 단위(톤)
- 클래스와 인터페이스 이름
- 클래스와 인터페이스 이름은 명사 또는 명사구로 작성하며, 대문자로 시작한다. 여러 단어로 이루어진 경우 각 단어의 첫글자를 대문자로 작성한다.
- ex) interface MessageService { ... }, class CarWeightCalculator implements MessageService { ... }
2.2. 변수 이름
변수 이름은 일반적으로 소문자로 시작한다. 변수명은 단순히 아무 이름이나 붙이는 것이 아니라, 해당 변수가 어떤 용도로 사용되는지 알 수 있도록 작성해야 한다. 따라서 변수명을 봤을 때 용도에 대한 힌트를 제공해야 하며, 의미를 알기 어려운 모호한 이름은 사용하지 않는 것이 좋다.
또한 변수 이름의 길이는 적절해야 하며, 변수가 사용되는 위치를 고려하여 정해야 한다. 예를 들어 매개변수는 매서드 안에서 짧게 사용되는 경우가 많기 때문에 되도록 간결하게 작성할 수 있다. 반면 필드 변수는 클래스 전체에서 사용되므로, 가능한 의미가 명확하게 드러나도록 작성하는 것이 좋다.
2.3. 패키지 이름
패키지 이름은 일반적으로 모두 소문자로 작성하며, 명사를 사용하는 것이 좋다. 패키지는 관련된 클래스나 인터페이스를 묶는 단위이므로, 패키지 이름만 보아도 어떤 기능이나 영역과 관련된 코드인지 어느 정도 파악할 수 있어야 한다.
2.4. 헝가리안 표기법
헝가리안 표기법은 변수 이름 앞에 타입이나 의미를 나타내는 접두어를 붙이는 명명 방식이다. 이를 통해 변수의 목적이나 변수가 어떤 값을 나타내는지에 대한 힌트를 줄수 있다.
다음 예시를 살펴보자.
- lAccountNum: long integer("I") 타입의 변수
- arru8NumberList: unsigned 8-비트 정수("arru8") 변수의 배열
- bReadLine(..): 바이트값의 리턴 코드를 가진 함수
- strName: 스트링을 가진 변수
- rwPosition: 행("rw")을 나타내는 변수
- usName: 사용되기 전에 정제할 필요가 있는 안전하지 않은 스트링("us")
- szName: '\0'로 끝나는 스트링("sz") 변수
헝가리안 표기법은 변수명만 보고도 해당 변수의 타입이나 의미를 어느 정도 알 수 있게 해주는 방식이다. 다만 현대 프로그래밍에서는 IDE나 정적 타입 시스템이 변수 타입을 쉽게 알려주기 때문에 프로젝트나 팀 코딩 표준에 따라 선택적으로 사용하는 경우가 많다.
2.5. 형식
코딩할 때는 코드의 형식을 일관되게 지키는 것이 중요하다. 대표적으로 들여쓰기와 괄호 사용이 있다. 들여쓰기와 괄호는 프로그램의 구조를 명확하게 보여 주기 위해 사용되며, 문장과 선언문은 구조에 맞게 들여쓰기하여 작성하는 것이 좋다.
또한 블록은 항상 괄호을 사용하여 작성하는 것이 좋다. 괄호를 생략하면 코드가 짧아 보일 수는 있지만, 어떤 문장이 조건문이나 반복문에 포함되는지 헷갈릴 수 있다. 따라서 의도가 명확하게 드러나도록 블록을 괄호로 감싸는 것이 안전하다.
또한 키워드와 다음 괄호 사이에는 공백을 두는 것이 좋다. 이렇게 하면 제어 구조와 메서드 호출을 더 쉽게 구별할 수 있다.
2.6. 문장과 수식
문장이나 수식은 너무 복잡하게 한 줄에 작성하기보다 필요한 경우 메서드나 클래스로 분리하여 작성하는 것이 좋다. 복잡한 로직을 적절한 단위로 나누면 코드의 의미를 더 쉽게 파악할 수 있고, 유지보수도 쉬워진다.
블록 문장도 명확하게 작성해야 한다. 특히 if문이나 반복문 같은 제어 구조가 중첩되어 있을 때는 어떤 문장이 어떤 조건에 포함되는지 헷갈릴 수 있다. 따라서 블록을 사용하여 코드의 범위를 명확히 하면 중첩 구조에서 발생할 수 있는 혼란을 줄일 수 있다.
수식의 경우에는 괄호를 이용하여 연산 순서를 명확히 하는 것이 좋다. 연산자 우선 순위에만 의존하면 코드를 읽는 사람이 계산 순서를 바로 이해하기 어려울 수 있다. 따라서 괄호를 사용해 의도한 연산 순서를 명확히 표현하면 코드의 가독성이 높아진다.
2.7. 오류 처리
코딩을 할 때는 잘못된 데이터를 어떻게 다룰 것인지 고려해야 한다. 프로그램은 항상 정상적인 입력만 받는 것이 아니기 때문에 실제로 존재하지 않는 계좌번호가 입력되거나 메서드가 허용하지 않는 매개변수를 받는 경우도 발생할 수 있다.
따라서 오류 처리는 단순히 오류가 발생한 뒤 예외를 처리하는 것에 그치지 않고, 잘못된 입력이 들어오지 않도록 미리 방지하는 과정까지 포함된다. 예를 들어 특정 메서드가 car, truck, bus와 같은 값만 허용한다면, 그 외의 값이 들어왔을 때 어떻게 처리할지 정해야 한다.
입력 오류를 방지하는 방법으로는 리스트 박스를 사용하여 사용자가 정해진 선택지 중에서 고르게 하거나, 디폴트 값을 지정하는 방법이 있다. 이렇게 하면 사용자가 잘못된 값을 직접 입력할 가능성을 줄일 수 있다.
2.8. 주석
주석 작성은 코딩에 있어 중요한 요소이다. 디버깅하는 동안 도움을 얻을 수 있고, 다른 사람들이 프로그램을 더 쉽게 이해하도록 도와준다.
하지만 주석을 많이 작성한다고 해서 항상 좋은 것은 아니다. 불필요한 단어는 생략하고, 코드만 보아도 알 수 있는 내용을 반복해서 설명하는 과도한 주석은 피하는 것이 좋다. 좋은 주석은 코드의 의미, 제약 조건, 사용 방법처럼 코드만으로 바로 알기 어려운 내용을 설명해 주어야 한다.
2.8.1. 클래스 불변조건(invariant)
클래스 불변조건은 클래스의 속성이 가져야 하는 의미와 제약 조건을 설명하는 것이다. 예를 들어 특정 필드가 어떤 범위의 값을 가져야 하는지, 배열에 어떤 데이터가 저장되는지 등을 주석으로 나타낼 수 있다.
- private int hr; // The hour of the day, in 0..23.
- private double[] temps; // themps[0...numRecorded-1] are the recorded temperatures
- private int numRecorded; // number of temperatures recorded
위의 코드에서 hr은 하루 중 시간을 나타내며 0 ~ 23까지의 값을 가진다. temps는 기록된 온도 값을 저장하는 배열이고, numRecorded는 기록된 온도의 개수를 의미한다.
2.8.2. 메서드 주석
메서드 주석은 메서드가 어떤 일을 수행하는지 설명하는 주석이다. Java에서는 보통 Javadoc 형식으로 메서드의 기능, 매개변수, 반환값 등을 설명한다.
/**
* Calculate the area of the triangle.
*
* @param b one of the sides of the triangle.
* @return The area of the triangle
*/
메서드 주석을 작성할 때는 메서드가 수행하는 일을 명확하게 설명하고 관련된 매개 변수와 반환값을 함께 언급하는 것이 좋다.
예를 들어 다음과 같이 작성할 수 있다.
/** Print the sum of a and b */
public static void printSum(int a, int b) { ... }
2.8.3. 클래스, 문장 주석
클래스 주석은 클래스가 어떤 목적을 가지고 만들어졌는지 설명하는 주석이다. 파일의 시작 부분에는 해당 클래스의 용도를 설명하는 주석이 포함되는 것이 좋다. 이를 통해 다른 개발자는 클래스를 처음 보더라도 이 클래스가 어떤 역할을 하는지 쉽게 파악할 수 있다.
다음 예시를 살펴보자.
/** An object of class Auto represeents a car.
Author: ~
Date of last modification: ~ */
public class Auto { ... }
위 예시에서는 Auto 클래스의 객체가 자동차를 나타낸다는 것을 설명하고 있다. 이처럼 클래스 주석은 클래스의 역할, 작성자, 마지막 수정일 등을 나타낼 수 있다.
문장 주석은 특정 문장이나 코드 블록의 의도를 설명하는 주석이다. 코드가 어떤 동작을 하는지 단순히 반복해서 설명하기보다는, 왜 그렇게 작성했는지 또는 어떤 논리적 의미를 가지는지 설명할 때 사용한다.
다음 예시를 살펴보자.
// Truthify x >= y by swapping x and y if needed.
if (x < y) {
int tmp = x;
x = y;
y = tmp;
}
3. 설계에서 코드 생성
다음은 설계로부터 코드를 생성하는 과정이다. 설계 단꼐에서 작성한 UML 다이어그램은 실제 코드 구조를 만드는 데 활용될 수 있다. IDE 도구를 사용하면 UML 다이어그램으로부터 클래스, 메서드, 속성 등 원시 코드의 골격을 자동으로 생성할 수도 있다.
이 과정에서 중요한 것은 설계에 나타난 클래스 간의 관계를 코드로 어떻게 구현할 것인지이다. 특히 클래스 사이의 연관 관계는 1대1, 1대N, N대N 관계에 따라 구현 방식이 달라진다.
3.1. 연관의 구현
클래스 A와 B 사이에 1대 1 연관 관계가 있는 경우, 1대 다 연관 관계가 있는 경우, 다대 다 연관 관계가 있는 경우에 따라 코드를 구현 방식이 달라지게 된다. 연관 관계는 크게 3가지로 나눌 수 있다.
3.1.1. 1대 1 연관
1대 1 연관은 하나의 객체가 다른 하나의 객체와 관계를 가지는 경우이다. 이때 A에서 B의 메서드를 호출할 필요가 있다면, A가 B에 대한 참조를 갖도록 구현한다. 반대의 경우에는 B가 A에 대한 참조를 갖도록 구현한다.
3.1.2. 1대 다 연관
1대 N 연관은 하나의 객체가 여러 개의 객체와 관계를 가지는 경우이다. 클래스 A에서 여러 인스턴스 B의 메서드를 호출할 필요가 있다면, 클래스 A가 클래스 B의 참조를 컬렉션이나 리스트 형태로 가지고 있도록 구현한다.
예를 들어 하나의 회원이 여러 개의 게시글을 가진다면, 회원 클래스가 게시글 객체들의 목록을 참조하는 방식으로 구현할 수 있다.
3.1.3. 다대 다 연관
N대 N 연관은 여러 객체가 서로 여러 객체와 관계를 가지는 경우이다. 이러한 관계를 그대로 구현하면 구조가 복잡해질 수 있기 때문에 중간에 연관 클래스를 도입하여 1대 N 관계로 나누어 설계하기도 한다.
예를 들어 학생과 과목의 관계를 생각해 보면, 한 학생은 여러 과목을 수강할 수 있고, 한 과목도 여러 학생이 수강할 수 있다. 이 경우 수강 정보를 나타내는 중간 클래스를 두면 학생과 수강 정보, 과목과 수강 정보의 1대 N 관계로 나누어 구현할 수 있다.
3.2. 시퀀스 다이어그램의 구현
시퀀스 다이어그램은 객체들 사이에서 메시지가 어떤 순서로 전달되는지를 보여주는 다이어그램이다. 시퀀스 다이어그램을 코드로 구현할 때는 메시지를 보내느 객체와 메시지를 받는 객체를 구분해야 한다.
만약 A 객체에서 B 객체로 나가는 메시지가 표시되어 있다면, 해당 메시지에 대응되는 메서드는 B 클래스에 정의되어야 한다. 즉, 메시지를 받는 객체가 그 동작을 수행할 책임을 가진다.
다음은 도서 대출 기능을 처리하는 CheckoutController 예시이다.
public class CheckoutController {
Patron p;
public String checkout(String callNo) {
BDNgr dbm = new DBMge();
Document d = dbm.getDocument(callNo);
String msg = " ";
if (d != null) {
Loan != new Loan(p, d);
dbm.save(l);
d.setAvailable(false);
dbm.save(d);
msg = "Checkout successful.";
} else {
msg = "Document not found.";
}
return msg;
}
}
위 코드에서 CheckoutController는 도서 대출 요청을 처리하는 컨트롤러 역할을 한다. 사용자가 도서 번호인 callNo를 입력하면 DBMgr 객체의 getDocument(callNo) 메서드를 호출하여 해당 문서를 찾는다.
여기서 getDocument() 메시지를 받는 객체는 DBMgr로, getDocument() 메서드는 DBMgr 클래스에 정의되어야 한다. 또한 대출 정보인 Loan 객체를 생성한 뒤, dbm.save(l)을 호출하여 대출 정보를 저장하고, d.setAvailable(false)를 호출하여 문서의 대출 가능 상태를 변경한다.
4. 리팩토링
리팩토링은 프로그램의 외부 동작은 변경하지 않고, 코드의 내부 구조를 재조정하는 과정이다. 즉, 기능의 결과는 그대로 유지하면서 코드의 디자인을 안전하게 개선하는 기술이다.
리팩토링이 필요한지 판단할 때는 코드스멜을 확인할 수 있다. 코드스멜은 코드에 문제가 있을 가능성을 알려주는 징후를 의미한다. 대표적인 코드 스멜을 다음과 같으며 리팩토링 하는 방법은 다음과 같다.
| 코드 스멜 | 설명 | 리팩토링 |
| 중복된 코드 | 기능이나 데이터 코드가 중복된다. | 중복을 제거한다. |
| 긴 메서드 | 메서드 길이가 너무 길다. | 메서드를 적정 수준의 크기로 나눈다. |
| 큰 클래스 | 한 클레스에 너무 많은 속성과 메서드가 존재한다. | 클래스 몸집을 줄인다. |
| 긴 파라미터 리스트 | 메서드의 파라미터 개수가 너무 많다 | 파라미터 개수를 줄인다. |
| 두 가지 이상의 이유로 수정되는 클래스 | 한 클래스의 메서드가 2가지 이유로 수정되면, 그 클래스는 한 가지 종류의 책임만을 수행하는 것이 아니다. | 한가지 이유만으로도 수정되도록 변경한다. |
| 여러 클래스를 동시에 수정 | 특정 클래스를 수정하면 그때마다 관련된 여러 클래스들 내에서 자잘한 변경을 해야 한다. | 여러 클래스에 흩어진 유사한 기능을 한 곳에 모이게 한다. |
| 다른 클래스를 지나치게 애용(Feature Envy) | 빈번히 다른 클래스로부터 데이터를 얻어 와서 기능을 수행한다 | 메서드를 그들이 애용하는 데이터가 있는 클래스로 옮긴다. |
| 유사 데이터들의 그룹 중복 | 3개 이상의 데이터 항목이 여러 곳에 중복되어 나타난다. | 해당 데이터들은 독립된 클래스로 정의한다. |
| 기본 데이터 타입 선호(Primtive Obsession) | 객체 형태 그룹을 만들지 않고, 기본 데이터 타입만 존재한다. | 같은 작업을 수행하는 기본 데이터 그룹을 별도의 클래스로 만든다. |
| Switch, if문 | switch 문장이 지나치게 많은 case를 포함한다. | 다형성으로 바꾼다(같은 메서드를 가진 여러 개의 클래스를 구현한다). |
| 병렬 상속 계층도(Parallel Inheritance Hierarchies) | [Shotgun Surgery]의 특별한 형태로, 비슷한 클래스 계층도가 지나치게 많이 생겨 중복을 유발한다. | 호출하는 쪽의 계층도는 그대로 유지하고 호출당하는 쪽을 변경한다. |
리팩토링의 목적은 소프트웨어의 디자인을 개선하고 코드를 이해하기 쉽게 만들며, 버그를 찾기 쉽게 하는 것이다. 또한 코드 구조가 정리되면 이후 기능을 더 빠르게 작성하고 수정할 수 있다.
다음 예시를 통해 코드스멜과 리팩토링 과정을 살펴보자.

이 예시는 아주 긴 메서드는 아니지만, if문 안에 서로 값을 바꾸는 swap 기능이 직접 작성되어 있다. 이렇게 특정 기능을 수행하는 코드가 메서드 안에 그대로 포함되어 있으면, 메서드의 역할이 복잡해지고 코드의 의도를 한 눈에 파악하기 어려워질 수 있다.
이 경우에는 swap 기능을 별도의 메서드로 추출하는 방식으로 리팩토링할 수 있다. 즉, if문 안에 직접 작성되어 있던 값 교환 코드를 swap() 메서드로 분리하고, 기존 위치에서는 swap() 메서드를 호출하도록 수정한다.
이렇게 하면 기존 프로그램의 실행 결과는 변하지 않지만, 코드의 구조는 더 명확해진다. 또한 swap 기능이 별도의 메서드로 분리되었기 때문에 코드의 가독성이 높아지고 이후 같은 기능이 필요할 때 재사용하기도 쉬워진다.
5. 코드 품질 향상 기법
리팩토링까지 완료한 뒤에도 코드 품질을 향상시키기 위한 활동이 필요하다. 대표적인 코드 품질 향상 기법에느 ㄴ코드 인스펙션, 정적 분석, 페어 프로그래밍 등이 있다.
5.1. 코드 인스펙션
코드 인스펙션은 프로그램을 직접 읽어 보고 눈으로 확인하면서 결함을 찾아내는 방법이다. 일반적으로 프로그램이 성공적으로 컴파일되고, 정적 분석 도구에 의해 기본적인 검사가 이루어진 후에 수행된다.
이 과정에서는 코드에 숨어 있는 결함을 찾아내고 코드가 코딩 표준을 잘 지키는지, 효율성에 문제가 없는지 등을 확인한다. 즉 단순히 코드가 실행되는지만 보는 것이 아니라 코드의 품질과 구조까지 함께 검토하는 과정이다.
이 과정에서는 발견된 결함에 대해 심각도와 타입을 구분할 수 있다. 심각도는 결함의 우선순위를 나타내며, 어떤 문제를 먼저 해결해야 하는지 판단하느 ㄴ기준이 된다. 결함의 타입에는 로직 문제, 컴퓨팅 문제, 인터페이스 또는 타이밍 문제, 데이터 처리 문제 등이 있다.
5.2. 정적 분석
정적 분석은 프로그램을 실행하지 않고, 프로그램 텍스트를 조직적으로 분석하여 결함을 찾아내느 방법이다. 정적 분석은 소프트웨어 도구를 이용하여 자동으로 수행할 수 있다.
정적 분석에서 결함을 찾는 방법은 크게 두 가지로 볼 수 있다. 첫 번째는 코드에 존재하는 비정상적인 패턴이나 원하지 않는 패턴을 방법이다. 두 번째는 실행 시 프로그램의 고장을 일으킬 수 있는 코드 상의 결함을 직접 찾는 방법이다.
예를 들어 선언되지 않은 변수를 사용했는지, 실행되지 않는 데드 코드가 존재하는지, 중복되 조건이나 불필요한 배정문이 있는지 등을 검사할 수 있다. 이로 인해 컴파일러가 잡아내지 못하는 문제도 발견할 수 있다.
정적 분석에서는 자료 흐름을 분석하여 자료 변칙(data anomaly)을 찾을 수 있다. 자료 변칙은 변수가 정의되지 않은 상태에서 사용되거나 정의된 값이 정상적으로 사용되지 않는 등 자료의 정의와 사용 관계에서 나타나는 비정상적인 패턴이다.
또한 정적 분석을 통해 사족(redundant) 코드도 찾을 수 있다. 사족 코드는 프로그램에 불필요하게 포함된 코드로, 중복된 배정문, 데드 코드, 조건 중복 등이 이에 해당한다. 이러한 문제는 컴파일러에 의해 항상 검출되는 것은 아니기 때문에 정적 분석 도구를 활용하여 확인하는 것이 좋다.
5.3. 테스트 중심 개발
테스트를 위한 코드를 작성한 후 기능을 구현하는 방식으로 주로 클래스 안에 있는 메서드를 테스트한다. 개발 과정은 다음 그림과 같다.

5.4. 짝 프로그래밍
짝 프로그래밍은 두 사람이 같은 컴퓨터를 사용하면서 함께 프로그래밍하는 방식이다. 한 사람은 코드를 작성하고, 다른 사람은 코드를 검토하거나 방향을 제시하면서 함께 문제를 해결한다.
혼자 개발할 때보다 스트레스를 줄이는 데 도움이 될 수 있다. 문제를 함께 고민하고 해결하기 때문에 개발 과정에서 느끼는 부담을 나눌 수 있으며, 성공했을 때의 성취감도 함께 공유할 수 있다.
또한 파트너와 계속해서 아이디어를 교환하기 때문에 팀의 소통을 향상시킬 수 있다. 서로의 코딩 방식과 문제 해결 방식을 이해하게 되며, 그 과정에서 팀원 간의 상호 이해와 협력도 높아진다.
그리고 두 사람이 함께 토론하면서 코드를 작성하기 때문에 더 창의적인 해결 방법을 찾을 수 있다. 결과적으로 간단하고 효율적인 해법을 만들어 내는 데 도움이 된다.
정리하면 짝 프로그래밍은 두 사람이 함께 코딩하면서 코드 품질을 높이고, 팀워크와 문제 해결 능력을 향상시키는 개발 방식이라고 할 수 있다.
'학교공부 > [소프트웨어공학]' 카테고리의 다른 글
| [소프트웨어공학] - 유지보수 (0) | 2026.06.08 |
|---|---|
| [소프트웨어공학] - 테스팅 (1) | 2026.06.08 |
| [소프트웨어공학] - UI 설계 (0) | 2026.06.08 |
| [소프트웨어공학] - 설계 원리 (1) | 2026.04.21 |
| [소프트웨어공학] - 요구 모델링 (0) | 2026.04.21 |