닷넷 프로젝트에서 사용할 수 있는 프레임웍을 한눈에 볼 수 있도록 정리해보자
프레임웍을 구분하는 방법은 여러가지이나 간단하게 티어별로 구분하도록 한다. 

 
프리젠테이션 티어 (웹)
1. ASP.NET MVC - 2008년 1사분기에 출시 예정이 ASP.NET 기반의 MVC 프레임웍. 마이크로소프트 제공
   - 소개
   - 다운로드
   - 포럼
2. MonoRail - 오픈 소스 Castle 프로젝트에서 만든 Rail와 유사한 MVC 웹 프로임웍
3. Maverick.NET - 자바 Maverick의 닷넷 버전으로 오픈 소스 MVC 웹 프레임웍, 업데이트 안됨
4. DotNetNuke - 포털 프레임웍에 가까움. 오픈 소스 

 
비지니스 티어 (DI Container)
1. Spring.NET - 오픈 소스. 자바 Spring의 닷넷 버전. 대표적인 DI Container.
2. ObjectBuilder - Enterprise Library(EntLib)에 들어 있으며, EntLib, Composite UI Application Block 등에 사용된 DI Container
3. Unity - Enterprise Library 4.0 버전에 사용될 DI Container로 ObjectBuilder 후속작. EL과 별개로 사용될 수 있도록 별도 프로젝트로 진행중이며 Codeplex에 현재 2008년 2월 CTP 공개.
4. Windsor Container - 오픈 소스 Castle 프로젝트 일환
5. StructureMap - 닷넷에서 가장 오래된 DI Container. Jeremy D. Miller가 만들고 유지보수하고 있는 오픈 소스 프레임웍
6. NInject - "Lightning-fast dependency injection for .net"을 표방한 오픈 소스 DI Container. 닷넷 프레임웍 3.5 및 Compact Framework 3.5 지원, Silverlight 지원 등이 특징 

 
데이터 티어
1. ADO.NET
2. LINQ to SQL - .NET Framework 3.5의 기본 기능. 엔티티와 테이블의 1:1 매핑. VS2008 툴 지원
3. ADO.NET Entity Framework (LINQ to Entity)
4. NHibernate - 자바 Hibernate의 닷넷 버전. 오픈 소스
5. iBatis.NET - 자바 iBatis의 닷넷 버전. 오픈 소스
6. Active Record - 오픈 소스 Castle 프로젝트의 일환. Active Record 패턴을 구현. NHibernate 기반이나 Configuration을 XML이 아닌 Attribute을 이용
7. LLBLGen Pro - 상용.
8. WilsonORMapper - 상용.
9. LightSpeed - 상용.
10. Codus - 상용.
11. Sooda - 폴란드에서 만든 오픈 소스 ORM 으로 폴란드에서는 구직시 도움되는 기술.
12. SubSonic - 오픈 소스 ORM. ASP.NET 3.5 Extension에 포함된 ASP.NET Dynamic Data 기능 구현에 사용됨.
13. NConstruct - 상용 / 무료, NHibernate 용 코드 자동 생성(HBM, Entity 등), VS용 project 파일 생성 등 

 
All-in-one
1. DotNetNuke - 복잡한 웹 싸이트 구축, CMS에 까깝다고 할 수 있음
2. Oxite - ASP.NET MVC를 사용, 블로그 엔진, Mix Online 싸이트 운영 

 
기타 범용 프레임웍 (혹은 라이브러리)
1. Enterprise Library - MS patterns and practices(PnP) 팀이 Codeplex를 통해 제공. 캐싱, 로깅, 암호화 등등
2. NVelocity - 템플릿 엔진. 진전이 없어 Castle 프로젝트에서 별도 진행
3. NUnit - 단위 테스트
4. NAnt - 빌드
5. log4net - 로깅
6. CruiseControl.NET - 통합 빌드, ThoughtWorks에서 개발한 오픈 소스

 

출처 : http://kingcrap.com/4

ASP.NET 마스터하기 #3 - ASP.NET의 아키텍처

필자의 잡담~

잡담이 없어서... ^^; 안재우님의 블로그는 http://blog.naver.com/saltynut 입니다

지난 시간에 우리는 ASP.NET의 설치에 대해서 알아봤습니다. 여러분들 중에는 이미 이전에 ASP.NET을 설치해두신 분들도 계실 것이고, 행여나 제 글을 읽고 무작정 설치하신 분들도 계실 겁니다.

자, 그럼 일단 우리가 생각해봐야 질문은...

"ASP.NET이 대체 뭘까요?"

"내가 ASP.NET을 설치한 이유는 뭘까요? 어디다 써 먹으려고 설치하셨습니까?"

어떤 개발자들은 자신이 설치하는 녀석이 뭔지도 모르고 일단 설치부터 하고 보는 사람들이 많습니다. 왜? 남들이 다 하니깐. 좋은 거라고 하니깐. 새로 나온 거라고 하니깐.

ASP.NET 아키텍처 가르쳐 준다면서 왜 이런 질문을 하냐구요? 성격이랑 정체도 모르면서 얘가 왜 이런 아키텍처로 만들어 졌는지를 이해할 수 있겠습니까?

ASP.NET을 한 줄로 정의하자면, ‘.NET 프레임워크를 기반으로 한 웹 애플리케이션 개발 모델, 프레임워크, 관련 기술을 총칭해서 부르는 것’이라고 하겠습니다. MSDN의 정의로는 ‘엔터프라이즈 수준의 웹 응용프로그램을 최소한의 코딩으로 구축하는데 필요한 서비스를 포함하는 통합 웹 개발 모델’이라고 합니다.

이 정의는 간단한 내용이지만, 상당히 중요한 사항을 담고 있습니다.

첫째, ASP.NET은 .NET 프레임워크를 기반으로 한다는 점입니다. 따라서 .NET에서 제공하는 클래스 라이브러리를 사용할 수 있으며, 프로그래밍 언어로는 .NET이 제공하는 모든 언어를 사용할 수 있고, .NET 프레임워크 기반의 애플리케이션에 적용되는 모든 요소들이 ASP.NET에도 적용된다는 것입니다. 그 얘기는 .NET에 대해서 잘 알지 못하면서 ASP.NET을 잘 안다는 것은 불가능하다는 의미도 됩니다.

둘째, ASP.NET은 웹 애플리케이션을 만들기 위한 것이라는 점입니다. 따라서 일반적인 웹 애플리케이션의 특성을 그대로 가지며, 웹 애플리케이션이 수행 가능한 범위 내에서만 능력을 발휘할 수 있다는 점입니다.

셋째, ASP.NET은 ‘엔터프라이즈 수준’의 애플리케이션을 맞추는데 가장 큰 초점을 맞추고 있다는 점입니다. 따라서 간단한 애플리케이션 등을 만드는데는 최적의 개발 모델이 아닐 수도 있다는 점을 염두에 두어야 하며, 엔터프라이즈 애플리케이션을 개발할 때 고려해야 할 사항들에 대해서도 알 필요가 있다는 점입니다.

이 세가지가 별거 아닌 거 같지만, 이후에 ASP.NET을 이해하면서 이 3가지 사항을 잘 염두에 두고 있느냐 아니냐에 따라서 많은 차이가 나게 됩니다.

절대 변하지 않는 진리, 웹의 동작방식

