Java Refactoring -6, 분기문에 복잡하게 꼬여있는 AND와 OR 연산자 개선

박태건·2021년 7월 19일
0

리팩토링-자바

목록 보기
6/13
post-thumbnail

레거시 코드를 클린 코드로 누구나 쉽게, 리팩토링

위 책을 보면서 정리한 글입니다.

분기문에 복잡하게 꼬여있는 AND와 OR 연산자 개선

분기문은 추가되고 시간이 흐를수록 이해할 수 없는 코드로 변질된다.

  • 분기문 : 코드 수행의 분기처리에 이용
  • 복잡한 분기문 사용은 코드를 이해하기 어렵게 만들어 개발자의 실수를 초래한다.
  • 요구사항의 계속된 추가로 분기되는 조건이 결합되면서 복잡한 분기문으로 변한다.
    • 기존 조건절에 새로운 조건절을 추가하여 결합하는 것이 문제에 대한 빠른 해결책이 되기 떄문.

개선방향

조건들을 쉬운 분기문으로 재구성하여 개선

  1. 기존 분기문에서 조건절을 추출하여 새로운 분기문을 구성
  2. 독립 조건 또는 결합 조건을 메서드 추출하여 새로운 분기문에 결합
  3. 테스트 코드를 통해 검증
  4. 기존 분기문의 조건절이 모두 추출되어 새로운 분기문에 결합될때까지 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) { // }
  1. 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) { // }
  1. 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차 요구사항을 모두 반영한 게임 모드

4차 요구사항 기반 코드

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;
        }

        // 중략
    }
}

위 코드의 문제점

  • AND나 OR 연산자로 묶인 조건에 따라 경우의 수가 많아져 가독성이 낮고, 테스트 케이스가 많아져 관리가 어렵다.
  • 테스트 코드 자체에 대한 신뢰성에도 문제가 생긴다.
  • 요구사항을 반영하는 과정에서 복잡한 조건절을 만들지 않고, 가독성이 높은 코드를 구현 가능.
  • 위의 코드는 조금 극단적인 예의 분기문

레거시 코드 개선 과정

복잡한 조건절은 생각의 패턴을 반영하여 개선

복잡한 조건문 개선을 위한 고려사항
1. 복잡한 분기문이 포함된 메서드를 사전에 충분히 작은 크기로 리팩토링을 시도.
2. 분기문이 포함된 메서드가 거대하면 그만큼 분기문을 리팩토링할 때 다른 부분에 의존할 가능성이 크다.
3. 복잡한 분기문 전체를 검증하는 대신 기존 분기문을 주석 처리하고 그 아래에 개선된 분기문을 새롭게 작성하여 조건들을 추출하고 결합하여 작은 기능부터 하나씩 검증

1차 기능 개선 : 독립된 분기문 추출

  • 복잡한 조건 결합 안에서 하나의 조건으로 의미가 있는 조건들을 파악
  • 예제의 가장 작은 크기의 독립된 조건
    • SINGLE
    • DOUBLE
    • TRIPLE
    • OUTER_BULL
    • BULLS_EYE

독립된 조건물 추출

	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;
        }

GameMode에 따른 조건문을 검증하기 위한 테스트 코드 클래스

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;
    }
}

2차 기능 개선 : 동일한 목적의 조건을 메서드로 추출

  • AND 연산자를 사용하여 결합된 여러 조건을 메서드 추출을 이용하여 분리
  • 남겨진 분기문에서 AND 연산자로 결합된 조건절을 살펴보면 다음 두 가지의 조합
    • 동일한 SINGLE SCORE 필드를 연속해서 두 번 맞추었는지
    • 동일한 SINGLE SCORE 필드를 연속해서 세 번 맞추었는지

동일한 SINGLE SCORE 필드를 연속해서 두 번 맞추었는지 판단하는 코드와 테스트 코드

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);
}

동일한 SINGLE SCORE 필드를 연속해서 세 번 맞추었는지 판단하는 코드와 테스트 코드

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에 따른 두 가지 분기 처리

GameMode 조건은 분기 대부분에 포함되어 모든 조건절에서 해당 조건을 결합해야 하므로, 조건 추출이 어렵다.
이 경우에는 분기문 자체의 분리를 고려해야 한다.

메서드 추출을 통한 분기문 분리

if(gameMode == GameMode.CUSTOMER) {
	calculatedScore = getCustomerScore(fieldType, preFieldType, point, oneStepPrePoint, twoStepPrePoint);
} else {
	calcaulatedScore = getStanadardScore(fieldType, point);
}

getCustomerScore() 메서드에 isDoubleShootAdvantage() 메서드와 isTripleShootAdvantage() 메서드 조건절 결합

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;
}

3차 기능 개선 : 불필요한 코드 정리

  • SINGLE SCORE에 대한 분기를 DOUBLE SCORE, TRIPLE SCORE에 대한 분기 뒤로 옮긴다.
  • SINGLE FIELD를 맞출 경우, DOUBLE SCORE와 TRIPLE SCORE를 받는 조건이 앞의 분기에서 처리되므로 SINGLE SCORE에 대한 조건절에서 불필요한 조건절 세 개를 제거 가능
  1. DoubleShoot, TripleShoot Advantage를 받는 경우가 아닌지 판별하기 위한 조건절
    !isDoubleShootAdvantage(feildType, point, oneStepPrePoint, twoStepPrePoint)
  2. OuterBulls Advantage를 받는 경우가 아닌지를 판별하기 위한 조건절
    !isOuterBullsAdvantage(preFiledType, fieldType)
  3. BullsEye Advantage를 받는 경우가 아닌지를 판별하기 위한 조건절
    !isBullsEyeAdvantage(preFieldType, fieldType)

개선 후 getCustomerScore 메서드

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;
}

개선된 레거시 코드

요약 및 정리

분기문이 복잡하게 결합되었을 때 발생할 수 있는 문제

  1. 복잡한 분기문은 가독성을 떨어뜨려서 코드의 유지보스를 어렵게 만들고, 잠재된 오류가 있을 가능성 있다.
  2. 여러 조건이 AND나 OR 연산자로 결합되면 조건에 대한 판단을 할 수 있는 경우의 수가 많아져 테스트 코드가 거대해지고 테스트의 신뢰도를 떨어뜨린다.

복잡한 분기문을 개선하기 위한 생각의 흐름

  1. 독립된 조건절의 추출을 통하여 복잡도를 낮추어 분석을 쉽게 만든다.
  2. AND와 OR로 결합된 조건 결합을 하나의 조건을 판단하는 메서드로 추출.
    • AND는 동일한 목적을 갖는 조건 결합으로 묶여있는 경향이 크다.
    • OR은 동일한 객체 또는 필드에 대한 조건으로 묶여 있는 경향이 크다.
  3. 추출된 조건들을 새롭게 결합.
  4. 추출과 결합 과정에서 해당 분기에 대한 검증을 진행,
profile
노드 리액트 스프링 자바 등 웹개발에 관심이 많은 초보 개발자 입니다

0개의 댓글