Skip to content
This repository was archived by the owner on Dec 30, 2021. It is now read-only.

Chapter 08. Boundaries

youngsikwon edited this page Dec 20, 2021 · 3 revisions

2021.10.23 (SAT) 10:20-12:00 (100mins)
🚀 Lead by. 'youngsikwon'

08. 경계

시스템에 들어가는 모든 소프트웨어를 직접 개발하는 경우는 드물다. 때로는 패키지를 사고, 때로는 오픈소스를 이용한다. 때로는 사내 다른 팀이 제공하는 컴포넌트를 사용한다.

어떤 식으로든 이 외부 코드를 우리 코드에 깔끔하게 통합 해야만 한다.

소프트웨어 경계를 깔끔하게 처리하는 기법과 기교를 알아보자.

외부 코드 사용하기

외부 코드 사용하기

  • 인터페이스 제공자와 사용자 사이에는 특유의 긴장이 존재

    • 패키지 제공자나 프레임워크 제공자는 적용성을 최대한 넓히려 애쓴다. (더 많은 환경에서 돌아가야 더 많은 고객)
    • 반면, 사용자는 자신의 요구에 집중하는 인터페이스를 바란다.
  • 예시 - java.util.Map

    • Map은 굉장히 다양한 인터페이스로 수많은 기능을 제공

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

    • 다양한 기능과 유연성은 유용하지만 그만큼 위험하다.

      • ex. 누구나 clear() 할 권한이 있다는 것
      • 설계시 Map에 특정 객체 유형만 저장하기로 결정했어도, Map은 객체 유형을 제한하지 않음.

ex) 맵의 생성과 객체 조회

Sensor라는 객체를 담는 Map을 만드려면 다음과 같이 Map 을 생성한다

Map sensors = new HashMap();

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

Sensor s = (Sensor)sensors.get(sensorId);
  • 위와 같은 코드가 한 번이 아니라 여러 차례 사용된다.
  • Map이 반환하는 Object를 올바른 유형으로 변환할 책임이 Map을 사용하는 클라이언트에 있다.
    • 깨끗한 코드라 보기 어렵다.
    • 의도도 분명히 드러나지 않는다.

제네릭을 사용하면 코드 가독성이 크게 높아진다.

Map<String, Sensor> sensors = new HashMap<Sensor>();
...
Sensor s = sensor.get(sensorId);

하지만 "Map<String, Sensor>이 사용자에게 필요하지 않은 기능까지 제공한다."는 문제는 해결하지 못한다.

캡슐화가 제일 좋은 방법 !

public class Sensors {
  private Map sensors = new HashMap();
  
  public Sensor getById(String id) {
    return (Sensor) sensor.get(id);
  }
}
  • 경계 인터페이스인 Map을 Sensors 안으로 숨긴다.
    • Map 인터페이스가 변하더라도 나머지 프로그램에는 영향을 끼치지 않는다.
      • 변할 가능성이 거의 없다고 여길지도 모르지만, 자바 5가 제네릭을 지원하면서 Map 인터페이스가 변했다는 사실을 명심해야 함.
    • Sensors 클래스 안에서 객체 유형을 관리하고 변환하기 때문에 제네릭을 사용하든 말든 문제가 안된다.
  • Sensors 사용자는 제네릭이 사용되었는지 여부에 신경 쓸 필요가 없다.
    • 제네릭 사용 여부는 Sensors 안에서 결정
  • Sensors 클래스는 프로그램에 필요한 인터페이스만 제공한다.
    • 코드를 이해하기 쉽고 오용하기 어렵다.
    • (나머지 프로그램이) 설계 규칙과 비즈니스 규칙을 따르도록 강제할 수 있다.
  • Map을 사용할 때마다 위와 같이 캡슐화 하라는 소리가 아니다.
    • Map을(혹은 유사한 경계 인터페이스를) 여기저기 넘기지 말라는 것이다.
    • Map과 같은 경계 인터페이스를 사용할 때는 이를 이용하는 클래스나 클래스 계열 밖으로 노출되지 않도록 주의한다.
    • Map 인스턴스를 공개 API의 인수로 넘기거나 반환값으로 사용하지 않는다.
