RE-Heat 개발자 일지

내 코드가 그렇게 이상한가요? - 7장 중첩을 제거하는 구조화 테크닉 본문

스터디/독서스터디

내 코드가 그렇게 이상한가요? - 7장 중첩을 제거하는 구조화 테크닉

RE-Heat 2024. 4. 16. 21:32

https://product.kyobobook.co.kr/detail/S000202521361

 

내 코드가 그렇게 이상한가요? | 센바 다이야 - 교보문고

내 코드가 그렇게 이상한가요? | 예약 판매부터 1만 부 돌파! 일본 아마존 IT 분야 베스트셀러 개발자가 직접 선정한 〈IT 엔지니어 도서 대상 2023〉 기술서 부문 대상 공감 100% 나쁜 코드 사례로

product.kyobobook.co.kr

제목 : 내코드가 그렇게 이상 한가요

저자 : 센바 다이야
옮긴이/역자 : 윤인성

 

이 책의 내용을 토대로 정리한 글입니다.

 

 

7장 컬렉션 : 중첩을 제거하는 구조화 테크닉

 

7.1 이미 존재하는 기능을 다시 구현하기

이미 널리 사용되는 기술과 해결법이 존재하면 기존 라이브러리를 쓰는 게 효율적이다.

=> 바퀴의 재발명을 피하자

 

코드 7.1 '감옥 열쇠 소지 여부 확인 코드'

boolean hasPrisonKey = false;
// items는 List<Item> 자료형
for (Item each : items) {
	if (each.name.equals("감옥 열쇠")){
		hasPrisonKey = true;
		break;
    }
}

 

코드 7.2 개선 코드

boolean hasPrisonKey = items.stream().anyMatch(
	item -> item.name.equals("감옥 열쇠")
);

 

또 다른 예시 - 문자열 뒤집는 기능

public static String reverseString(String input) {
    String reversed = "";
    for (int i = input.length() - 1; i >= 0; i--) {
        reversed += input.charAt(i);
    }
    return reversed;
}

 

문자열 뒤집는 기능 개선

public static String reverseString(String input) {
    StringBuilder reversed = new StringBuilder(input);
    return reversed.reverse().toString();
}

=> 반복문으로 바꾸는 대신 StringBuilder의 reverse 사용

 

7.2 반복 처리 내부의 조건 분기 중첩

① 조기 컨티뉴로 조건 분기 중첩 제거하기

반복문 내부의 조건 분기 중첩은 조기 리턴을 응요한 조기 컨티뉴로 해결할 수 있다.

 

■ 코드 개선과정

RPG에서 독 데미지를 받는 사양

for (Member member : members){
	// HP가 0보다 큰지 확인
	if ( 0 < member.hitPoint) {
    	//중독 상태인지 확이 
    	if (member.containsState(StateType.poison)){
        	member.hitpoint -=10;
            //HP -10 감소 후 음수거나 0이면 상태 변경
            if (member.hitPoint<=0){
            	member.hitPoint = 0;
                member.addState(StateType.dead);
                member.removeState(StateType.poison);
            }
        }    
    }
}
for (Member member : members){
	// HP가 0이면 죽은 상태이므로 다음 멤버로 넘어간다.
    if (member.hitPoint == 0) continue;
    // 독 상태가 아니면 다음 member로 넘어간다.
    if (!member.containsState(StateType.poison)) continue;
    
    // 독 데미지 -10
    member.hitpoint -= 10;
    
    // 독 데미지를 받은 후 살아있으면 다음 멤버로 넘어간다.
    if (member.hitpoint > 0) continue;
    
    // 음수가 나올 때를 대비해 HP를 0으로 맞춰주고 상태를 독 -> 죽음으로 변경
    member.hitPoint = 0;
    member.addState(StateType.dead);
    member.removeState(StateType.poison);
}

 

 

② 조기 브레이크로 중첩 제거하기

break는 처리를 중단하고 반복문 전체를 벗어나는 제어구문.

 

▶ 구현할 코드 - 연계공격

  • 첫 멤버부터 연계 공격 성공 여부 평가
  • 연계 공격 성공 시 해당 멤버 공격력에 1.1배 추가
  • 한 명이라도 연계 공격에 실패하면 후속 멤버 연계 평가 X 
  • 한 멤버의 추가 데미지(1.1배 적용된)가 30 이상이면 추가되는 데미지 총 데미지에 합산
  • 한 멤버의 추가 데미지가 30 이하면 연계 실패

■ 코드 개선 과정

int totalDamage = 0;
for (Member member : members) {
    // 연계 공격 성공 여부 확인
    if (member.hasTeamAttackSucceeded()) {
        // 1.1배 추가
        int damage = (int)(member.attack() * 1.1);
        // 연계공격 성공 데미지가 30 이상인지 여부 확인
        if (damage >= 30) {
            totalDamage += damage;
        } else {
            break;
        }
    } else {
        break;
    }
}
int totalDamage = 0;
for (Member member : members){
    if (!member.hasTeamAttackSucceeded()) break;

    int damage = (int)(member.attack() * 1.1);

    if (damage < 30) break;

    totalDamage += damage;
}

 

 

