자바를 이용하여 개발하는 개발자라면 누구나 자바 바이트코드가 JRE 위에서 동작한다는 사실을 잘 알고 있습니다. 이 JRE에서 가장 중요한 요소는 자바 바이트코드를 해석하고 실행하는 JVM(Java Virtual Machine)입니다. JRE는 자바 API와 JVM으로 구성되며, JVM의 역할은 자바 애플리케이션을 클래스 로더(Class Loader)를 통해 읽어 들여서 자바 API와 함께 실행하는 것입니다.

 

JVM(Java Virtual Machine)

JVM은 운영체제 위에서 동작하는 프로세스로 자바 코드를 컴파일해서 얻은 바이트 코드를 해당 운영체제가 이해할 수 있는 기계어로 바꿔 실행시켜주는 역할을 한다. (java 언어와 직접적인 연관이 있는 것이 아니라, class 파일만 있으면 실행해 준다. ex. 코틀린 등)

JVM에 대해 알아보기 전에 먼저 Java와 프로그램 실행 과정에 대해 알아보자.

Java의 가장 큰 특징 중 하나는 하드웨어/OS 에 상관없이 컴파일된 바이트코드가 플랫폼 독립적이라는 점이다.

(이 자바 바이트코드가 자바 코드를 배포하는 가장 작은 단위이다.)

그 이유는 다른 언어들과 다르게 자바는 OS 위에 JVM이 돌아가고, JVM이 컴파일된 코드(바이트코드)를 실행시켜주기 때문이다.

(참고로 JVM은 H/W와 OS 위에서 실행되기 때문에 JVM 자체는 플랫폼에 종속적 즉, 플랫폼에 따라 호환되는 JVM을 실행시켜줘야함)

 

좀 더 자세히 소스코드 실행 과정을 보자

1. .java 소스 코드 파일을 작성

2. 코드를 실행하게 되면 Java 컴파일러를 호출한다. 컴파일러는 코드에서 구문 오류 및 기타 컴파일 타임 오류를 확인하고, 오류가없는 경우에는 바이트 코드라는 중간 코드 .class 파일로 변환한다.

  • 바이트 코드는 플랫폼에 독립적이다. 또한, 바이트 코드는 중간 코드이므로 user/HW/OS 계층이 아닌 JVM에서만 이해할 수 있다.

3. 바이트 코드는 class loader (JVM 내부의 또 다른 내장 프로그램)에 의해 JVM으로 로드 된다.

  • 클래스로더는 동적로딩(Dynamic Loading)을 통해 필요한 클래스들을 로딩 및 링크하여 런타임 데이터 영역(Runtime Data area), 즉 JVM의 메모리에 올린다.

4. 바이트 코드 검증 도구(JVM 내부에 내장 된 프로그램)가 바이트 코드의 무결성을 확인하고 문제가 발견되지 않으면 인터프리터에 전달

  • JIT 컴파일러의 경우 기계 코드로 변환하기 위해 바이트 코드가 인터프리터에 전달되기 전, 전체 코드를 스캔하여 최적화 할 수 있는지 확인 (중복 제거 등)
  • JIT 컴파일러는 코드에 필요한 패키지만 포함 하거나, 코드 최적화, 중복 코드 제거 등과 같은 일을 하며 전체적으로 프로세스를 매우 빠르고 효율적으로 만든다.
  • JIT 컴파일러는 종류 마다 다르게 작동하며, optional 하다. (매번 호출되는 것은 아님)

5. JVM 내부의 인터프리터는 바이트 코드의 각 행을 실행 가능한 기계 코드로 변환하고, 이를 실행할 CPU와 같은 OS / 하드웨어에 전달

  • 실행엔진(Execution Engine)은 JVM메모리에 올라온 바이트 코드들을 명령어 단위로 하나씩 가져와서 실행

 

 

JVM 내부 구조에 대해 조금 더 자세히 살펴보면 아래와 같다.

JVM은 크게 Class Loader,GC, Runtime Data Area, Excute engine 세가지로 나뉜다. (여기에 GC까지 4가지로 보기도 한다)

위에서도 볼 수 있듯이, 클래스 로더(Class Loader)가 컴파일된 자바 바이트코드를 런타임 데이터 영역(Runtime Data Areas)에 로드하고, 실행 엔진(Execution Engine)이 자바 바이트코드를 실행한다.

 

1) 클래스 로더

자바는 동적 로드, 즉 컴파일타임이 아니라 런타임에 클래스를 처음으로 참조할 때 해당 클래스를 로드하고 링크하는 특징이 있다. 이 동적 로드를 담당하는 부분이 JVM의 클래스 로더이다. 아래는 클래스 로더의 특징이다.

  • 계층 구조: 클래스 로더끼리 부모-자식 관계를 이루어 계층 구조로 생성된다. 최상위 클래스 로더는 부트스트랩 클래스 로더(Bootstrap Class Loader)이다.
  • 위임 모델: 계층 구조를 바탕으로 클래스 로더끼리 로드를 위임하는 구조로 동작한다. 클래스를 로드할 때 먼저 상위 클래스 로더를 확인하여 상위 클래스 로더에 있다면 해당 클래스를 사용하고, 없다면 로드를 요청받은 클래스 로더가 클래스를 로드한다.
  • 가시성(visibility) 제한: 하위 클래스 로더는 상위 클래스 로더의 클래스를 찾을 수 있지만, 상위 클래스 로더는 하위 클래스 로더의 클래스를 찾을 수 없다.
  • 언로드 불가: 클래스 로더는 클래스를 로드할 수는 있지만 언로드할 수는 없다. 언로드 대신, 현재 클래스 로더를 삭제하고 아예 새로운 클래스 로더를 생성하는 방법을 사용할 수 있다.

각 클래스 로더는 로드된 클래스들을 보관하는 네임스페이스(namespace)를 갖는다. 클래스를 로드할 때 이미 로드된 클래스인지 확인하기 위해서 네임스페이스에 보관된 FQCN(Fully Qualified Class Name)을 기준으로 클래스를 찾는다. 비록 FQCN이 같더라도 네임스페이스가 다르면, 즉 다른 클래스 로더가 로드한 클래스이면 다른 클래스로 간주된다.

클래스 로더 위임 모델

 

클래스 로더가 클래스 로드를 요청받으면, 클래스 로더 캐시, 상위 클래스 로더, 자기 자신의 순서로 해당 클래스가 있는지 확인한다. 즉, 이전에 로드된 클래스인지 클래스 로더 캐시를 확인하고, 없으면 상위 클래스 로더를 거슬러 올라가며 확인한다. 부트스트랩 클래스 로더까지 확인해도 없으면 요청받은 클래스 로더가 파일 시스템에서 해당 클래스를 찾는다.

  • 부트스트랩 클래스 로더: JVM을 기동할 때 생성되며, Object 클래스들을 비롯하여 자바 API들을 로드한다. 다른 클래스 로더와 달리 자바가 아니라 네이티브 코드로 구현되어 있다.
  • 익스텐션 클래스 로더(Extension Class Loader): 기본 자바 API를 제외한 확장 클래스들을 로드한다. 다양한 보안 확장 기능 등을 여기에서 로드하게 된다.
  • 시스템 클래스 로더(System Class Loader): 부트스트랩 클래스 로더와 익스텐션 클래스 로더가 JVM 자체의 구성 요소들을 로드하는 것이라 한다면, 시스템 클래스 로더는 애플리케이션의 클래스들을 로드한다고 할 수 있다. 사용자가 지정한 $CLASSPATH 내의 클래스들을 로드한다.
  • 사용자 정의 클래스 로더(User-Defined Class Loader): 애플리케이션 사용자가 직접 코드 상에서 생성해서 사용하는 클래스 로더이다.

웹 애플리케이션 서버(WAS)와 같은 프레임워크는 웹 애플리케이션들, 엔터프라이즈 애플리케이션들이 서로 독립적으로 동작하게 하기 위해 사용자 정의 클래스 로더를 사용한다. 즉, 클래스 로더의 위임 모델을 통해 애플리케이션의 독립성을 보장하는 것이다. 이와 같은 WAS의 클래스 로더 구조는 WAS 벤더마다 조금씩 다른 형태의 계층 구조를 사용하고 있다.

