1. 탄생 배경

기존의 단위 테스트(Unit Testing)와 통합 테스트(Integration Testing)가 가진 한계를 극복하기 위해 만들어졌습니다.

  • 단위 테스트(Unit Testing) : 개별 함수클래스 등 작은 코드 단위를 테스트
    • 한계점
      • 시스템 전체의 흐름이나 상호작용을 확인하기 어려움
      • 실제 사용자 시나리오를 반영하지 못함
  • 통합 테스트(Integration Testing): 여러 모듈이나 컴포넌트를 통합한 테스트
    • 한계점
      • 전체 시스템 관점에서의 테스트 부족
      • 외부 시스템과의 연동 문제를 발견하기 어려움

두 테스트의 주요 문제점

  • 전체 시스템 흐름 검증 부족
  • 사용자 관점 부재
  • 시스템 간 의존성 문제 - 외부 시스템, API, 데이터베이스와의 연동에서 발생하는 의존성 문제
  • 데이터 무결성 확인 어려움 -  시스템 전반에 걸친 데이터 흐름을 확인하기 어려움
  • 복잡한 버그 미검출  - 여러 모듈컴포넌트의 상호 작용에서 발생하는 복잡한 버그

# 모듈(Module)

- 여러 개의 함수나 클래스를 모아 특정 기능을 수행하는 코드의 단위

    (예시) 사용자 관리 모듈: 회원가입, 로그인, 비밀번호 변경 등과 관련된 기능들을 모아 놓은 것

 

# 컴포넌트(Component) 

- 시스템에서 특정 기능이나 역할을 수행하는 독립적인 단위

- 독립적이고 재사용 가능한 단위로, 하나 이상의 모듈로 구성

    (예시) 인증 컴포넌트 : 사용자의 신원을 확인하고, 인증된 사용자에게 접근 권한부여 (jwt 토큰 발급, 검증 등) 

2. 사용하는 이유

  • 전체 시스템 검증: E2E 테스트는 전체 애플리케이션의 기능을 검증하여 시스템이 통합적으로 잘 작동하는지를 확인
  • 사용자 경험 반영: 실제 사용자 행동을 시뮬레이션 및 검증하여 시스템의 품질 보장
  • 시스템 간 의존성 문제 해결 : 외부 시스템과의 연동에서 발생하는 문제를 효과적으로 테스트
  • 데이터 무결성 확인 :  데이터가 시스템 전반에 걸쳐 올바르게 전달되고 처리되는지 검증.
  • 복잡한 워크플로우 테스트: 단순한 기능 테스트로는 발견하기 어려운 복잡한 시나리오나 예외 상황을 테스트

즉, 프로덕션 환경에서 발생할 수 있는 문제를 미리 발견하여 배포 후의 리스크를 감소시키기 위해서다.

3. 장점과 단점

장점:

  • 사용자 중심의 테스트: 실제 사용자의 행동을 기반으로 테스트하여 사용자 경험을 향상
  • 높은 신뢰성: 전체 시스템의 기능을 검증하기 때문에 배포 전에 시스템의 안정성을 높일 수 있습니다.
  • 문제 조기 발견: 통합된 환경에서의 문제점을 미리 발견하여 수정 비용을 줄입니다.
  • 비즈니스 로직 검증 : 실제 시나리오를 테스트함으로써 비즈니스 로직이 의도한 대로 동작하는지 확인할 수 있습니다.

단점:

  • 시간과 비용 소모: 테스트 범위가 넓어 실행 시간과 유지 보수 비용이 증가할 수 있습니다.
  • 복잡성 증가: 다양한 시나리오를 커버하려면 테스트 스크립트가 복잡해질 수 있습니다.
  • 테스트 불안정성: 작은 UI 변화나 환경 변화에도 테스트가 실패할 수 있어 민감하게 반응합니다.
  • 테스트 유지보수 어려움 : 작은 변경에도 테스트 스크립트 수정이 필요할 수 있습니다.

4. 요약

E2E 테스트

  • 기존 테스트 방식의 한계를 보완하여 전체 시스템 동작과 사용자 경험을 종합적으로 검증

도입 효과

  • 더 나은 품질의 소프트웨어 개발 가능.
  • 사용자에게 안정적이고 신뢰성 있는 서비스 제공.

E2E 테스트는 시스템 전체의 품질을 보장하기 위한 중요한 도구이지만, 효율적인 테스트 전략을 수립하여 단점을 최소화하는 것이 중요

 

