들어가며

이글은 로버트마틴의 클린코드 9장 Test 단원의 내용을 참고하여 작성하였습니다

오늘은 9장의 일부인 'FIRST 원칙'에 관하여 설명드릴까 합니다 😚


테스트 5가지 원칙 (F, I, R, S, T)

F (Fast 빠르게)
테스트는 빠르게 수행되어야 합니다

각 테스트는 빠르게 수행되어야 합니다.

빠르다의 속도는 상대적일 수 있지만 적어도 통합테스트를 돌리는데 있어서 부담감을 느끼지 않을 정도여야 한다고 생각합니다.

 

우리는 커밋을 하기전에 통합적인 테스트를 수행해봅니다.

하지만, 테스트가 많아지고 각 테스트 마다 소요시간이 오래걸린다면 우리는 배포하는데 어려움을 겪게 됩니다.

 

자바 스프링을 예로 들면 @SpringBootTest와 같이 통합적인 빈을 load하는 것을 지양해야 합니다 (테스트에 필요한 @Bean만 load)

 

I (Independat 독립적으로)
테스트는 독립적으로 수행되어야 합니다

각 테스트는 독립적으로 수행되어야 하며,

A테스트의 결과가 B테스트의 결과의 영향을 미쳐서는 안됩니다

 

다만, 중요한 정책이나 돈이나 자산의 테스트가 이루어져야 하는 케이스라면

@Order 같은 어노테이션을 사용해서 순서를 고정시켜 테스트를 진행하기도 합니다

 

R (Repeatable 반복가능하게)
테스트는 반복적으로 수행해도 결과가 같아야 합니다

테스트는 수정이 발생하지 않는한 매번 결과가 같아야 합니다.

1번 수행하든, 10번 수행하든 성공, 실패의 결과가 매번 동일해야 합니다

 

영화의 티켓값은 성인 기본요금은 10,000 원입니다. 4명의 티켓값은 40,000원 입니다.
티켓값을 계산하는 n번 수행할때마다 티켓값 총합은 매번 40,000 원이 나와야 합니다!!

 

S (Self-Validating 자가검증하는)
테스트는 자체적으로 검증이 가능해야 합니다

각 테스트의 결과는 '성공 또는 실패'여야 합니다

테스트가 스스로 성공과 실패를 가늠하지 않는다면 판단은 주관적이 되며
지루한 수작업 평가가 필요하게 됩니다

 

아래와 같이 결과값을 수동으로 확인하면 안됩니다!!

System.out.println(num);
logger.info(num)

 

Java 진영에서는 JUnit을 이용하면 아래와 같이 결과값을 얻을 수 있습니다

assertThat(num).isEqualTo(expected);

 

T (Timely 적시에)
테스트는 적시에 작성해야 합니다

 단위테스트는 테스트 하려는 실제 코드를 구현하기 직전에 구현해야 합니다

코드를 구현한 다음에 테스트 코드를 만들면 실제 코드가 테스트하기 어렵다는 사실을 발견할지도 모릅니다

이부분의 내용은 테스트주도 개발 (TDD, Test-Driven-Development)과 밀접한 관련이 있습니다

 

TDD 법칙 세가지

  • 실패하는 단위 테스트를 작성할 때까지 실제 코드를 작성하지 않는다
  • 컴파일은 실패하지 않으면서 실행이 실패하는 정도로만 단위테스트를 작성한다
  • 현재 실패하는 테스트를 통과할 정도로만 실제 코드를 작성한다

 

오늘은 테스트의 FIRST 원칙에 대해서 알아보았습니다.

FIRST 원칙과 더불어 TDD의 대한 관심도 많이 증가하고 있는데요,

저는 개인적으로는 TDD보다 우선되어야 할 것이 올바른 테스트 코드 작성 습관이라고 생각합니다.

 

TDD는 사실 개발자에게 높은 수준의 역량을 요구하고 있기에, 

TDD에 앞서 올바른 테스트 코드를 작성하는 것이 중요하다고 생각합니다!! 👻


참조

* 클린코드 - 로버트마틴

'Concept' 카테고리의 다른 글

[test] 테스트코드 작성을 위한 FIRST 원칙 | 클린코드  (0) 2021.10.29
블로그 이미지

yhmane

댓글을 달아 주세요

들어가며

시간 복잡도(Time Complexity)란 특정 알고리즘이 문제를 해결하는데 걸리는 시간을 의미하고,
통상적으로 최악의 경우(빅오, ‘O’)를 구해서 표기합니다

 

