오류 처리

오류 처리는 프로그램에 반드시 필요한 요소 중 하나일 뿐이다.
입력이나 디바이스가 실패할 수도 있기 때문이다.
내 생각엔 틀릴 수 없는 코드더라도 분명 가능성은 늘 존재한다.

여기저기 흩어진 오류 처리 코드로 인해 실제 코드가 하는 일을 파악하기가 어려울 수 있다.
오류 처리 코드로 인해 논리를 이해하기 어렵다면 깨끗한 코드가 아니다.

오류 코드보다 예외를 사용하라

Try-Catch-Finally 문부터 작성하라

try 블록에 들어가는 코드를 실행하면 어느 시점에서든 catch 블록으로 넘어갈 수 있다.

미확인 예외를 사용하라

함수1 -> 함수2 -> 함수3 -> 함수4(새로운 예외 발생)
이 때,

1) 함수3 부터 함수2, 함수1 까지 새로운 예외를 catch로 처리해야 하거나,
2) throw 키워드를 모두 붙여줘야 한다.
이러한 건 OOP의 OCP(개발-폐쇄 원칙)을 위반한다.

개방-폐쇄 원칙
객체는 확장에는 개발, 수정에는 폐쇄되어야 한다.

-> (하지만, 확인된 예외를 처리함으로써 어떤 예외인지 알아채기 쉽고, 보다 유지보수하기가 편리해지는 것 아닐까? 하는 생각이 든다)

예외에 의미를 제공하라

오류 메세지에 정보를 담아 예외와 함께 던진다.
실패한 연산 이름, 실패 유형 등...
오류가 발생한 원인과 위치를 더 찾기 쉬워진다.

호출자를 고려해 예외 클래스를 정의하라

정상 흐름을 정의하라

앞 절에서의 지침들을 따른다면 비즈니스 논리와 오류 처리가 잘 분리된 코드가 나온다.

/*
* 식비를 비용으로 청구했다면 직원이 청구한 식비를 총계에 더한다.
* 식비를 비용으로 청구하지 않았다면 일일 기본 식비를 총계에 더한다.
*/
//1번
try{
    MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
    m_total += expenses.getTotal();
} catch(MealExpensesNotFound e){
    m_total += getMealPerDiem();
}

//2번
public class PerDiemMealExpenses implements MealExpenses{
    public int getTotal(){
        //청구값이 있다면 청구값을 반환
        //청구값이 없다면 기본값을 반환
    }
}

MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
m_total += expenses.getTotal();

1번에서 2번으로 변경하게 되면 예외 상황을 처리할 필요가 없어진다.

null을 반환하지 마라

반환 타입이 nullable이라면 지속적으로 null check를 해야 한다.
만약 1번이라도 null check 코드가 빠졌다면 NPE가 발생한다.
nullable 타입을 반환하기보다 예외나 특수 사례 객체를 반환하라
외부 객체가 null을 반환한다면 감싸기 메서드를 구현해 예외를 던지거나 특수 사례 객체를 반환하라

//1번
List<Employee> employees = getEmployees();
if (employees != null){
    for(Employee e : employees){
        totalPays += e.getPay();
    }
}

//2번
List<Employee> employees = getEmployees();
for(Employee e : employees){
    totalPays += e.getPay();
}

굳이 null을 반환해야 할까?, 빈 리스트를 넘겨서 로직을 수행하지 않아도 되지 않은가?
이로써 쓸데없는 코드 2줄이 줄고, 보기에도 깔끔해졌다.

null을 전달하지 마라

메서드에서 null을 반환하는 방식도 나쁘지만, 메서드로 null을 전달하는 방식은 더 나쁘다.

대다수 프로그래밍 언어는 호출자가 실수로 null을 넘기면 적절히 처리하는 방법이 없다.
애초에 null을 넘기지 못하도록 금지하는 정책이 합리적이다.
인수로 null이 넘어오면 코드에 문제가 있다는 뜻이다.

결론

깨끗한 코드는 읽기도 좋아야 하지만 안정성도 높아야 한다.
이 둘은 상충하는 목표가 아니다.
오류 처리를 프로그램 논리와 분리해 독자적인 사안으로 고려하면 튼튼하고 깨끗한 코드를 작성할 수 있다.

객체와 자료 구조

변수를 private으로 선언하는 이유는 남들이 변수에 의존하지 않게 만들고 싶어서다.
변수 타입, 구현을 맘대로 바꾸고 싶어서다.

자료 추상화

