본문 바로가기

JAVA/JAVA 기초

자바 코딩의 기술 - #2 코딩 스타일

반응형

2.1 매직 넘버를 상수로 대체

void setPreset(int speedPreset) {
    if (speedPreset == 2) {
        setTargetSpeedKmh(16944);
    } else if (speedPreset == 1) {
        setTargetSpeedKmh(7667);
    } else  if (speedPreset == 0) {
        setTargetSpeedKmh(0);
    }
}

변경

static final int STOP_PRESET = 0;
static final int PLANETARY_SPEED_PRESET = 1;
static final int CRUISE_SPEED_PRESET = 2;

static final double STOP_SPEED_KMH = 0;
static final double PLANETARY_SPEED_KMH = 7667;
static final double CRUISE_SPEED_KMH = 16944;


void setPreset(int speedPreset) {
    if (speedPreset == CRUISE_SPEED_PRESET) {
        setTargetSpeedKmh(CRUISE_SPEED_KMH);
    } else if (speedPreset == PLANETARY_SPEED_PRESET) {
        setTargetSpeedKmh(PLANETARY_SPEED_KMH);
    } else  if (speedPreset == STOP_PRESET) {
        setTargetSpeedKmh(STOP_SPEED_KMH);
    }
}
  • 상수
  • 변수를 딱 한번만 존재하게 (static) 하고 변경될수 없게 (final) 강제.

2.2 정수 상수 대신 열거형

static final int STOP_PRESET = 0;
static final int PLANETARY_SPEED_PRESET = 1;
static final int CRUISE_SPEED_PRESET = 2;

static final double STOP_SPEED_KMH = 0;
static final double PLANETARY_SPEED_KMH = 7667;
static final double CRUISE_SPEED_KMH = 16944;

private double targetSpeedKmh;

void setPreset(int speedPreset) {
    if (speedPreset == CRUISE_SPEED_PRESET) {
        setTargetSpeedKmh(CRUISE_SPEED_KMH);
    } else if (speedPreset == PLANETARY_SPEED_PRESET) {
        setTargetSpeedKmh(PLANETARY_SPEED_KMH);
    } else  if (speedPreset == STOP_PRESET) {
        setTargetSpeedKmh(STOP_SPEED_KMH);
    }
}

void setTargetSpeedKmh(double speed) {
    targetSpeedKmh = speed;
}

변경

enum SpeedPreset {
    STOP(0), PLANETARY_SPEED(7667), CRUISE_SPEED(16944);

    final double speedKmh;

    SpeedPreset (double speedKmh) {
        this.speedKmh = speedKmh;
    }
}


private double targetSpeedKmh;

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

    setTargetSpeedKmh(speedPreset.speedKmh);
}

void setTargetSpeedKmh(double speedKmh) {
    targetSpeedKmh = speedKmh;
}
  • 유효하지 않는 입력값을 막는데 유용하다.
  • 가능한 옵션을 모두 열거할수 있다면 항상 정수 대신 enum 타입을 사용.
  • if-else 블록 제거.

2.3 For 루프 대신 For-Each

List<String> checks = Arrays.asList("Cabin Pressure", "Communication", "Engine");

Status prepareForTakeoff(Commander commander) {
    for (int i=0; i<checks.size(); i++) {
        boolean shouldAbortTakeoff = commander.isFailing(checks.get(i));
        if (shouldAbortTakeoff) {
            return Status.ABORT_TAKE_OFF;
        }
    }

    return Status.READY_FOR_TAKE_OFF;
}

변경

List<String> checks = Arrays.asList("Cabin Pressure", "Communication", "Engine");

Status prepareForTakeoff(Commander commander) {    
    for (String check : checks) {
        boolean shouldAbortTakeoff = commander.isFailing(check);
        if (shouldAbortTakeoff) {
            return Status.ABORT_TAKE_OFF;
        }
    }

    return Status.READY_FOR_TAKE_OFF;
}

리스트 내 다음 원소에 접근할때가 아니면 인덱스를 쓰지 않는다.
인덱스를 계속 추적할 필요가 없다.
또한 인덱스 변수에는 실수의 여지가 있다.
protected가 아니니 언제든지 덮어쓸수 있음.
<대신 <= 경우 IndexOutOfBoundsExceptions 발생 가능성

매 반복마다 자바는 자료 구조에서 새로운 객체를 가져와 check에 할당한다. (반복인덱스를 다루지 않아도 됨)
배열과 Set 처럼 인덱싱되지 않는 컬렉션에도 동작한다.

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

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

