ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Clean Code 7~8 장
    Backend/책 정리 2023. 3. 21. 00:41

    7장 - 오류 처리

    오류 처리는 프로그램에 반드시 필요한 요소 중 하나일 뿐이다. 해당 챕터에서는 우아하고 고상하게 오류를 처리하는 기법과 고려 사항 몇 가지를 소개하고 있다.

     

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

    오류가 발생하면 예외를 던지는 편이 낫다. 그러면 코드가 더 깔끔해진다.

     

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

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

    먼저 강제로 예외를 일으키는 테스트 케이스를 작성한 후 테스트를 통과하게 코드를 작성하는 방법을 권장한다. 그러면 자연스럽게 try 블록의 트랜잭션 범위부터 구현하게 되므로 범위 내에서 트랜잭션 본질을 유지하기 쉬워진다.

    public List<RecordedGrip> retrieveSection(String sectionName) {
    	try {
        	FileInputStream stream = new FileInputStream(sectionName)
        } catch (Exception e) {
        	throw new StorageException("retrieval error", e);
        }
        return new ArrayList<RecordedGrip>();
    }

     

    위 시점에서 먼저 리팩터링이 가능하다. catch 블록에서 예외 유형을 좁혀 실제로 FileInputStream 생성자가 던지는 FileNotFoundException을 잡아낸다.

     

    3. 미확인 예외를 사용하라(unchecked 예외)

    예전에는 확인된 예외를 멋진 아이디어라 생각했지만 현재는 미확인 예외를 사용하는 것이 정설로 여겨진다.

     

    우리는 확인된 오류가 치르는 비용에 상응하는 이익을 제공하는지 다져봐야 한다. 확인된 예외는 OCP(Open Closed Principle) 법칙을 위반한다. 메서드에서 확인된 예외를 던졌는데 catch 블록이 세 단계 위에 있다면 그 사이 메서드 모두가 선언부에 해당 예외를 정의해야 한다. 즉, 하위 단계에서 코드를 변경하면 상위 단계 메서드 선언부를 전부 고쳐야 한다는 의미이다.

     

    결과적으로 최하위 단계에서 최상위 단계까지 연쇄적인 수정이 일어날 수 있다!!!

     

    때로는 확인된 예외도 유용하다. 아주 중요한 라이브러리를 작성한다면 모든 예외를 잡아야 한다. 하지만 일반적인 애플리케이션은 의존성이라는 비용이 이익보다 크다. 추가로 비즈니스 로직상 일반적으로 확인된 예외의 경우 코드 상에서 catch로 잡아서 처리하는 부분이 많기 때문에 이런 부분에서는 확인된 예외를 이용하여 명확하게 표현하는 것이 더 좋을수도 있다.

     

    4. 예외에 의미를 제공하라

    예외를 던질 때는 전후 상황을 충분히 덧붙인다. 오류 메시지에 정보를 담아 예외와 함께 던진다. 실패한 연산 이름과 실패 유형도 언급한다. 애플리케이션이 로깅 기능을 사용한다면 catch 블록에서 오류를 기록하도록 충분한 정보를 넘겨준다.

     

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

    오류를 정의할 때 프로그래머에게 가장 중요한 관심사는 오류를 잡아내는 방법이 되어야 한다.

     

    아래 예시를 보자

    대다수 상황에서 우리가 오류를 처리하는 방식은 비교적 일정하다. 아래 코드에서 첫 번째 블럭의 코드를 보면 비슷한 형식으로 처리하는 것을 확인할 수 있다. -> 오류를 기록한다. 프로그램을 계속 수행해도 좋은지 확인한다.

     

    이때 두 번째 블럭의 코드 처럼 LocalPort 클래스로 ACMEPort를 감싸는 클래스는 매우 유용하다. 실제로 외부 API를 사용할 때는 감싸기 기법이 최선이다. 외부 API를 감싸면 외부 라이브러리와 프로그램 사이에서 의존성이 크게 줄어든다.

     

    마지막 장점으로 감싸기 기법을 사용하면 특정 업체가 API를 설계한 방식에 발목 잡히지 않는다. 프로그램이 사용하기 편리한 API를 정의하면 그만이다. 

     

    두 번째 블럭의 코드처럼 흔히 예외 클래스는 하나만 있어도 충분한 경우가 많다. 예외 클래스에 포함된 정보로 오류를 구분해도 괜찮은 경우가 그런 케이스이다. 한 예외는 잡아내고 다른 예외는 무시해도 괜찮은 경우라면 여러 예외 클래스를 사용한다.

     

    try {
    	port.open();
    } catch (DeviceREsponseException e) {
    	reportPortError(e);
        	logger.log("Device response exception", e);
    } catch (ATM1212UnlockedException e) {
    	reportPortError(e);
        	logger.log("unlock exception", e);
    } catch (GMXError e) {
    	reportPortError(e);
        	logger.log("Device response exception");
    } finall {
    	...
    }
    
    --------------------------------
    
    LocalPort port = new LocalPort(12);
    try {
    	port.open();
    } catch (PortDeviceFailure e) {
    	reportError(e);
        	logger.log(e.getMessage(), e);
    } finally {
    	...
    }
    
    public class LocalPort {
    	private ACMEPort innerPort;
        
        public LocalPort(int portNumber) {
        	innerPort = new ACMEPort(portNumber);
        }
        
        public void open() {
        	try {
            	port.open();
            } catch (DeviceREsponseException e) {
            	throw new PortDeviceFailure(e);
            } catch (ATM1212UnlockedException e) {
            	throw new PortDeviceFailure(e);
            } catch (GMXError e) {
            	throw new PortDeviceFailure(e);
            }
        }
        ...
    }

     

    6. 정상 흐름을 정의하라

    외부 API를 감싸 독자적인 예외를 던지고, 코드 위에 처리기(catch 블럭)를 정의해 중단된 계산을 처리한다. 대개는 멋진 처리 방식이지만, 때로는 중단이 적합하지 않은 때도 있다. 예제를 살펴보자

     

    첫 번째 코드에서 getMeals() 메서드를 항상 MealExpenses 객체를 반환하도록 수정하는 것이다. 만약 catch 블럭에 해당하는 에러가 발생한다면 아래에서 정의한 PerDiemMealExpenses 이스턴스를 반환해서 기존에 catch 블럭에 있던 로직을 getTotal() 메서드로 정의해서 사용하도록 코드를 수정할 수 있다.

     

    try {
    	MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
    	m_total += expenses.getTotal();
    } catch (MealExpensesNotFound e) {
    	m_total += getMealPerDiem();
    }
    
    ------------------
    
    MealExpenses expenses = expenseReportDAO.getMeals(employee.getID());
    m_total += expenses.getTotal();
    
    
    public class PerDiemMealExpenses implements MealExpenses {
    	public int getTotal() {
    		// 위에서 catch로 잡은 부분의 로직을 실행한다.
    	}
    }

     

    7. null을 반환하지 마라

    우리가 흔히 저지르는 오류를 유발하는 행위 중 첫째가 null을 반환하는 습관이다.

    null을 반환하는 코드는 일거리를 늘릴 뿐만 아니라 호출자에게 문제를 떠넘긴다. 누구 하나라도 null 확인을 빼먹는다면 애플리케이션이 통제 불능에 빠질지도 모른다.

     

    메서드에서 null을 반환하고픈 유혹이 든다면 그 대신 예외를 던지거나 특수 사례 객체를 반환하는 편이 더 좋다. 사용하려는 외부 API가 null을 반환한다면 감싸기 메서드를 구현해 예외를 던지거나 특수 사례 객체를 반환하는 방식을 고려해보자. 많은 경우 특수 사례 객체가 손쉬운 해결책이다.

     

    8. null을 전달하지 마라

    메서드에서 null을 반환하는 방식도 나쁘지만 메서드로 null을 전달하는 방식은 더 나쁘다. 정상적인 인수로 null을 기대하는 API가 아니라면 메서드로 null을 전달하는 코드는 최대한 피한다.

     

    대다수 프로그래밍 언어는 호출자가 실수로 넘기는 null을 적절히 처리하는 방법이 없다. 그렇다면 애초에 null을 넘기지 못하도록 금지하는 정책이 합리적이다. 즉, 인수로 null이 넘어오면 코드에 문제가 있다는 말이다. 이런 정책을 따르면 그만큼 부주의한 실수를 저지를 확률도 작아진다.

     

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

     


     

    8장 - 경계

    시스템에 들어가는 모든 소프트웨어를 직접 개발하는 경우는 드물다. 어떤 식으로든 외부 코드를 우리 코드에 깔끔하게 통합해야만 한다.

    해당 장에서는 소프트웨어 경계를 깔끔하게 처리하는 기법과 기교를 살펴본다.

     

    1. 외부 코드 사용하기

    인터페이스 제공자와 인터페이스 사용자 사이에는 특유의 긴장이 존재한다. 패키지 제공자나 프로엠워크 제공자는 적용성을 최대한 넓히려 애쓴다. 반면 사용자는 자신의 요구에 집중하는 인터페이스를 바란다. 이런 긴장으로 인해 시스템 경계에서 문제가 생길 소지가 많다.

     

    예를 들어 Map을 살펴보면 Map이 반환하는 Object를 올바른 유형으로 변환할 책임은 Map을 사용하는 클라이언트에 있다. 물론 제네릭스를 사용하면 코드 가독성이 크게 높아지지만, 사용자에게 필요하지 않은 기능까지 제공한다는 문제는 해결하지 못한다.

     

    만약 프로그램에서 Map 인스턴스를 여기저기로 넘긴다면 Map 인터페이스가 변할 경우, 수정할 코드가 상당히 많아진다.

    아래 코드는 Map을 좀 더 깔끔하게 사용한 코드이다.

     

    public class Sensors {
    	private Map sensors = new HashMap();
    
    	public Sensor getById(String id) {
    		return (Sensor) sensors.get(id);
    	}
    
    	...
    }

     

    경계 인터페이스인 Map을 Sensors 안으로 숨긴다. 따라서 Map 인터페이스가 변하더라도 나머지 프로그램에는 영향을 미치지 않는다. 제네릭스를 사용하든 하지 않든 더 이상 문제가 안 된다. 또한 Sensors 클래스는 프로그램에 필요한 인터페이스만 제공한다. 그래서 코드는 이해하기는 쉽지만 오용하기는 어렵다.

     

    Map 클래스를 사용할 때마다 위와 같이 캡슐화하라는 소리가 아니다. Map을 여기저기 넘기지 말라는 얘기이다. Map과 같은 경계 인터페이스를 이용할 때는 이를 이용하는 클래스나 클래스 계열 밖으로 노출되지 않도록 주의한다. Map 인스턴스를 공개 API의 인수로 넘기거나 반환값으로 사용하지 않는다.

     

    2. 경계 살피고 익히기

    곧바로 우리쪽 코드를 작성해 외부 코드를 호출하는 대신 먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히는 것이 좋다.

    이를 짐 뉴커크는 학습 테스트라 부른다.

    학습 테스트는 API를 제대로 이해하는지를 확인하는 셈이다. 즉, API를 사용하려는 목적에 초점을 맞춘다.

     

    학습 테스트에 드는 비용은 없다. 학습 테스트는 이해도를 높여주는 정확한 실험이다. 투자하는 노력보다 얻는 성과가 더 크다.

    만약 새 버전이 우리 코드와 호환되지 않으면 학습 테스트가 이 사실을 곧바로 밝혀낸다.

     

    3. 아직 존재하지 않는 코드를 사용하기

    경계와 관련해 또 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계다.

     

     

    만약 아직 설계되지 않은 API를 사용하게 된다면 일단 우리가 바라는 인터페이스를 정의하고 구현체는 나중으로 미룬다. 우리가 바라는 인터페이스를 구현하면 우리가 인터페이스를 전적으로 통제한다는 장점이 생긴다. 또한 가독성도 높아지고 코드 의도도 분명해진다.

     

    4. 깨끗한 경계

    경계에서는 흥미로은 일이 많이 벌어진다.  변경이 대표적인 예다.

    소프트웨어 설계가 우수하다면 변경하는데 많은 투자와 재작업이 필요하지 않다. 경계에 위치하는 코드는 깔끔히 분리한다.

     

    통제가 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리 코드에 의존하는 편이 훨씬 좋다.

     

    외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자. Map에서 봤듯이, 새로운 클래스로 경계를 감싸거나 아니면 ADAPTER 패턴을 사용해 우리가 원하는 인터페이스를 패키지가 제공하는 인터페이스로 변환하자. 어느 방법이든 코드 가독성이 높아지며, 경계 인터페이스를 사용하는 일관성도 높아지며, 외부 패키지가 변했을 때 변경할 코드도 줄어든다.

    'Backend > 책 정리' 카테고리의 다른 글

    Clean Code 12 ~ 13 장  (0) 2023.04.04
    Clean Code 9 ~ 11 장  (0) 2023.03.28
    Clean Code 5~6 장  (0) 2023.03.10
    Clean Code 3~4장  (2) 2023.03.07
    Clean Code 1~2장  (0) 2023.02.28

    댓글

Designed by Tistory.