클래스 로더가 아직 로드되지 않은 클래스를 찾으면, 다음 그림과 같은 과정을 거쳐 클래스를 로드하고 링크하고 초기화한다.

 

클래스 로드 매커니즘

  • 로드: 클래스를 파일에서 가져와서 JVM의 메모리에 로드한다.
  • 검증(Verifying): 읽어 들인 클래스가 자바 언어 명세(Java Language Specification) 및 JVM 명세에 명시된 대로 잘 구성되어 있는지 검사한다. 클래스 로드의 전 과정 중에서 가장 까다로운 검사를 수행하는 과정으로서 가장 복잡하고 시간이 많이 걸린다. JVM TCK의 테스트 케이스 중에서 가장 많은 부분이 잘못된 클래스를 로드하여 정상적으로 검증 오류를 발생시키는지 테스트하는 부분이다.
  • 준비(Preparing): 클래스가 필요로 하는 메모리를 할당하고, 클래스에서 정의된 필드, 메서드, 인터페이스들을 나타내는 데이터 구조를 준비한다.
  • 분석(Resolving): 클래스의 상수 풀 내 모든 심볼릭 레퍼런스를 다이렉트 레퍼런스로 변경한다.
  • 초기화: 클래스 변수들을 적절한 값으로 초기화한다. 즉, static initializer들을 수행하고, static 필드들을 설정된 값으로 초기화한다.

 

2) 런타임 데이터 영역 (=메모리)

런타임 데이터 영역

런타임 데이터 영역은 JVM이라는 프로그램이 운영체제 위에서 실행되면서 할당받는 메모리 영역이다. 런타임 데이터 영역은 6개의 영역으로 나눌 수 있다. 이중 PC 레지스터(PC Register), JVM 스택(JVM Stack), 네이티브 메서드 스택(Native Method Stack)은 스레드마다 하나씩 생성되며 힙(Heap), 메서드 영역(Method Area), 런타임 상수 풀(Runtime Constant Pool)은 모든 스레드가 공유해서 사용한다.

  • PC 레지스터: PC(Program Counter) 레지스터는 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성된다. PC 레지스터는 현재 수행 중인 JVM 명령의 주소를 갖는다.

JVM 스택

  • JVM 스택: JVM 스택은 각 스레드마다 하나씩 존재하며 스레드가 시작될 때 생성된다. 스택 프레임(Stack Frame)이라는 구조체를 저장하는 스택으로, JVM은 오직 JVM 스택에 스택 프레임을 추가하고(push) 제거하는(pop) 동작만 수행한다. 예외 발생 시 printStackTrace() 등의 메서드로 보여주는 Stack Trace의 각 라인은 하나의 스택 프레임을 표현한다.
    • 스택 프레임: JVM 내에서 메서드가 수행될 때마다 하나의 스택 프레임이 생성되어 해당 스레드의 JVM 스택에 추가되고 메서드가 종료되면 스택 프레임이 제거된다. 각 스택 프레임은 지역 변수 배열(Local Variable Array), 피연산자 스택(Operand Stack), 현재 실행 중인 메서드가 속한 클래스의 런타임 상수 풀에 대한 레퍼런스를 갖는다. 지역 변수 배열, 피연산자 스택의 크기는 컴파일 시에 결정되기 때문에 스택 프레임의 크기도 메서드에 따라 크기가 고정된다.
    • 지역 변수 배열: 0부터 시작하는 인덱스를 가진 배열이다. 0은 메서드가 속한 클래스 인스턴스의 this 레퍼런스이고, 1부터는 메서드에 전달된 파라미터들이 저장되며, 메서드 파라미터 이후에는 메서드의 지역 변수들이 저장된다.
    • 피연산자 스택: 메서드의 실제 작업 공간이다. 각 메서드는 피연산자 스택과 지역 변수 배열 사이에서 데이터를 교환하고, 다른 메서드 호출 결과를 추가하거나(push) 꺼낸다(pop). 피연산자 스택 공간이 얼마나 필요한지는 컴파일할 때 결정할 수 있으므로, 피연산자 스택의 크기도 컴파일 시에 결정된다.
    • 런타임 상수 풀
  • 네이티브 메서드 스택: 자바 외의 언어로 작성된 네이티브 코드를 위한 스택이다. 즉, JNI(Java Native Interface)를 통해 호출하는 C/C++ 등의 코드를 수행하기 위한 스택으로, 언어에 맞게 C 스택이나 C++ 스택이 생성된다.
  • 메서드 영역: 메서드 영역은 모든 스레드가 공유하는 영역으로 JVM이 시작될 때 생성된다. JVM이 읽어 들인 각각의 클래스와 인터페이스에 대한 런타임 상수 풀, 필드와 메서드 정보, Static 변수, 메서드의 바이트코드 등을 보관한다. 메서드 영역은 JVM 벤더마다 다양한 형태로 구현할 수 있으며, 오라클 핫스팟 JVM(HotSpot JVM)에서는 흔히 Permanent Area, 혹은 Permanent Generation(PermGen)이라고 불린다. 메서드 영역에 대한 가비지 컬렉션은 JVM 벤더의 선택 사항이다.
  • 런타임 상수 풀: 클래스 파일 포맷에서 constant_pool 테이블에 해당하는 영역이다. 메서드 영역에 포함되는 영역이긴 하지만, JVM 동작에서 가장 핵심적인 역할을 수행하는 곳이기 때문에 JVM 명세에서도 따로 중요하게 기술한다. 각 클래스와 인터페이스의 상수뿐만 아니라, 메서드와 필드에 대한 모든 레퍼런스까지 담고 있는 테이블이다. 즉, 어떤 메서드나 필드를 참조할 때 JVM은 런타임 상수 풀을 통해 해당 메서드나 필드의 실제 메모리상 주소를 찾아서 참조한다.
  • 힙: 인스턴스 또는 객체를 저장하는 공간으로 가비지 컬렉션 대상이다. JVM 성능 등의 이슈에서 가장 많이 언급되는 공간이다. 힙 구성 방식이나 가비지 컬렉션 방법 등은 JVM 벤더의 재량이다.

 

3) 실행 엔진

클래스 로더를 통해 JVM 내의 런타임 데이터 영역에 배치된 바이트코드는 실행 엔진에 의해 실행된다. 실행 엔진은 자바 바이트코드를 명령어 단위로 읽어서 실행한다. CPU가 기계 명령어을 하나씩 실행하는 것과 비슷하다. 바이트코드의 각 명령어는 1바이트짜리 OpCode와 추가 피연산자로 이루어져 있으며, 실행 엔진은 하나의 OpCode를 가져와서 피연산자와 함께 작업을 수행한 다음, 다음 OpCode를 수행하는 식으로 동작한다.

그런데 자바 바이트코드는 기계가 바로 수행할 수 있는 언어보다는 비교적 인간이 보기 편한 형태로 기술된 것이다. 그래서 실행 엔진은 이와 같은 바이트코드를 실제로 JVM 내부에서 기계가 실행할 수 있는 형태로 변경하며, 그 방식은 다음 두 가지가 있다.

  • 인터프리터: 바이트코드 명령어를 하나씩 읽어서 해석하고 실행한다. 하나씩 해석하고 실행하기 때문에 바이트코드 하나하나의 해석은 빠른 대신 인터프리팅 결과의 실행은 느리다는 단점을 가지고 있다. 흔히 얘기하는 인터프리터 언어의 단점을 그대로 가지는 것이다. 즉, 바이트코드라는 '언어'는 기본적으로 인터프리터 방식으로 동작한다.
  • JIT(Just-In-Time) 컴파일러: 인터프리터의 단점을 보완하기 위해 도입된 것이 JIT 컴파일러이다. 인터프리터 방식으로 실행하다가 적절한 시점에 바이트코드 전체를 컴파일하여 네이티브 코드로 변경하고, 이후에는 해당 메서드를 더 이상 인터프리팅하지 않고 네이티브 코드로 직접 실행하는 방식이다. 네이티브 코드를 실행하는 것이 하나씩 인터프리팅하는 것보다 빠르고, 네이티브 코드는 캐시에 보관하기 때문에 한 번 컴파일된 코드는 계속 빠르게 수행되게 된다.

