Skip to content

Latest commit

 

History

History
285 lines (236 loc) · 13.7 KB

08 경계.md

File metadata and controls

285 lines (236 loc) · 13.7 KB

들어가면서

외부 코드를 사용한다면 우리 코드에 맞게 깔끔하게 통합해야 한다.
이같은 소프트웨어 경계를 깔끔하게 처리하는 기법과 기교를 살펴보자

목차

1. 외부 코드 사용하기
2. 경계 살피고 익히기
3. log4j 익히기
4. 학습 테스트는 공짜 이상이다.
5. 아직 존재하지 않는 코드 사용하기
6. 깨끗한 경계

외부 코드 사용하기

경계 인터페이스를 직접적으로 사용하면 많은 문제가 발생한다.

패키지 제공자 / 프레임 워크 제공자 :

  • 더 많은 환경에서 코드가 돌아가도록 적용성을 최대한 넓히려 애쓴다.

패키지 사용자 / 프레임 워크 사용자 :

  • 자신의 요구에 집중하는 인터페이스(코드)를 원한다.

이러한 긴장으로 인해 시스템 경계에서 문제가 생길 소지가 많다.

Map

Map이 제공하는 기능성과 유연성은 유용하지만 그만큼 위험도 크다.

  • 필요하지 않은 기능 또한 제공

    • clear() 함수 :
      Map 인스턴스를 직접적으로 만든 프로그래머는 필요한 로직이 아니면 사용하지 않겠지만
      Map 인스턴스를 넘겨 받은 프로그래머는 언제든지 해당 메서드로 Map 값을 지울 수 있다.
  • 객체 유형에 제한이 없어 얼마든지 다른 객체 유형이 사용될 수 있다.

    • Sensor s = (Sensor) sesonrs.get(sensorId);
    • Map 이 반환하는 Object를 올바른 유형으로 변환할 책임이 사용자에게 넘겨진다.
    • 변환 코드를 작성하는 사람이 사용자이므로 코드 간의 불일치성을 초래할 수 있다.
    • 또한 함수와 인자 사이에 어떤 관계가 있는지 함수 이름을 보고도 알기가 힘들다.
    • 즉, 의도가 분명히 드러나지 않기 때문에 깨끗한 코드라 부르기 어렵다.
  • Map 인스턴스 사용 중에 인터페이스가 변할 경우, 수정할 코드가 많아진다.

    • 실제로 자바5 에서 제네릭을 도입했을 때 Map 인터페이스가 변한적 있다.
    • 이로 인해 제네릭 사용을 금하는 시스템도 있다.

Map 개선한 코드

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

}

경계 인터페이스인 Map을 Wrapper 클래스 안으로 숨겼다.
즉, 기존에 Map 클래스를 사용했던 코드들을 Sensor 클래스로 대체했다.
이같은 Wrapper 클래스를 사용하면 얻게 되는 장점들이 있다.

장점

  • 사용자는 Map에 제네릭스가 사용되었는지 여부에 신경 쓸 필요가 없어졌다.
  • Map 인터페이스가 변하더라도 Wrapper 객체를 사용하기에 다른 프로그램에는 영향을 미치지 않는다.
  • 프로그램에 필요한 인터페이스만 제공할 수 있다.
  • 필요한 인터페이스를 사용하는 과정에서 보다 명확한 이름을 지정해줄 수 있다.
  • Wrapper 클래스는 설계 규칙과 비즈니스 규칙을 따르도록 강제할 수 있다.

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

경계 살피고 익히기

외부 코드를 사용하면 적은 시간에 더 많은 기능을 출시하기 쉬워진다.
만약 외부에서 가져온 패키지를 사용하고 싶다면 어디서 어떻게 시작할까?

