ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 7. 컬렉션 : 중첩을 제거하는 구조화 테크닉
    Study/내 코드가 그렇게 이상한가요? 2024. 1. 16. 21:48
    반응형

    이 장에서는 배열과 List와 같은 컬렉션에서 발생할 수 있는 다양한 문제 사례들을 알아본다.

    1. 이미 존재하는 기능을 다시 구현하지 말기

    바퀴의 재발명
    이미 널리 사용되고 있는 기술과 해법이 존재하는데도, 이를 전혀 모르거나 의도적으로 무시하고 비슷한 것을 새로 만들어 내는 것

    대부분의 언어에서는 표준 컬렉션 라이브러리에 많은 기능이 구현되어 있다. 반복문을 사용해서 컬렉션을 직접 조작하고 있다면, 잠시 멈추고 표준 라이브러리에 같은 기능을 하는 메서드가 있는지 확인해보자.

    // 반복문을 사용하여 List를 직접 조작하는 예시
    boolean hasPrisonKey = false;
    for (Item each : items) {
        if (each.name.equals("prisonKey")) {
            hasPrisonKey = true;
            break;
        }
    }
    
    // 자바 표준 컬렉션 라이브러리 메서드 사용(anyMatch)
    boolean hasPrisonKey = items.stream().anyMatch(
        item -> item.name.equals("prisonKey");
    )

    2. 반복문 내부 조건 분기 중첩 해결하기

    // 멤버의 상태가 중독인 경우 hitPoint를 감소시키는 로직
    for (Member member : members) {
        if (0 < member.hitPoint) {
            if (member.containState(StateType.poison)) {
                member.hitPoint -= 10;
                if (member.hitPoint <= 0) {
                    member.hitPoint = 0;
                    member.addState(StateType.dead);
                    member.removeState(StateType.poison);
                }
            }
        }
    }

    위와 같이 반복문 내에서 중첩된 조건 분기는 가독성이 좋지 않다. 이러한 조건 분기 중첩을 해결하는 방법을 살펴보자.

    2.1 조기 continue로 제거하기

    continue는 실행하고 있는 처리를 건너 뛰고, 다음 반복으로 넘어가는 제어 구문이다.

    // 조기 continue로 조건 분기 중첩이 제거된 코드
    for (Member member : members) {
        if (member.hitPoint == 0) continue;	// 살아있지 않다면 건너뛰기
        if (!member.containsState(StateType.poision)) continue;	// 중독 상태가 아니라면 건너뛰기
        
        member.hitPoint -= 10;
        
        if (0 < member.hitPoint) continue;
        member.hitPoint = 0;
        member.addState(StateType.dead);
        member.removeState(StateType.poison);
    }

    다만, 조기 continue를 사용할 때는 기존 조건을 반전시켜 주어야 한다.

    • 기존 조건 : hitPoint가 0보다 크다면 조건 분기 실행
    • 변경 조건 : hitPoint가 0보다 같거나 작다면 continue

    2.2 조기 break로 제거하기

    break는 처리를 중단하고, 반복문 전체를 벗어나는 제어 구문이다. 반복문의 전체 반복이 하나의 조건에 영향을 받는다면, 해당 조건이 성사되지 않은 경우 break로 전체 반복문을 벗어나게 만들 수 있다.

    3. 응집도가 낮은 컬렉션 처리

    컬렉션 처리에서도 역시 하나의 컬렉션 처리를 여러 곳에서 구현하는 등 응집도가 낮은 코드가 발생할 수 있다. 이를 해결할 수 있는 방법을 알아보자.

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

    일급 컬렉션(퍼스트 클래스 컬렉션, First Class Collection) 패턴을 사용하여 컬렉션과 관련된 응집도가 낮아지는 문제를 해결할 수 있다. 

    일급 컬렉션 패턴 : 컬렉션과 관련된 로직을 캡슐화하는 디자인 패턴

    클래스 설계 원리를 반영하여 생각하면, 일급 컬렉션은 다음과 같은 요소로 구성되어야 한다.

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

    이에 따라, 컬렉션과 컬렉션을 조작하는 로직을 한 클래스에 응집하도록 설계한다.

    class Party {
        static final int MAX_MEMBER_COUNT = 4;
        private final List<Member> members;	// 컬렉션 자료형의 인스턴스 변수
        
        Party() {
            members = new ArrayList<Member>();
        }
        
        private Party(List<Member> members) {
            this.members = members;
        }
        
        
        // 컬렉션 자료형의 인스턴스 변수 members와 관련된 로직을 Party 클래스로 모은다.
        
        /**
        * 멤버 추가하기
        */
        Party add(final Member newMember) {
            if (exists(newMember)) {
                throw new RuntimeException("이미 파티에 참가되어 있습니다.");
            }
            if (isFull()) {
                throw new RuntimeException("이 이상 멤버를 추가할 수 없습니다.");
            }
            
            // 인스턴스 변수를 직접 변경하는 것에서 오는 부수효과를 막기 위해 새로운 리스트를 생성하여 리턴한다.
            final List<Member> adding = new ArrayList<>(members);
            adding.add(newMember);
            return new Party(adding);
        }
        
        boolean isAlive() { ... }
        
        boolean exists(final Member member) { ... }
        
        boolean isFull() { ... }
    }

    위와 같이 컬렉션을 사용하는 설계에서도 응집도가 높은 구성을 만들어야 한다.

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

    위 3.1에서와 같이 Party 클래스를 응집도가 높게 설계를 했더라도, 외부에 members 인스턴스 변수를 그대로 전달하면 Party 클래스 외부에서 마음대로 멤버를 추가하고 제거해 버릴 수 있다. 이러한 사용은 응집도가 낮을 때와 아무 차이가 없게 만든다.

    // Party 클래스 외부에서 members 리스트를 마음대로 조작하는 상황
    members = party.members();
    members.add(newMember);
    ...
    members.clear();

    이러한 상황을 막고 진정한 의미의 응집도가 높은 코드를 만들기 위해서, 외부로 전달할 때는 컬렉션의 요소를 변경하지 못하게 막아 두는 것이 좋다. 이 때 unmodifiableList 메서드를 사용한다. (기존에 인스턴스 변수에 final 키워드를 사용하던 것과 같은 맥락이다.)

    // 외부에서 변경하지 못하도록 불변 리스트로 만들어서 전달하기
    class Party {
        ...
        List<Member> members() {
            return members.unmodifiableList();
        }
    }

    unmodifiableList로 리턴되는 컬렉션은 요소를 추가하거나 제거할 수 없다. 따라서 Party 클래스 외부에서 마음대로 컬렉션을 조작하는 상황 자체를 방지할 수 있다.

    반응형
Designed by Tistory.