JIT 컴파일러가 컴파일하는 과정은 바이트코드를 하나씩 인터프리팅하는 것보다 훨씬 오래 걸리므로, 만약 한 번만 실행되는 코드라면 컴파일하지 않고 인터프리팅하는 것이 훨씬 유리하다. 따라서, JIT 컴파일러를 사용하는 JVM들은 내부적으로 해당 메서드가 얼마나 자주 수행되는지 체크하고, 일정 정도를 넘을 때에만 컴파일을 수행한다.

 

 

 

마지막으로 , JVM의 특징으로는 아래와 같은 것들이 있다.

  • 스택 기반의 가상 머신 (<-> 레지스터 기반 동작)
  • 심볼릭 레퍼런스: 기본 자료형(primitive data type)을 제외한 모든 타입(클래스와 인터페이스)을 명시적인 메모리 주소 기반의 레퍼런스가 아니라 심볼릭 레퍼런스를 통해 참조한다.
  • 가비지 컬렉션(garbage collection): 클래스 인스턴스는 사용자 코드에 의해 명시적으로 생성되고 가비지 컬렉션에 의해 자동으로 파괴된다.
  • 기본 자료형을 명확하게 정의하여 플랫폼 독립성 보장: C/C++ 등의 전통적인 언어는 플랫폼에 따라 int 형의 크기가 변하는데 반해, JVM은 기본 자료형을 명확하게 정의하여 호환성을 유지하고 플랫폼 독립성을 보장한다.
  • 네트워크 바이트 오더(network byte order): 자바 클래스 파일은 네트워크 바이트 오더를 사용한다. 인텔 x86 아키텍처가 사용하는 리틀 엔디안이나, RISC 계열 아키텍처가 주로 사용하는 빅 엔디안 사이에서 플랫폼 독립성을 유지하려면 고정된 바이트 오더를 유지해야 하므로 네트워크 전송 시에 사용하는 바이트 오더인 네트워크 바이트 오더를 사용한다. 네트워크 바이트 오더는 빅 엔디안이다.

 

Garbage Collector

이제 JVM의 가장 큰 특징, 가비지 컬렉터에 대해 알아보자.

gc는 동적으로 할당된 메모리 영역 중 사용하지 않는 영역을 방지하여 해제하는 기능을 말한다. (자바에서 동적으로 할당된 메모리 = 힙 영역)

 

Java GC는 객체가 가비지인지 판별하기 위해서 reachability라는 개념을 사용한다. 어떤 객체에 유효한 참조가 있으면 'reachable'로, 없으면 'unreachable'로 구별하고, unreachable 객체를 가비지로 간주해 GC를 수행한다. 한 객체는 여러 다른 객체를 참조하고, 참조된 다른 객체들도 마찬가지로 또 다른 객체들을 참조할 수 있으므로 객체들은 참조 사슬을 이룬다. 이런 상황에서 유효한 참조 여부를 파악하려면 항상 유효한 최초의 참조가 있어야 하는데 이를 객체 참조의 root set이라고 한다.

 

https://d2.naver.com/helloworld/329631 런타임 데이터 영역

런타임 데이터 영역은 위와 같이 스레드가 차지하는 영역들과, 객체를 생성 및 보관하는 하나의 큰 힙, 클래스 정보가 차지하는 영역인 메서드 영역, 크게 세 부분으로 나눌 수 있다. 위 그림에서 객체에 대한 참조는 화살표로 표시되어 있다.

 

힙에 있는 객체들에 대한 참조는 다음 4가지 종류 중 하나이다.

  • 힙 내의 다른 객체에 의한 참조
  • Java 스택, 즉 Java 메서드 실행 시에 사용하는 지역 변수와 파라미터들에 의한 참조
  • 네이티브 스택, 즉 JNI(Java Native Interface)에 의해 생성된 객체에 대한 참조
  • 메서드 영역의 정적 변수에 의한 참조

이들 중 힙 내의 다른 객체에 의한 참조를 제외한 나머지 3개가 root set으로, reachability를 판가름하는 기준이 된다.

 

reachability를 더 자세히 설명하기 위해 root set과 힙 내의 객체를 중심으로 다시 그리면 다음과 같다.

https://d2.naver.com/helloworld/329631 reachability

위 그림에서 보듯, root set으로부터 시작한 참조 사슬에 속한 객체들은 reachable 객체이고, 이 참조 사슬과 무관한 객체들이 unreachable 객체로 GC 대상이다. 오른쪽 아래 객체처럼 reachable 객체를 참조하더라도, 다른 reachable 객체가 이 객체를 참조하지 않는다면 이 객체는 unreachable 객체이다.

 

 

그렇다면 GC 매커니즘을 보자. 아래와 같은 메모리 영역에 대해,

  • Stack: 정적으로 할당한 메모리 영역으로, 원시 타입의 데이터가 값과 함께 할당된다. 힙 영역에 생성된 Object 타입의 데이터의 참조값 할당
  • Heap: 동적으로 할당한 메모리 영역으로, 모든 Object 타입의 데이터가 할당된다. 힙 영역의 Object를 가리키는 참조 변수가 스택 영역에 저장된다. 힙 영역은 New Generation 영역과 Old Generation 영역으로 이루어져 있다.
    • New generation:
      • Eden: 새로운 객체는 Eden 영역에 할당된다. Eden영역이 모두 사용되면 GC가 발생하는데, 이때 일어나는 가비지 컬렉터가 Minor GC라고 한다.
      • Servival 0: 이 이후에 살아남은 객체(Eden 영역의 Reachable 객체)를 Servival 0 영역으로 이동한다. Eden영역의 Unreachable 객체는 메모리에서 해제한다. Servival 0 영역이 다 차면 또 다시 Mark & Sweep 과정을 반복한다.
      • Servival 1: Servival 0 영역에서 살아남은 객체들을 Survival 1 영역으로 이동한다. 이동한 객체는 Age값 증가한다. 그 다음에 새로운 객체가 Eden 영역으로 들어와서 Minor GC가 발생하면 아까처럼 Servival 0 으로 가는 것이 아니라, 객체가 차 있는 곳으로 이동하기 때문에 바로 Servival 1 로 이동하게 된다. (즉, Servival 0/1중 둘 중 하나는 항상 비어있는 상태로 유지된다.) 만약 Servival 1 이 다 차면 Servival 1에 대해 Mark & Sweep 과정이 일어나고 Servival 0 으로 이동 + Age 1 증가하게 된다.
    • Old generation: Servival 영역의 Age 값이 증가하다가 특정 Age 값을 넘어서면, 그 때 Old generation으로 이동한다. 이 과정을 Promotion 과정이라고 한다. 만약 Old generation영역이 다 사용되면 Major GC가 발생한다.
    • -> 이 과정이 반복되면서 가비지 컬렉터가 메모리를 관리한다.

Garbage Collecter 과정을 Mark & Sweep이라고 하는데,

  • 가비지 컬렉터가 스택 영역의 모든 변수를 스캔하면서 각각 어떤 객체를 참조하고 있는지 찾아서 마킹한다.
  • Reachable Object가 참조하고 있는 객체도 찾아서 마킹한다.
  • 마킹되지 않은 객체를 힙 영역에서 제거한다.

 

 

용어

  • JRE(Java Runtime Environment): JVM + 라이브러리
    • 자바 애플리케이션을 실행할 수 있도록 구성된 배포판 (최소한의 배포 단위)
    • JVM과 핵심 라이브러리 및 자바 런타임 환경에서 사용하는 프로퍼티 세팅이나 리소스 파일을 가지고 있다.
    • 개발 관련 도구는 포함하지 않는다. (java compile을 위한 javac 등은 없음)
  • JDK (Java Development Kit): JRE + 개발 툴
    • JRE + 개발에 필요할 툴
    • 소스 코드를 작성할 때 사용하는 자바 언어는 플랫폼에 독립적.
    • 오라클은 자바 11부터는 JDK만 제공하며 JRE를 따로 제공하지 않는다.
    • Write Once Run Anywhere

 

Reference

docs.oracle.com/javase/specs/jvms/se8/html/index.html

 

The Java® Virtual Machine Specification

Tim Lindholm Frank Yellin Gilad Bracha Alex Buckley

docs.oracle.com

d2.naver.com/helloworld/329631

d2.naver.com/helloworld/1230

 