두 코드를 비교해보자
한 클래스는 구현을 외부로 노출하고 다른 클래스는 구현을 완전히 숨긴다.

//1번 - 구체적인 Point Class
public class Point{
    public double x;
    public double y;
}

//2번 - 추상적인 Point Class
public interface Point{
    double getX();
    double getY();
    void setCartesian(double x, double y);
    double getR();
    double getTheta();
    boid setPolar(double r, double theta);
}

2번은 직교좌표계인지 극좌표계인지 모른다.
자료 구조 이상을 표현한다. 클래스 메서드가 접근 정책을 강제한다.
1번은 직교좌표계를 사용한다. 개별적으로 값을 읽고 설정하게 강제한다.
구현을 노출한다. -> 변수를 private으로 하더라도 get/set이 노출된다면 구현이 외부로 노출되는 셈이다.

//3번
public interface Vehicle{
    double getFuelTankCapacityInGallons();
    double getGallonsOfGasoline();
}

//4번
public interface Vehicle{
    double getPercentFuelRemaining();
}

3번가 4번 사이에서는 4번이 더 좋다.
자료를 세세하게 공개하기보다는 추상적인 개념으로 표현하는 편이 좋다.

자료/객체 비대칭

앞서 소개한 두 예제는 객체와 자료구조의 차이를 보여준다.
객체는 추상화 뒤로 자료를 숨긴채 자료를 다루는 함수만 공개한다.
자료구조는 자료를 그대로 공개하며 별다른 함수는 제공하지 않는다.

//절차적인 코드
public class Rectangle{
    public Point topLeft;
    public double height;
    public double width;
}

public class Circle{
    public Point center;
    public double radius;
}

public class Geometry{
    public double area(Object shape) throws NoSuchShapeException{
        if (shape instanceof Rectangle){
            Rectangle r = (Rectangle)shape;
            return r.height * r.width;
        }else if(shape instanceof Circle){
            Circle c = (Circle)shape;
            return 3.14 * c.radius * c.radius;
        }else{
            throw new NoSuchShapeException();
        }
    }
}

//객체 지향 코드
public class Rectangle implements Shape{
    private Point topLeft;
    private double height;
    private double width;
    public double area(){
        return height*width;
    }
}

public class Circle implements Shape{
    private Point center;
    private double radius;

    public double area(){
        return 3.14 * radius * radius;
    }
}

객체 지향 vs 절차적인 코드

  • 객체 지향 코드에서는 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉽다.

  • 절차적인 코드에서는 기존 자료 구조를 변경하지 않으면서 새 함수를 추가하기 쉽다.

  • 객체 지향 코드에서는 새로운 함수를 추가하기 어렵다. 그러려면 모든 클래스를 고쳐야 한다.

  • 절차적인 코드에서는 새로운 자료 구조를 추가하기 어렵다. 그러려면 모든 함수를 고쳐야 한다.

개발을 하다보면 새로운 함수가 아니라 자료 타입이 필요한 경우가 필요하다 -> 객체 지향
새로운 자료 타입이 아닌 새로운 함수가 필요하다 -> 절차적인 코드 + 자료구조

디미터 법칙

디미터 법칙은 휴리스틱으로 모듈은 자신이 조작하는 객체의 속사정을 몰라야 한다는 법칙이다.
객체는 자료를 숨기고 함수를 제공한다. 객체는 조회 함수로 내부 구조를 공개하면 안된다는 의미다.

클래스 C의 메서드 f는 다음과 같은 객체의 메서드만 호출해야 한다

  1. 클래스 C
  2. f가 생성한 객체
  3. f 인수로 넘어온 객체
  4. C 인스턴스 변수(프로퍼티)에 저장된 객체

final String outputDir = ctxt.getOptions().getScratchDir().getAbsolutePath();

기차 충돌

위와 같은 코드를 기차 충돌이라 부른다.
위 코드는 나누는 편이 좋다.

Options opts = ctxt.getOptions();
File scratchDir = opts.getScratchDir();
final Strin goutputDir = scratchDir.getAbsolutePath();

자료 전달 객체

자료 구조체의 전형적인 형태는 공개 변수만 있고 함수가 없는 클래스다.
때로는 DTO라 일컫는다.
특히 DB와 통신 or 소켓으로 받은 Msg를 분석할 때 유용하다.

-> DTO는 자료구조이므로 함수를 제공하지 않는다.

결론