void disposeContaminatedSupplies() {
    for(Supply supply : supplies) {
        if(supply.isContaminated()) {
            supplies.remove(supply);
        }
    }
}
  • 다양한 자료 구조를 순회한다.
  • 대부분 자료구조를 읽기만한다. (찾는 작업)
  • 자료구조를 바꾸려면 조심해야 한다. (충돌 위험)

supplies를 순회하는 for 루프 안에서 supplies.remove(supply)를 호출할때 문제 발생.

  • List 인터페이스의 표준 구현이나 Set이나 Queue와 같은 Collection 인터페이스의 구현은 ConcurrentModificationException을 던진다.
  • List를 순회하면서 List를 수정할수 없다.
  • Collection을 순회하는 동안 그 컬렉션을 수정한다는 뜻이다.
  • 자바의 컴파일 타임 검사로 못 잡는다.
private List<Supply> supplies = new ArrayList<>();

void disposeContaminatedSupplies() {
    Iterator<Supply> iterator = supplies.iterator();
    while(iterator.hasNext()) {
        if (iterator.next().isContaminated()) {
            iterator.remove();
        }
    }
}
  • 리스트를 순회하면서 제품을 찾고 그후에 발견했던 제품을 모두 제거하는것이다.

  • 먼저 순회를 하고 나중에 수정하는 두단계로 접근법이다.

  • 순회하는 동안 바뀐 제품을 임시 자료 구조에 저장해야 한다. (시간과 메모리 비용)

  • Iterator를 활용하는 while 루프로 순회방식을 사용한다.

  • Iterator는 첫번째 원소부터 시작해서 리스트 내 원소를 가리키는 포인터처럼 동작한다.

  • hasNext()를 통해 원소가 남았는지 묻고 next()로 다음 원소를 얻고 반환된 마지막 원소를 remove()로 안전하게 제거한다.

  • CopyOnWriteArrayList와 같은 특수 List 구현은 순회하면서 수정하기도 한다.

  • 리스트에 원소를 추가하거나 제거할때마다 매번 전체 리스트를 복사하고 한다.

  • 람다를 사용하는 Collection.removeIf() 메서드를 사용할수 있다.

2.5 순회하면서 계산 집약적 연산하지 않기

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

List<Supply> find(String regex) {
    List<Supply> result = new LinkedList<>();
    for(Supply supply : supplies) {
        if (Pattern.machers(regex, supply.toString())) {
            result.add(supply);
        }
    }
}
  • 순회할때 수행할 연산에 주의해야 한다.
  • 계산 집약적 연산을 수행하면 성능 위험이 초래할수 있다.
  • 정적 메서드인 machers()를 호출하면 정규식인 String과 검색할 String을 제공하는 방식이다.

Pattern.machers(regex, supply.toString())는 오토마톤을 컴파일해 supply.toString()과 부합시킨다.

  • 정규식 컴파일은 클래스 컴파일처럼 시간과 처리 전력을 소모한다.
  • 일회성 동작이지만 반복할때마다 정규식을 컴파일한다.
public final class Pattern implements java.io.Serializable {
    //..
    public static boolean matches(String regex, CharSequence input) {
        Pattern p = Pattern.compile(regex);
        Matcher m = p.matcher(input);
        return m.matches();
    }
}
  • String.replaceAll() 처럼 자바 API 내 유명한 메서드도 똑같이 동작하는 여러 경우가 있다.
public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
    //..
    public String replaceAll(String regex, String replacement) {
        return Pattern.compile(regex).matcher(this).replaceAll(replacement);
    }
}
private List<Supply> supplies = new ArrayList<>();

List<Supply> find(String regex) {
    List<Supply> result = new LinkedList<>();
    Pattern pattern = Pattern.compile(regex);
    for(Supply supply : supplies) {
        if (pattern.machers(supply.toString()).matches()) {
            result.add(supply);
        }
    }
}
  • 계산이 많이 필요한 연산은 가능한 적게 한다.
  • 정규식을 딱 한번만 컴파일하면된다.
  • 반복해도 표현식 문자열은 바뀌지 않는다.

Pattern.machers() 호출에 들어 있는 두 연산을 분리

  1. 표현식 컴파일
    Pattern p = Pattern.compile(regex);
  • 정규식을 생성한다.
  • 계산이 많이 필요한 단계이므로 지역변수에 저장한다.
  1. 검색 문자열을 실행
    Matcher m = p.matcher(input);
    m.matches();
  • 컴파일된 표현식을 실행은 쉽고 빠른 연산이다.

