목차

좋은 테스트 코드를 만들기 위해 주의해야 할 사항

테스트코드와 유지보수

테스트 코드는 그 자체로 코드이기 때문에 제품 코드와 동일하게 유지보수 대상이 된다.

실패한 테스트를 통과시키기 위해 많은 노력이 필요하면 점점 테스트 코드에서 멀지고 TDD에서도 멀어진다.

 

악순환 예시

  • 테스트 코드를 유지보수하는데 시간이 많이 든다 → 실패하는 테스트 증가 →실패한 테스트에 무감각 → 깨지는 테스트를 방치(주석처리) → 테스트 실패 여부에 상관없이 빌드 배포 

이를 막기위해

테스트 코드 자체의 유지 보수성이 좋아야한다.

 

좋은 테스트 코드를 만들기 위해 주의해야 할 사항

1.  변수나 필드를 사용해서 기댓값 표현하지 않기   실제 값을 사용하자

(예시) 기대하는 값에 변수를 사용한 코드

// 기대하는 값에 변수 사용
@Test
void dateFromat(){
	LocalDate date = LocalDate.of(1945,8,15);
	String dateStr = formatDate(date)
	assertEquals(data.getYear() + "년 " +
		date.getMonthValue() + "월 " +
		date.getDayOfMonth() + "일 ", dateStr);
}


// 기대하는 값에 문자열 사용
...
assertEquals("1945년 8월 15일", dateStr);
...
  • 변수를 사용하면 메서드를 잘 못 사용했을 때 테스트가 깨져야 실수를 알 수 있다.
  • 문자열을 사용한 예시는 기대하는 값이 명확하다.
    • 메서드를 잘 못 사용할 일 없음
    • 테스트가 깨지면 formatDate() 메서드만 확인하면 해결

(예시) 단언과 객체 생성에 필드와 변수를 사용한 코드

