들어가기 전에
컴퓨터는 우리가 작성한 프로그램을 구동하기 위해 다양한 물리적 장치를 사용합니다. 그 중 하나는 메모리로, 프로그램이 필요한 정보가 저장되는 곳입니다. 메모리의 용량은 무한하지 않기 때문에, 때때로 프로그램에서 우리가 의도하지 않은 오류가 발생하기도 합니다.
학습 목표
메모리 용량이 프로그램의 구동에 미치는 영향을 설명할 수 있습니다.
핵심 단어
- 메모리
- 오버플로우
강의 듣기
들어가기 전에
컴퓨터는 우리가 작성한 프로그램을 구동하기 위해 다양한 물리적 장치를 사용합니다. 그 중 하나는 메모리로, 프로그램이 필요한 정보가 저장되는 곳입니다. 메모리의 용량은 무한하지 않기 때문에, 때때로 프로그램에서 우리가 의도하지 않은 오류가 발생하기도 합니다.
학습 목표
메모리 용량이 프로그램의 구동에 미치는 영향을 설명할 수 있습니다.
핵심 단어
강의 듣기
컴퓨터는 RAM(랜덤 액세스 메모리)이라는 물리적 저장장치를 포함하고 있습니다. 우리가 작성한 프로그램은 구동 중에 RAM에 저장되는데요, RAM은 유한한 크기의 비트만 저장할 수 있기 때문에 때때로 부정확한 결과를 내기도 합니다.
부동 소수점 부정확성
아래와 같이 실수 x, y를 인자로 받아 x 나누기 y를 하는 프로그램이 있다고 해봅시다.
#include <cs50.h>
#include <stdio.h>
int main(void)
{
// 사용자에게 x 값 받기
float x = get_float("x: ");
// 사용자에게 y 값 받기
float y = get_float("y: ");
// 나눗셈 후 출력
printf("x / y = %.50f\n", x / y);
}
나눈 결과를 소수점 50자리까지 출력하기로 하고, x에 1을, y에 10을 입력하면 아래와 같은 결과가 나옵니다.
x: 1
y: 10
x / y = 0.10000000149011611938476562500000000000000000000000
정확한 결과는 0.1이 되어야 하지만, float 에서 저장 가능한 비트 수가 유한하기 때문에 다소 부정확한 결과를 내게 되는 것입니다.
정수 오버플로우
비슷한 오류로, 1부터 시작하여 2를 계속해서 곱하여 출력하는 아래와 같은 프로그램이 있다고 해봅시다.
#include <stdio.h>
#include <unistd.h>
int main(void)
{
for (int i = 1; ; i *= 2)
{
printf("%i\n", i);
sleep(1);
}
}
우리가 변수 i를 int로 저장하기 때문에, 2를 계속 곱하다가 int 타입이 저장할 수 있는 수를 넘은 이후에는 아래와 같은 에러와 함께 0이 출력될 것입니다.
...
1073741824
overflow.c:6:25: runtime error: signed integer overflow: 1073741824 * 2 cannot be represented in type 'int'
-2147483648
0
0
...
정수를 계속 키우는 프로그램에서 10억을 넘기자 앞으로 넘어갈 1의 자리가 없어진 것입니다.
int에서는 32개의 비트가 다였기 때문입니다. 그 이상의 숫자는 저장할 수 없는 것입니다.
이런 오버플로우 문제는 실생활에서도 종종 발견됩니다.
1999년에 큰 이슈가 되었던 Y2K 문제는 연도를 마지막 두 자리수로 저장했던 관습 때문에 새해가 오면 ‘99’에서 ‘00’으로 정수 오버플로우가 발생하고, 새해가 2000년이 아닌 1900년으로 인식된다는 문제였습니다.
그리고 세계는 수백만 달러를 투자해서 프로그래머들에게 더 많은 메모리를 활용해서 이를 해결하도록 하였습니다.
이는 통찰력 부족으로 발생한 아주 현실적이고 값비싼 문제였습니다.
또한 다른 사례로 비행기 보잉 787에서 구동 후 248일이 지나면 모든 전력을 잃는 문제가 있었습니다.
왜냐하면 강제로 안전 모드로 진입하였기 때문입니다.
이는 소프트웨어의 변수가 248일이 지난 뒤에 오버플로우가되어 발생하였기 때문이었습니다.
248일을 1/100초로 계산하면 대략 2의 32제곱이 나옵니다.
보잉을 설계할때 사용한 변수보다 너무 커졌던 것입니다.
이를 해결하기 위해 주기적으로 재가동을 하여 변수를 다시 0으로 리셋했습니다.
따라서 다루고자 하는 데이터 값의 범위를 유의하며 프로그램을 작성하는 것이 중요합니다.
생각해보기
Y2K와 보잉787과 같은 문제를 방지하기 위해서는 프로그램을 어떻게 설계해야 할까요?
comment
데이터 값의 범위를 계산해서 메모리를 할당한다. 비용문제나 다른 기타 문제들로 넉넉한 메모리 공간을 확보할 수 없다면, 주기적으로 리셋한다.
큰 메모리값으로 설정한다
byte에 들어가는 8bit를 16bit 또는 그 이상으로 만들면 int 값(4byte)의 경우 2의 64승으로 물리적 저장공간을 늘리고 Y2K의 연도를 모두 기록하여 관리하면 될 것으로 본다. 보잉787의 경우는 bit를 16개로 늘리면 1000년 10000년도 더 사용 할 수있는 데이터 공간이 확보 될 것으로 생각됩니다.
오버플로우가 발생하지 않도록 데이터값의 범위를 유의해서 큰 메모리값을 바탕으로 프로그램을 설계한다.
메모리의 크기를 늘리거나 주기적으로 리셋한다.
오버플로우를 방지하기 위한 리셋값 설정
변수의 최댓값 계산 후 메모리 할당
메모리를 추가한다
변수 선언 시, 자료형에 주의한다
오버플로우가 발생하지 않게 메모리를 추가한다.
변수의 최대 크기를 고려 해서 메모리를 할당
0으로 리셋하거나 오버플로우가 발생하지 않게 메모리를 최대한 효율적으로 사용할 수 있게 코딩을 해줘야합니다
미리미리 사용할 비트수를 잘 설계한다
코드 작성 시 변수의 범위에 대해 고려하여 프로그래밍 하고, 오버플로우가 된다면
메모리를 더 늘리거나, 오버플로우가 되기 전 초기화를 한다.
오버플로우가 발생하지 않게 해야한다.
오버 플로우가 발생하지 않게 하려면?
저장공간을 더 크게 설정하거나
오버 플로우가 발생하기 전에 초기화를 해야한다.
변수에 할당되는 저장 공간을 더 크게 설정 or 오버플로우 발생 전 초기화 실행되도록 루프 작성
변수에 할당되는 저장공간을 더 크게 설정한다. 즉, 더 큰 자료형을 사용한다. (int >> bigint)
만약 물리적인 여건으로 불가능하다면, 오버플로우가 발생하기전에 초기화가 실행되도록 루프를 작성한다.
변수가 가질 수 있는 값의 범위를 고려해서 적절한 데이터 타입을 선택해야 해. 예를 들어, 만약 어떤 값이 32비트 정수의 범위를 넘어갈 수 있다면, 더 큰 범위를 가진 데이터 타입을 사용해야 해.
연산을 수행할 때 오버플로우가 발생할 수 있는지 검사해야 해. 이는 특히 덧셈, 곱셈 등의 연산에서 중요해.
오버플로우가 발생하면 적절하게 처리할 수 있도록 에러 처리 코드를 작성해야 해. 이는 프로그램이 예기치 않은 방식으로 동작하는 것을 방지해.
가능한 모든 입력 값에 대해 코드가 올바르게 동작하는지 확인하기 위해 유닛 테스트를 작성해야 해. 이는 특히 경계값에 대해 중요해.
두 가지 케이스와 같은 문제를 피하기 위해선, 시간과 날짜를 처리하는 방법에 대해 잘 이해하고 있어야 해. 이는 특히 시간대, 윤년 등의 복잡한 사항을 고려해야 해.
변수에 할당되는 저장공간을 더 크게 설정한다. 즉, 더 큰 자료형을 사용한다. (int >> bigint)
만약 물리적인 여건으로 불가능하다면, 오버플로우가 발생하기전에 초기화가 실행되도록 루프를 작성한다.
주어진 비용 내에서 메모리를 넉넉하게 확보하고 필요하지 않는 데이터의 저장을 지양하는 등 메모리를 효율적으로 활용하고 확장성이 좋은 코드를 작성한다.
이후 가능한 모든 예외 케이스를 시뮬레이션 하고 이에 맞는 대비책을 세워둔다.
물리적 메모리 크기에 한계가 있다면, 데이터가 오버플로우 되기 직전 일정 값으로 초기화한다.
구버전을 판매 중단하고, 새로운 버전을 만들어서, 비싸게 판다!