Postman 을 사용

GET 요청을 할 때 Body에 데이터를 담아서 보내어도 성공적으로 내가 원하는 값을 리턴 받을 수 있었다.

 

e2e 테스트에서 

Postman 사용할 때와 동일하게 GET 요청에 .send() 메서드를 사용하여 데이터를 전달.

하지만, GET 요청에서는 데이터를 본문(body)로 보내지 않기 때문에 빈 객체가 반환되는 문제가 발생.

 

.query() 를 이용해서 데이터를 전달해야 한다.

 

 

// e2e code
it('저장된 exercise 중 하나를 운동부위와 운동이름을 이용하여 검색하면 200 Ok 코드를 받는다.', async () => {
    // Given: 저장된 exercise
    const exerciseData: ExerciseDataFormatDto[] = [{ bodyPart: BodyPart.SHOULDERS, exerciseName: '숄더프레스' }];
    await request(app.getHttpServer()).post('/exercises/').send(exerciseData);

    // When: 운동부위와 운동이름을 이용하여 검색을 한다.
    const response = await request(app.getHttpServer())
      .get('/exercises/')
      .query({ bodyPart: BodyPart.SHOULDERS, exerciseName: '숄더프레스' });
    // Then:  200 Ok 코드를 받는다.
    expect(response.status).toBe(200);
  });


// service code
@Get('/exercises/')
async getExercises(@Query() query: any) {
  const data = { bodyPart: query.bodyPart, exerciseName: query.exerciseName }
  return await this.exerciseRepository.findExercises(data);
}

목차

【 강의 24 】 전문기술의 응용범위

❏  하테나의 데이터로 검색엔진 만들기

검색엔진을 만들 때 중요한 요소 중 하나인 역 인덱스( inverted index)에 대한 설명

역 인덱스의 두 가지 기본요소

  • Dictionary
  • Postings

❏  하테나 다이어리의 전문 검색 - 검색 서비스 이외에 검색 시스템 이용

‣ RDB 로 처리 -확장성 측면에서 문제

구현 방식

  1. 유저가 글을 작성하면 해당 글에 포함되어 있는 키워드를 전부 추출
  2. 이 단어들과 블로그의 연관성을 데이터베이스의 레코드로서 저장

>> 단어 "perl" 을 검색하면 해당 단어가 포함된 블로그 목록을 표시

<문제 > 레코드 수가 많아지면서 확장성 측면에서 많은 제한 발생 

 

‣ 검색기술의 응용

"포함하는 블로그" : 특정 단어를 포함하는 블로그 검색 

  • 검색엔진기술을 응용하여 구현 
  • 다이어리의 사양에 특화시킨 방법으로 검색속도 향상
    • 불필요한 결과정렬 방식을 제외:  날짜순으로만 정렬
    • 각 글의 id 를 하테나 다이어리의 사양에 특화시킨 방법으로 저장 

❏ 하테나 북마크의 전문 검색 - 세세한 요구를 만족시키는 시스템

"마이 북마크 검색" : 각 개인이 북마크한 개인 데이터로부터 검색하는 시스템

  • 각 사용자가 북마크를 하는 타이밍에 각 사용자별로 검색 인덱스 구현 및 갱신
  • 직접 구현 함으로써 세세한 요구사항에 대응

【 강의 25 】 검색 시스템의 아키텍처

❏  검색 시스템이 완성되기까지

‣ 검색 시스템의 여섯 단계 

  1. 크롤링
  2. 저장
  3. 인덱싱
  4. 검색
  5. 스코어링
  6. 결과표시

각 단계별 과제

  1. 대상의 문서를 가져올 플랫폼을 정해서 웹 크롤러를 만들어서 대량의 문서를 가져오는 작업
  2. 대량의 문서를 어떻게 저장할 것인가 (하나의 DB에 저장하면 해당 DB에 문제가 생기면 복구 불가 → 분산 DB에 저장해야하는 문제)
  3. 가져온 문서로 부터 인덱스(고속으로 검색하기 위한 구조)를 구축
  4. 검색 결과를 어떻게 정열할 것인가, 어떤 순서로 보여줄 것인가

❏  다양한 검색엔진

오픈소스

등 다양한 오픈소스가 있다. 

❏  전문 검색의 종류