private List<Integer> answers = Arrays.asList(1,2,3,4);
@Test
void arrayCheck(){
	// 어레이 저장 코드 : answers 을 사용
	// 저장 결과를 확인 : savedArray
	assertALL(
    	() -> assertEquals(answers.get(0), savedArray.get(0))
    	() -> assertEquals(answers.get(1), savedArray.get(1))
    	() -> assertEquals(answers.get(2), savedArray.get(2))
    	() -> assertEquals(answers.get(2), savedArray.get(3))
}

// 개선한 코드
@Test
void arrayCheck(){
	// 어레이 저장 코드 : Arrays.asList(1,2,3,4) 을 사용
	// 저장 결과를 확인 : savedArray
	assertALL(
    	() -> assertEquals(1, savedArray.get(0))
    	() -> assertEquals(2, savedArray.get(1))
    	() -> assertEquals(3, savedArray.get(2))
    	() -> assertEquals(4, savedArray.get(3))
}

 

 

첫 번째 테스트 코드 처럼 작성을 하면 메스드가 많은 경우

테스트를 이해하기 위해서 객체와 변수를 확인하기 위해 필드와 변수를 계속 확인 해야한다

 

개선된 코드의 경우 실제 값을 사용

가독성이 올라가고 생성된 객체와 변수를 확인하기 위해서 필드와 변수를 오갈 필요가 없다.

 

2.  두 개 이상을 검증하지 않기   검증 대상을 명확하게 구분 → 유지보수에 유리

하나의 테스트에 두 개 이상을 검증하면,

테스트에 실패 했을 때, 둘 중 무엇이 실패했는지 확인해야 한다.

→ 서로 다른 내용을 검증한다면(검증 대상이 명확하게 구분되다면),
          각 검증을 대상을 별도로 분리 → 테스트의 집중도 향상, 유지보수에 유리

3.  정확하게 일치하는 값으로 모의 객체 설정하지 않기   범용적인 값을 사용 

모의 객체는 가능하 범용적인 값을 사용

  • 한정된 값에 일치하도록 모의객체를 사용? →  약간의 코드 수정만으로도 테스트 실패
    • 이 경우 테스트 코드의 일부 값을 수정하면 모의 객체 관련 코드도 수정 필요

4.  과도하게 구현 검증하지 않기   실행 결과를 검증 

테스트 대상의 내부 구현을 검증을 하면?

  • 구현이 조금만 변경되어도 테스트가 실패한다

→ 내부 구현은 언제든지 바뀔수 있으므로 실행 결과를 검증해야한다.

(예) 테스트 대상이 "비밀번호의 강도"를 확인하는 코드면  → 실행 결과가 정확하게 약하다, 강하다를 반환하는지 검증하면 된다. 

 

5.  셋업을 이용해서 중복된 상황을 설정하지 않기   상황 구성 코드가 테스트 메서드 안에 작성

테스트 코드를 작성하다 보면 각 테스트 코드에서 동일한 상황이 필요할 때가 있다.

이경우 중복된 코드를 제거하기 위해 → @BeforeEach 메서드 ( 상황설정 메서드)를 이용해서 상황을 구성할 수 있다.

중복을 제거하고 코드 길이도 짧아져서 코드 품질이 좋아졌다고 생각할 수 있지만, 테스트 코드에서는 상황이 달라진다.

 

메서드가 문제가 생겨서 몇 달만에 테스트 코드를 다시 본 경우

  • @BeforeEach 에 설정된 상황?을 다시 확인해가면서 문제의 원인을 분석해야함 (위아래로 반복적으로 이동)
  • 모든 테스트 메서드가 동일한 상황을 공유하므로 이전에 한 변경으로 인해 테스트가 실패할 수 있다.
각 테스트 메서드는 검증을 목표로 하는 하나의 완전한 프로그램이여야 한다
→ 검증 내용을 스스로 잘 설명할 수 있어야 한다
→ 상황 구성 코드가 테스트 메서드 안에 위치해야 한다.

 

▶ 통합 테스트에서 데이터 공유 주의하기 → 

   통합 테스트 메서드는 데이터 초기화를 위한 코드를 작성하지 않아도 된다.

    (이유) 초기화를 위한 쿼리 파일을 조금만 변경해도 많은 테스트가 실패할 수 있다.

 

   통합 테스트 코드를 만들 때는 다음의 두 가지로 초기화 데이터를 나눠서 생각

     -  모든 테스트가 같은 값을 사용하는 데이터: 예) 코드값 데이터

     -  테스트 메서드에서만 피룡한 데이터 : 예) 중복 ID 검사를 위한 회원 데이터

 

▶ 통합 테스트의 상황 설정을 위한 보조 클래스 사용하기

   각 테스트 메서드에서 직접 상황을 구성하다 보면 여러 테스트 코드에서 중복되는 경우가 있다.

      이럴 때 상황 설정을 위한 클래스를 이용하면 편리하다.

public class UserGivenHelper{
	...
    public void givenUser(Stering id, String pw, String email){
    	// 코드
    }
}

@BeforeEach
void setUp() {
	given = new UserGivenHelper(jdbcTemplate);
}

@Test
void 동일ID가_이미_존재하면_익셉션() {
	given.givenUser("cbk", "pw", "cbk@cbk.com");

	// 실행, 결과 확인
	assertThrows(DupIdException.class,
            () -> register.register("cbk", "strongpw", "email@email.com")
	);
}

6. 실행 환경이 다르다고 실패하지 않기 → 프로젝트 폴더 기준 절대경로 사용

실행 환경에 따라 문제가 되는 가장 전형적인 이유는 파일 경로 → 프로젝트 폴더를 기준으로 절대경로 를 이용하자. 

 

7.  실행 시점이 다르다고 실패하지 않기 

시간 문제 : (예시) 회원의 만료여부를 확인하는 기능 

@Test
void notExpired() {
   LocalDateTime expiry = LocalDateTime.of(2024,10,15,0,0,0);
   Member m = Member.builder().expiryDate(expiry).build();
   assertFalse(m.isExpired()); // false 를 반환해야함
}
  •  2024.10.16일에 테스트하면 테스트 실패 

해결책

  • 코드에서 명시적으로 시간을 제어할 수 있는 방법을 선택
  • 시간 클래스를 작성