ArchUnit: 애플리케이션의 아키텍처를 테스트 할 수 있는 오픈 소스 라이브러리

=> package, class, layer, 슬라이스 간의 의존성을 확인할 수 있는 기능

www.archunit.org/userguide/html/000_Index.html#_what_to_check

 

ArchUnit User Guide

ArchUnit is a free, simple and extensible library for checking the architecture of your Java code. That is, ArchUnit can check dependencies between packages and classes, layers and slices, check for cyclic dependencies and more. It does so by analyzing giv

www.archunit.org

 

 

 

 

아키텍처 테스트란?

  • A 라는 패키지가 B (또는 C, D) 패키지에서만 사용 되고 있는지 확인 가능
  • XXXSerivce라는 이름의 클래스들이 XXXController 또는 XXXService라는 이름의 클래스에서만 참조하고 있는지 확인
  • XXXService라는 이름의 클래스들이 ..service.. 라는 패키지에 들어있는지 확인
  • A라는 애노테이션을 선언한 메소드만 특정 패키지 또는 특정 애노테이션을 가진 클래스를 호출하고 있는지 확인
  • 특정한 스타일의 아키텍처를 따르고 있는지 확인

 

ArchUnit을 사용하기 위해서 먼저 의존성을 추가해주자

testCompile 'com.tngtech.archunit:archunit:0.11.0'

 

그 다음 사용법은 아래와 같다.

참고: blogs.oracle.com/javamagazine/unit-test-your-architecture-with-archunit

@Test
public void Services_should_only_be_accessed_by_Controllers() {
    // 1. 특정 패키지에 해당하는 클래스를 (바이트코드를 통해) 읽어들인다
    JavaClasses importedClasses = new ClassFileImporter().importPackages("com.mycompany.myapp");

    // 2. 확인할 규칙을 정의한다
    ArchRule myRule = classes()
        .that().resideInAPackage("..service..")
        .should().onlyBeAccessed().byAnyPackage("..controller..", "..service..");

    // 3. 읽어들인 클래스들이 그 규칙을 잘 따르는지 확인한다
    myRule.check(importedClasses);
}

 

 

ArchUnit의 한가지 아쉬운점은 DisplayName을 설정하지 못한다

junit engine을 확장하여 archunit-junit 모듈을 만든 것이기 때문이다 (archunit engine을 따로 사용)

카오스 엔지니어링 툴: 로컬에서는 경험하기 힘든 운영 환경에서 발생하는 아래와 같은 이슈를 미리 확인해 볼 수 있는 툴이다.

  • 네트워크 지연
  • 서버 장애
  • 디스크 오작동
  • 메모리 누수

 

Chaos Monkey Spring Boot

codecentric.github.io/chaos-monkey-spring-boot/

 

Chaos Monkey for Spring Boot

This project provides a Chaos Monkey for Spring Boot and will try to attack your running Spring Boot App.

codecentric.github.io

애노테이션들이 붙어있는 Bean들의 메소드에 public이 붙여져 있다면 오른쪽의 공격들을 해 볼 수 있다. 응답 지연을 한 번 만들어 보자

 

 

 

먼저 의존성을 추가해주고 profile에 활성화를 시켜주어야 한다.

// 스프링 부트용 카오스 멍키
implementation 'de.codecentric:chaos-monkey-spring-boot:2.0.2'

// 스프링 부트 운영 툴로, 런 타임 중에 카오스 멍키 설정 변경 가능/헬스체크/로그 레벨 변경/매트릭스 데이터 조회 등 다양하게 사용 가능
implementation 'org.springframework.boot:spring-boot-starter-actuator'
//application.properties

//카오스 멍키 활성화
spring.profiles.active=chaos-monkey

//스프링 부트 Actuator 엔드 포인트 활성화
management.endpoint.chaosmonkey.enabled=true
management.endpoints.web.exposure.include=health,info,chaosmonkey

 

 

Repository 애노테이션이 붙은 곳에 카오스 멍키를 적용해 보기 위해서는 application.properties 파일에 아래 부분이 추가되어야 한다.

//application.properties
chaos.monkey.watcher.repository=true

 

그 다음 httpie를 이용하여 http 명령어를 보내서 활성화 시키면 된다.

httpie.io/

 

HTTPie – command-line HTTP client for the API era

CLI HTTP that will make you smile. JSON and sessions support, syntax highlighting, wget-like downloads, plugins, and more.

httpie.io

  • 카오스 멍키 활성화: http post localhost:8080/actuator/chaosmonkey/enable
  • 카오스 멍키 상태 확인: http localhost:8080/actuator/chaosmonkey/status
  • 카오스 멍키 watcher 설정 확인: http localhost:8080/actuator/chaosmonkey/watchers
    • 주의) watcher를 끄는 것은 바로 반영 되지만, 활성화의 경우는 런타임에 반영되지 않는다. 따라서 watcher의 변경은 properties 파일에서 하는 것이 좋다.

 

지연 공격은 아래와 같은 명령어를 입력하면 된다.

http POST localhost:8080/actuator/chaosmonkey/assaults level=3 latencyRangeStart=2000 latencyRangeEnd=5000 latencyActive=true
  • level=3: 3번 요청할 때마다 1번씩 공격하도록
  • latencyRangeStart=2000 latencyRangeEnd=5000: 2초부터 5초 내로 응답을 지연 시켜라
  • latencyActive=true: 지연 공격을 활성화 하도록

 

 

위와 같이 애플리케이션에 공격을 할 수도 있지만, 애플리케이션에 에러가 발생하는 상황을 재현할 수도 있다.

http POST localhost:8080/actuator/chaosmonkey/assaults level=3 latencyActive=false exceptionsActive=true exception.type=java.lang.RuntimeException

 

 

더 많은 예시는 아래를 참고하자

codecentric.github.io/chaos-monkey-spring-boot/2.1.1/#_examples

 

Chaos Monkey for Spring Boot Reference Guide

Chaos Monkey for Spring Boot can be customized to your planned experiment. You can decide which attacks you want to run and which parts of your application should be attacked. Except for the Watcher, you can also influence the behavior of the Chaos Monkey

codecentric.github.io

 

JMeter: 성능 특정 및 부하 테스트 기능을 제공하는 오픈 소스 자바 애플리케이션이다. 다양한 테스트를 지원하고 CLI도 지원한다

jmeter.apache.org/

 

Apache JMeter - Apache JMeter™

Apache JMeter™ The Apache JMeter™ application is open source software, a 100% pure Java application designed to load test functional behavior and measure performance. It was originally designed for testing Web Applications but has since expanded to oth

jmeter.apache.org

 

주요 개념

  • Thread Group: 한 쓰레드 당 유저 한명
  • Sampler: 어떤 유저가 해야 하는 액션
  • Listener: 응답을 받았을 할 일 (리포팅, 검증, 그래프 그리기 등)
  • Configuration: Sampler 또는 Listener가 사용할 설정 값 (쿠키, JDBC 커넥션 등)
  • Assertion: 응답이 성공적인지 확인하는 방법 (응답 코드, 본문 내용 등)

 

원래 제대로 성능테스트를 하려면 배포 서버와 성능 테스트용 서버가 달라야 한다.

왜냐면 같은 서버에서 하게 된다면 JMeter를 실행할 때에도 시스템 리소스를 사용하기 때문에 성능 테스트를 할 때 애플리케이션의 성능도 떨어질 수 있기 때문이다.

 

 

바이너리 zip 파일을 다운받은 후 압축을 푼 후에 ./bin/jmeter를 실행하면 아래와 같은 창이 뜬다

JMeter

 

 

가장 먼저 할 일은 Thread Group 만들기이다. (만든 테스트가 어떤 테스트인지 이름을 지어주는 것과 같다)

그 다음 Thread Group을 추가해주도록 한다.

  • Number of Threads: 쓰레드 개수 (동시에 요청을 보내는 유저의 수, 10명)
  • Ramp-up period: 쓰레드 개수를 만드는데 소요할 시간 (유저를 얼마나 빠른 시간동안 만들어 낼것이냐, 10초)
  • Loop Count: infinite 체크 하면 위에서 정한 쓰레드 개수로 계속 요청 보내기. 값을 입력하면 (해당 쓰레드 개수) X (루프 개수) 만큼 요청을 보낸다 (Sampler로 정의할 action을 몇 번 반복할 것인가?, 10명씩 2세트 총 20명)

 

