이번 시리즈에서 이전 기사에서 확인한 대로, 필자의 주장은 소프트웨어의 모든 조각이 재사용 가능한 코드 덩어리를 포함한다는 것이다. 예를 들어, 회사가 보안을 처리하는 방법은 아마 하나의 애플리케이션과 복수의 애플리케이션에 걸쳐 거의 일관적이다. 이는 필자가 관용적 패턴으로 참고하는 내용의 예제이다. 이러한 패턴은 소프트웨어의 특정한 조각을 빌드하는 동안 부딪히는 문제에 공통적인 솔루션을 표현한다. 관용적 패턴은 다음 두 가지 스타일에서 존재한다.
- 기술적 패턴 - 이는 트랜잭션, 보안 및 기타 인프라 요소를 포괄한다.
- 도메인 패턴 - 이는 단일 애플리케이션 또는 복수 애플리케이션에 걸쳐 이루어지는 비즈니스 문제에 공통 솔루션을 포함한다.
이전 기사에서 필자는 이러한 패턴을 어떻게 발견할 수 있는지에 대해 모든 관심을 집중시켰다. 그러나, 한 번 이를 발견하면, 재사용 가능한 코드로 이를 활용할 수 있어야 한다. 이 기사에서는 설계와 코드 사이의 관계를 조사하고, 특히 패턴을 채취하기 위해 표현적 코드를 더 간편하게 만드는 방법에 대해 알아본다. 그리고, 추상 스타일을 변경하여 때로는 추적할 수 없는 것처럼 보이는 설계 문제를 해결할 — 그리고 코드를 간소화할 — 수 있음을 확인할 것이다.
1992년으로 거슬러 올라가서, Jack Reeves는 "What is Software Design?"이라는 제목의 통찰력있는 에세이를 썼다. (온라인 사본은 참고자료 참조). 그는 여기에서 소프트웨어 개발자를 위한 인용 부호를 제거하는 것을 목표로 기존 엔지니어링(예: 하드웨어 엔지니어링 및 구조적 엔지니어링)을 소프트웨어 "엔지니어링"과 비교한다. 이 에세이는 몇 가지 흥미로운 결론에 도달한다.
Reeves가 관찰한 첫 번째 내용은 엔지니어링 노력을 위한 최종 결과물이 "문서의 일부 유형"이라는 점이다(필자의 이탤릭체). 다리를 설계하는 구조적 엔지니어는 실제 다리를 제공하지 않는다. 완성된 작업은 다리를 위한 설계이다. 해당 설계는 건축을 위해 제조업 팀에게 전달된다. 소프트웨어를 위한 유추적 설계 문서는 무엇인가? 이는 냅킨에 쓴 낙서, 화이트 보드의 낙서, UML 다이어그램, 순서 다이어그램 및 기타 유사한 아티팩트인가? 이는 모두 설계의 부분이지만, 이러한 콜렉션은 실질적인 것을 만들기 위해 제조업 팀으로 넘기는 데 충분하지 않다. 소프트웨어에서 제조업 팀은 컴파일러 및 배치 메커니즘이며, 이는 완료된 설계가 소스 코드 — 완료 소스 코드 —임을 의미한다. 기타 아티팩트는 코드 작성에 유용하지만, 최종 설계 결과물은 코드 그 자체이며, 소프트웨어에서 해당 설계가 코드에서부터 추상화될 수 없음을 의미한다.
다음으로 Reeves가 지적하는 요점은 제조업의 비용에 관한 것이며, 이는 일반적으로 엔지니어링 노력의 일환으로 고려되지 않지만 엔지니어링된 아티팩트에 대한 전체 비용 견적의 일부이다. 실제적인 것을 빌드하는 것은 비용이 많이 든다. 대개 전체 생산 프로세스 중 가장 비용이 많이 드는 부분이다. 이와는 대조적으로, Reeves는 다음과 같이 말한다.
"...소프트웨어는 빌드하기에 저렴하다. 이는 비용이 많이 들지 않는다는 말로 적합하지 않다. 즉, 너무 저렴해서 거의 무료이다."
그리고 그가 C++ 컴파일과 링크 주기를 견디는 중이었으며, 이는 시간이 엄청 많이 든다는 점을 유의하자. 이제 Java™ 영역에서 입력을 중지할 때마다 요정 팀이 활기를 불어 넣어 설계를 제조한다. 소프트웨어를 빌드하는 것은 이제 너무 자유롭기 때문에 거의 볼 수 없다. 아마도 설계를 자유롭게 구축하고 가상(what-if) 게임을 할 수 있다는 점을 좋아할 기존의 엔지니어에 비해 우리는 엄청난 장점이 있다. 다리 엔지니어가 실시간으로 무료로 설계를 해 볼 수 있다면 얼마나 정교한 다리가 될 것인지 상상할 수 있는가?
제조업의 편의성은 소프트웨어 개발에서 왜 더 많은 수학적 규칙이 없는지 설명한다. 기존 엔지니어들은 예측 가능성을 위해 수학적 모델과 기타 정교한 기술을 개발하였으므로, 이러한 특성을 판별하기 위해 별도의 조치를 빌드하도록 강요되지 않았다. 소프트웨어 개발자들은 이러한 수준의 분석이 필요하지 않다. 우리가 설계를 빌드하고 이를 테스트하는 것은 어떻게 작동할 것인지에 대한 공식적인 증거를 빌드하는 것보다 더 간편하다. 테스팅은 소프트웨어 개발의 엔지니어링 규칙이다. 이는 Reeves의 에세이에서부터 다음과 같이 가장 흥미로운 결론을 이끌어 낸다.
소프트웨어 설계가 구현되기에 상대적으로 간편하고 빌드하기에 본질적으로 자유롭다고 고려하면, 소프트웨어 설계가 엄청나게 크고 복잡한 경향이 있다는 점은 놀랍지 않은 사실이다.
사실, 필자는 소프트웨어 설계가 특히, 우리가 빌드하고 있는 것에 정교함이 끊임없이 올라간다는 점을 고려해보면 사람이 지금까지 시도한 것 중에 복잡한 것 중 하나라고 생각한다. 소프트웨어 개발이 불과 약 50년 동안 주류가 되었음을 생각하면, 일반적인 엔터프라이즈 시스템에서 빌드하기 위해 어느 정도의 복잡성을 관리해왔는지는 놀랍다.
Reeves의 에세이의 또 다른 결론은 소프트웨어에서 설계(즉, 전체 소스 코드 쓰기)가 지금까지 가장 비용이 많이 드는 활동이라는 것이다. 이는 설계할 때 낭비한 시간이 가장 비싼 자원의 낭비임을 의미한다. 이는 필자에게 창발적 설계를 상기시킨다. 코드 쓰기를 시작하기 전에 독자가 필요할 모든 것을 예상하도록 시도하는 데 엄청난 시간을 쓰면, 모르고 있는 내용을 아직 모르기 때문에 항상 어느 정도의 시간을 낭비할 것이다. 다시 말해서, 일부 요구사항이 생각했던 것보다 더 복잡하거나, 시작 부분에 문제를 전체적으로 이해하지 않았었기 때문에 소프트웨어를 쓸 때에 예상치 못한 시간 손실에 항상 직면하게 된다. 의사결정을 더 오래 미루면 미룰수록, 더 훌륭한 의사결정을 내리는 능력이 더 높아진다 — 왜냐하면 취득하는 컨텍스트와 지식은 시간이 흐르면서 늘어나기 때문이다. 이는 다음 그림 1에 표시된다.
그림 1. 의사결정을 더 오래 미루면 미룰수록, 더 컨텍스트화될 수 있다
린 이동(lean movement)은 책임이 따르는 마지막 순간(last responsible moment)이라는 훌륭한 단계가 있다 — 이는 의사 결정의 마지막 순간이 아니라 의사결정을 위한 책임이 따르는 마지막 순간이다. 더 오래 기다리면 기다릴수록, 더 적합한 설계를 위해 더 나은 기회가 있다.
하지만 Reeves의 에세이의 또 다른 결론은 인식 가능한 설계의 중요성을 중심으로 돌아가며, 이는 더 인식 가능한 코드로 변환된다. 코드에서 관용적 패턴을 찾는 것도 충분히 어렵지만, 언어가 추가 크러프트(cruft)를 추가하면 이는 더 어려워지게 된다. 예를 들어, 어셈블리 언어 코드 기반에서 관용적 패턴을 찾는 것은 매우 어렵다. 왜냐하면 해당 언어가 이해할 수 없는 요소를 너무 많이 부과하기 때문에 설계를 "확인하기" 위해 주변을 살필 수 있어야 한다.
설계가 코드이기 때문에, 독자는 가능한 한 가장 표현적인 언어를 선택해야 한다. 언어의 표현성을 활용하면 관용적 패턴이 발생하는 것을 더 간편하게 확인하게 된다. 왜냐하면, 설계의 매체가 더 분명하기 때문이다.
여기에 한 가지 예제가 있다. 이 시리즈의 이전 기사에서("Composed method and SLAP"), 필자는 일부 기존 코드에서 리팩토링 연습을 다루어, 작성한 메소드와 SLAP(single level of abstraction) 원칙을 적용했다. 필자가 이끌어낸 최상위 레벨 메소드는 다음과 같이 목록 1에 나타난다.
목록 1.
addOrder() 메소드를 위한 개선된 추상
public void addOrderFrom(ShoppingCart cart, String userName,
Order order) throws SQLException {
setupDataInfrastructure();
try {
add(order, userKeyBasedOn(userName));
addLineItemsFrom(cart, order.getOrderKey());
completeTransaction();
} catch (SQLException sqlx) {
rollbackTransaction();
throw sqlx;
} finally {
cleanUp();
}
}
// remainder of code omitted for brevity
|
이는 관용적 패턴으로 채취하기 위해 훌륭한 후보인 것처럼 보인다. 목록 2에 표시된 첫 번째 전달은 이를 "네이티브" 언어 — Java 언어 — 에서 다음과 같이 수행한다.
목록 2. 관용적 "작업 단위" 패턴 리팩토링하기
public void wrapInTransaction(Command c) {
setupDataInfrastructure();
try {
c.execute();
completeTransaction();
} catch (RuntimeException ex) {
rollbackTransaction();
throw ex;
} finally {
cleanUp();
}
}
public void addOrderFrom(final ShoppingCart cart, final String userName,
final Order order) throws SQLException {
wrapInTransaction(new Command() {
public void execute() {
add(order, userKeyBasedOn(userName));
addLineItemsFrom(cart, order.getOrderKey());
}
});
}
|
이 버전에서 필자는 Gang of Four의 Command 설계 패턴(참고자료 참조)을 사용하여 중복된 코드를 wrapInTransaction() 메소드로 추상화했다. addOrderFrom()
메소드는 이제 훨씬 더 인식 가능하다 — 이 메소드의 본질은(가장 중심부의 두 행) 더 분명하다. 하지만 추상의 해당 레벨에 도달하기 위해 Java 언어는 많은 기술적 크러프트(cruft)를 강제 실행한다. 독자는
익명의 내부 클래스가 작동하는 방법(Command 서브클래스의 인라인 선언)을 이해하고 execute() 메소드의 영향을 이해해야 한다. 예를 들어,
외부 클래스로부터 참조한 최종 오브젝트만 익명의 내부 클래스의 본문 내에서 호출 가능하다.
만약 필자가 더 표현적인 현대 Java 방언으로 이와 동일한 코드를 쓴다면 어떻게 되는가? 다음 목록 3에 Groovy를 사용하여 다시 쓴 동일한 메소드를 보여준다.
목록 3. Groovy로 다시 쓴
addOrderFrom() 메소드
public class OrderDbClosure {
def wrapInTransaction(command) {
setupDataInfrastructure()
try {
command()
completeTransaction()
} catch (RuntimeException ex) {
rollbackTransaction()
throw ex
} finally {
cleanUp()
}
}
def addOrderFrom(cart, userName, order) {
wrapInTransaction {
add order, userKeyBasedOn(userName)
addLineItemsFrom cart, order.getOrderKey()
}
}
}
|
이 코드(특히 addOrderFrom() 메소드)는 훨씬 더 인식 가능하다. Groovy 언어는 Command 설계 패턴을 포함한다. 즉, Groovy에서 중괄호
— { } —로 범위가 지정된 어느 코드나 자동으로 코드 블록이며, 코드 블록 참조를 보유하는 변수 다음에 열기 및 닫기 소괄호를 놓는
신택틱 슈가(syntactic sugar)를 통해 실행 가능하다. 이러한 내장 패턴을 통해 addOrderFrom() 메소드의 본문이 더 표현적이 될 수 있다(덜 둔감한 코드 덕분에). 또한 Groovy를
통해 매개변수 주변의 일부 소괄호를 제거할 수 있어 소음 문자를 줄일 수 있다.
다음 목록 4는 유사한 변환을 보여준다. 이번에는 Ruby(JRuby를 통해)를 사용한다.
목록 4. Ruby로 변환된
addOrderFrom() 메소드
def wrap_in_transaction
setup_data_infrastructure
begin
yield
complete_transaction
rescue
rollback_transaction
throw
ensure
cleanup
end
end
def add_order_from
wrap_in_transaction do
add order, user_key_based_on(user_name)
add_line_items_from cart, order.order_key
end
end
|
이 코드는 Java 버전보다 Groovy 코드와 더 유사하다. Groovy 코드와 Ruby 코드 사이의 주된 차이점은 Command 패턴 특성이다. Ruby에서는 어느 메소드나 코드 블록을 취할 수 있으며, 이는
메소드 본문 내에서 yield 호출을 통해 실행된다. 그러면 Ruby에서 인프라 요소의 특수 유형을 지정할 필요조차 없다. — 해당 기능은 이러한 일반 사용을 처리하는 언어 내에 존재한다.
다른 언어는 다른 방식으로 추상을 처리한다. 이 기사의 독자는 모두 몇 가지 만연된 추상 스타일 — 예를 들어, 구조적, 모듈형 및 오브젝트 지향 —에 익숙하며, 이는 다양한 언어에서 나타난다. 특정 언어로 오래 작업할 때, 이는 비로소 황금 망치가 된다. 즉, 모든 문제가 해당 언어에서 추상으로 몰아갈 수 있는 못처럼 보인다. 이는 특히 거의 전적으로 오브젝트 지향 언어(예: Java 언어)에 적용된다. 왜냐하면, 기본 추상이 계층이고 변동 가능한 상태이기 때문이다.
Java 영역은 Scala와 Clojure와 같은 기능적 언어에 이제 많은 관심을 보이고 있다. 기능적 언어로 코드할 때에 독자는 문제의 솔루션에 대해 다르게 생각한다. 예를 들어, 대부분의 기능적인 언어에서 기본값은 변동 가능한 변수가 아니라 불변의 변수를 작성하며, 이는 Java 접근방식과 정확히 반대되는 것이다. Java 코드에서 데이터 구조는 기본값으로 변동 가능하며, 독자는 이를 불변으로 작동하게 만들기 위해 코드를 더 추가해야 한다. 불변의 데이터 구조가 내재적으로 스레드와 깔끔하게 상호작용하기 때문에, 이는 멀티스레딩 애플리케이션을 기능적 언어로 쓰는 것이 훨씬 더 간편함을 의미한다.
추상은 온전히 언어 설계자의 영역이 아니다. 2006년에 OOPSLA에서 발표된 하나의 흥미로운 논문은 "Collaborative Diffusion: Programming Antiobjects"(참고자료 참조)라는 제목으로 안티오브젝트의 개념을 소개했다. 이는 우리가 생각하기에 수행해야 하는 것과 반대로 수행하는 오브젝트이다. 이 접근방식은 다음과 같이 해당 논문에서 설명한 문제를 다룬다. 오브젝트의 메타포는 우리가 현실에서 동기가 너무 많이 부여된 오브젝트를 작성하도록 노력하게 만드는 정도로 너무 지나치게 할 수 있다.
논문의 요지는 특정 추상 스타일에 열중하는 것이 너무 간편하여, 실제 되어야 하는 것보다 문제를 더 어렵게 만든다는 것이다. 독자가 솔루션을 안티오브젝트로 코드하여, 시각을 변경하여 더 간단한 문제를 해결할 수 있다.
논문에 인용된 예제는 이 개념을 잘 설명한다 — 이는 다음과 같은 1980년대 초의 원본 Pac-Man 비디오 콘솔 게임(그림 2에 표시됨)이다.
그림 2. 원본 Pac-Man 비디오 게임
원본 Pac-Man 게임은 일부 오늘날 손목시계보다 더 적은 프로세서 성능과 메모리를 보유했다. 게임 설계자는 제한된 자원을 고려할 때 하나의 심각한 문제가 있었다. 즉, 미로에서 두 개의 움직이는 오브젝트 사이의 거리를 어떻게 계산하는가? 이에 대한 프로세스 성능이 거의 없었기 때문에, 모든 게임 인텔리전스를 미로 자체로 빌드하여 안티오브젝트 접근방식을 취했다.
Pac-Man의 미로는 상태 머신이며, 각 셀은 보드의 각 반복에 대한 규칙을 실행한다. 설계자는 Pac-Man 냄새(smell)라는 개념을 발명했다. Pac-Man 캐릭터가 점유한 어느 셀이든지 최대 Pac-Man 냄새가 있었고, 가장 최근에 비워진 셀은 최대 Pac-Man 냄새에서 1을 뺀 것이 있었으며, 그 냄새는 신속하게 사라졌다. 유령(Pac-Man을 쫓고 Pac-Man보다 약간 더 빠르게 이동할 수 있음)은 냄새가 더 강력한 셀로 이동하는 시점에 Pac-Man 냄새에 마주칠 때까지, 임의로 가짜로 돌아다닌다. 유령 이동에 임의성을 어느 정도 추가하면 Pac-Man을 갖게 된다. 이 설계의 하나의 부작용은 유령이 Pac-Man을 차단하는 능력이 없는 것이다. 즉, 유령은 Pac-Man이 오는 것을 볼 수 없고, 어디에 있었는지만 확인할 수 있다.
이 문제를 이렇게 간단하게 다시 생각하여 내재된 코드를 훨씬 더 간단하게 만들었다. Pac-Man 설계자는 추상을 배경으로 변경하여 고도로 제한된 환경에서 그들의 목표를 달성했다. 특정한 고약한 문제에 직면할 때(특히 과도하게 복잡한 코드로부터 리팩토링할 때), 더 합리적으로 만들 수 있는 안티오브젝트 접근방식이 있는지 스스로 물어보자.
이번 기사에서 필자는 표현성이 왜 중요하며 코드에서 표현성의 표출에 대해 살펴보았다. 필자는 Jack Reeves의 엔지니어링 비교에 동의하는 반면, 완료된 소스 코드가 소프트웨어에서 설계 아티팩트라고 생각한다. 독자가 이를 한 번 이해하고 나면, 이는 과거의 실패에 대해 많은 내용을 설명한다(예를 들어, 다이어그래밍 언어가 필수적인 미묘한 차이를 캡처할 정도로 표현적이지 않기 때문에, UML 아티팩트에서부터 코드로 직접 이동하도록 시도하여 실패한 모델 구동형 아키텍처). 이러한 이해는 해당 설계(이는 코딩임)가 수행 가능한 가장 비용이 많이 드는 활동이라는 점에 대한 인식을 비롯한 몇 가지 부작용이 있다. 이는 코딩을 시작하기 전에 설계를 이해하는 데 유용한 사전 도구(예를 들어, UML 또는 이와 유사한 것)를 사용하지 말아야 한다는 것을 의미하지는 않지만, 코드는 해당 단계로 한 번 이동하면 실제 설계가 된다.
인식 가능한 설계가 중요하다. 설계가 더 표현적이면 표현적일 수록, 이를 수정하는 것이 더 간편해지며 실제로 창발적 설계를 통해 이로부터 관용적 패턴을 채취하는 것이 더 간편해진다. 다음 기사에서는 이러한 사고의 흐름을 계속 유지하면서 코드에서 채취하는 설계 요소를 활용하는 구체적인 방법을 제시할 것이다.
교육
-
The Productive Programmer(Neal
Ford저, O'Reilly Media, 2008년): 이 시리즈의 다양한 주제를 확장해 놓은 Neal Ford의 최신 저서이다.
-
"What is
Software Design?" (Jack Reeves저, C++ Journal, 1992년; developerdotstar.com에서 재판됨): 프로그래밍과 소프트웨어 설계 사이의 관계에 대한 이 에세이를 읽어보자.
-
Design Patterns(Erich Gamma 외 공저, Addison-Wesley, 1995년): Command 패턴을 비롯하여 설계 패턴에 대한 고전 작업이다.
- "Groovy: A DSL for
Java programmers"(Scott Davis저, developerWorks, 2009년 2월):
Practically
Groovy 시리즈를 읽고 Groove의 고급 구문으로 인식 가능한 정도가 더 높은(및 더 낮은) 코드를 쓸 수 있는 방법에 대해 배워보자.
- "Ruby off the rails"(Andrew Glover저, developerWorks, 2005년 12월): Java 개발자의 관점에서 Ruby에 대해 알아보자.
-
Hibernate: 이 대중적인 오픈 소스 오브젝트 지향 맵핑 프레임워크는 많은 편리한 관용적 패턴을 요약한다.
-
Spring: Spring 프레임워크는 모든 Javadom 중에 가장 유용한 프레임워크 중 하나로 고려된다.
-
Ruby on Rails: Rails는 Ruby(JRuby 포함) 언어에서 웹 애플리케이션을 작성하기 위한 정교한 프레임워크이다.
-
"Collaborative Diffusion: Programming Antiobjects"(Alexander Repenning저, OOPSLA 2006): 이 논문은 안티오브젝트
추상 접근방식을 설명한다.
-
기술 서점에서
다양한 기술 주제와 관련된 서적을 살펴보자.
-
developerWorks Java 기술 영역: Java 프로그래밍과 관련된 모든 주제를 다루는 여러 편의 기사를 찾아보자.
제품 및 기술 얻기
-
IBM 제품 평가판을
다운로드하거나 IBM SOA Sandbox의 온라인 시험판을 살펴보고
DB2®, Lotus®, Rational®, Tivoli® 및 WebSphere®의 애플리케이션 개발 도구 및
미들웨어 제품을 사용해 볼 수 있다.
토론
- My developerWorks 커뮤니티에 참여하자.

Neal Ford는 글로벌 IT 컨설팅 업체인 ThoughtWorks의 소프트웨어 아키텍트이자 Meme Wrangler이다. 애플리케이션, 교육용 자료, 매거진 기사 및 비디오/DVD 프리젠테이션을 설계 및 개발하며 다양한 기술과 관련된 서적의 저자 또는 편집자이기도 하다. 최근에 출판된 책으로는 The Productive Programmer가 있다. 대규모 엔터프라이즈 애플리케이션의 설계 및 빌드에 많은 관심을 가지고 있는 그는 전세계의 개발자 컨퍼런스에서 국제적으로 인정 받고 있는 연사로도 활동하고 있다. 그의 웹 사이트를 살펴보자.