클린 코드 - 8. 경계

이정우·2021년 12월 7일
1

Clean Code

목록 보기
8/10

시스템에 사용 되는 모든 코드를 직접 개발하는 경우는 드물다.
그렇기에 어떻게든 외부에서 개발한 코드를 내 코드에 깔끔하게 통합해야만 한다.

외부 코드 사용하기

인터페이스의 개발자는 인터페이스를 최대한 많은 곳에서 사용되도록 만들고 싶어하고, 사용자는 자기가 원하는 기능에 좀 더 집중된 인터페이스를 원한다. 이런 입장 차이로 인해 문제가 생길 소지가 많다.

java.util.Map을 살펴보면 다양한 인터페이스로 기능을 제공하고 있다. 기능들은 분명 유용하지만 그만큼 위험성도 존재한다. Map을 사용하는 누구든 Clear 메소드를 호출하면 데이터를 모두 지울 수 있다. 또한, Generics가 도입되기 전의 Map은 객체 유형을 제한하지 않았기에 원하는 모든 객체를 저장할 수 있었다.

지금은 Generics를 사용하여 가독성을 높이고 객체의 유형은 제한할 수 있게 되었지만, 여전히 필요로 하지 않는 메소드를 제공한다는 점은 변하지 않았다. 또한, Map 인터페이스가 변하게 된다면 Map을 사용하는 모든 부분에서 코드를 수정해야 할 것이다. 실제로도 Generics가 도입되면서 인터페이스가 변했다는 것을 생각해보면, 앞으로도 인터페이스가 변하지 않을 것이라는 보장을 할 수 없다.

/* Generics 도입 이전 */

// Map에 저장되는 객체의 유형이 정해져 있지 않음
Map sensors = new HashMap();

// 데이터를 꺼낼 때 사용자가 형 변환을 수행
Sensor s = (Sensor)sensors.get(sensorId);


/* Generics 도입 이후 */ 

// Map에 저장되는 객체의 유형을 지정
Map<String Sensor> sensors = new HashMap<>();

Sensor s = sensors.get(sensorId);

이러한 문제를 해결하기 위해서는 경계 인터페이스인 Map을 객체 안으로 숨겨 인터페이스가 변하더라도 나머지 프로그램에 영향을 미치지 않도록 해야 한다. 또한, 프로그램에 필요한 인터페이스만 제공할 수 있기 때문에 설계에 충실할 수 있을 것이다.

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

물론, Map을 사용할 때마다 객체 안으로 숨길 필요는 없다. Map과 같은 경계 인터페이스를 사용할 때, 사용하는 클래스의 밖으로 노출되지 않도록 주의하고 API의 인수나 반환값으로 사용하지 않도록 주의해야 한다.


경계 살피고 익히기

외부 코드를 사용하면 개발에 소요되는 시간을 많이 줄일 수 있다. 하지만, 무작정 사용하기 보다는 코드를 테스트하는 것이 바람직하다. 외부 코드를 익히는 것과 통합하는 것 모두 어렵기 때문에 바로 외부 코드를 호출하지 말고 간단한 테스트 케이스를 작성하여 익히는 것이 더욱 도움이 될 것이다. 이 과정을 학습 테스트라고 부른다.

학습 테스트는 프로그램에서 사용하려는 방식대로 외부 API를 호출하여 이해도를 높이는 과정으로, 사용하려는 목적에 초점을 맞춘다.

log4j 익히기

로깅 기능을 직접 구현하지 않고 아파치의 log4j 패키지를 사용한다고 생각해보자.

// 문서를 읽기 전, 테스트 케이스를 먼저 작성
@Test
public void testLogCreate() {
    Logger logger = Logger.getLogger("MyLogger");
    logger.info("hello");
}

// Appender가 필요하다는 오류 발생, Appender 추가
@Test
public void testLogAddAppender() {
    Logger logger = Logger.getLogger("MyLogger");
    ConsoleAppender appender = new ConsoleAppender();
    logger.addAppender(appender);
    logger.info("hello");
}

// 출력 스트림이 필요하다는 오류 발생, 스트림 추가
@Test
public void testLogAddAppender() {
    Logger logger = Logger.getLogger("MyLogger");
    logger.removeAllAppenders();
    logger.addAppender(new ConsoleAppender(
    			new PatternLayout("%p %t %m%n"), 
                	ConsoleAppender.SYSTEM_OUT));
    logger.info("hello");
}

...

위와 같이 테스트 케이스를 통해 간단한 콘솔 로거를 초기화하는 방법을 익힐 수 있다. 이때 얻은 지식을 독자적인 로거 클래스로 캡슐화한다면 나머지 프로그램은 log4j를 몰라도 된다.

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

학습 테스트에 드는 비용은 없지만, 필요한 지식만 확보하는 손쉬운 방법으로 이해도를 크게 높여주기 때문에 투자하는 노력보다 얻는 성과가 더 크다.

학습 테스트는 패키지가 예상대로 도는지 검증하기 때문에 경계 인터페이스의 새 버전이 출시되더라도 학습 테스트를 통해 호환 여부를 바로 검증할 수 있다.


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

경계의 또 다른 유형은 아는 코드와 모르는 코드를 분리하는 부분이다.

무선통신 시스템을 개발하는데, 송신기라는 반드시 필요한 하위 시스템이 인터페이스도 정해지지 않았다고 생각해보자. 그렇다고 송신기가 구현될 때까지 언제까지고 개발을 미루며 기다릴 수는 없을 것이다.

따라서 나에게 필요한 송신기의 기능을 분석해서 임시 인터페이스를 정의하여 메소드를 작성한다. 이렇게 인터페이스를 만들면 인터페이스를 내가 통제할 수 있으며 테스트도 아주 편하다는 장점이 있다.


깨끗한 경계

소프트웨어 설계가 우수하다면 유지 보수에 많은 노력이 필요하지 않을 것이다. 외부의 코드를 사용할 때는 유지 보수 비용이 너무 커지지 않도록 주의해야 한다.

새로운 클래스로 경계를 감싸거나 어댑터 패턴을 통해 인터페이스를 변환하여 외부 코드를 호출하는 코드를 최대한 줄인다면 코드 가독성, 일관성, 유지보수의 세 마리 토끼를 모두 잡을 수 있을 것이다.

0개의 댓글