앞으로 계획
- v1 단순히 nativeQuery CRUD 패턴구조
- V2 마무리
- ORM
- JPQL (Java Persistence Query Language) - select 랑 조인 해봄!!
- 인증 (session) - 나중에 변경예정
- Exception 컨트롤러
- 서비스 레이어
- OpenInView(Lazy Loading) - Eager을 쓰면 알수 없는 에러가 날 수있어서 Lazy를 쓰는게 좋다. 레이지는 서비스단에서 마무리하기
- Static DTO
- 유효성 검사
- 프로젝트 & Git 협업방법
- 주제정하기&화면설계서. 기획자가 피그마로 다 만들어줌
- HTML 디자인 수업
- V3 시작 (동시)
- 양방향 매핑
- 복잡한 쿼리(NativeQuery → QLRM 라이브러리)
- 복잡한 쿼리(JQPL 썼을때 동일하게 안나오면 → 매핑하는 법 배울 예정)
- 동적 쿼리(매개변수에 의해서 쿼리가 달라지는 것)
- OAUTH2.0
⇒ 여기까지 배우면 프로젝트를 할 수 있다.
- 테이블 설계 (동시) - 화면나오면 검사받고 테이블 만드는거 검사받기
- 프로젝트 진행(9.24일)
- 수업
- RestAPI 수업
- JWT 인증 방식
- Stream API
- 통합테스트
- API 문서
- flutter 수업
- 두번째 프로젝트 (이력서, 면접 준비) (9.25 시작예정!)
8/29 ~ 9/24 까지 프로젝트
BoardService

게시글수정화면가기 매서드 생성
BoardController

컨트롤러에서 updateForm 매서드 service.게시글수정화면가기 사용하도록 수정
++
만약에 컨트롤러에서 인증 검사를 안해서
boardService.게시글수정화면가기(id, sessionUser)에서
sessionUser 값이 null 이 들어가게 되면,,,
service에 null 값이 넘어가서

저기 sessionUser.getId()할때 null pointer exception 이 터지게 된다.
(→ 인증검사를 안한 컨트롤러의 잘못이다)
→ 인증검사를 안해서 null pointer exception이 발생하면 우리가 미처 처리하지 못한 예외이니
예외의 ex매서드가 실행된다.
위까지 하고
수정하기 버튼을 클릭하면


잘 이동한다.
++


게시글 목록보기&상세보기는 인증체크가 필요없어서 sessionUser == null 조건을 안적었따.
( → 이 두페이지는 누구나 들어갈 수 있음.)
상세보기 이동 코드에 sessionUser 를 넘겨준 이유는
BoardReponse.DetailDTO
에서 권한체크할 때 필요하기 때문 ( 버튼 생성용 권한체크 )BoardController

게시글쓰기 이동하는 코드에 인증 추가해주기
(이 코드의 핵심로직은 return “board/save-form”; )
++ 추가

여기서 화살표 처리한 것(인증 코드)
if(sessionUser == null){
throw new Exception401(”인증되지 않았습니다.”);
}
이 인증코드는 이 코드의 핵심로직이 아니다. → 부가로직
이 코드의 핵심로직은 네모친 부분 → 핵심로직
유효성 검사 같은것도 부가로직이다.
인증, 유효성 검사 → 부가로직
⇒ 부가로직은 처음부터 쓸 필요없다. 핵심로직부터 먼저 짜기.
입사하면 인증, 권한체크 같은 부가로직말고 핵심로직만 짜라고 할 것이다.
BoardRequest

BoardRequest에 UpdateDTO 생성 ⇒ 요청(Request) DTO 는 동일하게 생겨도 중복해서 만들기!
SaveDTO와 UpdateDTO 를 보면 둘다 매개변수로 title과 content를 담기 때문
왜 SaveDTO 가 있는데 UpdateDTO도 만들어야 하나 ? 생각할 수 있따.
따로 만들어야 하는 이유는
나중에 수정할 경우가 생길 수 있는데(만약에 사장님이 UPDATE할 때는 제목수정 못하게 해! 라고 하면 DTO의 title필드를 주석처리하거나 없애야 하는데 save할때는 필요하니까 없앨 수가 없다…)
이런 경우에 저장(save)이랑 수정(update)이랑 매서드를 공유하면 dto 수정이 불가능해짐
BoardController

