1. 유효성 검사
- 사용자에게 올바른 입력값을 받기 위한 사전 검사
- Pattern.matches(regex, value) 사용
- regex : 정규 표현식, 입력을 허용하는 규칙
- value : 입력받을 값
- 한글만 받고 싶을 때에 사용할 수 있는 Regular Expression Test
- 자음, 모음, 한글을 전부 포함
- ^ : 시작
- $ : 끝
- [ ] : 유효성 검사 패턴
- - : 범위 (a-z, A-Z 등, a에서 z까지)
- ^ : 제외 (!과 같은 의미, 대괄호 안에 있어야 작동)
- { } : 글자 수 (정수 or 범위 (-))
- + : 뒤에 추가 정규 표현식을 작성
@Test
public void 한글만된다_test() {
// 1. given
String value = "";
// 2. when
boolean result = Pattern.matches("^[ㄱ-ㅎㅏ-ㅣ가-힣]+$", value);
// 3. eye
System.out.println("Test : " + result);
}


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 = "saltedcaramel1234";
// 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 = "1한글$%^";
// String value = "ssar";
boolean result = Pattern.matches("^[^a-zA-Z]+$", value);
System.out.println("테스트 : " + result);
}
@Test
public void 영어와숫자만된다_test() throws Exception {
String value = "ssar2+3";
// 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);
}
@Test
public void user_username_test() throws Exception {
String username = "ssar";
// 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...s@fGf.ccm";
// 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 = "01022227777";
boolean result = Pattern.matches("^[0-9]{3}[0-9]{4}[0-9]{4}$", tel);
System.out.println("테스트 : " + result);
}
}
2. 모듈화
- Controller에 부착하여 재사용하기 위해, 유효성 검사를 모듈화
- DTO에서 값을 검사 : Controller의 책임 전가
- Annotation 활용
- 기존 코드 : Controller에서 입력받는 값에 따른 유효성 검사의 차이 - 입력값에 따라 달라짐
- 모듈화 불가
@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";
}
- 새 코드 : Annotation과 Errors를 활용하여 공통모듈을 생성 - 입력값이 달라도 코드 동일
- 모듈화 가능
// @Valid : 매개변수 내 Validation (Pattern, Size 등) 검사
@PostMapping("/join")
public String join(@Valid UserRequest.JoinDTO joinDTO, Errors 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("이메일 형식에 맞게 적어주세요");
*/
// pass if no error exists
if (errors.hasErrors()) {
// gather errors
List<FieldError> fErrors = errors.getFieldErrors();
// alert users (
for (FieldError fieldError : fErrors) {
throw new Exception400(fieldError.getField() + " : " + fieldError.getDefaultMessage());
}
}
userService.회원가입(joinDTO);
return "redirect:/login-form";
}

