클린 소프트웨어 - 애자일 설계란 무엇인가

애자일 설계란 무엇인가

시작

애자일 팀은 현재에 집중한다. 내일 필요해질 것이라고 생각하는 것들을 미리 만들어 두지 않는다. 현재 구조에 초점을 두고 이를 더욱 개선하기 위해 노력한다.

잘못된 설계의 증상

다음의 증상은 코드의 작은 부분이 아니라 소프트웨어의 전체 구조로 고루 퍼져 나간다.

  • 경직성 : 설계를 변경하기 어려움
  • 취약성 : 설계가 망가지기 쉬움
  • 부동성 : 설계를 재사용하기 어려움
  • 점착성 : 제대로 동작하기 어려움
  • 불필요한 복잡성 : 과도한 설계
  • 불필요한 반복 : 마우스 남용
  • 불투명성 : 혼란스러운 표현

원칙

위의 악취를 제거하고 현재 기능 집합에 대해 최적의 설계를 구성할 수 있도록 돕는 객체 지향 설계 원칙에 대한 것이다.

이들은 수십 년간 수많은 소프트웨어 개발자와 연구자에 의해 얻어진 것들이다.

  • 단일 책임 원칙
  • 개방 폐쇄 원칙
  • 리스코프 치환 원칙
  • 의존 관계 역전 원칙
  • 인터페이스 분리 원칙

악취와 원칙

악취는 하나 이상의 원칙을 위반했을 때 발생한다. 예를 들어 개방 폐쇄 원칙은 경직성의 악취를 불러 일으킨다.

아무 악취도 나지 않을 때는 원칙을 적용하지 않는다. 원칙에 대한 맹종은 불필요한 복잡성의 악취를 불러 일으킨다.


소프트웨어 프로젝트의 설계는 추상적인 개념으로, 구체적인 각 모듈, 클래스, 메소드의 형태와 구조뿐만 아니라 프로그램의 전체 형태와 구조와도 관련되어 있다. 이것은 결국에는 코드로 표현된다. 결국 소스 코드가 바로 설계다.

소프트웨어에서 어떤 것이 잘못되는가?

처음에 좋은 설계를 가지고 프로그램을 작성하고 있을지라도, 소프트웨어는 시간이 지나면서 점점 부패하기 시작한다. 부패 범위는 점점 늘어나서 유지보수하기 어렵게 된다. 결국 단순한 변경조차 매우 힘든 일이 되어 버린다.

다시 설계하는 것을 원하지만 이것도 쉽지 않다. 고객의 요구사항은 계속 변경하고 있어 설계가 이를 계속 쫓아야 하기 때문이다.

설계의 악취: 부패하고 있는 소프트웨어의 냄새

경직성

시스템을 변경하기 어렵다. 단순한 방법으로도 변경하기 어렵다. 한 군데의 변경이 이에 의존하는 모듈에서 단계적인 변경을 불러 일으킨다.

모듈을 변경하는 작업이 이에 의존하는 모듈의 변경 또한 초래하여 처음 생각했던 것보다 더 오래 걸리는 경우 해당 모듈은 경직성을 갖고 있던 것이다.

취약성

프로그램의 한 군데를 변경했을 때 많은 부분이 잘못된다. 특히 변경한 부분과 개념적으로 관련 없는 부분에서 오류가 발생한다.

이 문제를 해결하는 동안 다른 곳에서 또다른 문제가 발생하는 경우가 많다. 개발자는 꼬리에 꼬리를 무는 오류를 해결해야 한다.

부동성

다른 시스템에서 유용하게 사용될 수 있는 부분을 포함하고 있으나 이를 분리하는 것이 위험하고 힘들다.

점착성

  • 소프트웨어의 점착성

개발자가 설계를 유지하는 대신 엉터리 방법으로 시스템을 변경하는 경우 잘못된 동작을 하기가 옳은 동작을 하기보다 쉬워진다.

  • 환경의 점착성

개발 환경이 느리고 비효율적일 떄 발생한다. 컴파일 시간이 느려서 재컴파일을 필요로 하지 않는 방식으로 변경하고 싶어할 수 있다. 소스 코드를 체크인하는 것이 느려서 가능한 한 적은 체크인만을 필요로 하는 방식으로 변경하고 싶어할 수 있다. 설계가 유지되는지의 여부는 무시하고 말이다.

점착성이 있는 프로젝트는 소프트웨어의 설계를 유지하기 어려운 프로젝트다.

불필요한 복잡성

