[자바 코딩의 기술] 2. 코드 스타일, 자바 API 다듬기

3개월차 신입 비전공 개발자가 읽기 좋은 코드를 만들어보고자 공부하는 내용입니다. 부족하거나 새롭게 공부해보면 좋을 것 같은 추천, 글에서 발견된 문제에 대한 이야기는 언제든 환영합니다!

1. 매직 넘버를 상수로 대체

@Setter
public class ExampleService {
    
    private double targetSpeed;
    
    public void setPreset(int speedPreset) {
        if (speedPreset == 2) {
            setTargetSpeed(17944);     
        }
        if (speedPreset == 1) {
            setTargetSpeed(6767);
        }
        if (speedPreset == 0) {
            setTargetSpeed(0);
        }
    }
}
  • 매직넘버 : 표면상 의미 없는 숫자지만 프로그램의 동작을 제어하는 숫자 → 매직넘버를 많이 쓰게 되면 코드의 이해도 어려워지고 오류가 발생할 가능성이 올라감
  • 다른 개발자가 임의의 값으로 변경하게 되면 바로 오류 발생
  • speedPreset에 의해 targetSpeed가 결정되는데 이 때 개발자들은 선택할 수 있는 speedPreset의 옵션을 알 수 없음 + 컴파일러는 코드가 알지 못하는 수를 입력하지 못하도록 막을 수 없음 → 코드가 난해해짐과 동시에 오류 발생 확률이 올라감
  • 매직 넘버를 상수로 대체해보자
@Setter
public class ExampleService {
    static final int STOP_SPEED_PRESET = 0;
    static final int PLANETARY_SPEED_PRESET = 1;
    static final int CRUISE_SPEED_PRESET = 2;

    static final int STOP_SPEED_KMH = 0;
    static final int PLANETARY_SPEED_KMH = 6767;
    static final int CRUISE_SPEED_KMH = 17944;

    private double targetSpeed;

    public void setPreset(int speedPreset) {
        if (speedPreset == CRUISE_SPEED_PRESET) {
            setTargetSpeed(CRUISE_SPEED_KMH);
        }
        if (speedPreset == PLANETARY_SPEED_PRESET) {
            setTargetSpeed(PLANETARY_SPEED_KMH);
        }
        if (speedPreset == STOP_SPEED_PRESET) {
            setTargetSpeed(STOP_SPEED_KMH);
        }
    }
}
  • 명확하고 의미 있는 이름을 사용하니 어떠한 설정값으로 어떤 속도값과 연결되는지 이해하기도 쉬워짐

2. ENUM 사용하기

  • 1번의 코드에서 setPreset에는 모든 정수가 들어갈 수 있음 → 즉, 내부에서 처리할 수 있는 0, 1, 2 외에도 다양한 변수값이 들어 올 수 있음 → 이 경우 메소드는 아무것도 반환하거나 처리하지 않겠지만 유효하지 않는 값이 들어왔다는 것 자체가 버그를 야기할 수 있음
  • enum을 사용하면 컴파일러가 유효하지 않는 값을 미리 거절할 수 있게 할 수 있음!!
  • 이제 enum을 사용한 코드를 보자
@Setter
public class ExampleService {

    private double targetSpeed;

    public void setPreset(SpeedPreset speedPreset) {
        Objects.requireNonNull(speedPreset);

        setTargetSpeed(speedPreset.speedKmh);
    }

    enum SpeedPreset {
        STOP(0),
        PLANETARY_SPEED(6767),
        CRUISE_SPEED_KMH(17944);

        final double speedKmh;

        SpeedPreset(double speedKmh) {
            this.speedKmh = speedKmh;
        }
    }
}
  • enum을 사용할 때의 중요한 장점 중 하나는 이제 유효하지 않는 speedPreset을 setPreset() 메소드에 넣을 수 없다는 것 → 유효하지 않는 녀석이 오면 바로 컴파일러가 중지시킴
  • 여기에 더해 여전히 매직 넘버 대신 의미 있는 이름으로 참조함으로써 1번에서 살펴보았던 문제를 같이 해결할 수 있음.
  • 또한 enum을 사용함으로써 if 블록들을 제거할 수 있음 → 코드가 간결해짐
  • 위의 예제처럼 가능한 모든 옵션을 열거할 수 있다면 항상 정수 타입 대신 enum을 쓰는 게 좋음!