grep 형 

  • 검색 대상 문서를 처음부터 전부 읽는다 O(mn) text: m, word: n 
    • 계산량을 개선한 방법 : KMP(Knuth-Morris-Pratt, O(m + n)) BM(Boyer-Moore, 최악O(mn), 최선O(n/m)) 
  • 즉시성이 좋고, 검색누락이 없으며, 병렬화나 쿼리 확장이 용이(쿼리에 정규표현 사용)
    • UNIX 에서 유용한 명령어
  • 대규모 환경에서 구현하기에는 무리가 있는 방식

‣ Suffix 형

  • 문서를 검색 가능한 형태로 보유 - 전부 메모리에 올릴 수 있는 형태
  • 데이터 구조: Tire, Suffix Array, Suffix Tree
  • 이론적으로는 가능
  • 정보량이 크고 구현하기 어려움

‣ 역 인덱스형 (주류)

  • 실제 시스템에서 많이 채택되는 방식( Google)
  • 단어(term)와 문서를 연관짓는 방식
  • 즉시성 측면에서 뛰나지 못함 → 검색누락 발생 가능
    • grep 처럼 문서가 변경되면 바로 검색결과도 바뀌는 방식은 어려움

【 강의 26 】 검색엔진의 내부구조

❏  역 인덱스의 구조 - Dictionary + Postings

문서를 인덱스화 한다? → 아래의 그림을 말하는 것

아래의 그림에서 2번을 참고하면 각 단어에 연결되어있는 문서 번호가 있다.

Dictionary? 좌측에 있는 단어(term)의 집합

Postings? 우측에 있는 번호들처럼 각 단어를 포함하고 있는 문서는 몇 번인지를 나타내는 것

(예) Dictionary 내에 있는  term: '하테나' 를 포함하고 있는 Postings 내에는 1, 3, 4 가 있다.

❏  Dictionary 만드는 법

문장에 있는 단어를 term으로 추출한다.

단어를 추출할때 사용가능한 방식들

  • 사전 이용( Wikipedia)
  • 형태소 분석
  • n-gram 기법
  • AC (Aho-Corasick) 법 
    *Trie 에서 패턴 매칭으로 매칭이 진행되다가 도중에 실패했을 경우, 되돌아오는 길의 엣지를 다시 Trie에 추가한 데이터 구조를 사용한 방법 (p175, 20강)

‣ 언어의 단어를 term 으로 다루기

  • 사전과 AC법을 이용하는 방법
  • 형태소 분석을 통하여 단어찾는 법 - 검색 누락 발생 가능 
    • stemming 이나 Lemmatizer 등을 이용하여 부분적 극복가능

‣ n-gram을 term 으로 다루기

  • 텍스트를 n 자씩 잘라내는 방식 : "하테나 다이어리" -(2-gram)-> 하테 /테나 /나다 /다이 /이어 /어리 
  • 쿼리도 동일한 규칙으로 분할해야한다.
  • 원하지 않는 결과가 나올 수도 있다.
    • 東京都 문제 :   
      • 東京都 -(2-gram)->  東京 / 京都 
      • 문서: "東京 타워와 京都타워" 라는 문서를 인덱싱했을때. 이 문서가  東京都로 검색했을때 검색된다.

‣ 검색 시스템 평가와 Recall/Precision

재현률(Recall)/ 적합률(Precision)을 이용하여 검색 시스템에 특정 쿼리를 입력했을 때의 성능을 정량화 가능

  • Recall : 검색 결과에 쿼리를 포함한 문서가 얼마만큼 포함되었는지를 의미 ( 올바른 결과의 수 / 적합한 결과 총수)
  • Precision : 검색 결과가 얼마나 정확한 결과가 들어 있는지를 의미 ( 올바른 결과의 수 / 반환한 결과 총수)

n-gram 방식은 검색누락은 발생 안하지만 원하지는 않는 결과가 반환 → Recall이 우선

형태소 방식은 검색이 안되는 것도 있지만 원하지는 않는 결과가 반환되지 않음 → Precision 이 우선

 

타이틀, 코멘트, URL을 대상으로 검색할 때는 n-gram 사용

본문 검색에는 단어기반을 사용

❏  지금까지의 정리

검색엔진 중에 가장 많이 사용되는 방식은 역 인덱스 방식

역 인덱스 방식 : Dictionary + Postings 구조

Dictionary 를 만드는 방식에는 n-gram 과 형태소 분석을 기반으로 하는 방법이 있다.

❏  Postings 작성법

