위 책을 보면서 정리한 글입니다.
분기문은 추가되고 시간이 흐를수록 이해할 수 없는 코드로 변질된다.
- 기존 분기문에서 조건절을 추출하여 새로운 분기문을 구성
- 독립 조건 또는 결합 조건을 메서드 추출하여 새로운 분기문에 결합
- 테스트 코드를 통해 검증
- 기존 분기문의 조건절이 모두 추출되어 새로운 분기문에 결합될때까지 2, 3번의 과정을 반복
1. 독립 조건절 추출
- 조건절의 복잡도를 줄이기 위해 먼저 조건절 내에서 결합 조건과 독립 조건을 찾아서 추출 시도
- 독립 조건절을 분리하여 조건절의 복잡도를 낮추면 분석과 개선이 용이
- 개선 후, 추출한 조건절을 다시 결합
- 독립 조건은 OR 연산자 전후에 위치
2. AND로 결합된 조건들을 조건 판단하는 독립 메서드로 추출- AND로 결합된 조건들로 실제로는 하나의 조건을 성립하기 위한 여러 조건이 결합된 경우가 많다
- 결합 조건들을 판별하는 하나의 메서드로 추출
3. OR로 결합된 조건들을 판독하는 독립 메서드로 추출- OR로 결합되어 있으면 동일한 필드 혹은 객체에 대한 여러 조건 비교가 결합된 경우가 많다.
- 해당 필드 또는 객체를 파라미터로 전달받아서 조건에 대한 TRUE/FALSE로 반환하는 메서드로 추출
- 동일 객체 또는 필드에 대한 조건 결합이 아니더라도 AND와 같이 하나의 조건을 판별할 수 있는 조건 결합이면 결합을 메서드로 추출
잘 동작하는 분기문을 다시 분석하여 리팩토링 해야할까
- 코드의 수정, 개선 때문에 분기문 분석이 필요한 시점이 발생
- 복잡한 분기문은 가독성을 떨어트려 시간에 대한 비용 손실을 초래
- 테스트 코드를 작성할 때 문제 발생
- 복잡한 분기문은 많은 조건이 결합하기에 해당 조건들에 대한 경우의 수가 많다.
- 경우의 수가 많으면 결국 테스트 케이스가 많아지고, 테스트 코드도 거대해진다.
- 테스트 케이스가 모든 범위를 보장하는 것 역시 어렵다.
- 복잡한 분기문을 개선하면 코드의 가독성을 높이고, 테스트 코드가 명확해져 유지보수하는데 드는 시간과 노력을 줄일 수 있다.
조건문 추출
1. 독립 조건절 추출// 추출 전 if (condition1 == true && condition2 == true || condition3 == true) { // } else if(condition1 == true && concition2 == false || condition3 == false) { // } // 추출 대상 /* * condition3는 AND 연산자로 결합된 condition1과 condition2에 대한 독립조건으로 OR 연산자로 결합되어 있다. * condition3의 조건을 분리해도 condition1과 condition2의 결합에는 영향이 생기지 않는다. */ // 추출 후 boolean isSingle(condition1, condition2) { // } boolean isDouble(condition1, condition2) { // } // 분기문 개선 if(isSingle(condition1, condition2) || conditon3 == true) { // } else if(isDouble(condition1, condition2) || conditon 3 == false) { // }
- AND로 결합된 조건들을 판단하는 독립 메서드로 추출
// 추출 전 if(type == "run" && type2 == "night" && type3 == "mountain" && refactor == false) { // } // 추출 대상 /* * type1, type2, type3이 각각 run, night, mountain일 때, 해당 분기에 대한 조건이 true가 되는 type에 대한 조건을 판단 * type을 조건 판단하는 메서드로 추출 */ // 추출 후 boolean isRunAdvantageScore(Type type1, Type type2, Type type3) { // } // 분기문 개선 if(isRunAdvantageScore(type1, type2, type3) && refactore == false) { // }
- OR로 결합된 조건들을 조건 판단하는 독립 메서드로 추출
// 추출 전 if((type == "run" || type == "swim" && type == "cycle") && refactor == false) { // } // 추출 대상 /* * type이 run, swim, cycle 중 하나일 때, 해당 분기에 대한 조건이 true가 되는 type에 대한 조건을 판단 * type을 조건 판단하는 메서드로 추출 */ // 추출 후 boolean isExcerciseType(Type type) { // } // 분기문 개선 if(isExcerciseType(type) && refactore == false) { // }
레거시 코드
조건절이 어떻게 복잡해는지 단계별로 확인.
최초 요구사항
- Single : 과녁 점수 x 1
- Double : 과녁 점수 x 2
- Triple : 과녁 점수 x 3
- Outer bull : 50
- Bulls eye : 60
2차 요구사항
- Single : 과녁 점수 x 1
- Double : 과녁 점수 x 2
- Triple : 과녁 점수 x 3
- Outer bull : 50
- Bulls eye : 60
- 2차
- 동일 Single 과녁 2회 연속 : 과녁 점수 x 2
- 동일 Single 과녁 3회 연속 : 과녁 점수 x 3
3차 요구사항
- Single : 과녁 점수 x 1
- Double : 과녁 점수 x 2
- Triple : 과녁 점수 x 3
- Outer bull : 50
- Bulls eye : 60
- 2차
- 동일 Single 과녁 2회 연속 : 과녁 점수 x 2
- 동일 Single 과녁 3회 연속 : 과녁 점수 x 3
- 3차
- Outer bull 명중 후 다음에 맞힌 Single : 과녁 점수 x 2
- Bulls eye 명중 후 다음에 맞힌 Single : 과녁 점수 x 3
4 요구사항
- 오리지널 모드 : 1차 요구사항만 반영한 게임 모드
- 커스텀 모드 : 1, 2, 3차 요구사항을 모두 반영한 게임 모드
public class ScoreCalculator {
public void scoreCalculate(ShootInfo shootInfo) {
int calculatedScore = 0;
if(fieldType == FieldType.SINGLE
|| (gameMode == GameMode.CUSOMER
&& (point != oneStepPrePoint
&& preFieldType != FieldType.OUTER_BULL
&& preFieldType != FieldType.BULLS_EYE))) {
calculatedScore = point;
} else if(fieldType == FieldType.DOUBLE
|| (gameMode == GameMode.CUSOMER
&& ((fieldType == FieldType.SINGLE
&& point == oneStepPrePoint
&& point != twoStepPrePoint)
|| (preFieldType == FieldType.OUTER_BULL
&& fieldType == FieldType.SINGLE)))) {
calculatedScore = point * 2;
} else if(fieldType == FieldType.TRIPLE
|| (gameMode == GameMode.CUSOMER
&& ((fieldType == FieldType.SINGLE
&& point == oneStepPrePoint
&& point == twoStepPrePoint)
|| (preFieldType == FieldType.OUTER_BULL
&& fieldType == FieldType.SINGLE)))) {
calculatedScore = point * 3;
} else if(fieldType == FieldType.OUTER_BULL) {
calculatedScore = 50;
} else if(fieldType == FieldType.BULLS_EYE) {
calculatedScore = 60;
}
// 중략
}
}
위 코드의 문제점
복잡한 조건문 개선을 위한 고려사항
1. 복잡한 분기문이 포함된 메서드를 사전에 충분히 작은 크기로 리팩토링을 시도.
2. 분기문이 포함된 메서드가 거대하면 그만큼 분기문을 리팩토링할 때 다른 부분에 의존할 가능성이 크다.
3. 복잡한 분기문 전체를 검증하는 대신 기존 분기문을 주석 처리하고 그 아래에 개선된 분기문을 새롭게 작성하여 조건들을 추출하고 결합하여 작은 기능부터 하나씩 검증
if(fieldType == FieldType.SINGLE) {
calculatedScore = point;
} else if(fieldType == FieldType.DOUBLE) {
calculatedScore = point * 2;
} else if(fieldType == FieldType.TRIPLE) {
calculatedScore = point * 3;
} else if(fieldType == FieldType.OUTER_BULL) {
calculatedScore = 50;
} else if(fieldType == FieldType.BULLS_EYE) {
calculatedScore = 60;
}
if(
(gameMode == GameMode.CUSOMER
&& (point != oneStepPrePoint
&& preFieldType != FieldType.OUTER_BULL
&& preFieldType != FieldType.BULLS_EYE))) {
calculatedScore = point;
} else if(
(gameMode == GameMode.CUSOMER
&& ((fieldType == FieldType.SINGLE
&& point == oneStepPrePoint
&& point != twoStepPrePoint)
|| (preFieldType == FieldType.OUTER_BULL
&& fieldType == FieldType.SINGLE)))) {
calculatedScore = point * 2;
} else if(
(gameMode == GameMode.CUSOMER
&& ((fieldType == FieldType.SINGLE
&& point == oneStepPrePoint
&& point == twoStepPrePoint)
|| (preFieldType == FieldType.OUTER_BULL
&& fieldType == FieldType.SINGLE)))) {
calculatedScore = point * 3;
}
public class ScoreCalculatorTest {
private static ScoreCalculator testCalculator;
private DartsPlayer testPlayer;
private ShootInfo testShoot;
@BeforeClass
public static void intializeTestObject() {
testCalculator = new ScoreCalculator();
}
@Before
public void intializeTest() {
testPlayer = new DartsPlayer("TestPlayer");
testShoot = new ShootInfo(testPlayer);
}
@Test
public void testSingleScoreCalc_오리지널모드_싱글필드() {
// Given
int testShootPoint = 7;
testShoot.setPoint(testShootPoint);
testShoot.setFileType(FieldType.SINGLE);
GameMode gameMode = GameMode.ORIGINAL;
// When
testCalculator.ScoreCalculator(testShoot, gameMode);
// Then
assertEqauls(testShootPoint, testPlayer.getTotalScore());
}
@Tset
public void testDoubleScoreCalc_오리지널모드_더블필드() { // 중략 }
@Tset
public void testTripleScoreCalc_오리지널모드_트리플필드() { // 중략 }
@Tset
public void testOuterBullScoreCalc_오리지널모드_아웃터블() { // 중략 }
@Tset
public void testBullsEyeScoreCalc_오리지널모드_불스아이() { // 중략 }
@Tset
public void testSingleScoreCalc_커스터머모드_싱글필드() { // 중략 }
@Tset
public void testDoubleScoreCalc_커스터머모드_더블필드() { // 중략 }
@Tset
public void testTripleScoreCalc_커스터머모드_트리플필드() { // 중략 }
@Tset
public void testOuterBullScoreCalc_커스터머모드_아웃터블() { // 중략 }
@Tset
public void testBullsEyeScoreCalc_커스터머모드_불스아이() { // 중략 }
public void initScore() {
testShoot = null;
testPlayer = null;
}
}
public boolean IsDoubleShootAdvantage(FieldType, fieldType, int point, int oneSetpPrePoint, int twoStepPrePoint) {
boolean isDoubleShoot = false;
if(fieldType == FieldType.SINGLE
&& point == oneStepPrePoint
&& point != twoStepPrePoint
) {
isDoubleShoot = true;
}
return isDoubleShoot;
}
@Test
public void testIsDoubleShootAdvantage() {
boolean isDoubleShoot = false;
// Given
FieldType fieldType = FeildType.SINGLE;
int point = 7;
int oneStepPrePoint = 7;
int twoStepPrePoint = 5;
// When
isDoubleShoot = testCalculator.IsDoubleShootAdvantage(fieldType, point, oneSetpPrePoint, twoSetpPrePoint);
// Then
assertEquals(true, isDoubleShoot);
}
public boolean IsTripleShootAdvantage(FieldType, fieldType, int point, int oneSetpPrePoint, int twoStepPrePoint) {
boolean isTripleShoot = false;
if(fieldType == FieldType.SINGLE
&& point == oneStepPrePoint
&& point == twoStepPrePoint
) {
isTripleShoot = true;
}
return isTripleShoot;
}
@Test
public void testIsTripleShootAdvantage() {
boolean isTripleShoot = false;
// Given
FieldType fieldType = FeildType.SINGLE;
int point = 7;
int oneStepPrePoint = 7;
int twoStepPrePoint = 5;
// When
isTripleShoot = testCalculator.IsTripleShootAdvantage(fieldType, point, oneSetpPrePoint, twoSetpPrePoint);
// Then
assertEquals(true, isTripleShoot);
}
GameMode 조건은 분기 대부분에 포함되어 모든 조건절에서 해당 조건을 결합해야 하므로, 조건 추출이 어렵다.
이 경우에는 분기문 자체의 분리를 고려해야 한다.
if(gameMode == GameMode.CUSTOMER) {
calculatedScore = getCustomerScore(fieldType, preFieldType, point, oneStepPrePoint, twoStepPrePoint);
} else {
calcaulatedScore = getStanadardScore(fieldType, point);
}
if(fieldType == FieldType.SINGLE
&& !IsDoubleShootAdvantage(fieldType, point, oneStepPrePoint, twoStepPrePoint)) {
calculatedScore = point;
} else if(fieldType == FieldType.DOUBLE
|| IsDoubleShootAdvantage(fieldType, point, oneStepPrePoint, twoStepPrePoint)) {
calculatedScore = point * 2;
} else if(fieldType == FieldType.Triple
|| IsTripleShootAdvantage(fieldType, point, oneStepPrePoint, twoStepPrePoint)) {
calculatedScore = point * 3;
} else if(fieldType == FieldType.OUTER_BULL) {
calculatedScore = 50;
} else if(fieldType == FieldType.BULLS_EYE) {
calculatedScore = 60;
}
마찬가지로 메소드 추출을 통해서 남겨진 분기를 처리한다.
1. OUTER_BULL 필드를 맞힌 후 SINGLE FIELD를 맞히면 DOUBLE SCOR로 계산
2. BULLS_EYE 필드르 맞힌 후 SINGLE FILED를 맞히면 TRIPLE SCORE로 계산
public boolean isOuterBullsAdvantage(FieldType, preFieldType, FieldType fieldType) {
boolean isOuterBullsAdvan = false;
if(preFieldType == FieldType.OUTER_BULL
&& fieldType == FieldType.SINGLE
) {
isOuterBullsAdvan = true;
}
return isOuterBullsAdvan;
}
public boolean isBullsEyeAdvantage(FieldType, preFieldType, FieldType fieldType) {
boolean isBullsEyeAdvan = false;
if(preFieldType == FieldType.BULLS_EYE
&& fieldType == FieldType.DOUBLE
) {
isBullsEyeAdvan = true;
}
return isBullsEyeAdvan;
}
- DoubleShoot, TripleShoot Advantage를 받는 경우가 아닌지 판별하기 위한 조건절
!isDoubleShootAdvantage(feildType, point, oneStepPrePoint, twoStepPrePoint)- OuterBulls Advantage를 받는 경우가 아닌지를 판별하기 위한 조건절
!isOuterBullsAdvantage(preFiledType, fieldType)- BullsEye Advantage를 받는 경우가 아닌지를 판별하기 위한 조건절
!isBullsEyeAdvantage(preFieldType, fieldType)
public int getCustomerScore(FieldType preFieldType, int point, boolean oneStepPrePoint, boolean twoStepPrePoint) {
int calculatedScore = 0;
if(fieldType == FieldType.DOUBLE
|| isDoubleShootAdvantage(feildType, point, oneStepPrePoint, twoStepPrePoint)
|| isOuterBullsAdvantage(preFieldType, feildType)){
calculatedScore = point * 2;
} else if(fieldType == FieldType.TRIPLE {
|| isTripleShootAdvantage(feildType, point, oneStepPrePoint, twoStepPrePoint)
|| isBullsEyeAdvantage(preFieldType, feildType)){
calculatedScore = point * 3;
} else if(fieldType == FieldType.SINGLE) {
calculatedScore = pooint;
} else if(fieldType == FieldType.OUTER_BULL) {
calculatedScore = 50;
} else if(fieldType == FieldType.BULLS_EYE) {
calculatedScore = 60;
}
return calculatedScore;
}
분기문이 복잡하게 결합되었을 때 발생할 수 있는 문제
- 복잡한 분기문은 가독성을 떨어뜨려서 코드의 유지보스를 어렵게 만들고, 잠재된 오류가 있을 가능성 있다.
- 여러 조건이 AND나 OR 연산자로 결합되면 조건에 대한 판단을 할 수 있는 경우의 수가 많아져 테스트 코드가 거대해지고 테스트의 신뢰도를 떨어뜨린다.