이전 포스팅 에서 시간복잡도에 대해 알아보았는데요,
이번 포스팅에서는 순차탐색과 이진탐색에 대하여 알아보고
간단한 leetcode (릿코드) 예제와 함께 시간복잡도를 계산해 보도록 하겠습니다


Leetcode 문제 이해하기

Search Insert Position - LeetCode ‘EASY’ 난이도인 문제입니다

이진탐색을 공부하기에 좋은 예제라 생각하여 가져왔습니다 👀

  • 배열의 인덱스는 0부터 시작합니다
  • 오름차순으로 정렬된 array와, 타겟이 Input으로 제공됩니다
  • Output으로 타겟이 위치할 인덱스 위치를 return 하여 줍니다

1번 예제에서는 목표값 '5'가 배열에서 2에 위치한 것을 알 수 있습니다

4번 예제에서는 목표값 '0'이 배열에서 0번째 위치해야 하는 것을 알 수 있습니다


순차탐색 풀이 & 시간복잡도

순차 탐색의 경우, 처음부터 비교하여 비교가 끝날때까지 차례로 훑어보게 됩니다

  • Example1, 목표값 5 에서는 탐색 3번 [1, 3, 5]
  • Example2, 목표값 2 에서는 탐색 2번 [1, 3]
  • Example3, 목표값 7 에서는 탐색 4번 [1, 3, 5, 6]
  • Example4, 목표값 0 에서는 탐색 1번 [1]
  • Example5, 목표값 0 에서는 탐색 1번 [1]

최악의 경우 마지막 끝까지 4번 훑어보게 됩니다.

public int searchInsert(int[] nums, int target) {
    int length = nums.length;
    for (int i = 0; i < length; i++) {
        if (nums[i] == target) {
            return i;
        }

        if (target < nums[i]) {
            return i;
        }
    }
    return length;
}

즉, 최악의 경우인 n번까지 훑어보게 됨으로 시간복잡도는 O(n)을 가지게 됩니다.


이진탐색 풀이 & 시간복잡도

이진탐색의 경우 업&앤 다운으로 숫자맞추기를 한다고 보면 됩니다.

“1부터 100까지 숫자가 정렬되어 있을때 내가 생각하고 있는 숫자를 맞춰봐” (답은 43)
50 땡! -> 다운
25 땡 -> 업
36 땡 -> 업
43 정답

만약 순차탐색으로 숫자를 맞추게 된다면 1부터 43까지 차례로 확인을 하겠죠?

그러나 이진탐색은 위와 같이 4번의 질의로 정답을 맞추었습니다.

 

즉, 이진탐색은 순차탐색보다 효율적으로 숫자를 탐색하고 있습니다.

마찬가지로 leetcode 문제를 살펴보겠습니다

 

  • Example1, 목표값 5 에서는 탐색 1번 [5]
  • Example2, 목표값 2 에서는 탐색 2번 [3, 1]
  • Example3, 목표값 7 에서는 탐색 3번 [3, 5, 6]
  • Example4, 목표값 0 에서는 탐색 2번  [3, 1]
  • Example5, 목표값 0 에서는 탐색 1번  [1] 
public static int searchInsert(int[] nums, int target) {
    int first = 0;
    int last = nums.length - 1;

    while (first <= last) {
        int mid = (first + last) / 2;
        if (nums[mid] == target) {
            return mid;
        } else if (nums[mid] < target) {
            first = mid + 1;
        } else {
            last = mid - 1;
        }
    }
    return first;
}

이진탐색에서는 중앙값과 목표값을 비교하여 인덱스의 위치를 계속 조절하고 있습니다!!

이제 이진탐색의 시간복잡도를 구해보도록 하겠습니다.

  • 먼저 숫자 N이 주어집니다
  • 첫 탐색을 하게 되면 N/2 만큼 남게됩니다
  • 두번째 탐색을 하게 되면 1/2 * N/2 만큼 남게 됩니다
  • 세번째 탐색을 하게 되면 1/2 * 1/2 * N/2 만큼 남게 됩니다
  • k번째 탐색을 하게 된다면 (1/2)^k * N 만큼 남게됩니다
  • ‘(1/2)^k * N = 1’ 이 성립하기에 양변에 2^k을 곱하여 줍니다
  • N = 2^k가 되어 k 값을 구하게 되면 k = log2N 이 됩니다

