이번 장에서 다룰 내용
코드 추가에 대한 두려움의 증상
코드 추가에 대한 두려움 극복
코드 중복의 장단점 이해
이전 버전과의 호환성 보장
기능 토글로 위험 낮추기
코드를 추가하는 것이 수정하는 것보다 안전하다는 것을 인식하고, 무엇보다도 코드 복제 또는 확장을 통해 이 사실을 활용하는 방법에 대해 논의한다.
'고통스러울수록 더 시도하라!' 뭔가 두렵다면 더 이상 두렵지 않을 때까지 더 시도하라.
실패에 대한 두려움이 생산성을 가로막는다.
두려움은 사람들로 하여금 무언가를 시도하기 전에 만드는 방법에 대해 토론하거나 설계해야 한다는 강박이 들게한다.
잘못된 것을 만드는 것에 대한 두려움 > 제대로 못 만드는 것에 대한 두려움
=> 위험신호!
스파이크부터 시작하는 것이 이 문제를 극복하는 데 도움이 된다.
스파이크 형태를 취하며 탐색으로 시작한다. 스파이크 동안 생성된 코드는 메인으로 들어가지 않기 때문에 결함이 있는지 여부는 중요하지 않다. 따라서 두려움은 사라진다.
스파이크는 자신감과 함께 첫 번째 실제 버전을 더 좋게 만드는 데 사용할 수 있는 지식을 제공한다.
이햬관계자는 제품이 코드나 기능이 아니라 지식이라는 것을 인지해야한다.
스파이크의 장점을 유지하려면..
코드가 두려워 지는 또 다른 증상은 개발 도구와 파이프라인이 실제 프로덕션 코드보다 훨씬 더 정교한 경우이다.
어떤 팀은 첫 번째 비즈니스 로직을 작성하기도 전에 테스트 환경, 혁신적인 분기 전략 및 저장소 구조, 기능을 전환하는 시스템, 프런트엔드 프레임워크, 자동화된 빌드 및 배포를 설정하는 데 막대한 시간을 소비하기도 한다.
이런 도구는 위험이나 낭비를 줄이는 데 사용해야한다. 그것들에 너~무 많은 시간을 소비하는 것은 낭비나 위험에 대한 두려움이 산출물을 인도하려는 욕구보다 더 크다는 것을 암시한다. 그 과정에서 코드를 생성하지 않으면 비용을 줄일 수 없고 가치도 없다.
솔루션
진 킴(Gene Kim)이 공동 집필한 <<데브옵스 핸드북>> 에서 저자는 지원도구의 유지보수 및 개발과 같은 비기능 요구사항에 개발자 시간의 20%를 할당할 것을 권장한다.
파이브라인스오브코드 저자가 생각하는 가장 좋은 솔루션은
이해관계자의 요청에 의해 발생되지 않는 모든 작업은(비티켓 작업) 금요일에 몰아서 하는 것이다.
금요일에 개발자들은 실험과 주요 리팩터링을 수행하거나 개발작업을 자동화하여 품질을 개선하거나 낭비를 줄였다.
완벽한 코드는 존재한다고 믿지 않는다.
코드를 좀 더 효율적으로 만들려면 스킬과 프로파일링이 필요하다.
사용하기 쉽게 하려면 테스트와 실험이 필요하다.
쉽게 확장하려면 리팩터링과 선견 지명이 필요하다.
안정적인 코드를 만들려면 테스트나 정확한 데이터 타입의 사용이 필요하다.
이 모든 것은 시간이 걸린다.
코드를 작성할 떄 고려해야할 지표가 너무 많고 한 번에 모든 지표를 최적화할 수 없다는 점을 감안할때 가장 중요한것은 무엇일까?
바로 개발자의 삶에 맞춘 최적화이다. 즉, 일을 받는 순간부터 작업을 시작하기까지의 시간을 최대한 짧게 만드는 것이다. 그렇게 하면 코딩과 같은 개발자가 좋아하는 것을 하는 데 더 많은 시간을 보낼 수 있다.
그리고 테스트, 테스터,이해관계자 및 사용자로부터 피드백을 받을 때까지 실습을 최대화하고 피드백까지의 시간을 최소화할 수 있다는 추가 장점이 있다. 피드백 루프가 짧으면 피드백을 사용해서 개선 노력을 유도할 수 있기 떄문에 품질이 향상되는 것으로 알려져 있다.
코드 복제는 코드의 분기를 장려하거나 억제하는 방법이지만 고려해야할 두가지 중요한 특성이 있다.
첫째, 코드를 공유하면 코드가 사용되는 모든 위치에 영향을 미치기 쉽다. 이것은 동작에 대한 전역적인 변경을 신속하게 할 수 있음을 의미한다. 하지만 호출측의 한 부분에 대해서만 동작을 변경하는 것은 간단하지 않다. 반면 코드를 복사해서 동작을 공유하면 각 호출이 분리되어 호출하는 측에 따라 변경하기 쉽다.
반면, 수정이 필요할 경우 모든 위치에서 수정해야 해서 이 동작이 전역적인 영향을 주기 어렵다. 코드를 공유하면 전역적인 동작 변경 속도가 증가하고, 코드를 복사하면 지역적인 동작 변경 속도가 증가한다. -> 잘 판단해야할듯
둘째, 전역적인 동작 변경 속도가 높다는 것은 코드의 여러 다른 위치에 동시에 영향을 줄 수 있음을 의미한다.
취약성은 코드상으로 관련이 없어 보이는 곳에서 손상을 일으키는 시스템 변경 이다.
공유 함수의 각 호출 측에는 서로 다른 지역적인 불변속성이 있다고 생각할 수 있다. 공유 코드를 변경할 때마다 이런 불변속성은 공유 코드에 대해 지역적인 것이 아니므로 깨질 위험이 있다. 따라서 공유는 시스템의 취약성을 증가시킨다.
전역적인 동작에 대한 변경 속도를 높이는 것은 매력적이다. 하지만 시스템 취약성의 증가는 공유 코드에 좋지 않은 변경을 도입함으로써 전역적인 피해를 초래할 위험이 있다.
이런 단점은 테스트, 증명 또는 모니터링의 필요성을 증가시킨다.
복사된 코드는 완전히 분리되므로 한 곳을 위해 다른 곳을 손상시킬 위험이 없어 실험과 변경이 더 안전하다.
스파이크 동안 가능한 많은 복제를 권장한다. 가설을 테스트하는 빠른 방법이다.
복사된 소스를 통합하는 것이 맞는지 확인한다.
이것을 원본과 결합해야하는가 복사본이 바뀌면 원본이 바뀌어야하는가 팀이 통합된 코드를 가지고 있는가. 같은 질문에 답이 아니요일 경우 코드는 별도로 유지해야한다.
코드를 추가하는 또 다른 방법은 확장성을 이용하는 것이다. 일부 코드가 변경에 수용적이어야 한다는 것을 안다면 확장 가능하게 만들 수 있다.
이는 별도의 클래스로 변형을 만든다는 것을 의미한다.
이 경우 새로운 변형을 추가하는 것은 다른 클래스를 추가하는 것만큼 간단할 수 있다.
도메인의 특성이 규칙적이면 시간이 지남에 따라 더 많은 변형을 수용할 수 있다.
변형이 발생하는 지점은 코드를 더 복잡하게 만든다. 코드 흐름을 이해하는 것이 더 어렵기 때문에 나중에 수정하기가 더 어려울 수 있다. 모든 것을 확장 가능하게 만드는 것은 코드를 복잡하게 만들어 낭비이다.
우발적 복잡성: 도메인에 관련되지 않은 복잡성
본질적 복잡성: 코드는 실제 세계를 나타내므로 도메인으로 부터 일부 복잡성이 상속된다.
우발적 복잡성을 제한하기 위해 필요할 때까지 이러한 변형 지점의 도입을 연기해야한다. 이 책의 전반에 걸쳐 변형이 있는 곳마다 아래 3단계의 절차를 따랐다.
1. 코드를 복사한다.
2. 복사본을 가지고 작업해서 적용한다.
3. 합리적이라고 판단될 경우 원본과 통합한다.
이 방법은 다른 코드와 분리되어 있기 때문에 코드로 작업할 때 많은 자유를 가진다. 작업이 끝나면 쉽게 통합해서 구조를 드러낸다.
확작-수축 패턴은 일반적으로 데이터베이스에 주요 변경 사항을 안전하게 도입하는 데 사용한다.
1. 확장 단계에서 새로운 기능을 추가한다. 추가만 하기 때문에 안전하지만 이제 유지보수해야 할 동일한 동작에 대한 코드가 두 개 존재한다.
2. 호출자를 새로운 기능으로 천천히 이동시키면서 마이그레이션한다. 가장 오래 걸리는 단계이다.
3. 모든 호출자의 변경이 완료되면 수축단계를 수행해서 원래 버전의 코드를 삭제한다.
공개 인터페이스나 API를 통해 기능을 외부에 노출시킬 때가 많다. 사람들이 그런 코드에 의존하면 코드를 갱신할 때 의도하지 않은 부작용으로 그들을 보호할 책임이있다. 표준 솔루션은 버전관리이다. 코드를 버전화 할 떄는 호출자에게 변경에 대한 두려움 없이 기존 버전을 계속 사용할 수 있는 옵션을 제공한다.
호출자들에게 최대한의 안전을 제공하고자 한다면 코드의 생애 동안 이전 버전과의 호환성을 유지해야한다.
이것은 변경할 때마다 수정이 아닌, 공개 인터페이스의 새로운 메서드, API의 새로운 접점이나 이벤트 기반 시스템의 새로운 이벤트를 도입한다는것을 의미한다.
원래의 메서드는 기능을 유지한다.
이런방식으로 만들려면..
1. 변경하려는 기존 엔드포인트를 복제하다.
2. 영향을 미치지 않는다는 사실을 이용해 안전하게 변경 사항을 구현한다.
3. 원래 엔드포인트에 대한 코드로 통합한다.
이것은 1.0 버전이 완벽하지 않기에 우발적 복잡성을 추가시킨다. 이런 복잡성을 없애기 위해 이전 버전을 폐기하고 새로운 버전을 사용하도록 튜토리얼을 갱신하고 변경사항을 알린다.
모니터링을 통해 원본 버전 사용을 추적하고 원본버전을 사용하지 않으면 안전하게 제거한다.
진입점의 이름에 직접 버전을 표시하는것이 좋다.
지속적인 통합은 오류가 줄고 병합충돌에 대한 두려움을 줄일 수 있다 하지만
코드가 준비되지 않았다면? 또는 사용자가 새로운 기능을 사용할 준비가 되지 않았다면 어떻게 해야할까?
아무도 모르게 배포할 수도 있다. 그것은 if(false)블록으로 감싸는 것이다. 두려움없이 원하는 코드를 포함시킬 수 있다. 하지만 최소 요구사항은 컴파일이다.
이것이 기능 토글(켜기/끄기)의 이면에 있는 개념이다.
//새로운 클래스
class FeatureToggle {
}
//새로운 클래스
class FeatureToggle {
static feature() {return false}; // 새로운 기능 플래그
}
//새로운 클래스
class Context {
foo() {
if(FeatureToggle.featureA()){
}else{
code(); // 원래코드, 변경되지 않음
}
}
}
//새로운 클래스
class Context {
foo() {
if(FeatureToggle.featureA()){
code();
}else{
code(); // 원래코드, 변경되지 않음
}
}
}
class FeatureToggle {
static featureA() {
return Env.isSet("featureA"); //환경 변수를 이용한 기능 플래그
}
}
이제 로컬 시스템에서 변수를 설정해서 테스트할 수 있지만 여전히 다른 사람들에게는 보이지 않는다. 코드를 안전하게 배포할 수 있으며 고객이 준비되면 프로덕션 환경에서 환경 변수를 간단히 설정해서 코드를 실행할 수 있다. 이런 방식으로 작업하면 원하는 만큼 자주 통합 및 배포를 할 수 있다.
단점
1. 이 절차를 잊어버리거나 잘못 수행하면 의도치 않게 무언가를 프로덕션에 넣을 수 있음.
2. 기능 토글을 생성한 작업을 마칠 때 마다 토글을 제거하가 위한 작업을 예약해야한다. (6주후) 현재 프로덕션에서 기능이 켜져 있으면 else 부분을 제거한다. 그러지 않으면 if 를 제거한다. 기능 플래그는 코드베이스를 오염시키고 치명적인 오류를 일으킬 수 있어 오랫동안 유지해서는 안된다.
이 두가지 단점이 해결되면 모든 항목에서 토글이 올바르게 수행되고 있다는 확신을 갖게 되어 정기적으로 제거할 수 있다.
첫 번째 단계는 데이터베이스로 이동하고 운영에서 항목을 켜거나 끌 수 있도록 작은 UI를 만든다.
또한 단계적 롤-아웃을 구축할 수도 있다. 처음에는 10%의 사용자만 새로운 기능을 보고 제대로 동작하는지 확인한 다음 사용자 수를 점차 증가시킨다.
아이디어를 더 발전시켜서 '사용자가 뭔가를 구매했는가?' 와 같은 지표에 토글을 결합할 수 있다.
더 많은 사용자가 뭔가를 구매하면 빠르게 출시할 수 있다. (A/B 테스트)
여기서 사람이 할 일은 플래그가 켜져 있는지 꺼져 있는지 확인한 후 실제 삭제를 수행하는것이다.
'기능 토글이 if문에서 else를 사용하지 말 것 규칙을 위반하는 것이 아닌가?'
하는 의문이 생긴다.
이런 경우 코드를 전달하기 전에 기능플래그 내부의 부울에 클래스로 타입 코드 대체 패턴을 사용한다. true또는 false를 반환하는 대신 NewA 또는 OldA같은 것을 반환한다.
// 기능 토글
class FeatureToggle {
static featureA(){
return Env.isSet("featureA");
}
}
class ContextA{
foo() {
if(FeatureToggle.featureA()){
aCodeV2();
}
else{
aCodeV1();
}
}
}
class ContextB{
foo() {
if(FeatureToggle.featureA()){
bCodeV2();
}
else{
bCodeV1();
}
}
}
// 추상화에 의한 분기
class FeatureToggle {
static featureA() {
return Env.isSet("featureA")
? new Version2()
: new Version1();
}
}
class ContextA {
foo() {
FeatureToggle.featureA().aCode();
}
}
class ContextB {
bar() {
FeatureToggle.featureA().bCode();
}
}
interface FeatureA {
aCode(): void;
bCode(): void;
}
class Version1 implements FeatureA {
aCode() { aCodeV1(); }
bCode() { bCodeV1(); }
}
class Version2 implements FeatureA {
aCode() { aCodeV2(); }
bCode() { bCodeV2(); }
}
이 후 기능 토글을 제거할 떄가 되면 다음을 수행한다.
// 변경 전
class Version1 implements FeatureA {
aCode() { aCodeV1(); }
bCode() { bCodeV1(); }
}
class Version2 implements FeatureA {
aCode() { aCodeV2(); }
bCode() { bCodeV2(); }
}
// 변경 후
class Version2 implements FeatureA {
aCode() { aCodeV2(); }
bCode() { bCodeV2(); }
}
구현체가 하나뿐인 인터페이스를 만들지 말 것 규칙에 따라 인터페이스도 삭제한다.
마지막으로 나머지 클래스의 메서드와 기능 플래그를 인라인화한다.
// 변경 전
class ContextA {
foo() {
FeatureToggle.featureA().aCode();
}
}
class ContextB {
bar() {
FeatureToggle.featureA().bCode();
}
}
class Version2 implements FeatureA {
aCode() { aCodeV2(); }
bCode() { bCodeV2(); }
}
// 변경 후
class ContextA {
foo() {
aCodeV2()
}
}
class ContextB {
bar() {
bCodeV2();
}
}
// FeatureA 삭제
class Version2 implements FeatureA {
aCode() { aCodeV2(); }
bCode() { bCodeV2(); }
}
최종적으로 기능 토글의 흔적이 없는 다음과 같은 코드가 남는다.
class FeatureToggle{
}
class ContextA {
foo() {
aCodeV2();
}
}
class ContextB {
bar() {
bCodeV2();
}
}