그 다음엔 Sampler를 만들어야 한다. Sampler는 각각의 유저가 할 일을 정의해주는 것과 같다.

Sampler > HttpRequest

  • 여러 종류의 샘플러가 있지만 그 중에 우리가 사용할 샘플러는 HTTP Request 샘플러.
  • HTTP Sampler: 요청을 보낼 호스트, 포트, URI, 요청 본문 등을 설정
  • 여러 샘플러를 순차적으로 등록하는 것도 가능하다.

 

요청을 보낸 후 결과를 보기 위한 Listener 만들기 후에 아래 Listener 등을 추가해보자

  • View Results Tree
  • View Resulrts in Table
  • Summary Report
  • Aggregate Report

application의 한계를 알아보려면 요청을 많이 보내 보고 throughput을 보면 된다(TPS)

 

 

응답의 경우 Assertion을 추가하면 좀 더 자세하게 확인이 가능하다.

  • 응답 코드 확인
  • 응답 본문 확인

여기까지 UI로 jmx 파일을 만든 것이다. 이 것을 커맨드로 실행할려면 아래와 같은 명령어를 커맨트 창에 입력하면 된다.

 

 

CLI 사용하기

  • jmeter -n -t 설정 파일 -l 리포트 파일
    • -n: UI를 쓰지 않는다
    • -t: JMeter 테스트에 대한 설정
    • -l: 리포트

 

추가적으로 BlazeMeter을 사용하면 크롬에서 액션하는 것을 녹화한 후 JMeter에 추가할 수 있다.

chrome.google.com/webstore/detail/blazemeter-the-continuous/mbopgmdnpcbohhpnfglgohlbhfongabi?hl=en

 

BlazeMeter | The Continuous Testing Platform

Record Selenium and HTTP traffic to create a load and functional tests in less than 10 minutes (Apache JMeter Compatible).

chrome.google.com

 

'Java (+ Spring)' 카테고리의 다른 글

아키텍쳐 테스트: ArcUnit  (0) 2021.02.11
운영 이슈 테스트: Chaos Monkey  (0) 2021.02.11
도커로 테스트 하기: TestContainers  (0) 2021.02.10
Mock객체로 테스트 하기: Mockito  (0) 2021.02.10
Java Unit Test: Junit5  (0) 2021.02.10

TestContainers: 테스트에서 도커 컨테이너를 실행할 수 있도록 하는 라이브러리

www.testcontainers.org/

 

Testcontainers

 Testcontainers About Testcontainers is a Java library that supports JUnit tests, providing lightweight, throwaway instances of common databases, Selenium web browsers, or anything else that can run in a Docker container. Testcontainers make the followi

www.testcontainers.org

 

DB의 경우 DB마다 isolation, propagation 전략들이 다르다. (스프링에서는 DB에서 제공하는 이러한 전략들을 따를 뿐이다)

 

하지만 컨테이너의 테스트용 디비를 띄우고, 스크립트를 실행하고, 테스트가 끝난 후에는 컨테이너를 정리하는 등 복잡해질 수 있다.

이를 해결하기 위한 방법이 바로 TestContainers이다.

 

먼저 junit 확장체를 지원하는 testcontainers 모듈을 gradle에 추가한다.

여러 모듈 중에 현재 프로젝트에서 사용하고 있는 DB 모듈을 동일하게 사용하면 된다. 내 경우는 MariaDB를 사용하고 있음.

www.testcontainers.org/modules/databases/mariadb/

 

MariaDB Module - Testcontainers

 

www.testcontainers.org

testImplementation "org.testcontainers:junit-jupiter:1.15.1"
testCompile "org.testcontainers:mariadb:1.15.1"
@ActiveProfiles("test")
@Testcontainers
@SpringBootTest
class ToyRepositoryTest {

    @Autowired
    private ToyRepository toyRepository;

    @Autowired
    private OrganizationRepository organizationRepository;

    static MariaDBContainer mariaDB = new MariaDBContainer().withDatabaseName("toy_land_test");

    @BeforeAll
    static void beforeAll() {
        mariaDB.start();
    }

    @AfterAll
    static void afterAll() {
        mariaDB.stop();
    }
    
    @Test
    /* 테스트 */
}
spring:
  jpa:
    open-in-view: false
    generate-ddl: true
    show-sql: true
    hibernate:
      ddl-auto: create-drop
  datasource:
    url: jdbc:tc:mariadb:///toy_land_test
    driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver

 

위 코드의 경우 테스트를 실행시키기 전에 컨테이너를 띄우고 실행 후에는 컨테이너를 없애는 것이 반복되어 느릴 수 있다.

아래 코드처럼 컨테이너를 static으로 띄워두고 테스트를 실행 시키기 전에 DB를 비우는 방식으로 코드를 짤 수도 있다. (위의 경우보다 빠를 것이다.)

package com.openhack.toyland.domain.toy;

import static org.assertj.core.api.Assertions.*;
import com.openhack.toyland.domain.Organization;
import com.openhack.toyland.domain.OrganizationRepository;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.testcontainers.containers.MariaDBContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

@ActiveProfiles("test")
@Testcontainers
@SpringBootTest
class ToyRepositoryTest {

    @Autowired
    private ToyRepository toyRepository;

    @Autowired
    private OrganizationRepository organizationRepository;

    @Container
    static MariaDBContainer mariaDB = new MariaDBContainer().withDatabaseName("toy_land_test");

    @BeforeEach
    void beforeEach() {
        toyRepository.deleteAll();
        organizationRepository.deleteAll();
    }

    @Test
    /* 테스트 */
}

 

 

TestContainers가 제공하지 않은 일반적인 컨테이너를 만드는 방법 

  • GenericContainer(dockerImageName): 이미지 이름만 있으면 컨테이너를 만드는 것이 가능하다. 이 경우 먼저 로컬에서 이미지를 찾아보고, 로컬에 없으면 퍼블릭한 원격에서 찾아서 가져온다.
    • withEnv: 환경 변수 세팅이 필요하다.
    • withExposedPorts: 포트 노출 -> 일반적으로는 컨테이너가 랜덤하게 매핑해준다
    • waitingFor: 해당 컨테이너가 띄워졌는지 확인 후 테스트 실행

 

TestContainers의 로그 살펴보기

  • getLogs(): 모든 로그를 보는 방법
  • followOutput(new Slf4jLogConsumer(log)): 로그 스트리밍

 

컨테이너 내부의 정보를 테스트 코드에서 활용하는 방법

ApplicationContextInitializer
스프링 ApplicationContext를 프로그래밍으로 초기화 할 때 사용할 수 있는 콜백 인터페이스로, 특정 프로파일을 활성화 하거나, 프로퍼티 소스를 추가하는 등의 작업을 할 수 있다.

TestPropertyValues
테스트용 프로퍼티 소스를 정의할 때 사용한다.

Environment
스프링 핵심 API로, 프로퍼티와 프로파일을 담당한다.

Testcontainer를 사용해서 생성한 컨테이너에 대해 ApplicationContextInitializer를 구현하여 생성된 컨테이너에서 정보를 Environment에 넣어준다.

그 다음 @ContextConfiguration을 사용해서 아까 구현했던 ApplicationContextInitializer 구현체를 등록해주면, 테스트 코드에서 Environment, @Value, @ConfigurationProperties 등 다양한 방법으로 프로퍼티를 사용할 수 있게 된다.

@ActiveProfiles("test")
@Testcontainers
@Slf4j
@ContextConfiguration(initializers = StudyServiceTest.ContainerPropertyInitializer.class)
class StudyServiceTest {

    @Mock MemberService memberService;

    @Autowired StudyRepository studyRepository;

    @Value("${container.port}") int port;

    @Container
    static GenericContainer postgreSQLContainer = new GenericContainer("postgres")
            .withExposedPorts(5432)
            .withEnv("POSTGRES_DB", "studytest");
    @BeforeAll
    static void beforeAll() {
        Slf4jLogConsumer logConsumer = new Slf4jLogConsumer(log);
        postgreSQLContainer.followOutput(logConsumer);
    }
    