<!-- 
  ** Js에서 Object를 해시맵처럼 사용 금지~~!**
JS를 사용할    편리하게 사용하기 위해서는 다음과 같은 코드를 사용  -->
   const map = {};

// insert key-value-pair
map['key1'] = 'value1';
map['key2'] = 'value2';
map['key3'] = 'value3';

// check if map contians key
if (map['key1']) {
  console.log('Map contains key1');
}

// get value with specific key
console.log(map['key1']);
<!--  
  하지만 자바스크립트에는 이미 이러한 목적을 가진 Map 데이터형을 제공하고 있으며, Object를 사용 하는 것보다 Map을 이용해야 하는 이유!
-->

<!-- 
 번쨰.  많은 Key의 유형 
Object의 Key는 기호나 문자만을 가질  있습니다. 하지만, Map은 어떠한 형태라도 key로 사용될  있습니다.
 -->
const map = new Map();
const myFunction = () => console.log('I am a useful function.');
const myNumber = 666;
const myObject = {
  name: 'plainObjectValue',
  otherKey: 'otherValue'
};
map.set(myFunction, 'function as a key');
map.set(myNumber, 'number as a key');
map.set(myObject, 'object as a key');

console.log(map.get(myFunction)); // function as a key
console.log(map.get(myNumber)); // number as a key
console.log(map.get(myObject)); // object as a key

<!-- 
2. size 사용 가능 
 - Object의 경우 일반적인 방법으로는 size를 확인   없습니다. 하지만 Map은 가능
-->

const map = new Map();
map.set('someKey1', 1);
map.set('someKey2', 1);
...
map.set('someKey100', 1);

console.log(map.size) // 100, Runtime: O(1)

const plainObjMap = {};
plainObjMap['someKey1'] = 1;
plainObjMap['someKey2'] = 1;
...
plainObjMap['someKey100'] = 1;

console.log(Object.keys(plainObjMap).length) // 100, Runtime: O(n)

<!-- 
3.  나은 성능  
 - Map은 설계단계부터 데이터의 추가와 제거에 최적화 되어 있기 때문에 성능에 있어서 매우 유리합니다.
 - 맥북프로에서 천만개의 데이터 셋을 가지고 테스트 했을  Object는 1.6초의 처리시간이 필요했고 Map은 1ms 이하의 처리시간을 보였습니다.
-->
  • 개인적인 소견..
    • 자신이 관리하고, 입력하고 구현하는 객체, 클래스, 메서드 메서드 결과값을 내보낼 때는 별 신경쓰지 않고 보낼 수 있는 객체면 좋겠지만... 웬만하면
    • 다른 사용자가 쓰기 편한 객체로 내보내는 것이 좋은 것 같습니다.. :D

경계 살피고 익히기

  • 외부 코드를 사용하면 적은 시간을 들여 더 많은 기능을 제공할 수가 있습니다. 하지만!! 외부 패키지에 대한 테스트의 확인이 필요하게 됩니다. 코드를 개발하다 보면

  • 자신의 코드가 문제인지, 외부 라이브러리가 문제인지 명확히 파악하기 어려워 오랫동안 디버깅을 할때도 있습니다.

  • 이러한 문제를 해결 하기 위해서 '학습 테스트[Jim Newkirk는 이를 학습 테스트 (테스트 주도 개발 p222-237) 라 부른다.]' 라 불리는 방법을 사용합니다.

  • 학습 테스트는 외부 코드를 이용할 때 외부 코드를 이용하지 않고, 자신의 코드를 작성해 테스트한 뒤 외부 코드를 적용 하는 방법입니다.

  • 이런식으로 테스트를 진행하면 외부 코드의 문제여부를 명확히 파악할 수 있습니다.

Log4j 익히기

Log4J란?

log4j는 자바기반 로깅 유틸리티이다. 디버그용 도구로 주로 사용되고 있다.
 
