junit5은 TDD방식으로 개발하는데 있어서 견고하고 탄탄한 구조의 코드를 작성할 수 있게 해준다. 기본적으로 @Test 어노테이션으로 테스트하는데 추가 조건으로 아래 조건을 주어서 테스트가 되고
여러 입력값에 대해 동일한 파라미터화된 테스트도 지원이 된다.
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.*;
public class ParameterizedTestExample {
@ParameterizedTest
@CsvSource({
"1, 1, 2",
"2, 3, 5",
"3, 5, 8"
})
public void testAddition(int a, int b, int expectedResult) {
Calculator calculator = new Calculator();
assertEquals(expectedResult, calculator.add(a, b));
}
public class Calculator {
public int add(int a, int b) {
return a + b;
}
}
}
이것을 정리하면 아래와 같다.
- Assertions: 테스트의 결과를 검증하기 위해 사용됩니다. assertEquals, assertTrue, assertFalse, assertThrows 등 다양한 메서드를 제공합니다.
- Test Lifecycle: @BeforeEach, @AfterEach, @BeforeAll, @AfterAll 어노테이션을 통해 테스트 실행 전후에 필요한 설정 및 정리 작업을 수행할 수 있습니다.
- Parameterized Tests: 여러 입력 값에 대해 동일한 테스트를 반복 실행할 수 있습니다.
- Conditional Test Execution: 특정 조건에 따라 테스트 실행을 제어할 수 있습니다. 예를 들어, @EnabledOnOs 어노테이션을 사용하여 특정 운영 체제에서만 테스트를 실행할 수 있습니다.
- Nested Tests: 테스트 클래스 내에 중첩된 테스트 클래스를 정의하여 테스트 그룹을 만들 수 있습니다.
Logback은 SLF4J(Simple Logging Facade for Java)의 후속 프로젝트로 개발되어서 SLF4J와 Logback은 부트에서 기본 탑재되었고 동기성 로깅에 좋고, Log4j2는 비동기성 로깅에 좋다. 조건부 로깅, 고급 필터링, 플러그인 시스템 등 다양한 기능을 제공하고, 로그 이벤트를 큐(Queue)를 통해 비동기로 처리하여 애플리케이션 성능에 미치는 영향을 최소화한다.
log4j2.xml
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"/>
</Console>
<File name="File" fileName="logs/app.log" append="true">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"/>
</File>
</Appenders>
<Loggers>
<Root level="debug">
<AppenderRef ref="Console"/>
<AppenderRef ref="File"/>
</Root>
</Loggers>
</Configuration>
package com.example.demo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@RestController
class LoggingController {
private static final Logger logger = LoggerFactory.getLogger(LoggingController.class);
@GetMapping("/")
public String index() {
logger.debug("Debug log message");
logger.info("Info log message");
logger.warn("Warn log message");
logger.error("Error log message");
return "Hello, World!";
}
}
Log4j2는 고성능 비동기 로깅을 지원하여 성능이 중요한 애플리케이션에 적합하고, xml,json,yaml 파일 등으로 프로퍼티 설정 방법이 다양하다
기본탑재되는 Logback은 동기로깅인데 아래와 같은 즉시처리, 차단작업, 단순성의 특징이 있고 대량 로그는 성능저하 우려가 있다.
사용법은 아래와 같다.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SynchronousLoggingExample {
private static final Logger logger = LoggerFactory.getLogger(SynchronousLoggingExample.class);
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
logger.info("Synchronous log message " + i);
}
}
}
log4j2의 비동기 로깅은 버퍼링을 활용하여 대량 로그에서도 높은 성능을 기대할 수 있다.
log4j2.xml 설정 파일
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"/>
</Console>
<File name="File" fileName="logs/app.log" append="true">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p %c{1}:%L - %m%n"/>
</File>
</Appenders>
<AsyncLogger name="AsyncLogger" level="debug">
<AppenderRef ref="Console"/>
<AppenderRef ref="File"/>
</AsyncLogger>
<Loggers>
<Root level="debug">
<AppenderRef ref="AsyncLogger"/>
</Root>
</Loggers>
</Configuration>
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class AsynchronousLoggingExample {
private static final Logger logger = LoggerFactory.getLogger(AsynchronousLoggingExample.class);
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
logger.info("Asynchronous log message " + i);
}
}
}
동기로깅과 비동기로깅은 사용하는 시스템에 따라서 다르므로 신뢰성, 데이터 처리량 등을 고려하여 사용하면 좋다.
보통 밸리데이션 유효성 검사를 할 모델 클래스에 JSR-303 어노테이션을 추가하고, 대표적인 어노테이션으로는 @NotNull, @Size, @Email, @Pattern 등이 있다. null 관련된 것과 email, pattern 그리고 size 의 인덱스 체크를 하는 것이다.
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Size;
public class User {
@NotEmpty(message = "Name is required")
@Size(min = 2, max = 30, message = "Name must be between 2 and 30 characters")
private String name;
@NotEmpty(message = "Email is required")
@Email(message = "Email should be valid")
private String email;
@NotEmpty(message = "Password is required")
@Size(min = 6, message = "Password must be at least 6 characters")
private String password;
// Getters and setters
}
@Valid 어노테이션을 활용하면 DAO를 간단하게 유효성 체크를 할 수 있다.
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import javax.validation.Valid;
@Controller
public class UserController {
@GetMapping("/register")
public String showRegistrationForm(Model model) {
model.addAttribute("user", new User());
return "register";
}
@PostMapping("/register")
public String registerUser(@Valid @ModelAttribute("user") User user, BindingResult result, Model model) {
if (result.hasErrors()) {
return "register";
}
// 유효성 검사를 통과하면 사용자 저장 등의 로직 수행
model.addAttribute("message", "User registered successfully");
return "success";
}
}
타임리프에서 아래와 같이 체크해서 errors 에러메시지를 보여줄수 있다.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Registration Form</title>
</head>
<body>
<h1>Register</h1>
<form th:action="@{/register}" th:object="${user}" method="post">
<div>
<label>Name</label>
<input type="text" th:field="*{name}"/>
<div th:if="${#fields.hasErrors('name')}" th:errors="*{name}">Name Error</div>
</div>
<div>
<label>Email</label>
<input type="email" th:field="*{email}"/>
<div th:if="${#fields.hasErrors('email')}" th:errors="*{email}">Email Error</div>
</div>
<div>
<label>Password</label>
<input type="password" th:field="*{password}"/>
<div th:if="${#fields.hasErrors('password')}" th:errors="*{password}">Password Error</div>
</div>
<div>
<button type="submit">Register</button>
</div>
</form>
<div th:if="${message}" th:text="${message}"></div>
</body>
</html>
사용자정의 Validator를 만들수도 있는데
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
@Component
public class UserValidator implements Validator {
@Override
public boolean supports(Class<?> clazz) {
return User.class.equals(clazz);
}
@Override
public void validate(Object target, Errors errors) {
User user = (User) target;
if (user.getPassword().length() < 6) {
errors.rejectValue("password", "password.tooShort", "Password must be at least 6 characters");
}
// 추가적인 사용자 정의 유효성 검사 로직
}
}
BindingResult 객체를 사용해서 아래와 같이 추가해서 사용한다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.InitBinder;
// ...
@Autowired
private UserValidator userValidator;
@InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(userValidator);
}
마지막으로 인터셉터에 대해 살펴보면
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 요청 전 처리 로직
System.out.println("Pre Handle method is Calling");
return true; // true를 반환하면 요청을 계속 처리하고, false를 반환하면 요청을 중단합니다.
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 요청 후 처리 로직
System.out.println("Post Handle method is Calling");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 요청 완료 후 처리 로직
System.out.println("Request and Response is completed");
}
}
- preHandle(HttpServletRequest request, HttpServletResponse response, Object handler): 컨트롤러 실행 전에 호출됩니다.
- postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView): 컨트롤러 실행 후, 뷰 렌더링 전에 호출됩니다.
- afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex): 뷰 렌더링 후에 호출됩니다.
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
public class MyInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 요청 전 처리 로직
System.out.println("Pre Handle method is Calling");
return true; // true를 반환하면 요청을 계속 처리하고, false를 반환하면 요청을 중단합니다.
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// 요청 후 처리 로직
System.out.println("Post Handle method is Calling");
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 요청 완료 후 처리 로직
System.out.println("Request and Response is completed");
}
}
또한 인터셉터를 스프링 MVC 설정에 등록하여 특정 URL 패턴에 대해 적용할 수 있다. 이는 WebMvcConfigurer를 구현하여 설정할 수 있다.
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private MyInterceptor myInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(myInterceptor)
.addPathPatterns("/**") // 모든 경로에 대해 인터셉터 적용
.excludePathPatterns("/login", "/register"); // 특정 경로는 제외
}
}
인터셉터가 곰탕에 소금같은 이유는 인증인터셉터를 보면 사용자가 로그인 하지 않은 상태에서 보호된 리소스에 접근하려 할 때, 로그인 페이지로 리다이렉트 하는 인터셉터를 작성할수 있기 때문이다.
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession(false);
if (session == null || session.getAttribute("user") == null) {
response.sendRedirect("/login");
return false;
}
return true;
}
}
또한 요청 및 응답정보를 로깅하는 인터셉터 또한 곰탕의 소금처럼 좋다.
public class LoggingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("Request URL: " + request.getRequestURL());
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("Response Status: " + response.getStatus());
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("Request Completed");
}
}
HandlerInterceptor 인터페이스를 구현하여 인터셉터를 작성하고, WebMvcConfigurer를 구현하여 인터셉터를 등록하고 URL 패턴에 대해 적용한다.
'4차산업혁명의 일꾼 > Java&Spring웹개발과 서버 컴퓨터' 카테고리의 다른 글
스프링 버전 4와 5의 차이 정리 (2) | 2024.06.18 |
---|---|
OAuth와 SSR (2) | 2024.06.18 |
java 파일 다운로드/ 엑셀및 pdf 그리고 소켓통신 그리고 JSON 복습 (0) | 2024.06.17 |
javascript/jsp및 프론트 복습 (0) | 2024.06.17 |
웹개발의 추억을 반추하며 스프링 MVC 기초공사 복습과 스프링 Security (0) | 2024.06.17 |