    @BeforeEach
    void beforeEach() {
        System.out.println("===========");
        System.out.println(postgreSQLContainer.getMappedPort(5432));
        System.out.println(port);
        studyRepository.deleteAll();
    }

    @Test
    /* 테스트 */

    static class ContainerPropertyInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

        @Override
        public void initialize(ConfigurableApplicationContext context) {
            TestPropertyValues.of("container.port=" + postgreSQLContainer.getMappedPort(5432))
                    .applyTo(context.getEnvironment());
        }
    }

}

 

 

 

 

만약 테스트 컨테이너들을 여러개를 동시에 띄워서 사용해야 한다면? -> DockerComposeContainer

www.testcontainers.org/modules/docker_compose/

 

Docker Compose Module - Testcontainers

 Docker Compose Module Benefits Similar to generic containers support, it's also possible to run a bespoke set of services specified in a docker-compose.yml file. This is intended to be useful on projects where Docker Compose is already used in dev or o

www.testcontainers.org

static DockerComposeContainer composeContainer =
    new DockerComposeContainer(new File("src/test/resources/docker-compose-test.yml"))
            .withExposedService("redis_1", REDIS_PORT, Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30)))
            .withExposedService("elasticsearch_1", ELASTICSEARCH_PORT, Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30)));

위와 같이 docker-compose 파일을 주면 여러 컨테이너를 띄울 수 있다. (이때, 호스트쪽 포트 매핑은 안해두는 것이 낫다)

아직 컨테이너가 뜨지도 않았는데 테스트를 실행하는 것을 방지하기 위해 Wait.forListeningPort()를 추가 해 주는 것이 좋다.

 

위의 경우 docker-compose로 띄운 서비스의 정보를 참조하기 위해 구현하는 ApplicationContextInitializer는 아래와 같이 구현하면 된다.

static class ContainerPropertyInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

    @Override
    public void initialize(ConfigurableApplicationContext context) {
        TestPropertyValues.of("container.port=" + composeContainer.getServicePort("study-db", 5432))
                .applyTo(context.getEnvironment());
    }
}

 

 

Mock: 진짜 객체와 비슷하게 동작하지만 프로그래머가 직접 그 객체의 행동을 관리하는 객체.

Mockito: Mock을 지원하는 프레임워크로, 객체를 쉽게 만들고 관리하고 검증할 수 있는 방법을 제공한다.

 

martinfowler.com/bliki/UnitTest.html

 

bliki: UnitTest

Unit Tests are focused on small parts of a code-base, defined in regular programming tools, and fast. There is disagreement on whether units should be solitary or sociable.

martinfowler.com

단위 테스트를 생각할 때, 클래스나 오브젝트, 또는 행동과 연관된 로직이라고 다양하게 생각할 수 있다.

나중에 테스트를 작성하게 되면 단위테스의 단위를 어떻게 정의할 것인가, Mock은 어느 범위에서 적용할 것인가 등을 함께 정하는 것이 좋다고 함

 

 

Mockito의 경우 스프링 부트를 사용하게 되면 spring-boot-starter-test의 의존성을 따라 자동으로 들어오게 된다.

만약 이 것을 사용하지 않은 경우엔 아래 2가지 의존성 추가가 필요하다.

  • mockito-junit-jupiter: junit test에서 mockito를 연동해서 사용할 수 있는 추가적인 확장 구현체를 제공하는 라이브러리
  • mockito-core: mockito가 제공하는 기본적인 기능들이 들어있는 라이브러리

 

Mock은 그럼 언제 사용하냐면?

외부 api등을 호출할 때, 어떻게 답이 오는지 가정 등을 하고 mock 객체로 만들어 이 것이 어떻게 동작하는지 테스트할 경우 등 사용한다.

또, 내가 만들고 있는 코드가 의존하는 클래스의 구현체는 없지만 인터페이스는 있고, 그 인터페이스 기반으로 코드를 작성할 때 그 것을 확인하기 위해 사용되기도 한다.

아래와 같은 두 가지 방법으로 Mock 객체를 만들 수 있다.

  • Mockito.mock: 생성자로 주입하는 방법
  • @Mock + @ExtendWith(MockitoExtension.class) - 함수 밖에 테스트 클래스의 필드로 주입할 수도 있고, 파라미터 전달도 가능하다

 

Mock 객체를 만든 후에는 Mock 객체의 행동을 조작해야한다. 이를 Stubbing이라고 한다.

모든 Mock 객체의 행동은 기본적으로 아래와 같이 행동한다.

  • 리턴 타입이 있다면 Null 리턴한다. (Optional 타입은 Optional.empty 리턴)
  • Primitive 타입은 프리미티브 기본 값을 따름
  • 콜렉션은 비어있는 콜렉션으로 만들어준다
  • Void 메소드는 예외를 던지지 않고 아무런 일도 발생하지 않는다

Mock 객체를 조작하기 위해서는 

  • Mockito.when(/*조건*/).thenReturn(/*객체*/): 조건에 해당한다면 무조건 어떠한 객체를 리턴하라
  • Mockito.when(/*조건*/).thenReturn(/*객체*/).thenThrow~~.thenReturn~~: 메소드가 여러 번 호출될 때 stubbing을 같은 매개변수로 호출 되더라도 호출되는 순서에 따라 다르게 mocking. 즉, 동일한 조건에 대해 다양한 객체/예외를 리턴하도록 설정해 둘 수 있다.
  • doThrow(/*Exception*/).when(/*조건*/): 조건에 해당한다면 어떠한 예외를 던져라

 

마지막으로 Mockito로 만든 Mock 객체가 어떻게 사용됐는지(객체에 어떤 일이 일어났는지) 확인하는 방법이다.

  • verify(memberService, times(1)).notify(study): 예를 들어 mock객체 memberService의 함수 notify가 1번만 호출되어야 한다
  • inOrder(memberService): 함수 호출에 대해 순서를 부여하고 싶은 경우

다른 여러가지 조건은 아래 공식문서를 확인하도록 하자

javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html#verification_timeout

 

Mockito - mockito-core 3.7.7 javadoc

Latest version of org.mockito:mockito-core https://javadoc.io/doc/org.mockito/mockito-core Current version 3.7.7 https://javadoc.io/doc/org.mockito/mockito-core/3.7.7 package-list path (used for javadoc generation -link option) https://javadoc.io/doc/org.m

javadoc.io

 

 

Mockito에서는 별개로 BDD 스타일의 테스트를 할 수 있는 방법을 BddMockito 클래스를 통해 제공한다.

BDD (Behaviour-Driven Development): 애플리케이션이 어떻게 “행동”해야 하는지에 대한 공통된 이해를 구성하는 방법으로, TDD에서 창안했다.

BDD는 시나리오를 기반으로 테스트 케이스를 작성하며 함수 단위 테스트를 권장하지 않는다. 이 시나리오는 개발자가 아닌 사람이 봐도 이해할 수 있을 정도의 레벨을 권장한다. 하나의 시나리오는 Given, When, Then 구조를 가지는 것을 기본 패턴으로 권장하며 각 절의 의미는 다음과 같다.

Feature : 테스트에 대상의 기능/책임을 명시한다
Scenario : 테스트 목적에 대한 상황을 설명한다
Given : 시나리오 진행에 필요한 값을 설정한다 - 어떤 상황이 주어졌을 때
When : 시나리오를 진행하는데 필요한 조건을 명시한다 - 무엇인가를 하면
Then : 시나리오를 완료했을 때 보장해야 하는 결과를 명시한다 - ~~할 것이다
위의 내용을 개발 측면에서 더 간략하게 정리하면 테스트 대상의 상태 변화를 테스트하는 것이다.
테스트 대상은 A 상태에서 출발하며(Given) 어떤 상태 변화를 가했을 때(When) 기대하는 상태로 완료되어야 한다. (Then)
또는 Side Effect가 전혀 없는 테스트 대상이라면 테스트 대상의 환경을 A 상태에 두고(Given) 어떤 행동을 요구했을 때(When) 기대하는 결과를 돌려받아야 한다. (Then)

 

Jnuit5 - java8 이상을 필요로 하고 단위테스트를 작성할 때 사용한다.

 

junit4는 써보진 않았지만, 하나의 jar dependency library 형태로 들어오고, junit이 참조하는 다른 라이브러리들이 있는 그런 형태였다면,