@Test
void notExpired() {
   LocalDateTime expiry = LocalDateTime.of(2024,10,15,0,0,0);
   Member m = Member.builder().expiryDate(expiry).build();
   assertFalse(m.passedExpiryDate(LocalDateTime.of(2024,10,14,0,0,0)));
}

명시적으로 시간을 제어하면 언제 테스트를 하던지 테스트가 성공

 

랜덤 값 문제 : (예시) 숫자 야구 게임 

문제 : 테스트를 통과시킬 수 있는 값이 매번 바뀜 → 테스트는 랜덤하게 실패

해결법 

  • 직접 랜덤 값을 생성하지 말고, 생성자를 통해 값을 받거나 랜덤 값 생성을 다른 객체에게 위임하게 바꾼다.
    • 숫자 생성을 위한 클래스를 별도로 구현  등등

8. 필요하지 않은 값은 설정하지 않기 

  • 검증할 내용에 관련없는 값들은 해당 테스트 메서드에 있을 필요가 없다.
  • 테스트할 범위에 꼭 필요한 값들만 설정하면 테스트 코드도 짧아지고 가독성도 높아진다.

▶ 단위 테스트를 위한 객체 생성 보조 클래스

  • 상황 구성을 위해 필요한 데이터가 다소 복잡한 경우 -> 테스트를 위한 객체 생성 클래스 구현하는 편이 좋다.
    • 기본 값을 설정하고 변경하고 싶은 속성만 변경하도록 설정 가능해진다.

9.  조건부로 검증하지 않기 

테스트는 성공하거나 실패해야한다. 

조건을 단언하지 않으면 그 테스트는 성공하지도 실패하지도 않은 테스트가 된다. 

→ 조건도 단언을 하도록 수정하여 실패인지 성공인지 여부를 테스트해야 한다.

10.  통합 테스트는 필요하지 않은 범위까지 연동하지 않기 

  • 통합 테스트 실행시에 전체 애플리케이션을 구동하면 필요하지 않은 객체까지 생성하게 되어 테스트 실행이 느려진다.
  • 테스트에 필요한 연동 대상만 생성하면 테스트 수행 시간이 짧아진다. 

11. 더 이상 쓸모 없는 테스트 코드 → 제거

  • 소프트웨어가 제공해야 하는 기능을 테스트하는 코드가 아니라면 삭제한다.
    • 특정 클래스의 사용법을 익히기 위해 작성한 테스트 코드 → 삭제
  • 단지 테스트 커버리지를 높이기 위한 목적으로 작성한 테스트 코드는 실제 코드 유지보수에는 아무런 도움이 되지 않으므로 삭제한다.
    • User 클래스에 있는 단순한 메서드( getId, getName, ..)  → 삭제

21 강 알고리즘과 평가 하테네 북마크의 기사 분류

 

기사 분류란?

새로 도착한 기사를 해당 기사의 내용을 기반으로 자동으로 해당 카테고리를 판정하여 분류하는 것

 

카테고리 판정을 위해 사용한 기술 : 베이지안 필터

- 나이브 베이즈에 근거한 카테고리 추정 : 문서 D 가 카테고리 C 에 속할 확률을 구하는 방식

 

이 알고리즘이 실용화되기까지 거친 작업

  • 분류 엔진을 서버화
  • 학습 데이터 정기적인 백업 구현
  • 초기 학습 데이터를 수작업으로 준비
  • 분류 엔지의 정밀도 추적을 위한 통계구조 작성
  • 웹앱 인터페이스
  • 등등

위의 과정에서 배울 점

  • 기존 방법 익혀두기 : 기본적인 알고리즘을 익혀두어서 문제를 해결하기 위함
    • Tire 나 베이지안 필터 같은 것을 몰랐으면 문서를 자동으로 분류한다는 발상도 하기 힘듬
  • 대용량 데이터에 맞서 알고리즘을 선택하고 이를 응용하는 것이 어떤 것인지 그 감각을 익힐 필요가 있음

* 대규모 데이터를 대상으로 한 알고리즘 실용화에 관련된 예

- 키워드 링크

최초 구현 방식

1. 정규표현을 컴파일하는 처리

2. 정규표현에서 패턴을 매칭하는처리

 

