서론
Variable(변수)는 변하는 값이라는 뜻이다.
프로그래밍에서의 변수라는 개념도 개념적으로는 동일하다.
프로그래밍을 처음으로 시작한 2019년 3월부터,
졸업을 앞둔 2025년 2월까지 개발 피지컬은 조금씩 늘어왔다. (여전히 못한다.)
그리고 변화양상은, 내가 개발에 있어서 기본적인 정의들을 어떻게 대했는가에서 드러난다.
대학생활의 마무리를 지으며,
'변수'에 대한 관점의 변화를 회고하려 한다.
변수란 무엇인가?
Wiki백과에서는, "하나의 값을 저장할 수 있는 기억공간"이라고 서술한다.
변수의 목표는 값을 재사용하는 것이다.
우리가 원하는 '단어'로 값을 사용하고 싶은 것이다.
그렇다면 다음의 조건을 충족해야 한다.
1. 메모리 어딘가에 저장되어야 한다.
2. 변수이름으로 그 값을 찾아낼 수 있어야 한다.
단순화하면,
어떻게 저장할 것인가?, 어떻게 찾을 것인가? 의 고민이다!
사막(메모리)에서 바늘(변수) 찾기
메모리에 빈 공간이 있으면, 위의 테트리스처럼 빈 공간에 값을 저장하면 끝이다.
위의 테트리스는 눈으로 구분이 잘 된다.
하나의 블록은 하나의 색으로만 이루어져 있고.
블록의 특징마다 색이 다 다르기 때문에 구분하기가 쉽다.
하지만 컴퓨터는 0과 1로만 살아간다.
0과 1이 규칙성 없이 나열된 덩어리에서 저장한 값을 찾는 것은 꽤나 힘들어 보인다.
위의 테트리스에서 색이 사라지고, 0과 1의 무작위로 배치된 것처럼 보일 것이다.
저장된 값을 찾아내려면,
1. 시작점을 알아야 한다.
2. 끝점을 알아야 한다.
3. 어떻게 해석할지를 알아야 한다.
꽤나 어려워 보이는 문제이다.
'포인터'라는 말을 들어본 적이 있는가?
값을 가리키는 주소값을 의미한다.
1번 문제인 시작점은, 주소 자체를 기억하면 된다.
값이 저장된 장소의 첫 번째 바이트의 주소를 변수명에 매핑하면 된다.
2번과, 3번 문제는 자료형으로 해결하니 마저 보도록 하자.
자료형은 변수의 해석방식이다
변수명이 시작점을 기억한다면, 끝점과 해석방식은 누가 기억하고 있을까?
내가 감명받은 책 시리즈 중 하나인, "독하게 시작하는 C프로그래밍(최호성)".
위 책에서 자료형의 정의를 다음과 같이 내린다.
메모리에 특정한 위치에 있는 값을 해석하는 방식
- 최호성
정적언어를 다룰 때 우리는 타입을 명시한다. int, float, str..처럼 말이다.
자료형의 특징은, 타입별로 사이즈를 가진다는 것이다.
시작점을 알고 타입을 안다면, 몇 칸을 더 읽을지 알 수 있고
읽어 들인 0과 1 뭉치들을 숫자로 평가할지, 문자로 평가할지를 정할 수 있다는 것이다.
자료형이 위 2번 문제와 3번 문제를 해소한다.
int a = 10;
이 보잘것없는 한 줄에, 세 가지 미학이 있다.
a라는 명칭에는 값이 시작되는 주소값이 있다는 뜻.
int라는 명칭에는 시작점으로부터 4 bytes를 읽으라는 뜻. 읽은 값은 정수로 평가하라는 뜻.
당연한 이야기지만, 꽤나 아름답다고 생각한다.
생각 없이 배웠고, 사용했었던 변수란 참 많은 묘리가 담긴 것이다.
원시타입과 참조타입 변수
변수란 몰까.. 고민을 하거나 책을 읽다 보면
Primitive(원시) 타입과, Reference(참조) 타입이라는 단어를 마주한다.
단순하게 보면 값 자체를 저장하는가, 값의 주소를 저장하는가의 차이이다.
c언어를 처음 배울 때 다음과 같은 생각을 했었다.
값 자체로 변수에 저장할 수 있고, 값의 주소값(포인터)도 변수에 저장할 수 있다.
왜 하나의 방식이 아닌, 두 가지의 방식을 이용하는 거지?
- 20살의 나
이론적 관점으로 위를 대답한다면
stack, heap 저장되는 영역이 다르다.
이는 정적 크기와 동적 크기로 나뉜다.
즉, 메모리를 일관성 있고 compact 하게 다루는 방법.
실용적 관점으로 위를 대답한다면
값으로 비교할 것인가, 고윳값(주소)으로 비교할 것인가의 여부.
메서드가 존재하는가의 여부.
즉, 표현력과 고유성을 다루는 방법.
위 관점에 대한 수순을 밟아보도록 하자. 살짝 어색할 수 있다.
난 체계고 뭐고 손에 잡히는 대로 입에 넣으면서 공부했기 때문이다.(이때 공부 좀 똑바로 할 걸)
OOP, 행동이 타입(자료형)을 결정한다
Python, Js 같은 언어를 하면 다음 같은 광경을 볼 수 있다.
a = 'hello' + '3'
b = 1 + 3
print(a) # hello3
print(b) # 4
let a = 'hello' + 3
let b = 1 + 3
console.log(a) // hello3
console.log(b) // 4
문자 + 숫자는, 문자가 숫자보다 표현범위가 넓으니까 먹히는 게 당연하다고 생각했다.
숫자 + 숫자는, 당연히 숫자라고 생각했다. (세상에 당연한 건 죽음 말고 없다.)
컴파일러 입장에서 생각해 보자.
분명 같은 +인데, 어떠한 경우에는 이어 붙여야 하고
어떠한 경우에는 순수하게 값을 더해야 한다.
이는 자료형(타입)이 결정한다.
str객체는, +연산에 대해 이어 붙이도록 구현되어 있다.
int객체는, +연산에 대해 합을 구하도록 구현되어 있다.
연산에 대해서 어떻게 동작하는가는, 어떤 타입인가를 정의한다.
특정 타입이기에 특정 행동을 하는 것이 아니다.
특정 행동을 하기에, 특정 타입인 것이다.
- 객체지향의 사실과 오해(조영호)
이 책도, 내가 감명받은 책 시리즈 중 하나. (이름에 '호'자 들어간 사람들이 책을 잘 쓰신다)
순수한 값만으로는 이 역할을 수행하기 힘들다.
추가적인 동작, 참조, 즉 객체가 있어야 가능하다.
객체는 순수한 값보다 표현력이 풍부하다.
그 이유는, 알아서 지멋대로 원시 값을 객체로 바꾼다..!
이를 auto_boxing, wrapper_class등으로 들어봤을 것이다.
원시값을 하나하나 객체화 해서 연산을 하기는 귀찮으니,
연산을 시도하면, 객체화 해서 처리하고 다시 원시화 해주는 편한 기능이다.
단순한 관점에서 자료형이란, 값을 어떻게 해석할 것인가 이며
OOP 관점에서 자료형이란, 어떠한 행동을 할 수 있는가이다.
자료형도 변수 못지않게 심오하다.
객체가 원시값 보다 표현력이 높다면,
원시값의 장점은 무엇인가? 라는 생각이 필연적으로 든다.
표현력이 높다고 무조건 좋은건 아니다.
객체이지만, 원시스럽게 다루는 객체도 존재한다. (Entity, VO)
참조와 가비지 컬렉터(GC)
참조는, heap영역에서 사용되고 적절히 해제하지 않으면 메모리 누수가 발생한다.
메모리 누수 때문에 GC, Ownership등이 있는거 보면 중요한 주제다.
malloc, free등을 통해 명시적으로 메모리를 관리하기도 하고,
GC가 있는 언어는 알아서 메모리를 관리하기에 편하다.
GC의 방식은 다양하다 (STW, Reference-counting..)
이 사용하지 않는 변수를 어떻게 반환할 것인가? GC는 어떻게 동작할 것인가? 는 꽤나 심오한 주제다.
Python의 multi-thread가 왜 CPU-bound를 해결할 수 없는지(GIL-issue)를 이해할 수 있을 것이다.
쉽게 보면, 참조를 아무도 하지 않으면 GC가 삭제한다.
누가 쓰고 있는데 없어지면 문제가 생긴다는 심플한 원칙 때문이다.
여기서 또 의문이 든다...
누가 쓰고 있긴한데 없어져도 상관없을 수도 있잖아?
이와 관련한 개념으로 WeakRef(약한 참조)가 있다.
스코프의 생명주기와 클로저
그리고 만든 변수를 어디서 쓸 수 있는가? 또한 기본서에 나오는 중요한 내용이다.
정적 스코프 : 어디에 선언했는지를 중요하게 볼 것인가?
동적 스코프 : 어디서 호출했는지를 중요하게 볼 것인가?
일반적으로,
전역 스코프 : 프로그램 시작 ~ 프로그램 종료
지역 스코프 : 함수나 블록 생성 ~ 종료
블록 스코프 : { } 내부에서만 유효
위의 개념을 통해 변수의 충돌을 방지하고, 구조 및 가독성을 향상시킨다.
스코프 안에 있다는 건, 다시 사용할 수 있다는 것이며
고로 참조할 수 있기 때문에 GC에 먹히지 않는다.
여기서 스코프를 중첩한다면, GC는 먹을까, 먹지 않을까?
정답은 먹지 않는다.
외부 함수와 내부 함수를 조합하고, 그 사이의 스코프에
변수를 넣으면 그 변수는 내부 함수가 참조중이기에 살아 남는다.
GC에 먹히지 않는, 치외법권 영역을 만드는 느낌이다.
이 테크닉을 이용해서, 콜백등으로 활용할 수 있다.
최종 정리
처음엔, "값을 담는 상자에요~" 라고 배웠고.
다음엔, "값에 붙여둔 포스트 잇이에요~" 라고 배웠다.
스코프를 이해하고 나서 변수는,
중첩 스코프를 통해, 기믹적 프로그래밍을 할 수 있다는걸 배웠고,
스코프 운용의 중요성을 이해했다.
메모리를 이해하고 나서 변수는,
주소값과 자료형의 의미와 왜 중요한가를 배웠다.
OOP를 이해하고 나서 변수는,
단순히 값의 위치 뿐 아니라 행동을 보장한다는 것을 배웠고,
유연한 프로그래밍의 기반을 이해했다.
Reference에 대해 이해하고 나서 변수는,
Primitive와의 관계를 배웠고,
wrapper_class, auto-boxing을 이해했다.
GC를 이해하고 나서 변수는,
reference 기반의 관계를 배웠고,
알고리즘과 특성, 참조수준을 이해했다.
OOP를 사용하고 나서 변수는,
고유성이 중요한지, 값이 중요한지를 고민하게 됐고,
Entity와 VO를 이해했다.
변수에 대한 내 관점이 바뀌는 것을 되짚어 보니,
내가 진짜 4학년을 마치고 졸업을 한다는 생각이 든다.
이제서야 프로그래밍의 기초를 다 배웠구나.. 라는 안도감과 기대감.
다시 초심자의 마음으로 돌아가서, 공부하고 개발해야지
'프로그래밍 > 기본기' 카테고리의 다른 글
[작성 중] 적절하게 좋은 아키텍처를 향해 (0) | 2025.01.17 |
---|---|
Statement와 Expression (0) | 2024.12.10 |