Ch 8. 경계
요 챕터에서 경계라함은 외부 코드를 우리 코드에 활용해야할 때, (외부 API 연동 등) 테스트의 방식이나 구현의 경계를 의미한다.
- 외부 코드 사용하기 ex) Map 인터페이스 사용하기
Map<String, String> sensors = new HashMap<Sensor>(); sensors.get(sensorId);
보다 Map sensors = new HashMap(); (Seosor) sensors.get(id);
사용하기
- 왜냐하면 저렇게 세밀하게(
Map<String, String>
) 표현해 놓을수록, 이후 Map의 인터페이스가 변경되었을 때 바꿔야할 코드 조각들이 많아지기 때문이다. 후자와 같이 표현하면, 우리는 해당 부분만 변경하면 된다. String, String 형태를 지원하지 않도록 변경되는 상황도 있을 수 있으니.. 적어도 생성자는 변하지 않겠지.
- 결론은 경계 인터페이스: 내가 직접 개발한 인터페이스가 아닌 인터페이스 는 여기저기 넘기지 말고, 해당 인스턴스들은 공개 API의 인수로 넘기거나 반환값으로 사용하지도 마라
- 이제 이후 Map 인터페이스가 변하더라도 나머지 프로그램에는 영향을 미치지 않음.
- 외부 코드 및 API ( ex. 아파치의 log4j )를 사용할 때, 곧바로 우리쪽 코드를 작성해서 외부 API를 호출하는 대신, 먼저 간단한 TC들을 작성해서 외부 코드를 익힌다. 이를 학습 테스트라고 한다.
- log4j 라이브러리 자체를 매번 익히고 사용하기 번거로우니, 학습 테스트를 통해 익힌 지식들을 바탕으로 독자적 로거 클래스를 생성하여 캡슐화하면, 손쉽게 사용 가능하다.
- 학습 테스트는 매우 귀찮을 수 있다.. 하지만 테스트를 하면서 외부 API에 어떤 점이 버그인지, 나의 문제가 아니라 저 친구의 어느 부분때문에 문제가 될 수 있는지 등을 파악 가능하겠지.
- 하지만 가장 중요한 가치는 패키지의 새 버전이 나오게 될 경우 구성해둔 학습 테스트를 돌려 차이가 있는지 미리 확인하면, 외부 API 버전 업으로 발생하는 문제를 미리 해결할 수 있다는 점이다.
- 만약 외부 코드 및 API가 완성되기 전인데 나도 작업을 시작해야한다면.. 외부 코드를 통해 받을 클래스를 추상화해두고 (책의 예시에서는 Transmitter 인터페이스로 추상화 해뒀음), 테스트로 추상화된 클래스나 인터페이스를 상속받는 아이를 선언하여, 그 친구를 통해 테스트 해본다.
- ADAPTER 패턴으로 API 사용을 캡슐화 하여, API 변경시 수정할 코드를 한 곳으로 모아두는 것!
- ADAPTER 패턴에 대한 공부 필요
- 결론
- 경계에 위치하는 코드는 깔끔히 분리
- 새로운 클래스로 경계를 감싸기
- ADAPTER 패턴을 사용해서 패키지가 제공하는 인터페이스 (얘네는 우리 니즈대로 개발했을지, 더 많이 개발했을지, .. 모르지)를 우리가 원하는 인터페이스(우리의 니즈를 딱 맞춘 그런 인터페이스!) 로 변환하자.
Ch 9. 단위 테스트
TC의 필요성
- 일반적인 테스트가 현재 돌아가는 방식.. 클래스와 메서드를 공들여 ㄱ현한 후 임시 코드를 급조해 테스트를 수행 (
뭐.. 다들 그러시잖아요.. ㅎㅎ)
- 하지만 애자일과 TDD (Test Driven Development) 덕택에 단위 테스트를 실제 코드 작성 이전에 작성하고, 자동화하는 추세.
- TDD 법칙 세가지
- 어떤 케이스에서 실패할 수 있을지 충분히 생각한 후 실제 코드 작성
- 컴파일이 실패하지 않으면서 실행이 실패하는 정도로만 단위 테스트 작성
- 현재 실패하는 TC들을 통과할 정도로만 실제 코드 작성 (오버 스펙이 아닌 필요한 기능만 구현하게 되어 코드가 깔끔해질 수 있음)
- 지저분한 테스트 코드를 내놓으나, 테스트를 안하나 오십보 백보.. 실제 코드 변경시, 테스트 코드도 변해야 하는데, TC 코드가 더러우면 또 수정하는데 그만큼 리소스가 들기 때문.. -> TC 코드도 깔끔하게 짜라
- TC가 없다면! 시스템의 특정 부분을 수정했을 때, 다른 부분이 안전하다는 사실을 검증하지 못하므로 결함율이 높아지고, 이 때문에 개발자의 변경이 주저되어 코드가 침체되는 현상이 발생.. -> 깨끗한 단위 테스트로, 변경을 주저하지 말고 무결성을 검증하라!
- 테스트는 유연성, 유지보수성, 재사용성을 제공
깨끗한 단위 테스트
- 가독성이 좋은 코드
- Build - Operate - Check 패턴 (TC 자료를 만들고, 수행하고, 수행 결과를 assert 하는)
- 흔히 쓰는 시스템 조작 API 대신 테스트 코드용 특수 API(
뭐 어디 있는건 아니고, 어차피 너네가 개발해야함)를 사용해도 좋다. 이렇게 두개가 구분될 수 있는 이유는, TC에 적용되는 표준이 실제 코드에 적용되는 표준과 다소 다르기 때문
- TC 코드는 실제 코드만큼 효율적인 필요가 없음
- 추가 얘기) 실제 코드에서도 크게 무리가 아니라면 StringBuffer 피하기.. 보기에 추하거든 ㅋㅅㅋ
- 테스트당 assert는 최대한 하나만 사용하도록 한다. 두개 써야하면 TC를 쪼개는게 더 나을 수도. 근데 오히려 이게 코드 중복을 불러온다면 차라리 두개 그냥 써라.
- 물론 Template Method 패턴 사용하면 중복 제거가 가능하긴 함 (상위 부모 클래스를 두고, 각각 상속받아서 TC 코드 짜는거지), 근데 뭐.. 굳이 TC 코드인데 이래야 하나. 배보다 배꼽이 크다. 그냥 두개 쓰셈. 그냥, 이래저래 복잡한 경우 아니면 최대한 줄이도록 노력만 하자는 의미 ^_^
- 깨끗한 테스트가 가지는 다섯가지 규칙: FIRST!
- Fast: 테스트는 빨리 돌아야한다. (
엉엉.. 테스트가 너무 많아서 빠르게 안돈다구요..)
- Independent: 각 테스트는 서로 의존하면 안된다. 즉, 어떤 순서로 실행해도 괜찮아야 한다, (
상품 구매하고 환불테스트할 때 step1~step7 까지 수행하는 테스트도 있는데.. 글고 심지어 앞 테스트에서 동일 상품에 대해 update 테스트해둬서 뒤에서 실패하는 케이스도 있었음.. 이럴땐 rollbackonly 등의 옵션을 사용하도록 하자..)
- Repeatable: 어떤 환경에서도 반복 가능해야한다. 즉 개발이던, 로컬이던, QA 환경이던, Mac이던, 윈도우던, .. 다 되야한다. (
하지만 보안상으로 외부 API 사용에 대해 ACL 같은거 걸려있으면 다 뚫어줘야만 가능하지 ㅠ. 아닌가? 오히려 외부 API 사용하는 부분을 모두 모킹해둬야할까?)
- Self-Validating: 테스트는 bool값으로 결과를 내야지, 통과 여부를 알려고 로그 파일을 읽게 만들어서는 안된다. (
허허.. diffTest가 생각나는 이유는 뭘까.. 개선해야지..)
- Timely: 단위 테스트는 테스트하려는 실제 코드를 구현하기 직전에 구현한다. (
근데 솔직히 뭐 TDD가 이 얘기긴 하지만, 시간이 너무 오래걸려서.. 비현실적이긴 함. 혼자 플젝할때나 적용해봐야지)
Ch 10. 클래스
- Class 정의 순서: static public variable -> private variable -> public method -> private method
- SRP(Single Responsibiligy Principle): 클래스나 모듈을 변경할 이유가 단 하나뿐이어야 한다 -> 즉 클래스는 작아야한다
- 큰 클래스 몇 개가 아니라, 작은 클래스 여럿으로 이뤄진 시스템이 더 바람직하다!
- 인스턴스 변수 수가 작아야 한다.
- 인스턴스 변수: 객체마다 각자 사용할 수 있게 되는 일반적인 non-static 변수
- cf) 스태틱 변수:
static
키워드로 지정된 변수
- 메서드가 변수를 많이 사용할수록, 메서드와 클래스의 응집도가 높아진다. 클래스에서 선언한 변수를 메서드가 많이 사용하면 적합하게 잘 짜여졌다는 이야기. 응집도가 높은 클래스를 선호한다!
- 함수는 작게, 매개변수 목록은 짧게 -> 따르다보면 인스턴스 변수가 아주 많아지는데, 이렇게 되면 새로운 클래스로 쪼개는 것이 적합
- 즉, 응집도를 유지하면 작은 클래스 여럿이 나온다. 한 클래스의 많은 변수들을 일부 분리하여 새로운 클래스의 인스턴스 변수로 승격한다면 (몇몇 함수가 몇몇 변수만 사용할 경우!), 함수의 모듈화도 쉬워진다.
- cf) SOAP 서비스
- 웹서비스는 크게 REST 기반 웹서비스와 SOAP 기반 웹서비스로 분류된다. 초기에는 SOAP 기반 웹서비스를 이용하는 추세였으나, 여러가지 한계점으로 인해 REST 기반 웹서비스로 전환되는 추세이다.
- SOAP는 일반적으로 널리 알려진 HTTP, HTTPS, SMTP 등을 통해 XML 기반의 메시지를 컴퓨터 네트워크 상에서 교환하는 프로토콜
- 즉, 웹서비스 내의 모든 데이터는 XML로 표현된다. 는 모토를 가진 설계이다.
- 공개된 웹서비스가 이용될 때, 서비스 요청자와 서비스 제공자 간에 SOAP을 이용하여 서비스를 호출하고 결과를 돌려받게 된다. SOAP 메시지는 아래 그림과 같이 SOAP 봉투(envelope), SOAP 헤더(header), SOAP 바디(body)로 구성된 하나의 XML 문서로 표현되는 데 이러한 복잡한 구성으로 인해 HTTP 상에서 전 달되기 무겁고, 메시지 인코딩/디코딩 과정 등 웹 서비스 개발의 난이도가 높아 개발 환경의 지원이 필요하다.
- REST 구조에서의 리소스는 그들 의 고유한 URI를 가지며, HTTP의 기본 메소드인 GET/PUT/POST/DELETE만으로 접근할 수 있다. SOAP와 달리 리소스를 등록하고 저장하는 UDDI와 같은 중간 매체없이 고유 URL을 통해 직접 바로 전송하기 때문에 단순하고 빠르다.
- 출처: https://mygumi.tistory.com/55 [마이구미의 HelloWorld]
- 클래스는 변경하기 쉬워야한다.
- OCP와 유사한 이야기
- OCP: 확장에 개방적이고(기존 class 수정이 없도록) 수정에 폐쇄적이어야함(모든 상속받는 class에 대해 모두 수정 필요)
- 이를 만족하기 위해서 기본적으로 SRP가 지켜져야함
- 클래스 일부에서만 사용되는 비공개 메서드는 코드를 개선할 잠재적인 여지를 시사한다. -> 책임을 바로 분리하라
- 이상적인 시스템이라면 새 기능을 추가할 때 시스템을 확장할 뿐, 기존 코드를 변경하지 않는다.
- 변경으로부터 격리
- 구체적인 클래스는 상세한 구현(코드)을 포함하고, 추상 클래스는 개념만 포함
- 결합도는 낮을수록 좋다.
- 결합도: 각 시스템 요소가 다른 요소들 및 변경을 손쉽게 하지 못할 정도로 끈끈히 묶여있는 정도
- 결합도를 최소로 줄이면 자연스레 DIP를 만족하도록 구성할 수 있다.
- Dependency Inversion Principle: 클래스가 상세한 구현이 아니라 추상화에 의존해야한다는 원칙. 구체클래스보다 인터페이스나 추상 클래스를 사용하라!