테스트 주도 개발로 배우는 객체 지향 설계와 실천 - 2장 객체를 활용한 테스트 주도 개발
26 Mar 2021Intro
초서-독서 한 내용을 그대로 적는 곳이기 때문에 책을 읽지 않은 분들이 보기에 맥락이 애매할 수 있습니다.
초서 : 책을 읽는데 그치는 것이 아닌 손을 이용해 책의 중요한 내용을 옮겨 적음으로써 능동적으로 책의 내용을 수용하고 판단하여 새로운 지식을 재창조하는 과정. 메타인지 학습법
Index
- 들어가면서
- 1부 서론
- 1장 테스트 주도 개발의 핵심은 무엇인가?
- 2장 객체를 활용한 테스트 주도 개발 «
- 3장 도구 소개
- 2부 테스트 주도 개발 과정
- 4장 테스트 주도 주기 시작
- 5장 테스트 주도 개발 주기의 유지
- 6장 객체 지향 스타일
- 7장 객체 지향 설계의 달성
- 8장 서드 파티 코드를 기반으로 한 개발
- 3부 동작하는 예제
- 9장 경매 스나이퍼 개발 의뢰
- 10장 동작하는 골격
- 11장 첫 테스트 통과하기
- 12장 입찰 준비
- 13장 스나이퍼가 입찰하다
- 14장 스나이퍼가 경매에서 낙찰하다
- 15장 실제 사용자 인터페이스를 향해
- 16장 여러 품목에 대한 스나이핑
- 17장 Main 분석
- 18장 세부 사항 처리
- 19장 실패 처리
- 4부 지속 가능한 테스트 주도 개발
- 20장 테스트에 귀 기울이기
- 21장 테스트 가독성
- 22장 복잡한 테스트 데이터 만들기
- 23장 테스트 진단
- 24장 테스트 유연성
- 5부 고급 주제
- 25장 영속성 테스트
- 26장 단위 테스트와 스레드
- 27장 비동기 코드 테스트
2장 객체를 활용한 테스트 주도 개발
객체망
" 객체 지향 설계는 객체 자체보다 **'객체 간의 의사소통'** 에 더 집중한다. "
" 위대하고 성장 가능한 시스템을 만들 때의 핵심은 모듈 간의 의사소통에 있지, 모듈의 내부 특성이나 작동 방식에 있지 않다. "
- 객체 지향 시스템은 ‘협업하는 객체의 망’ 으로 구성되어 있다.
- 객체의 구성을 변경해 시스템 작동 방식을 바꿀 수 있다.
- 객체망의 행위에 대한 선언적 정의란
- 객체 구성을 ‘관리’ 할 목적으로 작성하는 코드.
- 객체 구성을 관리한다는 것은 시스템을 관리하는 것.
- ‘목적’ 에 집중하기 쉬워짐.
- 즉, 객체 구성을 관리하기 쉽게 구성하면 ‘변경이 쉬운 시스템’ 으로 됨.
값과 객체
책에서 쓰이는 용어의 정확한 대상을 정리
- 값
- 양이 고정된 불변 인스턴스.
- 개인적인 식별자가 없으므로 두 값 인스턴스의 상태가 같아면 사실상 동일한 셈.
- 객체
- 상태가 변할지도 모름.
- 객체는 변경 가능한 상태를 이용해 시간의 추이에 따른 객체의 행위를 나타냄.
- 두 상태가 정확히 동일하더라도 별개의 식별자를 지님.
(향후 어떤 메시지를 전달 받느냐에 따라 상태가 달라질 수 있기 때문)
메시지를 따르라
- 객체는 일반적인 의사소통 패턴을 따르고 서로간의 의존성이 명시적.
- 의사소통 패턴
- 객체들이 다른 객체와 상호 작용하는 방법을 관장하는 각종 규칙으로 구성.
- 인터페이스 통한 역할 파악, 객체간 전달 가능한 메시지, 언제 전달 가능한지 등
- 즉 의사소통 패턴은 ‘객체 간에 있을 법한 관계에 의미를 부여’ 하는 행위이다.
- 테스트와 목(mock) 객체는 객체 간의 의사소통을 또렷이 보여준다.
객체
- 객체를 역할, 책임, 협력자 측면에서 생각하라.
- 객체는 역할을 하나 이상 구현한 것.
- 역할은 관련된 책임의 집합.
- 책임은 어떤 과업을 수행하거나 정보를 알아야 할 의무.
- 협력은 객체나 역할의 상호 작용에 해당.
묻지 말고 말하라
- 디미터의 법칙
- 객체들의 ‘협력 정도를 제한’ 하면 결합도가 효과적으로 낮아진다.
- ‘한 줄에 점(.)을 하나만 찍는다.’
- 객체 구조의 경로를 따라 멀리 떨어져 있는 낯선 객체에 메시지를 보내는 설계는 피하라는 것.
public class Post {
private final List<Comment> comments;
public Post(List<Comment> comments) {
this.comments = comments;
}
public List<Comment> getComments() {
return comments;
}
}
public class Board {
private final List<Post> posts;
public Board(List<Post> posts) {
this.posts = posts;
}
public void addComment(int postId, String content) {
posts.get(postId).getComments().add(new Comment(content)); // 열차 전복 코드
}
...
}
문제점
- 위
addComment()
코드처럼getter
가 줄줄이 이어지는 코드 형태가 디미터 법칙을 위반한 전형적인 코드 형태. Board
객체는Post
객체에도 영향을 받고,Comment
객체에도 영향을 받음.- 이러한 설계는 객체 간 결합도를 높이고 객체 구조 변화에 쉽게 무너진다.
해결
- 객체는 ‘내부적으로 보유하고 있거나 메시지를 통해 확보한 정보만’ 가지고 의사 결정을 내려야 한다.
- 규칙화 (아래 메서드만 호출해야 한다)
- 객체 자신의 메서드 호출
- 메서드 파라미터로 넘어온 객체들의 메서드 호출
- 메서드 내부 생성, 초기화된 객체 메서드
- 인스턴스 변수로 가지고 있는 객체가 소유한 메서드
class Demeter {
private Member member;
public myMethod(OtherObject other) {
// ...
}
public okLawOfDemeter(Paramemter param) {
myMethod(); // 1. 객체 자신의 메서드
param.paramMethod(); // 2. 메서드의 파라미터로 넘어온 객체들의 메서드
Local local = new Local();
local.localMethod(); // 3. 메서드 내부에서 생성, 초기화된 객체의 메서드
member.memberMethod(); // 4. 인스턴스 변수로 가지고 있는 객체가 소유한 메서드
}
}
// 1급 컬렉션 객체
public class Comments {
private final List<Comment> comments;
public void add(String content) {
Comment newComment = Comment.newInstance(content);
comments.add(newComment);
}
}
public class Post {
private final Comments comments;
public Post(Comments comments) {
this.comments = comments;
}
public void addComment(String content) {
comments.add(content);
}
}
public class Board {
private final List<Post> posts;
public Board(List<Post> posts) {
this.posts = posts;
}
public void addComment(int postId, String content) {
Post post = posts.get(postId);
post.addComment(content);
}
}
디미터 법칙 주의사항
- 자료구조라면 디미터 법칙을 거론할 필요가 없다.
- 자료
- 별 다른 동작 없이 자료를 노출
- 자료구조를 사용하는 절차적 코드는 기존 자료구조를 변경하지 않으면서 새 함수를 추가하기 쉬움
- but 새 클래스 추가시 기존 함수 모두 변경해야함
- 객체
- 동작을 공개하고 자료를 숨김
- 객체 지향 코드는 기존 함수를 변경하지 않으면서 새 클래스를 추가하기 쉬움
- but 새 함수 추가시 기존 클래스 모두 변경해야함
- 자료
- 하나의 .을 강제하는 규칙이 아니다.
IntStream.of(1, 15, 3, 20).filter(x -> x > 10).count();
위 코드는 디미터 법칙을 위반한 코드가 아님- IntStream의 내부 구조가 노출되지 않았음. (다른 IntStream으로 변환할 뿐, 객체를 둘러싸고 있는 캡슐은 유지)
- 객체 내부 구현에 대한 어떤 정보도 외부로 노출하지 않는다면 괜찮다!
자료구조
public class Square {
public Point topLeft;
public double side;
}
public class Rectangle {
public Point topLeft;
public double height;
public double width;
}
public class Circle {
public Point center;
public double radius;
}
// 새로운 자료구조(Class)가 생긴다면 ?
// 기존 자료구조를 사용하는 Geometry(공용 코드) 메서드는 전부 수정 되어야 한다... (area 등)
public class Geometry {
public final double PI = 3.141592653589793;
public double area(Object shape) throws NoSuchShapeException {
if (shape instanceof Square) {
Square s = (Square) shape;
return s.side * s.side;
} else if (shape instanceof Rectangle) {
Rectangle r = (Rectangle) shape;
return r.height * r.width;
} else if (shape instanceof Circle) {
Circle c = (Circle) shape;
return PI * c.radius * c.radius;
} else {
throw new NoSuchShapeException();
}
}
// 새로운 함수 추가는 용이함
// 기존 자료구조 변경할 필요 없이 쭉쭉 추가 가능
}
- 따라서 위와 같은 방식(자료구조를 사용하는 절차적 코드)은 자료구조 방식에 유리
객체
public interface Shape {
double area();
// 새로운 함수가 추가된다면 ?
// 인터페이스를 구현한 모든 객체들의 수정이 필요함
}
public class Square implements Shape {
private Point topLeft;
private double side;
public double area() {
return side * side;
}
}
public class Rectangle implements Shape {
private Point topLeft;
private double height;
private double width;
public double area() {
return height * width;
}
}
public class Circle implements Shape {
private Point center;
private double radius;
public final double PI = 3.141592653589793;
public double area() {
return PI * radius * radius;
}
}
// 새로운 객체(클래스) 생성에는 용이함
// 기존 구현체들과는 독립적인 객체이기 때문
- 따라서 위와 같은 방식(객체 지향 코드)은 객체 방식에 유리
- 객체 지향적 코드는 디미터 규칙을 따라야 좋은 설계방식
그래도 가끔은 물어라
” 답을 가늠하는 데 도움이 되는 정보를 묻기보다는 진정 답하고자 하는 질문을 던져야 한다. “
public void reserveSeats(ReservationRequest request) {
for (Carriage carriage : carriages) {
if (carriage.getSeats().getPercentReserved() < percentReservedBarrier) {
request.reserveSeatsIn(carriage);
return
}
}
request.cannotFindSeats();
}
위 코드를 질의 메서드 형태도 변경해보면,
public void reserveSeats(ReservationRequest request) {
for (Carriage carriage : carriages) {
if (carriage.hasSeatsAvailableWithIn(percentReservedBarrier)) { // 질의 메서드
request.reserveSeatsIn(carriage);
return
}
}
request.cannotFindSeats();
}
- 적절한 객체에 행위가 자리 잡아 행위에 이해하기 쉬운 이름이 생기게 된다. (테스트 하기도 용이)
- 주의할 점은 질의가 정보를 객체 바깥으로 ‘새어 나가게’하여 시스템을 경직되게 만들 수 있다.
- 단지 구현이 아니라 ‘호출하는 객체의 의도’ 를 서술하는 질의를 작성하려고 애써야 한다.
협력 객체의 단위 테스트
” 초점을 맞추는 객체에 대해 각 객체가 서로 명령을 전달하고 자기 상태를 질의하는 수단을 노출하지 않아야 한다. “
단위테스트에서 단정문을 쓸 만한 부분이 하나도 없는 것처럼 보인다.
목 객체
- 테스트 대상 객체의 이웃을 다른 대체물, 즉 목 객체(mock object)로 대체하는 것.
- 발생하는 이벤트에 대해 대상 객체가 가짜 이웃과 ‘어떻게 상호 작용할지’ 지정할 수 있음.
(이 같은 명세를 ‘예상 구문’ 이라 함) - 테스트 동안 목 객체는 자신이 예상대로 호출됐는지 단정함.
- 나머지 테스트가 동작하는 데 필요한 행위(스텁 형태로 동작)를 구현하기도 함.
인터페이스 발견
- 객체에 필요한 보조 역할을 파악하는 데 도움.
- 이러한 보조 역할은 자바 인터페이스로 정의되어 있고 시스템의 나머지 부분을 개발할 때 실제 구현처럼 동작.
목 객체를 활용한 TDD 지원
테스트 핵심 구조
- 필요 ‘목 객체’ 생성
- 대상 객체를 포함한 ‘실제 객체’ 생성
- 대상 객체에서 목 객체가 ‘어떻게 호출될지 예상하는 바’ 기술 (예상 구문)
- 대상 객체에서 ‘유발(trigger) 메서드(하나 또는 여러 개) 호출’
- ‘결과 값이 유효’ 하고 ‘예상되는 메서드 호출이 모두 일어났는지’ 확인
목 객체를 활용한 객체 테스트 는 다음과 같이 진행된다.
- 이웃하는 객체의 ‘목 인스턴스를 생성’
- 해당 목 인스턴스를 어떻게 호출하고 상태를 검사하는가에 관한 ‘예상 구문 정의’
- ‘스텁’ 형태로 동작할 필요가 있는 행위 구현
모조 객체
- 테스트의 콘텍스트를 담고 있음
- 목 객체를 생성
- 예상 구문과 스텁 관리
” 이 책 전반에 걸쳐 반복해서 강조하듯이 중요한 것은 모든 테스트 의도를 명확하게 해서 테스트를 거친 기능과 보조 역할을 담당하는 기반 구조, 객체 구조를 서로 구분하는 것이다. “
- 들어가면서
- 1부 서론
- 1장 테스트 주도 개발의 핵심은 무엇인가?
- 2장 객체를 활용한 테스트 주도 개발 «
- 3장 도구 소개
- 2부 테스트 주도 개발 과정
- 4장 테스트 주도 주기 시작
- 5장 테스트 주도 개발 주기의 유지
- 6장 객체 지향 스타일
- 7장 객체 지향 설계의 달성
- 8장 서드 파티 코드를 기반으로 한 개발
- 3부 동작하는 예제
- 9장 경매 스나이퍼 개발 의뢰
- 10장 동작하는 골격
- 11장 첫 테스트 통과하기
- 12장 입찰 준비
- 13장 스나이퍼가 입찰하다
- 14장 스나이퍼가 경매에서 낙찰하다
- 15장 실제 사용자 인터페이스를 향해
- 16장 여러 품목에 대한 스나이핑
- 17장 Main 분석
- 18장 세부 사항 처리
- 19장 실패 처리
- 4부 지속 가능한 테스트 주도 개발
- 20장 테스트에 귀 기울이기
- 21장 테스트 가독성
- 22장 복잡한 테스트 데이터 만들기
- 23장 테스트 진단
- 24장 테스트 유연성
- 5부 고급 주제
- 25장 영속성 테스트
- 26장 단위 테스트와 스레드
- 27장 비동기 코드 테스트