SmalltalkBestPracticePatterns:4.3

From 흡혈양파의 번역工房
Jump to: navigation, search
4.3 임시 변수

임시 변수

임시 변수는 표현식의 값을 저장하고 재사용하게 해준다. 이는 메서드의 성능이나 가독성을 향상시키는 데에 사용할 수 있다. 다음 패턴들은 임시 변수의 사용을 조장하고 안내해준다. 예제는 스몰토크로부터 발췌했지만 그에 관한 논의는 프로시저 범위의 변수로 된 언어에 모두 적용된다.


임시 변수

Composed Method(p.21)에 임시 저장이 필요하다.

  • 추후 메서드 내에서 표현식의 값을 사용하려면 어떻게 값을 저장하는가?


FP와 같이 상태가 없는(stateless) 언어는 변수가 무엇인지에 대한 개념을 전혀 포함하지 않는다. 표현식의 값이 필요하면 표현식을 평가한다. 프로그램 내 여러 장소에서 값이 필요하다면 여러 장소에서 평가하면 된다.


언어의 디자이너에게는 상태가 없는 언어의 추상 프로퍼티가 매력적으로 보일 수도 있지만 실용적인 프로그램들은 계산의 표현식을 간소화하기 위해 변수를 사용한다. 각 변수는 여러 개의 구분된 특징을 가진다. 변수의 범위는 그것을 사용 가능한 범위 내에 있는 텍스트 영역(textual area)이다. 변수의 범위는 그 값이 얼마나 지속되는지를 정의한다. 변수의 타입은 변수로 전달되는 메시지의 서명이다.


긴 확장범위(extent), 넓은 통용범위(scope), 큰 타입(type)은 변수를 관리하기 힘들게 만든다. 다수의 변수에서 세 가지 요인이 모두 나타난다면 그 프로그램은 전체적으로 볼 때만 이해가 가능할 것이다. 변수의 통용범위, 확장범위, 타입을 가능한 한 제한할 때만이 이해, 수정, 재사용이 쉬운 프로그램을 생산한다.


또 다른 언어 디자인 결정으로는 변수의 명시적 선언을 필요로 하는지 여부와 관련된다. FORTRAN과 같은 초기 언어들은 변수의 존재를 자동적으로 감지하였다. ALGOL과 그 자손들은 명시적 선언을 필요로 했다. 명시적 선언은 프로그램을 쓰는 프로그래머에게는 짐이 되었지만 프로그램을 이해해야 하는 다른 누군가에겐 성공적이었다. 변수의 존재와 사용은 종종 독자가 시작하는 첫 번째 장소가 된다.


  • 통용범위와 확장범위가 단일 메서드인 변수를 생성하라. 메서드 선택자 바로 아래에서 선언하라. 그리고 표현식이 유효해지는 즉시 할당하라.

임시 변수는 목표로 가는 중간에 위치한 계산을 이해시키는 데 도움을 준다. 따라서:

Rectangle>>bottomRight
    | right bottom |
    right := self left + self width.
    bottom := self top + self height.
    ^right @ bottom


위 코드보다 아래 코드를 더 쉽게 읽을 수 있다: (그림 순서 변경)

Rectangle>>bottomRight
    ^self left + self width @ (self top + self height)


Collecting Temporary Variable은 추후 사용을 위해 중간 결과를 저장한다. Caching Temporary Variable(p.106)은 값을 저장함으로써 성능을 향상시킨다. Explaining Temporary Variable(p.108)은 복한 표현식을 나눔으로써 가독성을 향상시킨다. Reusing Temporary Variable(p.109)는 메서드에서 side-effecting 표현식의 값을 사용하도록 해준다.


Collecting Temporary Variable (수집하는 임시 변수)

때때로 중간 결과를 수집하는 데에 Temporary Variable(p.103)이 사용된다.

  • 후에 메서드에서 사용되도록 점차적으로 값을 수집하는 방법은?


열거(enumeration) 프로토콜의 우측 집합은 이 질문을 고려할 가치가 없도록 만들 것이다. 특히 Inject:info: 는 종종 임시 변수의 필요성을 제거한다. 아래와 같은 코드는:

| sum |
sum := 0.
self children do: [:each | sum := sum + each size].
^sum


아래와 같이 재작성할 수 있다:

^self children
    inject: 0
    into: [:sum :each | sum + each size]


복잡한 프로그램에서는 열거 전략의 다양성 때문에 inject:into: idiom을 일반화시키기가 불가능하다. 예를 들어, 두 컬렉션을 합쳐 컬렉션 a로부터 하나의 요소, 컬렉션 b로부터 하나의 요소 등등으로 갖고 싶다면 어떨까. 그럴 경우 특별한 열거 메서드가 필요할 것이다:

^self leftFingers
    with: self rightFingers
    inject: Array new
    into: [:sum :eachLeft :eachRight |
    ...(sum copyWith: eachLeft) copyWith: eachRight]


