예시
RegexTest
package shop.mtcoding.blog.temp;
import org.junit.jupiter.api.Test;
import java.util.regex.Pattern;
// https://regex101.com
public class RegexTest {
@Test
public void 한글만된다_test() {
String value = "ㅏㅏㅑㅇㅎㄹ";
boolean result = Pattern.matches("^[가-힣ㄱ-ㅎㅏ-ㅣ]+$", value);
System.out.println("테스트 : " + result);
}
@Test
public void 한글은안된다_test() throws Exception {
String value = "$86..ssa";
// String value = "$86..ssa";
boolean result = Pattern.matches("^[^가-힣ㄱ-ㅎㅏ-ㅣ]+$", value);
System.out.println("테스트 : " + result);
}
@Test
public void 영어만된다_test() throws Exception {
String value = "ssar#";
// String value = "ssar2";
boolean result = Pattern.matches("^[a-zA-Z]+$", value);
System.out.println("테스트 : " + result);
}
@Test
public void 영어는안된다_test() throws Exception {
String value = "SAss";
// String value = "ssar";
boolean result = Pattern.matches("^[^a-zA-Z]+$", value);
System.out.println("테스트 : " + result);
}
@Test
public void 영어와숫자만된다_test() throws Exception {
String value = "ssar2@";
// String value = "ssar2&";
// String value = "ssar한글";
boolean result = Pattern.matches("^[a-zA-Z0-9]+$", value);
System.out.println("테스트 : " + result);
}
@Test
public void 영어만되고_길이는최소2최대4이다_test() throws Exception {
String value = "ssar";
// String value = "ssarm";
boolean result = Pattern.matches("^[a-zA-Z]{2,4}$", value);
System.out.println("테스트 : " + result);
}
// 소문자, 대문자, 숫자, 특수문자가 포함되어야 하고, 최소 2자부터 20자 사이여야 한다
@Test
public void password_test() {
String password = "2#Ed ";
boolean result = Pattern.matches("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[!@#$%^&*()\\-_=+\\[\\]{}|;:'\",.<>?/`~\\\\])[a-zA-Z0-9!@#$%^&*()\\-_=+\\[\\]{}|;:'\",.<>?/`~\\\\]{2,20}$", password);
System.out.println("테스트 : " + result);
}
@Test
public void user_username_test() throws Exception {
String username = "";
// String username = "ssa^";
boolean result = Pattern.matches("^[a-zA-Z0-9]{2,20}$", username);
System.out.println("테스트 : " + result);
}
@Test
public void user_email_test() throws Exception {
String email = "s...sd@co.kr";
// String username = "@fGf.ccm"; // +를 *로 변경해보기
boolean result = Pattern.matches("^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\\.[a-zA-Z]{2,3}$", email);
System.out.println("테스트 : " + result);
}
@Test
public void user_fullname_test() throws Exception {
String fullname = "코스";
// String fullname = "코스ss1";
boolean result = Pattern.matches("^[a-zA-Z가-힣]{1,20}$", fullname);
System.out.println("테스트 : " + result);
}
@Test
public void account_gubun_test() throws Exception {
String gubun = "TRANSFER"; // WITHDRAW(8), DEPOSIT(7), TRANSFER(8)
boolean result = Pattern.matches("^(WITHDRAW|DEPOSIT|TRANSFER)$", gubun);
System.out.println("테스트 : " + result);
}
@Test
public void account_gubun_test2() throws Exception {
String gubun = "TRANSFER"; // WITHDRAW(8), DEPOSIT(7), TRANSFER(8)
boolean result = Pattern.matches("^(TRANSFER)$", gubun);
System.out.println("테스트 : " + result);
}
@Test
public void account_tel_test() throws Exception {
String tel = "0102227777";
boolean result = Pattern.matches("^[0-9]{3}[0-9]{4}[0-9]{4}$", tel);
System.out.println("테스트 : " + result);
}
}
UserController
package shop.mtcoding.blog.user;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import shop.mtcoding.blog._core.error.ex.Exception400;
import shop.mtcoding.blog._core.util.Resp;
import java.util.Map;
import java.util.regex.Pattern;
@RequiredArgsConstructor
@Controller
public class UserController {
private final UserService userService;
private final HttpSession session;
@GetMapping("/join-form")
public String joinForm() {
return "user/join-form";
}
@PostMapping("/join")
public String join(UserRequest.JoinDTO joinDTO) {
// 유효성 검사
boolean r1 = Pattern.matches("^[a-zA-Z0-9]{2,20}$", joinDTO.getUsername());
boolean r2 = Pattern.matches("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[!@#$%^&*()])[a-zA-Z\\d!@#$%^&*()]{6,20}$", joinDTO.getPassword());
boolean r3 = Pattern.matches("^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\\.[a-zA-Z]{2,3}$", joinDTO.getEmail());
if (!r1) throw new Exception400("유저네임은 2-20자이며, 특수문자,한글이 포함될 수 없습니다");
if (!r2) throw new Exception400("패스워드는 4-20자이며, 특수문자,영어 대문자,소문자, 숫자가 포함되어야 하며, 공백이 있을 수 없습니다");
if (!r3) throw new Exception400("이메일 형식에 맞게 적어주세요");
userService.회원가입(joinDTO);
return "redirect:/login-form";
}
@GetMapping("/api/check-username-available/{username}")
public @ResponseBody Resp<?> checkUsernameAvailable(@PathVariable("username") String username) {
Map<String, Object> dto = userService.유저네임중복체크(username);
return Resp.ok(dto);
}
@GetMapping("/login-form")
public String loginForm() {
return "user/login-form";
}
@PostMapping("/login")
public String login(UserRequest.LoginDTO loginDTO, HttpServletResponse response) {
User sessionUser = userService.로그인(loginDTO);
session.setAttribute("sessionUser", sessionUser);
if (loginDTO.getRememberMe() == null) {
Cookie cookie = new Cookie("username", null);
cookie.setMaxAge(0); // 즉시 삭제
response.addCookie(cookie);
} else {
Cookie cookie = new Cookie("username", loginDTO.getUsername());
cookie.setMaxAge(60 * 60 * 24 * 7); // cookie 하루동안 유지
response.addCookie(cookie);
}
return "redirect:/";
}
@GetMapping("/logout")
public String logout() {
session.invalidate();
return "redirect:/login-form";
}
@GetMapping("/user/update-form")
public String updateForm() {
// ViewResolver -> prefix = /templates/ suffix = .mustache
return "user/update-form";
}
@PostMapping("/user/update")
public String update(UserRequest.UpdateDTO updateDTO) {
User sessionUser = (User) session.getAttribute("sessionUser");
User user = userService.회원정보수정(updateDTO, sessionUser.getId());
// session 동기화 -> 동기화 안해주면 바꾸기 전 정보를 보게 된다
session.setAttribute("sessionUser", user);
return "redirect:/";
}
}
GlobalValidationHandler
package shop.mtcoding.blog._core.error;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.FieldError;
import shop.mtcoding.blog._core.error.ex.Exception400;
import java.util.List;
// Aspect, PointCut, Advice
@Aspect // 관점관리
@Component
public class GlobalValidationHandler {
// 관심사를 분리시킴 -> AOP
// @Before: PostMapping,PutMapping 이 들어있는 메서드를 실행하기 직전에 Advice 를 호출하라
@Before("@annotation(org.springframework.web.bind.annotation.PostMapping) || @annotation(org.springframework.web.bind.annotation.PutMapping)")
// pointcut
public void badRequestAdvice(JoinPoint jp) { // 대리인 / jp는 실제 실행될 메서드의 모든 것을 투영하고 있다(리플렉션)
Object[] args = jp.getArgs(); // 메서드의 매개변수들 (매개변수의 갯수와 상관없이 배열로 return)
for (Object arg : args) { // 매개변수의 갯수만큼 반복 (annotation은 매개변수의 갯수에 포함되지 않는다)
// Errors 타입이 매개변수에 존재하고,
if (arg instanceof Errors) { // instanceof -> 타입검증 (다형성을 만족한다)
System.out.println("error 400 처리 필요함");
Errors errors = (Errors) arg;
// 실제 에러가 존재한다면
// 공통모듈 -> 관심사 분리
if (errors.hasErrors()) {
List<FieldError> fErrors = errors.getFieldErrors();
for (FieldError fieldError : fErrors) {
throw new Exception400(fieldError.getField() + ":" + fieldError.getDefaultMessage());
}
}
}
}
}
}
UserRequest
validation annotation
✅ 주요 Validation Annotation 목록
어노테이션 | 설명 |
@NotNull | 값이 null 이면 안 됨 (빈 문자열은 허용, "" 불가, " " 가능) |
@Null | null 값만 허용 (특정 상황에서 사용, 예: 업데이트에서는 금지) |
@NotEmpty | null 또는 빈 문자열/컬렉션이면 안 됨 |
@NotBlank | 문자열이 null , 빈 문자열, 공백만 있는 경우 허용 안 됨 |
@Email | 이메일 형식인지 확인 |
@Size(min=, max=) | 문자열, 배열, 컬렉션 등의 크기 제한 |
@Min(value) | 숫자 최소값 제한 |
@Max(value) | 숫자 최대값 제한 |
@Positive / @PositiveOrZero | 양수 / 0 포함 양수만 허용 |
@Negative / @NegativeOrZero | 음수 / 0 포함 음수만 허용 |
@Pattern(regexp="...") | 정규표현식으로 문자열 패턴 검증 |
@AssertTrue / @AssertFalse | 해당 값이 true / false 여야 함 |
@Digits(integer=, fraction=) | 정수 자리수와 소수점 자리수 제한 |
package shop.mtcoding.blog.user;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
public class UserRequest {
//insert 용도의 dto에는 toEntity 메서드를 만든다
@Data
public static class JoinDTO {
@Pattern(regexp = "^[a-zA-Z0-9]{2,20}$", message = "유저네임은 2-20자이며, 특수문자,한글이 포함될 수 없습니다")
private String username;
@Size(min = 4, max = 20)
private String password;
@Pattern(regexp = "^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\\.[a-zA-Z]{2,3}$", message = "이메일 형식으로 적어주세요")
private String email;
public User toEntity() {
return User.builder()
.username(username)
.password(password)
.email(email)
.build();
}
}
@Data
public static class LoginDTO {
@Pattern(regexp = "^[a-zA-Z0-9]{2,20}$", message = "유저네임은 2-20자이며, 특수문자,한글이 포함될 수 없습니다")
private String username;
@Size(min = 4, max = 20)
private String password;
private String rememberMe; // check되면 on 안되면 null
}
@Data
public static class UpdateDTO {
private String password;
private String email;
}
}
UserController
package shop.mtcoding.blog.user;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import shop.mtcoding.blog._core.util.Resp;
import java.util.Map;
@RequiredArgsConstructor
@Controller
public class UserController {
private final UserService userService;
private final HttpSession session;
@GetMapping("/join-form")
public String joinForm() {
return "user/join-form";
}
@PostMapping("/join")
public String join(@Valid UserRequest.JoinDTO joinDTO, Errors errors) { // @Valid 가 붙으면 invoke 하기 전에 리플렉션 타고 DTO 내부의 어노테이션을 확인해서 터지면 errors에 넣어준다
// 부가로직 (유효성 검사) - 공통모듈 / 공통모듈화가 되어야지 함수화해서 재활용 가능
// 유효성 검사
// boolean r1 = Pattern.matches("^[a-zA-Z0-9]{2,20}$", joinDTO.getUsername());
// boolean r2 = Pattern.matches("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[!@#$%^&*()])[a-zA-Z\\d!@#$%^&*()]{6,20}$", joinDTO.getPassword());
// boolean r3 = Pattern.matches("^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\\.[a-zA-Z]{2,3}$", joinDTO.getEmail());
//
// if (!r1) throw new Exception400("유저네임은 2-20자이며, 특수문자,한글이 포함될 수 없습니다");
// if (!r2) throw new Exception400("패스워드는 4-20자이며, 특수문자,영어 대문자,소문자, 숫자가 포함되어야 하며, 공백이 있을 수 없습니다");
// if (!r3) throw new Exception400("이메일 형식에 맞게 적어주세요");
userService.회원가입(joinDTO);
return "redirect:/login-form";
}
@GetMapping("/api/check-username-available/{username}")
public @ResponseBody Resp<?> checkUsernameAvailable(@PathVariable("username") String username) {
Map<String, Object> dto = userService.유저네임중복체크(username);
return Resp.ok(dto);
}
@GetMapping("/login-form")
public String loginForm() {
return "user/login-form";
}
@PostMapping("/login")
public String login(@Valid UserRequest.LoginDTO loginDTO, Errors errors, HttpServletResponse response) { // 어노테이션 붙은 매개변수 옆에 Errors가 있어야 DTO에서 터지면 errors에 넣어줌. 그 사이에 다른 매개변수가 있으면 그 매개변수를 확인하기 때문에 자리 잘 확인하기
// 부가로직(유효성 검사) - 공통모듈 / 공통모듈화가 되어야지 함수화해서 재활용 가능
// 핵심로직 -> seesionUser ~ cookie
User sessionUser = userService.로그인(loginDTO);
session.setAttribute("sessionUser", sessionUser);
if (loginDTO.getRememberMe() == null) {
Cookie cookie = new Cookie("username", null);
cookie.setMaxAge(0); // 즉시 삭제
response.addCookie(cookie);
} else {
Cookie cookie = new Cookie("username", loginDTO.getUsername());
cookie.setMaxAge(60 * 60 * 24 * 7); // cookie 하루동안 유지
response.addCookie(cookie);
}
return "redirect:/";
}
@GetMapping("/logout")
public String logout() {
session.invalidate();
return "redirect:/login-form";
}
@GetMapping("/user/update-form")
public String updateForm() {
// ViewResolver -> prefix = /templates/ suffix = .mustache
return "user/update-form";
}
@PostMapping("/user/update")
public String update(@Valid UserRequest.UpdateDTO updateDTO, Errors errors) {
User sessionUser = (User) session.getAttribute("sessionUser");
User user = userService.회원정보수정(updateDTO, sessionUser.getId());
// session 동기화 -> 동기화 안해주면 바꾸기 전 정보를 보게 된다
session.setAttribute("sessionUser", user);
return "redirect:/";
}
}