생성한 UpdateDTO를 사용해서 리팩토링 하기
→ RequestParam으로 title,content 받지 말고, BoardRequest.UpdateDTO로 받아라!
++
더티체킹
더티체킹이란 ? 상태 변경 검사
JPA에서는 트랜잭션이 끝나는 시점에
변화가 있는 모든 엔티티 객체를 데이터베이스에 자동으로 반영해준다.

BoardService

boardRepository.findById(id); 가 실행되고 나면
영속성 컨텍스트 (Persistence Context)에 객체로 응답받은 board(테이블)이 담긴다.
board → 영속객체. (영속성 컨텍스트에 저장되어 있음)
이 영속화되어있는 애를 ‘수정’하면 트랜젝션이 종료시에 update 쿼리가 자동으로 날아간다.
위에서 setTItle, setContent 로 영속객체의 기존 값을 수정하고 있다.
그럼 트랜젝션이 종료될때, update 쿼리가 실행된다.

boardRepositoryTest에서 테스트하면 update 쿼리가실행되는것을 볼 수 있다. (더티체킹)

위처럼 비영속객체(board)를 만들어서 set을하면(수정하면) 아무일도 일어나지 않는다.
* flush 가 아닌 persist 를 사용하면 insert 됨
*flush 는 DB에다가 현재 변경된 상태에 대한 결과를쏘라고 하는것. 모아두었다가.
++
boardRepository.findById(id); 동일한 아이디를 2번 넣어서 실행시켰는데 캐싱이 안되는 문제 발생.(→ select 가 2번 실행되어버림)
→ 원인 : 우리가 직접 쿼리를 쓰면 캐싱이 안됨!

지금 findById는 우리가 작성한 쿼리를 실행시키고 있다. (네이트브 쿼리든, jpql이든 뭐든)
영속성 컨텍스트에서 캐싱 안하고.
++
원래 실무에서는 엔티티 매니저를 이용해서 영속성 컨테스트(PC)를
조회하고 캐싱하든가 쿼리쓰든가 함
→ em.find(id); 코드 작성 → 영속성 컨텍스트에 캐싱할 수 있는게 있는지 조회(find)
→ 없으면 쿼리실행시킴(update)
→ 요약 : em.find 하면 먼저 pc에서 찾고 없으면 db에서 조회한다!

++
++

트렌젝션이 종료되면 변경을 감지해서 한번에 flush 를 함
++
회사의 erp 시스템은 myBatis 를 사용한다..,
JPA 를 쓰는 회사는 복잡한 쿼리 쓰려면 queryDSL? 을 사용함

boardService 게시글 수정 완성

boardController 완성

수정하면 잘 된다.
글수정하기완료를 클릭학고 콘솔을 보면

트랜젝션이 끝나고 flush 될때 update 쿼리가 실행되는것을 확인할 수 있다.
유효성 검사
// 그림
DS 와 같은 라인에 GlobalExceptionHandler 와 viewResolver 가 있음.
예를 들어, 컨트롤러에서 return “/board/list”하면
fileReader 로 해당 파일을 다 읽고, 순수한 html 파일이 나온다. → 이걸 view resolver 가 함.
AOP는 관점 지향 프로그래밍 → 핵심로직이 아닌 부가로직을 프록시로 처리!
→ aop는 리플렉션으로 작동한다.
→ 부가로직(유효성 검사 등)을 앞단에서 처리한다.
리플렉션으로 매개변수의 값을 분석해서 앞단(프록시)로 끌어서 처리하는게 aop이다!
핵심로직을 제외한 모든 부가로직을 따로 처리하는게 aop이다!
(→ aop는 프록시패턴과 리플렉션으로 작동한다!)
build.gradle