시간복잡도는 상수를 제거하여 표현하기에 이진탐색의 시간복잡도는 logN으로 표현할 수 있습니다

주의, 이진탐색의 경우 숫자가 정렬되어 있어야 합니다 (오름차순, 내림차순)


출처

Search Insert Position - LeetCode
이진 탐색과 시간 복잡도 분석 (Binary Search and its Time Complexity Analysis)

블로그 이미지

yhmane

댓글을 달아 주세요

스레드 Pool

병렬 작업 처리가 많아지면 스레드 갯수가 증가하고 그에 따른 스레드 생성과 스케줄링으로 인해 메모리 사용량이 늘어납니다.
갑작스런 병렬 작업의 폭증으로 인한 스레드의 폭증을 막으려면 스레드풀을 사용해야 합니다.

스레드풀은 작업 처리에 사용되는 스레드를 제한된 갯수만큼 정해 놓고 큐에 들어오는 작업들을 하나씩 스레드가 맡아 처리합니다. 작업 처리가 끝난 스레드는 다시 작업 큐에서 새로운 작업을 가져와 처리합니다. 그렇기 때문에 작업 처리 요청이 폭증 되어도 스레드의 전체 개수가 늘어나지 않으므로 애플리케이션의 성능이 급격히 저하되지 않습니다

자바는 스레드풀을 생성하고 사용할 수 있도록 ExecutorService 인터페이스와 Executors 클래스를 제공하고 있습니다


스레드풀 생성 및 종료

스레드풀 생성

스레드풀을 생성하는 방법은 크게 2가지가 있습니다.

  1. newCachedThreadPool()
    • 초기스레드0, 코어스레드0, 최대스레드 Integer.MAX_VALUE
  2. newFixThreadPool(int nThread)
    • 초기스레드0, 코어스레드0, 최대스레드 nThreads
  • 초기 스레드 수 : ExecutorService 객체가 생성될 때 기본적으로 생성되는 스레드 수
  • 코어 스레드 수 : 스레드가 늘어난 후 사용되지 않은 스레드를 스레드 풀에서 제거할 때 최소한으로 유지해야할 수
  • 최대 스레드 수 : 스레드풀에서 관리하는 최대 스레드 수
newCachedThreadPool()

스레드 개수보다 작업 개수가 많아지면 새로운 스레드를 생성하여 작업을 처리합니다
만약 스레드가 60초동안 아무일을 하지않으면 스레드를 종료시키고 스레드풀에서 제거합니다

ExecutorService executorService = Executors.newCachedThreadPool();
newFixedThreadPool(int nThreads)

스레드 개수보다 작업 개수가 많으면 스레드를 새로 생성하여 작업을 처리합니다
newCachedThreadPool과 다른 점은 일을 하지 않아도 스레드를 제거하지 않습니다

ExecutorService executorService = Executors. newFixThreadPool();

newCachedThreadPool(),newFixedThreadPool() 메서드를 사용하지 않고
직접 스레드 개수들을 설정하고 싶다면 직접 ThreadPoolExecutor 객체를 생성하면 됩니다.

ExecutorService threadPool = new ThreadPoolExecutor(
    3,                                     // 코어 스레드 개수
    100,                                // 최대 스레드 개수
    120L,                                // 놀고 있는 시간
    TimeUnit.SECONDS,                     // 놀고 있는 시간 단위
    new SynchronousQueue<Runnable>()     // 작업큐
);

 


스레드풀 종료

스레드풀의 스레드는 기본적으로 데몬 스레드가 아니기 때문에 main 스레드가 종료되더라도 작업을 처리하기 위해 계속 실행 상태로 남아 있습니다. 중요한 부분인데 main() 메서드가 실행이 끝나도 애플리케이션 프로세는 종료되지 않습니다. 따라서 애플리케이션 (ex, Spring Batch)을 종료하려면 스레드풀을 종료시켜 스레드들이 종료상태가 되도록 처리해야 합니다

executorService.shutDown();  
// 또는
executorService.shutDownNow();

shutdown()
작업큐에 남아있는 작업까지 모두 마무리 후 종료
shoutdownNow()
작업큐 작업 잔량 상관없이 강제 종료


스레드 작업 생성과 처리 요청

작업 생성