// 1. "hello"를 출력하는 테스트 케이스
@Test
public void testLogCreate() {
  Logger logger = Logger.getLogger("My Logger");
  logger.info("hello");
}


// Appender라는 뭔가가 필요하다는 오류 발생
// 2. 그래서 ConsoleAppender를 생성 후 테스트 케이스
@Test
public void testLogAddAppender() {
  Logger logger = Logger.getLogger("MyLogger");
  ConsoleAppender appender = new ConsoleAppender();
  logger.addAppender(appender);
  logger.info("hello");
}

// 이번에는 Appender에 출력 스트림이 없다는 사실 발견
// 3. 출력 스트림이 있어야 정상 아닌가? 구글링 후 다음과 같이 시도
@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");
}

// 이제야 제대로 돌아간다.
// 그런데 ConsoleAppender에게 콘솔에 쓰라고 알려야 하다니 뭔가 수상..
// 흥미롭게도 ConsoleAppender.SYSTEM_OUT 인수를 제거 했더니 문제가 없다.
// 하지만, PatternLayout을 제거했더니 또 다시 출력 스트림이 없다는 오류가 뜬다. 아주 수상하다.
// 좀 더 구글을 뒤지고, 문서를 읽어보고, 테스트를 돌려보며 log4j가 돌아가는 방식을 상당히 많이 이해
// 여기서 얻은 지식을 간단한 단위 테스트 몇개로 표현
// 이제 모든 지식을 독자적인 로거 클래스로 캡슐화
// 그러면 나머지 프로그램은 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");
    }
}

학습 테스트는 공짜가 아니다.

앞에서 말한 학습 테스트에 드는 비용이 없습니다. 프로그램을 개발하기 위해서는 API를 배워야 하므로 이는 필요지식을 확보할 수 잇는 쉬운 방법입니다.
학습 테스트는 이해도를 높여주는 정확한 실험이기도 합니다.

이는 노력에 대한 투자보다 얻는 성과가 더 큽니다. 패키지가 새로 나올떄 마다 학습 테스트를 진행한다면 패키지의 새버전이 나와도 적용하기가 쉬워진다. 
그렇지 않다면 오래된 버전을 필요이상으로 오랫동안 사용할려고 하게 됩니다.

결론 : 깨끗한 경계

  • SW 경계에서는 많은 일이 발생합니다. 코드 변경이 대표적인이다. 소프트웨어 설꼐가 우수하다면 변경에 많은 비용이 들지 않습니다(엄청난 시간, 노력, 재작업을 요구 하지 않음).
    • 통제 하지 못하는 코드를 사용할 때는 너무 많은 투자를 하거나 향후 변경 비용이 지나치게 커지지 않도록 각별히 주의!!
  • 경계에 위치하는 코드는 깔끔히 분리한다, 또한 기대치를 정의하는 테스트 케이스도 작성한다.
    • 외부 패키지를 세세히 알 필요 없음. 외부 패키지에 의존하는 대신 통제 가능한 우리 코드에 의존 하는 편이 훨씬 나음(외부 코드에 휘둘리지 않도록)
  • 외부 패키지를 호출하는 코드를 가능한 줄여 경계를 관리하자.
    • 새로운 class로 경계를 감싸거나
    • Adapter Pattern을 사용해 원하는 인터페이스를 패키지가 제공 하는 인터페이스로 변환
  • 경계를 관리하면 코드 가독성이 높아지고, 경계 인터페이스를 사용하는 일관성도 높아지며, 외부 패키지가 변했을 떄 변경할 코드도 줄어든다.

개인적인 느낌 : 외부 코드에 휘둘리지 않는다는 것을 외부 의존성에 대해서는 의도한 대로 동작한 것을 확인하는 정도만 신경 써도 괜찮다는 의미로 이해 했으며, 내부 동작이 어떤지는 블랙박스 마냥 여기고, 반환하는 데이터를 어떻게 가공할 것인지에 대해 로직 구현에 집중하라는 의미로 와닿다.

Clone this wiki locally