현재 시점에서는 유용하지 않은 요소가 설계에 포함되어 있다면 불필요한 복잡성을 포함하게 된다.

물론 향후 변경에 대비하여 코드를 유연하게 유지하고 변경을 최소화할 수 있겠으나, 효과가 정반대로 나타내는 경우도 있다.

앞으로 일어날지도 모르는 일을 과도하게 준비함으로써, 설계는 절대 사용되지 않는 구성 요소들로 어지러워진다. 설계는 사용되지 않는 설계 요소들의 부담을 감당해야 하며, 소프트웨어는 점점 더 복잡하고 이해하기 어려워진다.

불필요한 반복

잘라내기 및 붙이기를 통해 불필요하게 반복되는 코드를 작성하게 된다.

같은 코드가 조금씩 다른 형태로 계속 반복하여 나타나면서, 이들을 추상화하기 더욱 어려워지게 된다. 이는 시스템을 이해하고 유지보수하기 쉽게 만드는 것을 어렵게 한다.

반복되는 단위에서 발견된 버그는 모든 반복 부분에서 고쳐져야 한다. 하지만 이러한 반복 부분은 전부 조금씩 다르기 때문에 고치는 작업도 항상 똑같지 않다.

불투명성

모듈을 이해하기 어렵다. 코드는 시간이 지나면서 명료하지 않고 뒤얽힌 방식으로 작성되어 코드를 명료하고 표현적으로 유지하려는 노력을 지속적으로 해야 한다.

개발자는 처음에 코드를 작성할 때 이 코드가 명료하다고 생각할 수 있다. 하지만 이것은 현 시점에서 개발자가 코드와 친숙해 있기 떄문이며, 이후 다른 사람이나 코드를 작성한 사람이 코드를 바라볼 때 이해하기 힘들 수 있다.

개발자는 읽는 사람의 입장에서 생각하고 자신의 코드를 리팩토링하려는 노력을 해야 한다. 다른 사람이 자신의 코드를 검토하게 할 필요가 있다.

무엇이 소프트웨어의 부패를 촉진하는가?

애자일이 아닌 환경에서는 초기 설계에서 예상하지 않았던 요구사항의 변경이 설계의 퇴화를 불러 일으킬 수 있다.

하지만 정말로 요구사항은 매우 변덕스럽게 변경된다. 요구사항의 변경 때문에 설계가 실패한다면, 우리의 설계와 방식에 문제가 있는 것이다. 이러한 변경에 탄력적인 설계를 만드는 방식을 찾고, 그것이 부패하지 않도록 보호할 수 있는 방식을 사용해야 한다.

애자일 팀은 소프트웨어가 부패하도록 내버려두지 않는다

애자일 팀은 변경을 보람으로 삼는다.

시스템의 설계를 가능한 한 명료하고 단순하게 유지하고, 이를 단위 테스트와 인수 테스트로 뒷받침한다. 이를 통해 설계를 유연하고 변경하기 쉬운 것으로 유지할 수 있다. 팀은 이러한 유연성을 이용하여 매번 요구사항에 알맞는 것으로 설계를 개선해 나갈 수 있다.

‘Copy’ 프로그램

var isForPrinter = false
var isForPunch = false

func copy() {
var c: Int = 0
while c != EOF {
c = isForPrinter ? readFromPrinter() : readFromKeyboard()
isForPunch ? writeToPunch(c) : writeToPrinter(c)
}
}

책은 이 예제에서, 처음에 제시된 요구사항을 따라 멋진 프로그램을 만들어 냈지만, 시간이 지남에 따라 요구사항이 추가되고, 이를 쫓는 방식으로 프로그램에 코드를 추가하면서 설계가 망가진 모습을 보여준다. 잘 동작하기는 하지만 앞으로도 요구사항이 추가될 것이라고 가정했을 때 갈수록 코드를 수정하기가 힘들어질 것이다.

변화 예상하기

위의 예제는 몇 번의 변경 후 여러 설계의 악취를 내뿜게 되었다. 몇 번의 변경이 더 가해지면 프로그램은 완전히 엉망이 되어버릴 것이다.

고객을 생각하지 않고 프로그램을 작성하는 것은 훨씬 쉬울 것이다. 하지만 이는 불가능하다. 애자일 팀은 요구사항의 변경을 기꺼이 받아들여야 한다. 요구사항은 언제나 바뀐다는 것을 인지하고 있어야 한다. 우리는 변화하는 요구사항의 세계에 살고 있고, 우리가 만든 소프트웨어가 이런 변화 속에서 살아남을 수 있게 만드는 것이 바로 우리가 해야 하는 일이다.