라이브러리 추가
UserRequest
NotEmpty 어노테이션 추가
인터셉터도 AOP도 프록시 역할을 할 수 있음
→ 이 2개를 스프링에서 제공하는 이유는 DS는 스프링이 만든 거니까 건드리지 말라는 것 ㅎ
인터셉터와 AOP
앞에서 실행할지 뒤에서 실행할지 정해줘야함!
AOP(@before 손씻기, @after 양치하기) → 메서드 분석(리플렉션)
Interceptor(prehandle 손씻기, afterhandle양치하기) → prehandle과 afterhandle은 어노테이션은
아님.
→ 둘다 써도 됨
→ 차이는 aop는 매개변수를 분석할 수 있고, 인터셉터는 이전, 이후에 실행하는것만 지정할 수 있음
→ AOP는 매서드를 분석할 수 있다. 매개변수, 어노테이션 등을 분석할 수 있음.
→ 젓가락이라는 매개변수를 분석할 수 있음
→ 젓가락을 검사. 젓가락이 더러우면 한번 씻어야함.
→ 예를들어. 인증 검사는
request 객체 안의 주소를 분석해서 처리할 수 있기 때문에 인터셉터로 처리할 수 있따.
→ 유효성 검사는
뭐가 들어올지 모름. → 매개변수를 분석해서 처리해야하니 AOP로 처리한다.
- 아침먹기(젓가락)
if(젓가락 == 더러움) throw
⇒ aop는 매개변수를 동적으로 분석할 수 있으니 이 코드를 자기 안에서 처리할 수 있다.
샌드위치먹기 - 핵심로직
- 점심먹기(젓가락)
삼겹살먹기 - 핵심로직
- 저녁먹기(젓가락)
김치찌개먹기 - 핵심로직

코어 패키지안에 2가지 패키지를 만들어줌
interceptor → 뭘 실행할지
config → 언제 실행될지
설정함


default로 구현된 이유가 선택해서 구현하라고!
preHandle 매서드 실행전
postHandle view가 리턴되고 나서
afterConpletion → ds가 뷰 리졸버한테 만들어달라고 하고 나서

작성해줌




/board 쳐보니 프리핸들 동작함
⇒ 인터셉터가 동작한 것
인증을 다 여기로 빼자.
매개변수 필요없으니 aop 대신 인터셉터로.
필터의 위치는 디스패쳐 앞.
필터에서 throw걸면 절대 글로벌익셉션핸들러한테 안감
인터셉터에서 throw 하면 나를 호출한 ds한테 넘기고
ds는 geh한테 처리하라고 줌. 스프링 내부에서 처리하게 됨
필터는 왜 그게 안될까.
필터는 톰캣꺼여서 앞단에 있음.
filter - dispatcher servlet - interceptor - aop - controller 순서
(global Exception Handelr 와 view resolver 는 dispatcher servlet 과 같은 라인에 있다)
++
print 는 \n 해야지 파싱할 때 알아먹을 수 있따.
printwriter는 기본적으로 자기가 flush를 가지고 있다.
소켓통신, flush 개념..

인터셉터에 만들어주고

board컨트롤러의 인증 조건문 다 제거
지금은 /user, /board 를 인터셉터에 적었지만 인증이 필요한 주소는 앞에 /api 를 붙일 예정

api로 수정

수정하기에 인증이 필요하니 api 붙여ㅜㄴ다.
그리고 다르,ㄴ것도

메인페이지 그냥 / 로 수정해줌
/board 로 리턴되게 한 다른 코드도 / 로 수정하기
뷰 파일가서 링크 수정하기

header 파일

detail.mustache

list. 게시글 상세보기는 인증이 필요없으니 수정 X

save-form.mustache

update-form.mustache

로그인 로그아웃도 메인으로 이동하도록 해둔거 수정해두기

위와같이 수정해두면 된다!

로그인 안하고 상세보기로 들어가면 null 이 터지니까

boardReponse 의 detailDTO 매서드의 이 권한체크 부분 코드 위와같이 수정해주기
그럼 이제 저 alert 창 안뜨고 상세페이지로 갈 수 있다.
AOP

라이브러리 추가
기술보다는 기본기, 기본개념을 알아야한다. ㅎ
인터셉터를 아는것보다 기본개념..!!~~!~!~!~!

에러 패키지에 핸들러추가

- Aspect 붙이기 → AOP로 등록
before, after, around 3개 가 있음

이렇게 작성하고

로그인폼을 때리면

잘 실행되는걸 확인할 수 있다.
리플렉션은 어노테이션으로 발동시키는게 제일 좋다!
정규표현식?을 이용해서 before 에 *.

core 패키지에 Hello 어노테이션 생성 ( → 위치가 core)

어노테이션 만들고

hello 어노테이션 붙은 매서드 실행되기 직전에 이게 실행됨