term 이 해당 문서 내에 출현 위치도 저장하는 경우 ( Full Inverted Index)

  • 스피닛을 뽑아낼때 이 단어가 문장 내의 어디에 포함되어 있느지 바로 알수 있음
  • 스코어링에도 도움
  • 필터링에도 용이

문서 ID만을 저장하는 경우

  • 정렬(오름차순/내림차순) → VB Code로 압축
  • 어느 정도의 데이터의 압축률과 빠른 전개 성능
  • 구조  term → 압축된 Postings List 
    • key-value 스토어에 적합

❏  Scoring 에 대한 보충

검색결과를 어떤 순서로 표시할 건지는 상당히 중요한 문제!!

 

예를 들어 Google은 문서의 중요성을 고려해서 랭킹을 매겨서 검색결과를 표시한다.

주요 알고리즘 : PageRank

PageRank + 검색어의 출현위치 + 이외 다양한 알리고리즘을 이용해여 문서의 중요성을 파악

 

문서의 중요도를 파악하는데 자주사용되는 알고리즘 TF/IDF 가 있다.

목차

JUnit 5 모듈 구성

Assertions 클래스의 주요 단언 메서드

테스트 라이프사이클

테스트 메서드 간 실행 순서 의존과 필드 공유하지 않기

추가 애노테이션: @DisplayName, @Disabled

모든 테스트 실행하기

 

JUnit 5 모듈 구성

JUnit 5는 크게 세 개의 요소로 구성되어 있다.

  • JUnit 플랫폼 : 테스팅 프레임워크를 구동하기 위한 런처와 테스트 엔진을 위한 API를 제공

  • JUnit 주피터(Jupiter) : JUnit 5를 위한 테스트 API와 실행 엔진을 제공

  • JUnit 빈티지(Vintage) : Junit 3과 4로 작성된 테스트를 JUnit 5 플랫폼에서 실행하기 위한 모듈을 제공

Assertions 클래스의 주요 단언 메서드

메서드 설명
assertEquals(expected, actual) actual 값이 expected 값과 같은지 검사한다.
assertNotEquals(unexpected, actual) actual 값이 unexpected 값과 같지 않은지 검사한다.
assertSame(Object expected, Object actual) 두 객체가 동일한 객체인지 검사한다.
assertNotSame(Object unexpected, Object actual) 두 객체가 동일하지 않은 객체인지 검사한다.
assertTrue(boolean condition) 값이 true인지 검사한다.
assertFalse(boolean condition) 값이 false인지 검사한다.
assertNull(Object actual) 값이 null 인지 검사한다.
assertNotNull(Object actual) 값이 null 이 아닌지 검사한다.
fail() 테스트를 실패 처리한다.

 

테스트 라이프사이클

■@BeforeEach 와 @AfeterEach

@BeforeEach 테스트를 실행하기 전  필요한 준비 작업을 할 때 사용
@AfeterEach 테스트를 실행한 후에 정리할 것이 있을때 사용 
  1. 테스트 메서드를 포함한 객체 생성
  2. (존재하면) @BeforeEach 애노테이션이 붙은 메서드 실행
  3. @Test 애노테이션이 붙은 메서드 실행
  4. (존재하면) @AfterEach 애노테이션이 붙은 메서드 실행

■@BeforeAll 와 @AfeterAll

@BeforeAll 한 클래스의 모든 테스트 메서드를 실행하기 전에 한번 실행
@AfeterAll 한 클래스의 모든 테스트 메서드를 실행한 후에 실행

 

테스트 메서드 간 실행 순서 의존과 필드 공유하지 않기

테스트를 할때  테스트 메서드를 작성한 순서 대로 실행된다는 가정하에 테스트 메서드를 작성하면 안된다.

 

각 테스트 메서드는 서로 독립적으로 동작해야 한다. (서로 의존하면 안된다.)

  • 하나의 테스트의 결과가 다른 테스트에 영향을 미치면 안된다.
  • 테스트 메서드가 서로 필드를 공유하면 안된다.

 

추가 애노테이션: @DisplayName, @Disabled

java 는 메서드 이름만으로 테스트의 내용을 설명하기 부족할 수 있다.

이때 @DisplayName("테스트 내용") 을 이용해서 테스트 설명 가능

모든 테스트 실행하기

모든 테스트를 실행하는 경우

  • 코드를 원격 리포지토리에 푸시하기 전
  • 코드를 빌드해서 운영 환경에 배포하기 전

모든 테스트를 실행하여 실패하는 테스트가 존재하면 코드 푸시와 배포를 멈추고 원인을 찾기 위함.

 

 