객체는 동작을 공개하고 자료를 숨긴다.
기존 동작을 변경하지 않으면서 새 객체 타입을 추가하기는 쉽다.
하지만 기존 객체에 새 동작을 추가하기는 어렵다.

자료구조는 별다른 동작없이 자료를 노출한다.
그래서 기존 자료 구조에 새 동작을 추가하기는 쉽다.
하지만 기존 함수에 새 자료 구조를 추가하기는 어렵다.

형식 맞추기

팀으로 진행한다면 팀에서 합의한 규칙을 정하고 팀원 모두가 그 규칙을 따라야 한다.

형식을 맞추는 목적

코드 형식은 중요하다! 너무 중요하다!
'돌아가는 코드'가 개발자의 1차원적인 의무일 수도 있지만, 사실상 오늘 구현한 기능은 다음 버전에서 바뀔 확률은 아주 높다.
그런데 오늘 구현한 코드의 가독성은 앞으로 바뀔 코드의 품질에 거대한 영향을 준다.
코드가 지속적으로 바뀌어도 처음 잡아놓은 스타일과 가독성 수준은 이후에도 계속 영향을 끼친다.

적절한 행 길이를 유지하라.

개념은 빈 행(줄바꿈)으로 분리하라

일련의 행 묶음은 생각 하나를 표현한다.
생각 사이사이는 빈 행을 넣어 분리해야 마땅하다.

세로 밀집도

수직거리

이 함수에서 호출하는 다른 함수를 찾기 위해 미로같은 코드를 뒤진 경험이 있는가?
이 조각, 저 조각이 어디에 있는지 찾고 기억하는 데에 시간과 노력이 너무 많이 든다.
-> 서로 밀접한 개념은 세로로 가까이 둬야한다.

팀 규칙

프로그래머라면 각자 선호하는 규칙이 있다. 하지만 해당 팀에 속한다면 팀의 규칙에 따라야한다.
소프트웨어 시스템의 스타일은 일관적이고 매끄러워야 한다.
여러 스타일이 섞인 시스템은 독자에게 신뢰감을 주지 않는다.

주석

잘 달린 주석은 어떤 정보보다 유용하다. 하지만 경솔하고 근거없는 주석은 코드를 이해하기 어렵게 만든다
사실 주석이 필요한 코드는 실패다. 코드만으로 의도를 표현해야 한다. 이를 실패했기 때문에, 주석을 다는 것이다.

코드만이 정확한 정보를 제공하는 유일한 출처다

주석은 나쁜 코드를 보완하지 못한다

엉망인 코드에 주석을 다는 것보다 엉망인 코드를 손보는 것이 훨씬 좋다.

코드로 의도를 표현하라

코드만으로 의도를 설명하기 어려운 경우가 존재한다.

// 직원이 복지 혜택을 받을 자격이 있는지 검사한다
if((employee.flags & HOURLY_FLAG) && (employee.age > 65)){
    ...
}
VS
if(employee.isEligibleForFullBenefits())

좋은 주석

어떤 주석은 필요하거나 유익하다.

법적인 주석

정보를 제공하는 주석

의도를 설명하는 주석

의미를 명료하게 밝히는 주석

a.compareTo(a) == 0; // a==a
a.compareTo(b) != 0; // a!=b

결과를 경고하는 주석

  1. //여유 시간이 충분하지 않으면 실행하지 마십시오.
    최근에는 @Ignore("실행이 너무 오래걸린다.") 라는 Anotation을 이용해 테스트 케이스를 꺼버리지만, 이전에는 주석을 달았다.

  2. //해당 객체는 스레드에 안전하지 못하다.
    // 따라서 각 인스턴스를 독립적으로 생성해야 한다.

TODO 주석

실제로 나도 자주 사용한다.
요즘 IDE는 TODO 주석을 찾아주는 기능을 제공해서 잊어버릴 염려는 없다. 그래도 너무 많은 TODO 주석은 좋지 않다.

중요성을 강조하는 주석

//여기서 a라는 객체는 정말 중요하다. a의 역할은 이며 ~으로 사용해야 한다.

나쁜 주석

위에서 언급되지 않은 주석들이 나쁜 주석이다.
좋지 않은 코드/실패한 코드를 커버하기 위한 주석 들이 대다수다.

주절거리는 주석

같은 이야기를 중복하는 주석

오해할 여지가 있는 주석

의무적으로 다는 주석

  • 코드로도 충분히 이해할 수 있지만, 의무적으로 주석을 남겨 더 헷갈릴 여지를 주는 주석

+ Recent posts