JUnit은 JVM 생태계에서 가장 널리 사용되는 테스트 프레임워크로, 주로 단위 테스트를 위한 도구를 제공하지만, 통합 테스트에도 활용할 수 있다.
JUnit의 주요 특징은 어노테이션 기반 테스트 지원, 단정문(Assert)을 통한 테스트 결과 검증이 있다.
통합 테스트
여러 컴포넌트를 조합하여 전체 비즈니스 로직을 검증한다. @SpringBootTest를 주로 사용하지만, 대규모 프로젝트에서는 실행 시간이 길어질 수 있다.
단위 테스트
단위 테스트는 코드의 특정 모듈이 의도한 대로 동작하는지 각 함수와 메소드에 대한 개별 테스트 케이스를 작성하여 검증합니다.
FIRST 원칙
효과적인 단위 테스트를 위한 5가지 원칙을 FIRST 원칙이라고 한다.
Fast: 테스트는 빠르게 실행되어야 한다.
Independent: 각 테스트는 독립적이어야 한다.
Repeatable: 테스트는 항상 동일한 결과를 produce해야 한다.
Self-Validating: 테스트는 자체적으로 결과를 검증할 수 있어야 한다.
Timely: 테스트는 프로덕션 코드 작성 전이나 동시에 작성되어야 한다.
JUnit 5의 구조
JUnit 5는 주로 세 가지 주요 모듈로 구성되지만, 실제로는 다양한 하위 모듈로 구성되어 있어 더 많은 기능을 제공한다.
JUnit Jupiter
JUnit Jupiter는 JUnit 5에서 새로 도입된 프로그래밍 및 확장 모델을 정의한다. Jupiter는 다음과 같은 하위 모듈로 구성된다.
모듈 | 설명 |
junit-jupiter-api | JUnit Jupiter API는 테스트를 작성하는 데 사용되는 애노테이션과 인터페이스를 제공한다. 예를 들어, @Test, @BeforeEach, @AfterEach 등이 포함된다. |
junit-jupiter-engine | JUnit Jupiter로 작성된 테스트를 발견하고 실행하는 런타임 컴포넌트이다. |
junit-jupiter-params | 다양한 파라미터를 사용하여 동일한 테스트 메서드를 여러 번 실행할 수 있도록 지원한다. csv 파일이나 Java Stream을 사용하여 파라미터화된 테스트를 실행할 수 있다. |
JUnit Platform
JUnit Platform은 JUnit 5에서 테스트를 발견하고 실행하기 위한 기반 역할을 한다. Platform은 다음과 같은 하위 모듈로 구성된다.
모듈 | 설명 |
junit-platform-commons | JUnit Platform에서 공통으로 사용하는 유틸리티와 헬퍼 클래스, 내부 기능을 제공한다. |
junit-platform-engine | 테스트 엔진을 정의하고 구현하기 위한 인터페이스를 제공한다. |
junit-platform-launcher | 테스트를 발견하고 실행하는 런처이다. 다양한 테스트 엔진을 통합하여 사용할 수 있다. |
junit-platform-reporting | 테스트 실행 결과를 보고하는 기능을 제공한다. 결과를 콘솔에 출력하거나 파일로 저장할 수 있다. |
junit-platform-suite-api | 다양한 테스트 클래스를 하나의 스위트로 묶어 실행할 수 있는 API를 제공한다. |
junit-platform-runner | JUnit 4 테스트 러너를 사용하여 JUnit 5 테스트를 실행할 수 있도록 한다. |
junit-platform-surefire-provider | Apache Maven Surefire 플러그인과 통합되어 JUnit 5 테스트를 실행하는 데 사용된다. |
junit-platform-console-standalone | 독립 실행형 모드로 JUnit 5 테스트를 실행할 수 있는 콘솔 애플리케이션이다. 명령줄에서 직접 테스트를 실행하고 결과를 볼 수 있다. |
JUnit Vintage
JUnit Vintage는 JUnit 3과 JUnit 4로 작성된 테스트를 JUnit 5 환경에서 실행할 수 있도록 지원한다. 다음과 같은 하위 모듈로 구성된다
모듈 | 설명 |
junit-vintage-engine | Unit 3과 JUnit 4로 작성된 기존 테스트를 JUnit 5 환경에서 실행할 수 있도록 지원한다. 기존 코드를 수정하지 않고도 JUnit 5로 마이그레이션할 수 있게 해준다. |
JUnit 애노테이션
Spring Boot 테스트 애노테이션
@SpringBootTest
@SpringBootTest는 통합 테스트를 실행하기 위해 전체 Spring 애플리케이션 컨텍스트를 로드한다. 모든 빈이 로드되며, 실제 애플리케이션 환경에서 테스트가 실행된다.
@SpringBootTest
class MyApplicationTests {
@Test
void contextLoads() {
// 테스트 코드
}
}
@WebMvcTest
@WebMvcTest는 웹 레이어(컨트롤러와 관련된 부분)만 테스트하기 위해 사용된다. 보통 Spring MVC 컨트롤러 테스트에 사용되며, 서비스나 리포지토리는 목(mock)으로 처리해야 한다.
@WebMvcTest(MyController.class)
class MyControllerTests {
@Autowired
private MockMvc mockMvc;
@Test
void testController() throws Exception {
mockMvc.perform(get("/endpoint"))
.andExpect(status().isOk());
}
}
@DataJpaTest
@DataJpaTest는 JPA 리포지토리 레이어만 테스트하기 위해 사용된다. 데이터베이스와의 상호작용을 테스트하며, 일반적으로 내장형 데이터베이스를 사용한다.
기본적으로 테스트 메서드마다 트랜잭션을 열고, 메서드가 종료되면 자동으로 롤백한다.
@DataJpaTest
class MyRepositoryTests {
@Autowired
private MyRepository myRepository;
@Test
void testFindByName() {
// 리포지토리 테스트 코드
}
}
@RestClientTest
@RestClientTest는 REST 클라이언트를 테스트하기 위해 사용된다. RestTemplate이나 WebClient를 사용하여 외부 API와 상호작용하는 코드를 테스트한다.
@RestClientTest(MyService.class)
class MyServiceTests {
@Autowired
private MyService myService;
@Test
void testRestClient() {
// REST 클라이언트 테스트 코드
}
}
@AutoConfigureMockMvc
@AutoConfigureMockMvc는 @SpringBootTest와 함께 사용하여 MockMvc를 자동 구성한다. 주로 전체 애플리케이션 컨텍스트를 로드하지 않고도 컨트롤러 테스트를 수행할 때 사용된다.
@SpringBootTest
@AutoConfigureMockMvc
class MyApplicationTests {
@Autowired
private MockMvc mockMvc;
@Test
void testEndpoint() throws Exception {
mockMvc.perform(get("/endpoint"))
.andExpect(status().isOk());
}
}
@ContextConfiguration
테스트의 ApplicationContext를 설정하는 데 사용된다. 클래스나 XML 파일을 통해 설정을 로드할 수 있다. 다양한 컨텍스트 설정 파일을 로드하거나 특정 설정 클래스를 지정하여 컨텍스트를 설정할 때 사용된다.
@ContextConfiguration(classes = TestConfig.class)
class MyTests {
@Autowired
private MyService myService;
@Test
void testService() {
// myService가 TestConfig에 의해 설정됨
}
}
@ContextConfiguration(locations = "classpath:app-config.xml")
class MyTests {
@Autowired
private MyService myService;
@Test
void testService() {
// myService가 app-config.xml에 의해 설정됨
}
}
@Import
하나 이상의 @Configuration 클래스를 현재 테스트 클래스의 컨텍스트에 가져온다. 주로 특정 구성 클래스만을 가져와 테스트 환경을 설정하고자 할 때 사용된다. 예를 들어, 테스트 중 특정 빈을 정의하거나 설정을 커스터마이즈 할 때 유용하다.
@Configuration
class TestConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}
@SpringBootTest
@Import(TestConfig.class)
class MyTests {
@Autowired
private MyService myService;
@Test
void testService() {
// myService가 TestConfig에 의해 설정됨
}
}
@SpringJUnitConfig
@SpringJUnitConfig는 @ExtendWith(SpringExtension.class)와 @ContextConfiguration을 조합한 애노테이션이다. JUnit 5와 함께 Spring 컨텍스트를 설정한다.
@SpringJUnitConfig(MyConfig.class)
class MyTests {
@Autowired
private MyService myService;
@Test
void testService() {
// 서비스 테스트 코드
}
}
@TestConfiguration
테스트 전용 @Configuration 클래스를 정의할 때 사용된다. 보통 내부 클래스로 정의되어, 테스트 범위 내에서만 사용된다. 테스트에 필요한 설정을 분리하여 정의할 때 사용된다.
@SpringBootTest
class MyTests {
@TestConfiguration
static class MyTestConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}
@Autowired
private MyService myService;
@Test
void testService() {
// 서비스 테스트 코드
}
}
@ActiveProfiles
@ActiveProfiles는 테스트 실행 시 활성화할 Spring 프로파일을 지정한다. 특정 프로파일에 따른 빈 설정을 테스트할 수 있다.
@ActiveProfiles("test")
@SpringBootTest
class MyTests {
@Autowired
private MyService myService;
@Test
void testService() {
// 서비스 테스트 코드
}
}
@Sql
테스트 메소드나 클래스가 실행되기 전에 SQL 스크립트를 실행하거나, 실행 후에 SQL 스크립트를 실행하도록 지정할 수 있다. 데이터베이스 초기화, 테스트 데이터 삽입, 또는 테스트 후 클린업 작업을 위해 사용한다.
@SpringBootTest
@Sql(scripts = "/test-schema.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
@Sql(scripts = "/test-data.sql", executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD)
public class SomeRepositoryTests {
@Autowired
private SomeRepository repository;
@Test
void testFindById() {
SomeEntity entity = repository.findById(1L).orElse(null);
assertNotNull(entity);
}
}
확장 애노테이션
@ExtendWith
JUnit5에서 확장 메커니즘을 제공하여, 테스트 클래스에 특정 동작을 추가할 수 있다. 특히, 스프링과의 통합을 위해 SpringExtension을 사용할 수 있다.
// JUnit5와 스프링을 통합하고 스프링의 의존성 주입을 테스트에서 사용할 수 있게 해준다.
@ExtendWith(SpringExtension.class)
public class SomeSpringTests {
@Autowired
private SomeService service;
@Test
void testService() {
assertNotNull(service);
}
}
// Mockito를 사용한 단위 테스트에 사용된다.
@ExtendWith(MockitoExtension.class)
public class SomeMockitoTests {
@Mock
private SomeService service;
@InjectMocks
private SomeController controller;
@Test
void testController() {
when(service.someMethod()).thenReturn("expected");
String response = controller.someEndpoint();
assertEquals("expected", response);
}
}
SpringBootTest와 @ExtendWith의 차이점
컨텍스트 로딩
@SpringBootTest는 전체 애플리케이션 컨텍스트를 로드한다.
이는 모든 빈을 초기화하고, 실제 애플리케이션처럼 동작하게 한다.
테스트 실행 속도
@SpringBootTest는 전체 컨텍스트를 로드하기 때문에 상대적으로 더 느리다.
@ExtendWith를 사용한 테스트는 필요한 부분만 로드하므로 더 빠르게 실행된다.
테스트 격리성
@SpringBootTest는 전체 컨텍스트를 로드하여 여러 테스트가 동일한 환경을 공유할 수 있다.
@ExtendWith는 필요한 빈만 로드하므로 테스트 간의 격리성이 높아진다.
@DirtiesContext
테스트 실행 후에 애플리케이션 컨텍스트를 "더럽혀진" 것으로 표시하고, 이후의 테스트에서 동일한 컨텍스트를 재사용하지 않도록 한다. 특정 테스트가 애플리케이션 컨텍스트의 상태를 변경하여 다른 테스트에 영향을 미칠 가능성이 있을 때 사용된다. 이 애노테이션을 사용하면, 해당 테스트가 실행된 후 컨텍스트가 재설정되거나 다시 로드된다.
테스트 클래스에 적용하면, 클래스 내의 모든 테스트가 실행된 후 컨텍스트가 더럽혀진 것으로 간주되고 특정 테스트 메소드에 적용하면, 해당 메소드가 실행된 후에만 컨텍스트가 더럽혀진 것으로 간주된다.
@SpringBootTest
class MyTests {
@Autowired
private MyService myService;
@Test
@DirtiesContext
void testThatModifiesContext() {
myService.modifyContext();
// 이 테스트가 끝난 후 컨텍스트는 더럽혀진 것으로 표시되고 다시 로드된다.
}
@Test
void anotherTest() {
// 이 테스트는 새로운 컨텍스트에서 실행된다.
}
}
기본 애노테이션
애노테이션 | 설명 |
@Test | 단순히 테스트 메소드임을 나타냄. JUnit은 이 애노테이션이 붙은 메소드를 테스트 메소드로 인식. |
@BeforeEach | 각 테스트 메소드가 실행되기 전에 실행. 주로 테스트 준비 작업에 사용. |
@AfterEach | 각 테스트 메소드가 실행된 후에 실행. 주로 테스트 준비 작업에 사용. |
@BeforeAll | 모든 테스트 메소드가 실행되기 전에 한 번 실행. 반드시 static 메소드여야 한다. |
@AfterAll | 모든 테스트 메소드가 실행된 후에 한 번 실행. 반드시 static 메소드여야 한다. |
@DisplayName | 테스트 클래스나 테스트 메소드의 이름을 커스텀할 수 있다. |
@Disabled | 특정 테스트 클래스나 메소드를 비활성화한다. |
조건부 애노테이션
애노테이션 | 설명 | 예 |
@EnabledOnOs | 특정 운영체제에서만 테스트를 실행한다. | @EnabledOnOs(OS.WINDOWS) |
@DisabledOnOs | 특정 운영체제에서 테스트를 비활성화한다. | @DisabledOnOs(OS.MAC) |
@EnabledOnJre | 특정 JRE 버전에서만 테스트를 실행한다. | @EnabledOnJre(JRE.JAVA_11) |
@DisabledOnJre | 특정 JRE 버전에서 테스트를 비활성화한다. | @DisabledOnJre(JRE.JAVA_8) |
@Enabledlf | SpEL(Spring Expression Language) 표현식을 사용하여 조건이 참일 때만 테스트를 실행한다. | @EnabledIf("2 * 3 == 6") |
@Disabledlf | SpEL(Spring Expression Language) 표현식을 사용하여 조건이 참일 때 비활성화한다. | @DisabledIf("'CI' == systemEnvironment['ENV']") |
테스트 메소드 순서 애노테이션
@TestMethodOrder
테스트 메소드 실행 순서를 지정할 수 있다. 클래스와 메소드에 적용할 수 있다.
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
class OrderedTests {
@Test
@Order(1)
void testA() {
// 테스트 코드
}
@Test
@Order(2)
void testB() {
// 테스트 코드
}
}
테스트 인스턴스 애노테이션
@TestInstance
테스트 클래스의 인스턴스 라이프사이클을 제어하는 데 사용된다. 이를 통해 테스트 인스턴스가 생성되고 파괴되는 시점을 결정할 수 있다. 기본적으로 JUnit 5는 각 테스트 메소드마다 새로운 인스턴스를 생성하지만, @TestInstance를 사용하면 클래스당 하나의 인스턴스를 사용할 수 있다.
옵션 | 설명 |
TestInstance.Lifecycle.PER_METHOD | 기본값이다. 각 테스트 메소드마다 새로운 테스트 인스턴스가 생성된다. @BeforeAll과 @AfterAll 메소드가 static이어야 합니다. 각 테스트가 독립적으로 실행되어야 하는 경우 유용합니다. |
TestInstance.Lifecycle.PER_CLASS | 클래스당 하나의 테스트 인스턴스가 생성된다. @BeforeAll과 @AfterAll 메소드를 static으로 정의할 필요가 없다. 테스트 메소드들이 상태를 공유할 수 있다. 클래스 수준에서 상태를 유지해야 하는 경우 사용한다. 인스턴스 생성 비용이 높을 때 인스턴스 재사용으로 성능 최적화할 수 있다. |
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class TestLifecycle {
@BeforeAll
void init() {
// 클래스당 하나의 인스턴스 초기화 코드
System.out.println("BeforeAll - 초기화 코드");
}
@Test
void testA() {
// 테스트 코드 A
System.out.println("testA 실행");
}
@Test
void testB() {
// 테스트 코드 B
System.out.println("testB 실행");
}
}
파라미터화 테스트 애노테이션
@ParameterizedTest
하나의 테스트 메서드를 여러 입력 값에 대해 반복 실행할 수 있게 해준다. 이를 통해 중복 코드를 줄이고 다양한 입력 값에 대한 테스트를 효율적으로 수행할 수 있다. 동일한 로직을 다양한 입력 값에 대해 검증하고자 할 때 사용한다.
@ValueSource
매개 변수화된 테스트에서 사용할 단일 값 소스를 제공하는 어노테이션이다. 지원되는 타입은 short, byte, int, long, float, double, char, boolean, String 등이 있다. 매개 변수화된 테스트에 단순한 리터럴 값(숫자, 문자열 등)을 제공할 때 사용한다.
public class ValueSourceTests {
@ParameterizedTest
@ValueSource(ints = {1, 2, 3, 4, 5})
void testWithValueSource(int argument) {
assertNotNull(argument);
}
}
@CsvSource
JUnit 5의 파라미터화된 테스트에서 사용되는 어노테이션 중 하나로, CSV(Comma-Separated Values) 형식의 데이터를 테스트 메서드에 제공할 수 있게 해준다. 이를 통해 복잡한 입력 값 세트를 간편하게 지정하고, 다양한 경우의 테스트를 효율적으로 수행할 수 있다. 원시 자료형만 가능하다.
public class CsvSourceTests {
@ParameterizedTest
@CsvSource({
"1, 2, 3",
"2, 3, 5",
"3, 5, 8"
})
void testAddition(int a, int b, int expected) {
assertEquals(expected, a + b);
}
@ParameterizedTest
@CsvSource({
"racecar, true",
"radar, true",
"hello, false"
})
void testIsPalindrome(String candidate, boolean expected) {
assertEquals(expected, isPalindrome(candidate));
}
boolean isPalindrome(String text) {
return new StringBuilder(text).reverse().toString().equals(text);
}
@ParameterizedTest
@CsvSource(value = {
"1; 2; 3",
"2; 3; 5",
"3; 5; 8"
}, delimiter = ';')
void testAdditionWithCustomDelimiter(int a, int b, int expected) {
assertEquals(expected, a + b);
}
}
@MethodSource
테스트 메서드에 제공할 매개 변수들을 외부 메서드에서 생성하여 제공할 수 있게 해준다. 이를 통해 복잡한 테스트 데이터를 동적으로 생성하거나 다양한 형식의 데이터를 사용할 수 있다.
public class MethodSourceTests {
@ParameterizedTest
@MethodSource("provideStringsForIsBlank")
void testIsBlank(String input) {
assertTrue(input == null || input.trim().isEmpty());
}
static Stream<String> provideStringsForIsBlank() {
return Stream.of(null, "", " ");
}
@ParameterizedTest
@MethodSource("provideArgumentsForAddition")
void testAddition(int a, int b, int expected) {
assertEquals(expected, a + b);
}
static Stream<Arguments> provideArgumentsForAddition() {
return Stream.of(
Arguments.of(1, 2, 3),
Arguments.of(2, 3, 5),
Arguments.of(3, 5, 8)
);
}
@ParameterizedTest
@MethodSource("provideListForTesting")
void testList(List<Integer> numbers, int expectedSum) {
int sum = numbers.stream().mapToInt(Integer::intValue).sum();
assertEquals(expectedSum, sum);
}
static Stream<Arguments> provideListForTesting() {
return Stream.of(
Arguments.of(Arrays.asList(1, 2, 3), 6),
Arguments.of(Arrays.asList(4, 5, 6), 15),
Arguments.of(Arrays.asList(7, 8, 9), 24)
);
}
}
@EnumSource
특정 열거형(enum) 타입의 모든 상수나 일부 상수를 테스트 메서드에 제공할 수 있게 해준다. 이를 통해 enum 타입의 다양한 경우를 테스트할 수 있다.
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import static org.junit.jupiter.api.Assertions.*;
public class EnumSourceTests {
enum Color {
RED, GREEN, BLUE, YELLOW
}
/**
* 테스트: 모든 Color 열거형 값을 테스트한다.
*/
@ParameterizedTest
@EnumSource(Color.class)
void testAllColors(Color color) {
assertNotNull(color);
}
/**
* 테스트: RED 및 BLUE만을 테스트한다.
*/
@ParameterizedTest
@EnumSource(value = Color.class, names = {"RED", "BLUE"})
void testSpecificColors(Color color) {
assertTrue(color == Color.RED || color == Color.BLUE);
}
/**
* 테스트: GREEN을 제외한 모든 색을 테스트한다.
*/
@ParameterizedTest
@EnumSource(value = Color.class, names = "GREEN", mode = EnumSource.Mode.EXCLUDE)
void testExcludeColors(Color color) {
assertFalse(color == Color.GREEN);
}
/**
* 테스트: 이름이 ".*LLOW"와 일치하는 색을 테스트한다 (예: YELLOW).
*/
@ParameterizedTest
@EnumSource(value = Color.class, names = ".*LLOW", mode = EnumSource.Mode.MATCH_ALL)
void testRegexColors(Color color) {
assertTrue(color == Color.YELLOW);
}
}
Mockito 애노테이션
@Mock
목 객체를 생성한다. 목 객체는 실제 객체를 대신하여 테스트에 사용된다.
@ExtendWith(MockitoExtension.class)
public class MyTests {
@Mock
private MyService myService;
@Test
void testService() {
when(myService.doSomething()).thenReturn("Mocked Result");
assertEquals("Mocked Result", myService.doSomething());
}
}
@InjectMocks
Mockito 라이브러리를 사용하여 모의 객체(mock)를 주입할 실제 객체를 생성한다. 이를 통해 의존성을 자동으로 주입받을 수 있다. 테스트 대상 객체를 생성하고, 해당 객체의 의존성을 목(mock)으로 주입하고자 할 때 사용한다.
@ExtendWith(MockitoExtension.class)
public class MyTests {
@Mock
private MyRepository myRepository;
@InjectMocks
private MyService myService;
@Test
void testService() {
when(myRepository.find()).thenReturn("Mocked Data");
assertEquals("Mocked Data", myService.getData());
}
}
@Spy
실제 객체를 생성하되, 일부 메소드만 목킹한다. 실 객체의 동작을 유지하면서 특정 메소드의 동작을 변경할 때 사용한다.
@ExtendWith(MockitoExtension.class)
public class MyTests {
@Spy
private MyService myService;
@Test
void testService() {
doReturn("Mocked Result").when(myService).doSomething();
assertEquals("Mocked Result", myService.doSomething());
}
}
@Captor
인자 캡처를 위한 목 캡처 객체를 생성한다. 목 객체의 메소드가 호출될 때 전달된 인자를 캡처하여 확인할 때 사용한다.
@ExtendWith(MockitoExtension.class)
public class MyTests {
@Mock
private MyService myService;
@Captor
private ArgumentCaptor<String> captor;
@Test
void testService() {
myService.doSomething("Argument");
verify(myService).doSomething(captor.capture());
assertEquals("Argument", captor.getValue());
}
}
@MockBean
스프링 컨텍스트에서 특정 빈을 목 객체로 대체하여, 통합 테스트 시 다른 빈이 목 객체를 사용하도록 한다.
@SpringBootTest
public class MyTests {
@MockBean
private MyService myService;
@Test
void testService() {
when(myService.doSomething()).thenReturn("Mocked Result");
assertEquals("Mocked Result", myService.doSomething());
}
}
@SpyBean
스프링 컨텍스트에서 특정 빈을 스파이 객체로 대체하여, 통합 테스트 시 다른 빈이 스파이 객체를 사용하도록 한다.
@SpringBootTest
public class MyTests {
@SpyBean
private MyService myService;
@Test
void testService() {
doReturn("Mocked Result").when(myService).doSomething();
assertEquals("Mocked Result", myService.doSomething());
}
}
'JVM' 카테고리의 다른 글
[JVM] MockServer (0) | 2024.07.29 |
---|---|
BDDMockito (0) | 2024.07.28 |
테스트 커버리지와 JaCoCo (0) | 2024.07.27 |
StringBuffer vs StringBuilder (0) | 2024.07.22 |
java.security.invalidKeyException: Illegal Key Size (0) | 2024.05.04 |