소프트웨어의 설계가 요구사항 변경 때문에 퇴화한다면, 우리는 애자일 방식대로 하고 있지 않은 것이다.

Copy 프로그램의 애자일 설계

protocol Reader {
func read() -> Int
}

protocol Writer {
func write(_ c: Int)
}

final class KeyboardReader: Reader {
func read() -> Int {
return readFormKeyboard()
}
}

final class PrinterReader: Reader {
func read() -> Int {
return readFromPrinter()
}
}

final class PrinterWriter: Writer {
func write(_ c: Int) {
writeToPrinter(c)
}
}

final class PunchWriter: Writer {
func write(_ c: Int) {
writeToPunch(c)
}
}

func copy(reader: Reader, writer: Writer) {
var c: Int = 0
while c != EOF {
c = reader.read()
writer.write(c)
}
}

이렇게 입력기와 출력기를 추상화한다면 앞으로 입력기나 출력기가 추가될 때 코드를 수정하는 것 대신 코드를 확장하는 것으로 요구사항에 대응할 수 있고, 설계는 망가지지 않게 된다. 이처럼 미래에 있을 비슷한 종류의 변경에도 탄력적이 되도록 만들어야 한다.

애자일 팀은 개방 폐쇄 원칙을 따른다. 위의 코드가 개방 폐쇄 원칙을 따르도록 작성한 코드다. 프로그램을 수정하는 대신 확장을 통해 새로운 기능을 추가할 수 있다.

하지만 최초에 모듈을 설계할 때는 이것이 얼마나 변경될 것인지 예상하려고 하지 않는다. 할 수 있는 가장 간단한 방식으로 작성하고, 요구사항이 변경되고 나서야 이런 종류의 변경에 탄력적이 되도록 모듈의 설계를 바꾼다.

예를 들어 최초에 하나의 입력기와 하나의 출력기가 존재했는데, 새로운 입력기가 추가된 경우 입력기에 대해 추상화하여 설계를 유연하게 가져갈 수 있으나, 출력기는 아직 추가되지 않았으므로 출력기에 대한 추상화는 이 단계에서 하지 않을 수 있다.

애자일 개발자는 해야 할 일을 어떻게 알았는가?

객체 지향 설계의 기본적인 주의에 따라 본인이 해야 할 일을 인지할 수 있다.

최초에 Copy 모듈은 입력기와 출력기에 직접적으로 의존한다. 의존성의 방향 때문에 유연하지 않은 프로그램 구조를 갖게 된다. 상위 수준의 모듈이 하위 수준의 모듈이 하는 일을 알게 되므로, 하위 수준 모듈의 세부 사항이 바뀔 때 상위 수준 모듈의 정책이 영향을 받게 된다.

이를 거꾸로 뒤집어 상위 모듈이 하위 모듈에 의존하지 않도록 해야 했고, 추상화에 의존하는 것으로 의존성을 역전시켰다.

애자일 실천방법으로 문제를 찾아내고, 설계 원칙을 적용하여 문제를 진단하고, 적절한 디자인 패턴을 사용하여 문제를 해결하는 것. 이것이 바로 설계 작업이다.

가능한 한 좋은 상태로 설계 유지하기

애자일 개발자는 가능한 한 설계를 적절하고 명료한 상태로 유지하기 위해 애쓴다. 이 과정은 매일, 매시간, 분마다 이루어지는 것이어야 한다. 절대 소프트웨어가 부패되는 것을 지켜보고 있지 않는다. 작은 부패라 할지라도 방치하면 나중에는 감당할 수 없을 만큼 위험이 커질 것이다.

설계는 명료한 상태로 유지되어야 한다. 그리고 설계의 가장 중요한 표현인 소스 코드 역시 명료한 상태로 유지되어야 한다. 우리는 코드가 부패되도록 내버려둘 수 없다.

결론

애자일 설계는 결과가 아닌 과정이다.

원칙, 패턴, 소프트웨어의 구조와 가독성을 향상시키기 위한 방식의 연속적인 적용이다. 원칙과 패턴은 매 주기를 거치면서 코드 및 코드가 포함하는 설계를 명료하게 유지하려는 시도의 일환으로 적용되며, 크고 중요한 설계에 적용하는 것이 아니다.

모든 시점에서 시스템의 설계를 가능한 한 간단하고, 명료하고, 표현적으로 유지하려는 노력이다.