자바 스프링 개발자를 위한 실용주의 프로그래밍
객체지향
1장 절차지향과 비교하기
-
순차지향과 절차지향은 엄밀히 다른 개념임.
- 순차지향 프로그래밍 : 말 그대로 코드를 위에서 아래로 읽음.
- 절차지향 프로그래밍 : 함수 지향 프로그래밍.
-
절차지향은 책임을 프로시저로 나누고 프로시저에 할당.
-
객체지향은 책임을 객체로 나누고 객체에 할당.
C언어는 객체지향을 구현할 수 없는 이유
객체를 추상화한 역할에 책임을 할당함. 예를 들어서
public interface Calcuable {
void calc(int a, int b)
}
이런식으로 인터페이스를 만들어서 객체를 추상화하고, 이 인터페이스를 구현한 클래스를 만들어서 책임을 할당함.
그러나 C언어는 이런 추상화를 할 수 없으며, 추상화가 불가능하다는 특징 때문에 객체지향을 구현할 수 없음.
객체지향이란?
객체가 책임을 갖게 됐고 객체의 역할이 정해졌으며, 어떤 목표를 달성하기 위해서 서로 다른 객체와 협력을 함. 이게 객체지향의 본질임.
객체지향 사고방식, TDA
- Tell Dont Ask : 객체에게 물어보지 말고 시켜라 라는 원칙
예를 들어서, 아래와 같은 코드가 있다고 치면
public class Shop {
public void sell(Account account, Product product) {
if (account.getBalance() > product.getPrice()) {
...
}
}
}
이 코드는 객체에게 물어보는 코드임. 객체에게 물어보는 것이 아니라 객체에게 시키는 코드로 바꾸면 아래와 같음.
public class Shop {
public void sell(Account account, Product product) {
if(account.canBuy(product)) {
...
}
}
}
이러한 변경을 통해 아래와 같은 3가지 이점을 얻을 수 있음.
- 유연성 증가 : 각 객체가 자신의 상태와 동작을 관리하므로 외부에서 객체 내부의 상태를 직접 알 필요가 없음
- 결합도 감소 : 객체 내부의 세부 사항이 감춰지므로 다른 객체와의 상호 의존성을 줄일 수 있음.
- 재사용성 향상 : canBuy() 메서드를 다른 클래스에서도 사용할 수 있음.
2장 객체의 종류
VO
- VO의 특징
- 불변성 : 값이 변하지 않음.
- 불변성을 지키기 위해서 모든 필드를 final로 선언.
- 모든 값은 원시타입이어야 함 👉 참조 타입이 있다면 참조 타입 내의 값이 변경 가능할 수 있기 때문.
- 👆 위와 같은 경우 안됨.
- VO 안의 모든 함수는 순수함수여야 함. (항상 같은 값을 반환해야 하고 Random 이딴거 쓰면 안됨.)
- final class로 선언되야 함. (상속되면 안됨.)
- 동등성 : 값의 가치는 항상 같음.
- equals()와 hashCode() 메서드를 오버라이딩해서 동등성을 보장. (값이 같으면 동일한 객체로 취급)
- 자가검증 : 값은 그 자체로 올바름. 1은 사실 1.01이지 않을까? 같은 고민을 할 필요 X.
- 불변성 : 값이 변하지 않음.
Entity
Entity는 3종류로 나눔.
- 도메인 엔티티
- DB 엔티티
- JPA 엔티티
프로그래밍 언어와 데이터베이스 분야에서 표현하고 싶은 유무형의 자산 정보를 지칭하는 데 개체라는 용어를 사용.
NoSQL에서 Entity
관계형 데이터베이스에서 사용하는 엔티티라는 용어는 도큐먼트 데이터베이스에서 도큐먼트라는 용어에 대응됨.
3장 행동
데이터 위주의 사고와 행동 위주의 사고
자동차를 만들어 달라는 요청에 두 개발자가 아래와 같이 구현함.
- A 개발자
public class Car {
private Frame frame;
private Engine engine;
private Tire tire;
...
}
- B 개발자
public class Car {
public void drive() {}
public void changeDirection() {}
public void accelerate() {}
...
}
A 개발자를 데이터 위주의 사고를 가진 개발자, B 개발자를 행동 위주의 사고를 가진 개발자라고 함.
- 데이터 위주로 만들어진 클래스는 구조적인 데이터 덩어리를 만드는 데 사용하는 구조체와 다를바 없음.
- 전혀 객체지향 스럽지 않음.
- 객체를 구분 짓는 요인은 데이터가 아닌 행동임.
덕 타이핑이란
덕 테스트: 만약 어떤 새가 뒤뚱뒤뚱 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면 나는 그 새를 오리라고 부를 것이다.
- 덕 타이핑은 덕 테스트에서 유래한 용어임.
- 위 말을 개발자 관점에서 보면
행동이 같다면 같은 클래스로 부르겠다.
라는 의미. - Typescript는 덕타이핑을 지원.
class Duck {
walk() {
console.log('walk');
}
swim() {
console.log('swim');
}
}
class Person {
walk() {
console.log('walk');
}
swim() {
console.log('swim');
}
}
val duck: Duck = new Person();
- 행동이 곧 역할을 정의하고 역할이 곧 객체를 정의함.
구현을 생각하면 데이터 위주의 사고가 된다
public class Car {
private int degree; // 자동차의 각도(0도 ~ 360도)
public void changeDirection(int degree) {
this.degree = degree;
}
}
- 방향을 바꾸는 행동을 구현할려고 하다보니 degree라는 필드가 생김.
- 데이터 위주의 사고로 돌아옴.
- 구현을 고민했기 때문.
- 행동을 고민하면서 구현이나 알고리즘을 고민해서는 안됨.
- 협업시 인터페이스를 정의해놓고 각자 자리에서 본인의 역할을 다하면 됨.
인터페이스와 행동을 다르다
- 인터페이스는 외부에서 어떤 객체에게 행동을 시키고자 할 때 메시지를 보낼 수 있는 창구.
- 인터페이스란 어떤 행동을 지시하는 방법의 집합.
행동 위주의 사고를 하는 방법
- 실체에 집중할 때 데이터 위주의 사고를 하고 역할에 집중할 때 행동 위주의 사고를 함.
- ex : 자동차와 탈것, 자동차 -> 실체 / 탈것 -> 역할
- 행동과 역할에 집중하라는 것이 단순히 추상화를 많이 하라는 뜻이 아님.
함수와 메서드의 차이
- 함수의 각 입력값은 정확히 하나의 출력값으로 대응됨.
- 같은 입력에 대해 항상 같은 출력을 해야함.
- 같은 입력에 대해 두 개의 출력을 갖는 것은 함수가 아님.
- 예를 들어 Vehicle 인터페이스의 구현체 Car와 Airplane이 있을 때 vehicle.move() 함수의 결과가 다를 수 있다면 그건 함수가 아님.
- 메서드란 어떤 메시지를 처리해 달라는 요청을 받았을 때 이를 어떻게 처리하는지 방법(method)를 정의한 것.
9장 모듈
모듈이란
- 어떤 목적을 수행하기 위해 분리된 작은 구성 단위를 뜻함.
- 소프트웨어 공학에서 말하는 모듈이란 프로그램의 기본 구성요소이면서 라이브러리를 포괄하는 조금 더 큰 규모의 용어임.
- 독립성과 은닉성을 만족하며 연관된 코드들의 묶음.
- 따라서 모듈 시스템이란 의존성 관리와 캡슐화 관리에 대한 정보를 제공할 수 있어야 함.
- 의존성 관리 : 모듈을 사용하기 위해 어떤 의존성이 필요한지 명시할 수 있어야 함.
- 캡슐화 관리 : 모듈은 불필요한 구현을 외부로 드러내지 않아야 함.
Angular or NestJS or JS의 모듈
- import를 통해 의존성을 명시해주고 export를 통해 사용가능한 인터페이스를 제공함.
java 모듈 디스크립터
- java 9에서 module-info.java라는 모듈 디스크립터를 도입.
- module-info.java라는 파일을 통해서 모듈 시스템을 구현.
- 그럼 java 9 이전에는 모듈의 개념이 없었냐? 👉 맞음.
독립성
- 모듈이 독립적이어야 하는 이유는 유지보수를 용이하게 하고, 확장성을 높이고, 코드의 재사용성을 높이기 위함.
- 코드를 테스트하기 쉽게 만들 수 있어 전체 시스템의 품질을 높이는데 기여.
- A라는 패키지가 B 패키지에 의존하면 A 패키지의 독립성이 상대적으로 떨어질 수 있음.
- 👉 단 절대적으로 A 패키지가 "독립성이 떨어진다."라고 표현할 수는 없음.
- 모듈이 독립적이기 위해선 다음과 같은 룰들을 지켜야 함.
- 최대한 내부에서 해결하라.
- 외부에는 강하게 의존하지 마라.
- 외부 시스템을 사용한다면 외부 시스템의 사용을 명시하라.
하위 의존성 추적 기능으로 얻을 수 있는 이점
- 하위 모듈의 중복 검사와 불필요한 의존성이 있는지 검사할 수 있음.
- 하위 모듈에 보안 취약점이 생겼을 때 상위모듈 파악하기 쉬워짐.
- 동적 로딩이 가능해짐.
- 자바 스크립트에는 **트리 셰이킹(tree shaking)**이라는 개념이 있음.
- 애플리케이션에서 사용되지 않는 코드를 식별해 제거함으로써 번들(bundle) 파일의 크기를 줄이고 성능을 향상 시킴.
은닉성
- 하이럼 법칙
API 사용자가 충분히 많다면 계약을 어떻게 했는지는 크게 중요하지 않습니다. 시스템의 모든 관측 가능한 행동은 사용자에 의해 결정될 것입니다.
- API를 온갖 해괴망측한 방법으로 사용함.
- API에 할당된 암묵적 책임을 기대하고 사용하는 사용자가 많아질 수 있음.
- 자바 패키지 시스템이 문제임.
예를 들어, 어떤 라이브러리에 TEMP라는 의존성이 있었는데 A 개발자가 TMEP의 의존성을 더 이상 사용하지 않고 메이저 버전을 올렸음. 그런데 해당 라이브러리를 사용하는 B 개발자가 TEMP에 있는 코드를 사용하는 상황이 생김. B 개발자가 A 개발자에게 롤백을 요청하는 경우 발생. 이처럼, 사용자 뿐만이 아닌 라이브러리 개발자라면 개발자 역시 내 라이브러리를 온갖 해괴망측한 방법으로 사용할 수 있다는 사실을 고려해야 함.
- 몽키 패치
파이썬에서 프로그램이 실행되는 런타임에 동적으로 코드를 수정해서 기존 클래스나 모듈, 함수 등의 동작을 변경하는 것을 말함. 외부 라이브러리에 있는 버그를 임시로 수정하는데 유용하지만 개발자들이 온갖 해괴망측한 방법으로 사용.
패키지 구조
계층 기반 구조
project
└── src/main/java
└── com.demo.myapp
├── presentation
│ ├── UserController.java
│ ├── CafeController.java
│ ├── BoardController.java
│ └── PostController.java
├── business
│ ├── UserService.java
│ ├── CafeService.java
│ ├── BoardService.java
│ ├── PostService.java
│ └── repository
│ ├── UserRepository.java
│ ├── CafeRepository.java
│ ├── BoardRepository.java
│ └── PostRepository.java
├── domain
│ ├── User.java
│ ├── Cafe.java
│ ├── Board.java
│ └── Post.java
└── infrastructure
├── UserRepositoryImpl.java
├── UserJpaEntity.java
├── UserJpaRepository.java
├── CafeRepositoryImpl.java
├── CafeJpaEntity.java
├── CafeJpaRepository.java
├── BoardRepositoryImpl.java
├── BoardJpaEntity.java
├── BoardJpaRepository.java
├── PostRepositoryImpl.java
├── PostJpaEntity.java
└── PostJpaRepository.java
- 레이어에 관한 약간의 이해와 이에 대응하는 스프링 컴포넌트가 무엇인지만 알면 누구나 쉽게 개발할 수 있음.
- 도메인이 눈에 들어오지 않는다는 단점이 있음.
- 비즈니스 코드를 한곳에 모아 볼 수 없음.
도메인 기반 구조
project
└── src/main/java
└── com.demo.myapp
├── user
│ ├── presentation
│ │ └── UserController.java
│ ├── application
│ │ └── UserService.java
│ ├── repository
│ │ └── UserRepository.java
│ ├── domain
│ │ └── User.java
│ └── infrastructure
│ ├── UserRepositoryImpl.java
│ ├── UserJpaEntity.java
│ └── UserJpaRepository.java
├── cafe
│ ├── presentation
│ │ └── CafeController.java
│ ├── application
│ │ └── CafeService.java
│ ├── repository
│ │ └── CafeRepository.java
│ ├── domain
│ │ └── Cafe.java
│ └── infrastructure
│ ├── CafeRepositoryImpl.java
│ ├── CafeJpaEntity.java
│ └── CafeJpaRepository.java
├── board
│ ├── presentation
│ │ └── BoardController.java
│ ├── application
│ │ └── BoardService.java
│ ├── repository
│ │ └── BoardRepository.java
│ ├── domain
- 계층 기반 구조보다는 복잡해짐.
- 도메인이 눈에 들어옴.
10장 도메인
- 도메인은 애플리케이션이 해결하고자 하는 문제 영역을 의미.
- 사업가 입장에서 소프트웨어 시스템을 만들게 되는 계기.
- 사용자가 겪는 문제를 해결해주는 것이 비즈니스.
- 소프트웨어는 그러한 해결책 중 하나로 선택.
- 린
- 군더더기 없는이라는 뜻으로 도요타의 생산(learn production) 사례를 통해 크게 유명해짐.
- 린 스타트업.
- 린 방식의 업무 스타일.
- 사용자들이 겪는 문제 영역이 바로 도메인.
- 우리가 개발해야할 것은 애플리케이션이 아니라
도메일 어플리케이션
임. - 도메인을 분석하고 사용자들이 겪는 문제를 인지해 소프트웨어적인 해결책을 제시할 수 있어야 함.
- 도메인 모델과 영속성 객체(JPA Entity)는 구분해야 하는가?
- 통합할 경우.
- 개발속도가 빠름.
- 단일 책임 원칙을 위반함.
- 데이터베이스 스키마 변경이나 마이그레이션 걱정을 먼저하게 됨. 👉 데이터베이스 위주의 사고가 됨.
- 분리할 경우.
- 개발 속도가 느려짐.
- 작성해야 하는 코드 양이 늘어남.
- 데이터베이스 변경에 따른 도메인의 변경이 일어나지 않아도 됨.
- 통합할 경우.
11장 알아두면 유용한 스프링 활용법
- interface를 List로 Bean 주입을 받게 되면 해당 interface를 구현한 모든 Bean이 들어오게 됨.
public interface NotificationChannel {
void notify();
boolean supports(Account account);
}
public class NotificationService {
private final List<NotificationChannel> notificationChannels;
public void notify(Account account, String message) {
if (notificationChannels.supports(account)) {
NotificationChannel.notify(account, message);
}
}
}
- 자기호출.
- 아래 코드는 Transactional이 적용이 안됨.
@Controller
@RequiredArgsConstructor
class MyClass {
private final MyService myService;
@GetMapping
@ResponseStatus(OK)
public Object doSomething() {
myService.doSomething1();
return null;
}
}
@Service
@RequiredArgsConstructor
class MyService {
public void doSomething1() {
}
@Transactional
public void doSomething2() {
}
}
12장 자동 테스트
- 인수 테스트는 시스템이 비즈니스 요구사항을 만족해서 소유권을 넘기기 전에 수행하는 테스트 단계를 뜻함.
- 시스템을 인수인계하기 전에 시스템이 비즈니스 요구사항과 일치하는지 마지막으로 검증하는 단계.
- 인수 테스트는 테스트 단계 중 가장 마지막에 수행하는
단계
를 뜻함. - 인수 테스트 과정에는 수동 테스트가 있을 수도 있고 자동 테스트가 있을 수도 있음.
- 수동 테스트의 단점
- 인수 테스트 과정의 테스트를 대부분 수동으로 구성하면 작업 자들이 인수 테스트를 수행할 때 너무 많은 비용이 듬.
- 테스트 절차를 사람이 직접 수행하고 눈으로 확인하는 데는 많은 시간과 노력이 들기 때문.
- 사람이 테스트를 수행하기 때문에 실수할 수도 있고 버그 상황을 눈치채지 못하고 넘어갈 수 있음.
- 한 번 만들어진 수동 테스트는 누적 됨. 👉 해당 기능이 없어지는 날까지 해야함.
- 자동 테스트를 해야 하는 이유
- 테스트가 실패했다는 것은 개발 과정에서 의도하지 않은 버그가 만들어졌음을 의미.
- 자동 테스트는 수동 테스트와는 다르게 개발자에게 좋은 의미로 누적. 👉 코드 변경을 할 때 용감하게 함.
Regression
- Regression
- 시스템에서 정상적으로 제공하던 기능이 어떤 배포 시점을 기준으로 제대로 동작하지 않게 되는 상황.
- 회귀 버그라고도 부름.
- 프로젝트 구조를 온전히 파악하기 전까지 자신감 있게 개발하지 못한다는 것은 어떤 숙련된 개발자가 와도 본인의 기량을 전부 발휘할 수 없음.
- 기존 코드를 변경하거나 리팩터링 하는 경우 어떤 사이드 이펙트가 발생할지 모르기 때문.
의도
- 아래와 같은 코드가 있다고 가정할 때
userRepository.findById
메서드의 존재 이유를 제대로 파악불가. - 주석이나 메서드 명을 통해서 의도가 정확히 파악되지 않음.
- user가 있는지 확인한다는 주석이나
userRepository.assertExistence
같은 이름으로 메서드명을 지었다면 이해하기 쉬웠을 것.
- user가 있는지 확인한다는 주석이나
- 이 때, 테스트 코드가 작성되어 있다면 테스트 코드 그 자체나 테스트 코드 실패를 통해서
userRepository.findById
메셔드의 의도를 파악할 수 있고 존재해야 하는 코드인지 파악 가능. - 그래서 테스트는 책임에 대한 계약이라고 할 수 있음.
public void doSomething(long userId) {
userRepository.findById(userId);
// 이하 생략
}
레거시 코드
- 레거시 코드는 통상적으로 오래된 소프트웨어 시스템에 존재하는 코드를 의미함.
- 하지만 마이클 c. 페더스는 다음과 같이 얘기함.
- "내게 레거시 코드란 테스트 코드가 없는 코드를 의미한다."
- 이 정의에 따르면 레거시 코드는 테스트가 작성되있지 않아 변경하기 어려운 코드를 레거시라고 함.
- 바로 어제 짠 코드도 테스트 코드가 없다면 레거시 코드가 될 수 있다는 것.
13장 테스트 피라미드
-
대중적인 분류체계인 테스트 피라미드
- 단위 테스트
- 소프트웨어를 구성하는 가장 작은 단위를 검증하는 테스트를 의미함.
- 객체나 컴포넌트에 할당된 작은 책임하나가 예상대로 동작하는지 확인하는 테스트라고 할 수 있음.
- 통합 테스트
- 여러 컴포넌트나 객체가 협력하는 상황을 검증하는 테스트.
- 통합 테스트는 독립적으로 만들었던 객체들이 상호작용하면서 생길 수 있는 상황을 검증.
- 서로 다른 모듈이나 시스템 간의 인터페이스를 테스트하는 것이라 볼 수도 있음.
- E2E 테스트(인수테스트)
- 사용자의 관점에서 시스템이 어떻게 동작하는지 검증하는 테스트.
- 사용자의 요구사항을 만족하는지 검증하는 테스트라고 할 수 있음.
- 단위 테스트
-
단위 테스트가 제일 많아야 하고 그 다음으로 통합 테스트가 많아야 하고 그 다음으로 E2E 테스트가 많아야 함.
- 단위 테스트 80%, 통합 테스트 15%, E2E 테스트 5% 정도가 적절한 비율이라고 함.
구글의 테스트 피라미드
- 위에서 말한 단위 테스트, 통합 테스트, E2E 테스트는 설명하기 모호함.
- 구글에서 쓰이는 테스트 피라미드 모델은 다음과 같음.
- 대형 테스트 : 멀티 서버에서 동작하는 테스트를 의미.
- 중형 테스트 : 단일 서버에서 동작하되, 멀티 프로세스, 멀티 스레드를 사용할 수 있는 테스트를 의미.
- 소형 테스트 : 단일 서버, 단일 프로세스, 단일 스레드에서 동작하며 디스크 I/O, 블로킹 호출이 없는 테스트를 의미.
테스트 분류 기준
- 테스트는 결정적이고 빠를 수록 좋음.
- 결정적이어야 한다는 말은 같은 코드를 대상으로 실행하는 테스트는 항상 같은 응답을 해야한다는 의미.
- 즉, 네트워크 환경 Disk I/O 처리 상태 등에 따라서 테스트 결과가 달라지면 안됨.
- 테스트에 H2를 사용한다는 것은 H2를 위한 별도의 프로세스를 실행한다는 의미임.
- 따라서 H2 데이터베이스를 사용하는 테스트는 중형 테스트라고 할 수 있음.
14장 테스트 대역
- 테스트 대역이란, 영화에 나오는 스턴트맨(대역)처럼 실제 객체를 대신하는 객체를 의미함.
유형 | 설명 |
---|---|
Dummy | 아무런 기능이 없음. |
Stub | 특정한 상황에 대해 미리 정해진 결과를 반환함. |
Fake | 자체적인 로직이 있음. |
Mock | 아무런 동작을 하지 않지만 어떤 행동이 호출됐는지 기록함. |
Spy | 실제 객체를 대신하면서 호출된 행동을 기록함. |
Dummy
- 아무런 동작도 수행하지 않는 객체.
- 아래와 같이 SomethingFilter를 구현해서 테스트 한다고 가정함.
- 테스트 목표는 giveMe attribute가 text인 경우 응답의 Content-Type을 text/plain으로 변경하는 것.
- 하지만 filterChain의 역할은 아무것도 없는데 그럼에도 filterChain은 넣어줘야 함.
- 이럴 경우 filterChain을 Dummy로 대체할 수 있음.
public class SomethingFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// text를 요청하는 request라면 응답의 Content-Type을 text/plain으로 변경
if (request.getAttribute("giveMe").equals("text")) {
response.setContentType("text/plain");
}
// 책임 연쇄 패턴에 따라 다음 필터를 실행하기 위해 필터 체인의 doFilter를 호출
chain.doFilter(request, response);
}
}
@Test
public void 요청에_text로_달라는_요청이_있으면_응답의_콘텐츠_타입은_text_plain이어야_함() {
// given
ServletRequest request = new MockHttpServletRequest();
request.setAttribute("giveMe", "text");
ServletResponse response = new MockHttpServletResponse();
// when
SomethingFilter filter = new SomethingFilter();
filter.doFilter(request, response,
new FilterChain(
@Override
public void doFilter(ServletRequest request, ServletResponse response) {
// do nothing
})
);
// then
assertThat(response.getContentType()).isEqualTo("text/plain");
}
- 위와 같이 Dummy로 생성된 FilterChain은 아무런 동작을 하지 않지만 테스트 실행 결과에는 영향이 없음.
Stub
- Stub은 '부본', '짧은 부분'이라는 뜻이 있음.
- 원본과 비슷하게 만들어 참고로 보관하는 서류를 뜻하는 말.
- 응답을 최대한 비슷하게 만들어 참고로 사용하는 객체.
@RequiredArgsConstructor
public class UserService {
private final UserRepository userRepository;
public User register(UserCreateDto userCreateDto) {
if(userRepository.findByEmail(userCreate.getEmail()).isPresent()) {
throw new DuplicatedException("이미 존재하는 이메일입니다.");
}
...
}
}
- 위와 같은 코드를 테스트한다고 가정할 때, 아래와 같이 Stub을 사용할 수 있음.
class StubExistUserRepository implements UserRepository {
@Override
public Optional<User> findByEmail(String email) {
return Optional.of(User.builder()
.id(1L)
.email(email)
.nickname("nickname")
.build());
}
}
class StubEmptyUserRepository implements UserRepository {
@Override
public Optional<User> findByEmail(String email) {
return Optional.empty();
}
}
- 그리고 위 Stub을 Injection 해서 테스트를 진행할 수 있음.
Fake
- 자체적인 로직을 갖고 있는 객체.
- 예를 들면 위에서는
UserRepository
의 특정한 입력에 대한 특정한 반환을 했다면 Fake은 UserRepository의 로직을 직접 구현한 객체를 의미함.
public class FakeUserRepository implements UserRepository {
private final long autoGeneratedId = 0L;
private final List<User> data = new ArrayList<>();
@Override
public Optional<User> findByEmail(String email) {
return data.stream()
.filter(user -> user.getEmail().equals(email))
.findFirst();
}
@Override
public User save(User user) {
if(user.getId() == null || user.getId() == 0) {
// create 동작
} else {
// update 동작
}
return user;
}
}
Mock
- 아무런 기능도 안하지만 주로 메서드 호출이 발생했는지 여부를 검증.
public class MockVerificationEmailSender implements EmailSender {
private boolean called = false;
@Override
public void send(Email email) {
called = true;
}
public boolean isCalled() {
return called;
}
}
Spy
- 어떤 메서드가 호출되고 이벤트가 몇 번 발생했는지 확인할 때 사용.
public class SpyUserRepository extends UserRepository{
private int findByEmailCall = 0;
private int saveCall = 0;
@Override
public Optional<User> findByEmail(String email) {
findByEmailCall++;
return super.findByEmail(email);
}
@Override
public User save(User user) {
saveCall++;
return super.save(user);
}
}
- 또는 UserRepository를 implements하고 JpaUserRepository를 주입받아서 구현할 수도 있음.
public class SpyUserRepository implements UserRepository {
private final UserRepository userRepository;
private int findByEmailCall = 0;
private int saveCall = 0;
public SpyUserRepository(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public Optional<User> findByEmail(String email) {
findByEmailCall++;
return userRepository.findByEmail(email);
}
@Override
public User save(User user) {
saveCall++;
return userRepository.save(user);
}
}
상태기반 검증 & 행위기반 검증
- 상태기반 검증
- 테스트 대상의 상태가 어떻게 변화하는지 검증하는 방법.
- 행위기반 검증
- 테스트의 검증 동작에 메서드 호출 여부를 보게하는 방법.
- 상태기반 검증으로 하는 편이 좋음. 👉 테스트를 책임 단위로 볼 수 있기 때문.
- 상태기반 테스트는 '어떻게 목표를 달성해왔는가?'에 집중함.
결론
- 테스트 대역을 적절히 사용하면 중형으로 보였던 테스트를 소형으로 변경할 수 있음.
- 기술을 숙달하기 전에 기반 지식은 반드시 숙지해야 함.
- 자바가 갑자기 멸망하면 js나 python의 테스팅 프레임웤을 다시 공부해야함.
- 하지만 테스트 대역을 알면 어떤 언어에서도 응용이 가능함.
- 추상화와 의존성 역전을 잘 해놔야 대역을 활용하기 좀 더 편해짐.