7.3 응집도가 낮은 컬렉션 처리

출처 : https://velog.io/@guswns3371/응집도-흩어져-있는-것들

 

컬렉션 처리도 응집도가 낮아지기 쉽다.

 

예시

1. 멤버를 추가하는 기능(addMember)이 필드 맵 관련 클래스, 특별 이벤트 제어 클래스에 중복

2. 파티 멤버가 1명이라도 존재하는지 확인하는 기능 중복

class FieldManager {
    // 멤버를 추가합니다.
    void addMember(List<Member> members, Member newMember) {
        if (members.stream().anyMatch(member -> member.id == newMember.id)) {
            throw new RuntimeException("이미 존재하는 멤버입니다.");
        }
        if (members.size() == MAX MEMBER_COUNT){
            throw new RuntimeException("이 이상 멤버를 추가할 수 없습니다.");
        }
        members.add(newMember);
    }

    // 파티 멤버가 1명이라도 존재하면 true를 리턴
    boolean partyIsAlive(List<Member> members) {
        return members.stream().anyMatch(member -> member.isAlive());
    }
}
// 게임 중에 발생하는 특별 이벤트를 제어하는 클래스
class SpecialEventManager {
    // 멤버를 추가합니다.
    void addMember(List<Member> members, Member member) {
        members.add(member)
    }
}
// 전투를 제어하는 클래스
class BattleManager {
    // 파티 멤버가 1명이라도 존재하는 경우 true를 리턴
    boolean membersAreAlive(List<Member> members) {
        boolean result = false;
        for (Member each : members) {
            if (each.isAlive()) {
                result = true;
                break;
            }
            return result;
        }
    }
}

 

① 컬렉션 처리를 캡슐화 하기

컬렉션과 관련된 응집도가 낮아지는 문제는 일급 컬렉션 패턴을 사용해 해결할 수 있다.

일급컬렉션(First Class Collection)이란 컬렉션과 관련된 로직을 캡슐화하는 디자인 패턴을 말한다.

 

클래스는 인스턴스 변수, 인스턴변수에 잘못된 값이 할당되지 않게 막고 정상적으로 조작하는 메서드가 필요하다.

따라서 일급 컬렉션은 다음과 같은 요소로 구성된다.

  • 컬렉션 자료형의 인스턴스 변수
  • 컬렉션 자료형의 인스턴스 변수에 잘못된 값이 할당되지 않게 막고, 정상적으로 조작하는 메서드

 

멤버 컬렉션 List<member>를 인스턴스 변수로 가지는 클래스 Party 작성

class Party {
    static final int MAX_MEMBER_COUNT = 4;
    private final List<Member> members;

    Party() {
        members = new ArrayList<Member>();
    }

    //Party의 List<Member>주입은 Party 클래스에만 하도록 제한    
    private Party(List<Member> members) {
        this.members = members;
    }

    //멤버 추가하기    
    Party add(final Member newMember) { //부수효과 막기 위해 Party로 return
        if (exists(newMember)) {
            throw new RuntimeException("이미 파티에 참가돼 있습니다");
        }
        if (isFull()) {
            throw new RuntimeException("이 이상 멤버를 추가할 수 없습니다");
        }

        // 새 인스턴스를 만들고 return해 기존 members 변수에 영향을 주지 않는다.(부수효과 방지)
        final List<Member> adding = new Arraylist<>(members);
        adding.add(newMember);
        return new Party(adding);
    }

    // 파티 멤버 1명이라도 살아있는지 확인
    boolean isAlive() {
        return members.stream().anyMatch(each -> each.isAlive());
    }

    // 이미 멤버가 파티에소속돼 있는지 확인
    boolean exists(Member member) {
        return members.stream().anymatch(each -> each.id == member.id);
    }

    // 파티 인원이 최대인지 확인
    boolean isFull() {
        return members.size() == MAX_MEMBER_COUNT;
    }
}

 

 

멤버 추가하기, 파티가 한 명이라도 살아있으면 true를 리턴하는 기능 등 로직을 모두 한 곳에 모아 클래스를 정의하면 응집도가 높은 설계가 가능하다.

 

② 외부로 전달할 때 컬렉션의 변경 막기

파티 멤버 전원의 상태 화면을 표시하는 기능을 만드려면 List<Member>에 접근해 전체 데이터를 참조할 수 있어야 한다. 하지만 인스턴스 변수를 외부로 전달하면 Party 클래스 외부에서 마음대로 멤버를 추가하거나 제거할 수 있다.

따라서 외부로 전달할 땐 컬렉션 요소를 변경하지 못하게 막아야 한다.

 

Java엔 unmodifiableList() 즉, 읽기 전용으로 리턴해 클래스 외부에서 마음대로 컬렉션을 조작하는 상황을 방지할 수 있다.

class Party {
    List<Member> members() {
        return members.unmodifiableList();
    }
}