메서드가 지속되는 시간 동안 Stream을 생성하는 편이 훨씬 간단하다:

| results |
results := Array new writeStream.
self leftFingers with: self rightFingers do:
    [:eachLeft :eachRight | results nextPut: eachLeft; nextPut: eachRight].
^results contents


  • 복잡한 열거에서 객체들을 수집하거나 합쳐야 하는 경우, 컬렉션 또는 합친 값을 유지하기 위해 임시 변수를 사용하라.

Collection>>deepCopy 내의 "answer" 변수는 Collecting Temporary Variable(수집하는 임시 변수)의 간단한 예제이다:

deepCopy
    | answer |
    answer := self species new.
    self do: [:each | answer add: each copy].
    ^answer


Role Suggesting Temporary Variable Name(p.102)는 변수를 명명하는 방법을 설명한다.


Caching Temporary Variable (저장하는 임시 변수)

성능 측정에서 메서드 내 표현식이 병목지점(bottlemeck)임을 보였다.

  • 메서드의 성능을 어떻게 향상하는가?


성능과 관련된 많은 결정에 있어, 기계자원이 제한되어 있고 쉽게 만족하지 않는 사용자의 요구에 따라 프로그래밍 스타일을 희생시킨다. 성공적인 성능 조정은 이러한 상호관계를 명시적으로 인식하고, 관리의 증가로 인한 비용보다 성능의 증가로 보상하는 변화의 도입에 달려 있다.


변수에서와 마찬가지로 성능 조정 결정의 통용범위와 확장범위는 극적으로 그 비용에 영향을 미친다. 성능과 관련된 변경사항이 단일 객체로 제한되는 것은 바람직하며, 단일 메서드에만 영향을 미치는 변경사항은 더 바람직하다.


모든 성능 조정에는 두 가지 핵심적인 방법이 있다 - 코드를 덜 자주 실행하든가, 아니면 비용이 적은 코드를 실행하던가이다. 둘 중 첫 번째 방법은 가치가 높은 것이 보통이다. 이는 가독성을 이유로 하여 표현법이 동일한 값을 리턴하더라도 여러 번 실행되는 것이 보통이라는 사실에 의존한다. Caching은 표현식의 값을 저장하여 다음에 그 값이 즉시 사용되도록 한다.


캐시의 가장 큰 문제는 표현식이 동일한 값을 리턴한다는 가정과 관련된다. 사실이 아니라면 어떨까? 한동안은 사실이지만 표현식의 값이 변경되면 어떨까? 캐시를 유효하게 유지 시 야기되는 복잡성을 제한하는 방법은 캐시에 사용된 변수의 통용범위와 확장범위를 제한함으로써 가능하다.


  • 임시 변수가 유효해지는 즉시 표현식의 값으로 설정하라. 메서드 나머지 부분에 표현식 대신 변수를 사용하라.

예를 들어, 수신자의 bounds를 사용하는 그래픽 코드를 갖고 있을 수 있다. bounds의 계산에 비용이 많이 들어가는 경우, 아래와 같은 코드를:

self children do: [:each | ...self bounds...]


아래로 변형할 수 있다:

| bounds |
bounds := self bounds.
self children do: [:each | ...bounds...]


bounds의 계산 비용이 메서드의 비용을 초과하는 경우, 자녀수에서 비용이 선형(linear)인 메서드를 취한 후 상수인 메서드가 된다.


Role Suggesting TEmporary Variable Name(p.102)는 변수의 명명 방법을 설명한다. Caching Instance Variable는 표현식 값이 다수의 메서드에서 사용되는 경우 값들을 저장한다.


Explaining Temporary Variable (설명하는 임시 변수)

Temporary Variable(p.103)을 이용해 복잡한 메서드의 가독성을 향상시킬 수 있다.

  • 메서드 내에서 복합 표현식을 간소화하는 방법은?


순간적인 열정으로 인해 꽤 복잡한 메서드 내에서 표현식을 작성할 수 있다. 대부분의 표현식은 처음엔 단순하다. 하지만 실제 데이터를 보는 순간 당신의 순진한 가정은 절대 작동하지 않을 것임을 깨닫는다. 이 복잡성을 추가하고, 저걸 추가하고, 다음엔 저것을 추가하다가 메시지 계층들이 급격히 증가할 때까지 추가하게 된다. 디버깅하는 도중에는 컨텍스트를 많이 갖고 있기 때문에 모두 이해가 가능하다. 6개월 후에 그러한 메서드를 다시 보면 꽤 다른 느낌을 경험할 것이다.


메서드를 올바르게 수정 시에는 여러 객체에 변경을 필요로 할지 모른다. 단순히 관찰 중이라면 전념을 다해 몰두하기가 부적절할 것이다.


  • 복합 표현식에서 subexpression을 꺼내라. 그 값을 복합 표현식 이전에 임시 변수로 할당하라. 복합 표현식에서 대신 변수를 사용하라.