스레드 하나의 작업은 Runnable 또는 Callable 구현 클래스로 표현합니다.
Runnable과 Callable의 차이는 작업 처리 완료후 리턴값이 있느냐 없느냐 입니다.

Ruunable
Runnable task = new Runnable() {
    @Override
    public void run() {
        // 스레드가 처리할 작업 내용
    }
}
Callable
Callalbe<T> task = new Callable<T>() {
    @Override
    public T call throws Exception() {
        // 스레드가 처리할 작업 내용
        return T;
    }
}

작업 처리 요청

public class ExecuteExample {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(2);

        for(int i = 0; i < 10; i++){
            Runnable runnable = new Runnable() {
                @Override
                public void run() {
                    ThreadPoolExecutor threadPoolExecutor = (ThreadPoolExecutor) executorService;
                    int poolSize = threadPoolExecutor.getPoolSize();
                    String threadName = Thread.currentThread().getName();

                    System.out.println("[총 스레드 개수:" + poolSize + "] 작업 스레드 이름: "+threadName);
                    int value = Integer.parseInt("윤호");
                }
            };

            //스레드풀에게 작업 처리 요청
            executorService.execute(runnable);
            //executorService.submit(runnable);

            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                 e.printStackTrace();
            }        
        }     
        executorService.shutdown();
    }
}
execute()로 실행했을 경우


execute로 실행했을 때에는 작업 처리 도중 예외가 발생하면 해당 스레드는 제거 되고 새 스레드가 계속 생성됩니다.

submit()으로 실행 했을 경우


submit의 경우 예외가 발생하더라도 스레드가 종료되지 않고 계속 재사용이 되어 다른 작업을 처리하는 것을 볼 수 있습니다

