if( state == 0 ) { // do something } else if( state == 1 ) { // do something }이렇게 작성된 코드를 보면 코드의 의도가 무엇인지 전혀 알 수 없습니다. 그렇다고 "상태가 0인 경우"라고 주석을 다는 것은 전혀 의미가 없습니다. 그런 정도는 코드가 스스로 외치고 있습니다. 그렇다면, 어떤 주석을 달아야 할까요? "전송을 요청한 경우", "오류가 발생해서 재전송을 요청한 경우"와 같은 형태로 주석을 달아야 합니다. 주석을 달고 나면 코드를 읽는 것은 분명 쉽지만, 0과 1이라는 숫자는 찝찝합니다. 이처럼 어떤 의미를 갖는 숫자를 그대로 사용하는 것을 마법 상수(Magic Number)라 부릅니다. 따라서, 이를 의미있는 이름을 가진 상수로 변경하는 것이 바람직합니다.
#define S_REQUEST_TRANS 0 #define S_REQUEST_RETRY 1상수 이름을 정의할 때는 해당 도메인에서만 쓰이는 것을 구분하기 위해 적절한 접두어를 사용하는 것이 좋습니다. 여기서는 "S_"라는 것을 붙였습니다. 리눅스 커널에서 네트워크 필터링 기능과 관련되어 정의된 상수들은 모두 "NF_"라는 접두어를 붙입니다. 이 때문에 코드 내의 어디에서든 NF_로 시작하면 넷필터라는 도메인에서 사용되는 상수라는 것을 알 수 있습니다. 상수를 정의하는 방법이나 의미의 사용은 사용하는 언어에 따라 조금씩 다릅니다. 클래스나 네임스페이스 안에 캡슐화하는 경우도 있을 것이고, 전역으로 선언하는 경우도 있을 겁니다. 보다 좋은 상수 선언 방법은 해당 언어 레퍼런스를 참고하기 바랍니다.
이제, 의미있는 상수를 정의했고, 0과 1을 대체하면 다음과 같이 됩니다. if( state == S_REQEUST_TRANS ) { // do something } else if( state == S_REQUEST_RETRY ) { // do something }이런 코드에서는 코드 만으로도 의미가 명확하기 때문에 주석이 필요없습니다. 있던 주석을 제거해도 됩니다. 이처럼 숨겨진 의도를 갖고 있는 경우에는 적절하게 코드를 재정의하는 것으로 숨겨진 의도를 제거할 수 있습니다. 즉, 숨겨진 의도를 가진 코드를 주석으로 설명하는 것은 대부분의 경우에 잘못된 주석을 생성하는 것에 불과합니다. 그러나, 고수들의 코드이기 때문에 주석이 없다는 것은 잘못된 설명입니다. 주석을 써야 한다, 말아야 한다라는 이분법적인 사고가 "고수들의 코드엔 주석이 없다"라는 말을 만들어내는 것은 아닌가 합니다. 나는 고수들의 코드일수록 주석은 적재적소에 쓰일 수 있어야 한다고 생각합니다. 이런 경우를 하나 설명해 보겠습니다.
#ifndef _ASM_SEGMENT_H #define _ASM_SEGMENT_H #define __KERNEL_CS 0x10 #define __KERNEL_DS 0x18 #define __USER_CS 0x23 #define __USER_DS 0x2B #endif매우 단순하게 되어 있습니다. 0x10이나 0x18이라는 값 만으로는 어떤 특권 레벨을 선택한 것이며, 어떤 옵션들을 선택한 것인지 전혀 알 수 없습니다. 0x10이라는 값을 2진수로 바꾸고, 각 비트 순서에 대해서 CPU 매뉴얼을 보고 일일이 비교해야만 그 의미를 이해할 수 있습니다.
#ifndef _ASM_SEGMENT_H #define _ASM_SEGMENT_H /* * The layout of the per-CPU GDT under Linux: * * 0 - null * 1 - reserved * 2 - reserved * 3 - reserved * * 4 - unused <==== new cacheline * 5 - unused * * ------- start of TLS (Thread-Local Storage) segments: * * 6 - TLS segment #1 [ glibc"s TLS segment ] * 7 - TLS segment #2 [ Wine"s %fs Win32 segment ] * 8 - TLS segment #3 * 9 - reserved * 10 - reserved * 11 - reserved * * ------- start of kernel segments: * * 12 - kernel code segment <==== new cacheline * 13 - kernel data segment * 14 - default user CS * 15 - default user DS * 16 - TSS * 17 - LDT * 18 - PNPBIOS support (16->32 gate) * 19 - PNPBIOS support * 20 - PNPBIOS support * 21 - PNPBIOS support * 22 - PNPBIOS support * 23 - APM BIOS support * 24 - APM BIOS support * 25 - APM BIOS support * * 26 - ESPFIX small SS * 27 - unused * 28 - unused * 29 - unused * 30 - unused * 31 - TSS for double fault handler */ #define GDT_ENTRY_TLS_ENTRIES 3 #define GDT_ENTRY_TLS_MIN 6 #define GDT_ENTRY_TLS_MAX (GDT_ENTRY_TLS_MIN + GDT_ENTRY_TLS_ENTRIES - 1) #define TLS_SIZE (GDT_ENTRY_TLS_ENTRIES * 8)각 비트들에 대한 주석이 추가된 것을 알 수 있습니다. 또한, 각 비트 필드들의 의미도 세세하게 나뉘어서 GDT_ENTRY_TLS_ENTRIES와 같이 다양한 상수로 정의되어 있습니다. 실제 소스 코드는 상수가 더 많습니다. __USER_CS나 __KERNEL_CS 같은 정의도 다음과 같이 바뀌었습니다.
#define GDT_ENTRY_DEFAULT_USER_DS 15 #define __USER_DS (GDT_ENTRY_DEFAULT_USER_DS * 8 + 3) #define GDT_ENTRY_KERNEL_BASE 12 #define GDT_ENTRY_KERNEL_CS (GDT_ENTRY_KERNEL_BASE + 0) #define __KERNEL_CS (GDT_ENTRY_KERNEL_CS * 8)여전히 몇 개의 상수가 걸리지만, 보다 의미가 명확해진 것을 알 수 있습니다. 그렇지 않습니까?
// ==++== // // // Copyright (c) 2002 Microsoft Corporation. All rights reserved. // // The use and distribution terms for this software are contained in the file // named license.txt, which can be found in the root of this distribution. // By using this software in any fashion, you are agreeing to be bound by the // terms of this license. // // You must not remove this notice, or any other, from this software. // // // ==--== /*============================================================ ** ** Class: Hashtable ** ** ** ** Purpose: Hash table implementation ** ** Date: September 25, 1999 ** ===========================================================*/ namespace System.Collections { using System; using System.Runtime.Serialization; // The Hashtable class represents a dictionary of associated keys and values // with constant lookup time. // // Objects used as keys in a hashtable must implement the GetHashCode // and Equals methods (or they can rely on the default implementations // inherited from Object if key equality is simply reference // equality). Furthermore, the GetHashCode and Equals methods of // a key object must produce the same results given the same parameters for the // entire time the key is present in the hashtable. In practical terms, this // means that key objects should be immutable, at least for the time they are // used as keys in a hashtable. // // When entries are added to a hashtable, they are placed into // buckets based on the hashcode of their keys. Subsequent lookups of // keys will use the hashcode of the keys to only search a particular bucket, // thus substantially reducing the number of key comparisons required to find // an entry. A hashtable"s maximum load factor, which can be specified // when the hashtable is instantiated, determines the maximum ratio of // hashtable entries to hashtable buckets. Smaller load factors cause faster // average lookup times at the cost of increased memory consumption. The // default maximum load factor of 1.0 generally provides the best balance // between speed and size. As entries are added to a hashtable, the hashtable"s // actual load factor increases, and when the actual load factor reaches the // maximum load factor value, the number of buckets in the hashtable is // automatically increased by approximately a factor of two (to be precise, the // number of hashtable buckets is increased to the smallest prime number that // is larger than twice the current number of hashtable buckets). // // Each object provides their own hash function, accessed by calling // GetHashCode(). However, one can write their own object // implementing IHashCodeProvider and pass it to a constructor on // the Hashtable. That hash function would be used for all objects in the table. // // This Hashtable is implemented to support multiple concurrent readers and // one concurrent writer without using any synchronization primitives. All // read methods essentially must protect themselves from a resize occuring while // they are running. This was done by enforcing an ordering on inserts & // removes, as well as removing some member variables and special casing the // expand code to work in a temporary array instead of the live bucket array. // All inserts must set a bucket"s value and key before setting the hash code // & collision field. All removes must clear the hash code & collision field, // then the key then value field (at least value should be set last). // ///지금까지 살펴본 것처럼 주석에겐 아무 죄가 없다. 주석을 적재적소에 사용하지 못하는 것이 문제일 뿐이다. 주석과 코드를 함께 사용해서 읽을 수 있다면 설명 코드에 숨겨진 의도가 있더라도 "리팩터링"을 통해 수정하면 그만이다. 그러나, 주석도 없고, 숨겨진 의도나 지식으로 가득찬 코드는 읽기도 어렵고, "리팩터링"을 수행하기도 어렵다. 이런 코드가 진짜 문제일 뿐이다.[Serializable()] public class Hashtable : IDictionary, ISerializable, I DeserializationCallback, ICloneable { /* Implementation Notes: Dictionary was copied from Hashtable"s source - any bug fixes here probably need to be made to Dictionary as well. This Hashtable uses double hashing. There are hashsize buckets in the table, and each bucket can contain 0 or 1 element. We a bit to mark whether there"s been a collision when we inserted multiple elements (ie, an inserted item was hashed at least a second time and we probed this bucket, but it was already in use). Using the collision bit, we can terminate lookups & removes for elements that aren"t in the hash table more quickly. We steal the most significant bit from the hash code to store the collision bit. Our hash function is of the following form: h(key, n) = h1(key) + n*h2(key) where n is the number of times we"ve hit a collided bucket and rehashed (on this particular lookup). Here are our hash functions: h1(key) = GetHash(key); // default implementation calls key.GetHashCode(); h2(key) = 1 + (((h1(key) >> 5) + 1) % (hashsize - 1)); The h1 can return any number. h2 must return a number between 1 and hashsize - 1 that is relatively prime to hashsize (not a problem if hashsize is prime). (Knuth"s Art of Computer Programming, Vol. 3, p. 528-9) If this is true, then we are guaranteed to visit every bucket in exactly hashsize probes, since the least common multiple of hashsize and h2(key) will be hashsize * h2(key). (This is the first number where adding h2 to h1 mod hashsize will be 0 and we will search the same bucket twice). We previously used a different h2(key, n) that was not constant. That is a horrifically bad idea, unless you can prove that series will never produce any identical numbers that overlap when you mod them by hashsize, for all subranges from i to i+hashsize, for all i. It"s not worth investigating, since there was no clear benefit from using that hash function, and it was broken. For efficiency reasons, we"ve implemented this by storing h1 and h2 in a temporary, and setting a variable called seed equal to h1. We do a probe, and if we collided, we simply add h2 to seed each time through the loop. A good test for h2() is to subclass Hashtable, provide your own implementation of GetHash() that returns a constant, then add many items to the hash table. Make sure Count equals the number of items you inserted. Note that when we remove an item from the hash table, we set the key equal to buckets, if there was a collision in this bucket. Otherwise we"d either wipe out the collision bit, or we"d still have an item in the hash table. */ // Table of prime numbers to use as hash table sizes. Each entry is the // smallest prime number larger than twice the previous entry. private readonly static int[] primes = { 11,17,23,29,37,47,59,71,89,107,131,163,197,239,293,353,431,521,631,761,919, 1103,1327,1597,1931,2333,2801,3371,4049,4861,5839,7013,8419,10103,12143,14591, 17519,21023,25229,30293,36353,43627,52361,62851,75431,90523, 108631, 130363, 156437, 187751, 225307, 270371, 324449, 389357, 467237, 560689, 672827, 807403, 968897, 1162687, 1395263, 1674319, 2009191, 2411033, 2893249, 3471899, 4166287, 4999559, 5999471, 7199369 };
/* author: traxacun at ...... created: 2002. 12. last modified: 2005. 11. 17. changed logs: 20051117,mm_post_safe() modified,traxacun,HTTP Response Splitting 대응 20051117,mm_get_safe() modified,traxacun,HTTP Response Splitting 대응 20050803,mm_setTitle( $title ) 20040507,mm_hasSpace() added,traxacun 20040507,mm_eatMarks() added,traxacun 20040209,mm_getTeacherName() added,coffin 20040204,mm_safe() added,traxacun 20040131,mm_ip2long() added,traxacun 20040131,mm_long2ip() added,traxacun 20040128,mm_highlightWord() added,traxacun 20040128,mm_clearXmlTag() added,traxacun 20040112 mm_DropDownBoxEx() added, coffin 20040103 mm_getSmallCover() added, coffin 20031225,mm_isLogged() added,traxacun 20031224,mm_displayError() added,traxacun 20031215,mm_hasBannedWords() added,traxacun 20031215,mm_isValidDigit() added,traxacun 20031212,mm_isValidBase64() added,traxacun 20031209,mm_isValidShortName() added,traxacun 20031209,mm_isValidDisplayName() added,traxacun 20031209,mm_isValidCategoryName() added,traxacun 20031209,mm_isValidTemplateName() added,traxacun 20031209,mm_isValidSkinName() added,traxacun 20031209,mm_isBlank() added,traxacun 20031204,mm_br2nl() added,traxacun 20031204,mm_br2nl2() added,traxacun 20031127,mm_wrapHtmlTagEx() added,traxacun 20031127,mm_get_safe() added,traxacun 20031126,mm_logError() added,traxacun 20031126,mm_wrapHtmlTag() added,traxacun 20031119,mm_post_safe() added, traxacun 20031112,mm_getFlagValue() added,traxacun 20031112,mm_getFlagName() added,traxacun 20031121,mm_fieldError() added,coffin function: function string mm_displayList($array[][]) interate through 2-dimensions array to display list of records. especially process the result from mm_getRecords(). 각 레코드 목록을 보여주는 함수 mm_getRecords() 함수는 데이터베이스에서 쿼리한 결과를 2차원 배열로 반환하며 이를 위 함수를 사용해서 쉽게 표시할 수 있다. ex. echo mm_displayList($arrList);이 코드의 주석은 매우 길기 때문에 일부만 표시했습니다. 대부분의 웹 사이트 프로젝트들이 그렇듯이 시간이 없었습니다. 때문에, 라이브러리를 구축하더라도 별도의 함수 라이브러리를 위한 문서를 작성하는 것은 어려웠습니다. 또다른 문제는 이 라이브러리를 다른 사람들에게 전달하고, 사용하게 해야 한다는 것이었습니다. 일일이 교육하는 것으로 해결될 문제가 아니었습니다. 목표는 간단했습니다. 웹 사이트 프로젝트에 필요한 각 영역별로 하부 라이브러리를 개발하는 것, 정형화된 PHP 함수 호출 순서의 단순화, 또 이를 통한 PHP 함수 사용의 제거, 생산성 향상이었습니다. 때문에 위와 같은 주석 형태를 정의했습니다. 사람들은 함수 설명이 포함된 부분만을 편집해서 출력해서 자신의 책상에 올려두었고, 이는 훌륭한 함수 레퍼런스 역할을 했습니다. 문서화가 잘되어있기 때문에 개발자들의 저항은 적었습니다. 예를 들어, mm_queryValue( $query) 함수가 있습니다. 이 함수의 사용법은 다음과 같습니다.
$query = "SELECT name from mm_users WHERE id = "myid""; $name = mm_queryValue( $query )myid라는 ID를 가진 사용자가 있는지 알고 싶고, 그에 따라 처리하고 싶다면 다음과 같이 사용했습니다.
$query = "SELECT name from mm_users WHERE id = "myid""; if( !mm_queryScalar( $query ) ) echo "그런 사용자 없음";mm_queryValue() 같은 함수를 위해 대부분의 PHP 개발자들은 다음과 같은 코드를 사용해야 했습니다.
$query = "SELECT name from mm_users WHERE id = "myid""; $dc = mysql_pconnect ($dbhost, $dbuser, $dbpassword); if( $dc == null ) echo "error"; if( !mysql_select_db( $dbname, $dc ) ) echo "error"; $rows = mysql_query($query, $dc); $row = mysql_fetch_array($rows); echo $row[0]; // 사용자 이름 mysql_close( $dc );라이브러리를 사용할 때와 사용하지 않을 때의 생산성 차이는 엄청났기 때문에 라이브러리를 사용했습니다. 그리고, 라이브러리를 사용하면서 코드들을 조합한 응용이 보일 때면 주석의 함수 예제에 지속적으로 추가하여 보완했습니다. 라이브러리 사용을 퍼트릴 수 있었던 데에는 주석의 역할이 컸습니다. XP도 아니고, 문서화도 하지 않지만, 적절한 주석 사용을 통해 코드가 변경되면 그 변경 내용이 즉각 반영되게 했습니다. 주석이 이처럼 긴 경우는 코드의 목적, 배경 지식을 설명하기 위한 경우가 대부분입니다. 실제 코드 내에서의 주석은 보통 이처럼 길지 않습니다. 매우 길어봐야 3줄 이내일 것이며, 3줄 이상이라면 이는 코드에 숨겨진 의도가 지나치게 많은 것이며, 이를 드러낼 수 있도록 코드를 바꿔야 하며, 주석을 줄일 수 있어야 합니다.
이전 글 : PHP 데이터객체로 비즈니스 로직 단순화하기
다음 글 : Tapestry: 컴포넌트 중심 프레임워크
최신 콘텐츠