3. For문 대신 For-each 구문 활용

  • 인덱스를 활용한 for문의 경우 인덱스 변수(i)를 사용하는 과정에서 실수가 발생할 여지가 있음
  • 인덱스 변수가 제공하는 정보를 자세히 알아야 하는 경우는 드뭄
  • for-each 구문의 경우 iterator를 사용
  • 인덱스 변수를 반복문 내에서 임의로 처리하지 않기 때문에 인덱스 변수를 사용할 때 발생하는 문제를 방지할 수 있음
  • 대부분의 경우에는 for-each 구문을 쓰는 게 좋지만 정말 아주 만에 하나라도 인덱스 반복문을 써야 할 때는 컬렉션의 특정 부분만 반복해야 하거나 명시적으로 인덱스 변수를 사용해야할 때 뿐!

4. 순회하면서 컬렉션 수정하지 않기

List<Supply> supplies = new ArrayList<>();

public void dispose() {
    for (Supply supply : supplies) {
        if (supply.isContamiated()) {
            supplies.remove(supply);
        }
    }
}
  • 이렇게 구현하게 되면 List의 표준 구현, Set이나 queue와 같은 collection은 concurrentModificationException을 던짐 → collection을 순회하는 동안 수정한다는 익셉션
  • 또한 하나의 supply라도 문제가 있을 경우 무조건 충돌이 발생 → 문제가 발생하기 전까지는 해당 코드가 문제를 일으키는 코드인지 확인할 수 없음
public void dispose() {
    Iterator<Supply> iterator = supplies.iterator();
    while (iterator.hasNext()) {
        if (iterator.next().isContamiated()) {
            iterator.remove();
        }
    }
}
  • iterator를 쓰면 먼저 순회하고 변질된 데이터를 찾고, 그 후에 변질된 데이터를 제거함.
  • iterator에 대한 공부 필요

5. 순환하며 계산 집약적 연산 하지 않기

public void dispose(String regex) {
    List<Supply> result = new ArrayList<>();
    for (Supply supply : supplies) {
        if (Pattern.matches(regex, supply.toString())) {
            result.add(supply);
        }
    }
}
  • 위의 코드를 보면 반복문이 돌 때마다 정규식을 컴파일 하고 있음 → 성능의 저하가 발생
  • 잠재적 성능 저하를 막기 위해서는 반복문 내에서 가능한 한 연산의 횟수를 줄이면 됨
  • 위의 예제에서는 메소드를 호출할 때 딱 한 번만 정규식을 컴파일 하면 됨
public void dispose(String regex) {
    List<Supply> result = new ArrayList<>();
    Pattern pattern = Pattern.compile(regex);
    for (Supply supply : supplies) {
        if (pattern.matcher(supply.toString()).matches()) {
            result.add(supply);
        }
    }
}

6. 새 줄로 그루핑

  • 코드 블록이 붙어 있으면 하나의 덩어리로 간주 → 별개의 블록을 새 줄로 분리하면 코드의 이해도와 가독성을 높일 수 있음
  • 그루핑할 때 참고하면 좋은 비유
    • 훌륭한 기사의 구조(article)
    • 제목(클래스명)
    • 세션 머릿말(공개 멤버, 생성자, 메소드)
    • 세부 내용(비공개 메소드)
    • 이 정도의 구조만 가지고 코드를 작성해도 읽을 때 해당 클래스를 더 명확하게 이해할 수 있음

7. 이어붙이기 대신 서식화

  • 문자열 혹은 결과를 내보낼 때 그 결과가 어떻게 생겨 먹었는지 이해하기 쉬워야 함
  • 서식 문자열로 이를 해결
  • 핵심은 String 레이아웃(어떻게 출력할지)와 데이터(무엇을 출력할지)를 구분하는 것

8. 직접 만들지 말고 자바 API 사용하기

  • 자바 클래스 라이브러리는 유용한 API가 가득 들어 있음 → 굳이 시간 들여서 뻘짓해서 API 만들 필요가 전혀 없음!
public int countFrequency(TestAVo aVo) {
    if (aVo == null) {
        throw new NullPointerException();
    }

    int frequency = 0;
    for (TestAVo eleAVo : aVoList) {
        if (aVo.equals(eleAVo)) {
            frequency++;
        }
    }
    return frequency;
}
  • 언뜻 보기에는 잘 만든 메소드처럼 보이지만 자바가 이미 이에 해당하는 내용을 쓸 수 있도록 다 API가 만들어져 있음
public int countFrequency(TestAVo aVo) {
    Objects.requireNonNull(aVo);

    return Collections.frequency(aVoList, aVo);
}
  • 단 2개의 자바 API 만으로 깔끔하게 처리 → 자바 API를 잘 활용하도록 하자!
  • 직접 작성한 코드는 버그를 유발할 가능성이 매우 높으며, 자바 API를 사용하면 기존 기능을 구현하지 않음으로써 시간을 절약할 수 있음
  • 개발하다가 더 나은 코드가 있을 것 같다는 느낌이 들면 그건 API로 반드시 존재함

좋은 웹페이지 즐겨찾기