리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

리팩터링 적용 방법을 아는 것과 제때 적용할 아는 것은 다르다.

리팩터링을 언제 시작하고 언제 그만할지 판단하는 일은 리팩터링 작동 원리를 아는 못지않게 중요하다

다만 이런 기준을 제기할 수는 없다. 프로젝트마다 다들 것이기 때문이다.

장에선 리팩터링 대상을 찾는 방법을 알려준다.

 

3.1 기이한 이름

코드는 단순하고 명료한 것이 최선이다.

누가 이름만 보고도 무슨 일을 하고, 어떻게 사용해야 하는지 있어야 좋은 이름이다.

 

이름은 IDE 쉽게 바꿀 있으므로 좋은 이름이 생각나면 주저하지 말고 바꾸자

 

이름 바꾸는 것은 이름을 다르게 표현하는 것이 아니다. 적절한 이름이 떠오르지 않는다면 설계에 근본적인 문제가 있을 가능성이 높다.

 

이름을 정리하면 코드가 간결해져 문맥 파악에 도움된다.

 

3.2 중복 코드

똑같은 구조 코드가 여러 곳에 반복된다면, 코드들의 차이점이 없는지 주의깊게 봐야한다.

또한 하나가 변경사항이 생기면 다른 모든 코드도 살펴봐야한다.

 

예시

클래스에서 메서드가 동일한 표현식을 쓴다.

이땐 함수 추출하기로 메서드가 추출된 메서드를 호출하게 바꾼다.

 

메서드 코드가 비슷한 경우, 문장 슬라이드하기로 비슷한 부분을 곳에 모아 함수 추출하기를 적용할 있는지 살펴본다.

 

같은 부모로부터 파생된 서브 클래스들에 코드 중복이 있다면, 메서드 올리기를 적용해 부모로 옮긴다.

 

3.3 함수

오랜 기간 활용되는 프로그램들은 짧은 함수로 구성된 경우가 많다.

짧은 함수는 코드가 끝없이 위임하는 방식으로 작성된다.

함수를 짧게 구성할 , 코드를 이해하고 공유하고 선택하기 쉬워진다.

 

예전 언어는 호출 비용이 커서 짧은 함수를 피했다.

요즘 언어는 프로세스 안에서의 함수 호출 비용이 거의 없다.

 

짧은 함수로 구성된 코드를 이해하기 쉽게 만드려면 좋은 이름을 지어야 한다.

좋은 이름을 가진 함수는 본문을 필요가 없다.

이러기 위해서 적극적으로 함수를 쪼개야 한다.

주석을 달아야 코드는 함수로 만든다.

원래 코드보다 길어지더라도 함수로 만든다.

 

함수 이름을 지을 , 동작방식이 아닌 함수가 무엇을 하는지에 대한 '의도' 표현해야한다.

 

함수를 짧게 만드는 작업은 거의 합수 추출하기로 한다.

 

매개변수와 임시 변수가 많으면 함수 추출하기 힘들다. 이런 상황에서 함수 추출 똑같이 추출된 함수로 매개변수가 많아진다.

 

임시 변수를 질의 함수로 바꾸기로 임시 변수 수를 줄일 있다.

매개변수 객체 만들기와 객체 통째로 넘기기로 매개변수 수를 줄일 있다.

 

추출할 코드를 찾는 방법으로 주석 살펴보기가 있다. 보통 주석은 이해하기 난해한 곳에 붙어있기 때문이다.

 

조건문과 반복문도 추출 대상 후보다.

조건문은 조건문 분해하기를 적용한다.

switch문은 case 마다 함수 추출하기를 적용한다.

같은 조건을 기준으로 나뉘는 switch문은 조건부 로직을 다형성으로 바꾸기를 적용한다.

 

반복문도 코드를 추출해 함수를 만든다. 이때 함수에 적합한 이름이 떠오르지 않는다면, 함수는 가지 이상 작업이 섞여 있을 가능성이 있다. 이땐 반복문 쪼개기를 적용해 작업을 분리한다.

 

3.4 매개변수 목록

매개변수 목록이 길어지면 자체로 이해하기 어려워진다.

 

다른 매개변수에서 값을 얻어올 있는 매개변수는 매개변수를 질의 함수로 바꾸기로 제거할 있다.

사용 중인 데이터 구조에서 값들을 뽑아 각각 별개 매개변수로 전달하는 코드는 객체 통째로 넘기기를 적용해서 원본 데이터 구조를 그대로 넘긴다.

항상 함께 전달되는 매개변수들은 매개변수 객체 만들기로 하나로 묶어버린다.

함수 동작방식을 정하는 플래그 매개변수는 플래그 인수 제거하기로 없앤다.

 

클래스는 매개변수 목록을 줄이는 효과적인 수단이다.

여러 함수 특정 매개변수들의 값을 공통으로 사용할 여러 함수를 클래스로 묶기를 적용해 공통 값들을 클래스의 필드로 정의한다.

 

3.5 전역 데이터

가장 악취가 지독한 축에 속한다.

 

전역 데이터의 문제는 코드 베이스 어디에서든 건드릴 있기 때문에 누가 값을 바꿨는지 찾아낼 메커니즘이 없다.

전역 데이터의 대표적인 형태는 전역 변수지만 클래스 변수와 싱글톤도 같은 문제를 공유한다.

 

이를 방지하기 위해 변수 캡슐화하기를 적용한다.

데이터를 함수로 감싸는 것만으로도 데이터 수정하는 부분을 쉽게 찾을 있고, 접근 제한을 있다. 나아가 접근자 함수들을 클래스나 모듈에 넣고, 안에서만 사용할 있도록 접근 범위를 최소로 제한할 있다.

 

3.6 가변 데이터

코드의 다른 곳에서 다른 값을 기대한다는 사실을 모르고 수정해버리면 오작동하게 된다.

문제가 드문 조건에서만 발생한다면 원인은 찾기 힘들다.

이런 문제로 함수형 프로그래밍에서는 데이터는 변하지 않고, 데이터 변경 복사본을 사용하여 반환한다.

 

데이터 수정에 따른 위험을 줄이는 방법

변수 캡슐화하기를 적용해 함수를 통해서만 값을 수정하도록 감시

하나의 변수 용도가 다른 값들을 저장하느라 값을 갱신한다면 변수 쪼개기를 이용하여 용도별로 독립 변수에 저장하여 갱신 발생 여지를 없앤다. 그리고 갱신로직은 다른 코드와 떨어트린다.

API 만들 때는 질의 함수와 변경 함수 분리하기를 적용해 필요한 경우에만 사이드이펙트가 있는 함수를 호출하도록 한다.

가능한 세터 제거하기

 

값을 다른 곳에서 설정할 있는 가변 데이터는 특히 위험하다. 경우 파생 변수를 질의 함수로 바꾸기를 적용한다.

 

유효범위가 좁다면 가변 데이터라도 문제를 일으키지 않는다. 하지만 나중에 유효범위가 넓어질 있기 때문에 여러 함수를 클래스로 묶기나 여러 함수를 변환 함수로 묶기를 활용해 변수를 갱신하는 코드들의 유효범위를 제한한다.

구조체처럼 내부 필드에 데이터를 담고 있는 변수는 참조를 값으로 바꾸기를 적용하여, 내부 필드를 직접 수정하지 말고 구조체를 통째로 교체하는 것이 좋다.

 

 

3.7 뒤엉킨 변경

개발자는 코드를 수정할 시스템에서 고쳐야 군데를 찾아서 부분만을 수정하길 바란다. 이것이 불가능하다면 뒤엉킨 변경과 산탄총 수술 하나이다.

 

뒤엉킨 변경은 단일 책임 원칙이 제대로 지켜지지 않을 나타난다.

하나의 모듈이 서로 다른 이유들로 인해 여러 가지 방식으로 변경되는 일이 많을 발생한다.

예시로 DB추가 함수 개를 바꿔야 한다면 뒤엉킨 변경이 발생한 것이다.

 

순차적으로 실행되는게 자연스러운 맥락이라면, 다음 맥락에 필요한 데이터를 특정한 데이터 구조에 담아 전달하게 하는 식으로 단계 분리한다.

전체 처리 과정 곳곳에서 각기 다른 맥락의 함수를 호출하는 빈도가 높다면, 맥락에 해당하는 적당한 모듈들을 만들어서 관련 함수들을 모은다.

 

 

 

3.8 산탄총 수술

뒤엉킨 변경과 비슷하면서도 정반대

코드가 변경할 때마다 자잘하게 수정해야 하는 클래스가 많을 ,

변경할 부분이 퍼져 있다면, 수정을 못할 확률이 증가한다.

 

이럴 변경 대상들을 함수 옮기기와 필드 옮기기로 모두 모듈에 묶어두면 좋다.

비슷한 데이터를 다루는 함수가 많으면 여러 함수를 클래스로 묶기를 적용한다

데이터 구조를 변환하거나 보강하는 함수들에는 여러 함수를 변환 함수로 묶기를 적용한다.

 

어설프게 분리된 로직을 함수 인라인하기나 클래스 인라인하기 같은 인라인 리팩터링으로 하나로 함치는 것도 하나의 방법이다.

메서드나 클래스가 비대해지지만, 인라인류는 나중에 추출하기 리팩터링이 용이해진다.

 

3.9 기능 편애

프로그램 모듈화할 코드를 여러 영역으로 나눈 같은 영역 내에서 상호작용은 늘리고, 영역 사이 상호작용은 줄이는 주력한다.

 

기능편애는 어떤 함수가 자기가 속한 모듈의 함수나 데이터보다 다른 모듈의 함수나 데이터와 상호작용 일이 많을 풍기는 냄새다.

 

대게 함수 옮기기나 함수 추출하기로 옮기면 된다.

함수가 사용하는 모듈이 다양해 어디로 옮길지 명확하지 않을 때는 가장 많은 데이터를 포함한 모듈로 옮긴다. 그리고 함수 추출하기로 함수를 여러 조각으로 나눈 적합한 모듈로 옮긴다.

 

전략 패턴과 방문자 패턴은 뒤엉킨 변경 냄새를 없앨 활용하는 패턴이다.

함께 변경할 대상을 한데 모은다.

 

 

3.10 데이터 뭉치

데이터 항목이 여러 곳에서 함상 함께 뭉쳐 다니는 것을 말한다.

 

경우 클래스 추출하기로 하나의 객체로 묶는다. 그리고 매개변수 객체 만들기나 객체 통째로 넘기기로 매개변수 수를 줄인다.

매개변수로 넘긴 클래스가 함수 내에서 데이터를 전부 사용하지 않아도 된다. 중요한 것은 전보다 함수 선언부가 깔끔해진다는

 

데이터 뭉치인지 판별하는 방법은 데이터 뭉치 하나만 지워봤을 나머지 데이터만으로는 의미가 없다면, 데이터 뭉치이다.

 

3.11 기본형 집착

자신에게 주어진 문제에 맞는 기초 타입을 직접 정의하기를 꺼리는 것을 말한다.

주로 문자열을 다룰 흔히 나타난다.

전화번호를 예를 들면, 단순히 문자열로만 표현하기엔 부족하다. 사용자에게 보여줄 일관된 형식으로 출력해주는 기능이라도 있어야한다.

 

기본형을 객체로 바꾸기를 적용해 의미있는 자료형으로 변경하는게 좋다.

 

3.12 반복되는 switch

중복된 switch문이 문제가 되는 이유는 조건절을 하나 추가할 때마다 다른 switch문들도 모두 찾아서 함께 수정해야 하기 때문이다. 이럴 다형성은 반복된 switch문의 문제를 효과적으로 해결해준다.

 

3.13 반복문

과거 언어들은 반복문에 대안을 제시하지 못했다. 현재 언어들은 일급 함수를 지원하는 경우가 많아 반복문을 파이프라인으로 바꾸기를 적용해 반복문을 제거할 있다.

 

3.14 성의없는 요소

프로그래밍 언어가 제공하는 함수, 클래스, 인터페이스 코드 구조를 잡는데 활용하는 것이 요소다.

 

성의없는 요쇼로는 본문 코드를 그대로 쓰는 것과 다름없는 함수, 실질적인 메서드는 하나뿐인 클래스 등이 있다.

 

함수 인라인하기나 클래스 인라인하기, 상속을 사용했다면 계층 합치기로 제거한다.

 

3.15 추측성 일반화

나중에 필요할 거라는 생각으로 만든 코드를 말한다. 당장은 필요 없는 후킹(hooking)포인트와 특이 케이스 처리 로직을 작성해둔 코드에서 나타만다.

 

하는 일이 거의 없는 추상 클래스는 계층 합치기로 제거

쓸데없이 위임하는 코드는 함수 인라인하기나 클래스 인라인하기로 제거

본문에서 사용되지 않는 매개변수는 함수 선언 바꾸기로 제거

 

추측성 일반화 코드는 테스트 코드에서만 사용하는 함수나 클래스에서 흔히 있다.

 

 

3.16 임시 필드

임시 필드를 갖도록 작성하면 코드를 이해하기 어렵다. 사용자는 쓰이지 않는 것처럼 보이는 필드가 존재하는 이유를 파악하느라 골치 아프다.

 

이런 필드들을 발견하면 클래스 추출하기로 옮긴다. 그리고 함수 옮기기로 임시 필드들과 관련된 코드를 클래스로 옮긴다.

 

3.17 메시지 체인

클라이언트가 객체를 통해 다른 객체를 얻은 방금 얻은 객체에 다른 객체를 요청하는 식으로, 다른 객체를 요청하는 작업이 연쇄적으로 이어지는 코드를 말한다.

이럴 경우 중간 단계를 수정하면 클라이언트 코드도 수정해야 한다.

 

문제는 위임 숨기기로 해결할 있다.

래픽터링은 여러 지점에 사용할 있지만, 이러면 모두 중개자가 돼버린다.

 

3.18 중개자

객체의 대표적인 기능 하나로, 외부로부터 세부사항을 숨겨주는 캡슐화가 있다.

캡슐화하는 과정에서 위임이 자주 활용된다.

 

위임이 지나치면 문제가 된다. 클래스 메서드 대부분이 다른 클래스에 구현을 위임하고 있다면, 중개자 제거하기를 통해 실제로 일하는 객체와 직접 소통하도록 만든다.

 

3.19 내부자 거래

모듈 사이의 거래가 많으면 결합도가 높아진다고 한다. 따라서 빈도를 낮추는 것이 좋다.

 

데이터를 주고 받는 모듈들이 있다면 함수 옮기기와 필드 옮기기로 떼어놓아서 사적으로 처리하는 부분을 줄인다.

여러 모듈이 같은 관심사를 공유한다면 공통 부분을 정식으로 처리하는 3 모듈을 새로 만들거나 위임 숨기기를 이용해 다른 모듈이 중간자 역할을 하게 한다.

 

3.20 거대한 클래스

클래스가 너무 많은 일을 하면 필드 수가 늘어나고 클래스에 필드가 많으면 중복 코드가 생기기 쉽다.

 

클래스 추출하기로 유사한 필드들 일부를 따로 묶는다.

일반적으로 접두나 접미가 같은 필드들이 좋은 후보군이 된다.

분리할 컴포넌트를 원래 클래스의 상속관계로 만들고 싶다면 슈퍼클래스 추출하기나 타입 코드를 서브클래스로 바꾸기를 적용한다.

 

코드량이 많은 클래스도 중복 코드와 혼동을 을으킬 여지가 크다. 이럴 클래스 내부에서 중복을 제거한다.

 

클라이언트들이 거대 클래스를 이용하는지 패턴을 파악하여 클래스를 쪼갤지 단서를 얻을 수도 있다. 클라이언트들이 거대 클래스의 특정 기능 그룹만 주로 사용하면, 기능 그룹을 여러 클래스로 분리한다.

 

3.21 서로 다른 인터페이스의 대안 클래스들

클래스를 사용할 장점은 필요에 따라 다른 클래스로 교체할 있다는 점이다

교체하려면 인터페이스가 같아야 한다. 따라서 함수 선언 바꾸기로 메서드 시그니처를 일치시킨다. 이것만으로 부족하면. 함수 옮기기를 이용해 인터페이스가 같아질 때까지 필요한 동작들을 클래스 안으로 넣는다. 이과정에서 클래스들 사이 중복 코드가 생기면 슈퍼클래스 추출하기를 사용한다.

 

3.22 데이터 클래스

데이터 클래스는 데이터 필드와 그에 따른 게터/세터만 있는 클래스

단순히 데이터 저장 용도로만 쓰인다. 때문에 함부로 다를 때가 많다.

public 필드가 있다면 레코드 캡슐화하기로 숨긴다. 변경하면 안되는 필드는 세터를 제거한다.

 

게터나 세터를 사용하는 메서드를 찾아서 함수 옮기기로 데이터 클래스로 옮길 있는지 확인한다.

메서드를 옮기기 힘들다면 함수 추출하기로 옮길 있는 부분만 별도 메서드로 뽑아낸다.

 

3.23 상속 포기

서브클래스에서 부모로부터 상속받기 싫은 경우

 

같은 계층에 서브클래스를 하나 새로 만들고, 메서드 내리기와 필드 내리기를 활용해서 물려받지 않을 부모 코드를 새로 만든 서브클래스로 넘긴다. 그러면 부모 클래스에는 공통부분만 남는다

 

현재는 방식을 권하지 않는다. 일부 동작을 재활용하기 위해 상속을 사용하기도 하는데, 실무에선 유용하기 때문이다. 냄새가 나긴해도 심한 정도가 아니다.

 

상속 포기 냄새는 서브클래스가 부모의 동작은 필요로 하지만 인터페이스는 따르고 싶지 않을 심하게 난다. 인터페이스를 따르지 않을 거면, 아예 상속 메커니즘에서 빼야한다.

 