junit5는 그 자체로 여러 모듈화(junit platform, jupiter, vintage)가 되어있다. 

  • Platform:  테스트를 실행해주는 런처 제공. TestEngine API 제공 => 런쳐를 통해 콘솔이나 java 메인 메소드, intellij 등에서 테스트 실행 가능
  • Jupiter: TestEngine API 구현체로 JUnit 5를 제공
  • Vintage: JUnit 4와 3을 지원하는 TestEngine 구현체

스프링 부트 프로젝트에는 기본적으로 junit5의존성이 추가되어 있다.

 

기본적인 어노테이션은 아래와 같다.

  • @Test
  • @BeforeAll - test class 안에 있는 여러 테스트가 모두 실행 될 때, 모든 테스트가 실행되기 전에 한 번만 호출. 구현할 때에는 반드시 static 메소드로 구현해야 하고, private는 불가능하다. 또한, 리턴 타입이 있으면 안됨
  • @AfterAll - 마찬가지로 모든 테스트가 실행 된 이후 한 번만 호출. static void 형태로 작성한다.
  • @BeforeEach - 모든 테스트를 실행할 때, 각각의 테스트를 실행하기 이전에 한 번씩 호출된다. 마찬가지로 리턴 타입은 void이어야 하며 private를 사용하지 않도록 한다.
  • @AfterEach - 마찬가지로 모든 테스트 이후 각각 실행된다. 리턴 타입은 void, private는 사용하지 않도록 한다.
  • @Disabled - 필요 없는 테스트 등을 빼고 실행할 때 사용

공식문서에서는 private를 `must not`으로 사용하지 말라고 나와있지만,, 리플렉션 때문에 private를 사용해도 사실 돌아가긴 한다.

sowhat4.tistory.com/73

 

Junit5 @BeforeEach private method가 동작하는 이유

발단 어느 날과 다름없이 비즈니스 로직을 먼저 작성하고 테스트 코드를 작성하는 코드 몽키 ing 중 🐒 테스트 코드 실행 전 초기 설정이 필요하여 Junit5의 @BeforeEach 사용 호기심이 발동하여 접근

sowhat4.tistory.com

 

 

일반적으로 테스트를 실행하면 테스트에 이름을 표기하는 방법은 아래와 같다. (기본 표기 전략은 메소드 이름)

메소드 이름은 길어지면 snake case로 작성한다.

  • @DisplayNameGeneration: 메소드와 클래스 레퍼런스를 사용해서 테스트 이름을 표기하는 방법 설정하는 방법으로, 기본 구현체로 ReplaceUnderscores 제공 (ex.@DisplayNameGeneration(DisplayGenerator.ReplaceUnderscores.class)) 
  • @DisplayName: 어떤 테스트인지 테스트 이름을 보다 쉽게 표현할 수 있는 방법을 제공하는 애노테이션이다. @DisplayNameGeneration 보다 우선 순위가 높다.

 

한 테스트에서 여러 Assertion문이 있는 경우에는 앞에 있는 assert문이 실패할 경우 그 다음을 보지 않는데,

이 것을 한번에 실행해 주는 방법이 assertAll이다. 각 assert문을 람다식으로 묶어 주면 한번에 실행하여 결과를 알 수 있다.

assertAll(
	() -> assertNotNull(study),
    () -> assertEquals(StudyStatus.DRAFT, study.getStatus(), () -> "스터디를 처음 만들면" + StudyStatus.DRAFT + "상태이다."),
    () -> assertTrue(study.getLimit()>0, "스터디 최대 참석 가능 인원은 0명 이상이다.");
);

참고로, assert문이 실패했을 때 메세지를 supplier를 사용하여 나타내게 되면 테스트가 실패할 때만 연산이 일어나기 때문에 성능상 조금 더 이점이 있다.

이와 유사하게 테스트 태깅 등으로 그룹화를 할 수 있다.

  • @Tag: 테스트를 그룹화 ex) 테스트가 오래 걸려서 로컬에서 테스트하기 적절하지 않는지/테스트가 짧게 끝나는지 등
    • 실행 방법
      • intellij에서 edit contiguration > test kind를 Tags로 바꾸기 > tag expression 설정해 주기
      • maven 설정: maven-surefire-plugin의 그룹을 설정해주기

 

 

조건에 따라 테스트를 실행할 수도 있다. (특정한 OS, 특정한 java version, 환경 변수 등)

  • assumeTrue(조건): 예를 들어 테스트의 환경변수가 로컬일 경우에만 실행할 때, 아래와 같이 진행할 수 있다.
assumeTrue("LOCAL".equalsIgnoreCase(System.getenv("TEST_ENV")));

/*테스트*/
  • assumingThat(조건, 테스트)
assumingThat("LOCAL".equalsIgnoreCase(System.getenv("TEST_ENV")), /*테스트*/);

 

  • @EnabledXXX 와 @DisabledXXX
    • OnOS: OS 종류
    • OnJre: java 버전
    • IfSystemProperty
    • IfEnvironmentVariable: 환경변수
    • If

 

junit5에서 제공하는 애노테이션들은 메타애노테이션들을 사용할 수 있는데, 그렇기 때문에 composed 애노테이션을 만들어서 사용할 수 있다고 한다.

즉, 커스텀 애노테이션들을 만들 때, 그 애노테이션 위에 기존의 메타애노테이션들을 사용하게 되면, 기존의 기능이 적용된다고 할 수 있다.

이 외에도 파라미터를 여러 인자값을 주면서 테스트를 반복할 수 있다.

junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests

 

JUnit 5 User Guide

Although the JUnit Jupiter programming model and extension model will not support JUnit 4 features such as Rules and Runners natively, it is not expected that source code maintainers will need to update all of their existing tests, test extensions, and cus

junit.org

 

기본적으로 junit이 테스트를 실행할 때, 클래스의 인스턴스를 만들 때 기본 전략은 테스트 메소드 마다 생성하게 된다.

테스트 순서는 예측할 수 없다. 기본적으로 메소드가 선언되어있는 순서대로 실행되긴 하지만,,, 꼭 그런건 아니라고 한다.

그래서 테스트 간 의존성을 없애기 위해서, 서로 공유하는 값을 바뀌지 않게 하기 위해서 각각 인스턴스를 만들어서 테스트를 실행하게 된다.

 

junit5부터는 이 기본 전략을 바꿀 수 있는 방법이 있다.

  • @TestInstance(Lifecycle.PER_CLASS)
    • 테스트 클래스당 인스턴스를 하나만 만들어 사용한다. => 이 경우 BeforeAll/AfterAll이 static 메소드일 필요가 없어진다
    • 경우에 따라, 테스트 간에 공유하는 모든 상태를 @BeforeEach 또는 @AfterEach에서 초기화 할 필요가 있다.
    • @BeforeAll과 @AfterAll을 인스턴스 메소드 또는 인터페이스에 정의한 default 메소드로 정의할 수도 있다. 

아까 말했듯이, 테스트 순서는 정해져 있지 았다. 그래서 테스트를 실행할 순서를 정의해서 상태 정보를 유지하며 usecase를 테스트할 수 있는 방법도 있다.

그렇게 하기 위해서는 위 방법처럼 TestInstance 애노테이션으로 의존성을 공유하는 것이 먼저이다.

그 다음 @TestMethodOrder 애노테이션을 클래스에 붙여준다. 기본 구현체는 아래 3가지를 제공해 준다.

  • Alphanumeric
  • OrderAnnoation: @Order(int), 낮은 값부터 실행시킨다
  • Random

 

마지막으로 junit5에서 extension을 사용하는 방법은 아래 공식문서를 보고 좀 더 공부해봐야겠다.

 

junit.org/junit5/docs/current/user-guide/#extensions

 

JUnit 5 User Guide

Although the JUnit Jupiter programming model and extension model will not support JUnit 4 features such as Rules and Runners natively, it is not expected that source code maintainers will need to update all of their existing tests, test extensions, and cus

junit.org

 

토큰 기반 인증을 사용하는 이유