외부 패키지 사용에 앞서 테스트를 진행하자
외부 패키지에 대한 테스트는 우리의 책임은 아니다.
하지만 우리 자신을 위해 우리가 사용할 외부 코드를 미리 테스트하는 편은 바람직하다.
예를 들어 타사 라이브러리를 우리 코드에 적용시켜 테스트를 동작을 확인했는데
버그가 발생하면 우리 코드 문제인지 외부 라이브러리 문제인지 오랜 디버깅을 통해서 알 수 있다.

외부 코드를 익히기는 어렵다.
외부 코드를 통합하기도 어렵다.
2가지를 동시에 하기는 두 배나 어렵다.
그렇다면 다르게 접근하는 것은 어떨까?

우리 쪽 코드를 작성해 외부 코드를 호출하는 대신
먼저 간단한 테스트 케이스를 작성해 외부 코드를 익히면 어떨까?
즉, 사용에 앞서 외부 코드에 대한 학습을 테스트로 진행하는 것이다.
그리고 이를 학습 테스트라고 부른다.

학습 테스트는 프로그램에서 사용하려는 방식대로 외부 API를 호출한다.
통제된 환경에서 API를 제대로 이해하는지를 확인하는 셈이다.
학습 테스트는 API를 사용하려는 목적에 초점을 맞춘다.

log4j 익히기

로깅 기능을 직접 구현하는 대신 log4j 패키지를 사용한다고 가정하자
패키지를 내려받아 소개 페이지를 연다. (document)
문서를 자세히 읽기 전에 테스트 케이스를 작성한다.
화면에 "hello"를 출력하는 테스트 케이스다.

잠깐!!!

    @Test
    public void testLogCreate() {
        Logger logger = LogManager.getLogger("MyLogger");
        logger.info("hello");
    }

필자는 현재 위 코드만으로도 정상적으로 동작하고
클린 코드에서 제시된 코드들은 과거 버전의 코드들이기에 현재와 다른 부분이 많다.

사실 해당 챕터에서는 log4j를 익히는 것이 본질이 아니다.
학습 테스트를 이용해서 외부 경계 패키지를 학습하는 방법을 배우는 것이다.
그렇기 때문에 코드 동작보다는 어떻게 학습 테스트를 진행해야 할지를 생각하기를 바란다.

학습 테스트 진행

사전 준비물 : 해당 라이브러리를 의존받고 관련 문서를 켜놓자

@Test
pubilc void testLogCreate() {
      Logger logger = Logger.getLogger("MyLogger");
      logger.info("hello");
}
  • 테스트 케이스를 돌렸더니 Appender가 필요하다는 오류가 발생한다.
  • 문서를 읽어본다.
  • 문서에 ConsoleAdapter라는 클래스가 있는 것을 발견하고 적용한다.
@Test
public void testLogAddAppender() {
    Logger logger = Logger.getLoger("MyLogger");
    ConsoleAppender appender = new ConsolAppender();
    logger.addAppender(appender);
    logger.info("hello");
}
  • 이번에는 ConsoleAdapter에 출력 스트림이 없다는 사실을 발견한다.
  • 구글을 검색후 관련 코드를 적용시켜준다.
@Test
public void testLogAddAppender() {
    Logger logger = Logger.getLoger("MyLogger");
    logger.removeAllAppenders();
    logger.addAppender(new ConsoleAppender(new PatternLayout(
                    "%p %t %m%n"), ConsoleAppender.SYSTEM_OUT));
    logger.info("hello");
}
  • 이제야 코드가 제대로 돌아간다.
  • 하지만 의문점은 남아있다.
  • ConsoleAppender에게 콘솔에 쓰라고 알려야 한다고
@Test
public void testLogAddAppender() {
    Logger logger = Logger.getLoger("MyLogger");
    logger.removeAllAppenders();
    logger.addAppender(new ConsoleAppender(new PatternLayout(
                    "%p %t %m%n")));
    logger.info("hello");
}
  • ConsoleAppender.SYSTEM_OUT 를 제거했다.
  • 문제가 없으며 여전히 콘솔에 hello가 찍힌다.