두 번째 항목, ASP.NET이 웹 애플리케이션이라는 것은 ASP.NET이 웹이라는 한계를 벗어나지는 못한다는 것은 이미 강조된 사실입니다. 마찬가지로 웹의 동작방식은 적어도 당분간은 절대 변하지 않을 특성들을 다음과 같이 가지고 있습니다.

  • Request(요청) & Response(응답)의 구조를 취한다.
  • 기본적으로 전송 시에 HTTP 프로토콜을 사용한다.
        HTTP 프로토콜은 기본적으로 상태를 가지지 않는다(Stateless).
  • Request를 보내고, Response를 처리하는 웹 클라이언트가 있어야 한다.
  • Request를 처리하고, Response를 보내는 웹 서버가 있어야 한다.
  • 기본적으로 Request에는 Get 방식과 Post 방식이 존재한다.
  • 기본적으로 Response의 최종 형태는 정적 HTML이다.

    위의 내용을 포함해서 보다 순차적으로 이 내용을 도식화하면 다음과 같습니다.

    뻔한 내용입니다만, 위 그림 내에 지금까지의 웹 기술들이 발전해오면서 이루고자 했던 목표들이 다 존재합니다. 예를 몇 가지 들어볼까요? 대용량 바이너리 데이터를 전송하기 위해 HTTP 업로드 컴포넌트가 나왔고 HTTP 이어받기 기능이 등장합니다. 주고 받는 프로토콜의 전송 수준에서의 보안(Transport Level Security)을 위해 SSL(Secured Socket Layer)가 등장합니다. 최대한 많은 요청을, 가장 빨리 처리하기 위해 웹 서버 기술이 발전합니다. 요청에 따라 동적으로 응답을 생성해 내기 위해 웹 서버 애플리케이션 기술(CGI, ISAPI, ASP, ASP.NET, PHP 등)이 나왔습니다. 정적 컨텐츠인 HTML에 동적인 측면과 상호작용성(Interactivity), 재사용성을 부여하기 위해 DHTML, JavaScript, CSS, ActiveX, 애플릿 등이 나오게 됩니다. 효율적으로 요청을 처리하고, 응답 속도를 보다 빠르게 하고, 응답 결과를 렌더링 해서 보여주기 위해 브라우저 기술이 발전합니다. 전세계는 저 그림 속의 내용을 보다 효율적으로 개선하기 위해 그 오랜 시간을 투자한 셈입니다.

    그럼 위 그림에서 ASP.NET에 해당하는 부분은 어디일까요? (이거에 대한 답이 아직도 안 떠오르신다면 별로 이 글을 성의 있게 읽지 않으셨거나, 관심이 없으시거나, 빨리 이 길을 접고 리니지 게임머니 작업장을 차리시는게 개인과 사회를 위한 도움이 되실지도…)

    위 그림처럼 웹 서버 단의 영역입니다. 다시 한번 정리하자면 요청을 받아서 응답을 만들어내는 역할을 하는 부분입니다. 이미 잘 아실 역사적인 얘기를 간단히 해보자면, 초기의 웹 서버는 HTML(*.htm, *.html), 그림파일(.gif, .jpg)와 같은 정적 컨텐츠에 대한 요청을 받아서 이를 그대로 전달할 뿐이었습니다. 초기의 웹은 FTP나 Gopher와 같이 뭔가 ‘통신’을 한다는 의미보다는 단순히 ‘전송’을 한다는 의미가 강했습니다. 웹이 진화되면서 정적 컨텐츠가 아닌 동적 컨텐츠를 전달할 방법이 없을까를 궁리하게 되었고, 동적 요청을 위한 매개변수를 어떻게 전달할 것인가와 이를 받아서 어떻게 처리해야 하는가라는 문제가 대두되게 됩니다.

    매개변수의 전달은 웹 프로그래밍을 해봤으면 들어봤음직한 Get 방식과 Post 방식이 존재합니다. Get 방식은 URL 뒤에 ? 이후에 키-값 쌍으로 이루어진 Query String을 붙여서 매개변수를 전달하는 방법입니다. 이에 비해 Post 방식은

    태그를 사용하며, 키-값 쌍을 HTTP 헤더 내에 집어넣어서 전달하는 방법입니다.

    정적 컨텐츠에 대한 요청과 동적 컨텐츠에 대한 요청을 구분하기 위해 편의상 동적 컨텐츠를 구분하기 위한 확장자를 사용하게 됩니다. 즉, .html이나 .htm 대신에 .cgi, .asp, .php, .jsp, .aspx 등을 사용하게 된 것입니다. 이렇게 동적 컨텐츠로 구분되는 확장자로 요청이 들어올 경우, 웹 서버는 이를 가로채서 동적 컨텐츠 처리 모듈에 던져주게 됩니다.

    동적 컨텐츠를 처리하는 모듈, 즉 웹 애플리케이션은 최초에는 CGI 형태가 사용되었지만, 시간이 흐름에 따라 ISAPI 방식과 Script 기반 방식 등으로 진화되어 나갔습니다. 현재 우리가 사용하고 있는 대부분의 방식은 Script 기반 방식으로 보면 되겠습니다. ASP.NET 역시 여기에 속한다고 보면 되겠습니다. (사실 이런 얘기들은 옛날 ASP 책들을 보면 서두에 자세하게 잘 설명되어 있습니다.)

    그런데, CGI -> ISAPI -> Script 기반 방식 등으로 변경되어 온 이유는 무엇일까요?

    우선 첫째는 복잡한 것에서 간단한 것으로의 이동입니다. 간혹 예외도 있긴 하지만, 모든 IT 기술은 복잡한 것이 간단해질 때 성공하는 경우가 많습니다. 물론 복잡도의 감소와 해당 기술 보유자의 월급은 반비례합니다만.. ^^ CGI 시절까지만 해도 일단 C나 C++를 알아야 하는 것은 기본이었습니다. 이 당시 유명했던 게시판 중에 CrazyBoard가 있었던 걸로 기억하는데 게시판 소스의 양도 엄청난데다 분석하려면 상당한 시간을 투자해야 했습니다. 현재 우리가 쓰고 있는 ASP 게시판 소스는 그 시절과는 비교할 수 없을 정도로 분량도 적은데다 이해하기 쉽습니다. 게시판 소스만 있으면 초등학생도 직접 게시판을 올릴 수 있을 정도이니까요.

    둘째로 보다 적은 리소스를 소모하면서 보다 많은 요청에 대응해야 한다는 목적을 달성하기 위한 것입니다. 해묵은 얘기입니다만, CGI 시절에는 요청당 하나씩의 프로세스가 만들어져서 사용자 수가 적을 때는 괜찮지만 사용자 수가 늘어날 경우 엄청난 리소스 소모와 함께 성능이 떨어지기 시작했습니다. ISAPI 방식은 이를 보완하여, 요청 수와 관계없이 프로세스는 하나만 만들고, 처리모듈은 DLL 형태로 만드는 것이라고 생각하면 이해가 쉬울 겁니다. ASP의 예를 들면, asp.dll이 .asp 확장자에 대해 처리하는 ISAPI 모듈입니다.

    ASP.NET 역시 이러한 ISAPI 방식을 동일하게 사용합니다만 기존 ASP에서 발생되는 문제점을 해결하기 위해서 아키텍처가 변경되었습니다. 앗, 감이 오셨습니까? 지금까지의 글은 이번 글의 핵심내용을 유도하기 위한 설명이고, 우리가 알아봐야 할 건 대체 기존 ASP 아키텍처에서 어떤 문제가 있었고, ASP.NET의 아키텍처는 이를 극복하기 위해 어떤 형태를 취하고 있는 가입니다.

    ASP의 아키텍처와 문제점

    사실 ASP의 아키텍처에 대해 자세히 알고 싶다면 이 글이 아니라 다른 ASP 글이나 서적을 참고하십시오. 저는 어디까지나 간단하게 설명을 하도록 하겠습니다.

    ASP는 기본적으로 IIS 웹 서버 상에서 사용될 목적으로 만들어졌습니다. ASP가 처음 등장한 건 훨씬 이전이긴 하지만, 널리 사용된 것이 NT 4.0의 옵션팩에 탑재된 IIS 4.0부터 이니깐 이 때를 기준으로 설명을 하도록 하겠습니다.

    IIS 4.0 시절, ASP가 구동되는 기본 아키텍처는 다음과 같은 In-Process 모델입니다.

    inetinfo.exe는 System 계정으로 구동되는 IIS의 메인 프로세스입니다. 이 프로세스는 .asp 확장자에 대한 요청이 올 때 asp.dll을 자신의 프로세스 내에 로딩해서 요청을 처리하게 됩니다.

    In-Process 모델의 문제는 asp.dll 내에서 문제가 발생해서 asp.dll이 죽어버린 경우, 메인 프로세스인 inetinfo.exe마저 죽어버린다는 겁니다. 쉽게 말해서 무한루프를 도는 .asp 페이지가 있는 경우, 그 페이지 하나 때문에 웹 서버 전체가 먹통이 되어 버린다는 겁니다.

    Windows 2000과 Windows XP의 IIS 5.x에서는 이를 방지하기 위해 COM+ 서비스와 IIS를 결합하여 다음과 같은 옵션을 가지게 됩니다.

    IIS를 다뤄본 분이시라면 한번쯤은 본 적이 있을 겁니다만, 구체적으로 이게 뭐하는 건지 모르시는 분들도 많더군요. 우선 ‘낮음(IIS 프로세스)’은 IIS 4.0의 In Process 모드와 동일한 것입니다. 따라서 성능은 가장 좋지만, 안정성은 가장 떨어집니다.

    보통(풀링됨)과 높음(격리됨)은 웹 애플리케이션을 inetinfo.exe 내에서 구동하는게 아니라 별도의 프로세스(dllhost.exe)에서 구동하는 Out-of-Process 모델입니다. 둘 다 기본 개념은 유사하지만 약간의 차이점이 있는데, 먼저 ‘보통(풀링됨)’으로 지정된 웹 애플리케이션들은 하나의 dllhost.exe 내에서 풀링되어 동작하게 됩니다.

    쉽게 설명하면 inetinfo.exe가 아닌 별도의 dllhost.exe 안에서 In Process 모드처럼 동작한다고 보면 됩니다. 풀링된 애플리케이션들은 모두 단일한 dllhost.exe 내에서 동작됩니다.

    이와 관련하여 구성요소 서비스에서는 Out-of-Process Pooled Applications라는 COM+ 애플리케이션을 찾아볼 수 있습니다.

    풀링은 성능(리소스)과 안정성이라는 양쪽을 나름대로 절충한 것이라 볼 수 있습니다. 일단 inetinfo.exe와 dllhost.exe라는 두 개의 프로세스로 분리함으로써 안정성은 어느 정도 확보됩니다만, 역시 풀링되어 있는 애플리케이션이 죽었을 경우에는 같은 dllhost.exe 내의 다른 애플리케이션도 같이 죽는다는 단점이 있습니다.

    높음(격리됨)은 각 웹 애플리케이션을 별도의 dllhost.exe에서 동작시키는 방법입니다.

    완전히 격리를 시키는 것이기 때문에 안정성 측면에서는 가장 우수하겠지만, inetinfo.exe 입장에서는 n개의 dllhost.exe와 Out-Of-Process 통신을 해야 하며, n개의 dllhost.exe가 돌게 되므로 사용되는 리소스가 더 많아져서 성능이 다소 떨어지게 됩니다.

    IIS 5.x에서는 이러한 형태로 ASP 애플리케이션에 대해 나름대로의 안정성을 확보했습니다만, 역시 여기서도 제기되는 단점들은 존재합니다. dllhost.exe는 한번 구동되면 명시적으로 종료를 시켜주기 전까지는 계속해서 구동하게 됩니다. 물론 잘 동작한다면 상관이 없지만, 문제(데드락 상황과 같은)가 발생했을 때는 반드시 수동으로 재시작을 해줘야 한다는 단점이 존재합니다. 즉, 자신이 죽는다고 해서 웹 서버 전체나 다른 웹 애플리케이션에 영향을 미치는 건 해결했지만, 자기 자신이 죽었을 때 처리할 수 있는 방법이 모호하다는 것입니다. 또한 버그 등으로 인해 리소스의 누수가 발생하는 경우 애플리케이션을 재시작함으로써 리소스의 클린업을 할 수 있는데(시스템을 재부팅하는 걸 생각해보세요), 이러한 과정(통상 Recycling이라고 합니다)을 지원하지 않는다는 것입니다. 또한 dllhost.exe는 웹 애플리케이션만을 위한 전용이 아닌 COM+ 서버 활성화 모드(Out-of-Process)에서 사용되는 범용 호스팅 프로세스입니다. 그렇기 때문에 웹 애플리케이션을 위한 구성 정보를 설정해서 동작 특성을 변경하는 것을 지원하지 않는다는 것입니다.

    IIS에서 COM+ 애플리케이션을 구동할 때도 동일한 문제가 발생했습니다. 예전에는 VB 6.0이나 ATL로 비즈니스 로직 컴포넌트를 만들어서 COM+에 올려 구동하곤 했는데, COM+가 라이브러리 활성화(In Process)일 때는 컴포넌트가 죽을 경우 호스팅 프로세스인 IIS가 죽게 되므로, IIS 안정성을 위해 서버 활성화(Out-Of-Process)로 돌리는 경우가 많았습니다.

    웹 서버가 죽는다는 것이 얼마나 큰 문제인지는 다들 알고 계실 겁니다. 당연히 죽게 되면 서비스를 못하게 되니깐 문제가 되죠. 죽은 웹 서버를 살리려면 재시작을 하면 되는데, 재시작을 해도 해결이 되지 않는 문제도 역시 존재합니다. 대표적인 것으로 ASP 시절에는 상태 정보, 즉 세션을 IIS 프로세스 내에서 In Process 모드로 관리합니다. 즉 IIS를 재시작하게 되면 세션 정보가 날아가 버린다는 것입니다.

    IIS가 죽을 때 상태 정보가 손실되는 것도 문제지만, 웹 팜(Web Farm) 환경처럼 웹 서버를 여러 대 두고 로드밸런싱하는 경우 역시 문제가 됩니다. 이 경우 서버 간의 상태 정보를 어떻게 공유해야 할지 방법이 막연해집니다. 결국은 L4를 Dedicated 모드(Per Session)로 설정해서 세션을 고정시켜버리거나 상태 정보를 DB와 같은 제3의 저장소로 돌려서 우회해야 합니다. 전자는 로드밸런싱의 취지를 저하시키게 될테고, 후자는 이를 지원하기 위한 코드를 직접 다 작성해야 한다는 단점이 있습니다.

    ASP.NET의 아키텍처와 개선사항

    지금까지 진정한 이번 글의 주제를 위해 기나긴 여정을 왔습니다. 지금까지 언급된 문제점들을 극복하기 위해 ASP.NET은 다음과 같은 구조를 가지고 있습니다.

    지난 번 ASP.NET의 설치 시에 aspnet_isapi.dll이라는 ISAPI 처리기가 등록되는 것을 봤을 겁니다. 이 놈은 .aspx와 같이 ASP.NET 관련 확장자를 가로채서 실제로 처리하는 작업자 프로세스(Worker Process)인 aspnet_wp.exe에게 전달합니다.

    aspnet_wp.exe는 내부에 요청의 전후에 개입하기 위한 HttpModule과 개별 요청을 처리하기 위한 HttpHandler로 구성됩니다. HttpModule이나 HttpHandler는 기본으로 제공되는 것 외에 사용자가 정의해서 확장하는 것이 가능합니다. aspnet_wp.exe 내에서 실제로 일어나는 일들은 다음 번에 좀 더 자세하게 알아 보도록 하겠습니다. 일단 여기서는 aspnet_wp.exe가 실제 요청을 처리해서 응답을 만들어내는 녀석이라고만 생각해 둡시다.

    일단 여기까지만 봤을 때, ASP.NET은 별도의 전용 프로세스인 aspnet_wp.exe에서 구동되므로 자신이 죽더라도 inetinfo.exe에는 영향을 미치지 않습니다. 따라서 이전 dllhost.exe에서 구동될 때처럼 안정성 측면에서는 확보가 된 셈이죠. aspnet_isapi.dll은 실행 중인 aspnet_wp.exe가 없을 때는 바로 이 프로세스를 다시 실행하며, 구성정보에 따라서 aspnet_wp.exe의 동작을 제어할 수도 있습니다. 이전 글에서 machine.config의 내용을 보면 이를 제어하기 위한 내용들이 존재합니다. 예를 들어 timeout을 지정해서 일정시간 동안 요청이 없으면 종료시켜버릴 수도 있고, 일정시간 이상 데드락이 걸렸을 때는 재시작하는 기능도 존재합니다. 다양한 구성정보에 따라서 동작 특성을 제어하는 것 역시 가능해졌다는 의미입니다.

    위 그림의 우측에 보면 aspnet_state.exe라는 프로세스가 있습니다. ASP.NET 역시 기본적으로는 세션을 In Process 모드로 관리하지만, 설정에 따라서 그림과 같이 세션을 별도의 프로세스(aspnet_state.exe)에서 관리할 수 있습니다. 따라서 aspnet_wp.exe가 죽는다고 하더라도 세션 정보 자체는 여전히 살아있게 되는 거죠. 상태 정보 관리에 대한 자세한 내용 역시 다음에 다루도록 하겠습니다.

    이리하여 대략 ASP.NET의 아키텍처가 왜, 무엇을 개선하기 위해 어떻게 구성되었는지에 대해 다뤄보았습니다. 사실 몇 가지 빠진 부분도 있긴 한데 여기에서 설명해야 할지 다음 글에서 설명해야 할지 약간 애매한데다, 너무 글이 길어지는 것 같아서 다음으로 넘기도록 하겠습니다.


  • authored by Lancers (안재우)


    3. 상속과 interface의 문제점

    3.1. 상속

    3.1.1. 상속에 있어서의 생성자 (constructor)

    "child의 default 생성자가 있으면 그 생성자에는 parent의 생성자 (base()) 호출이 compile시 자동으로 삽입된다." 인라인 초기화 구문 상에서 지정해 놓은 내용이 있다면 이 내용들은 기본 생성자보다 앞서서 처리된다. 처리 순서를 요약하면 다음과 같다.

    1. 자식 클래스의 멤버 필드 내 인라인 표현식
    2. 상위 클래스의 멤버 필드 내 인라인 표현식
    3. 자식 클래스 생성자에서 지정한 상위 클래스 생성자의 호출
    4. 자식 클래스 생성자의 호출
    3.1.2. down cast는 본질적으로 매우 위험하다.

    down cast - child의 type으로 parent를 casting - 는 parent 형태의 type이 정말 child type인지 compile시에는 알 수 없다. 실행 시에 type check가 이루어지므로 runtime시에 InvalidCastException이 발생할 가능성이 커진다.

     

    "프로그래밍 시 오류는 가능한 compile 시에 처리하는 것이 좋다."

     

    덧. down cast에 대한 방비책이 몇 가지가 있다. 다음은 그 예시이다.

    • 실제 형식 변환 이전에 해당 개체가 정말 지정한 형식과 호환성이 있는지 확인하는 방법: is 연산자를 통하여 주어진 개체를 해당 형식으로 down cast하는 것이 안전한지의 여부를 boolean 값으로 평가할 수 있다.
    • 형식 변환 실패 시 null 참조로 fallback하도록 만드는 방법: as 연산자를 통하여 주어진 개체를 해당 형식으로 down cast 시도하되, 호환되는 형식이 아니거나 처음부터 null 참조였을 경우에는 null을 반환하도록 할 수 있다. 이 때, C# 2.0 컴파일러를 사용 중이라면, 연이어서 변환에 실패하였을 경우 추가 fallback 처리를 다음과 같이 할 수 있다.

      string s = (sth as string) ?? String.Empty;
    • 위의 is나 as 연산자의 평가 결과와 무관하게 동작하는 경우도 있다. 클래스 내에 implicit, explicit operator method를 추가하였을 경우 상속 관계와 무관한 casting이 발행되기도 한다. implicit operator method가 주어진 형식에 대하여 존재하는 경우 해당 형식으로의 대입 구문에서 자동으로 호출되며, explicit operator method가 주어진 형식에 대하여 존재하는 경우 강제 형변환 구문을 통하여 이를 명시적으로 호출할 수 있다. 하지만 양쪽 모두 꼭 필요한 경우가 아니면 explicit operator method만을 제한적으로 사용하는 것이 혼란을 줄일 수 있다.
    • primitive type의 경우 강제 형변환을 통하여 손실 형변환을 유도할 수 있으며 이는 상속 관계로 설명될 수 있는 것이 아니다.
    3.1.3. 봉인 클래스의 protected 액세스 한정자를 사용하면 경고가 발생한다.

    sealed 키워드로 선언된 클래스나 구조체에서 protected 멤버를 선언하면 컴파일러 경고가 발행된다. 왜냐하면, 상속 대상이 더 이상 존재하지 않기 때문에 protected 멤버는 원래의 의미가 아닌 private 멤버로 의미가 변질되기 때문이다.

    3.2. interface

    3.2.1. interface는 interface일 뿐 다중 상속의 대용품이 아니다.

    interface를 method signature - 추상 클래스와 같이 구현부는 없고 선언부만 있는 method -의 용도, 즉 클래스나 구조체의 프로토타입 정도로만 생각하는 것이 옳은 판단이다. 즉, interface는 abstract method가 있는 클래스와 유사하지만 상속의 의미와는 그 용도가 다르다. 공통된 type을 정의하는 것으로 보아야 하고, 어떤 공통된 기능을 "지원한다"는 의미로 해석하는것이 옳다.

     

    또한 interface는 클래스를 재이용하기 위해 상속을 사용하여 캡슐화의 파괴를 수반하는 것을 방지하는 기능이 있다. 상속을 사용하면 모두 구현 후 마치 소스 코드가 여기저기 천 조각을 주워 모아 만든 '누더기' 같이 보이는 것에 한숨을 쉰 경험이 있을 것이다. 이 부분을 interface로 구현하면 보다 깔끔한 코드가 나오게 된다. 물론 public과 protected를 적절히 잘 사용해도 되긴 하지만 말이다.

     

    하지만 상속은 메서드 오버라이드한 경우 클래스를 마음대로 개조해 버린 셈이 되므로 어디선가 묘한 모순이 발생하게 될 가능성도 높아질 뿐 아니라 추상 클래스의 경우 실제 구현부가 어디에 위치하는지도 애매하게 느껴질 수 있어 불안정한 코드가 되고 만다.

     

    덧. 인터페이스의 설계는 최대한 단순하고 가볍게 하되, 한 번 외부에 공표한 인터페이스는 바꾸지 않는 것을 전제로 해야 한다. 인터페이스는 이 인터페이스를 구현하는 클래스들간의 약속이며 이를 깨뜨리게 될 경우 프로그램 코드의 일부 혹은 전체를 매번 새로 컴파일해서 전체 소프트웨어를 재 배포해야 하는 초과 비용을 발생하게 만든다. 즉, 작은 단위의 기능으로만 인터페이스를 신중하게 추가하고 관리해야 한다.

    3.3. 상속 제대로 사용하기

    "그렇다면 제대로 된 상속은 어떻게 판단할 수 있을까?"

     

    상속은 'is a' 관계가 성립해야 올바르다. 즉 '서브클래스 (자식) is a 슈퍼클래스 (부모)'가 성립해야 한다. 예를 들면 'Red is a Color'는 올바른 명제이지만, 'Engine is a Car'는 'has a'' 관계이므로 상속이 아닌 Composition 관계 - 또는 - Delegation 관계로 'Engine has a Car'라는 명제가 올바르게 된다.

     

    Composition은 '객체를 field가 갖게 하는 방법'을 뜻하므로 'has a' 관계가 정확히 성립한다. 상속 대신 composition과 delegation (조작이나 처리를 다른 객체에 위임)을 사용하면 다음과 같은 장점이 있다.

     

    1. 상속에서는 슈퍼 클래스가 허용하고 있는 조작을 서브 클래스에서 모두 허용하게 되지만, composition과 delegation에서는 조작을 제한할 수 있다.
    2. 클래스는 결코 변경할 수 없지만, composition하고 있는 객체는 자유롭게 변경할 수 있다. 예를 들면 학생 클래스가 항상, 영원히 학생이 아니라 나중에 취직을 하여 직장인 클래스가 되도록 할 수 있다는 의미이다.

     

    상속을 composition과 delegation으로 변경하는 요령은? 여기서 Shape를 상속한 Polyline과 Circle을 변경한다면 다음과 같다.

     

    1. Shape (부모)의 공통된 내용을 구현한 구현 클래스 (ShapeImpl)를 만든다.
    2. Polyline과 Circle 클래스에서 ShapeImpl을 composition하고 부모와 공통되지 않는 method를 각각 위임받는다.
    3. ShapeImpl 클래스의 method를 추출한 IShape 인터페이스를 작성하고 Polyline과 Circle에서 이 인터페이스를 구현한다.



    출처 : http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=18&MAEULNO=8&no=1563&page=4

    2. 닷넷은 Pointer 환경이다? (닷넷에는 Pointer밖에 없다?)

    2.1. 닷넷은 값 형식을 제외하곤 모두 Pointer이다.

    "닷넷에는 포인터가 없다" - 또는 - "일부 언어에서만 호환성을 유지하기 위하여 포인터를 남겨놓았을 뿐 포인터는 자바와 마찬가지로 사용하지 않는다"라고 닷넷의 장점이라고 생각하는 것은 입문자도 외우고 있다. 하지만 이 부분은 의외로 닷넷을 혼란스럽게 하는 주범이라고 생각한다. 닷넷에 포인터가 없기는 커녕 값 형식 (Primitive (int, short, char, long, ...), 구조체, 나열 상수 (enum))을 제외하면 "포인터 밖에 없는 환경이다"라는 명제가 성립되게 된다. 사실 여기서 포인터라고 함은 C의 그것과는 조금 다른 reference(참조)로 보는 것이 온당하지만...

     

    "즉, 닷넷의 클래스형의 변수는 모두 포인터이다."

     

    덧. System.IntPtr, System.UIntPtr은 정수형 포인터를 가리키는 형식으로 자바가 정확하게 지원해주지 않는 C 언어의 포인터를 닷넷 환경에서는 이들 형식을 이용하여 다룰 수 있다. 그러나 이 글에서 이야기하는 개념과는 거리가 먼 상호 운용성에 관한 토픽이므로 큰 관련성이 없다.

    2.2 null은 객체인가?

    닷넷에서 공참조 (힙에 실제로 참조되는 object가 없는 참조)의 경우는 당연히 객체가 붙어있지 않다. 이러한 상태에서 다음의 MSIL 명령어를 호출하는 동작을 수행할 경우 NullReferenceException이 발생한다고 닷넷 SDK 문서 상에는 기술되어있다.

    • callvirt : 메서드 호출 (호출 대상이 null 참조일 경우 발생)
    • cpblk : 메모리 영역 복사 (잘못된 주소 발견시 발생)
    • cpobj : 값 형식 객체 복사 (잘못된 주소 발견시 발생)
    • initblk : 메모리 영역 초기화 (잘못된 주소 발견시 발생)
    • ldelem.<type> : 지정된 배열 인덱스의 항목을 가져오기 (배열이 null 참조일 경우 발생)
    • ldelema : 지정된 배열 인덱스의 항목의 개체를 가져오기 (배열이 null 참조일 경우 발생)
    • ldfld : 필드 값 찾기 (개체가 null 참조이고 정적 필드가 아닐 경우 발생)
    • ldflda : 필드 주소 찾기 (개체가 null 참조이고 정적 필드가 아닐 경우 발생)
    • ldind.<type> : (잘못된 주소 발견시 발생)
    • ldlen : 배열 길이 조회 (배열이 null 참조일 경우 발생)
    • stelem.<type> : 배열 요소 가져오기 (배열이 null 참조일 경우 발생)
    • stfld : 개체 참조/포인터의 값 교체 (개체가 null 참조이고 정적 필드가 아닐 경우 발생)
    • stind.<type> : 주어진 주소에 값 쓰기 (형식 불일치 발생시 발생)
    • throw : 예외 발생 명령어 (예외 발생 대상 개체가 null일 경우 발생)
    • unbox : 참조 형식 내의 값 형식 개체를 가져올 때 (참조 형식이 null일 경우 발생)

    위에서 강조 표시한 항목들은 실제 C#, VB.NET 환경에서 만날 수 있는 사항들과 관련이 있는 것을 열거한 것이다. 여기서 논점이 되는 것은 null을 개체로 볼것인가 아닌가에 대한 문제이다.

     

    공참조는 어떤 객체도 참조하고 있지 않는다고 단정하고 있다. 하지만 '==' 연산에 있어 두개의 객체가 모두 null이거나 동일한 객체 또는 배열 참조의 경우 true라고 되어있는것으로 봐서 서로 다른 두 객체가 동일한 null을 참조하고 있으므로 true가 된것이 아닌가 하는 생각을 할 수 있다.

     

    즉, null이 Object의 instance 형태는 아니지만 개념적으로 봤을 때 null도 object라고 봐야 하지 않을까?

     

    덧. C# 2.0부터 새롭게 소개된 Nullable 형식에서 사용하는 null 키워드는 그 의미가 조금 다르다. 앞서 값 형식은 참조 형식과는 달리 값 그 자체를 취급한다고 하였는데, 데이터베이스 환경에서는 이런 설정과는 또 다르게 값 형식에도 null 상태를 허용하고 있다. 이런 불일치성을 해결하기 위하여 등장한 것이 Nullable 형식인데, 특별히 이 형식에 대해서는 Nullable 형식이 클래스 형식이 아님에도 불구하고 null을 값을 지정하지 않고 Nullable 개체를 새로 만든 것으로 대체하여 처리한다.

     

    int? aa = null;
    System.Nullable<int> aa = null;
    int? aa = new System.Nullable<int>();
    System.Nullable<int> aa = new System.Nullable<int>();
    // 값이 지정되지 않은 int 형식을 만들기 위하여 위의 네 구문은 모두 같습니다.

    int? bb = 3;
    System.Nullable<int> bb = 3;
    int? bb = new System.Nullable<int>(3);
    System.Nullable<int> bb = new System.Nullable<int>(3);
    // 값이 지정된 int 형식을 만들기 위하여 위의 네 구문은 모두 같다.

    bool b = (aa == null);
    bool b = aa.HasValue;
    // 값이 지정되었는지 아닌지를 판단하기 위하여 위의 두 구문은 모두 같다.
    object o = (aa.HasValue ? aa.Value : null);
    object o = aa ?? null;
    // 값이 있는지 없는지에 따라 선택적으로 값을 가져오게 하기 위한 방안으로 위의 구문은 모두 같다.

    2.3 String에 대하여

    String Object에 대한 생각.

     

    string str = "111222";
    string a = "111";
    string b = "222";
    string c = "111";
    string d = b;
    string t = str.Substring(0, 3); // 111
    string u = b.Clone().ToString(); // 222

     

    위의 소스를 보고 다음이 참인지 생각해 보자. (== 연산자나 Equals 메서드는 값을 비교하는 것이므로, ReferenceEquals 메서드를 이용하여 같은 개체들인지 확인해보기로 한다.)

    1. String.ReferenceEquals(str, (a+b)): 이것은 두 개의 참조와 하나의 참조를 비교했으므로 당연히 false이다.
    2. String.ReferenceEquals(a, b): 이것은 당연히 false이다.
    3. String.ReferenceEquals(d, b): d 참조에 b의 참조를 복사한 것이므로 결국 d는 b를 가리킨다. 따라서 true이다.
    4. String.ReferenceEquals(a, t): a와 t는 둘 다 값이 "111"이다. 하지만 이것은 서로 다른 참조를 가져 false이다. 그렇다면 다음 5번도 false일까?
    5. String.ReferenceEquals(a, c): 이것은 true이다. 4번의 경우와는 구분되는 것이, 이와 같이 프로그래머가 코드 상에 직접 기입해넣은 문자열도 실행 중에는 하나의 완벽한 객체로 분류된다. a와 c는 이렇게 코드 상에 서술된 문자열에 대한 같은 참조를 서로 나누어 받은 것이므로 a와 c는 코드 상에 서술되어있던 문자열에 대한 참조를 나누어받았을 뿐이다. 그러므로 이런 결과가 나타나게 된다.

    2.4 객체 지향의 캡슐화 파괴 주의

    "object pointer를 반환하는 getter method는 객체 지향의 캡슐화가 파괴될 가능성이 있다." 이는 object형의 field (member variable)의 getter에서 이 object를 그냥 반환하면 이 object를 받은 쪽이나 참조하고 있는 다른쪽에서 이 object의 내용을 변경하게 되므로 사실 캡슐화 (은닉)는 이루어지지 않았다고 보는 것이 정확하다.

     

    "이럴 경우 object를 clone(복제)하여 반환하지 않아도 되는지를 반드시 생각해 본다."

     

    object의 복사에는 shallow copy와 deep copy가 있다.

     

    // (참고) Member에는 두 개의 field (Identity Class 형의 id와 Family Class 형의 family)가 있다.

    // Shallow Copy Example
    public Member ShallowCopy()
    {
        Member newer = new Member();
        newer.id = this.id;
        newer.family = this.family;
        return newer;
    }

    // Deep Copy Example
    public Member DeepCopy()
    {
         Member newer = new Member();
         newer.id = new Identity(this.id.Identity, this.id.Name);
         newer.family = new Family(this.family.FamilyName, this.family.FamilyInfo);
         return newer;
    }

     

    위 소스에서 보듯이 Shallow Copy는 object를 복사하여 반환한 것 처럼 보이지만, 사실은 Member Object만 새로 생성되었을 뿐 Member의 field는 newere와 this 둘다 서로 같은 힙의 id와 family를 참조한다. 하지만 두 번째 method인 Deep Copy의 경우 member field를 새로 생성하여 복사하므로 서로 다른 id와 family이다.

     

    클래스를 직접 구현하고 있을 동안 System.Object 클래스의 protected 메서드 (외부에서는 사용할 수 없지만 내부적으로 상속되어져 내려오는 메서드) 중 하나인 MemberwiseClone 메서드를 이용하여 자기 자신의 단순 복사본을 생성할 수 있다. 하지만 Deep Copy를 구현하려면 ICloneable 인터페이스를 이용하여 별도로 Clone 메서드를 통하여 구현하도록 하는 것이 바람직하다. 하지만 ICloneable 인터페이스를 구현했다고 해서 이 개체가 반드시 Deep Copy를 지원하는 것이라고는 볼 수 없다.*

    (참고) object를 immutable (변하지 않는, 불변의 객체)로 만드는 요령

    1. 모든 field (member variable)를 생성자(constructor)를 이용하여 초기화한다.
    2. 모든 field는 private으로 선언하고, getter property는 만들되 setter property는 만들지 않는다. 이로서 모든 속성들을 readonly property로 변경한다.

    즉, 값을 변경하기 위해서는 object를 다시 만들어야만 하는 불편은 있지만 안전하게 사용하고자할 때 유용한 방법이다.

    2.5 배열에 대하여

    2.5.1 배열은 object인가?

    CLR에서 배열은 object로 취급되어 object와 같이 기술된다. 클래스의 상속도로 보았을 때에도 모든 배열은 암시적으로 System.Array를 부모 클래스로 하며, System.Array는 다시 System.Object를 부모 클래스로 한다. 그리고 int[] iarr = new int[10]; 구문에서처럼 new 연산자로 Heap 영역에 object를 생성하므로 object임을 알 수 있다.

    2.5.2. 배열의 length는 왜 Java와 달리 프로퍼티인가?

    Java와는 달리 닷넷의 배열은 길이를 가져오기 위한 방법으로 getter property를 이용하는데, Java이든 닷넷이든 이 과정은 컴파일러의 트릭과 함께 CLR의 협조를 기반으로 이루어지는 특수한 논리가 숨어있다.

     

    어찌되었든 간에 배열의 길이를 조사하는 것 자체는 필드도 아니고 메서드도 아니고, 프로퍼티도 아닌 단순한 기계 명령어 하나로 통일된다고 봐야 한다. (이전 섹션에서 언급한 ldlen 명령 참고) 그러나 이런 식의 문법적 요소를 남겨두는 이유는, 언어 호환성을 위한 것으로 컴파일러 제작자가 아닌 일반 소프트웨어 개발자들에게는 피상적인 의미 그 이상이 되지는 않는다.

    2.5.3. readonly와 배열에 대하여...

    우리가 흔히 앞에서도 나온바 있지만 readonly는 값을 변경할 수 없는 것이라고만 생각하지 object로 되어있을 경우 그 object는 변경 가능하다는 것을 잊곤한다. 배열도 object이므로 마찬가지다.

     

    readonly int[] iarr = new int[5]; 일 경우 iarr = null; 은 오류로 취급되지만, iarr[3] = 5;는 오류가 발생하지 않는다. 즉, readonly로 지정되어있는것은 iarr이지 iarr이 가리키는 곳의 배열의 요소들은 아니다.

    2.5.4. 닷넷에서의 다차원 배열은 논리적인 개념일 뿐이다.

    가령 2차원 배열처럼 보이는 int[][] iarr 또는 int[,] iarr은 논리적으로는 행열 구조처럼 보일 수 있지만 실제 컴퓨터 메모리 배치는 그렇지 않다. 두개의 배열이 각각 구분되어있는 상태에서 서로 유기적으로 통합된 상태이거나, 지정된 크기만큼의 메모리 공간을 사용하는 1차원 배열이 재구성된 것으로 이해하는 것이 올바르다.

    2.6. 인수 (parameter/argument) 전달의 개념

    2.6.1. 닷넷에서 parameter (argument) 전달은 무조건 'call by value'이다.

    값 형식의 경우 호출한 쪽의 변수값은 호출 받은 method 내에서 값이 변경되어도 변경되지 않는다. 참조 형식의 경우도 참조되는 object에 대해서는 함께 변경되지만 참조 포인터는 call by value이다. object를 가리키는 pointer는 call by value로 변경되지만 Heap의 실제 object 내용은 변경되지 않는다.

    2.6.2. C와 같은 언어는 static linking이지만, 닷넷은 dynamic linking이다.

    따라서 닷넷은 클래스가 처음에 한꺼번에 메모리에 로드되는것이 아니라 런타임시에 그것이 필요해 졌을 때 로드되고 링크된다. static field의 영역도 클래스가 로드되는 시점에서야 비로소 확보된다. 이렇게 되면 최초 가동 시간이 단축되고 끝까지 사용하지 않는 클래스의 경우 신경 쓸 필요가 없어지게 된다.

     

    따라서 static field는 프로그램이 시작되어 해당 클래스가 필요해 졌을 때 CLR이 알아서 load/link 해준다. 즉, static field는 프로그램이 실행되기 시작할 때 부터 끝날 때 까지 계속해서 존재하는 것이라고 보면 된다.

    (참고) linking의 의미

    link된다는 것은 클래스가 memory에 loading될 때 특정 메모리 번지에 loading되는 데 이 메모리 번지는 loading될 때 마다 다른 번지 수에 loading된다. 이 때의 메모리 주소값 (Java에서는 실제 메모리 값이 아닐 수 있다)을 현재 실행 중인 프로그램에서 알 수 있도록 하여 해당 클래스에 대한 참조가 가능하도록 연결하는 과정이다.

     

    정적 (static) link라는 것은 이러한 메모리에 대한 주소 정보를 컴파일 시에 compiler가 미리 결정하는 것이고, 동적 (dynamic) link라는 것은 프로그램 수행 중 결정되는 것을 의미한다. 정적인 link의 경우 직접적으로 메모리의 번지값이 할당되는 것이 아니라 offset 값 (기준 위치로부터의 index 값)으로 연결시킨다.

    2.7 GC에 대하여 잠깐!

    2.7.1. Garbage Collection은 만능이 아니다.

    닷넷에는 free가 없다. GC가 알아서 해준다. 하지만 GC 수행 중에는 프로그램의 퍼포먼스가 크게 떨어질 수 있기 때문에 GC가 자주 발생하지 않도록 프로그램을 설계하는 것이 좋다. 서비스 되고 있는 시스템에서도 가끔 시스템이 응답이 늦어지는 시점이 있는데, 이는 GC가 수행되고 있기 때문일 가능성이 높다.

     

    그렇다면 GC가 자주 발생하지 않도록 해야 하는데 가장 좋은 방법은 무엇일까? 그것은 바로 불필요한 객체를 생성하지 않는 것이 아닐까?

     

    닷넷에 free가 없다는 것은 매력적이다. 그 이유는 두 개의 변수가 heap 내에서 하나의 object를 reference하고 있을 경우 실수로 하나의 변수만 free해버리면 나머지 하나는 dangling pointer라하여 reference pointer가 모르는 사이에 사라져버려 곤경에 처하는 것을 예방해 주기 때문이다.

     

    참고로 System.Object 클래스에는 Finalize 메서드가 있어 GC 수행 시점에 호출되는 메서드가 있지만 이것은 GC가 언제 수행될지 알 수 없으므로 과신하면 안된다.

     

    덧. 상호 연동성 프로그래밍을 많이 하는 경우, 소멸자와 함께 반드시 생각해야 할 것이 IDisposable 인터페이스의 구현이다. 이 인터페이스를 구현하여, 언제 호출될지 알 수 없는 소멸자가 아닌 명시적으로 호출 가능한 소거 함수를 배치하여 필요없을 때 외부 리소스 (플랫폼 호출을 통하여 부른 malloc 같은 함수들의 메모리 블럭)들을 미리 제거할 수 있다. 그리고 소멸자나 Finalize 메서드와도 Dispose 메서드를 연결시켜서 Dispose가 이루어지지 않아서 발생하는 리소스 누수를 예방해야 한다.

    2.8 닷넷 Pointer 결론

    2.8.1 결국 닷넷에는 포인터가 있는 것인가, 없는 것인가?

    닷넷은 Heap 내의 Object를 참조(reference)하고 있고, 참조는 결국 개념이 포인터와 유사한 것이므로, 닷넷에 포인터가 없다는 것은 어불성설이다.

     

    주. 이 부분에 대해 Object를 이해하시면 족히 이런 문제는 사라질 것으로 봅니다. 클래스에 대한 인스턴스 (object)들은 reference로 밖에 가질(참조될)수 없기 때문입니다. 컴파일러 입장이 아닌 기반 언어들 (C#, VB.NET, J#, C++ CLR 등)의 사상을 가지고 이해하는 것이 좋을듯 합니다.

     

    java.sun.com의 Java programmer's FAQ에 이런 질문 글이 올라온 적이 있다. "Java에는 pointer가 없다고 하는데, linked list는 어떻게 만들어야 하는가?"라는 질문이었는데 이에 대한 답이 참 명쾌하다. 이것은 비슷한 메카니즘을 가지는 다른 GC 환경 플랫폼에서도 동일하게 적용되는 것이다.

     

    Java에 관한 많은 오해 중에서 이것이 가장 심각한 것이다. 포인터가 없기는 커녕 Java에 있어 객체지향 프로그래밍은 오로지 Pointer에 의해 행해진다. 다시 말해 객체는 항상 포인터를 경유해서만 access가 이루어지며 결코 직접적으로 access 되지 않는다. pointer는 reference (참조)라고 불리며 당신을 위해 자동으로 참조된다.

     

    닷넷에 포인터가 없다고 주장하는 모든 서적과 글들의 내용은 혼란을 야기할 가능성이 크다. 이러한 주장은 GC 플랫폼 환경을 정확히 숙지하지 않은 사람의 서술일 가능성이 높으므로 재고해보아야 할 표현이다.



    출처 : http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=18&MAEULNO=8&no=1562&page=4

    '개발자가 놓치기 쉬운 자바의 기본 원리' 라는 글의 내용을 보고 같은 내용을 닷넷을 기준으로 서술한다면 재미있을 것 같다는 생각에 곧바로 글을 옮겨적어봅니다. 좋은 소스를 제공해주신 다음 커뮤니티본부 커뮤니티개발1팀의 전성호님께 감사드립니다. 이 글이 닷넷에 입문하시는 분들이나 기본적인 원리때문에 고민하셨던 분들께 좋은 역할을 해주기를 바랍니다.

     

    출처: http://dna.daum.net/technote/java/PrincipleOfJavaInternalForDeveloperEasyToLost

     

    요약: 개발자가 놓치기 쉬운 닷넷의 기본 원리에 대하여 기본적이긴 하지만 개발하면서 느끼고 경험한 내용을 정리하였다.

    1. 객체 지향의 구멍 static

    1.1 닷넷은 객체 지향 환경이다?

    "닷넷은 자바와 마찬가지로 객체지향 언어이다" 라는 주장을 자주 접하게 된다. 만일 이것이 사실이라면 닷넷을 사용하는 한 "기존의 절차 지향 프로그래밍을 전혀 할 수 없을게 아닌가?" 라는 생각이 들지만 사실은 그렇지 않다. static을 이용하면 비 객체지향 언어처럼 코딩할 수 있다.

     

    static method, static property, static event는 instance가 아닌 클래스에 속하는 method로, class method, class property, class event라고 부른다. 반대로 static이 아닌 method, property, event은 instance method, instance property, instance event라고 부른다.

     

    static method, static property에는 this가 없다. instance method, property에는 숨겨진 파라미터로 this가 건네진다. (아래 "객체지향에 흔히 있는 오해" 참고) 하지만 static method, static property는 절차 지향의 함수와 동일하므로 숨겨진 파라미터 this는 없다. 그래서 static method, static property에서는 전달한 this가 없으므로 instance method, instance property를 호출하거나 instance field를 참조할 수 없는 것이다.

    (참고) 객체지향에 흔히 있는 오해

    • 오해 1. "객체 지향에서는 객체 끼리 서로 메시지를 주고 받으며 동작한다."라는 말을 듣고 다음과 같이 생각할 수 있다. "객체 지향에서는 객체가 각각 독립하여 움직인다는 것인가, 그러면 각 객체에 독립된 thread가 할당되어 있단 말인가?" 그렇지 않다. "메세지를 보낸다"라는 것은 단순히 각 객체의 함수 호출에 불과하다.
    • 덧. 메세지를 보낸다라는 개념을 같은 컴퓨터 내에서가 아닌 네트워크 상의 관점으로 확장을 하였을 때, 우리가 흔히 알고 있는 XML Web Service, DCOM, CORBA와 같은 Remote Procedure Call 기반 통신 기법으로 발전하게 되는 것이다.
    • 오해 2. "객체 지향에서는 method, property, event가 class에 부속되어 있다"는 말을 듣고 다음과 같이 생각할 수 있다. "그러면 instance별로 method의 실행 코드가 복제되고 있는 것이 아닌가?" 물론 이것도 오해다. method의 실행 코드는 종래의 함수와 동일한 어딘가 다른 곳 (CLR의 메모리 영역 내)에 존재하며 그 첫 번째 파라미터로 객체의 포인터 this가 건네질 뿐이다.
    • 오해 3. "그렇다면 각 instance가 method의 실행 코드를 통째로 갖고 있지 않는 것은 확실하지만, method의 실행 코드의 포인터는 각 instance별로 보관하고 있는 것이 아닌가?" 이것은 약간 애매한 오해이긴 하다. CLR 스펙에서는 JVM 스펙에서와 마찬가지로 별도의 메모리 영역 내에 실행 코드를 가지고 있는 상황에서 각 메서드 호출 시 Stack Frame이 생성되어 실행되고 실행 완료 시 복귀 주소를 전달한다.

    1.2 전역 변수

    static에서 public field는 전역 변수 (global variable, 글로벌 변수)이다. 여기서 "글로벌 변수는 왜 안되는가"에 대해 잠깐 생각해 본다. 우리는 흔히 "글로벌 변수는 될수있는한 사용하지 않는 것이 좋다"라고 한다. 그 이유는 글로벌 변수는 어디서든 참조할 수 있고 값을 변경할 수 있기 때문이다.

     

    또한 파라미터나 리턴값으로 교환해야할 정보를 글로벌 변수를 경유(사용)하여 건네주면 함수의 역할이 불분명해지고 흐름도 애매해진다. 마지막 이유로는 "글로벌 변수는 하나밖에 없다"는 것이다. 이는 어디서 이 값을 변경했는지 알 수 없게 하는 지름길이고 실무에서도 간혹 발생하긴 하지만, 이 하나밖에 없는 변수가 버전업으로 두개가 필요하게 되었을 때 확장도 대형 프로젝트에서는 힘들어진다.

     

    따라서 static에서 public은 readonly나 const를 붙여 상수나 읽기 전용 변수로 사용해야지 그 외의 용도는 자제하는 것이 좋을 것이다.

     

    (참고) readonly 초기화에서의 주의점. 예를 들어 다음과 같은 코드를 보았을 때 우려되는 점은 무엇인가?

     

    public static readonly System.Drawing.Color White = new System.Drawing.Color(255, 255, 255);

     

    readonly 변수는 인라인 문장과 생성자 권역 내에서 한 번 초기화되면 변경이 불가능한데 object로 초기화할 경우 WHITE라는 필드가 변경될 수 없는 것이지 그것이 가리키는 객체는 아니라는 점이다.

     

    과거 신규 서비스 개발시 readonly 변수 필드에 설정파일을 읽어 cache하는 singleton class의 특정 member를 이용하여 초기화할 경우 이 멤버값이 변경되면 readonly 변수의 값이 변경되었는데 프로그램에서는 이상한 짓을 하는 원인을 찾기가 상당히 어려웠던 경험을 하고난 후 부터 이런 코드는 냄새나는 코드로 여겨지게 되었다.

     

    static은 글로벌 변수와 동일하므로 남발해서는 안된다. static을 사용할 경우 다음 두 가지는 최소한 기억한다.

    1. static field는 readonly의 경우와 달리 정말 "하나여도 되는지" 여부를 잘 생각해야 한다.
    2. static method는 주저하지 말고 쓰되 다음 두 가지의 경우 매우 활용적이다.
    3. 다른 많은 클래스에서 사용하는 Utility Method 군을 만드는 경우. (주로 Utility Class의 method)
    4. 클래스 안에서만 사용하는 "하청 메서드 (private method)". 이유를 예를 들어 설명하면, 아래와 같은 조금은 과장된 클래스가 있다고 하자.

     

    public class T
    {
        private int a;
        private int b;
        private int c;

        private int Calc()
        {
            c = a + b;
            return c * c;
        }

        // ....other method or getter/setter...
    }

     

    위의 클래스 T의 경우 내부에서는 Calc라는 instance 함수를 사용하게 되면 c의 값이 매번 변하게 된다. 이는 무심코 하는 실수로 클래스 내에서 private method는 모든 멤버 instance 변수에 접근 가능하게 되면서 발생하게 된다. c의 값이 변하지 않기를 바랄 수 있다. 이 때 안전한 방법은 다음과 같이 Calc 하청 메서드를 static method로 수정하면 안전하다.

     

    private static int Calc(int a, int b)
    {
        int c = a + b;
        return c * c;
    }

     

    여기서 a와 b는 멤버 변수를 접근할 수 없어 전달해야 한다. (static에는 this가 없어 instance field를 참조할 수 없다는 것은 이미 위에서 설명했다.) 또한 c도 같은 이유로 사용할 수 없어 로컬 변수로 선언하여 사용하고 있다. 이럴 경우 메서드가 약간 커질 수 있지만 instance member 변수를 안전하게 사용할 수 있다는 장점이 있다. 이것은 static을 다시한번 생각하게 하는 좋은 예가 되었을 것이다.



    출처 : http://www.devpia.com/MAEUL/Contents/Detail.aspx?BoardID=18&MAEULNO=8&no=1561&page=4

    + Recent posts