한 가지 예로 Visual Smalltalk의 LinearHashTable>>findKeyIndex:for: 에 사용되는 "lastIndex" 변수가 있다. "size" 메시지는 빠르기 때문에, 변수가 사용되지 않는다면 메서드의 성능은 그다지 변하지 않을 것이다. 수신자의 크기는 색인에 대한 최종 값을 평균내는 데에(mean) 사용되므로, 변수는 메서드를 설명하는 데에 도움을 준다:

LinearHashTable>>findKeyIndex: element for: client
    | index indexedObject lastIndex |
    lastIndex := self size.
    ...


Role Suggesting Temporary Variable Name(p.102)는 변수의 명명 방법을 설명한다. Composed Method(p.21)는 subexpression을 그것이 속한 곳에 위치시키고 명명한다.


Reusing Temporary Variable (재사용하는 임시 변수)

Temporary Variable(p.103)을 이용해 한 번 이상 실행될 수 없는 표현식의 값을 재사용할 수 있다.

  • 표현식의 값이 변경될 때 메서드 내 여러 장소에서 표현식을 사용하는 방법은?


임시 변수가 없는 메서드는 임시 변수가 있는 메서드에 비해 이해하기가 쉽다. 하지만 표현식의 평가로 인한 부작용 혹은 외부의 영향으로 인해 값이 변하나 그 값을 한 번 이상 사용해야 하는 상황에 직면하기도 한다. 임시 변수의 사용은 그러한 상황에서 사용할 만한 가치가 있는데, 그 이유는 임시 변수를 사용하지 않을 시 코드가 작동하지 않기 때문이다.


예를 들어, 스트림으로부터 읽고 있을 경우, "stream next"의 평가는 스트림의 변경을 야기한다. 읽은 값과 키워드의 리스트를 서로 대조해보고 있다면 값을 저장해야 한다. 따라서:



아마도 당신의 의도와는 다를 것이다. 대신 임시 변수 내에 값을 저장하여 "stream next"는 한 번만 실행하도록 해야 한다.



외부 세계의 영향을 받는 자원들 또한 이러한 조치를 필요로 한다. 가령 동일한 답을 보장받길 원한다면 "Time millisecondClockValue"를 한 번 이상 실행해선 안 된다.

  • 표현식을 한 번 실행하고 임시 변수를 설정하라. 메서드 나머지 부분에 표현식 대신 변수를 사용하라.

Role Suggesting Temporary Variable Name(p.110)는 변수의 명명 방법을 설명한다.


Role Suggesting Temporary Variable Name (역할을 제시하는 임시 변수명)

Collecting Temporary Variable(p.105)는 계산의 중간 결과를 저장한다. Caching Temporary Variable(p.106)는 중복 계산을 제거함으로써 성능을 향상시킨다. Explaining Temporary Variable(p.108)은 복합 표현식을 포함하는 메서드를 읽기 쉽게 만든다. Reusing Temporary Variable(p.109)는 side-effecting 표현식을 포함하는 메서드를 실행한다.

  • 임시 변수를 무엇이라 부르는가?


변수에 관한 정보를 전달하는 데에 중요한 두 가지 영역이 있다. 첫 번째는 타입이다. 코드를 수정하길 원하는 독자들은 변수를 차지하는 객체에게 어떤 책임을 맡겨야 하는지 알 필요가 있다. 두 번째 중요한 영역은 역할, 즉 객체가 계산에 어떻게 사용되는지와 관련된다. 역할을 이해하는 것은 변수가 사용되는 메서드를 이해하는 데에 중요하다. 변수의 종류마다 타입과 역할을 전달하는 데에 다른 명명 규칙이 필요하다.


임시 변수는 컨텍스트에 따라 역할에 대한 정보를 전달한다. 아래를 살펴보면:

| sum |
sum := 0.
...sum...


타입을 혼동할 리가 만무하다. 임시 변수가 표현식에 의해 초기화된다고 하더라도 표현식만 잘 작성되면 그 타입은 이해할 수 있을 것이다:

| bounds |
bounds := self bounds.
... bounds ...


반면 역할은 명시적 전달을 필요로 한다. 임시 변수의 이름이 "a," "b"와 그 유명한 "temp"로 된 코드는 아마 누구든 한 번쯤 읽어본 경험이 있을 것이다. 독자로서 당신은 "아하! 'b'가 부모 위젯의 좌측 오프셋(left offset)이구나"라고 깨닫기까지는 위와 같이 필요 없는 이름을 포함하는 코드를 검토해야 한다.


  • 계산에서 임시 변수의 역할에 따라 이름을 붙여라.

변수의 명명을 향후 독자들에게 귀중한 전술적 정보를 전달할 수 있는 기회로 사용하라. "results"는 Collecting Temporary Variable(수집하는 임시 변수)에 훌륭한 이름이다. "right"와 "bottom"은 Explaining Temporary Variables(설명하는 임시 변수)에 좋은 이름이다.


Notes