@Test
public void testLogAddAppender() {
    Logger logger = Logger.getLoger("MyLogger");
    logger.removeAllAppenders();
    logger.addAppender(new ConsoleAppender());
    logger.info("hello");
}
  • 하지만 new PatternLayout("%p %t %m%n")을 제거하니 문제가 발생했다.
  • 또다시 출력 스트림이 없다는 오류가 발생했다.
간단한 doc 예시)    
ConsoleAppender == 설정되지 않은 상태     
  • 문서를 살펴보니 ConsoleAppender 생성자는 설정되지 않은 상태라 말한다.
  • 당연하지도 유용하지도 않다.
  • 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");
    }
}
  • 구글을 좀 더 뒤지고, 문서를 읽어보고, 테스트를 돌려보며 log4j를 상당히 이해했다.
  • 그리고 이같은 방법으로 얻은 지식을 간단한 단위 테스트 몇 개로 표현했다.

이후부터는 모든 지식을 독자적인 로거 클래스로 캡슐화를 진행해도 되고
그러면 나머지 프로그램은 log4j 경계 인터페이스를 몰라도 되는 코드가 완성된다.

결론
우선 외부 문서를 처음부터 의존하지 말고
내가 생각한 방법, 내가 생각한 원리를 토대로 테스트 코드를 작성해본다.
그리고 나서 생기는 지속적인 오류를 문서와 구글링을 통해 학습을하고
그 과정에서 왜?라는 질문을 스스로에게 던지면서 다양한 원리를 익히도록 노력하자

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

학습 테스트는 이해도를 높여주는 정확한 실험이다.

학습 테스트는 공짜가 이상이다.
투자하는 노력보다 얻는 성과가 더 크다.

  • 외부 패키지에 대한 이해도를 높여준다.
  • 외부 패키지가 예상대로 도는지 검증한다.
  • 외부 패키지의 새 버전으로 이전하기 쉬워진다.
  • 외부 패키지의 새 버전이 나온다면 학습 테스트를 돌려 호환성을 확인할 수 있다.

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

우리가 바라는 인터페이스를 작성하기
Adapter pattern도 활용

경계에는 아는 코드와 모르는 코드를 분리하는 경계의 유형이 있다.
때로는 우리 지식이 경계에 미치지 못하는 영역도 있다. (알려해도 알 수 없는)

예를 들어 - 무선통신 시스템에 들어갈 소프트웨어 개발

지정한 주파수를 이용해 이 스트림에 들어오는 자료를 아날로그 신호로 전송하라.    
  • '송신기' 하위 시스템에 대한 지식이 없음
  • 관련 API도 아직 설계되지 않았다.
  • API를 통한 구현이 아직 힘든 상황이라 자체적으로 인터페이스부터 작성 시작
  • 인터페이스는 주파수와 자료 스트림을 입력 받도록 했다.
  • 이후 API가 완성되어 Adapter 패턴을 이용하여 양측의 간극을 메꾸기도 했다.

우리가 바라는 인터페이스를 구현하면
우리가 인터페이스를 전적으로 통제한다는 장점이 있다.
또한 코드의 가독성이 높아지고 코드 의도도 분명해진다.
또한 테스트도 아주 편리하게 할 수 있다. (경계 테스트 활용 가능)

깨끗한 경계

소프트웨어 설계가 우수하다면
변경하는데 많은 투자와 재작업이 필요하지 않다.
엄청난 시간과 노력과 재작업을 요구하지는 않는다.
통제하지 못하는 코드를 사용할 때는 비용이 지나치게 커지지 않도록 각별히 주의해야 한다.

경계에 위치한 코드는 깔끔히 분리한다.
또한 기대치를 정의하는 테스트 케이스도 작성한다.
이쪽 코드에서 외부 패키지를 세세하게 알아야 할 필요가 없다.(불필요한 기능은 알 필요 없다.)
통제가 불가능한 외부 패키지에 의존하는 대신 통제가 가능한 우리 코드에 의존하는 것이 좋다.

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