실행하는 방법

  • 메이븐 : mvn test
  • 그레이들 : gradle test
  • 인텔리J, 이클립스: src/test/java 폴더에서 JUnit Test 실행

목차

기능 명세

설계 과정을 지원하는 TDD

기능 명세 구체화

 

기능 명세

설계는 기능 명세로부터 시작한다.

 

기능은 입력과 결과를 가진다.

입력 - 기능을 실행하는데 필요한 값

결과 - 기능을 수행한 결과로 상황에 따라 달라질 수 있다.

  • 리턴 값
  • 익셉션 : throw 된 예외
  • 변경 : DB/ 시스템 에 변경된 사항

 

다양한 형태의 요구사항 문서를 이용해서 기능 명세를 구체화

▶ 기능 명세를 구체화하는 동안 입력과 결과를 구상

도출한 기능 명세를 코드에 반영

설계 과정을 지원하는 TDD

TDD 는  테스트 코드 만들기 → 테스트를 통과시키기 위해 코드 구현 → 리팩토링 의 반복.

 

테스트 코드를 만들기 위해 필요한 사항

  • 테스트할 기능을 실행
  • 실행 결과를 검증

 
기능을 실행할 수 없으면 테스트를 할 수 없다.
  • 테스트에서 실행할 수 있는 객체나 함수가 필요

  • 실행할 객체가 존재하려면 클래스 와 메서드도 필요

    • 클래스나 메서드를 정의하기 위한 이름을 결정

    • 메서드를 실행할 때 사용할 인자의 타입과 개수를 결정

예를 들어 "암호 강도 측정과 만료일 계산 예제" 에서 결정한 사항

  • 클래스 이름
  • 메서드 이름
  • 메서드 파라미터
  • 실행 결과

이 과정에서 이름(기능을 정확하게 표현하는 이름)을 고민하고 파라미터 타입과 리턴 타입을 고민했다. 

→ 이러한 과정이 설계 과정

 

구현하는 기능을 정확하게 표현하는 이름을 사용하는 것이 중요하다.
→ 이름을 통해서 어떻게 동작하는지 예상이 가능 해야한다.

이름을 고민하는 시간을 아까워하지 말자.

 

 

■ 필요한 만큼 설계하기

  • TDD는 테스트를 통과할 만큼만 코드를 작성한다.
  • 필요한 것으로 예측해서 미리 코드를 만들지 않는다.
  • 실제 예외처리가 필요한 시점에서 익셉션을 도출

기능 명세 구체화

테스트 코드를 작성하려면 입력과 결과가 명확해야 한다.

  • 전달받은 요구사항에서 애매한 점을 발견?! → 해당 담당자에게 질문을 하여 구체적으로 정리

(예시) 만료일 계산 기능

 

요구사항

  • 서비스를 사용하려면 매달 1만 원을 선불로 납부한다. 납부일 기준으로 한 달 뒤가 서비스 만료일이 된다.
  • 2개월 이상 요금을 납부할 수 있다.
  • 10만 원을 납부하면 서비스를 1년 제공한다

개발자 :  한 달 뒤가 어느 시점인지 애매하다.

질문

  • 4월 1일에 만원을 납부하면 만료일은 언제예요? 4월 30일인가요? 5월 1일인가요?
  • 1월 31일에 만원을 납부하면 만료일은 언제인가? 2월 28일인가요? 아니면 30일 뒤인 3월 2일인가요?
  • 만약 윤년인 경우는 어떻게 되나요?
  • 1월 29일이나 1월 30일에 만원을 납부해도 2월 28일인가요? 윤년이면 2월 29일이 되구요?
  • 1월 31일이나 5월 31일처럼 만원을 납부한 일자가 다음 달 말일 일자보다크면 이때 만료일은 다음 달 말일이라고 생각면 될까요?

이와 같이 애매한 점을 질문하여 구체적인 예를 이용하여 테스트 코드를 만든다.

또한 테스트 코드를 만들면서 애매한 점이 있으면 질문을 하여 구체적인 예를 통해 모호함을 해결해야한다.

 

복잡한 로직을 구현해야 하는 것은 결국 개발자이므로
개발자는 최대한 예외적인 상황이나 복잡한 상황에 해당하는 구체적인 예를 끄집어내야한다.
이를 위한 가장 좋은 방법은 담당자와 대화를 하는 것이다.

+ Recent posts