본문 바로가기
Book

[독서 기록] 클린 코드 7장, 8장 / 오류 처리, 경계

by Renechoi 2022. 11. 9.

클린 코드, 로버트 C. 마틴, 박재호 이해영 옮김, 인사이트

 
Clean Code(클린 코드)
『Clean Code(클린 코드)』은 오브젝트 멘토(Object Mentor)의 동료들과 힘을 모아 ‘개발하며’ 클린 코드를 만드는 최상의 애자일 기법을 소개하고 있다. 소프트웨어 장인 정신의 가치를 심어 주며 프로그래밍 실력을 높여줄 것이다. 여러분이 노력만 한다면. 어떤 노력이 필요하냐고? 코드를 읽어야 한다. 아주 많은 코드를. 그리고 코드를 읽으면서 그 코드의 무엇이 옳은지, 그른지 생각도 해야 한다. 좀 더 중요하게는 전문가로서 자신이 지니는 가치와 장인으로서 자기 작품에 대한 헌신을 돌아보게 된다.
저자
로버트 C 마틴
출판
인사이트
출판일
2013.12.24

 

 

뭔가 잘못될 가능성은 늘 존재한다. 뭔가 잘못되면 바로 잡을 책임은 바로 우리 프로그래머에게 있다.

- 130p 

 

 

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

... 

목록 7-2는 오류를 발견하면 예외를 던지는 코드다. 

 

public class DeviceController {
	  ...
	  public void sendShutDown() {
		    try {
			      tryToShutDown();
		    } catch (DeviceShutDownError e) {
			      logger.log(e);
		    }
	  }

	  private void tryToShutDown() throws DeviceShutDownError {
		    DeviceHandle handle = getHandle(DEV1);
		    DeviceRecord record = retrieveDeviceRecord(handle);
		    pauseDevice(handle); 
		    clearDeviceWorkQueue(handle); 
		    closeDevice(handle);
	  }

	  private DeviceHandle getHandle(DeviceID id) {
		    ...
		    throw new DeviceShutDownError("Invalid handle for: " + id.toString());
		    ...
	  }
	  ...
}

- 131p

 

 

다음은 파일이 없으면 예외를 던지는지 알아보는 단위테스트다. 

 

@Test(expected = StorageException.class)
public void retrieveSectionShouldThrowOnInvalidFileName() {
    sectionStore.retrieveSection("invalid - file");
}

 

단위 테스트에 맞춰 다음 코드를 구현했다. 

public List<RecordedGrip> retrieveSection(String sectionName) {
    // 실제로 구현할 때까지 비어 있는 더미를 반환한다.
    return new ArrayList<RecordedGrip>();
}

그런데 코드가 예외를 던지지 않으므로 단위 테스트는 실패한다. 잘못된 파일 접근을 시도하게 구현을 변경하자. 아래 코드는 예외를 던진다. 

 

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

 

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

 

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

 

- 132 ~ 133p

 

 

미확인 예외를 사용하라

예외에 의미를 제공하라

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

정상흐름을 정의하라

null을 반환하지 마라

null을 전달하지 마라

- 133 ~ 142p 

 

 

 

다음은 두 지점 사이의 거리를 계산하는 간단한 메서드다. 

 

public class MetricsCalculator {
    public double xProjection(Point p1, Point p2) {     
        return (p2.x - p1.x) * 1.5; // 일부러 절차 지향적인 클래스로 만든것     
    }
    ...
}

...

다음과 같이 새로운 예외 유형을 만들어 던지는 방법이 있다. 

public class MetricsCalculator {
    public double xProjection(Point p1, Point p2) {     
        if(p1 == null || p2 == null) {
	    throw InvalidArgumentException(
	        "Invalid argument for MetricsCalculator.xProjection");
	}
        return (p2.x - p1.x) * 1.5; // 일부러 절차 지향적인 클래스로 만든것     
    }
    ...
}

다음은 또 다른 대안이다. 

public class MetricsCalculator {
    public double xProjection(Point p1, Point p2) {     
        assert p1 != null : "p1 should not be null";
	assert p2 != null : "p2 should not be null";
	return (p2.x - p1.x) * 1.5;    
    }
}

- 141p

 

 


 

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

- 144p 

 

 

java.util.Map을 살펴보자. ... 아래 목륵을 보면 첫째가 clear()메서다. 즉, Map 사용자라면 누구나 Map 내용을 지울 권한이 있다는 말이다. ... Sensor라는 객체를 담은 Map을 만들려면 다음과 같이 Map을 생성한다. 

 

Map sensors = new HashMap(); 

 

Sensor 객체가 필요한 코드는 다음과 같이 Sensor 객체를 가져온다.

 

Sensor s = (Sensor)sensors.get(sensorId); 

 

위와 같은 코드가 한 번이 아니라 여러 차례 나온다. 즉 Map이 반환하는 Object를 올바른 유형으로 변환할 책임은 Map을 사용하는 클라이언트에게 있다. ... 대신 다음과 같이 제네릭스를 사용하면 코드 가독성이 크게 높아진다. 

 

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

}

경계 인터페이스인 Map을 Sensors 안으로 숨긴다. 따라서 Map 인터페이스가 변하더라도 나머지 프로그램에는 영향을 미치지 않는다. 제네릭스를 사용하든 하지 않든 더 이상 문제가 안 된다. 

 

- 143 ~ 146p 

 

 

경계 살피고 익히기

- 146p 

 

 

log4j 익히기

- 147p

 

좀 더 구글을 뒤지고, 문서를 읽어보고, 테스트를 돌린 끝에 목록 8-1을 얻었다. 그동안 log4j가 돌아가는 방식을 상당히 많이 이해했으며 여기서 얻은 지식을 간단한 단위 테스트 케이스 몇 개로 표현했다. 

 

public class LogTest {
    private Logger logger;

    @Before
    public void initialize() {
        logger = Logger.getLogger("logger");
        logger.removeAllAppenders();
        Logger.getRootLogger().removeAllAppenders();
    }

    @Test
    public void basicLogger() {
        BasicConfigurator.configure();
        logger.info("basicLogger");
    }

    @Test
    public void addAppenderWithStream() {
        logger.addAppender(new ConsoleAppender(
            new PatternLayout("%p %t %m%n"),
            ConsoleAppender.SYSTEM_OUT));
        logger.info("addAppenderWithStream");
    }

    @Test
    public void addAppenderWithoutStream() {
        logger.addAppender(new ConsoleAppender(
            new PatternLayout("%p %t %m%n")));
        logger.info("addAppenderWithoutStream");
    }
}

- 148p 

 

 

 

학습 테스트는 공짜 이상이다.

- 149p 

 

 

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

- 150p 

 

깨끗한 경계

- 151p 

 

 

반응형