3.24 주석

주석이 장황할 수록 코드를 잘못 작성했기 때문인 경우가 많다.

 

주석을 남겨야할 같으면 먼저 함수를 리팩터링해본다. 그래도 필요하다 싶으면 남긴다.

 

 

 

 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

 

2.5 리팩터링 고려할 문제

손익을 제대로 이해하고 있어야 한다.

 

기능 개발 속도 저하

리팩터링의 궁극적인 목표는 개발 속도를 높여서, 적은 노력으로 많은 가치를 창출하는 것이다.

 

기능을 구현하기 편해지겠다 싶은 리팩터링이라면 주저하지 않고 리팩터링부터 한다.

 

직접 건드릴 일이 거의 없거나, 불편한 정도가 심하지 않으면 하지 않는

개선할 방향이 마땅히 떠오르지 않으면 일단 둔다.

 

보통은 리팩터링을 너무 안해서 문제

 

코드베이스가 건강하면 기존 코드를 새로운 방식으로 조합하기 쉬워서 복잡한 기능을 빨리 추가할 있다.

 

리팩터링의 본질은 코드베이스를 예쁘게 꾸미는데 있지 않다. 오로지 경제적인 이유로 하는 것이다.

 

 

코드 소유권

리팩터링 대상 코드가 다른 곳에서 호출하는 경우, 또는 API 경우 누가 얼마나 쓰고있는지 모른다.

기존 코드를 두고 기존 코드가 다른 함수를 호출하는 식으로 리팩터링해도 되지만, 그러면 인터페이스가 지져분해진다.

 

이를 방지하기 위해선 조직 시스템을 손봐야한다.

단위로 코드 소유권을 공유하여, 코드를 호출하는 팀원 코드(클라이언트) 수정할 있게 하는것, 그리고 수정한 팀원 코드를 커밋 요청을 코드 주인인 팀원에게 하는 것이다.

 

 

브랜치

가장 흔한 단위 작업 방식 기능 브랜치 방식

팀원 마다 브랜치를 가진다.

어느 정도 작업을 하면, 마스터 브랜치로 통합한다.

방식은 프로덕션 버전으로 릴리스할 마스터에 통합하는 경우가 많다.

이렇게 되면 독립 브랜치로 작업하는 기간이 길어져 마스터로 통합하기 어려워지는 경우가 발생할 있다.

이를 방지하려면 수시로 개인 브랜치를 마스터로 리베이스 또는 머지를 해야한다.

 

여기서 말하는 머지와 통합 차이

머지는 개인 브랜치로 마스터를 MERGE 하는 (마스터 =단방향=> 브랜치)

통합은 머지 전에 마스터로부터 PULL 마스터로 PUSH 하는 (마스터 <=양방향=> 브랜치)

 

 

통합 인터벌이 길면 발생할 있는 흔한 문제는 다음과 같다.

누군가 개인 브랜치에서 작업한 내용을 마스터에 통합하기 전까지 다름 팀원은 내용을 모른다.

내가 함수 이름을 변경했고, 마스터로 통합을 늦게 했다.

사이 팀원은 이름이 바뀔지 모르기 때문에 함수를 호출하는 코드를 개발했다.

그리고 내가 마스터로 통합을 하면, 팀원 코드에서 버그가 발생한다.

 

처럼 통합주기가 길면 길수록 머지 복잡도가 급격히 높아진다.

이를 방지하기 위해 주기(최소 하루 한번) 짧게 가져가는 방식을 지속적 통합(Continuous Integration:CI) 또는 트렁크 기반 개발(Trunk-Based Development:TBD)이라 한다.

 

CI 위해선 마스터를 건강하게 유지하도록 거대한 기능을 잘게 쪼개는 방법과 기능을 키고 있는 기능 토글을 적용할 알아야 한다.

 

CI 리팩터링과 궁합이 좋다. 기법을 합친 것이 익스트림 프로그래밍(eXtreme Programming:XP)이다.

 

기능별 브랜치 방식도 통합 주기를 짧게 가져가면 문제를 최소화 있다.

상황에 맞게 정책을 적용해 사용하면 된다.

 

 

 

테스팅

리팩터링 특징은 겉보기 동작은 똑같이 유지된다는 것이다.

 

리팩터링은 단계별 변경 폭이 작아 도중에 발생한 버그를 쉽게 잡을 있다.

만일 원인을 찾더라도 버전 관리 시스템을 이용하여 가장 최근에 정상 작동하던 상태로 돌리면 그만이다.

 

핵심은 오류를 재빨리 잡는 것이다.

이렇게 하려면 코드의 다양한 측면을 검사하는 테스트 스위트(자가 테스트 코드) 필요하다.

 

자가 테스트 코드는 기능 추가를 안전하게 해준다.

조금에 변경에도 계속 테스트를 한다면, 문제를 일으킨 부분이 순식간에 들어난다.

또한 리팩터링 과정에서 버그가 생길 있다는 불안을 해소할 있다

 

테스트 코드 없이 리팩터링하는 방법도 있다.

테스트 커버리지가 좁은 범위에선 안전하다고 검증된 가지 리팩터링 기법만을 사용해도 효과적으로 리팩터링 있다.

 

 

레거시 코드

레거시 코드의 문제, 대체로 복잡하고 테스트도 제대로 갖춰지지 않은 것이 많다.

무엇보다 다른 사람이 작성한 것이라 거부감이 든다.

 

레거시 시스템을 파악할 리팩터링이 도움된다.

레거시 시스템은 테스트 코드가 없어 리팩터링하기 까다롭다.

 

이를 위해 테스트를 보강해야하는 이것도 상당히 까다롭다.

문제는 마땅히 해결할 방법이 없다. 그래서 처음부터 자가 테스트 코드를 만들어야 한다.

 

캠핑 규칙을 상기하며, 부분 별로 조금씩 나누어 정복하는 것이 최선이다.

 

 

2.6 리팩터링, 아키텍처, 애그니(YAGNI)

기존 방식, 코딩 전에 설계와 아키텍처를 완성하고 코딩 시작

코딩 전에 아키텍처를 확정지으려 문제는 소프트웨어 요구사항을 사전에 모두 파악해야 한다는 것이다. 사실상 실현 불가능한 목표다.

 

리팩터링은 이런 관점을 바꿔 코딩 시작 후에도 요구사항 변화에 따른 대응하도록 코드 베이스를 설계해준다.

 

향후 변경에 유연하도록 유연성 메커니즘을 소프트웨어에 심지도 한다.

예를 들어, 나중에 사용할 같은 매개변수를 미리 적용해 두는

이것도 결국 예측에 불과하기에 매개변수가 늘어난다던가 변수가 생기면 오히려 변화에 대응하는 능력을 떨어뜨를 때가 대부분이다.

 

유연성 매커니즘이란 예상되는 결과를 미리 예측하고 대비해두는

 

리팩터링은 앞으로 어느 부분에 유연성이 필요하고 어떻게 해야 변화에 가장 대응할지 추측하지 않고 현재 요구사항에 최선에 소프트웨어를 만든다.

진행하면서 사용자 요구사항을 이해하게 되면 아키텍처도 그에 맞게 리팩터링한다.

필요하다고 확신이 때만 유연성 메커니즘을 미리 추가한다.

 

