Make Large Scale Changes Incrementally with Branch By Abstraction by Hubert Shin

원문보기

많은 개발팀들이 버전 컨트롤상의 브랜치를 사용하는 것에 익숙하다. 특히 분산 환경의 버젼 컨트롤 시스템을 갖춰야 하는 경우에는 이러한 현상이 두드러진다. 때문에, 브랜치를 이용한 통합을 반대하는 지속적인 딜리버리(Continuous Delivery)라는 개념은 논란 거리가 될 수 밖에 없다. 지속적인 딜리버리의 정의에 의하면, 브랜치가 있는 상태는 코드가 온전히 통합된 것이 아니다. 하지만, 상식적으로 보통 어플리케이션에 매우 큰 변경이 있을 때 브랜치를 만들어 쓰는 경우가 잦다. 이런 경우 지속적인 딜리버리에서를 수행하려면, 우리는 과거와 같은 브랜치를 개선하기 위해 "추상화된 브랜치(branch by abstraction)" 이라는 테크닉을 쓸 수 있다.

추상화된 브랜치(branch by abstraction) : 어플리케이션의 큰 변화를 만들 때 주 흐름(Mainline)에 점진적으로 반영하는 패턴이다.

Paul Hammant 이 그의 블로그에 제공한 예제는 이 패턴을 이용하여 Hibernate 에서 iBatis로의 전환하는 방법을 보여준다. 그의 예제에서 볼 수 있는 작업과 동일한 내용이, 내가 제품관리자로 일하는 Go(지속적인 통합 및 애자일 릴리즈관리 플랫폼)를 만드는 팀에서도 일어나고 있으며 여기서는 반대로 iBatis에서 Hibernate로의 전환이 진행되고 있다. 우리 팀에서는 이미 일년 이상 위와 같은 방식으로 작업을 진행하였다. 또한 우리 팀에서는 동시에 Velocity 와 JsTemplate기반의 UI를 JRuby on Rails로 전환하는 작업도 진행하고 있다.

이 두 가지 변경(iBatis --> Hibernate, Velocity & JsTemplate --> JRuby on Rails) 다, 천천히 점진적으로 일어나고 있다. 매일, 수차례씩 변경사항을 Mercurial 레파지터리 내 주 흐름에 체크인하며 작업하고 있다. 어떻게 그렇게 할 수 있는지 궁금하지 않는가?

iBatis 에서 Hibernate로의 전환

우리 팀은 다음 두 가지 이유 때문에 iBatis에서 Hibernate로의 전환을 결정했다. 먼저, 이미 많은 수의 SQL을 작성하였고 데이타베이스 스키마를 잘 이용할 수 있는 상황이라 우리는 ORM을 효과적으로 쓸 수 있다고 생각했다. 그리고, Hibernate의 두번째 레벨 캐시(second level cache)는 좋은 성능을 낼 수 있었다.

물론, 우리는 전체 코드베이스를 한번에 옮기는 것을 원하지 않았다. 그래서 DB 호출이 필요한 새로운 기능을 추가 할 때마다, 우리는 iBatis를 쓰던 예전 호출을 Hibernate 를 쓰는 새로운 호출로 전환했다.

Go의 코드들은 표준 레이어 아키텍처(콘트롤러가 차례로 레파지터리를 사용하는 서비스들을 호출하는 것)를 쓰기 때문에 퍼시스턴스 로직을 업데이트 하는 작업이 비교적 수월했다. 왜냐하면 모든 데이터베이스 엑세스 코드는 레파지터리 패턴을 쓰는 레파지터리 클래스들로 캡슐화되어 있어서, iBatis에서 Hibernate로의 전환 시 레파지터리 클래스를 한번에 한 개씩 점진적으로 수정하면 되었다. 다시 말하면, 이 서비스 레이어는 퍼시스턴스 프레임웍에 대해 전혀 알 수 없는 구조였다.

내 동료인 Pavan K S는 추상화된 브랜치를 만드는 것에 대해 다음과 같이 이야기 한다. "추상화된 브랜치 작업의 핵심은 개발자가 예전에 하던 소스 추가 습관을 절대 못하게 하는 훈련에 있다. 이는 다시 말하면 아무리 더 쉽고 빠르더라도 iBatis 쿼리를 추가하지 못하도록 강제화 한다는 것이다. 대신 반드시 Hibernate를 쓰도록 한다. 이것이 일이 진행되게 만드는 유일한 방법이다. 한 가지 이를 강제화 할 수 있는 구체적인 방법을 소개한다면, iBatis 쿼리가 추가되면 빌드가 실패하도록 하는 것이다. "

