위에서 설명한 것과 같이 JAVA는 객체지향적 언어 답게
가능한 모든 것을 객체화해 관리하려고 하는 부분이 인상 깊다.
그렇다면 이이서 C#의 힙 영역과 스택 영역에 대해서 살펴보자.
기본적으로 JAVA의 스택 영역과 힙 영역와 비슷하다고 보면된다.
정적 할당이 필요할 때는 스택을 사용하며,
동적 할당이 필요할 경우는 힙을 사용한다.
하지만,
재미 있게도 C#은 JAVA와 조금 다른 관리 기법을 사용하는데
먼저 아래의 코드를 보자.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | public void Method1(){ int x = 1; int y = 2; Calculator cal1 = new Calculator(); } public void Method2(){ int x = 3; object i = x; int y = x; } | cs |
위와 같은 Method1과 Method2가 있다고 가정해보자.
먼저 Method1을 살펴보자.
| public void Method1(){ int x = 1; int y = 2; Calculator cal1 = new Calculator(); } | cs |
기본적으로 C#은 2번째 코드와 3번째 코드는 스택 영역에 쌓이게 된다.
2번째 코드가 먼저 실행됐으니,
순서적으로는 위와 같이 2번째 위에 3번째 코드가 쌓이게 될 것이다.
그렇다면 4번째 코드는 어디에 쌓이게 될까?
지금까지 내용을 바탕으로 예상해보자면
위의 사진과 같이
힙 영역에 쌓일 가능성이 높아 보인다.
하지만 예상과 다르게 4번째 코드는 스택으로서 쌓인다.
위와 같이 스택에는 해당 객체의 주소값을 담고 있는 포인터 값으로 존재한다.
따라서 4번째 코드와 같은 객체들은
실제 힙 영역에서 존재하고 관리하게 되며
실행 순서는 포인터로서 스택에 저장 된다고 할 수 있다.
그렇기 때문에 스택으로서 해제될 때는
단순히 담겨 있는 주소 값이 삭제되기 때문에
힙 영역에서의 객체는 그대로 남겨져 있게 된다.
따라서 간단한 이 3줄의 코드 실행 순서는 스택에 의해 관리되며,
스택에는 4번째줄, 3번째줄, 2번째줄이 저장되게 된다.
다만, 4번째 줄의 데이터안에는 포인터 값만 존재하며
실제 객체는 힙 영역에서 관리 한다.
이 Method1라는 메소드가 실행이 완료 되면
스택에 할당되어 있던 대로 차례대로
4번째 코드,3번째 코드, 2번째 코드가 해제 되지만,
4번째 코드는 포인터가 데이터 값으로 들어 있으므로
실제 힙 영역에 존재하고 있는 Calculaotr라는 메소드는 메모리가 해제되지 않는다.
이는 후에 가비지 컬렉터에 의해 해제 된다.
이 외에 스택이 힙 보다 비교적 빠르고 안전하다는 것은 동일하며
다른 점이라고 한다면 힙 영역에 존재하는 것 또한
포인터로서 스택에 관리되는 것 정도일 것이다.
다음으로 Method2를 살펴보기 앞서서
이해를 좀 더 편하게 하기 위해 Method2를
각각 Method3과 4으로 나누겠다.
먼저 Method3의 코드는 다음과 같다.
| public void Method3(){ int x = 3; int y = x; } | cs |
3번째 코드에서 x를 정의했고,
5번째 코드에서 y를 정의하고, y의 값을 x를 참조하게 끔 구성했다.
이 메소드의 경우 5번 째 줄은 x의 값을 참조하지만,
재미있게도 C#에서 5번째 줄은 힙이 아닌 스택에서 관리하게 된다.
하지만 이렇게 선언된 이 후 이 두 가지 x와 y값은 별개로서 존재 한다.
즉, 선언 이후에는 x값을 바꾼다고 하더라도, y값이 변하지 않는다는 말이다.
예컨데, 위의 코드의 경우
각각 x와 y의 값은 3이 값으로서 저장될 것이다.
하지만 이 후 x를 4로 바꾸는 코드를 넣고
이를 실제 실행해 메모리에서 해제된다하더라도
y가 4로 변하지는 않는다는 이야기 이다.
이는 단순히 처음 변수 y를 정의할 때 변수 x자체가 아닌
안에 담겨 있는 값인 3만을 가져 온 것이기 때문에
이 후에 변수 x 값을 바꾼다 하더라도 변수 y의 값이 변하지 않는다.
이는 C 계열 언어에서 자주 나타나는
값에 의한 호출(Call by Value)와 동일한 개념이다.
이를 C#에서는 값 유형(Value Type)이라고 부르며
이와 반대의 경우는 참조 유형(Reference Types)이라 부른다.
참조 유형 또한 참조에 의한 호출(Call by reference)과 동일한 개념이다.
이어서 Method4을 살펴보자.
| public void Method4(){ int x = 3; object i = x; } | cs |
그렇다면 위와 같이 3번째 줄과 5번째 줄의 유형이 다르면 어떻게 될까?
이 경우 C#에서는 자동적으로 값 유형(Value Type) 3의 값을
참조 유형(Reference Types)으로 바꾸어서 변수 i에 저장해 준다.
왜냐하면, 이런 처리를 해주지 않으면 당연히
타입이 맞지 않아 컴파일러 선에서 걸러지기 때문이다.
이를 Boxing이라고 하며,
이에 반대인 경우를 UnBoxing이라고 한다.
쉽게 말해서 C#에서는 이를 자동적으로
형 변환(Type Conversion)을 해준다고 이해하면 편할 것이다.
하지만, JAVA에서 String 관련,
특히 값과 값을 이을 때(+ 구문으로) 성능 저하가 발생하듯이
자동적으로 형변환 해주는 것들은
이것이 반복되면 반복될 수록 엄청난 퍼포먼스의 저하를 발생 시킨다.
동일하게 C#에서 이런 Boxing, UnBoxing 또한
퍼포먼스가 저하되기 때문에 금지까지는 아니지만
가급적 사용하지 않는 것을 권장 한다.
이에 대한 비교는 딱히 내가 하지 않아도
다른 사이트에서 비교를 해놓았기 때문에 아래의 링크를 참고하길 바란다.
위의 사이트에서는 직접 실행해 Boxing이 UnBoxing보다
큰 성능 저하를 나타내는 것을 검증한 것을 보여 주며,
여기서도 또한
가능한 Boxing과 Unboxing을 사용하지 않도록 권장하고 있다.
결론
여기까지 해서 스택과 힙에 대한 기본적인 설명과
JAVA와 C계열인 C#에서 어떻게 이 둘을 활용하는지에 대해 살펴보았다.
개인적으로는 모든 것을 객체화 하려하는 JAVA보다는
C#의 경우가 스택과 힙에 대한 사용이 좀 더 퍼포먼스가 좋지 않나 싶다.
엔터프라이즈 급 시스템을 동일하게
두 언어로 개발한다면 C#의 경우가 눈에 띄게 좋지 않을까 싶다.
물론 그렇다고 하더라도
여러 글에서도 이야기 했듯이
무조건 C#으로 개발하는 것이 좋다고는 할 수 없다.
왜냐하면 모든 것은 상황에 맞게 조절해야하며,
이는 아키텍터의 몫이기 때문이다.
아무리 C#이 좋다고 한들,
비교적 뛰어난 C# 엔지니어들이 JAVA 엔지니어보다
부족하다면, 당연히 JAVA쪽을 선택하는 것이 훌륭한 아키텍터라고 생각 한다.
왜냐하면 익히 알고 있듯이 클라이언트들에게
이런 퍼포먼스 문제에 그 다지 관심이 없기 때문이다.
그들에게 있어서 중요한 관심 대상은
얼마나 빠른가, 얼마나 버그가 없는 좋은 품질인가, 최신 기술인가 보다는
정해진 예산내에 적절한 품질의 제품을 개발해 주길 원하기 때문이다.
이런 것들에 대한 포인트를 정확히 잡는 것이
아키텍터가 해야 할 과제가 아닌가 싶다.