2020. 4. 27. 01:20ㆍ[공부] 독서/리팩토링 2판
다음 책을 읽고 정리합니다. http://www.yes24.com/Product/Goods/89649360?scode=032&OzSrank=1
리팩토링
전통적인 소프트웨어 개발 방법에서는 완벽한 설계를 만들고 코드를 짜려하였다. 그러나 완벽한 설계라는 것은 존재하지 않는다. 시간이 흐르면서 코드는 수정되고 시스템의 무결성은 망가진다. 또한 구조도 엉망이 된다. 리팩토링을 한다는 것은 지속적으로 설계를 한다는 의미다.
리팩토링을 통해 시스템을 구축하는 과정에서 설계가 무엇인가를 배우게된다. 그 결과 개발의시작부터 끝가지 프로그램은 우수한 설계를 유지할 수 있다.
리팩토링은 겉으로 드러나는 코드의 기능은 바꾸지 않고 내부 구조를 개선하는 소프트웨어 수정 과정이다. 버그가 생길 가능성을 최소로 줄이며 코드를 정리하는 정제된 방법이다. 리팩토링의 본질은 코드를 예쁘게 꾸미는 것이 아니다. 리팩토링은 개발 기간을 단축시키고자 하는 것이다. 또한기능 개발 속도가 저하 된다고 리팩토링을 하지 않는 다는 것은 틀린 말이다.
재구성
재구성과 리팩토링은 다르다.
재구성은 포괄정인 개념이며 리팩토링은 재구성의 한 종류이다.
리팩토링을 하다 코드가 깨져서 며칠 고생했다 라는 말이 나오면 십중팔구 리팩토링 한 것이 아니다.
리팩토링은 특정한 절차를 따라 코드를 정라한 것이 리팩토링이다.
리팩토링을 진행할 때
리팩토링을 하기 전 리팩토링할 코드를 검사해줄 테스트 코드를 만들어야한다.
리팩토링을 위해선 테스트 코드가 반드시 필요하다.
조금씩 변경하고 매번 테스트를 하는 것이 리팩토링의 핵심이다.
리팩토링이 끝낼 때 마다 커밋한다. 그래야 중간에 문제가 생기더라도 정상 상태로 돌아가기 쉽다.
너무 많이 수정하려다 실수를 저지르면 디버깅하기 어려워지게된다. 결과적으로 작업시간이 늘어난다.
조금씩 수정하여 피드백 주기를 짧게 가져가는 습관이 좋다.
좋은 코드를 가늠하는 기준은 얼마나 수정하기 쉬운가이다.
리팩토링과 성능
리팩토링 기법 중에 반복문을 쪼개라, 함수를 쪼개라와 같은 기법들이 있다. 이때 반복문을 분리하여 반복문이 중복되면 성능이 느려질까 걱정할 수도 있다. 또한 리팩토링 과정중 불필요한 함수 호출이 생겨서 콜스택이 늘어나 성능에 영향을 줄까 걱정할 수도 있다. 하지만 이러한 변화는 실제로 성능에 미치는 영향은 미미할 때가 많다.
경험 많은 프로그래머 조차 코드의 실제 성능을 예측하기는 힘들다. 최근의 똑똑한 컴파일러 들은 캐싱 기법 등으로 무장되어 있어 우리의 직관을 초월하는 결과를 만들어준다. 그러므로 반복문을 중복 분리하는 것을 부담스러워 하지 말라.
물론 때로는 리팩토링이 성능에 큰 영향을 주기도 한다. 그러나 저자는 그러한 경우에도 개의치 않고 리팩토링을 한다고 한다. 잘 다듬어진 코드는 성능 개선 작업이 훨씬 수월하기 때문이다. 리팩토링 과정에서 성능이 크게 떨어졌다면 리팩토링 후 시간을 내어 성능을 개선한다. 리팩토링 코드를 되돌리기도 하지만 대체로 리팩토링된 코드는 성능 개선을 효과적으로 수행하는 경우가 많다.
저자의 조언은 특별한 경우가 아니라면 일단 성능 문제는 무시하라. 리팩토링의 목적은 코드를 수정, 이해하기 쉽게 하기 위함이다. 성능 문제는 성능 최적화 단계에서 다루면 된다. 성능을 개선하기 위해 수정하다 보면 프로그램은 더 어려운 형태로 변하기 쉽고 개발이 더뎌진다.
성능
성능에 대한 흥미로운 사실은 대부분의 프로그램이 전체 코드중 극히 일부에서 대부분의 시간을 허비한다는 사실이다.
성능 문제는 구조를 혁신적으로 개선한다거나 몇개의 중복을 제거해서 해결하는 것이 아니다.
프로파일러로 프로그램을 분석하여 시간과 공간을 많이 잡아먹는 지점을 알아내고 이를 수정하는 것이다.
리팩토링이 잘 되어 있다면 기능 추가가 빨리 끝나서 성능에 집중할 수 있고 성능을 세밀하게 분석할 수 있다.
설계 지구력 가설
저자가 만든 가설이다. 내부 설계에 심혈을 기울이면 소프트 웨어의 지구력이 높아져서 빠르게 개발할 수 있는 상태를 오래 지속할 수 있다.
리팩토링은 언제하는가
리팩토링은 따로 하는게 아니다. 리팩토링은 프로그래밍 과정중에 자연스럽게 녹아들어야 한다. 그래도 기준을 제시하자면
- 3의 법칙 : 중복이 3번 이상 반복된다면 리팩토링하라.
- 보기 싫은 코드가 보이면 리팩토링하라.
- 코드에 악취가 나면 하라.
코드의 악취
- 이상한 이름
마땅한 이름이 떠오르지 않는다면 설계에 근본적인 문제가 있는 것이다. - 코드 중복
- 함수가 지나치게 길다
옛날에는 함수가 길어서 쪼개는 것은 서브 루틴을 호출 비용이 커서 꺼려졌다.
하지만 요즘의 언어는 프로세스 안에서의 함수 호출이 거의 없어졌다.
그러므로 적극적으로 함수를 쪼개야한다.
주석을 달아야 할 만한 부분은 함수를 만들어야한다. - 함수에 매개변수가 지나치게 많이 들어간다.
- 매개변수가 많아지면 그자체로 이해하기 어려워진다.
- 전역 데이터
전역 데이터는 코드 베이스 어디에서 든 건드릴 수 있고 값을 누가 바꿨는지 찾아낼 방법이 없다.
추적이 힘든 것을 방지하기 위해선 변수를 캡슐화 해줘야한다. - 가변 데이터
- 뒤엉킨 변경
하나의 모듈이 서로 다른 이유들로 여러가지 방식으로 인해 변경될 때를 말한다.
단일 책임 원칙을 지키지 않을 경우 나타나는 문제다. - 산탄총 수술
코드를 변경할 때마다 자잘하게 수정하는 클래스가 많을 경우 발생한다. - 기능 편애
어떤 함수가 자기가 속한 함수나 데이터 보다 다른 모듈의 함수, 데이터와 상호 작용하는 경우가 많을경우를 말한다.
함수를 재배치해서 이를 해결할 수 있다. - 데이터 뭉치
- 기본형에 집착
- Switch 문이 반복
- 반복문
반복문을 파이프라인으로 바꾸는 것이 좋다.
또한 같은 조건으로 동작하는 반복문일지라도 문맥이 다른 경우라면 분리할 수 있다면 분리하는 것이 좋다. - 성의없는 요소
- 유연성 메커니즘
모든 상황을 고려하다 보면 유연성 메커니즘이 오히려 변화에 대응하는 능력을 떨어뜨리기도한다. 이런 경우 코드가 지저분해진다. 리팩토링을 활용하면 다르게 접근할 수 있다. 그저 현재까지 파악된 요구사항만을 해결하는 소프트웨어를 구축한다. 그리고 추후에 리팩토링한다. 유연성 메커니즘 보다는 단순한 시스템이 변경하기 쉽다. 이를 점진적 설계, YAGNI (You aren’t going to need it, 애그니, 필요 없을 거다) 등으로 부른다. 아키텍쳐를 고려하지 말라는 의미가 아니다. 리팩토링 없이 점진적 설계는 의미가 없다. - 임시 필드
- 메시지 체인
- 중개자
- 내부자 거래
- 거대한 클래스
- 서로 다른 인터페이스의 대안 클래스
- 데이터 클래스
- 상속 포기
- 주석
주석이 장황하게 달린다면 코드를 잘못 작성했기 때문인 경우가 많다.
CI (Continuos Integration)
기능 브랜치 방식에는 단점이 있다. 독립 브랜치로 작업하는 시간이 길어질 수록 작업 결과를 마스터로 통합하기 힘들어진다. 이러한 상황을 피하기 위해 리베이스와 머지를 수시로 해줘야한다. 머지와 통합은 다르다. 머지는 마스터를 브랜치로 당기는 것이고 통합은 머지한 이후 마스터로 푸쉬까지 하는 것이다. 기능별 브랜치들은 통합 주기를 짧게 가져갸아한다. 2~3일 단위보다 짧아야 좋다. CI 는 짧은 주기로 지속적으로 통합한다는 의미다. CI 에 따르면 모든 팀원이 최소 하루에 한번 마스터와 통합 해야한다. CI 를 완벽하게 따를 수 없더라도 통합 주기는 최대한 짧아야한다. CI 는 리팩토링과 어울린다.
테스트
리팩토링을 위해서는 테스트 코드가 필수적이다. 자가 테스트 코드는 통합 과정에서 발생하는 충돌을 잡는 메커니즘으로 활용할 수도 있다. 그러므로 CI 도 밀접한 연관이 있다. 테스트는 CI / CD 의 핵심이다.
자주 테스트하고 전체 테스트는 하루에 최소 한번은 돌려봐야한다. 모든 public 메소드에 빠짐없이 테스트를 작성하지는 말라. 이러면 의욕이 떨어진다. 테스트는 위험 요인을 줌심으로 작성해야한다. 테스트의 목적은 현재 그리고 미래의 버그를 찾는데 있다.테스트는 테스트 케이스 하나당 하나의 요소만 검증하는 것이 좋다. 너무 많은 요소를 검증하려하면 나머지 검증은 실행해보지도 못하고 테스트가 실패할 수 있다. 그렇게 되면 실패 원인을 파악하는데 유용한 정보를 놓칠 수 있다.
테스트 케이스가 충분한가는 테스트 커버리지가 아니다. 주관적인 것이다. 테스트 코드를 수정, 작성하는데 시간이 오래걸린다면 테스트 케이스를 과하게 작성하고 있는 것은 아닌지 의심하라. 하지만 너무 많은 경우보다 너무 적은 경우가 훨씬 많다.
기본적인 리팩토링 기법
- 함수 추출하기
함수를 쪼개서 추출하는 기준은 다양하다. 예를들어 함수 하나가 한 화면을 넘어가지 않도록하기, 재사용성을 기준으로도 함수를 나누기 등이 있다. 저자는 목적과 구현의 분리라는 기준으로 하는 것이 좋다 말한다. 함수는 무조건 짧은게 좋고 컴파일러가 캐싱할 수 있기 때문에 최적화에 유리할 때가 많다.
짧은 함수의 이점은 함수 이름을 잘 지어야만 발휘된다. 함수는 어떻게 동작하는지가 아니라 무엇을 하는지가 들어나야한다. 하나의 함수가 여러개의 값을 반환해야하는 경우라면 함수를 여러개로 만들어야 하는 것은 아닌지를 생각하라. - 변수 추출하기
- 함수 인라인하기
- 변수 인라인하기
- 변수 이름 바꾸기
- 함수 선언 바꾸기
함수가 이상한 경우 함수의 선언을 바꿔야한다. 메소드 이름이 이상하거나 매개변수가 불필요하게 많거나 부족하거나. 이럴경우는 선언을 바꿔줘야한다. - 변수 캡슐화
접근자만 캡슐화하는 것이 아니라. 가능하면 값도 캡슐화하라. 즉 매개변수 값, 반환 값을 사용할 때 원본 데이터에 영향이 가지 않도록 관리할 수 있으면 그렇게 하는 것이 좋다. 가능하면 복제본을 넘겨주거나 넘겨 받는 것이 좋다. - 매개변수 객체 만들기
- 여러 함수를 클래스로 묶기
- 여러 함수를 변환 함수로 묶기
- 단계 쪼개기
모듈이 잘 분리되어있다면, 다른 모듈의 상세 내용은 전혀 기억하지 못해도 원하는 대로 수정을 마칠 수 있다.
캡슐화
모듈을 분리하는 가장 중요한 기준은 시스템에서 각 모듈이 자신을 제외한 다른 부분에 드러내면 안되는 비밀을 얼마나 잘 숨기고 있느냐이다. 얼마전까지 괜찮은 캡슐화가 나중에는 어색해질 수 있다. 리팩토링은 결코 미안하다고 말하지 않는다.
- 레코드를 캡슐화하라.
레코드 (plain object, dictionary, hash …) 를 클래스로 캡슐화 하라. - 컬렉션을 캡슐화하라.
컬렉션 원소를 getter / setter 로 관리하면 클라이언트가 실수로 컬렉션을 바꿀 가능성이 있다. - 기본형을 객체로 바꿔라.
Primitive 타입에 집착할 필요가 없다. 객체를 사용하면 더 깨끗해질 수가 있다. - 임시변수를 질의 변수로 바꿔라.
여러 곳에서 똑같은 방식으로 계산되는 변수를 발견하면 함수로 바꿀 수 있을지 보라. - 클래스 추출하기
- 일부 데이터와 메소드가 따로 묶일 수 있다면 분리하라.
- 클래스 인라인하기
- 중개자 사용하기 (프록시, 위임, Wrapper)
- 중개자 제거하기
- 알고리즘 교체하기
디미터의 법칙
디미터의 법칙은 최소 지식의 원칙이라고도 불린다.
내부 정보를 가능한 숨기고 밀접한 모듈과 상호 작용하여 결합도를 낮추자는 원칙이다.
이 과정에서 위임, Wrapper 메소드가 너무 늘어날 수 있다.
저자의 조언
- 프로그램이 새로운 기능을 추가하기 편한 구조가 아니라면 먼저 기능을 추가하기 쉬운 형태로 리팩토링을 하고 나서 기능을 추가한다. 뛰어난 개발자는 새 기능을 추가하는 가장 빠른 방법이 기능을 추가하기 쉽도록 코드를 수정하는 것임을 알고 있다.
- 기능 추가 커밋과 리팩토링 커밋을 분리하자는 조언들이 있지만 이 견해에 대해서 저자는 동의하지 않는다. 왜냐하면 리팩토링은 기능 추가와 밀접하게 엮인 경우가 많기 때문이다. 굳이 나누는 것은 시간 낭비일 수 있다.
- 코드 리뷰는 좋다. 코드 리뷰를 통해 다른 사람의 관점을 파악할 수 있고 읽기 좋은 코드에 더 다가갈 수 있다. 또한 다른 사람의 아이디어를 얻을 수도 있다는 장점이 있다. 다만 코드 리뷰는 PR 로 하는 것 보다. 페어 프로그래밍을 하는 것이 좋다.
- 굳이 수정할 필요가 없다면 리팩토링을 하지마라. 외부에서 내부 구현을 알 필요가 없고 API 호출하듯 사용하기만 해도 되는 것이라면 굳이 리팩토링하지 마라. 또한 처음부터 새로 작성하는 게 쉬울 경우도 굳이 리팩토링하지마라.
- 코드에 소유권을 둬서 관리하는 회사들이 있는데, 그렇게 하지마라. 프로그래머마다 각자 책임지는 영역이 있을 수 있지만 다른 사람이 코드를 수정하지 못하도록 막으라는 것은 아니다.
저자의 습관
- 함수의 반환 값으로 사용하는 변수는 result 라는 이름을 쓴다.
- 함수가 데이터의 형태를 바꾼다면 함수에 transform 이라는 용어를 사용하고 데이터에 부가 정보를 붙인다면 enrich 라는 용어를 사용한다.
- 변수 인라인과 함수 인라인에 지역변수를 제거하는 것이 좋다. 불필요한 지역변수를 제거하는 것은 유효 범위를 신경 써야 할 대상을 줄여준다.
- 가변 데이터는 금방 상한다. 가급적이면 데이터를 최대한 불변으로 취급해야한다.
- 인터페이스를 지원하지 않는 언어일 경우 슈퍼 클래스를 선언하고 메소드 실행 부분에 에러를 던지는 코드를 넣어둬서 유사하게 구현할 수 있다.
사설) 자바스크립트에서 타입 데이터를 다형성으로 처리하기.
챕터 1에 자바스크립트에서 타입 데이터를 다형성으로 처리하는 예시가 나온다. 이는 챕터 12에 더 상세한 내용이 준비되어있다. 알다시피 일반적으로 객체 지향 언어에서는 타입을 가진 객체로 인해 조건부 로직이 생겼을 때 서브 타이핑을과 다형성을 활용할 수 있다. 하지만 자바스크립트의 경우 타입을 가진 이벤트 데이터가 plain object 인 경우가 많다. 이로인해 이벤트를 서브타이핑하기 힘들고 이벤트에 다형성 처리 로직을 넣는게 힘들다. 이 경우 타입에 따라 알맞는 서브 클래스를 만들어 내려주는 팩토리를 만들 수 있다. 그리고 팩토리가 내려준 서브 클래스에서 다형성 로직을 대신 처리해주게 할 수 있다. (이를 492p 에서는 '간접 상속으로 처리'한다고 말하는 듯 싶다.)
다만 이 경우 객체 지향적인가? 라고 묻는다면 그렇지 않은 코드인 것 같다. 객체 지향에서는 객체에게 물어보는게 아니라 객체에게 시켜야하기 때문이다. 반면 좋은 코드인가? 라고 물어본다면 그렇다 라고 대답할 수 있을 것 같다.
'[공부] 독서 > 리팩토링 2판' 카테고리의 다른 글
리팩토링 2판 - 01 (0) | 2020.04.30 |
---|