❗
@Valid
가 붙으면 invoke
하기 전에 리플렉션 타고 DTO 내부의 Annotation
을 확인해서 터지면 errors에 넣어준다
❗
Annotation
붙은 매개변수 옆에 Errors
가 있어야 DTO에서 터지면 errors에 넣어줌그 사이에 다른 매개변수가 있으면 그 매개변수를 확인하기 때문에 자리 잘 확인하기
핵심로직
UserController - Join
userService.회원가입(joinDTO);
return "redirect:/login-form";
UserController - Login
// 핵심로직 -> seesionUser ~ cookie
User sessionUser = userService.로그인(loginDTO);
session.setAttribute("sessionUser", sessionUser);
if (loginDTO.getRememberMe() == null) {
Cookie cookie = new Cookie("username", null);
cookie.setMaxAge(0); // 즉시 삭제
response.addCookie(cookie);
} else {
Cookie cookie = new Cookie("username", loginDTO.getUsername());
cookie.setMaxAge(60 * 60 * 24 * 7); // cookie 하루동안 유지
response.addCookie(cookie);
}
return "redirect:/";
부가로직
- 공통 모듈 - 분리하려면 공통 모듈이 되어야 한다
- 공통 모듈화가 되어야지 함수화해서 재활용 가능
UserController - Join
// 부가로직 (유효성 검사) -
if (errors.hasErrors()) {
List<FieldError> fErrors = errors.getFieldErrors();
for (FieldError fieldError : fErrors) {
throw new Exception400(fieldError.getField() + ":" + fieldError.getDefaultMessage());
}
}
UserController - Login
// 부가로직 (유효성 검사) - 공통모듈 / 공통모듈화가 되어야지 함수화해서 재활용 가능
if (errors.hasErrors()) {
List<FieldError> fErrors = errors.getFieldErrors();
for (FieldError fieldError : fErrors) {
throw new Exception400(fieldError.getField() + ":" + fieldError.getDefaultMessage());
}
}
AOP
프로그래밍 패러다임의 하나로, 관심사를 분리하여 코드의 모듈화를 개선하기 위한 기법
코드의 가독성과 유지 보수성을 높일 수 있다.
리플렉션을 통해 우리는 깃발(어노테이션)을 원하는 위치에 설정(Pointcut)하고, 그 깃발이 있는 코드가 실행될 때 수행할 공통기능(Advice)을 적용할 수 있다
핵심로직을 실행하기 전에 부가로직을 호출하고 접근 → 프록시패턴 (Proxy Pattern)
프록시패턴 (ex. 대리인)
DS → Proxy → Controller
Proxy에서 validation check
관점지향 프로그램 → 목적은 같으나 관점이 다르다 , 목적의 행위에 따라 관점이 달라질 수 있음, 관점 따라 다른 실행
핵심로직 - 옷을 입는다 / 부가로직 - 어떻게 옷을 입나
핵심로직과 부가로직을 분리시켜야 하는데, 분리시키려면 부가로직을 공통모듈로 만들어야한다 → 리플렉션으로 구현하면 부가로직을 공통 모듈로 만드는 게 가능
공통모듈화가 되면 Advice로 뺄 수 있다
<AOP의 핵심개념>
- Aspect → 관점
- Advice → 공통모듈이 들어가있는 행위
- Pointcut → 어디에서 실행할 지 결정 (Advice를 어디다가 꽂을건지?)
GlobalValidationHandler
package shop.mtcoding.blog._core.error;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class GlobalValidationHandler {
// 직전
@Before("@annotation(shop.mtcoding.blog._core.error.anno.MyBefore)")
public void beforeAdvice(JoinPoint jp) { // 리플렉션된 메서드의 정보가 jp에 담김
String name = jp.getSignature().getName();
System.out.println("Before Advice : " + name);
}
// 직후
@After("@annotation(shop.mtcoding.blog._core.error.anno.MyAfter)")
public void afterAdvice(JoinPoint jp) {
String name = jp.getSignature().getName();
System.out.println("After Advice : " + name);
}
// 앞뒤 (프록시)
@Around("@annotation(shop.mtcoding.blog._core.error.anno.MyAround)")
public Object aroundAdvice(ProceedingJoinPoint jp) {
//before
String name = jp.getSignature().getName();
System.out.println("Around Advice 직전 : " + name);
try {
//after
Object result = jp.proceed(); // 컨트롤러 함수가 호출됨. 타입을 알수없기 때문에 Object 타입
System.out.println("Around Advice 직후 : " + name);
System.out.println("result 값 : " + result);
return result;
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}
- Pointcut →
@Before("@annotation(shop.mtcoding.blog._core.error.anno.MyBefore)")
- Advice → method 내부
- Aspect → 관점 분리 (GlobalValidationHandler -> 관점에 따라 다르게 행동할 Object)
4.3 정규표현식을 사용해서 어노테이션 찾고 값 주입하기
import java.util.stream.IntStream;
@Aspect
@Component
public class LoginAdvice {
@Around("execution(* shop.mtcoding.aopstudy.controller.*.*(..))")
public Object loginUserAdviceAround(ProceedingJoinPoint jp) throws Throwable {
MethodSignature signature = (MethodSignature) jp.getSignature();
Method method = signature.getMethod();
Object[] args = jp.getArgs();
Parameter[] parameters = method.getParameters();
int loginUserAopIndex = IntStream.range(0, parameters.length)
.filter(i -> parameters[i].isAnnotationPresent(LoginUserAop.class))
.findFirst()
.orElse(-1);
if (loginUserAopIndex >= 0) {
HttpServletRequest req = ((ServletRequestAttributes)
RequestContextHolder.getRequestAttributes()).getRequest();
HttpSession session = req.getSession();
User principal = (User) session.getAttribute("loginUser");
args[loginUserAopIndex] = principal;
// 해당 메서드 실행
Object result = jp.proceed(args);
// 실행 후 결과 값을 가지고, 로그를 남길 수 있음. (실재 로그는 log4j 사용해야함)
System.out.println(result);
return result;
}
return jp.proceed();
}
}
AOP에서 Before, After, Around는 각각 다른 시점에서 AOP 어드바이스를 실행하는 방법입니다.
- @Before: 메서드 실행 전에 어드바이스를 실행합니다. 이 시점에서 JoinPoint 객체를 통해 메서드의 인자, 클래스, 메서드 이름 등의 정보를 가져올 수 있습니다. @Before 어노테이션을 사용하여 선언합니다.
- @After : 메서드 실행 후에 어드바이스를 실행합니다. 이 시점에서 JoinPoint 객체를 사용할 수 있으며, 메서드의 실행 결과나 예외 정보 등을 가져올 수 있습니다. @After 어노테이션을 사용하여 선언합니 다.
- @Around : 메서드 실행 전후 모두 어드바이스를 실행합니다. 이 시점에서 ProceedingJoinPoint 객체를 사용하여 메서드 실행을 수행하고, 실행 결과를 반환할 수 있습니다. @Around 어노테이션을 사용하여 선언하며, 메서드 실행 전후에 추가적인 로직을 수행할 수 있습니다.
즉, @Around 어노테이션을 사용하여 @Before 는 메서드 실행 전에 추가적인 로직을 수행하는 어드바이스이고, @After 는 메서드 실행 후에 추가적인 로직을 수행하는 어드바이스입니다. @Around 는 메서드 실행 전후에 추가적인 로직을 수행할 수 있는 가장 범용적인 어드바이스입니다. AOP에서 메서드의 파라미터 값을 검사하거나 변경하는 작업을 수행할 때는 @Before 어노테이션을 사용하 고, 메서드 실행 시에 새로운 값을 주입하거나, 결과를 가공하는 작업을 수행할 때는 @Around 어노테이션을 사용하는 것이 일반적입니다.
Share article