3. Proxy Pattern
- 공통 모듈을 프록시 패턴을 활용해서 분리
- 코드의 재사용 용이
- GlobalValidationHandler : 유효성 검사를 위한 Advice
- Before, After, Around
- Before : Target이 실행되기 전에 실행되는 함수
- Target의 사전 동작을 관리 가능
- After : Target이 실행되고 난 후 실행되는 함수
- Target의 사후 동작을 관리 가능
- Around : Target이 실행되기 전, 실행 후 둘 다 실행되는 함수
- Target의 사전, 사후 동작을 전부 관리 가능
@Component
@Aspect // Proxy
public class GlobalValidationHandler {
@Before("@annotation(shop.mtcoding.blog._core.error.anno.MyBefore)")
public void beforeAdvice(JoinPoint 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) {
String name = jp.getSignature().getName();
System.out.println("around advice 직전 : " + name);
try {
Object result = jp.proceed(); // 컨트롤러 함수 호출
System.out.println("around advice 직후 : " + name);
System.out.println("result : " + result);
return result;
} catch (Throwable e) {
throw new RuntimeException(e);
}
}
}
With ChatGPT : ✅ 보완된 요약:
@Around
, @Before
, @After
차이점@Around vs @Before / @After
@Before
와@After
는 관심 대상 메서드의 실행 전후에만 개입할 수 있음@Before
: 메서드 실행 직전에 실행됨@After
: 메서드 실행 직후에 실행됨- 이들 어노테이션은 **핵심 로직(= invoke)**의 실행을 차단하거나 재정의할 수 없음
- 단,
JoinPoint
를 통해 메서드 이름, 매개변수, 타겟 객체 정보 등은 읽을 수 있음 - 예외 발생 여부와 무관하게 실행되는 특징이 있음 (
@After
는finally
처럼 작동)
@Around
는 핵심 메서드 실행 자체를 제어할 수 있는 유일한 어노테이션ProceedingJoinPoint.proceed()
를 호출해야 실제 메서드가 실행됨- 따라서, 실행 시점 지연, 실행 생략, 반복 실행, 예외 감싸기 등도 가능
@Before
,@After
,@AfterReturning
,@AfterThrowing
의 모든 기능을 포함 가능- 리턴값 변형이나 예외 핸들링을 포함한 전체 흐름 커스터마이징에 적합
- 실무 팁
- 단순히 로깅, 인증체크, 파라미터 확인 정도라면
@Before
/@After
로 충분 - 트랜잭션 처리, 리턴 값 변조, 예외 래핑 등 전체 흐름이 필요한 경우는
@Around
사용
4. 적용
- GlobalExceptionHandler의 Pointcut을 PostMapping, PutMapping에 적용
- 매개 변수 내에 Errors를 찾음
- 있으면, Errors 내에 항목이 있는지 검사 (Errors.hasErrors())
- 있으면 유효성 검사
@Aspect
@Component
public class GlobalValidationHandler {
// 관심사를 분리
// PostMapping 혹은 PutMapping이 붙어 있는 메서드를 실행하기 직전에 Advice 호출
@Before("@annotation(org.springframework.web.bind.annotation.PostMapping) || @annotation(org.springframework.web.bind.annotation.PutMapping)")
public void badRequestAdvice(JoinPoint jp) { // 실행될 실제 메서드의 모든 것을 투영 (Reflect)
Object[] args = jp.getArgs(); // 메서드의 매개변수들 (배열로 리턴)
for (Object arg : args) { // 매개변수 갯수만큼 (Annotation 제외)
// execute when method's args have Errors
if (arg instanceof Errors) {
System.out.println("Exception400 처리 필요");
Errors errors = (Errors) arg; // Downcasting
// execute when Errors.size() > 0 -> errors exist
if (errors.hasErrors()) {
// gather errors
List<FieldError> fErrors = errors.getFieldErrors();
// alert users (
for (FieldError fieldError : fErrors) {
throw new Exception400(fieldError.getField() + " : " + fieldError.getDefaultMessage());
}
}
}
}
}
}
- Target : PostMapping + Body가 있는 메서드 (update, save)
- BoardController
@PostMapping("/board/save")
public String save(@Valid BoardRequest.SaveDTO saveDTO, Errors errors) {
User sessionUser = (User) session.getAttribute("sessionUser");
boardService.글쓰기(saveDTO, sessionUser);
return "redirect:/";
}
@PostMapping("/board/{id}/update")
public String update(@PathVariable("id") Integer id, @Valid BoardRequest.UpdateDTO updateDTO, Errors errors) {
User sessionUser = (User) session.getAttribute("sessionUser");
boardService.글수정(updateDTO, sessionUser.getId(), id);
return "redirect:/board/" + id;
}
- BoardRequest
@Data
public static class SaveDTO {
// title=제목1&content=내용1 -> isPublic은 null
// title=제목1&content=내용1&isPublic -> isPublic은 공백
// title=제목1&content=내용1&isPublic= -> isPublic은 스페이스
@NotEmpty(message = "제목을 입력해주세요.") // null, 공백(""), 스페이스(" ") 안됨
private String title;
@NotEmpty(message = "내용을 입력해주세요.")
private String content;
private String isPublic;
public Board toEntity(User user) {
return Board.builder()
.title(title)
.content(content)
.isPublic(isPublic == null ? false : true)
.user(user) // user객체 필요
.build();
}
}
@Data
public static class UpdateDTO {
@NotEmpty(message = "제목을 입력해주세요.")
private String title;
@NotEmpty(message = "내용을 입력해주세요.")
private String content;
private String isPublic; // "on" or null
public Boolean isPublicChecked() {
return "on".equals(isPublic);
}
}
- UserController
@PostMapping("/user/update")
public String update(@Valid UserRequest.UpdateDTO updateDTO, Errors errors) {
User sessionUser = (User) session.getAttribute("sessionUser");
// update user_tb set password = ?, email = ? where id = ?
User userPS = userService.회원정보수정(updateDTO, sessionUser.getId());
// 세션 동기화
session.setAttribute("sessionUser", userPS);
return "redirect:/";
}
@PostMapping("/join")
public String join(@Valid UserRequest.JoinDTO joinDTO, Errors 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";
}
@PostMapping("/login")
public String login(@Valid UserRequest.LoginDTO loginDTO, HttpServletResponse response, Errors errors) {
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);
response.addCookie(cookie);
}
return "redirect:/";
}
- UserRequest
@Data
public static class UpdateDTO {
@Pattern(regexp = "^[a-zA-Z0-9]{2,20}$", message = "유저네임은 2-20자이며, 특수문자,한글이 포함될 수 없습니다")
private String username;
@Size(min = 4, max = 20, message = "크기가 4자에서 20자 사이여야 합니다.")
private String password;
@Pattern(regexp = "^[a-zA-Z0-9.]+@[a-zA-Z0-9]+\\.[a-zA-Z]{2,3}$", message = "이메일 형식으로 적어주세요")
private String email;
}
// insert 용도의 dto에는 toEntity 메서드를 만든다.
@Data
public static class JoinDTO {
// DTO 유효성 검사 annotation
// 1. Pattern
@Pattern(regexp = "^[a-zA-Z0-9]{2,20}$", message = "유저네임은 2-20자이며, 특수문자,한글이 포함될 수 없습니다")
private String username;
// 2. Size
@Size(min = 4, max = 20, message = "크기가 4자에서 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, message = "크기가 4자에서 20자 사이여야 합니다.")
private String password;
private String rememberMe; // check되면 on, 안되면 null
}
- LoveController
@PostMapping("/love")
public Resp<?> saveLove(@Valid @RequestBody LoveRequest.SaveDTO reqDTO, Errors errors) {
User sessionUser = (User) session.getAttribute("sessionUser");
LoveResponse.SaveDTO respDTO = loveService.좋아요(reqDTO, sessionUser.getId());
return Resp.ok(respDTO);
}
- LoveRequest : boardId는 Integer 타입 : NotBlank, NotEmpty 사용 불가 → NotNull
@Data
public static class SaveDTO {
@NotNull(message = "해당하는 글이 없습니다.")
private Integer boardId;
public Love toEntity(Integer sessionUserId) {
return Love.builder()
.board(Board.builder().id(boardId).build())
.user(User.builder().id(sessionUserId).build())
.build();
}
}
- ReplyController
@PostMapping("/reply/save")
public String save(@Valid ReplyRequest.SaveDTO saveDTO, Errors errors) {
User sessionUser = (User) session.getAttribute("sessionUser");
replyService.댓글저장(saveDTO, sessionUser);
return "redirect:/board/" + saveDTO.getBoardId();
}
- ReplyRequest
@Data
public static class SaveDTO {
@NotNull(message = "해당하는 글이 없습니다.")
private Integer boardId;
@NotEmpty(message = "내용을 입력해주세요.")
private String content;
public Reply toEntity(User sessionUser) {
return Reply.builder()
.content(content)
.user(sessionUser)
.board(Board.builder().id(boardId).build())
.build();
}
}
Share article