Velocity와 JsTemplate에서 JRuby on Rails로의 전환

또한 우리는 자바기반 UI들을 JRuby on Rails로 전환하고 싶었다. 왜냐하면 JRuby on Rails 기반 UI가 테스트 코드를 쓰기 더 쉽고 더 빨리 UI를 개발할 수 있도록 해주기 때문이다. 이번에도 변경은 점진적으로 수행했다. 어플리케이션의 새 페이지를 추가할때, 우리는 JRuby on Rails를 쓰도록 했고 UI 하나가 완성될 때마다 나머지 어플리케이션들과 이 새로 작성한 페이지를 링크 시켰다.

또한, 우리는 페이지에 변경이 있을 때마다 또한 위와 같은 전환을 하고 싶었다. 그래서 우리는 페이지에 변경할 요구사항이 있을 때에도 새로운 기술들을 활용하도록 했다. JRuby on Rails를 쓰는 새 버전의 페이지에 대한 작업을 완료하고 나서 나머지 어플리케이션들은 새 버젼의 페이지를 향하도록 URI를 변경하였다. 이 시점에 우리는 과거 버젼의 페이지를 삭제했다. 이러한 방식을 수행하다 보니 현재 Go의 대부분의 UI는 JRuby on Rails를 쓰고 있지만, 일부 소스는 여전히 Java 기술을 쓰고 있다.

하지만 페이지만 본다면 아마 무엇이 JRuby on Rails인지 무엇이 Java 인지 알아채기 어려울 것이다. 예전과 현재 둘 다 같은 스타일링을 썼기 때문이다. 이를 확인하려면 URI를 봐야한다. /go/tab으로 시작하는 URI는 예전 버젼의 Velocity 로 라우팅 한다. 다른 모든 URI는 JRuby on Rails 를 쓰고 있다. 이들은 예전 UI또한 여전히 호출하던 서비스 레이어를 호출하고 있다.
어떻게 추상화된 브랜치가 동작하는가
추상화된 브랜치는 당신의 시스템에 큰 변경을 반영해야 할 때 다음과 같이 점진적으로 수행한다. 
  1. 변경이 필요한 시스템 부분에 추상 레이어를 생성한다. 
  2. 시스템의 다른 부분을 이 추상 레이어를 쓰도록 리팩토링 한다.
  3. 새로운 클래스를 생성하여, 새로운 구현을 한다. 그리고 이미 생성한 추상 레이어가 이전 또는 새로 생긴 클래스를 필요에 따라 위임하도록 한다. 
  4. 이전 버젼 구현 내용을 삭제한다.
  5. 앞의 두 스텝을 반복하며 필요한 때 완성된 시스템을 전달한다. 
  6. 새 구현내용이 온전히 이전 버젼을 대체하면 추상 레이어를 삭제할 수도 있다.
Branch by Abstraction

마틴파울러는 위 단계들에 다양한 문제가 일어날 가능성에 대해 다음과 같이 언급한다 "가장 단순하게 접근할 수 있는 방법은 당신이 전체 추상 레이어를 구현하고, 당신이 쓰는 모든것을 리팩토링 하며, 새로운 구현내용을 만들어 낸 뒤, 스위치를 딱 켜 모든 것이 한번에 동작하도록 하는 것이다. 하지만 이 방법은 여러가지 문제점을 일으킬 가능성이 크다. 아마도 전체 추상 레이어를 구현하지 못하고 기능 일부의 부분집합만 만들었을 수도 있고, 기능을 이관하고 나서 , 잘못된 기능의 덩어리(새것과 헌 것이 동시에 존재하는 상태 때문에)를 쓰게 될 수도 있다. 또는 주상화로의 코드 호출을 이동(shift)시켜 나머지를 움직이기 전에 두가지 방법다 구현했을 수도 있다.

iBatis/Hibernate 예제에서 보면, 추상 레이어는 레파지터리 레이어이다. 이것은 퍼시스턴스 프레임웍이 이용되는 곳의 상세 구현 내용을 숨겨준다. 또한 JRuby on Rails 예제에서는 추상 레이어는 서블릿 엔진이다. 이것은 JRuby on Rails 프레임웍이나 표준 Java 서블릿으로 URI가 매치되면 dispatch 해주는 역할을 한다.