login-form 에 우리가 만든 어노테이션 붙여주고
++
리플렉션 어려우니까 스프링이 만들어둔 기능이 aop임. AOP 써. 쉽쟈나~ 이러면서,
매개변수를 분석할 수 있는 장점이 있다.
어떤 매개변수를 가지면 어떻게 처리할지도 다 설정할 수 있다.


로그인폼 때리면 호출됨
조인폼처럼 안붙인거 실행시키면 안나옴

어라운드로 바꾸면
- 이전 이후를 다 관리할 수 있다.

조인포인트를 통해서 매개변수에 접근할 수 있다.
throw 날리기

완성
어라운드는 헬로어노테이션이 붙은 매서드의 전후를 관리.
jp.proceed() 는 헬로 어노테이션이 붙은 함수. 임
그게 실행되어서 그 리턴값이 들어옴
→ 위에선 loginFORM 을 때려서 user/login-form 이 리턴됨
마지막에 리턴 proceed 하면 디스패처 서블릿이 가져가서 뷰 리졸버한테 넘겨서 뷰 만듦.

다 완성시켜서
로그인폼을 url 에서 때리면

잘나옴

로그인폼에 sout 추가해서 로그인폼때려서 확인해보면


이렇게 나옴
++
리플렉션을 적용해서 어노테이션을 활용한다는게 이런거임!!!
@transactional 도 리플렉션이 발동해서 수행되는 것.
→ 매서드 시작과 끝을 관리.
@transactional 들어가보면 type, method 에 걸 수 있는데,
만약 클래스에 걸면, 해당 클래스의 모든 매서드가 실행될때 트렌젝션이 걸림.
→ select 에는 트렌젝션을 걸 필요가없으니 이렇게는 사용하지 말기
AOP 정리
aop 개념 적기
1. 라이브러리 등록
implementation 'org.springframework.boot:spring-boot-starter-aop'
2. 어노테이션 만들기
package shop.mtcoding.blog.core;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Hello {
}
3. AOP 만들기
package shop.mtcoding.blog.core.error;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
@Component
@Aspect // AOP 등록
public class GlobalValidationHandler {
@Around("@annotation(shop.mtcoding.blog.core.Hello)")
//@Before("@annotation(org.springframework.web.bind.annotation.GetMapping)")
public Object hello1(ProceedingJoinPoint jp) throws Throwable {
System.out.println("aop hello1 before 호출됨");
Object proceed = jp.proceed(); // @Hello 어노테이션이 붙은 함수 호출 "user/login-form";
System.out.println("aop hello1 after 호출됨");
System.out.println(proceed);
return proceed;
}
}
4. 발동시키기
@Hello
@GetMapping("/login-form")
public String loginForm() {
System.out.println("loginForm 호출됨");
return "user/login-form";
}

출처 : AOP 정리 (notion.site)
유효성 검사하는 매서드 만들어주기

before, after 는 Proceeding 아니고 그냥 JoinPoint 써줌

@NotEmpty 붙여주기 (→ 공백도 안되고 null도 안됨)
아까 validation 라이브러리 넣은 이유가 이 어노테이션 쓸라고 넣은거임

컨트롤러에서 DTO 쓰는 부분에 @Valid (→ jakarta 꺼) 붙여주기

dto 앞에 @valid 붙여주면
dto 객체가 만들어질때 안에 어노테이션을 분석해서 (→@notEmpty 같은거)
null이거나 공백이면 Errors 에 넣어줌
로그인 실행해보면


이렇게 확인할 수 있다.,

수정하기를 하면

이렇게 매서드가 실행되기 전에 위의 validation 이 실행되어 콘솔에서 확인할 수 있다.
글쓰기에 들어가서

@NotEmpty 인 title과 content를 “” 으로 둔 채 글쓰기완료를 클릭하면

에러가 2개 발생한 것을 확인할 수 있다.

제목만 작성하고 글쓰기완료를 누르면

내용에 “”이 들어가서 에러가 1개 발생한 것을 확인할 수 있다.
++
정규표현식 모를 땐 (→ 챗지피티가 알려준다.)