1. Stateless 서버

  • Stateful 서버는 클라이언트에게서 요청을 받을 때 마다, 클라이언트의 상태를 계속해서 유지하고, 이 정보를 서비스 제공에 이용한다.
  • ex) 세션을 유지하는 웹서버: 유저가 로그인을 하면, 세션에 로그인이 되었다고 저장을 해 두고, 서비스를 제공 할 때에 그 데이터를 사용한다. 세션은 서버 컴퓨터의 메모리나 데이터베이스에 저장한다.
  • Stateless 서버는 반대로, 상태를 유지 하지 않는다. 상태정보를 저장하지 않으면, 서버는 클라이언트측에서 들어오는 요청만으로만 작업을 처리한다. => 클라이언트와 서버의 연결고리가 없기 때문에 서버의 확장성 (Scalability) 이 높아진다.

2. 모바일 어플리케이션에 적합하다

모바일 어플리케이션에서 안전한 API 를 만들기 위해선 쿠키같은 인증시스템은 이상적이지 않다고 한다. (쿠키 컨테이너를 사용해야 함). 토큰 기반 인증을 도입한다면 쉽게 구현 가능

 

3. 인증정보를 다른 어플리케이션으로 전달

대표적인 예제로는, OAuth 가 있다. 소셜 계정들을 이용하여 다른 웹서비스에서도 로그인 할 수 있게 하는 것이다.

 

4. 보안

토큰 기반 인증 시스템을 사용하여 어플리케이션의 보안을 높일 수 있다. 무조건은 아니지만,,,

 

 

 

JWT란?

jwt.io/introduction/

JWT 는 . 을 구분자로 3가지의 문자열로 되어있다. 구조는 아래와 같다.

 xxxxx.yyyyy.zzzzz 

 

1. 헤더

  • typ: 토큰의 타입을 지정
  • alg: 해싱 알고리즘을 지정. 해싱 알고리즘으로는 보통 HMAC SHA256 혹은 RSA 가 사용되며, 이 알고리즘은, 토큰을 검증 할 때 사용되는 signature 부분에서 사용된다.
{
  "typ": "JWT",
  "alg": "HS256" // HMAC SHA256 이 해싱 알고리즘으로 사용
}

 

 

2. Payload

토큰에 담을 정보가 들어있다. 여기에 담는 정보의 한 ‘조각’ 을 클레임(claim) 이라고 부르고, 이는 name / value 의 한 쌍으로 이뤄져있다. 토큰에는 여러개의 클레임 들을 넣을 수 있다. 클레임은 3가지 종류가 있다.

  • Registered claims: 서비스에서 필요한 정보들이 아닌, 토큰에 대한 정보들을 담기위하여 이름이 이미 정해진 클레임들로, optional
  • Public claims: 충돌이 방지된 (collision-resistant) 이름을 가지고 있어야 한다. 충돌을 방지하기 위해서는, 클레임 이름을 URI 형식으로 짓는다.
  • Private claims: 위의 두 경우가 아닌 모든 경우를 말한다. 양 측간에 (보통 클라이언트 <->서버) 협의하에 사용되는 클레임 이름들로, 공개 클레임과는 달리 이름이 중복되어 충돌이 될 수 있으니 사용할때에 유의해야한다.

3. 서명

헤더의 인코딩값과, 정보의 인코딩값을 합친후 주어진 비밀키로 해싱을 해서 생성한다. 그 결과를 base64 형태로 나타낸다. (문자열을 인코딩 하는게 아닌 hex  base64 인코딩을 해야함)

 

 

Spring Security란?

세션-쿠키방식으로 인증, id - password 방식

  • 스프링 기반의 어플리케이션에서 보안을 위해 인증 권한 부여를 사용하여 접근을 제어하는 프레임워크
  • 커스터마이징 가능
  • filter 기반으로 동작하기 때문에 Spring MVC와 분리되어 관리 및 동작
  • 허용되지 않은 페이지에 사용자가 접근할 경우 스프링 시큐리티는 페이지 호출 전에 인증이 되어있는지를 체크하고 페이지에 접근할 수 있는 권한이 있는지 체크 -> 허용 / 차단
  • 크리덴셜 기반 인증 사용

 

 

webfirewood.tistory.com/m/115?category=694472

스프링 시큐리티는 주로 서블릿 필터와 이들로 구성된 필터체인을 사용하고 있다. 아래는 동작 플로우이다.

1. 사용자가 로그인 정보와 함께 인증 요청(Http Request)

2. AuthenticationFilter가 이 요청을 가로챕니다. 이 때 가로챈 정보를 통해 UsernamePasswordAuthenticationToken이라는 인증용 객체를 생성합니다.

3. AuthenticationManager의 구현체인 ProviderManager에게 UsernamePasswordAuthenticationToken 객체를 전달합니다.

5. 실제 데이터베이스에서 사용자 인증정보를 가져오는 UserDetailsService에 사용자 정보(아이디)를 넘겨줍니다.

6. 넘겨받은 사용자 정보를 통해 DB에서 찾은 사용자 정보인 UserDetails 객체를 만듭니다. 이 때 UserDetails 는 인증용 객체와 도메인용 객체를 분리하지 않고 인증용 객체에 상속해서 사용하기도 합니다.

7. AuthenticationProvider는 UserDetails를 넘겨받고 사용자 정보를 비교합니다.

8. 인증이 완료되면 권한 등의 사용자 정보를 담은 Authentication 객체를 반환합니다.

9. 다시 최초의 AuthenticationFilter에 Authentication 객체가 반환됩니다.

10. Authentication 객체를 SecurityContext에 저장합니다.

 

최종적으로 SecurityContextHolder는 세션 영역에 있는 SecurityContext에 Authentication 객체를 저장한다. 세션에 사용자정보를 저장한다는 것은 스프링 시큐리티가 전통적인 세션-쿠키 기반의 인증 방식을 사용한다는 것을 의미한다.

스프링 시큐리티는 기본적으로 약 10개의 스프링 시큐리티 필터가 자동으로 설정되는데, 훨씬 다양한 필터체인을 사용하여 커스터마이징을 할 수 있다.

더보기

spring security는 필터 기반으로 동작하며 spring security에서 실행되는 필터만 20개가 넘는다

<http>태그의 auto-config속성이 true로 설정이 되면 10개 정도의 필터가 체인으로 만들어져서 동작한다.

      - SecurityContextPersistenceFilter
      - LogoutFilter
      - UsernamePasswordAuthenticationFilter
      - DefaultLoginPageGeneratingFilter
      - BasicAuthenticationFilter
      - RequestCacheAwareFilter
      - SecurityContextHolderAwareRequestFilter
      - AnonymousAuthenticationFilter
      - SessionManagementFilter
      - ExceptionTranslationFilter
      - FilterSecurityInterceptor

 

 

서블릿 필터? 필터 체인?

 

필터

컨테이너에 전달된 요청이 서블릿을 호출하기 전에 이 필터를 거치게 된다.

필터에서 서블릿의 service 메소드를 호출할 때 request, response 객체를 인자로 넘겨주게 된다. (servlet의 response 객체는 OuputStreamWriter 객체를 참조하고 있고 servlet 에서는 이 객체를 통해 직접 응답을 클라이언트로 전달)

문제는 필터에서 response 객체에 접근하고 싶을 때 발생한다. service 메소드가 종료된 시점에서 이미 response 가 클라이언트로 전달 된 다음이기 때문에 필터에서는 response 객체에 접근할 수가 없다. 이 때 wrapper 클래스를 사용하여 필터에서 wrapper 클래스를 만들어 진짜 response객체 대신 래퍼객체를 보내면 된다. 그렇게 되면 servlet에서는 실제 outputstream이 아닌 래퍼의 outputstream에 응답을 작성하게 되고 필터에서 이 응답에 접근할 수 있게 된다.

 

 

 

Spring Security oauth2란?

 

 

 

 

 

 

 

 

 

 

momentjin.tistory.com/144

momentjin.tistory.com/146

 

 

'Java (+ Spring)' 카테고리의 다른 글

Mock객체로 테스트 하기: Mockito  (0) 2021.02.10
Java Unit Test: Junit5  (0) 2021.02.10
Spring boot: JPA 설정 방법 및 주의할 점  (1) 2020.07.31
Spring: Spring MVC 설정 하기  (0) 2020.07.26
Android: tutorial page  (0) 2020.07.18

+ Recent posts