Go는 비교적 작은 프로젝트이다. 여기는 10명 이하의 개발자가 있고 몇 년 정도밖에 안 되었다. 하지만, 위의 원칙은 모든 규모의 프로젝트에 적용될 수 있다. ThoughtWorks 팀에서는 심지어 대규모 분산 개발 환경 프로젝트에서도 성공적으로 이 패턴을 쓸 수 있었다.

사실, 추상화된 브랜치는 개발 프로세스에 보다 많은 작업을 줄 수 있다. 특히 코드기반이 잘 구조화되지 않은 곳에서는 특히 그렇다. 이 방식을 따르자면 열심히 고민하고 조금 천천히 개발하면서 점진적인 변경을 적용해야 한다. 하지만 여전히 이 추가적인 노력은 팀에 도움이 된다. 따라서, 구조를 변경하는 것이 목표라면, 추상화된 브랜치를 사용하는 것은 매우 중요한 고려사항이다.

추상화된 브랜치의 중요한 이점은 코드의 구조를 바꿀 때에도 지속적인 딜리버리가 가능하도록 만든다는데 있다. 이것은 당신의 아키텍처의 변화와 관계 없이 계획된 릴리즈가 가능하다는 의미이다. 때문에, 만약 구조를 바꾸는 것보다 더 우선순위가 높은 새로운 추가기능과 같은 어떤 일이 주어질 때도, 구조 변경을 잠시 멈추고 우선순위가 높은 것부터 먼저 작업할 수도 있다.

추상화된 브랜치를 위해서는 출구 전략을 가지는 것이 중요하다. 추상화된 브랜치를 수행하는 경우 당신이 고생스러운 큰 규모의 변경을 다루지 않아도 된다면, 가장 중요한 이관의 일부가 완료된 경우, 절반 정도만 완료된 상태로도 더 이상 변경을 진행하지 않고 싶어지는 유혹을 받는 경우가 많다. 그러나 두 개 이상의 기술을 동시에 써야 하는 상황은 시스템을 유지보수하기 더 어렵게 만들며, 팀은 여러 기술을 동시에 이해해야 하는 상태가 된다. 이 상태는 때로는 현실적으로 존재할 수 있는 것이지만, 모두가 반드시 알고 있어야 하는 상황이기도 하다.

추상화된 브랜치와 버젼컨트롤 상에서의 브랜치 비교

추상화된 브랜치는 어쩌면 잘못된 이름이다. 왜냐하면 추상화된 브랜치는 대규모 변경이 시스템에 일어날 때 버젼 컨트롤 시스템에 브랜치를 사용하는 것에 대안을 나타내기 때문이다. 많은 팀들은 보통 버젼 컨트롤의 브랜치들을 만들어 커다란 변경을 반영해야 할 사항이 있는 도중에도, 동시에 주흐름에 있는 기능을 개발하거나 버그를 수정한다. 이 과정의 문제는 주흐름으로 코드를 병합할 때, 매우 큰 고통이 따른다. 커다란 변경을 만들 때 들이는 고통과, 주흐름을 위해 버그 수정 따위의 일을 하는 고통을 함께 겪어야 한다.

버젼콘트롤상에 브랜치를 쓴다는 것은 당신의 소스를 주흐름과 합치고 싶을 때, 반드시 병합해야 하는 고통스러운 상황을 피해갈 수 없다는 의미이다. 만약 당신이 기능마다 브랜치를 쓰면, 상황은 더욱 안 좋아진다. 떼문에 일반적으로, 기능 변경이나 큰 기능들의 변경을 반영하기 위해 브랜치들을 쓰는 것은 여러가지 이유로 잘못된 선택이라 할 수 있다. 게다가 이러한 상황은 지속적인 딜리버리와 리팩토링또한 못하게 만든다. 마틴 파울러가 쓴 훌륭한 글들을 한번 읽어보라 "왜 기능 브랜치가 나쁜가", "기능 토글을 대안으로 쓰는 방법"

물론 버젼 콘트롤 상의 모든 브랜치가 나쁘다는 말은 아니다. 당신이 스파이크를 위해 브랜치를 만들고 그 소스코드를 버리는 경우는 매우 괜찮은 예제이다. 작지만 심각한 버그 수정을 위해 릴리즈 브랜치를 만드는 것 또한 필요한 일이다. 그러나 지속적인 디플로이를 연습하는 많은 팀들은 보통 이런 것을 신경쓰지 않는다. 왜냐하면 보통 어떤 문제를 주 흐름에서 고치는 것이 쉽고, 롤백(roll back)보다 롤 포워드(roll forward)가 수월하다. 이런 상황에는 릴리즈간 delta(변경요인) 가 매우 작다.