지금은 정규식 공부하지 말고 이렇게 챗지피티한테 물어보기.
** 쌤이 준 정규표현식 DTO에 활용한 코드
package com.example.kakao.user;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
import java.util.Collections;
public class UserRequest {
@Getter
@Setter
public static class JoinDTO {
@NotEmpty
@Pattern(regexp = "^[\\w._%+-]+@[\\w.-]+\\.[a-zA-Z]{2,6}$", message = "이메일 형식으로 작성해주세요")
private String email;
@NotEmpty
@Size(min = 8, max = 20, message = "8에서 20자 이내여야 합니다.")
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[@#$%^&+=!~`<>,./?;:'\"\\[\\]{}\\\\()|_-])\\S*$", message = "영문, 숫자, 특수문자가 포함되어야하고 공백이 포함될 수 없습니다.")
private String password;
@NotEmpty
private String username;
public User toEntity() {
return User.builder()
.email(email)
.password(password)
.username(username)
.roles(Collections.singletonList("ROLE_USER"))
.build();
}
}
@Getter
@Setter
public static class LoginDTO {
@NotEmpty
@Pattern(regexp = "^[\\w._%+-]+@[\\w.-]+\\.[a-zA-Z]{2,6}$", message = "이메일 형식으로 작성해주세요")
private String email;
@NotEmpty
@Size(min = 8, max = 20, message = "8에서 20자 이내여야 합니다.")
@Pattern(regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[@#$%^&+=!~`<>,./?;:'\"\\[\\]{}\\\\()|_-])\\S*$", message = "영문, 숫자, 특수문자가 포함되어야하고 공백이 포함될 수 없습니다.")
private String password;
}
@Getter
@Setter
public static class EmailCheckDTO {
@NotEmpty
@Pattern(regexp = "^[\\w._%+-]+@[\\w.-]+\\.[a-zA-Z]{2,6}$", message = "이메일 형식으로 작성해주세요")
private String email;
}
}
위처럼 사용한다.
정규식을 적용하기 전에 Test 하는 클래스를 만들어보자.

테스트에 temp 패키지랑 RegexTest 클래스 만들어주기 ( → 정규식 테스트 )
package shop.mtcoding.blog.temp;
import org.junit.jupiter.api.Test;
import java.util.regex.Pattern;
// java.util.regex.Pattern
public class RegexTest {
@Test
public void 한글만된다_test() throws Exception {
String value = "ㄱ";
boolean result = Pattern.matches("^[가-힣]+$", value);
System.out.println("테스트 : " + result);
}
@Test
public void 한글은안된다_test() throws Exception {
String value = "abc";
boolean result = Pattern.matches("^[^ㄱ-ㅎ가-힣]*$", value);
System.out.println("테스트 : " + result);
}
@Test
public void 영어만된다_test() throws Exception {
String value = "ssar";
boolean result = Pattern.matches("^[a-zA-Z]+$", value);
System.out.println("테스트 : " + result);
}
@Test
public void 영어는안된다_test() throws Exception {
String value = "가22";
boolean result = Pattern.matches("^[^a-zA-Z]*$", value);
System.out.println("테스트 : " + result);
}
@Test
public void 영어와숫자만된다_test() throws Exception {
String value = "ab12";
boolean result = Pattern.matches("^[a-zA-Z0-9]+$", value);
System.out.println("테스트 : " + result);
}
@Test
public void 영어만되고_길이는최소2최대4이다_test() throws Exception {
String value = "ssar";
boolean result = Pattern.matches("^[a-zA-Z]{2,4}$", value);
System.out.println("테스트 : " + result);
}
// username, email, fullname
@Test
public void user_username_test() throws Exception {
String username = "ssar";
boolean result = Pattern.matches("^[a-zA-Z0-9]{2,20}$", username);
System.out.println("테스트 : " + result);
}
@Test
public void user_fullname_test() throws Exception {
String fullname = "메타코딩";
boolean result = Pattern.matches("^[a-zA-Z가-힣]{1,20}$", fullname);
System.out.println("테스트 : " + result);
}
@Test
public void user_email_test() throws Exception {
String fullname = "ssaraa@nate.com"; // ac.kr co.kr or.kr
boolean result = Pattern.matches("^[a-zA-Z0-9]{2,10}@[a-zA-Z0-9]{2,6}\\.[a-zA-Z]{2,3}$", fullname);
System.out.println("테스트 : " + result);
}
@Test
public void account_gubun_test1() throws Exception {
String gubun = "DEPOSIT"; // ac.kr co.kr or.kr
boolean result = Pattern.matches("^(DEPOSIT)$", gubun);
System.out.println("테스트 : " + result);
}
@Test
public void account_gubun_test2() throws Exception {
String gubun = "TRANSFER"; // ac.kr co.kr or.kr
boolean result = Pattern.matches("^(DEPOSIT|TRANSFER)$", gubun);
System.out.println("테스트 : " + result);
}
@Test
public void account_tel_test1() throws Exception {
String tel = "010-3333-7777"; // ac.kr co.kr or.kr
boolean result = Pattern.matches("^[0-9]{3}-[0-9]{4}-[0-9]{4}", tel);
System.out.println("테스트 : " + result);
}
@Test
public void account_tel_test2() throws Exception {
String tel = "01033337777"; // ac.kr co.kr or.kr
boolean result = Pattern.matches("^[0-9]{11}", tel);
System.out.println("테스트 : " + result);
}
}
선생님이 준 파일에서 복붙

테스트해보면 “한글”이라고 넣으면 TRUE

“ㄱ”을 넣으면 FALSE 가 나오는 것을 확인할 수 있다.
이렇게 정규식을 가져다 쓰면 되는데 구글링해서 일일이 찾기는 어려우니
챗지피티한테 물어보자.
위의 경우 한글말고 ㄱ같은 자음도 TRUE로 나오는 정규표현식 알려줘! 하면서 코드 붙여넣어서
물어보면 다 알려준다!
그리고 DTO
UserRequest

어노테이션 notBlank 말고 notEmpty 걸기 (공백 null 둘다 허용안함)
? ) NotBlank와 NotEmpty의 차이가 뭐지 ?
@notEmpty 와 @notBlank 의 차이가 뭐야 ?
ChatGPT의 말:
ChatGPT
@NotEmpty
와 @NotBlank
는 둘 다 자바에서 주로 사용되는 유효성 검사 어노테이션이지만, 서로 다른 상황에서 사용됩니다. 주로 빈 문자열과 공백 문자열을 처리하는 방식에서 차이가 있습니다.1. @NotEmpty
- 대상:
String
,Collection
,Map
, 또는Array
와 같은 빈 상태를 가질 수 있는 객체.
- 검사 조건:
null
이 아니고, 비어 있지 않음을 검사합니다.
- 적용 예시: 이 어노테이션은 문자열이 비어 있지 않거나, 컬렉션이 최소한 하나의 요소를 가지고 있는지 확인하는 데 사용됩니다.
- 빈 문자열
""
은 실패 (유효성 검사에 통과하지 않음). null
값은 실패.- 공백 문자열
" "
는 통과 (공백도 비어있지 않으므로 통과).
예시:
java코드 복사
@NotEmpty
private String name;
2. @NotBlank
- 대상:
String
데이터.
- 검사 조건:
null
이 아니고, 비어 있지 않으며, 공백으로만 이루어지지 않음을 검사합니다.
- 적용 예시: 이 어노테이션은 문자열이 비어 있지 않고, 단순히 공백 문자로만 이루어져 있지 않은지 확인하는 데 사용됩니다.
- 빈 문자열
""
은 실패. null
값은 실패.- 공백 문자열
" "
도 실패 (공백만 있는 문자열은 비어있는 것으로 간주).
예시:
java코드 복사
@NotBlank
private String username;
주요 차이점 요약:
@NotEmpty
:null
이나 빈 문자열/컬렉션은 허용하지 않지만, 공백만 있는 문자열은 허용됩니다.
@NotBlank
:null
이나 빈 문자열뿐만 아니라 공백만 있는 문자열도 허용하지 않습니다.
예시 비교:
java코드 복사
@NotEmpty
private String description; // null, ""은 허용되지 않지만, " "는 허용됩니다.
@NotBlank
private String username; // null, "", " " 모두 허용되지 않습니다.
따라서 입력 데이터가 단순히 빈 값(
""
)인지 확인하려면 @NotEmpty
를 사용하고, 공백(" "
)도 허용하지 않으려면 @NotBlank
를 사용하는 것이 적합합니다.
넣어주면
로그인할때 1개라도 에러(공백)걸리면 errors 에 간다.
(→ loginDTO로 username 혹은 password 공백으로 전달되면 유효성 검사에서(@Valid) 적발되고 (?)
걸리면 바로 뒤에 붙어있는 Errors 로 보냄 )
- @Valid DTO 와 Errors 쓸때는 붙여서 써야한다. 중간에 파라미터 오면 Errors로 전달안됨!!
실행해보면

username 크기를 4글자 이상으로 적었을 때

제대로 알려줌

패스워드를 공백으로 두고 시도하면

제대로 알려준다.
- 요청(Request) dto (→ BoardRequest, UserRequset ) 에 다 NotEmpty 적어주기
- NotEmpty에 message를 줘서 내가 원하는 문구로 알릴 수 있는데 기본 문구가 잘 되어있으니
@NotEmpty(message = "비워놓지마. (근데 이거 안써줘도 됨)")
이렇게 message 쓰지말고 NotEmpty 만 쓰자.
BoardRequest
package shop.mtcoding.blog.board;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;
import shop.mtcoding.blog.user.User;
public class BoardRequest {
// 요청DTO는 동일하게 생겨도 중복해서 만들기
@Data
public static class UpdateDTO { // title, content 2개만 담으면 된다.
@NotEmpty
private String title;
@NotEmpty
private String content;
// insert 가 아니라 UPDATE 이니까 투엔티티 없어도 댐
}
@Data
public static class SaveDTO { // title, content 2개만 담으면 된다.
//@Pattern(regexp = ) 정규표현식 패턴
@NotEmpty
private String title;
@NotEmpty(message = "비워놓지마. (근데 이거 안써줘도 됨)")
private String content;
// insert 할 때는 toEntity 를 만든다.
public Board toEntity(User sessionUser) { // 날짜는 엔티티에 @CreationTimeStamp 붙여주면 자동으로 들어간다.
return Board.builder()
.title(title)
.content(content)
.user(sessionUser)
.build(); // shift + enter
}
}
}
UserRequest
package shop.mtcoding.blog.user;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
// 내부클래스로 관리!!
// 이렇게 하면 위험하지 않음
public class UserRequest {
@Data // getter, setter, tostring 들고 있음
public static class joinDTO {
// static 이니까 new 하기전에 이 클래스는 static 에 뜬다.
@NotEmpty
private String username;
@NotEmpty
private String password;
@Pattern(regexp = "^[\\w._%+-]+@[\\w.-]+\\.[a-zA-Z]{2,6}$", message = "이메일 형식으로 작성해주세요")
@NotEmpty
private String email;
// 매서드 만들기
// 이 매서드는 DTO -> UserObject (유저 엔티티) 로 바꾸는 역할을 함!!
public User toEntity() { // insert 하는건 toEntity로 만들어서 넣으면 완전 편리하고 코드가 심플해짐!!
// 아래에서 유저객체 만들어서 return 함!
return User.builder().username(username).password(password).email(email).build();
}
}
// 요청 바디 데이터 받기 위해 dto 생성함!!! (dto 만드는 목적)
// 이렇게 만들어두면 관리하기 편하다.
// 나중에 사용하기도 편함
@Data // getter, setter, tostring 들고 있음
public static class loginDTO {
// static 이니까 new 하기전에 이 클래스는 static 에 뜬다.
@Size(min = 2, max = 4) // 최소 2자, 최대 4자
@NotEmpty
private String username;
@NotEmpty
private String password;
}
}
DTO에 NotEmpty 어노테이션 다 붙여주고

BoardController에 가서
DTO앞에 @Valid 없는거 다 붙여주고, Errors도 매개변수로 적어주기
Errors 대신 BindingResult 같은거 적어놓은 코드도 종종 보일텐데
BindingResult 같은 애들의 부모가 다 Errors 여서 (다형성) 다 Errors 구나 이해하면 된다.
@Valid BoardRequest.UpdateDTO updateDTO, @PathVariable(”id”) int id, Errors errors
→ 이렇게 쓰면 안됨. 에러가 안나서 디버깅도 안됨 ㅎ
→ DTO 바로 뒤에 Errors 가 있어야 함
→ 아니면 UpdateDTO 뒤에 id 에 값을 넣어버림
→ @Valid BoardReuqest.UpdateDTO updateDTO, Errors errors, @PathVariable(”id”) int id
이렇게 써주거라

인터셉터 적용하고 유효성 검사까지 완료한 파일
Share article