6장 - 조건분기: 미궁처럼 복잡한 분기처리를 무너뜨리는 방법


6.1 조건 분기가 중첩되어 낮아지는 가독성

6.1.1 조기 리턴으로 중첩 제거하기

조기 리턴 (early return)을 통해 중첩 악마를 퇴치할 수 있음

예를 들어, 원래 1. 살아있고 2. 움직일 수 있고 3. 마나가 남아 있을 때 마법을 쓸 수 있는 조건이 있었다고 한다면

// 살아있는가
if (0 < member.hitPoint) {
	// 움직일 수 있는가
   	if (움직이냐?)
       	// 마나가 남아 있는가 
        if (마나 있냐?)
}

원래 조건을 반전하여 조기 리턴을 활용해 if문의 중첩을 벗어날 수 있음

// 살아 있지 않은 경우를 리턴하므로 처리가 종료됨
if (member.hitPoint <= 0) return;
if (!움직이냐?) return;
if (!마나있냐?) return;

마법공격실행함수()

이렇게 모든 조건들을 반전하여 모든 중첩을 제거하여 가독성이 좋아질 수 있음

또한 이러한 조기 리턴의 가독성이 좋아진다는 장점 외에도
조건 로직과 실행 로직을 분리할 수 있다는 장점도 존재함

마법을 쓸 수 없는 조건은 앞부분에 조기 리턴으로 모았고 마법 발동할 때의 로직을 뒤로 모았기 때문에 조건과 실행을 나누어서 볼 수 있음

만약 여기에 다른 요구 사항들이 추가되어야 하는 상황에서도 간단하게 로직을 추가할 수 있음

이렇게 조기 리턴을 활용해서 조건에 따라 실행 흐름이 달라지는 일을 막는 것은 3.4에서 소개했던 가드와 비슷함


6.2 switch 조건문 중복

같은 switch문들이 여러 개 사용 되기 시작할 때가 존재

예를 들어, MagicType에 따른 이름, 마나소모량, 공격력 세 가지를 반환하는 코드가 3번이나 중복으로 나타나는 것은 매우 좋지 않은 코드

그 이유로는 두 가지 switch에는 분명히 새로운 기술인 '헬파이어'를 추가 했는데 이름, 마나 소모량에만 넣고 공격력에는 넣지 않아서 문제가 생길 수 있고

이 뿐만 아니라 계속해서 새로운 요구 사항으로 '테크니컬 포인트'라는 요구 사항이 생겨 또 다른 switch를 만들어야할 수도 있게 되는 문제가 생길 수 있음

이런 문제에서 폭발적으로 늘어나고 있는 switch문에서의 공통점으로는 무엇이 있을까?

바로 같은 기준으로 분기를 태운다는 것!

모두 MagicType이란 조건식을 사용하고 있고 switch가 중복 코드가 된 것

그럼 이렇게 게임 뿐만이 아니라 종류에 따라 처리를 전환하는 상황은 굉장히 많을 땐 어떻게 해야할까? (예를 들어, 나이별 영화 요금, 휴대전화 요금제)

6.2.5 조건 분기 모으기

switch 조건문 중복을 해소 하려면, 단일 책임 선택의 원칙을 생각해봐야합니다.

단일 책임 선택의 원칙


"소프트웨어 시스템이 선택지를 제공해야 한다면, 그 시스템 내부의 어떤 한 모듈만으로 모든 선택지를 파악할 수 있어야한다."

다시 말해, 조건식이 같은 조건 분기를 여러 번 작성하지 말고 한 번에 작성해야한다는 것

이를 위해선 한 조건에 위에서 말한 여러 요구 사항들을 모으면 된다

switch (magicType) {
	case fire:
   	name = '파이어';
       costMagicPoint = 2;
       attckPower = 20;
       costTechnicalPoint = 1;
}

6.2.6 인터페이스로 switch 조건문 중복 해소하기

단일 책임의 원칙으로 하나의 switch만을 쓰다가 변경하고 싶은 부분이 많아지면 로직이 점점 거대해지고 클래스가 거대해짐

이렇게 클래스가 거대해질 때 관심사에 따라 작은 클래스로 분할하는 것이 좋은데 이럴 떄 활용하는 것이 '인터페이스' (자바와 같은 객체지향 문법, 기능 변경을 편하게 할 수 있다고 생각하면 됨)

인터페이스를 서로 다른 자료형을 같은 자료형처럼 사용할 수 있게 해줌

class Rectangle implements Shape {
	private final double width;
   private final double height;
   
   public double area() {
   	return width * height;
   }
}

class 서클은 생략

Shape shape = new Circle(10);
print(shape.area())  // 귀찮아서 print 씀
Shape = new Rectangle(20, 25);
print(shape.area()) // 여기동

이렇듯 각각의 코드를 간단하게 실행할 수 있게 해준다는 것이 인터페이스의 큰 장점이란 것

6.2.7 인터페이스를 switch 조건무 중복에 응용 (전략 패턴)

또 다른 인터페이스의 장점은 다른 로직을 같은 방식으로 처리할 수 있다는 점

interface Magic {
	String magicName();
    int costMagicPoint();
    int attackPower();
    int costTechnicalPoint();
}

class Fire implements Magic {
	private final Member member;
    
    Fire(final Member member) {
    	this.member = member;
    }
    
    ~
}

이렇게 Magic이란 인터페이스로 마법 처리 전체를 변경하면 switch 조건문을 사용하지 않고도 마법 별로 처리를 나눌 수 있음

이처럼 인터페이스를 활용하여 처리를 한꺼번에 전환하는 설계를 '전략 패턴'이라고 함

이렇게 전략 패턴을 쓰고 있고, 만약 일부 클래스의 구현을 깜빡했다하면 애초에 컴파일부터가 실패하기 때문에 구현하지 않았다는 실수조차 방지할 수 있음


6.3 조건 분기 중복과 중첩

6.3.1 정책 패턴으로 조건 집약하기

'같은 판정의 로직을 계속계속 재사용 하려면 어떻게 해야 할까요?'

이럴 때 사용하는 것이 정책 패턴
조건을 부품처럼 만들고, 부품으로 만든 조건을 조합해서 사용하는 패턴

(예시 생략)


6.4 자료형 확인에 조건 분기 사용하지 않기

instanceof 같은 자료형 판정 연산자를 활용한다면 인터페이스를 활용하더라도 조건 분기가 그대로 남아있을 수 있음

게다가 여기에 다른 class들이 추가 된다면 조건 분기가 코드 계속 중복되는 것

이와 같은 로직은 리스코프 치환 원칙이라는 소프트웨어 원칙을 위반

리스코프 치환 원칙


클래스 기반 자료형과 하위 자료형 사이에 성립하는 규칙으로,
'기반 자료형을 하위 자료형으로 변경해도, 코드는 문제 없이 동작 해야한다.'는 의미
(기반 자료형은 인터페이스를 구현한 클래스를 의미)


인터페이스를 잘 사용하는지가 곧 설계 능력의 전환점


플래그 매개 변수

boolean 자료형의 매개 변수를 플래그 매개변수라고 부름

플래그 매개변수는 어떤 일을 하는지 굉장히 예측하기 힘들고
그 안에서 내부 로직을 확인해야하므로 가독성이 낮아짐

boolean뿐만 아니라, int 자료형을 사용해 기능을 전환하는 경우에도 같은 문제가 발생

그렇기 때문에, 플래그 매개변수를 받는 메서드는 기능 별로 분리하는 것이 좋다

profile
기록, 꺼내 쓸 수 있는 즐거움

0개의 댓글