목차

기능 명세

설계 과정을 지원하는 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일처럼 만원을 납부한 일자가 다음 달 말일 일자보다크면 이때 만료일은 다음 달 말일이라고 생각면 될까요?

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

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

 

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

목차

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

테스트코드와 유지보수

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

실패한 테스트를 통과시키기 위해 많은 노력이 필요하면 점점 테스트 코드에서 멀지고 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.

+ Recent posts