execute와 submit의 차이는 두가지가 있습니다
하나는 execute()는 작첩 처리 결과를 받지 못하고, submit()((은 작업 처리 결과를 받을 수 있도록 Future를 리턴합니다.
두번째로는 위의 예에서 알아본 예외 발생의 경우입니다. execute()는 스레드를 종료하고 스레드풀에서 제거됩니다. submit()의 경우에는 예외가 발생하더라도 스레드는 종료되지 않고 다음 작업을 위해 재사용됩니다

그렇기 때문에 가급적 스레드 생성의 오버헤드를 줄이기 위해서 submit()을 사용하는 것이 좋습니다


참조

  • ‘이것이 자바다’ - 신용권님 저
블로그 이미지

yhmane

댓글을 달아 주세요

이전 포스팅에선 다음과 같이 알아 봤는데요

  • Thread 생성
  • Thread 동기화 블록

이번에는 Thread가 제공하는 몇가지 메서드를 확인해보고
Thread 상태를 제어하는 방법에 대해 알아보도록 하겠습니다


Thread 상태

스레드를 생성하고 start() 메서드를 호출하면 곧바로 스레드가 실행되는 것처럼 보이지만, 사실은 '실행대기' 상태가 됩니다.
실행대기 상태란 아직 스케줄링이 되지 않아 실행을 기다리고 있는 상태를 말합니다.
실행대기 상태에 있는 스레드 중에서 스케줄링으로 선택된 스레드가 비로서 CPU를 점유하고 run() 메서드를 실행하게 됩니다.
이때를 실행 상태라고 합니다.

아래의 사진으로 스레드의 대략적인 상태를 살펴 보겠습니다

실행 상태의 스레드는 다시 실행대기 상태로 돌아갈 수 있고
실행, 실행대기 상태를 번갈아가며 자신의 run() 메서드를 수행합니다.
run() 메서드가 종료되면 스레드의 실행은 멈추게 됩니다.
이 상태를 종료 상태라고 합니다

경우에 따라서 ‘실행상태’에서 ‘일시정지’ 상태로 가기도 하는데,
일시정지 상태에서는 스레드가 실행할 수 없는 상태입니다.
일시정지 상태로는 WAITING, TIMED_WAITING, BLOCKED가 있습니다.
스레드가 다시 실행 상태로 가기 위해서는 일시정지 상태에서 실행대기 상태가 되어야 합니다.

상태 Enum 상수 설명
객체 생성 NEW 레드 객체 생성, 아직 start() 메서드 호출되지 전의 상태
실행대기 RUNNABLE 실행 상태로 언제든지 갈 수 있는 상태
일시정지 WAITING 다른 스레드가 통지(notify) 할 때까지 기다리는 상태
일시정지 TIMED_WAITING 주어진 시간 동안 기다리는 상태
일시정지 BLOCKED 사용하고자 하는 객체의 락이 풀릴 때까지 기다리는 상태
종료 TERMINATED 실행을 마친 상태

스레드의 상태 제어

사용자는 미디어 플레이어에서 동영상을 보다가 일시 정지 시킬수도 있고, 종료시킬수도 있습니다
정지는 다시 동영상을 보겠다는 의미로 스레드를 일시정지 상태로 만들어야 합니다
종료는 더 이상 동영상을 보지 않겠다는 의미이므르 미디어 플레이어는 스레드를 종료 상태로 만들어야 합니다
이와 같이 스레드의 상태를 변경하는 것은 스레드 상태 제어라고 합니다

아래는 스레드 상태를 제어하는 메서드입니다. 하나씩 메서드를 알아보도록 하겠습니다

일시정지 (sleep)

sleep() - 주어진 시간동안 일시 정지 상태로 만들고, 주어진 시간이 지나면 자동적으로 실행대기 상태가 됩니다
try {
    Thread.sleep(3000);
} catch (InterruptedException e) {
    e.printStackTrace();
} 

Thread.sleep(3000) 메서드를 통해 3초간 ‘일시정지’ 상태가 되고 3초가 지나면 다시 ‘실행대기’ 상태로 돌아오게 됩니다.

실행 양보 (yield)

yield() - 실행중에 우선순위가 높은 또는 동일한 다른 스레드에게 실행을 양보하고 실행 대기 상태가 됩니다
if (work) {
    System.out.println(getName() + " 스레드 작업 내용");
} else {
    Thread.yield();
}

yield() 메서드 호출시 해당 Thread는 우선 순위가 높거나 동일한 Thread에게 실행을 양보하고 실행 대기 상태로 전환됩니다

다른 스레드의 종료를 기다림 (join)

join() -메서드를 호출한 스레드는 일시 정지 상태가 됩니다. 실행 대기 상태로 가려면 join() 메서드를 멤버로 가지는 스레드가 종료되거나, 매개값으로 주어진 시간이 지나야 합니다. 이부분은 예제를 보도록 하겠습니다
SumThread
public class SumThread extends Thread {
    private long sum;
    public long getSum() {
        return sum;
    }

    public void run() {
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
    }
}
JoinExample
public class JoinExample {
    public static void main(String[] args) {
        SumThread sumThread = new SumThread();
        sumThread.start();

        try {
            sumThread.join();
        } catch (Exception e) {
        }

        System.out.println("합 : " + sumThread.getSum());
    }
}

sumthread.join()을 걸어 두었기 때문에 메인 Thread는 합연산이 끝날때까지 기다리게 됩니다. 따라서, 결과값은 5050이 나오게 됩니다

여기서 의문이 들 수 있습니다. Thread.sleep(3000)으로도 충분히 가능한데 무엇이 차이인가? sleep은 일정 시간동안만 실행대기 상태로 만들어줍니다. join의 경우 thread의 작업을 처리할때까지 묶어 둘 수 있기 때문에 멀티스레드 환경에서 동기화를 맞춰야 할 경우 유용하게 사용할 수 있습니다.

스레드간 협업 (wait, notify, notifyAll)

notify, notifyAll - 동기화 블록 내에서 wat 메서드에 의해 일시 정지 상태에 있는 스레드를 실행 대기 상태로 만들어 줍니다
wait - 동기화 블록 내에서 스리드를 일시 정지 상태로 만듭니다. 매개값으로 시간이 주어지면 일정 시간이 지난후 자동으로 실행대기 상태가 되고, 시간이 주어지지 않으면 notify, notifyAll 메서드에 의해 실행 대기 상태로 갈 수 있습니다

스레드 종료 (stop, interrupt)

Thread는 자신의 run() 메서드가 모두 실행되면 자동으로 종료됩니다. 경우에 따라서 Thread를 즉시 종료할 필요가 있습니다

stop - 갑자기 종료하면 스레드가 사용중이던 자원들이 불안전한 상태로 남겨지기 때문에 deprecated 되었습니다
interrupt - 일시 정지 상태의 스레드에서 InterruptedException 예외를 발생시켜, 예외 처리 코드(catch)에서 실행 대기 상태로 가거나 종료 상태로 갈 수 있도록 합니다

참조

  • '이것이 자바다' - 신용권님 저
블로그 이미지

yhmane

댓글을 달아 주세요