문제점

데이터가 늘어날 수록 키워드의 개수가 증가 -> 키워드 캐수에 비례하는 계산량이 소요

 

해결방안

Tire(트라이)를 사용한 매칭 구현으로 변경

 

Tire? 선택

트리구조의 일조인 데이터 구조

  • 탐색대상 데이터의 공통 접두사를 모아서 트리구조를 이루는게 그 특징
  • 문자열 집합을 트리구조로 해서 효율적으로 저장

 

AC 법 - Tire에 의한 매칭을 더욱 빠르게하는 방법

Aho-Corasick(AC 법)

  • Tire 에서의 패턴매칭으로 매칭이 진행되다가 도중에 실패했을 경우, 되돌아오는 길의 엣지를 다시 Tire에 추가한 데이터 구조를 사용하는 방법
  • 계산량이 사전 크기에 의존하지 않는 빠른 방법
  • 사전 내에서 패턴 매칭을 수행하는 오토마톤을 구축하고 입력 텍스트에 대해 선형 계산 시간을 실현

Regexp::List  로 변경

Perl 정규표현 라이브러리로 Tire에 의해 최적화된 정규표현으로 변환시키는 라이브러리

  • 계산량이 줄어든다.
  • 정규표현으로 사용할 수 있다.(유연성)

키워드 링크 구현, 변이 및 고찰 요약

변이: 정규표현 → AC 법 → Refexp::List

고찰

  • 심플한 구현이 주효했지만, 데이터가 커지면서 계산량 문제가 발생
  • 선택한 알고리즘의 근본적인 문제점을 해결하기 위하여 한 알고리즘 평가를 통해 계산량의 관점에서 문제를 해결

처음부터 최적의 구현을 사용하는 것이 반드시 옳지는 않다.

데이터가 대규모가 될 시기를 대비해서 본질적인 문제의 해결방법을 머릿속에 넣어두어야 한다.

탄생배경

전통적인 애플리케이션 배포방식에서 자주 마주하는 문제점 "It works on my machine":

  • 개발 환경과 운영 환경의 불일치
  • 복잡한 환경 설정과 종속성 관리의 어려움

이러한 문제를 해결하기 위해, 애플리케이션을 어디서나 동일한 환경에서 실행할 수 있는 기술의 필요성이 대두되어습니다.

그 결과, 애플리케이션을 컨테이너로 패키징하여 어디서나 동일하게 실행할 수 있게 해주는 가상화 플랫폼인 Docker 가 탄생하게 되었습니다.

Docker 를 사용하는 이유

특징  설명
컨테이너화 (Containerization) 애플리케이션과 그 종속성을 하나의 컨테이너로 묶어 일관된 실행 환경을 제공
이식성 (Portability) 도커를 지원하는 환경에서 애플리케이션을 쉽게 이식가능 →개발, 테스트, 배포과정의 단순화
격리성 (Isolation) 각 컨테이너는 자체 격리된 환경에서 실행되어서 애플리케이션 간의 충돌을 방지
효율성 (Efficiency)  호스트 시스템의 커널을 공유하여 가상머신을 사용하는 것에 비해 리소스 오버헤드가 감소,
즉, 가상머신은 각자 운영체제를 따로 실행하느라 자원을 더 많이 소모하는 반면, 도커는 운영체제를 공유해 자원 낭비를 줄여준다.
확장성(Scalability) 컨테이너는 수요에 따라 애플리케이션을 쉽게 확장하거나 축소 가능
재현성(Reproducibility) Docker 이미지는 버전 관리되고 공유될 수 있어 애플리케이션이 일관되게 배포되도록 보장

장점

  • 일관된 환경 제공: 개발부터 운영까지 동일한 환경을 유지하여 환경 불일치로 인한 문제를 최소화
  • 신속한 배포 및 롤백: 이미지 기반 배포로 빠른 배포와 롤백이 가능
  • 유연한 스케일링: 컨테이너 오케스트레이션 도구(Kubernetes 등)를 사용하여 수요에 따라 쉽게 확장하거나 축소가능
  • 리소스 효율성: 시스템 자원을 효율적으로 사용하여 비용 절감에 도움
  • 풍부한 생태계: Docker Hub 등 다양한 이미지와 커뮤니티 지원이 활발
  • CI/CD 파이프라인 통합 용이