이런 설계 방식을 간결한 설계, 점진적 설계, YAGNI(you aren't going to need it)등으로 부른다.

YAGNI 아키텍처를 고려하지 말라는 뜻이 아니라 아키텍처와 설계를 개발 프로세스에 녹이는 방식이다.

 

YAGNI 적용이 선제적인 아키텍처에 소홀하라는 뜻이 아니다.

확실한 부분은 미리 설계해두고, 나중에 이해도가 높아졌을 나머지를 리팩터링으로 설계한다.(진화형 아키렉처 원칙)

 

 

2.7 리팩터링과 소프트웨어 개발 프로세스

익스트림 프로그래밍은 지속적 통합, 자가 테스트 코드, 리팩터링, 등을 하나로 묶은 프로세스다.

 

자가 테스트 코드 + 리팩터링 = 테스트 주도 개발(Test-Driven Development:TDD)

 

익스트림 프로그래밍은 최초의 애자일 소프트웨어 방법론 하나다.

애자일을 제대로 적용하려면 리팩터링에 대한 팀의 역량과 열정이 뒷받침되어 프로세스 전반에 리팩터링이 자연스럽게 스며들도록 해야 한다.

 

리팩터링의 번째 토대는 자가 테스트 코드다.

 

팀으로 개발하면서 리팩터링 하려면 팀원이 다른 팀원의 작업을 방해하지 않으면서 리팩터링을 있어야 한다.

이를 위해선 지속적 통합이 필요하다. 그래야 리팩터링으로 인한 코드 변화를 빠르게 알아내지 못해 생기는 문제를 방지할 있다.

함수 이름 변경, 함수 제거 등을 한다면, 잦은 통합 말고 호출부에서 감지할 방법이 마땅치 않다.

이런 문제를 빠르게 발견할 있게 돕는 자가 테스크 코드는 지속적 통합의 핵심 요소다.

 

지속적 통합, 자가 테스트 코드, 리팩터링 기법을 적용하면 YAGNI 설계 방식으로 개발을 진행 있다.

추측에 근거한 유연성 메커니즘을 갖춘 시스템보다 단순한 시스템이 변경하기 훨씬 쉽다.

 

위와 같은 토대를 마련했다면, 애자일이 주는 이점인 지속적인 배포를 수행할 있다.

이로 인해 고객의 비즈니스 요구를 프로덕션 코드로 빠르게 반영해 빠르게 배포할 있다.

 

 

 

2.8 리팩터링과 성능

직관적인 설계 vs 성능

 

리팩터링이 성능을 저하시킬까봐 걱정하는 사람이 많다.

저자는 리팩터링이 성능이 느려진다고 해도 진행한다. 이유는 소프트웨어를 이해하기 쉽게 만들기 때문이다.

 

성능을 무시하기 때문이아니라 경험상 성능이 낮아지는 경우가 드물기 때문이다.

만약 느려진다고 해도 리팩터링된 코드는 되려 튜닝하기 좋은 상태이다.

 

빠른 소프트웨어 작성 방법 가지

예산 분배 방식

가장 엄격한 방식, 설계를 여러 컴포넌트로 나눠 각각 시스템 자원(시간,공간) 예산을 할당

컴포넌트는 할당된 예산만 사용 가능, 컴포넌트간 예산 교환 가능

 

지속적인 관심 기울이기

가장 흔한 방식이지만 효과는 좋지 않다.

성능 개선을 위해 수정하다보니 코드만 다루기 어렵게 변한다.

방식은 최적화가 프로그램 전체에 퍼지게 된다.

 

일부분만 최적화

대부분 프로그램은 전체 코드 극히 일부에서 시간을 소비한다. 그래서 코드 전체를 고르게 최적화 한다면, 대부분이 낭비인 것이다.

최적화는 '90% 시간은 낭비'라는 통계에서 착안한 것이다.

 

성능 최적화는 신경 안쓰고, 코드를 다루기 쉽게 만드는 데만 집중하며 개발한다.

성능 최적화 단계에선 프로파일러 프로그램으로 시간과 공간을 잡아먹는 지점을 탐색한다.

특정된 부분들만 개선한다.

성능이 개선되지 않았다면, 이전 개선 내용을 되돌리고, 다른 최적화 대상을 개선한다.

과정을 목표 성능이 도달할때까지 반복한다.

 

이렇게 되면 필요한 부분만 개선하게 된다.

 

리팩터링해두면, 튜닝에 소비할 시간을 있다. 또한 코드 범위가 줄어, 프로그램으로 성능을 세밀하게 측정할 있다. 때문에 튜닝이 쉬워진다.

 

결과적으로 리팩터링은 단기적으로는 성능이 떨어질 수도 있으나 최적화 단계에선 튜닝하기가 쉬워져 빠른 소프트웨어를 얻게 된다.

 

2.9 리팩터링의 유래

명확한 유래는 없다.

현재 리팩터링이란 자체는 중요성이 충분히 검증됐고, 증거로 많은 IDE에서 자동 리팩터링 기능까지 지원한다.

 

 

2.10 리팩터링 자동화

과거 리팩터링과 현재 리팩터링에서 가장 변화는 자동 리팩터링 지원 도구 등장이다.

IDE에서 지원하는 자동 리팩터링

 

리팩터링을 자동화하는 가장 수준 낮은 방식은 텍스트로 조작하는 것이다.

불완전하기에 테스트해보기 전까지 신뢰해선 안된다.

텍스트 에디터가 해당한다.

 

코드를 텍스트가 아닌 구문 트리로 인식해 자동 리팩터링하는 것이 제대로된 방식이다.

IDE 해당한다.

 

언어마다 자동 리팩터링을 지원하는 정도가 달라진다.

자바와 같은 정적 타입 언어는 안전하게 리팩터링을 지원할 있다.

자바는 클래스 이름이 조금만 달라도 다른 타입이다.

메서드 이름 변경 같은 메서드 이름이라도 타입에 맞게 정확히 걸러낸다.

 

자동 리팩터링 기능은 IDE 강력하므로 리팩터링은 IDE 진행하는 것이 좋다.

 

 

 

 

2.11 알고 싶다면

리팩터링이 궁금하다, 월리엄 웨이크의 "리팩터링 워크북"

패턴과 접목된 리팩터링, 조슈아 케리에프스키의 "패턴을 활용한 리팩터링"

레거시 코드를 리팩터링하는 방법, 마이클 페더스의 "레거시 코드 활용 전략"

 

 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

2.1 리팩터링 정의

소프트웨어의 겉보기 동작은 그대로 유지한 채, 코드를 이해하고 수정하기 쉽도록 내부 구조를 변경하는 기법

 

올바른 리팩터링은 리팩터링이 중간에 멈춰도 항상 동작 보장한다.

 

번에 바꿀 있는 작업을 수많은 단계로 잘게 나눠서 작업하는 모습이 비효율적이라고 생각할 있다.

하지만 이렇게 잘게 나눔으로써, 단계들이 체계적으로 구성되며

무엇보다 디버깅하는 시간이 줄어든다.

 

리팩터링 전 후 코드는 똑같이 동작해야 한다.

 

리팩터링과 성능 최적화 차이

코드를 변경하지만 프로그램 전반적인 기능은 유지한다.

목적이 다르다.

 

리팩터링은 코드를 이해하고 수정하기 쉽게 만드는 것이다.

프로그램 성능은 좋아질 수도 아빠질 수도 있다.

 

성능 최적화는 속도 개선에만 신경 쓴다.

코드는 다루기 어렵게 바뀔 수도 있다.

 

 

2.2 두 개의 모자

소프트웨어 개발 목적이 "기능 추가"인지 "리팩터링"인지 명확히 해야한다.

 

"기능 추가"모자를 썼다면, 절대 기존 코드를 건들지 않고, 기능 추가만한다.

진척도는 테스트를 추가해 통과하는지

"리팩터링"모자를 썼다면, 기존 코드만 재구성하고, 절대 기능 추가는 하지 않는다.

기능 추가 모자를 썼을 테스트를 누락한게 아니라면, 테스트도 추가하지 않는다.

 

물론 소프트웨어 개발 모자를 번갈아가며 있다.

 

 

2.3 리팩터링하는 이유

리팩터링하면 소프트웨어 설계가 좋아진다

리팩터링하지 않으면 소프트웨어 설계(아키텍처) 썩기 쉽다.

 

어키렉터가 무너짐 => 코드만 봐서 설계 파악 어려움

설계 유지보수가 어려워짐

 

설계가 나쁘면 코드가 길어진다.

같은 코드가 여러 곳에 나타날 있기 때문이다.

그래서 중복 코드 제거는 설계 개선 작업의 중요한 축을 차지한다.

 

코드량이 준다고 성능이 좋아지진 않는다. 하지만 코드량이 줄면 수정에 드는 노력이 감소한다.

코드가 수록 실수 없이 수정하기 힘들어 진다.

 

코드 중복은 코드 부분만 살짝 바꿔서는 시스템이 예상대로 작동하지 않을 있다.

 

 

리팩터링하면 소프트웨어를 이해하기 쉬워진다

컴퓨터에게 시키려는 일과 이를 표현한 코드의 차이를 최대한 줄여야 한다.

 

소스 코드를 컴퓨터만 사용하는게 아니다, 나중에 누군가 코드를 수정하고자 일게 있다.

 

프로그래밍에서는 사람이 가장 중요하지만 이를 소홀하기 쉽다.

컴파일 시간이 몇초 걸리는 보다. 다른 프로그래머가 코드를 제대로 이해하도록 코드 수정 시간을 줄이는 것이 유익하다.

 

리팩터링은 코드를 읽히게 한다.

 

리팩터링은 다른 사람은 배려하기 위해서가 아니다. 사실 다른 사람이 바로 자신일 때가 많다.

 

 

 

리팩터링하면 버그를 쉽게 찾을 있다

코드를 이해하기 쉬우면 버그를 찾기 쉽다.

리팩터링하면 코드가 하는 일을 깊이 파악하게 되면서, 버그를 찾기 수월해진다.

 

 

리팩터링하면 프로그래밍 속도를 높일 있다

보통 리팩터링에 대해 설명하면 품질을 높인다는 것엔 이견이 없다.

내부 설계와 가독성 개선이 버그를 주어들게 하고 이는 모두 품질 향상에 직결된다.

하지만 리팩터링하는 시간이 드니 전체 개발 속도는 떨어질까봐 걱정할 수도 있다.

 

보통 시스템을 오래 개발 중인 개발자들과 얘기하다 보면 초기에는 진척이 빨랐지만 현재는 새기능을 하나 추가하는 훨씬 오래 걸린다는 말을 많이 한다.

기존 코드에 코드를 추가하기도 어렵고, 과정에서 버그는 덤이다.

기존 코드에 패치에 패치를 거듭하면서 프로그램 동작을 파악하기 어려워진다.

종국엔 새로 개발하는게 빠르겠다는 생각을 하게 된다.

 

내부 설계가 소프트웨어는 새로운 기능을 추가할 지점과 어떻게 고칠지를 쉽게 찾을 있다.

 

모듈화가 되어 있으면 전체 코드 베이스 작은 일부만 이해하면 된다.

 

 

2.4 언제 리팩터링해야 할까?

프로그래밍을 하는 도중에 주기적으로 하는 것이 좋다. 

즉, 별도 시간을 마련하지 않고, 일상 코딩에 리팩터링을 녹여내는 것

3의 법칙

  1. 처음에는 그냥 한다.
  2. 비슷한 일을 번째로 하게 되면, 일단 계속 진행한다.
  3. 비슷한 일을 번째 하게 되면 리팩터링한다.

준비를 위한 리팩터링: 기능을 쉽게 추가하게 만들기

리팩터링하기 가장 좋은 시점은 코드베이스에 기능을 새로 추가하기 직전이다.

 

현재 코드를 살펴보며, 구조를 살짝 바꾸면 다른 작업하기 쉬워질 부분을 찾는다.

예를 들어, 리터럴 개가 방해되는 함수가 있다고 하자.

함수를 복제해서 값만 수정해서 써도 되지만, 이러면 나중에 변경이 생길 복제해서 코드 전부를 수정해야 한다. 심지어 여기저기 숨어있기까지 하다.

 

이해를 위한 리팩터링: 코드를 이해하기 쉽게 만들기

코드 수정을 위해선 먼저 코드가 하는 파악이 우선이다.

코드를 파악할 때마다 코드의 의도가 명확하게 드러나도록 리팩터링할 여지가 있는지 찾아보고 손본다.

조건부 로직 구조가 이상하지 않은지, 함수 이름이 이상한지

 

과정에서 코드를 어느 정도 이해하게 된다. 다만 기억을 그대로 코드에 반영해두지 않으면, 휘발성 기억이 되버린다.

 

리팩터링을 진행하면 코드가 깔끔하게 정리된다. 이렇게 정리되면 전에 보이면 설계가 눈에 들어온다.

 

리팩터링을 진행하면 코드를 보다 깊은 수준까지 이해하게 된다.

 

쓰레기 줍기 리팩터링

보이 스카우트 원칙에 기반한 내용

 

비효율적으로 처리하는 코드를 발견할 때가 있다.

 

로직이 쓸데없이 복잡하거나, 매개변수화한 함수 하나면 일을 거의 똑같은 함수 여러 개로 작성

 

이럴 간단하다면, 즉시 고치고, 시간이 걸린다면 메모를 해두고 하던 일을 마무리하고, 처리한다.

 

리팩터링의 멋진 점은 각각의 작은 단계가 코드를 깨뜨리지 않는다.

그래서 작업을 잘게 나눠 달에 걸쳐 진행하더라도 문제가 되지 않는다.

계획된 리팩터링과 수시로 하는 리팩터링

준비를 위한 리팩터링, 이해를 위한 리팩터링, 쓰레기 줍기 리팩터링은 기회가 때만 진행한다.

 

개발에 들어가기 전에 리팩터링 일정을 따로 잡지 않고, 기능 추가나 버그를 잡는 동안 함께 진행한다.

, 프로그래밍 과정에 녹여 자연스럽게 한다.

 

리팩터링은 프로그래밍과 구분되는 별개의 활동이 아니다.

마치 if 작성할 시간을 따로 잡지 않는 것과 같다.

 

계획된 리팩터링은 그동안 리팩터링에 소홀 했을 진행한다.

결과적으로 계획된 리팩터링을 안해도 되는 것이 좋다.(수시로 리팩터링 했다는 증거)

 

오래 걸리는 리팩터링

대부분의 리팩터링은 분에서 시간 사이다.

때론 전체가 붙어도 걸리는 대규모 리팩터링도 있다.

이런 상황에서도 전체가 리팩터링에 매달리는 것은 효율적이지 않다.

리팩터링은 중간에 멈춰도 코드를 깨트리지 않는 다는 장점이 있기 때문에 팀원 각자가 리팩터링할 코드를 만다면, 각자 조금씩 개선하는 식이 효율적이다.

 

코드 리뷰에 리팩터링 활용하기

경험이 많은 개발자의 노하우를 적은 개발자에게 전수할 있다.

눈엔 명확한 코드가 다른 팀원에게 명확하지 않을 있다.

코드 리뷰를 하면 다른 사람의 아이디어를 얻을 있다는 장점도 있다.

관리자에게는 뭐라고 말해야 할까?

상당수의 관리자와 고객은 코드베이스 상태가 생산성에 미치는 영향을 모른다.

이런 상황에 있는 이들에게는 "리팩터링한다고 말하지 말라" 조언한다.

 

개발자는 프로다. 프로 개발자는 소프트웨어를 최대한 빨리 만들어야 한다.

리팩터링은 결과적으로 소프트웨어 생산성을 높인다.

리팩터링하지 말아야

외부 API 다루듯 호출해서 쓰는 코드라면 지저분해도 그냥 둔다.

내부 동작을 이해해야 시점에 리팩터링해야 효과를 제대로 있기 때문이다.

 

리팩터링하는 보다 처음부터 새로 작성하는 쉬울 때도 리팩터링하지 않는다. 판단은 노련한 개발자가 아니면 내리기 힘들다.

'IT책, 강의 > 리팩터링' 카테고리의 다른 글

03 - 코드에서 나는 악취  (0) 2023.07.10
02 - 리팩터링 원칙 - 02  (0) 2023.07.08
01 - 리팩터링: 첫 번째 예시 - 02  (0) 2023.07.04
01 - 리팩터링: 첫 번째 예시 - 01  (0) 2023.07.02
00 - 들어가며  (0) 2023.07.01
 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

 

 

GitHub - rkwhr0010/refactoring: 리팩터링책 내용

리팩터링책 내용. Contribute to rkwhr0010/refactoring development by creating an account on GitHub.

github.com

1.6 계산 단계와 포맷팅 단계 분리하기

지금까진 프로그램의 논리적인 요소 파악을 위해 코드 구조를 보강했다.

리팩터링 초기 단계에서 흔히 수행한다.

 

골격은 충분히 개선 됐으니, 이제 원하는 기능 변경 작업을 차례다.

statement()함수의 HTML 버전 만들기

 

구조를 개선해서 처음 코드보다 작업하기 편해졌다.

계산코드가 모두 분리됐기 때문에 일곱 줄짜리 최상단 코드에 대응하는 HTML 버전만 작성하면 된다.

 

HTML버전 statement()함수를 만드는데 문제점이 있다. 계산 코드가 전부 내부 중첩 함수라 그것을 그대로 복사해서 만들면 코드 중복이 발생한다.

 

문제 해결법으로 가장 추천하는 방식은 단계 쪼개기다.

statement() 로직을 단계로 나누는 것이다.

단계는 statement() 필요한 데이터를 처리하고,

다음 단계는 앞서 처리한 결과를 텍스트나 HTML 표현하는

function statement(invoice, plays) {
    return renderPlainText(invoice, plays) //본문 전체를 별도 함수로 추출
}
function renderPlainText(invoice, plays) {
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;
    for(let perf of invoice.performances){
        result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
    }
    result += `총액: ${usd(totalAount())}\n`; 
    result += `적립 포인트: ${totalVolumeCredits()}점\n`;
    return result;
    function totalAount(){ 
        let result = 0;
        for(let perf of invoice.performances){
            result += amountFor(perf);
        }
        return result;
    }
…. 이하 추출한 본문




function statement(invoice, plays) {
    const statementData = {};
    return renderPlainText(statementData, invoice, plays) //중간데이터 구조를 인수로 전달
}
function renderPlainText(data, invoice, plays)

 

renderPlainText() 인수 invoice 중간 데이터구조로 옮기기

function statement(invoice, plays) {
    const statementData = {};
    statementData.customer = invoice.customer;
    statementData.performances = invoice.performances;
    return renderPlainText(statementData, /*invoice,*/ plays) //필요없어진 인수 삭제
}
function renderPlainText(data, plays) {
    let result = `청구 내역 (고객명 : ${data.customer})\n`;
    for(let perf of data.performances){
        result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
    }
    result += `총액: ${usd(totalAount())}\n`; 
    result += `적립 포인트: ${totalVolumeCredits()}점\n`;
    return result;
function totalAount(){

 

데이터 복사

function statement(invoice, plays) {
    const statementData = {};
    statementData.customer = invoice.customer;
    statementData.performances = invoice.performances.map(enrichPerformance);
    return renderPlainText(statementData,  plays) 

    function enrichPerformance(aPerformance){
        const result = Object.assign({}, aPerformance);//얕은 복사 수행
        return result;
    }
}

이렇게 데이터를 복사해서 사용하는 이유는 데이터를 불변으로 취급하기 위함

function statement(invoice, plays) {
    const statementData = {};
    statementData.customer = invoice.customer;
    statementData.performances = invoice.performances.map(enrichPerformance);
    return renderPlainText(statementData,  plays) 
    function enrichPerformance(aPerformance){
        const result = Object.assign({}, aPerformance);
        result.play = playFor(result);//중간 데이터에 연극 정보를 저장
        return result;
    }
    function playFor(aPerformance){ //renderPlainText 중첩함수를 옮김
        return plays[aPerformance.playID];
    }
}

옮기면서 renderPlainText()에서 playFor()사용했던 부분 전부 변경

 

 

amountFor() 비슷하기 옮기기

function statement(invoice, plays) {
    const statementData = {};
    statementData.customer = invoice.customer;
    statementData.performances = invoice.performances.map(enrichPerformance);
    return renderPlainText(statementData,  plays) 
    function enrichPerformance(aPerformance){
        const result = Object.assign({}, aPerformance);
        result.play = playFor(result);//중간 데이터에 연극 정보를 저장
        result.amount = amountFor(result);
        return result;
    }
    function playFor(aPerformance){ //renderPlainText 중첩함수를 옮김
        return plays[aPerformance.playID];
    }

    function amountFor(aPerformance) {
        let result = 0;
        switch(aPerformance.play.type){
            case "tragedy" : 
            result = 40000;
            if(aPerformance.audience > 30){
                result += 1000 * (aPerformance.audience - 30);
            }
            break;
            case "comedy" : 
            result = 30000;
            if(aPerformance.audience > 30){
                result += 10000 + 500 * (aPerformance.audience - 20);
            }
            result += 300 * aPerformance.audience;
            break;
            default: throw new Error(`알수 없는 장르: ${aPerformance.play.type}`);
        }
        return result; 
    }
}

volumeCreditsFor()옮기기

function statement(invoice, plays) {
    const statementData = {};
    statementData.customer = invoice.customer;
    statementData.performances = invoice.performances.map(enrichPerformance);
    return renderPlainText(statementData,  plays) 
    function enrichPerformance(aPerformance){
        const result = Object.assign({}, aPerformance);
        result.play = playFor(result);//중간 데이터에 연극 정보를 저장
        result.amount = amountFor(result);
        result.volumeCredits = volumeCreditsFor(result);
        return result;
    }
    function playFor(aPerformance){ //renderPlainText 중첩함수를 옮김
        return plays[aPerformance.playID];
    }

    function amountFor(aPerformance) {
        let result = 0;
        switch(aPerformance.play.type){
            case "tragedy" : 
            result = 40000;
            if(aPerformance.audience > 30){
                result += 1000 * (aPerformance.audience - 30);
            }
            break;
            case "comedy" : 
            result = 30000;
            if(aPerformance.audience > 30){
                result += 10000 + 500 * (aPerformance.audience - 20);
            }
            result += 300 * aPerformance.audience;
            break;
            default: throw new Error(`알수 없는 장르: ${aPerformance.play.type}`);
        }
        return result; 
    }
    function volumeCreditsFor(aPerformance){
        let volumeCredits = 0;
        volumeCredits += Math.max(aPerformance.audience -30,0);
        if("comedy" === aPerformance.play.type) 
            volumeCredits += Math.floor(aPerformance.audience/5);
        return volumeCredits;
    }
}

마지막 총합을 구하는 부분 옮기기

 

function statement(invoice, plays) {
    const statementData = {};
    statementData.customer = invoice.customer;
    statementData.performances = invoice.performances.map(enrichPerformance);
    statementData.totalAmount = totalAmount(statementData);
    statementData.totalVolumeCredits = totalVolumeCredits(statementData);
    return renderPlainText(statementData,  plays) 
    function enrichPerformance(aPerformance){
        const result = Object.assign({}, aPerformance);
        result.play = playFor(result);//중간 데이터에 연극 정보를 저장
        result.amount = amountFor(result);
        result.volumeCredits = volumeCreditsFor(result);
        return result;
    }
    function playFor(aPerformance){ //renderPlainText 중첩함수를 옮김
        return plays[aPerformance.playID];
    }

    function amountFor(aPerformance) {
        let result = 0;
        switch(aPerformance.play.type){
            case "tragedy" : 
            result = 40000;
            if(aPerformance.audience > 30){
                result += 1000 * (aPerformance.audience - 30);
            }
            break;
            case "comedy" : 
            result = 30000;
            if(aPerformance.audience > 30){
                result += 10000 + 500 * (aPerformance.audience - 20);
            }
            result += 300 * aPerformance.audience;
            break;
            default: throw new Error(`알수 없는 장르: ${aPerformance.play.type}`);
        }
        return result; 
    }
    function volumeCreditsFor(aPerformance){
        let volumeCredits = 0;
        volumeCredits += Math.max(aPerformance.audience -30,0);
        if("comedy" === aPerformance.play.type) 
            volumeCredits += Math.floor(aPerformance.audience/5);
        return volumeCredits;
    }
    function totalAmount(data){ 
        return data.performances //반복문을 파이프라인으로 바꿈
            .reduce((total, p) => total + p.volumeCredits, 0 );
    }
    function totalVolumeCredits(data){
        return data.performances //반복문을 파이프라인으로 바꿈
            .reduce((total, p)=>total+p.volumeCredits, 0);
    }
}

statemnet() 필요한 데이터 처리 코드 함수로 빼기

function statement(invoice, plays) {
    return renderPlainText(createStatementData(invoice, plays));
    function createStatementData(invoice, plays) {
        const statementData = {};
        statementData.customer = invoice.customer;
        statementData.performances = invoice.performances.map(enrichPerformance);
        statementData.totalAmount = totalAmount(statementData);
        statementData.totalVolumeCredits = totalVolumeCredits(statementData);
        return statementData; 
    }

중간 데이터 부분 함수로 빼기

function statement(invoice, plays) {
    return renderPlainText(createStatementData(invoice, plays));
    function createStatementData(invoice, plays) {
        const statementData = {};
        statementData.customer = invoice.customer;
        statementData.performances = invoice.performances.map(enrichPerformance);
        statementData.totalAmount = totalAmount(statementData);
        statementData.totalVolumeCredits = totalVolumeCredits(statementData);
        return statementData; 
}

 

분리된 코드를 각 파일에 나눠서 별도 파일에 저장해 기존 코드를 재사용한다.

나는 실습 편의상 한 파일로 진행했다.

 

1.7 중간 점검: 파일( 단계) 분리됨

//statement.js 파일이라 가정
function statement(invoice, plays) {
    return renderPlainText(createStatementData(invoice, plays));
}
function htmlStatement(invoice, plays){
    return renderHtml(createStatementData(invoice,plays));
    //중간데이터 생성 함수를 공유한다.
}
function renderHtml(data, plays) {
    let result = `<h1>청구 내역 (고객명 : ${data.customer})</h1>\n`;
    result += "<table>\n";
    result += "<tr><th>연극</th><th>좌석 수</th><th>금액</th></tr>"
    for(let perf of data.performances){
        result += `<tr><td>${perf.play.name}</td><td>(${perf.audience}석)</td><td>${usd(perf.amount)}</td></tr>\n`;
    }
    result += "</table>\n";
    result += `<p>총액: <em>${usd(data.totalAmount)}</em></p>\n`; 
    result += `<p>적립 포인트: <em>${data.totalVolumeCredits}</em>점</p>\n`;
    return result;
}
function renderPlainText(data, plays) {
    let result = `청구 내역 (고객명 : ${data.customer})\n`;
    for(let perf of data.performances){
        result += `${perf.play.name} : ${usd(perf.amount)} (${perf.audience}석)\n`;
    }
    result += `총액: ${usd(data.totalAmount)}\n`; 
    result += `적립 포인트: ${data.totalVolumeCredits}점\n`;
    return result;
}
function usd(aNumber){
    return new Intl.NumberFormat("en-US", {
        style: "currency",
        currency: "USD",
        minimumFractionDigits: 2
    }).format(aNumber/100); 
}
//statement.js 파일이라 가정 끝

//createStatementData.js 별도 파일이라 가정
function createStatementData(invoice, plays) {
    const statementData = {};
    statementData.customer = invoice.customer;
    statementData.performances = invoice.performances.map(enrichPerformance);
    statementData.totalAmount = totalAmount(statementData);
    statementData.totalVolumeCredits = totalVolumeCredits(statementData);
    return statementData; 
    function enrichPerformance(aPerformance){
        const result = Object.assign({}, aPerformance);
        result.play = playFor(result);
        result.amount = amountFor(result);
        result.volumeCredits = volumeCreditsFor(result);
        return result;
    }
    function playFor(aPerformance){ 
        return plays[aPerformance.playID];
    }

    function amountFor(aPerformance) {
        let result = 0;
        switch(aPerformance.play.type){
            case "tragedy" : 
            result = 40000;
            if(aPerformance.audience > 30){
                result += 1000 * (aPerformance.audience - 30);
            }
            break;
            case "comedy" : 
            result = 30000;
            if(aPerformance.audience > 30){
                result += 10000 + 500 * (aPerformance.audience - 20);
            }
            result += 300 * aPerformance.audience;
            break;
            default: throw new Error(`알수 없는 장르: ${aPerformance.play.type}`);
        }
        return result; 
    }
    function volumeCreditsFor(aPerformance){
        let volumeCredits = 0;
        volumeCredits += Math.max(aPerformance.audience -30,0);
        if("comedy" === aPerformance.play.type) 
            volumeCredits += Math.floor(aPerformance.audience/5);
        return volumeCredits;
    }
    function totalAmount(data){ 
        return data.performances 
            .reduce((total, p) => total + p.amount, 0 );
    }
    function totalVolumeCredits(data){
        return data.performances 
            .reduce((total, p)=>total+p.volumeCredits, 0);
    }
}
//createStatementData.js 별도 파일이라 가정 끝

전체 로직을 구서하는 요소들이 뚜렸하게 분리되면서 파악이 쉬워졌다.

그리고 모듈화하면서 html버전도 재사용으로 쉽게 만들었다.

프로그래밍에선 명료함이 중요하다.

 

 

 

1.8 다형성을 활용해 계산 코드 재구성하기

amountFor()함수를 보면 연극 장르에 따라 계산 방식이 달라진다.

이런 조건부 로직은 코드 수정 획수가 늘어날수록 골칫거기로 전락한다.

 

"조건부 로직을 다형성으로 바꾸기"

리팩터링은 조건부 코드 덩어리를 다형성을 활용하는 방식으로 바꿔준다.

리팩터링을 적용하려면 상속 계층부터 정의해야 한다.

 

공연료 계산기 만들기

여기서 핵심은 각 공연 정보를 중간 데이터 구조에 채워주는 enrichPerformance()함수

이 함수는 조건부 로직을 포함한 함수인 amountFor()와 volumeCreditsFor()를 호출한다.

두 함수를 전용 클래스로 옮기는 작업을 수행한다.

 

createStatementData()함수속
...
    function enrichPerformance(aPerformance){
        const calculator = new PerformanceCalculator(aPerformance); //공연료 계산기 생성
        const result = Object.assign({}, aPerformance);
        result.play = playFor(result);
        result.amount = amountFor(result);
        result.volumeCredits = volumeCreditsFor(result);
        return result;
    }


최상위
class PerformanceCalculator{
    constructor(aPerformance){
        this.performance = aPerformance;
    }
}

함수들을 계산기로 옮기기

//createStatementData.js 별도 파일이라 가정

class PerformanceCalculator{
    constructor(aPerformance, aPlay){
        this.performance = aPerformance;
        this.play = aPlay;
    }
    get amount(){
        let result = 0;
        switch(this.play.type){
            case "tragedy" : 
            result = 40000;
            if(this.performance.audience > 30){
                result += 1000 * (this.performance.audience - 30);
            }
            break;
            case "comedy" : 
            result = 30000;
            if(this.performance.audience > 30){
                result += 10000 + 500 * (this.performance.audience - 20);
            }
            result += 300 * this.performance.audience;
            break;
            default: throw new Error(`알수 없는 장르: ${this.play.type}`);
        }
        return result; 
    }
    get volumeCredits(){
        let result = 0;
        result += Math.max(this.performance.audience -30,0);
        if("comedy" === this.play.type) 
        result += Math.floor(this.performance.audience/5);
        return result;
    }
}
function createStatementData(invoice, plays) {
    const statementData = {};
    statementData.customer = invoice.customer;
    statementData.performances = invoice.performances.map(enrichPerformance);
    statementData.totalAmount = totalAmount(statementData);
    statementData.totalVolumeCredits = totalVolumeCredits(statementData);
    return statementData; 
    function enrichPerformance(aPerformance){
        const calculator = new PerformanceCalculator(aPerformance, playFor(aPerformance)); //공연 정보를 계산기로 전달
        const result = Object.assign({}, aPerformance);
        result.play = calculator.play;
        result.amount = calculator.amount;
        result.volumeCredits = calculator.volumeCredits;
        return result;
    }
    function playFor(aPerformance){ 
        return plays[aPerformance.playID];
    }

    function totalAmount(data){ 
        return data.performances 
            .reduce((total, p) => total + p.amount, 0 );
    }
    function totalVolumeCredits(data){
        return data.performances 
            .reduce((total, p)=>total+p.volumeCredits, 0);
    }
}
//createStatementData.js 별도 파일이라 가정 끝

 

 

공연료 계산기를 다형성 버전으로 만들기

클래스에 로직을 담았으니 다형성을 지원하도록 만든다.

먼저 타입 코드를 서브클래스로 바꾸기를 진행한다.

 

그러기 위해선 클래스를 직접적으로 생성하는 코드를 팩토리 함수를 호출하게 하여 의존성을 낮춰야 한다.


class PerformanceCalculator{
    constructor(aPerformance, aPlay){
        this.performance = aPerformance;
        this.play = aPlay;
    }
    get amount(){
        return new Error('서브클래스에서 처리하도록 설계되었습니다.');
    }
    get volumeCredits(){
        let result = 0;
        result += Math.max(this.performance.audience -30,0);
        if("comedy" === this.play.type) 
        result += Math.floor(this.performance.audience/5);
        return result;
    }
}
class TragedyCalcultor extends PerformanceCalculator{
    get amount(){
        let result = 0;
        result = 40000;
        if(this.performance.audience > 30){
            result += 1000 * (this.performance.audience - 30);
        }
        return result;
    }
}
class ComedyCalcultor extends PerformanceCalculator{
    get amount(){
        let result = 0;
        result = 30000;
        if(this.performance.audience > 30){
            result += 10000 + 500 * (this.performance.audience - 20);
        }
        result += 300 * this.performance.audience;
        return result;
    }
}
function createPerformanceCalculator(aPerformance, aPlay){
    switch(aPlay.type){
        case "tragedy" : return new TragedyCalcultor(aPerformance,aPlay);
        case "comedy" : return new ComedyCalcultor(aPerformance,aPlay);
        default: throw new Error(`알 수  없는 장르 : ${aPlay.type}`);
    }
}

 

1.9 상태 점검 : 다형성을 활용하여 데이터 생성하기

//createStatementData.js 별도 파일이라 가정

class PerformanceCalculator{
    constructor(aPerformance, aPlay){
        this.performance = aPerformance;
        this.play = aPlay;
    }
    get amount(){
        return new Error('서브클래스에서 처리하도록 설계되었습니다.');
    }
    get volumeCredits(){
        return Math.max(this.performance.audience -30,0);
    }
}
class TragedyCalcultor extends PerformanceCalculator{
    get amount(){
        let result = 0;
        result = 40000;
        if(this.performance.audience > 30){
            result += 1000 * (this.performance.audience - 30);
        }
        return result;
    }
}
class ComedyCalcultor extends PerformanceCalculator{
    get amount(){
        let result = 0;
        result = 30000;
        if(this.performance.audience > 30){
            result += 10000 + 500 * (this.performance.audience - 20);
        }
        result += 300 * this.performance.audience;
        return result;
    }
    get volumeCredits(){
        return super.volumeCredits + Math.floor(this.performance.audience/5);
    }
}
function createPerformanceCalculator(aPerformance, aPlay){
    switch(aPlay.type){
        case "tragedy" : return new TragedyCalcultor(aPerformance,aPlay);
        case "comedy" : return new ComedyCalcultor(aPerformance,aPlay);
        default: throw new Error(`알 수  없는 장르 : ${aPlay.type}`);
    }
}
function createStatementData(invoice, plays) {
    const statementData = {};
    statementData.customer = invoice.customer;
    statementData.performances = invoice.performances.map(enrichPerformance);
    statementData.totalAmount = totalAmount(statementData);
    statementData.totalVolumeCredits = totalVolumeCredits(statementData);
    return statementData; 
    function enrichPerformance(aPerformance){
        const calculator = createPerformanceCalculator(aPerformance, 
            playFor(aPerformance)); //생성자 대신 팩토리 함수 이용
        const result = Object.assign({}, aPerformance);
        result.play = calculator.play;
        result.amount = calculator.amount;
        result.volumeCredits = calculator.volumeCredits;
        return result;
    }
    function playFor(aPerformance){ 
        return plays[aPerformance.playID];
    }

    function totalAmount(data){ 
        return data.performances 
            .reduce((total, p) => total + p.amount, 0 );
    }
    function totalVolumeCredits(data){
        return data.performances 
            .reduce((total, p)=>total+p.volumeCredits, 0);
    }
}
//createStatementData.js 별도 파일이라 가정 끝

조건부 로직을 생성 함수하나로 옮겼다.

연극 장르별 계산 코드들을 함께 묶었다.

 

1.10 마치며

리팩터링을 크게 3 단계로 진행했다.

1단계 : 원본 함수를 중첩 함수 여러개로 나누기

2단계 : 쪼개기를 적용, 계산 코드/출력 코드 분리

3단계 : 계산 로직을 다형성으로 표현

 

좋은 코드를 가늠하는 확실한 방법은 '얼마나 수정하기 쉬운가'다

코드를 수정할 때 고쳐야 할 곳을 쉽게 찾고, 오류 없이 빠르게 수정할 수 있어야 한다.

고객의 요구사항을 빠르게 반영할 수 있어야 한다.

 

 

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
<script src="plays.json" type="text/javascript"></script>
<script src="invoices.json" type="text/javascript"></script>
<script>



//statement.js 파일이라 가정
function statement(invoice, plays) {
    return renderPlainText(createStatementData(invoice, plays));
}
function htmlStatement(invoice, plays){
    return renderHtml(createStatementData(invoice,plays));
    //중간데이터 생성 함수를 공유한다.
}
function renderHtml(data, plays) {
    let result = `<h1>청구 내역 (고객명 : ${data.customer})</h1>\n`;
    result += "<table>\n";
    result += "<tr><th>연극</th><th>좌석 수</th><th>금액</th></tr>"
    for(let perf of data.performances){
        result += `<tr><td>${perf.play.name}</td><td>(${perf.audience}석)</td><td>${usd(perf.amount)}</td></tr>\n`;
    }
    result += "</table>\n";
    result += `<p>총액: <em>${usd(data.totalAmount)}</em></p>\n`; 
    result += `<p>적립 포인트: <em>${data.totalVolumeCredits}</em>점</p>\n`;
    return result;
}

function renderPlainText(data, plays) {
    let result = `청구 내역 (고객명 : ${data.customer})\n`;
    for(let perf of data.performances){
        result += `${perf.play.name} : ${usd(perf.amount)} (${perf.audience}석)\n`;
    }
    result += `총액: ${usd(data.totalAmount)}\n`; 
    result += `적립 포인트: ${data.totalVolumeCredits}점\n`;
    return result;
}

function usd(aNumber){
    return new Intl.NumberFormat("en-US", {
        style: "currency",
        currency: "USD",
        minimumFractionDigits: 2
    }).format(aNumber/100); 
}
//statement.js 파일이라 가정 끝


//createStatementData.js 별도 파일이라 가정


class PerformanceCalculator{
    constructor(aPerformance, aPlay){
        this.performance = aPerformance;
        this.play = aPlay;
    }
    get amount(){
        return new Error('서브클래스에서 처리하도록 설계되었습니다.');
    }
    get volumeCredits(){
        return Math.max(this.performance.audience -30,0);
    }
}
class TragedyCalcultor extends PerformanceCalculator{
    get amount(){
        let result = 0;
        result = 40000;
        if(this.performance.audience > 30){
            result += 1000 * (this.performance.audience - 30);
        }
        return result;
    }
}
class ComedyCalcultor extends PerformanceCalculator{
    get amount(){
        let result = 0;
        result = 30000;
        if(this.performance.audience > 30){
            result += 10000 + 500 * (this.performance.audience - 20);
        }
        result += 300 * this.performance.audience;
        return result;
    }
    get volumeCredits(){
        return super.volumeCredits + Math.floor(this.performance.audience/5);
    }
}

function createPerformanceCalculator(aPerformance, aPlay){
    switch(aPlay.type){
        case "tragedy" : return new TragedyCalcultor(aPerformance,aPlay);
        case "comedy" : return new ComedyCalcultor(aPerformance,aPlay);
        default: throw new Error(`알 수  없는 장르 : ${aPlay.type}`);
    }
}

function createStatementData(invoice, plays) {
    const statementData = {};
    statementData.customer = invoice.customer;
    statementData.performances = invoice.performances.map(enrichPerformance);
    statementData.totalAmount = totalAmount(statementData);
    statementData.totalVolumeCredits = totalVolumeCredits(statementData);
    return statementData; 

    function enrichPerformance(aPerformance){
        const calculator = createPerformanceCalculator(aPerformance, 
            playFor(aPerformance)); //생성자 대신 팩토리 함수 이용
        const result = Object.assign({}, aPerformance);
        result.play = calculator.play;
        result.amount = calculator.amount;
        result.volumeCredits = calculator.volumeCredits;

        return result;
    }
    function playFor(aPerformance){ 
        return plays[aPerformance.playID];
    }
    
    function totalAmount(data){ 
        return data.performances 
            .reduce((total, p) => total + p.amount, 0 );
    }
    function totalVolumeCredits(data){
        return data.performances 
            .reduce((total, p)=>total+p.volumeCredits, 0);
    }
}

//createStatementData.js 별도 파일이라 가정 끝

console.log(statement(invoices,plays));
document.write(htmlStatement(invoices,plays));
</script>
</body>
</html>

 

 

 

 

'IT책, 강의 > 리팩터링' 카테고리의 다른 글

03 - 코드에서 나는 악취  (0) 2023.07.10
02 - 리팩터링 원칙 - 02  (0) 2023.07.08
02 - 리팩터링 원칙 - 01  (0) 2023.07.06
01 - 리팩터링: 첫 번째 예시 - 01  (0) 2023.07.02
00 - 들어가며  (0) 2023.07.01
 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

 

이 책은 개발자를 대상으로 쓰여진 책이기 때문에 흥미를 위해 첫 장부터 리팩터링을 수행하는 방식으로 진행한다.

첫 장부터 리팩터링이 나오니 그 과정을 전부 파악할 순 없다. 그냥 진행되는 과정에 감을 잡는 정도면 충분하다.

 

예제

https://github.com/rkwhr0010/refactoring/tree/main/refactoring/chap01

 

GitHub - rkwhr0010/refactoring: 리팩터링책 내용

리팩터링책 내용. Contribute to rkwhr0010/refactoring development by creating an account on GitHub.

github.com

리팩터링 과정마다 기존 코드를 커밋&푸시해 갱신하는 방식이 아닌 Ex01 ==> Ex02 이렇게 갱신하는 방식으로 따라했다.

 

폴더 구조는 해당 목차에 리팩터링이 없다면, 건너뛴다.

1.1 자, 시작해보자!

	
	plays = {
	    "hamlet":{"name" : "Hamlet", "type" : "tragedy"},
	    "as-like" : {"name":"As You Like It", "type" : "comedy"},
	    "othello":{"name":"Othello", "type":"tragedy"}
	}
	
	invoices = 
	    {
	        "customer" : "BigCo" ,
	        "performances" : [
	            {
	                "playID":"hamlet",
	                "audience" : 55
	            },
	            {
	                "playID" :"as-like",
	                "audience": 35
	            },
	            {
	                "playID":"othello",
	                "audience" :40
	                
	            }
	        ]
	    }
	
	
	<!DOCTYPE html>
	<html lang="en">
	<head>
	    <meta charset="UTF-8">
	    <meta http-equiv="X-UA-Compatible" content="IE=edge">
	    <meta name="viewport" content="width=device-width, initial-scale=1.0">
	    <title>Document</title>
	</head>
	<body>
	    <script src="plays.json" type="text/javascript"></script>
	    <script src="invoices.json" type="text/javascript"></script>
	<script>
	    //기본 함수
	    function statement(invoice, plays) {
	        let totalAmount = 0;
	        let volumeCredits = 0;
	        let result = `청구 내역 (고객명 : ${invoice.customer})\n`;
	        const format = new Intl.NumberFormat("en-US", {
	            style: "currency",
	            currency: "USD",
	            minimumFractionDigits: 2
	        }).format;
	        for(let perf of invoice.performances){
	            const play = plays[perf.playID];
	            let thisAmount = 0;
	            switch(play.type){
	                case "tragedy" : //비극
	                thisAmount = 40000;
	                if(perf.audience > 30){
	                    thisAmount += 1000 * (perf.audience - 30);
	                }
	                break;
	                case "comedy" : //희극
	                thisAmount = 30000;
	                if(perf.audience > 30){
	                    thisAmount += 10000 + 500 * (perf.audience - 20);
	                }
	                thisAmount += 300 * perf.audience;
	                break;
	                default: throw new Error(`알수 없는 장르: ${play.type}`);
	            }
	            //포인트를 적립한다.
	            volumeCredits += Math.max(perf.audience -30,0);
	            //희극 관객 5명 마다 추가 포인트를 제공한다.
	            if("comedy" === play.type) volumeCredits += Math.floor(perf.audience/5);
	            //청구 내역을 출력한다.
	            result += `${play.name} : ${format(thisAmount/100)} (${perf.audience}석)\n`;
	            totalAmount += thisAmount;
	        }
	        result += `총액: ${format(totalAmount/100)}\n`;
	        result += `적립 포인트: ${volumeCredits}점\n`;
	        return result;
	    }
	    console.log(statement(invoices,plays))
	</script>
	</body>
</html>

청구 내역 (고객명 : BigCo)
Hamlet : $650.00 (55석)
As You Like It : $580.00 (35석)
Othello : $500.00 (40석)
총액: $1,730.00
적립 포인트: 47점

공연료 관련 프로그램, 예제 코드 출력 시 위와 같은 결과 출력

 

1.2 예시 프로그램을 본소감

프로그램이 작동하는 상황에서 그저 코드가 "지저분하다" 이유로 불평하는 것은 프로그램 구조를 너무 미적인 기준으로만 판단하는 걸까?

컴파일러는 코드가 이쁘건 말건 상관없다. 하지만 그 코드를 수정하는 사람은 미적인 상태에 민감하다

설계가 나쁘면 수정이 어렵다. 수정할 부분을 특정하기도 힘들고 그과정에서 실수를 하기 쉽상이다.

 

프로그램이 새로운 기능을 추가하기에 편한 구조가 아니라면, 먼저 기능을 추가하기 쉬운 형태로 리팩터링하고 나서 원하는 기능을 추가한다

HTML 출력하는 기능을 추가해야한다고 가정하자

HTML 태그를 전부 넣어야되니 함수 복잡도가 증가하기에 기존 함수를 복사해서  HTML버전을 만드는 것이 일반적일 것이다. 이러면 중복코드도 발생할 것이고, statement()함수 수정이 생기면 HTML버전도 함께 수정해야 한다.

 

리팩터링이 필요한 이유는 이러한 변경 때문이다.

리팩터링은 지금 코드가 절대 변경될 없고, 동작한다면 굳이 필요 없다.

 

 

1.3 리팩터링의 첫 단계

리팩터링 단계는 항상 같다. 리팩터링할 코드 영역을 꼼꼼하게 검사해줄 테스트 코드부터 마련한다.

이유는 리팩터링 기법들의 버그 발생 여지를 최소화하기 위함이다.

리팩터링 또한 코드를 변경하는 행위로 버그가 충분히 스며들 가능성이 있다,

변경 때마다 테스트 코드를 돌린다

테스트 코드는 작성하는데 시간이 걸리지만, 나중에 디버깅 시간이 줄어 결과적으로 전체 작업 시간이 감소된다.

 

 

1.4 statement()함수 쪼개기

함수를 리팩터링할 때는 먼저 전체 동작을 각각의 부분으로 나눌 있는 지점을 찾는다.

 

switch(play.type){
    case "tragedy" : //비극
    thisAmount = 40000;
    if(perf.audience > 30){
        thisAmount += 1000 * (perf.audience - 30);
    }
    break;
    case "comedy" : //희극
    thisAmount = 30000;
    if(perf.audience > 30){
        thisAmount += 10000 + 500 * (perf.audience - 20);
    }
    thisAmount += 300 * perf.audience;
    break;
    default: throw new Error(`알수 없는 장르: ${play.type}`);
}

switch문을 보면, 공연에 대한 요금을 계산하고 있다.

이러한 사실은 코드를 분석해서 얻은 정보다. 이런 식으로 파악한 정보는 휘발성이 강해 다음에 코드를 다시 본다면 까먹는 경우가 많다.

 

코드를 별도 함수로 추출해 명확한 이름을 붙이자. amountFor()

추가로 함수 내부에서 쓰이는 변수, 파라미터 이름도 명확하게 변경한다.

컴퓨터가 이해하는 코드는 바보도 작성할 수 있다. 사람이 이해하도록 작성하는 프로그래머가 진정한 실력자다.

좋은 코드라면 의도가 명확히 표현돼야 한다. 요즘 IDE 이름변경이 매우 쉬우므로 좋은 이름으로 바꾸길 주저하지 말자

//기본 함수
function statement(invoice, plays) {
    let totalAmount = 0;
    let volumeCredits = 0;
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;
    const format = new Intl.NumberFormat("en-US", {
        style: "currency",
        currency: "USD",
        minimumFractionDigits: 2
    }).format;

    for(let perf of invoice.performances){
        const play = plays[perf.playID];
        
        let thisAmount = amountFor(perf, play);//함수로 추출했다.

        //포인트를 적립한다.
        volumeCredits += Math.max(perf.audience -30,0);
        //희극 관객 5명 마다 추가 포인트를 제공한다.
        if("comedy" === play.type) volumeCredits += Math.floor(perf.audience/5);

        //청구 내역을 출력한다.
        result += `${play.name} : ${format(thisAmount/100)} (${perf.audience}석)\n`;
        totalAmount += thisAmount;
    }
    result += `총액: ${format(totalAmount/100)}\n`;
    result += `적립 포인트: ${volumeCredits}점\n`;

    return result;

    function amountFor(aPerformance, play) {//명확한 이름으로 변경
        let result = 0;

        switch(play.type){
            case "tragedy" : //비극
            result = 40000;
            if(aPerformance.audience > 30){
                result += 1000 * (aPerformance.audience - 30);
            }
            break;
            case "comedy" : //희극
            result = 30000;
            if(aPerformance.audience > 30){
                result += 10000 + 500 * (aPerformance.audience - 20);
            }
            result += 300 * aPerformance.audience;
            break;
            default: throw new Error(`알수 없는 장르: ${play.type}`);
        }
        return result; 
    }
}

 

play 변수 제거하기

aPerformance 루프 변수에서 오기에 순회마다 값이 변경된다.

반면에 play 개별공연(aPerformance)에서 얻기 때문에 매개변수로 전달할 필요가 없다.

amountFor() 안에서 다시 계산하면 된다.

이런 식으로 함수를 쪼갤 때마다 불필요한 매개변수를 최대한 제거한다.

이러한 임시 변수들 때문에 로컬 범위에 존재하는 이름이 많아져 추출 작업이 복잡해진다.

 for(let perf of invoice.performances){
        const play = playFor(perf); // 우변을 함수로 추출
... 

function playFor(aPerformance){
    return plays[aPerformance.playID];
}

정상동작 테스트함 다음 과정 변수 인라인하기

 

for(let perf of invoice.performances){
    // const play = playFor(perf); //변수 인라인하기
    let thisAmount = amountFor(perf, playFor(perf));
    //포인트를 적립한다.
    volumeCredits += Math.max(perf.audience -30,0);
    //희극 관객 5명 마다 추가 포인트를 제공한다.
    if("comedy" === playFor(perf).type) volumeCredits += Math.floor(perf.audience/5);
    //청구 내역을 출력한다.
    result += `${playFor(perf).name} : ${format(thisAmount/100)} (${perf.audience}석)\n`;
    totalAmount += thisAmount;
}

amountFor()함수에서 playFor()함수로 인해 매개변수를 하나 줄일 있게 됐다.

    function amountFor(aPerformance/*, play*/) {//필요없어진 매개변수제거
        let result = 0;
    
        switch(playFor(aPerformance).type){
            case "tragedy" : //비극
            result = 40000;
            if(aPerformance.audience > 30){
                result += 1000 * (aPerformance.audience - 30);
            }
            break;
            case "comedy" : //희극
            result = 30000;
            if(aPerformance.audience > 30){
                result += 10000 + 500 * (aPerformance.audience - 20);
            }
            result += 300 * aPerformance.audience;
            break;
            default: throw new Error(`알수 없는 장르: ${playFor(aPerformance).type}`);
        }
        return result; 
    }

지역 변수를 제거해서 함수 추출하기가 훨씬 쉬워졌다.

이유는 유효범위를 신경써야할 대상이 줄었기 때문이다.

추출 작업 전에 거의 항상 지역 변수부터 제거하도록 하자

 

amountFor()함수 파라미터는 정리했으니 statement() 보자

임시 변수인 thisAmount 선언되고 값이 변하지 않는다.

따라서 "변수 인라인하기" 적용한다.

 

	statement()속…
	
	    for(let perf of invoice.performances){
        	//let thisAmount = amountFor(perf); //thisAmount변수 인라인
	        //포인트를 적립한다.
	        volumeCredits += Math.max(perf.audience -30,0);
	        //희극 관객 5명 마다 추가 포인트를 제공한다.
	        if("comedy" === playFor(perf).type) volumeCredits += Math.floor(perf.audience/5);
	        //청구 내역을 출력한다.
	        result += `${playFor(perf).name} : ${format(amountFor(perf)/100)} (${perf.audience}석)\n`;
	        totalAmount += amountFor(perf);//thisAmount변수 인라인
	    }

 

적립 포인트 계산 코드 추출하기

play 변수를 제거한 결과 로컬 유효범위의 변수가 하나 줄어서 적립 포인트 계산 부분을 추출하기가 쉬워졌다.

 

	function statement(invoice, plays) {
	    let totalAmount = 0;
	    let volumeCredits = 0;
	    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;
	    const format = new Intl.NumberFormat("en-US", {
	        style: "currency",
	        currency: "USD",
	        minimumFractionDigits: 2
	    }).format;
	    for(let perf of invoice.performances){
	        //포인트를 적립한다.
	        volumeCredits += Math.max(perf.audience -30,0);
	        //희극 관객 5명 마다 추가 포인트를 제공한다.
	        if("comedy" === playFor(perf).type) volumeCredits += Math.floor(perf.audience/5);
	        //청구 내역을 출력한다.
	        result += `${playFor(perf).name} : ${format(amountFor(perf)/100)} (${perf.audience}석)\n`;
	        totalAmount += amountFor(perf);//thisAmount변수 인라인
	    }
	    result += `총액: ${format(totalAmount/100)}\n`;
	    result += `적립 포인트: ${volumeCredits}점\n`;
	    
	    return result;
}

처리해야 변수 perf, volumeCredits

volumeCredits처리
//함수 추출
function volumeCreditsFor(perf){
    let volumeCredits = 0;
    volumeCredits += Math.max(perf.audience -30,0);
    if("comedy" === playFor(perf).type) 
        volumeCredits += Math.floor(perf.audience/5);
    return volumeCredits;
}

 ...............

    for(let perf of invoice.performances){
        volumeCredits += volumeCreditsFor(perf);//추출한 함수를 이용해 값을 누적
        result += `${playFor(perf).name} : ${format(amountFor(perf)/100)} (${perf.audience}석)\n`;
        totalAmount += amountFor(perf);
    }

테스트 동작 확인 이제 적절한 이름으로
...............
function volumeCreditsFor(aPerformance){
    let volumeCredits = 0;
    volumeCredits += Math.max(aPerformance.audience -30,0);
    if("comedy" === playFor(aPerformance).type) 
        volumeCredits += Math.floor(aPerformance.audience/5);
    return volumeCredits;
}

 

format 변수 제거하기

임시 변수는 나중에 문제를 일으킬 있다.

임시 변수는 자신이 속한 루틴에서만 의미가 있어서 루틴이 길고 복잡해지기 쉽다.

 

임시 변수를 제거하는 것이 다음 리팩터링

 

리팩토링 대상코드

const format = new Intl.NumberFormat("en-US", {
    style: "currency",
    currency: "USD",
    minimumFractionDigits: 2
}).format;

format 임시 변수에 함수를 대입한 형태다.(함수 포인터처럼)

직접 함수를 선언해 사용하도록 바꾼다.

function statement(invoice, plays) {
    let totalAmount = 0;
    let volumeCredits = 0;
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;
    for(let perf of invoice.performances){
        volumeCredits += volumeCreditsFor(perf);
        result += `${playFor(perf).name} : ${format(amountFor(perf)/100)} (${perf.audience}석)\n`;
        totalAmount += amountFor(perf);
    }
    result += `총액: ${format(totalAmount/100)}\n`;//임시 변수였던 format을 함수 호출로 대체
    result += `적립 포인트: ${volumeCredits}점\n`;

    return result;
}
function format(aNumber){
    return new Intl.NumberFormat("en-US", {
        style: "currency",
        currency: "USD",
        minimumFractionDigits: 2
    }).format(aNumber);
}

format만으로 함수가 하는 일을 충분히 설명하지 못한다. 함수에 핵심 기능에 걸맞는 이름을 짓는다.(화폐 단위 맞추기)

    for(let perf of invoice.performances){
        volumeCredits += volumeCreditsFor(perf);
        result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
        totalAmount += amountFor(perf);
    }
    result += `총액: ${usd(totalAmount)}\n`;
    result += `적립 포인트: ${volumeCredits}점\n`;

    return result;
}
function usd(aNumber){
    return new Intl.NumberFormat("en-US", {
        style: "currency",
        currency: "USD",
        minimumFractionDigits: 2
    }).format(aNumber/100); // 단위 변환 로직도 이 함수로 안으로 이동
}

함수를 작게 쪼개는 리팩터링은 이름을 잘지어야만 효과가 있다.

이름이 좋으면 함수 본문을 읽지 않고도 무슨 일을 하는지 있다.

 

이름 바꾸기는 쉬우므로 이름 짓기를 주저하지 말고 짓자. 좋은 이름이 떠올르면 다시 변경하면 그만이다.

 

 

volumeCredits 변수 제거하기

변수는 반복문을 바퀴 때마다 값을 누적하기 때문에 리팩터링하기가 까다롭다.

function statement(invoice, plays) {
    let totalAmount = 0;
    let volumeCredits = 0;
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;
    for(let perf of invoice.performances){
        volumeCredits += volumeCreditsFor(perf);
        result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
        totalAmount += amountFor(perf);
    }
    result += `총액: ${usd(totalAmount)}\n`;
    result += `적립 포인트: ${volumeCredits}점\n`;

    return result;
}

먼저 반복문 쪼개기로 volumeCredits 값이 누적되는 부분을 따로 빼낸다.

위를 아래처럼

function statement(invoice, plays) {
    let totalAmount = 0;
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;

    for(let perf of invoice.performances){
        //청구 내역을 출력한다.
        result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
        totalAmount += amountFor(perf);
    }
    let volumeCredits = 0;// 변수 선언을 반복문 앞으로 옮김
    for(let perf of invoice.performances){ // 값 누적 로직을 별도  for문으로 분리
        volumeCredits += volumeCreditsFor(perf);
    }
    result += `총액: ${usd(totalAmount)}\n`;
    result += `적립 포인트: ${volumeCredits}점\n`;

    return result;
}

이렇게 분리하면 임시 변수를 질의 함수로 바꾸기가 수월해진다.

함수로 추출한 것으로 변수 인라인을 한다.

    for(let perf of invoice.performances){
        //청구 내역을 출력한다.
        result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
        totalAmount += amountFor(perf);
    }
    result += `총액: ${usd(totalAmount)}\n`;
    result += `적립 포인트: ${totalVolumeCredits()}점\n`;
return result;

반복문이 중복되는 것을 꺼리는 이들이 많다.

이정도 중복은 성능에 미치는 영향이 미미할 때가 많다.

 

실제로 리펙터링한 결과를 비교해도 실행 시간 차이가 거의 없다.

 

경험 많은 프로그래머조차 코드의 실제 성능을 정확히 예측하지 못한다.

이유는 똑똑한 컴파일러들은 최신 캐싱 기법 등으로 무장하고 있어서 우리의 직관을 초월하는 결과를 내어주기 때문이다.

또한 소프트웨어 성능은 대체로 코드의 몇몇 작은 부분에 의해 결정되므로 외의 부분은 수정한다고 해도 성능 차이를 체감할 없다.

 

때로는 리팩터링이 성능에 상당한 영향을 주기도 한다. 그런 경우라도 개의치 않고 리팩터링한다.

다음어진 코드라야 성능 개선 작업도 훨씬 수월하기 때문이다.

추가로 시간내어 작업을 한다면 결과적으로 깔끔하고, 빠른 코드를 얻게 된다.

 

따라서 리팩터링으로 인한 성능 문제에 대한 저자의 조언은

"특별한 경우가 아니라면 일단 무시하라"

리팩터링 성능이 떨어진다면, 그때 성능 개선하자

 

리팩터링 중간에 테스트가 실패하고 원인을 바로 찾기 못하면 가장 최근 커밋으로 돌아가서 리팩터링의 단계를 작게 나눠서 다시 시도할 있다.

 

코드가 복잡할 수록 단계를 작게 나누면 작업 속도가 빨라지기 때문이다.

 

totalAmount 앞에 과정과 동일하게 제거한다.

반복문 쪼개고, 변수 초기화 문장을 옮기고, 함수를 추출,

	    for(let perf of invoice.performances){
	        //청구 내역을 출력한다.
	        result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
	    }
	    let totalAmount = tmpFunction();
	    function tmpFunction(){ //함수 추출/ 이름은 임시로
	        let totalAmount = 0;
	        for(let perf of invoice.performances){
	            totalAmount += amountFor(perf);
	        }
	        return totalAmount;
	    }
	
	…..
	
	
	    function totalAount(){ 
	        let result = 0; // 함수 안 변수이름도 자기 스타일에 맞게 변경
	        for(let perf of invoice.performances){
	            result += amountFor(perf);
	        }
	        return result;
	    }
	    result += `총액: ${usd(totalAount())}\n`; //함수 인라인 후 의미있는 이름으로 변경하기
	    result += `적립 포인트: ${totalVolumeCredits()}점\n`;

 

1.5 중간 점검: 난무하는 중첩 함수

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
</head>

<body>
<script src="plays.json" type="text/javascript"></script>
<script src="invoices.json" type="text/javascript"></script>
<script>

function statement(invoice, plays) {
    let result = `청구 내역 (고객명 : ${invoice.customer})\n`;
    for(let perf of invoice.performances){
        result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
    }
    result += `총액: ${usd(totalAount())}\n`; 
    result += `적립 포인트: ${totalVolumeCredits()}점\n`;
    return result;

    //중첩 함수 시작
    function totalAount(){ 
        let result = 0;
        for(let perf of invoice.performances){
            result += amountFor(perf);
        }
        return result;
    }

    function totalVolumeCredits(){
        let volumeCredits = 0;
        for(let perf of invoice.performances){ 
            volumeCredits += volumeCreditsFor(perf);
        }
        return volumeCredits;
    }
    function usd(aNumber){
        return new Intl.NumberFormat("en-US", {
            style: "currency",
            currency: "USD",
            minimumFractionDigits: 2
        }).format(aNumber/100); 
    }
    function volumeCreditsFor(aPerformance){
        let volumeCredits = 0;
        volumeCredits += Math.max(aPerformance.audience -30,0);
        if("comedy" === playFor(aPerformance).type) 
            volumeCredits += Math.floor(aPerformance.audience/5);
        return volumeCredits;
    }
    function playFor(aPerformance){
        return plays[aPerformance.playID];
    }
    function amountFor(aPerformance) {
        let result = 0;
        switch(playFor(aPerformance).type){
            case "tragedy" : 
            result = 40000;
            if(aPerformance.audience > 30){
                result += 1000 * (aPerformance.audience - 30);
            }
            break;
            case "comedy" : 
            result = 30000;
            if(aPerformance.audience > 30){
                result += 10000 + 500 * (aPerformance.audience - 20);
            }
            result += 300 * aPerformance.audience;
            break;
            default: throw new Error(`알수 없는 장르: ${playFor(aPerformance).type}`);
        }
        return result; 
    }
    
}

console.log(statement(invoices,plays));
</script>

</body>

</html>

최상위 statement()함수는 이제 일곱줄 뿐이며, 출력할 문장 생성 역할만 한다.

 

계산 로직은 여러 보조 함수로 빼냈다.

전체적인 흐름을 이해하기가 훨씬 쉬워졌다.

 

 

 

 

 

 

 
 

<!DOCTYPE html>
<html lang="ko">

<헤드>
<메타 문자셋="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>문서</title>
</헤드>

<몸>
<script src="plays.json" type="text/javascript"></script>
<script src="invoices.json" type="text/javascript"></script>
<스크립트>

기능문(인보이스, 재생) {
let result = ` 청구 내역 (고객명 : ${invoice.customer})\n`;
for(invoice.performances의 let perf){
result += `${playFor(perf).name} : ${usd(amountFor(perf))} (${perf.audience}석)\n`;
}
result += `전체: ${usd(totalAount())}\n`;
result += `적립 포인트: ${totalVolumeCredits()}점\n`;
반환 결과;

//중첩 맛있게 시작
함수 totalAount(){
let 결과 = 0;
for(invoice.performances의 let perf){
결과 += amountFor(perf);
}
반환 결과;
}

함수 totalVolumeCredits(){
let volumeCredits = 0;
for(invoice.performances의 let perf){
volumeCredits += volumeCreditsFor(perf);
}
반환 volumeCredits;
}
함수 usd(숫자){
새로운 Intl.NumberFormat("en-US", {를 반환합니다.
스타일: "통화",
통화: "USD",
minimumFractionDigits: 2
}).format(숫자/100);
}
기능 volumeCreditsFor(aPerformance){
let volumeCredits = 0;
volumeCredits += Math.max(aPerformance.audience -30,0);
if("코미디" === playFor(aPerformance).type)
volumeCredits += Math.floor(aPerformance.audience/5);
반환 volumeCredits;
}
함수 playFor(aPerformance){
반환 재생[aPerformance.playID];
}
함수 amountFor(aPerformance) {
let 결과 = 0;
switch(playFor(aPerformance).type){
사례 "비극":
결과 = 40000;
if(aPerformance.audience > 30){
결과 += 1000 * (aPerformance.audience - 30);
}
부서지다;
케이스 "코미디":
결과 = 30000;
if(aPerformance.audience > 30){
결과 += 10000 + 500 * (aPerformance.audience - 20);
}
결과 += 300 * aPerformance.audience;
부서지다;
default: throw new Error(`알수없는 장르: ${playFor(aPerformance).type}`);
}
반환 결과;
}

}

console.log(statement(invoices,plays));
</스크립트>

</body>

</html>

 

'IT책, 강의 > 리팩터링' 카테고리의 다른 글

03 - 코드에서 나는 악취  (0) 2023.07.10
02 - 리팩터링 원칙 - 02  (0) 2023.07.08
02 - 리팩터링 원칙 - 01  (0) 2023.07.06
01 - 리팩터링: 첫 번째 예시 - 02  (0) 2023.07.04
00 - 들어가며  (0) 2023.07.01
 

리팩터링 | 마틴 파울러 | 한빛미디어- 교보ebook

코드 구조를 체계적으로 개선하여 효율적인 리팩터링 구현하기, 20여 년 만에 다시 돌아온 마틴 파울러의 리팩터링 2판 리팩터링 1판은 1999년 출간되었으며, 한국어판은 2002년 한국에 소개되었다

ebook-product.kyobobook.co.kr

 

리팩터링이란?

겉으로 드러나는 코드의 기능은 바꾸지 않으면서 내부 구조를 개선하는 방식으로 소프트웨어 시스템을 수정하는 과정

 

기존 방식은 설계부터 하고 코드를 작성하는 방식

이 방식은 시간이 지남에 따라 요구사항에 따른 변경이 생기면서 구조가 깨진다.

 

리팩터링은 위 방식의 반대로 처음부터 완벽한 설계를 갖추기보다 개발을 진행하면서 지속적으로 설계한다.

 

대상

프로그래머를 대상으로 리팩터링 지침서, 따라서 프로덕션 코드를 접할 기회가 없으면, 예제만으로 자신을 것으로 만들기 힘들

 

다루는 내용

버그가 생기지 않으면서 효율적인 구조로 리팩터링하는 방식을 학습

 

사용 언어

ES6 기준, 자바스크립트를 사용한다.(큰 의미는 없음 접근성 때문)

자바스크립트에 깊은 문법은 사용하지 않음

클래스를 활용하는 모든 객체지향 언어에서 응용해서 사용할 수 있다.

 

책을 효율적으로 읽기

리팩터링이 뭔지 모를 , 1 읽기

리팩터링해야 이유를 알고 싶다,  1~2 읽기

리팩터링 찾고 싶다, 3 읽기

리팩터링을 실습하고 싶다, 1~4장까지 꼼꼼히 읽고, 나머지를 빠르게 훑어보기

 

 

 

 

 

 

 

 

헤드 퍼스트 디자인 패턴 | 에릭 프리먼 | 한빛미디어- 교보ebook

14가지 GoF 필살 패턴!, 경력과 세대를 넘어 오랫동안 객체지향 개발자의 성장을 도와준 디자인 패턴 교과서의 화려한 귀환! 》 2005년부터 디자인 패턴 도서 분야 부동의 1위 》 디자인 패턴의 고

ebook-product.kyobobook.co.kr

프록시는 접근을 제어하고 관리한다.

다양한 변형 프록시가 존재한다.

프록시 패턴의 정의

특정 객체로의 접근을 제어하는 대리인을 제공한다.

  • 원격 프록시를 써서 원격 객체로 접근 제어
  • 가상 프록시로 생성하기 힘든 자원으로 접근 제어
  • 보호 프록시로 접근 권한이 필요한 자원으로 접근 제어

 

자바는 java.lang.reflect 패키지를 제공한다. 이 패키지 기능을 사용하여 프록시 기능을 구현할 수 있다.

이렇게 만들어진 프록시는 런타임 중 생성되서, 동적 프록시(dynamic proxy)라 한다.

 

리플랙션 패키지를 사용하면 자바에서 Proxy 클래스를 생성해 주므로 필요한 정보만 전달해주면 된다.

InvocationHandler 가 핵심이다.

 

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.HashMap;

public class ProtectProxyTest {
	static interface Person{
		String getName();
		String getGender();
		String getInterests();
		int getGeekRating();
		
		void setName(String name);
		void setGender(String gender);
		void setInterests(String interests);
		void setGeekRating(int rating);
	}
	
	static class PersonImpl implements Person{
		String name;
		String gender;
		String interests;
		int rating;
		int ratingCount = 0;
		
		public String getName() {
			return name;
		}
		public void setName(String name) {
			this.name = name;
		}
		public String getGender() {
			return gender;
		}
		@Override
		public int getGeekRating() {
			if(ratingCount == 0 ) return 0;
			return (rating/ratingCount);
		}
		public void setGender(String gender) {
			this.gender = gender;
		}
		public String getInterests() {
			return interests;
		}
		public void setInterests(String interests) {
			this.interests = interests;
		}
		
		@Override
		public void setGeekRating(int rating) {
			this.rating = rating;
			ratingCount++;
		}
		
	}
	static class OwnerInvocationHandler implements InvocationHandler{
		Person person;
		
		public OwnerInvocationHandler(Person person) {
			this.person = person;
		}
		@Override
		//proxy객체참조, method 객체가 호출한 메서드 정보, args 메서드 인자정보
		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			try {
				if(method.getName().startsWith("get")) {
					return method.invoke(person, args);
				}else if(method.getName().startsWith("setGeekRating")) {
					//나한테 평가는 불가
					throw new IllegalAccessException();
				}else if(method.getName().startsWith("set")) {
					return method.invoke(person, args);
				}
			} catch (InvocationTargetException e) {
				e.printStackTrace();
			}
			return null;
		}
	}
	static class NonOwnerInvocationHandler implements InvocationHandler{
		Person person;
		
		public NonOwnerInvocationHandler(Person person) {
			this.person = person;
		}
		@Override
		public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
			try {
				if(method.getName().startsWith("get")) {
					return method.invoke(person, args);
				}else if(method.getName().startsWith("setGeekRating")) {
					return method.invoke(person, args);
				}else if(method.getName().startsWith("set")) {
					//내것 아니니까 수정 불가
					throw new IllegalAccessException();
				}
			} catch (InvocationTargetException e) {
				e.printStackTrace();
			}
			return null;
		}
	}
	
	static Person getOwnerProxy(Person person) {
		return (Person)Proxy.newProxyInstance(//정적 메서드
				person.getClass().getClassLoader(),//클래스 로더
				person.getClass().getInterfaces(),//프록시에서 구현할 모든 인터페이스
				new OwnerInvocationHandler(person));//InvocationHandler 구현한 클래스
	}
	static Person getNonOwnerProxy(Person person) {
		return (Person)Proxy.newProxyInstance(
				person.getClass().getClassLoader(),
				person.getClass().getInterfaces(),
				new NonOwnerInvocationHandler(person));
	}
	
	
	static Person getPersonFromDatabase(String name) {
		return (Person)datingDB.get(name);
	}
	
	static void initializeDatabase() {
		Person joe = new PersonImpl();
		joe.setName("김자바");
		joe.setInterests("자동차, 컴퓨터, 음악");
		joe.setGeekRating(7);
		datingDB.put(joe.getName(), joe);

		Person kelly = new PersonImpl();
		kelly.setName("박자바");
		kelly.setInterests("웹쇼핑, 영화, 음악");
		kelly.setGeekRating(6);
		datingDB.put(kelly.getName(), kelly);
	}
	
	static HashMap<String, Person> datingDB = new HashMap<String, Person>();
	
	public static void main(String[] args) {
		initializeDatabase();
		drive();
	}

	static void drive() {
		Person joe = getPersonFromDatabase("김자바");
		
		Person ownerProxy = getOwnerProxy(joe);//프록시 생성
		
		System.out.println("이름은 " + ownerProxy.getName());
		ownerProxy.setInterests("볼링, 바둑");
		System.out.println("본인 프록시에 관심 사항을 등록합니다.");
		try {
			ownerProxy.setGeekRating(10);
		} catch (Exception e) {
			System.out.println("본인 프록시에 괴짜 지수를 매길 수 없습니다.");
		}
		System.out.println("괴짜 지수 " + ownerProxy.getGeekRating());

		Person nonOwnerProxy = getNonOwnerProxy(joe);// 프록시 생성
		
		System.out.println("이름은 " + nonOwnerProxy.getName());
		try {
			nonOwnerProxy.setInterests("볼링, 바둑");
		} catch (Exception e) {
			System.out.println("타인 프록시에는 관심 사항을 등록할 수 없습니다.");
		}
		nonOwnerProxy.setGeekRating(3);
		System.out.println("타인 프록시에 괴짜 지수를 매깁니다.");
		System.out.println("괴짜 지수 " + nonOwnerProxy.getGeekRating());

	}

}
이름은 김자바
본인 프록시에 관심 사항을 등록합니다.
본인 프록시에 괴짜 지수를 매길 수 없습니다.
괴짜 지수 7
이름은 김자바
타인 프록시에는 관심 사항을 등록할 수 없습니다.
타인 프록시에 괴짜 지수를 매깁니다.
괴짜 지수 1

핵심 정리

프록시 패턴을 사용하면 객체에 대리인을 내세워서 클라이언트 접근을 제어할 수 있다.

원격 프록시는 클라이언트와 원격 객체 사이 데이터 전달을 관리

가상 프록시는 생성비용이 큰 객체 접근을 제어(지연로딩)

보호 프록시는 권한을 확인하여 객체 접근을 제어

이외 도 다양한 프록시 변형이 존재한다. 캐시 서버도 프록시 일종이다.

자바에는 동적 프록시 기능이 내장되어 있다. 

 

 

헤드 퍼스트 디자인 패턴 | 에릭 프리먼 | 한빛미디어- 교보ebook

14가지 GoF 필살 패턴!, 경력과 세대를 넘어 오랫동안 객체지향 개발자의 성장을 도와준 디자인 패턴 교과서의 화려한 귀환! 》 2005년부터 디자인 패턴 도서 분야 부동의 1위 》 디자인 패턴의 고

ebook-product.kyobobook.co.kr

전략 패턴과 상태 패턴은 용도가 다를 뿐 그 설계는 비슷하다.

상태 패턴은 내부 상태를 바꿔 객체가 행동을 바꿀 수 있도록 한다.

public class StateTest {
	static class GumballMachine{
		//상태 표현 상수
		final static int SOLD_OUT = 0;
		final static int NO_QUARTER = 1;
		final static int HAS_QUARTER = 2;
		final static int SOLD = 3;
		
		//현재 상태 변수
		int state  = SOLD_OUT;
		int count = 0 ;
		
		public GumballMachine(int count) {
			this.count= count;
			if(count>0) {
				state = NO_QUARTER;
			}
		}
		
		public void insertQuarter() {
			if(state == HAS_QUARTER) {
				System.out.println("동전은 한 개만 넣어 주세요.");
			}else if (state == NO_QUARTER) {
				state = HAS_QUARTER;
				System.out.println("동전을 넣으셨습니다.");
			}else if (state == SOLD_OUT) {
				System.out.println("매진되었습니다. 다음 기회에 이용해 주세요.");
			}else if (state == SOLD) {
				System.out.println("알맹이를 내보내고 있습니다.");
			}
		}
		
		public void ejectQuarter() {
			if(state == HAS_QUARTER) {
				System.out.println("동전이 반환됩니다.");
				state = NO_QUARTER;
			}else if (state == NO_QUARTER) {
				state = HAS_QUARTER;
				System.out.println("동전을 넣어 주세요");
			}else if (state == SOLD_OUT) {
				System.out.println("동전을 넣지 않으셨습니다. 동전이 반환되지 않습니다.");
			}else if (state == SOLD) {
				System.out.println("이미 알맹이를 뽑으셨습니다.");
			}
		}
		
		public void turnCrank() {
			if(state == SOLD) {
				System.out.println("손잡이는 한 번만 돌려 주세요.");
			}else if (state == NO_QUARTER) {
				System.out.println("동전을 넣어 주세요.");
			}else if (state == SOLD_OUT) {
				System.out.println("매진되었습니다.");
			}else if (state == HAS_QUARTER) {
				System.out.println("손잡이를 돌리셨습니다.");
				state = SOLD;
				dispense();
			}
		}
		public void dispense() {
			if(state == SOLD) {
				System.out.println("알맹이를 내보내고 있습니다.");
				if(0 == (count -= 1)) {
					System.out.println("더 이상 알맹이가 없습니다.");
					state = SOLD_OUT;
				} else {
					state = NO_QUARTER;
				}
			}else if (state == NO_QUARTER) {
				System.out.println("동전을 넣어 주세요.");
			}else if (state == SOLD_OUT) {
				System.out.println("매진입니다.");
			}else if (state == HAS_QUARTER) {
				System.out.println("알맹이를 내보낼 수 없습니다.");
			}
		}
		public void refill(int numGumBalls) {
			this.count = numGumBalls;
			state = NO_QUARTER;
		}
		public String toString() {
			StringBuffer result = new StringBuffer();
			result.append("\nMighty Gumball, Inc.");
			result.append("\nJava-enabled Standing Gumball Model #2004\n");
			result.append("Inventory: " + count + " gumball");
			if (count != 1) {
				result.append("s");
			}
			result.append("\n갬블머신 현재 상태 ");
			if (state == SOLD_OUT) {
				result.append("매진");
			} else if (state == NO_QUARTER) {
				result.append("동전 투입 대기 중");
			} else if (state == HAS_QUARTER) {
				result.append("손잡이 돌리기 대기 중");
			} else if (state == SOLD) {
				result.append("알맹이 배출 중");
			}
			result.append("\n");
			return result.toString();
		}
	}
	public static void main(String[] args) {
		GumballMachine gumballMachine = new GumballMachine(5);

		System.out.println(gumballMachine);

		gumballMachine.insertQuarter();
		gumballMachine.turnCrank();

		System.out.println(gumballMachine);

		gumballMachine.insertQuarter();
		gumballMachine.ejectQuarter();
		gumballMachine.turnCrank();

		System.out.println(gumballMachine);

		gumballMachine.insertQuarter();
		gumballMachine.turnCrank();
		gumballMachine.insertQuarter();
		gumballMachine.turnCrank();
		gumballMachine.ejectQuarter();

		System.out.println(gumballMachine);

		gumballMachine.insertQuarter();
		gumballMachine.insertQuarter();
		gumballMachine.turnCrank();
		gumballMachine.insertQuarter();
		gumballMachine.turnCrank();
		gumballMachine.insertQuarter();
		gumballMachine.turnCrank();

		System.out.println(gumballMachine);
	}
}
Mighty Gumball, Inc.
Java-enabled Standing Gumball Model #2004
Inventory: 5 gumballs
갬블머신 현재 상태 동전 투입 대기 중

동전을 넣으셨습니다.
손잡이를 돌리셨습니다.
알맹이를 내보내고 있습니다.

Mighty Gumball, Inc.
Java-enabled Standing Gumball Model #2004
Inventory: 4 gumballs
갬블머신 현재 상태 동전 투입 대기 중

동전을 넣으셨습니다.
동전이 반환됩니다.
동전을 넣어 주세요.

Mighty Gumball, Inc.
Java-enabled Standing Gumball Model #2004
Inventory: 4 gumballs
갬블머신 현재 상태 동전 투입 대기 중

동전을 넣으셨습니다.
손잡이를 돌리셨습니다.
알맹이를 내보내고 있습니다.
동전을 넣으셨습니다.
손잡이를 돌리셨습니다.
알맹이를 내보내고 있습니다.
동전을 넣어 주세요

Mighty Gumball, Inc.
Java-enabled Standing Gumball Model #2004
Inventory: 2 gumballs
갬블머신 현재 상태 손잡이 돌리기 대기 중

동전은 한 개만 넣어 주세요.
동전은 한 개만 넣어 주세요.
손잡이를 돌리셨습니다.
알맹이를 내보내고 있습니다.
동전을 넣으셨습니다.
손잡이를 돌리셨습니다.
알맹이를 내보내고 있습니다.
더 이상 알맹이가 없습니다.
매진되었습니다. 다음 기회에 이용해 주세요.
매진되었습니다.

Mighty Gumball, Inc.
Java-enabled Standing Gumball Model #2004
Inventory: 0 gumballs
갬블머신 현재 상태 매진

각 행동마다 상태를 전이하는 부분이 있다.

그리고 행동(메서드) 마다 상태별 경우의 수에 대한 코드가 있다.

public class StatePattern {
	static interface State {
		void insertQuarter();

		void ejectQuarter();

		void turnCrank();

		void dispense();

		void refill();
	}
	
	static abstract class BaseState implements State{
		GumballMachine gumballMachine;
		public BaseState(GumballMachine gumballMachine) {
			this.gumballMachine = gumballMachine;
		}
	}
	
	static class NoQuarterState extends BaseState{
		public NoQuarterState(GumballMachine gumballMachine) {
			super(gumballMachine);
		}
		public void insertQuarter() {
			System.out.println("동전을 넣으셨습니다.");
			gumballMachine.setState(gumballMachine.getHasQuarterState());
		}
		public void ejectQuarter() {
			System.out.println("동전을 넣어 주세요");
		}
		public void turnCrank() {
			System.out.println("동전을 넣어 주세요.");
		}
		public void dispense() {
			System.out.println("동전을 넣어 주세요.");
		}
		public void refill() {
		}
		@Override
		public String toString() {
			return "동전 기다리는 중";
		}
	}
	static class HasQuarterState extends BaseState{
		public HasQuarterState(GumballMachine gumballMachine) {
			super(gumballMachine);
		}
		public void insertQuarter() {
			System.out.println("동전은 한 개만 넣어 주세요.");
		}
		public void ejectQuarter() {
			System.out.println("동전이 반환됩니다.");
			gumballMachine.setState(gumballMachine.getNoQuarterState());
		}
		public void turnCrank() {
			System.out.println("손잡이를 돌리셨습니다.");
			gumballMachine.setState(gumballMachine.getSoldState());
		}
		public void dispense() {
			System.out.println("알맹이를 내보낼 수 없습니다.");
		}
		public void refill() {
		}
		@Override
		public String toString() {
			return "손잡이 돌기는 것 기다리는 중";
		}
	}
	static class SoldOutState extends BaseState{
		public SoldOutState(GumballMachine gumballMachine) {
			super(gumballMachine);
		}
		public void insertQuarter() {
			System.out.println("매진되었습니다. 다음 기회에 이용해 주세요.");
		}
		public void ejectQuarter() {
			System.out.println("매진되었습니다.");
		}
		public void turnCrank() {
			System.out.println("손잡이를 돌렸지만, 알맹이는 없습니다.");
		}
	 
		public void dispense() {
			System.out.println("알맹이를 내보낼 수 없습니다.");
		}
		
		public void refill() {
			gumballMachine.setState(gumballMachine.getNoQuarterState());
		}
		@Override
		public String toString() {
			return "매진";
		}
	}
	static class SoldState extends BaseState{
		public SoldState(GumballMachine gumballMachine) {
			super(gumballMachine);
		}
		public void insertQuarter() {
			System.out.println("알맹이를 내보내고 있습니다.");
		}
		public void ejectQuarter() {
			System.out.println("이미 알맹이를 뽑으셨습니다.");
		}
		public void turnCrank() {
			System.out.println("손잡이는 한 번만 돌려 주세요.");
		}
		public void dispense() {
		}
		public void refill() {
			gumballMachine.releaseBall();
			if (gumballMachine.getCount() > 0) {
				gumballMachine.setState(gumballMachine.getNoQuarterState());
			} else {
				System.out.println("이런, 알맹이가 다 떨어졌어요");
				gumballMachine.setState(gumballMachine.getSoldOutState());
			}
		}
		@Override
		public String toString() {
			return "알맹이 내보내는 중";
		}
	}
	
	
	static class GumballMachine{
		State state;
		int count = 0;
		
		final State soldOutState;
		final State noQuarterState;
		final State hasQuarterState;
		final State soldState;
		
		public GumballMachine(int numberGumballs) {
			soldOutState = new SoldOutState(this);
			noQuarterState = new NoQuarterState(this);
			hasQuarterState = new HasQuarterState(this);
			soldState = new SoldState(this);
			
			this.count = numberGumballs;
			state = numberGumballs > 0 ? noQuarterState : soldOutState;
		}
		void setState(State state) {
			this.state = state;
		}
	    public State getState() {
	        return state;
	    }

	    public State getSoldOutState() {
	        return soldOutState;
	    }

	    public State getNoQuarterState() {
	        return noQuarterState;
	    }

	    public State getHasQuarterState() {
	        return hasQuarterState;
	    }

	    public State getSoldState() {
	        return soldState;
	    }
		
		public void insertQuarter() {
			state.insertQuarter();
		}
		
		public void ejectQuarter() {
			state.ejectQuarter();
		}
		
		public void turnCrank() {
			state.turnCrank();
			state.dispense();
		}
		
		public void refill(int count) {
			this.count += count;
			System.out.println("겜볼 기계가 방금 리필되었습니다. 개수는 다음과 같습니다." + this.count);
			state.refill();
		}
		
		public void releaseBall() {
			System.out.println("알맹이가 슬롯에서 굴러 나옵니다....");
			if (count > 0) {
				count = count - 1;
			}
		}
		public int getCount() {
			return count;
		}
		
		public String toString() {
			StringBuffer result = new StringBuffer();
			result.append("\n겜블 머신_");
			result.append("\n자바 기반 Gumball Model #2004");
			result.append("\n항목: " + count + " 알맹이");
			if (count != 1) {
				result.append("들");
			}
			result.append("\n");
			result.append("머신 상태 :" + state + "\n");
			return result.toString();
		}
	}
	public static void main(String[] args) {
		GumballMachine gumballMachine = new GumballMachine(2);

		System.out.println(gumballMachine);

		gumballMachine.insertQuarter();
		gumballMachine.turnCrank();

		System.out.println(gumballMachine);

		gumballMachine.insertQuarter();
		gumballMachine.turnCrank();
		gumballMachine.insertQuarter();
		gumballMachine.turnCrank();
		
		gumballMachine.refill(5);
		gumballMachine.insertQuarter();
		gumballMachine.turnCrank();

		System.out.println(gumballMachine);
	}
}

상태 인터페이스로 추상화 다형성을 이용

각 상태의 행동을 별개의 클래스로 국지화

앞으로 생길 수 있는 새로운 상태에 대해 OCP를 준수할 수 있다.

 

상태 패턴의 정의

객체의 내부 상태가 바뀜에 따라서 객체의 행동을 바꿀 수 있다.

마치 객체의 클래스가 바뀌는 것과 같은 결과

 

상태 패턴과 전략 패턴은 비슷하나 용도가 다르다.

상태 패턴은 상태 객체에 일련 행동이 캡슐화, Context 객체에서 여러 상태 객체 중 한 객체에게 모든 행동을 위임한다.

클라이언트는 상태 객체를 몰라도 된다.

전략 패턴은 클라이언트가 Context에게 어떤 전략 객체를 사용할지를 지정한다. 주로 런타임 환경에서 객체를 변경할 수 있는 유연성을 제공하는 용도로 쓰인다.

 

상태 패턴은 객체에 수많은 조건문을 넣는 대신 상태 패턴을 사용한다고 생각하면 된다.

 

상태 패턴에서 상태 전환을 Context에서 해도, 상태 클래스에서 해도 상관없다. 다만, 상태 전환 코드를 상태  클래스에 넣으면 상태 클래스 간 의존성이 생긴다. 이 문제로 위 예제에선 겜블머신에 게터 메소드를 사용했다.

 

 

 

헤드 퍼스트 디자인 패턴 | 에릭 프리먼 | 한빛미디어- 교보ebook

14가지 GoF 필살 패턴!, 경력과 세대를 넘어 오랫동안 객체지향 개발자의 성장을 도와준 디자인 패턴 교과서의 화려한 귀환! 》 2005년부터 디자인 패턴 도서 분야 부동의 1위 》 디자인 패턴의 고

ebook-product.kyobobook.co.kr

서로 다는 방식으로 저장한 것을 공통으로 다루고 싶다.

import java.util.*;
import java.util.Map.Entry;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class IteratorTest {
	static Supplier<IntStream> supply(){
		return ()-> new Random().ints(10, 1, 100);
	}
	
	static class ClassArr<E> implements Iterable<E>{
		E[] dataArr;
		
		public ClassArr(E[] dataArr) {
			this.dataArr = (E[]) dataArr;
		}

		@Override
		public Iterator<E> iterator() {
			return new Iterator<E>() {
				int cursor = 0;
				public boolean hasNext() {
					return cursor < dataArr.length;
				}
				public E next() {
					return dataArr[cursor++];
				}
			};
		}
		
	}
	
	static class MapClass implements Iterable<Map.Entry<Object, List<Integer>>>{
		Map<Object, List<Integer>> map = new HashMap<>();

		public MapClass() {
			map= supply().get().boxed().collect(Collectors.groupingBy(n->n));
		}
		
		@Override
		public Iterator<Entry<Object, List<Integer>>> iterator() {
			return map.entrySet().iterator();
		}

	}

	
	static class ArrClass implements Iterable<Integer>{
		int[] intArr = supply().get().toArray();

		@Override
		public Iterator<Integer> iterator() {
			return new Iterator<Integer>() {
				int cursor = 0;
				public Integer next() {
					return intArr[cursor++];
				}
				public boolean hasNext() {
					return cursor<intArr.length;
				}
			};
		}
	}
	static class ListClass implements Iterable<Integer>{
		List<Integer> intList = supply().get().boxed().toList();
		@Override
		public Iterator<Integer> iterator() {
			return intList.iterator();
		}
		
	}
	
	public static void main(String[] args) {
		String[] test = {"배열은","향상된","반복문","사용가능"};
		//배열 향상 포문 가능
		for(String data : test) {
			System.out.print(data+" ");
		}
		System.out.println();
		//어떠한 클래스도 Iterable만 알맞게 구현하면 전부 향상 포문 가능하다.
		for(String data : new ClassArr<>(test)) {
			System.out.print(data+" ");
		}
		System.out.println();
		for(Integer val : new ArrClass()) {
			System.out.print(val+" ");
		}
		System.out.println();
		for(Integer val :new ListClass()) {
			System.out.print(val+" ");
		}
		System.out.println();
		for(Entry en : new MapClass()) {
			System.out.print(en + " ");
		}
	}
}
배열은 향상된 반복문 사용가능 
배열은 향상된 반복문 사용가능 
61 13 57 51 30 49 25 5 48 48 
34 91 69 72 70 1 18 5 26 55 
49=[49] 17=[17] 97=[97] 35=[35] 52=[52] 53=[53] 56=[56] 40=[40] 45=[45] 15=[15]

반복자 패턴 알아보기

반복자 패턴은 Iterator 인터페이스에 의존한다

인터페이스만 있으면 배열, 리스트, 해시테이블 모든 컬렉션을 순회할 있다.

반복자 패턴의 정의

집합체 내에서 어떤 식으로 일이 처리되는지 전혀 모르는 상태에서 안에 들어있는 모든 항목을 대상으로 반복 작업을 수행할 있다.

 

또한 서로 다른 종류의 집합체라도 Iterator 구현해 리턴하면 같은 방법으로 순회를 있다.

집합체는 요소 순회만 Iterator에게 위임하고, 내부 자료구조 관리만 잘하면 된다.

컴포지트 패턴의 정의

반복자 패턴만으론 처리하기 어려워 메뉴 관리에 도움되는 컴포지트 패턴 도임

객체를 트리구조로 구성해서 부분-전체 계층구조를 구현한다. 컴포지트 패턴을 사용하면 클라이언트에서 개별 객체와 복합 객체를 똑같은 방법으로 다룰 수 있다.

 

import java.util.ArrayList;
import java.util.List;

public class CompositeTest {
	//구성요소
	static abstract class MenuComponent{
		public void add(MenuComponent menuComponent) {
			throw new UnsupportedOperationException();
		}
		public void remove(MenuComponent menuComponent) {
			throw new UnsupportedOperationException();
		}
		public MenuComponent getChild(int i) {
			throw new UnsupportedOperationException();
		}
		public String getName() {
			throw new UnsupportedOperationException();
		}
		public String getDescription() {
			throw new UnsupportedOperationException();
		}
		public double getPrice() {
			throw new UnsupportedOperationException();
		}
		public boolean isVegetarian() {
			throw new UnsupportedOperationException();
		}
		public void print() {
			throw new UnsupportedOperationException();
		}
	}
	//잎
	static class MenuItem extends MenuComponent{
		String name;
		String description;
		boolean vegetarian;
		double price;
		
		public MenuItem(String name, String description, boolean vegetarian, double price) {
			this.name = name;
			this.description = description;
			this.vegetarian = vegetarian;
			this.price = price;
		}
		
		public String getName() {
			return name;
		}
		
		public String getDescription() {
			return description;
		}
		
		public boolean isVegetarian() {
			return vegetarian;
		}
		
		public double getPrice() {
			return price;
		}
		
		public void print() {
			System.out.print("  "+getName());
			if(isVegetarian()) {
				System.out.print("(v)");
			}
			System.out.println(", "+getPrice());
			System.out.println("    -- "+getDescription());
		}
		
	}
	//노드
	static class Menu extends MenuComponent{
		List<MenuComponent> menuComponents = new ArrayList<>();
		String name;
		String description;
		
		public Menu(String name, String description) {
			this.name = name;
			this.description = description;
		}
		
		public void add(MenuComponent menuComponent) {
			menuComponents.add(menuComponent);
		}
		public void remove(MenuComponent menuComponent) {
			menuComponents.remove(menuComponent);
		}
		public MenuComponent getChild(int i) {
			return menuComponents.get(i);
		}
		
		public String getName() {
			return name;
		}
		public String getDescription() {
			return description;
		}
		
		public void print() {
			System.out.print("\n"+getName());
			System.out.println(", "+getDescription());
			System.out.println("----------------------");
			
			for(MenuComponent menuComponent : menuComponents) {
				menuComponent.print();
			}
			
		}
	}
	
	static class Waitress{
		MenuComponent allmenus;

		public Waitress(MenuComponent allmenus) {
			this.allmenus = allmenus;
		}
		
		public void printMenu() {
			allmenus.print();
		}
	}
	
	
	public static void main(String[] args) {
		MenuComponent phMenu = new Menu("팬케이크 하우스 메뉴", "아침 메뉴");
		MenuComponent dinerMenu = new Menu("객체마을 식당 메뉴", "점심 메뉴");
		MenuComponent cafeMenu = new Menu("카페 메뉴", "저녁 메뉴");
		MenuComponent dessertMenu = new Menu("디저트 메뉴", "디저트를 즐겨 보에쇼");
		
		MenuComponent allMenu = new Menu("전체 메뉴", "전체 메뉴");
		
		allMenu.add(phMenu);
		allMenu.add(dinerMenu);
		allMenu.add(cafeMenu);
		
		phMenu.add(
				new MenuItem("K&B 팬케이크 아침정식"
				,"팬케이크에 스크램블에그와 토스트", true, 2.99));
		phMenu.add(
				new MenuItem("레귤러 팬케이크 아침정식"
				,"팬케이크에 계란 후라이와 소세지", false, 2.99));
		phMenu.add(
				new MenuItem("블루베리 팬케이크"
				,"팬케이크와 블루베리, 블루베리 시럽", true, 3.49));
		phMenu.add(
				new MenuItem("와플"
				,"와플과 블루베리 또는 딸기를 드립니다.", true, 3.59));
		
		dinerMenu.add(
				new MenuItem("채식주의자용 BLT"
				, "통밀 위에 콩고기 베이컨, 상추, 토마토를 얹은 메뉴", true, 2.99));
		
		dinerMenu.add(
				new MenuItem("BLT"
				, "통밀 위에 베이컨, 상추, 토마토를 얹은 메뉴", false, 2.99));
		dinerMenu.add(
				new MenuItem("오늘의 스프"
				, "감자 샐러드를 곁들인 오늘의 스프", true, 2.99));
		dinerMenu.add(
				new MenuItem("핫도그"
				, "사워크라우트, 갖은 양념, 양파, 치즈가 곁들여진 핫도그", true, 2.99));
		dinerMenu.add(
				new MenuItem("파스타"
				, "마리나라 소스 스파게티, 효모빵도 드립니다.", true, 3.89));
		dinerMenu.add(dessertMenu);
		
		
		dessertMenu.add(
				new MenuItem("애플 파이"
				, "바삭바삭한 크러스트에 바닐라 아이스크림이 얹혀 있는 애플 파이", true, 1.59));
		dessertMenu.add(
				new MenuItem("치즈케이크"
				, "초콜릿 그레이엄 크러스트 위에 부드러운 뉴욕 치즈케이크", true, 1.99));
		dessertMenu.add(
				new MenuItem("소르베"
				, "라스베리와 라임의 절묘한 조화", true, 1.89));
		
		cafeMenu.add(
				new MenuItem("베지 버거와 에어 프라이"
				, "통밀빵, 상추, 토마토, 감자 튀김이 첨가된 베지 버거", true, 1.59));
		cafeMenu.add(
				new MenuItem("오늘의 스프"
				, "통밀빵, 상추, 토마토, 감자 튀김이 첨가된 베지 버거", false, 0.69));
		cafeMenu.add(
				new MenuItem("투리토"
				, "통 핀토콩과 살사, 구아카몰이 곁들여진 푸짐한 부리토", true, 0.89));
		
		Waitress waitress = new Waitress(allMenu);
		waitress.printMenu();
	}
}
전체 메뉴, 전체 메뉴
----------------------

팬케이크 하우스 메뉴, 아침 메뉴
----------------------
  K&B 팬케이크 아침정식(v), 2.99
    -- 팬케이크에 스크램블에그와 토스트
  레귤러 팬케이크 아침정식, 2.99
    -- 팬케이크에 계란 후라이와 소세지
  블루베리 팬케이크(v), 3.49
    -- 팬케이크와 블루베리, 블루베리 시럽
  와플(v), 3.59
    -- 와플과 블루베리 또는 딸기를 드립니다.

객체마을 식당 메뉴, 점심 메뉴
----------------------
  채식주의자용 BLT(v), 2.99
    -- 통밀 위에 콩고기 베이컨, 상추, 토마토를 얹은 메뉴
  BLT, 2.99
    -- 통밀 위에 베이컨, 상추, 토마토를 얹은 메뉴
  오늘의 스프(v), 2.99
    -- 감자 샐러드를 곁들인 오늘의 스프
  핫도그(v), 2.99
    -- 사워크라우트, 갖은 양념, 양파, 치즈가 곁들여진 핫도그
  파스타(v), 3.89
    -- 마리나라 소스 스파게티, 효모빵도 드립니다.

디저트 메뉴, 디저트를 즐겨 보에쇼
----------------------
  애플 파이(v), 1.59
    -- 바삭바삭한 크러스트에 바닐라 아이스크림이 얹혀 있는 애플 파이
  치즈케이크(v), 1.99
    -- 초콜릿 그레이엄 크러스트 위에 부드러운 뉴욕 치즈케이크
  소르베(v), 1.89
    -- 라스베리와 라임의 절묘한 조화

카페 메뉴, 저녁 메뉴
----------------------
  베지 버거와 에어 프라이(v), 1.59
    -- 통밀빵, 상추, 토마토, 감자 튀김이 첨가된 베지 버거
  오늘의 스프, 0.69
    -- 통밀빵, 상추, 토마토, 감자 튀김이 첨가된 베지 버거
  투리토(v), 0.89
    -- 통 핀토콩과 살사, 구아카몰이 곁들여진 푸짐한 부리토

 

 

 

 

헤드 퍼스트 디자인 패턴 | 에릭 프리먼 | 한빛미디어- 교보ebook

14가지 GoF 필살 패턴!, 경력과 세대를 넘어 오랫동안 객체지향 개발자의 성장을 도와준 디자인 패턴 교과서의 화려한 귀환! 》 2005년부터 디자인 패턴 도서 분야 부동의 1위 》 디자인 패턴의 고

ebook-product.kyobobook.co.kr

알고리즘을 캡슐화하는 패턴

 

public class TemplateMethodTest {
	static class Coffee{
		void prepareRecipe() {
			boilWater();
			brewCoffeeGrinds();
			purInCup();
			addSugarAndMilk();
		}
		
		public void boilWater() {
			System.out.println("물 끊이는 중");
		}
		public void brewCoffeeGrinds() {
			System.out.println("필터로 커피를 우려내는 중");
		}
		public void purInCup() {
			System.out.println("컵에 따르는 중");
		}
		public void addSugarAndMilk() {
			System.out.println("설탕과 우유를 추가하는 중");
		}
	}
	static class Tea {
		void prepareRecipe() {
			boilWater();
			steepTeaBag();
			purInCup();
			addLemon();
		}
		public void boilWater() {
			System.out.println("물 끊이는 중");
		}
		public void steepTeaBag() {
			System.out.println("찻잎을 우려내는 중");
		}
		public void addLemon() {
			System.out.println("레몬을 추가하는 중");
		}
		public void purInCup() {
			System.out.println("컵에 따르는 중");
		}
	}
	public static void main(String[] args) {
		Coffee coffee = new Coffee();
		Tea tea = new Tea();
		coffee.prepareRecipe();
		tea.prepareRecipe();
		
	}
}
물 끊이는 중
필터로 커피를 우려내는 중
컵에 따르는 중
설탕과 우유를 추가하는 중
물 끊이는 중
찻잎을 우려내는 중
컵에 따르는 중
레몬을 추가하는 중

알고리즘이 거의 같다. 두 클래스 공통된 부분을 추상화하는 것이 좋겠다.

public class TemplateMethodTest2 {
	
	//더 추상화 할 요소는?
	abstract static class CaffeineBeverage{
		abstract void prepareRecipe();
		public void boilWater() {
			System.out.println("물 끊이는 중");
		}
		public void purInCup() {
			System.out.println("컵에 따르는 중");
		}
	}
	
	static class Coffee extends CaffeineBeverage{
		void prepareRecipe() {
			boilWater();
			brewCoffeeGrinds();
			purInCup();
			addSugarAndMilk();
		}
		
		
		public void brewCoffeeGrinds() {
			System.out.println("필터로 커피를 우려내는 중");
		}
		
		public void addSugarAndMilk() {
			System.out.println("설탕과 우유를 추가하는 중");
		}
	}
	static class Tea extends CaffeineBeverage{
		void prepareRecipe() {
			boilWater();
			steepTeaBag();
			purInCup();
			addLemon();
		}
		public void steepTeaBag() {
			System.out.println("찻잎을 우려내는 중");
		}
		public void addLemon() {
			System.out.println("레몬을 추가하는 중");
		}
	}
}

더 추상화할 여지가 있다. 단순히 메서드 이름이 아니라 동작 자체를 보면 추상화할 것이 더 존재함을 알 수 있다.

무언가를 우려낸다는 행위와, 무언가를 첨가하는 것

public class TemplateMethodTest3 {
	//abstract 메서드가 없더라도 abstract를 붙여 인스턴스화를 방지할 수 있다.
	abstract static class CaffeineBeverage{
    	//핵심 템플릿 메서드 final
		final void prepareRecipe() {
			 boilWater();
			 brew();
			 purInCup();
			 addCondiments();
		}
		//약간의 동작 차이만을 보이므로 재정의를 강제하도록 abstract 키워드를 붙인다.
		protected abstract void addCondiments();
		protected abstract void brew();
		public void boilWater() {
			System.out.println("물 끊이는 중");
		}
		public void purInCup() {
			System.out.println("컵에 따르는 중");
		}
	}
	
	static class Coffee extends CaffeineBeverage{
		@Override
		public void addCondiments() {
			System.out.println("설탕과 우유를 추가하는 중");
		}
		@Override
		public void brew() {
			System.out.println("필터로 커피를 우려내는 중");
		}
	}
	static class Tea extends CaffeineBeverage{
		@Override
		protected void addCondiments() {
			System.out.println("레몬을 추가하는 중");
		}
		@Override
		protected void brew() {
			System.out.println("찻잎을 우려내는 중");
			
		}
	}
}

템플릿 메소드 패턴 알아보기

템플릿 메소드는 알고리즘의 각 단계를 정의하며, 서브 클래스에서 일부 단계를 구현할 수 있도록 유도한다.

핵심은 템플릿 메소드를 final로 선언해 각 알고리즘의 순서를 통제한다.

 

템플릿 메소드 패턴의 장점 

템플릿 메소드에서 알고리즘을 독점해 처리한다.

알고리즘이 한 군데 모여 있다. 

 

템플릿 메소드 패턴의 정의

알고리즘의 골격을 정의한다.

템플릿 메소드를 사용하면 알고리즘의 일부 단계를 서브클래스에서 구현할 수 있다.

알고리즘의 구조를 그대로 유지하면서 알고리즘의 특정 단계를 서브클래스에서 재정의할 수 있다.

abstract class AbstractClass{
    final void templateMethod() {
        primitiveOperation1();
        primitiveOperation2();
        concreteOperation();
        hook();
    }
    public void hook() {}
    public void concreteOperation() {}
    protected abstract void primitiveOperation2();
    protected abstract void primitiveOperation1();
}

템플릿 메소드 후크 알아보기

hook 메서드는 구상메서드지만, 구현한게 아무것도(혹은 거의없는) 없다.

hook 메서드는 알고리즘 사이사이에 마음것 위치할 있다.

서브클레스에서 필요 오버라이드해서 사용할 목적이다.

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

import designpattern.headfirst.chapter8.TemplateMethodTest3.Coffee;
import designpattern.headfirst.chapter8.TemplateMethodTest3.Tea;

public class TemplateMethodTest4 {
	abstract static class CaffeineBeverageWithHook{
		final void prepareRecipe() {
			 boilWater();
			 brew();
			 purInCup();
			 if(customerWantsCondiments()) {
				 addCondiments();
			 }
		}
		boolean customerWantsCondiments() {
			return true;
		}
		protected abstract void addCondiments();
		protected abstract void brew();
		public void boilWater() {
			System.out.println("물 끊이는 중");
		}
		public void purInCup() {
			System.out.println("컵에 따르는 중");
		}
	}
	
	static class CoffeeWithHook extends CaffeineBeverageWithHook{
		@Override
		public void addCondiments() {
			System.out.println("설탕과 우유를 추가하는 중");
		}
		@Override
		public void brew() {
			System.out.println("필터로 커피를 우려내는 중");
		}
		
		@Override
		public boolean customerWantsCondiments() {
			String answer = getUserInput();
			
			if(answer.equalsIgnoreCase("y")) {
				return true;
			}else {
				return false;
			}
		}
		private String getUserInput() {
			String answer = null;
			System.out.print("커피에 우유와 설탕을 넣을까요?(y/n)");
			try{
				BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
				answer = br.readLine();
			}catch(IOException e) {
				System.out.println(e.getCause());
			}
			if(answer == null) {
				return "no";
			}
			return answer;
		}
	}
	static class TeaWithHook extends CaffeineBeverageWithHook{
		@Override
		protected void addCondiments() {
			System.out.println("레몬을 추가하는 중");
		}
		@Override
		protected void brew() {
			System.out.println("찻잎을 우려내는 중");
			
		}
		@Override
		public boolean customerWantsCondiments() {
			String answer = getUserInput();
			if(answer.equalsIgnoreCase("y")) {
				return true;
			}else {
				return false;
			}
		}
		private String getUserInput() {
			String answer = null;
			System.out.println("차에 레몬을 넣을까요? (y/n) ");
			try{
				BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
				answer = br.readLine();
			}catch(IOException e) {
				System.out.println(e.getCause());
			}
			if(answer == null) {
				return "no";
			}
			return answer;
		}
	}
	
	public static void main(String[] args) {
		Tea tea = new Tea();
		Coffee coffee = new Coffee();
		tea.prepareRecipe();
		coffee.prepareRecipe();
		
		TeaWithHook teaWithHook = new TeaWithHook();
		CoffeeWithHook coffeeWithHook = new CoffeeWithHook();
		
		System.out.println("\n홍차 준비중...");
		teaWithHook.prepareRecipe();
		System.out.println("\n커피 준비중...");
		coffeeWithHook.prepareRecipe();
	}
}
물 끊이는 중
찻잎을 우려내는 중
컵에 따르는 중
레몬을 추가하는 중
물 끊이는 중
필터로 커피를 우려내는 중
컵에 따르는 중
설탕과 우유를 추가하는 중

홍차 준비중...
물 끊이는 중
찻잎을 우려내는 중
컵에 따르는 중
차에 레몬을 넣을까요? (y/n) 
n

커피 준비중...
물 끊이는 중
필터로 커피를 우려내는 중
컵에 따르는 중
커피에 우유와 설탕을 넣을까요?(y/n)y
설탕과 우유를 추가하는 중

할리우드 원칙

먼저 연락하지 마세요. 저희가 연락 드리겠습니다.

 

의존성 부패를 방지하기 위함

의존성 부패는 고수준 구성요소가 저수준 구성 요소에 의존하고, 저수준 구성 요소는 다시 고수준 구성 요소에 의존하고를 반복하는 것 

순환 의존성

 

할리우드 원칙을 사용하면, 저수준 구성 요소가 시스템에 접속할 수는 있지만 언제, 어떻게 구성 요소를 사용할지는 고수준 구성 요소가 결정한다.

, 고수준 구성 요소가 저수준 구성 요소에게 "먼저 연락하지 마세요. 제가 먼저 연락 드리겠습니다." 라고 얘기하는 것과 같다.

할리우드 원칙과 템플릿 메소드 패턴

할리우드 원칙과 템플릿 메소드 패턴의 관계는 쉡게 있다.

템플릿 메소드 패턴을 써서 디자인하면 자연스럽게 서브클래스에게 "우리가 연락할 테니까 먼저 연락하지 "라고 얘기하는 셈이다.

 

자바 API 템플릿 메소드 패턴 알아보기

    public static void sort(Object[] a) {
        if (LegacyMergeSort.userRequested)
            legacyMergeSort(a); //일부를 위임
        else
            ComparableTimSort.sort(a, 0, a.length, null, 0, 0);
    }

    /** To be removed in a future release. */
    private static void legacyMergeSort(Object[] a) {
        Object[] aux = a.clone();
        mergeSort(aux, a, 0, a.length, 0);
    }

Arrays.sort 는 알고리즘을 구현하고, 일부 단계를 서브클래스에서 구현하라는 템플릿 메서드 정의와 완전히 같지는 않다.

자바에서 모든 배열이 정렬 기능을 사용할 수 있도록 만들기 위해 정적 메서드 sort를 만들고 대소를 비교하는 부분은 배열 타입 객체에서 구현하도록 만들었다.

이런 점에서 sort()메소드 구현 자체는 템플릿 메소드 패턴의 기본 정신에 충실하다.

Arrays의 서브클래스를 만들어야 한다는 제약조건이 없어 더 유연하기도 하다.

단점

	public class ArraysSortTest {
	    static class Dummy {}
	    
	    public static void main(String[] args) {
	        Dummy[] dumArr = {new Dummy(), new Dummy()};
	        Arrays.sort(dumArr);
	    }
	}
	
	Exception in thread "main" java.lang.ClassCastException: class designpattern.headfirst.chapter8.ArraysSortTest$Dummy cannot be cast to class java.lang.Comparable (designpattern.headfirst.chapter8.ArraysSortTest$Dummy is in unnamed module of loader 'app'; java.lang.Comparable is in module java.base of loader 'bootstrap')
	 at java.base/java.util.ComparableTimSort.countRunAndMakeAscending(ComparableTimSort.java:320)
	 at java.base/java.util.ComparableTimSort.sort(ComparableTimSort.java:188)
	 at java.base/java.util.Arrays.sort(Arrays.java:1041)
	 at designpattern.headfirst.chapter8.ArraysSortTest.main(ArraysSortTest.java:10)
	
	sort 메서드에서 해당 객체 배열이 Comparable 을 구현했는지 컴파일러에서 체크를 할 수 없어 런타임 에러를 유발한다…

핵심 정리

템플릿 메소드는 알고리즘의 단계를 정의하며 일부 단계를 서브 클래스에서 구현하도록 할 수 있다.

템플릿 메소드 패턴은 코드 재사용에 도움된다.

후크 메서드는 서브 클래스에서 선택적으로 재정의한다.

할리우드 원칙에 의하면, 저수준 모듈은 언제 어떻게 호출할지는 고수준 모듈에서 결정해야 한다.

실전에서 교과서적으로 패턴과 반드시 일치하지 않을 수 있다. 

팩토리 메소드 패턴은 템플릿 메소드 패턴의 특화 버전이다.

+ Recent posts