2.6 새 줄로 그루핑

enum DistanceUnit {
    MILES, KILOMETERS;

    static final double MILE_IN_KILOMETERS = 1.60934;
    static final int IDENTITY = 1
    static final double KILOMETER_IN_MILES = 1 /  MILE_IN_KILOMETERS;

    double  getConversionRate(DistanceUnit unit) {
        if (this == unit) {
            return IDENTITY;
        }        
        if (this == MILES && unit == KILOMETERS) {
            result MILE_IN_KILOMETERS;
        } else {
            return KILOMETER_IN_MILES;
        }
    }
}
  • 코드 블록이 서로 붙어 있으면 한 덩어리로 간주한다.
  • 별개 블록을 새로줄로 분리하면 코드 이해도를 높일수 있다.
enum DistanceUnit {
    MILES, KILOMETERS;

    static final int IDENTITY = 1;

    static final double MILE_IN_KILOMETERS = 1.60934;
    static final double KILOMETER_IN_MILES = 1 /  MILE_IN_KILOMETERS;

    double  getConversionRate(DistanceUnit unit) {
        if (this == unit) {
            return IDENTITY;
        }

        if (this == MILES && unit == KILOMETERS) {
            result MILE_IN_KILOMETERS;
        } else {
            return KILOMETER_IN_MILES;
        }
    }
}

IDENTITY 필드를 다른 상수들과 분리.

  • IDENTITY 필드는 특정 단위와는 독립적이고 마일과 킬로미터 간 두변환률보다 더 추상적이다.

두 if 블록을 서로 분리.

  • 첫번째는 같은 단위인지 확인
  • 두번째는 변환

2.7 이어붙이기 대신 서식화

String entry = author.toUpperCase() + ": [" + formattedMonth + "-" +
    today.getDayOfMonth() + "-" + today.getYear() + "](Day " +
    (ChronoUnit.DAYS.between(start, today) + 1) +")> " +
    message + System.lineSeparator();
  • 긴 문자열을 생성할때는 서식 문자열을 사용하면 읽기 더 쉽다.
String entry = String.format("%S: [%tm-%<te-%<tY](Day %d)> %s%n",
        author,
        today,
        ChronoUnit.DAYS.between(start, today) + 1, message);
  • String 레이아웃(어떻게 출력할지)와 데이터(무엇을 출력할지)를 분리하는것.
  • 서식 문자열은 %로 표기하는 특수위치 지정자 문자를 사용해 하나의 블록으로 일관된 String을 정의한다.

포맷형식

  • %S 대문자
  • %tm 월
  • %te 날짜
  • %tY 년도
  • <문자를 추가함으로써ㅓ 위치 지정자 세개가 같은 입력 데이터를 읽게 한다.
  • %d 10진수
  • %s 문자
  • %n 행바꿈
  • 문자열이 길면 StringTemplate을 쓴다.

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

class Inventory {

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

    int getQuantity(Supply supply) {
        if (supply == null) {
            throw new NullPointerException("supply must not be null");
        }

        int quantity = 0 ;
        for (Supply supplyInStock : supplies) {
            if (supply.equals(supplyInStock)) {
                quantity++;
            }
        }
        result quantity;
    }
}
  • API에 있는 기능을 다시 구현하지 말고 가능하면 재사용해야 한다.
  • 전문가들이 끊임없이 자바 API를 작성하고 최적화하면서 빠르고 버그도 거의 없는 표준 라이브러리가 만들어지고 있다.
class Inventory {

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

    int getQuantity(Supply supply) {
        Objects.requireNonNull(supply, "supply must not be null");

        return Collections.frequency(supplies, supply);
    }
}
  • API를 알면 일반적으로 코드의 문제를 훨씬 더 간단히 해결할수 있다.

 

자바의 코딩의 기술

사이먼 하러, 요르기 레너드, 심누스 디에츠 지음

심지현 옮김

반응형

'JAVA > JAVA 기초' 카테고리의 다른 글

자바 코딩의 기술 - #7 객체 디자인  (0) 2021.12.07
병렬 프로그래밍  (0) 2021.08.18
자바 코딩의 기술 - #1 코드 정리  (0) 2020.11.26
자바 코딩 규약  (0) 2019.11.26
Object의 clone() 복사  (0) 2019.03.12