단점

  • 보안 문제: 호스트 커널을 공유하기 때문에 가상 머신보다 보안 격리가 약할 수 있음
  • 복잡한 관리: 다수의 컨테이너 관리와 오케스트레이션이 복잡할 수 있음.
  • 윈도우 지원 제한: 초기에는 리눅스 중심으로 개발되어 윈도우 환경에서 제약이 있을 수 있습니다.
  • 디버깅 어려움: 컨테이너 내부의 문제를 진단하고 디버깅하는 데 어려움이 있을 수 있습니다.

Common Use Cases

  • Web Applications: Deploying and scaling web applications.
  • Microservices Architecture: Building and managing distributed applications composed of small, independent services.
  • Data Science: Running data science pipelines and machine learning models.
  • Continuous Integration and Continuous Delivery (CI/CD): Automating the testing and deployment of applications.

19 강 알고리즘과 평가

알고리즘이란?

 

Algorithm 은 어떤 값 또는 값의 집합을 입력으로 하고 어떤 값 또는 값의 집합을 출력으로 하는,
명확하게 정의된(well-defined) 계산 절차다.

 

넓은 의미의 알고리즘

  • 특정 문제를 해결하기 위한 일반적인 절차나 규칙의 모음
    (예시: 도메인 로직의 처리의 흐름, DB에서 레코드를 얻ㅇ어서 적절하게 처리한 후 결과를 출력하는 것)

좁은 의미의 알고리즘

  • 명확하게 정의된 계산문제에 대해 정의된 계산 절차를 수행하는 것 (주로 책에서 다루는 내용)

 

알고리즘을 배우는 의의

알고리즘은 엔지니어에게 공통언어 → 커뮤니케이션을 위해서 알아둘 필요가 있음

유한한 컴퓨터 자원을 효과적으로 활용

알고리즘을 이용하여 새로운 문제에 대처가능

알고리즘의 평가

아래로 갈수록 계산량이 많아진다. 

대규모 데이터를 대상으로 할 경우 O(n log n) 까지가 실용적이다.

 

계산량 개념 적용

  • 시간 계산량(실행시간, 단계 횟수)
  • 공간계산량 (메모리 사용량)

계산량과 상수항

계산량의 Order 표기에서는 "상수항" 을 무시한다.

 

상수항?

함수 호출, 함수로 부처 값을 반환하기 위한 처리, 일차변수를 확하는 처리, if 문으로 분기시키는 처리 등이 이에 해당

→ 실질적으로는  복잡해질 수록 상수항이 계산량에 영향을 미친다. 경우에 따라서 상수항을 줄이는 최적화도 필요하다.

 

최적화시 유의할 점

  • 처음부터 상수항을 줄이는 최적화를 하지 않는다.
  • 현재 프로그램에 무엇이 문제인지를 정확하게 파악 후 최적화 실행

개선시 문제 해결을 위해 규명해야할 사항

  • 알고리즘을 교체해서 개선할 것인가
  • 상수항을 줄여서 개선할 것인가
  • 물리적으로 리소스가 부족하므로 하드웨어를 교환해서 성능을 개선 할것 인가

알고리즘의 실제 활용 → 예측과 측정이 중요

* 단순한 알고리즘이 더 나은 경우도 있다.

* 고전적인 알고리즘이 좋은 경우도 있다.

 

써드파티 소스를 잘 활용하자

정평이 난 알고리즘은 제 3자 이용하기 쉽도록 이미 구현된 소스가 공개되어 있는 경우가 많다.

 

하지만, 이러한 라이브러리를 바로 사용할 수가 없을 수도 있다.

해당 알고리즘이 어떻게 구현이 되어있는지 알아야 한다.

→ 우리가 원하는 사양으로 변경가능

→ 불필요한 구현을 줄여 성능, 유지보수, 테스트, 유연성, 자원 사용 등의 측면에서 다양한 문제를 방지가능

+ Recent posts