유일하게 브랜치 사용을 허락할만한 상황은 당신의 코드기반이 진흙속의 큰공 패턴을 쓸 때이다. 이 시나리오 상에서는 추상화 레이어를 만드는 것조차 힘들다. 이것을 하기 위해서는 먼저 반드시 "seam(이음새, 경계선)"(정적 타입의 OO 언어를 쓸 때 인터페이스들의 집합으로 나타나는) 을 먼저 착아야 한다. 이 위에 추상 레이어를 넣을 수 있다. 만약 seam을 찾기 어렵다면, 리팩토링 시리즈들을 이용하여 아예 새로 코드를 만들어낼 수도 있다. 만약 사정이 있어 이것마저 불가능 하다면, 아마도 새 브랜치를 만들어 이것이 가능하게 하는 상태를 만드는 것을 할 수 밖에 없을 것이다. 

다른 패턴들과의 관계 

리팩토링과의 관계

리팩토링은 "관찰할 수 있는 외부의 행동 변화없이 소프트웨어 구조를 보다 이해하기 쉽고 수정하기 쉽게 만들기 위해 내부 구조를 변경하는 것" 으로 정의된다. 이러한 맥락에서, 앞서 언급한 추상화된 브랜치 예제는 또한 리팩토링의 좋은 예이다. 추상화된 브랜치를 통해 연관된 리팩토링 기법으로 효과적으로 프로그래밍을 할 수 있으며, 어플리케이션 아키텍처의 커다란 변경을 이룰 수 있는 방식이다. 당신의 소프트웨어를 언제든 릴리즈 할 수 있게 하는 역량과 리팩토링을 할수 있는 역량은 주흐름에 있는 소프트웨어 개발에 매우 도움이 될 것이다.

기능 토글과의 관계
추상화된 브랜치를 기능 토글과 혼동하는 경우가 종종있다. 두 패턴 다 주 흐름에 시스템의 변경을 점진적으로 수행할 수 있도록 한다. 차이점은 기능 토글은 새로운 기능의 개발을 위한 방식으로 시스템이 동작하는 동안에 사용자가 이 새 기능을 확인할 수 없게 한다. 기능 토글은 때문에 특정 기능이나 기능의 집합이 어플리케이션안에서 보이도록 할 지 말지에 대해 디플로이 시점이나 런타임 시점에 결정해야 한다.

추상화된 브랜치는 큰 변경에 대해 점진적으로 어플리케이션을 변경하는 개발 테크닉이다. 추상화된 브랜치는 기능 토글과 합치는 것도 가능하다. 예를들어 이들을 이용하면 특정 데이터 제어 호출을 위한 특정 셋을 구현하여 iBatis와 Hibernate 사이에서 어떤 것을 이용하게 할 지 런타임 환경에서 결정할 수 있다. 그러나 현실에서는 보통 개발자들에 의해 의존관계 역전 설정을 통해 빌드타임 안에 하드 코딩 되거나 baked 되는 방식으로 결정되곤 한다.

교살(strangler) 어플리케이션과의 관계
교살 어플리케이션 패턴은 전체 시스템(보통 레거시)과 새로운 것을 완전히 대체하려고 할 때 사용하는 패턴이다. 때문에 이것은 추상화된 브랜치보다 상위레벨에서 동작한다. 즉, 시스템의 콤포넌트를 대체하는 식으로 점진적으로 변경한다. SOA를 쓰는 경우 이 둘 사이의 경계선은 최초 보통 애매하다.

이건 그냥 좋은 객체지향 설계가 아닌가?
그렇다. 특히나 코드가 SOLID 원칙을 따르는 경우 이 패턴을 적용하기 더 쉬워진다. 특히 의존관계 역전 원칙과 인터페이스 분리 원칙(ISP)를 따르는 경우 또한 이 패턴 적용은 매우 간단하다. 인터페이스 분리 원칙은 중요하다 왜냐하면 구현을 위해 전환하는데 매우 적절할 수준의 균일 레벨을 제공하기 때문이다. 내 회사 동료인 David Rice는 추상화된 브랜치는 특정 콤포넌트의 구현 내용을 변경할 때에만 실용적이라고 꼬집듯이 Martin Fowler는 다른 구현에 의해 제거될 수 있는 콤포넌트 같은 단위 정도에 걸맞은 방식이라고 이야기 한다.

핑백

덧글

  • 2014/12/18 11:11 # 삭제 답글

    좋은 독서 게시물 및 더 포럼에 게시 기대됩니다!
댓글 입력 영역