8장. 경계

프라이마리모·2024년 4월 22일

Clean Code

목록 보기
7/15
post-thumbnail

외부 코드를 우리 코드에 깔끔하게 통합하기 위해 소프트웨어 경계를 깔끔하게 정리하는 기법과 기교를 살펴본다.

외부 코드 사용하기

패키지/프레임워크 제공자는 적용성을 최대한 넓히려 애쓰고, 사용자는 자신의 요구에 집중하는 인터페이스를 바란다. 이러한 차이로 인해 시스템 경계에서 문제가 생길 소지가 많다.

java.util.Map은 다양한 인터페이스로 수많은 기능을 제공한다.

/*
 * 기본 예제, Sensor라는 객체를 담는 Map 생성
 */
Map sensors = new HashMap();
Sensor s = (Sensor)sensors.get(sensorId);	//Sensor 객체 호출

/*
 * 예제2, 기본예제에서 제네릭스(Generics) 사용 -> 가독성 향상
 * 기본 문제점인 Map<String, Sensor>가 사용자에게 필요하지 않은 기능까지 제공한다는 점은 해결 불가
 */
Map<String, Sensor> sensors = new HashMap<Sensor>();
Sensor s = sensors.get(sensorId);	//Sensor 객체 호출

/*
 * 예제3, 기본예제 리팩토링(캡슐화)
 * 제네릭스의 사용 여부는 Sensors 내부에서 결정 -> 사용자는 제네릭스가 사용 여부와 무관
 * 프로그램에 필요한 인터페이스만 제공 -> 코드 이해 쉬움, 오용 어려움
 */
public class Sensors {
	//경계 인터페이스인 Map을 Sensors안으로 숨겨 Map 인터페이스가 변하더라도 나머지 프로그램에 영향 X
	private Map sensors = new HashMap();	
    
    public Sensor getById(String id) {
    	return (Sensor)sensors.get(id);
    }
}

경계 살피고 익히기

외부 코드를 사용할 때 곧바로 우리쪽 코드를 작성해 외부 코드를 호출하는 대신 먼저 프로그램에서 사용하려는 방식대로 외부 API를 호출하는 간단한 테스트 케이스를 작성해 외부 코드를 익힌 후 사용하는 것을 학습 테스트라 부른다. 학습 테스트는 API를 사용하려는 목적에 초점을 맞춘다.

log4j 익히기

로깅 기능을 직접 구현하는 대신 아파치의 log4j 패키지 사용 시 다음과 같이 테스트 케이스를 작성하고 실행하는 과정을 통해 독자적인 로거 클래스로 캡슐화 할 수 있다. 이 클래스외 나머지 프로그램은 log4j 경계 인터페이스를 몰라도 된다.

로그 화면에 "hello" 출력

  • 최초 테스트 코드 구동 -> "Appender 필요" 오류 발생
@Test
public void testLogCreate() {
	Logger logger = Logger.getLogger("MyLogger");
    logger.info("hello");
}
  • ConsoleAppender 추가 후 구동 -> 미출력 오류 발생, 출력 스트림 필요 판단
@Test
public void testLogAppender() {
	Logger logger = Logger.getLogger("MyLogger");
    ConsoleAppender appender = new ConsoleAppender();
    logger.addAppender(appender);
    logger.info("hello");
}
  • 출력 스트림 추가 후 구동 -> 출력 완료, ConsoleAppender 목적 재확인
@Test
public void testLogAppender() {
	Logger logger = Logger.getLogger("MyLogger");
    logger.removeAllAppenders();
    logger.addAppender(new ConsoleAppender(
    	new PatternLayout("%p %t %m%n"),
        ConsoleAppender.SYSTEM_OUT));	//ConsoleAppender.SYSTEM_OUT 제거 무관, PatternLayout 제거 시 오류 발생
    logger.info("hello");
}
  • 기본 ConsoleAppender 생성자는 '설정되지 않은 상태' 임을 확인 -> 최종 테스트 케이스 작성
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"),
            ConsoleAppender.SYSTEM_OUT));
        logger.info("addAppenderWithoutStream");
    }
}

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

학습 테스트는 필요한 지식만 확보하는 손쉬운 방법이자 이해도를 높여주는 정확한 실험이다.
학습 테스트는 패키지가 예상대로 도는지 검증한다. 패키지 새 버전이 출시되면 우리 코드와 호환되는지 아닌지를 학습 테스트가 곧바로 밝혀낸다. 학습 테스트를 이용한 학습이 필요하든 아니든 실제 코드와 동일한 방식으로 인터페이스를 사용하는 테스트 케이스가 필요하다.

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

경계와 관련해 또 다른 유형은 아는 코드와 모르는 코드를 분리하는 경계다.
무선통신 시스템에 들어갈 소프트웨어를 개발한다고 할 때, 소프트웨어에 '송신기'라는 하위 시스템이 필요하다고 하자. '송신기' 시스템에서 제공하는 인터페이스도 없는 상태인 경우, 아래 순서에 따라 설계하고 프로젝트를 진행하면 개발에 수월하다.

  • '송신기' 하위 시스템과 아주 먼 부분부터 작업을 시작한다.
  • 필요한 경계 인터페이스를 정의한다.
    이는 우리가 인터페이스를 전적으로 통제할 수 있으며, 코드 가독성도 높아지고 코드 의도도 분명해진다는 장점이 있다.
    • '송신기' 모듈에게 원하는 기능을 정의한다.
      지정한 주파수를 이용해 이 스트림에서 들어오는 자료를 아날로그 신호로 전송하라.
    • 임시 인터페이스를 정의한다.
      Transmitter <Interface>클래스 생성 -> transmit(frequency, stream) 메서드 추가, 주파수와 자료 스트림을 입력으로 받음
  • 송신기 API에서 CommunicationController 분리, Transmitter 인터페이스를 통해 송신기 API와 통신한다.
  • 적절한 FakeTransmitter 클래스를 작성하여 CommunicationController를 테스트한다.
  • 추후 제대로 된 송신기 API가 정의되면 TransmitterAdapter를 구현해 Adapter를 통해 실제 송신기 API를 미리 작성한 FakeTransmitter처럼 변경하여 기존 소스 수정 없이 API와 통신할 수 있도록 한다.
    ADAPTER 패턴을 활용하면 API 사용을 캡슐화 해 API가 바뀔 때 수정할 코드를 최소화 할 수 있다.

깨끗한 경계

  • 경계에 위치하는 코드는 깔끔히 분리
  • 기대치를 정의하는 테스트 케이스 작성
  • 통제가 불가능한 외부 패키지 대신 통제 가능한 우리 코드에 의존하는 편이 좋다.
  • 외부 패키지를 호출하는 코드를 가능한 줄여 경계 관리
    (새로운 클래스로 경계를 감싸거나 ADAPTER 패턴 사용)
profile
개발공부 요약노트

0개의 댓글