당니의 개발자 스토리
회원 등록 본문
회원 등록
자 이번 시간에는 회원 가입을 만들어 보겠습니다.

우리가 이전 시간에 만들었던 홈 화면에서

회원 가입을 딱 누르면,

제가 members/new로 가도록 개발해놨단 말이에요.

여기 보시면 태그 a랑 href는 아시죠?

링크 태그 <a> 사용법 : https://mjmjmj98.tistory.com/122
이 링크 태그를 통해서 members/new로 이동하도록 되어 있습니다. 그래서 여기서부터 회원 가입 폼이 떠야 되겠죠.
자 회원 가입 폼이 떴을 때를 생각을 해서 먼저 폼 객체를 만들 거예요.

제가 java 코드는 웬만하면 다 직접 치고요. html은 웬만하면 복사 붙이기를 하겠습니다.

일단 이렇게 하고 여기에다가도 재밌는 기능을 하나 넣을 거에요.

@NotEmpty 라고 jakarta.validation 라는 게 있어요. 뭐냐면 name은 필수로 받을 거고 나머지는 필수가 아닌 거에요. 이렇게 해서 회원 이름은 필수입니다. 이렇게 해놓으면 jakarta.validation을 통해서 스프링이 MemberForm을 validation 해줍니다. name에 값이 비어 있으면 뭔가 오류가 난다는 거겠죠.
그 다음에 컨트롤러를 등록해 볼 거에요.

MemberController를 만들고 @Controller 적으면 되겠죠. 그리고 @RequiredArgsConstructor를 해줍니다.

그리고 나서, 멤버 서비스를 갖다 쓸 거에요. 컨트롤러가 주로 서비스를 갖다 쓰니까 기본적으로 이정도 깔아야 되겠죠.
그 다음에 이제 MemberForm으로 가보겠습니다.

Form을 만들거고요. 자 여기서 @GetMapping(value = "/members/new") 해서 이렇게 만들 겁니다.

이제 이 Model 이라는 게 뭐냐면, 스프링 대부분의 강의를 들으실 분은 기본적으로 아실 텐데,

여기 model에다가 제가 addAttribute 해서 MemberForm이라고 하면, 컨트롤러에서 View로 넘어갈 때 이 파라미터에 들어가 있는 데이터를 실어서 넘깁니다.
그 다음에 return 하고, 넘어갈 화면을 정해야 되겠죠.

자 그럼 이제 이 HTML을 만들어야 돼요. 지금 보면 컨트롤러에서 이 createMemberForm 화면으로 이동할 때 어떻게 합니까? MemberForm 이라는 완전히 빈 껍데기 객체를 가지고 가요. 이거를 가지고 가는 이유는 뭐 빈 화면이니까 아무것도 없을 수도 있는데, 뭔가 validation 이라든가 이런 것을 해주거든요. 그래서 이제 빈 껍데기라도 이렇게 들고 갑니다.
자 createMemberForm을 만들어야 되겠네요.

resources의 templates에 directory가 members가 나와야 됩니다.

화면은 복붙 하겠습니다. 이제 한번 설명을 쭉 드릴게요.
서버를 띄운 다음에 가볼게요.

회원가입으로 가면 이제는 MemberController를 통해서

지금의 html이 나와요.

회원가입으로 가면 컨트롤러가 /members/new로 컨트롤러에 가서

컨트롤러가 이 템플릿으로 이동해서 렌더링을 한 거죠.

여기서 이제 데이터를 쭉 입력을 하면 돼요. 이름, 도시, 뭐 그냥 대충 적어서 submit 하면,

지금은 오류가 나요. 갈 데가 없기 때문이죠. 근데 우선 이런 화면이다라는 걸 보시고요.

createMemberForm.html이 이 화면인거죠. 이제 설명을 드릴게요.

자 먼저 똑같이 header를 include 합니다. 이거를 hierarchy 스타일로 하면 이런 코드가 없어도 되는데, 제가 최대한 심플하게 설명 드리려고 그냥 제일 편한 방법인 include 스타일을 선택했구요.
자 이러면 header, bodyHeader html을 include 합니다.

그 다음에 저희가 잘 아는 form 태그가 나오죠. 여기서부터 여기까지 form 태그가 있습니다. 사실 이게 핵심이죠.

이 action은 /members/new로 가고 그 다음에 method가 post로 되어 있습니다.

이거는 GET 방식이구요.

자 여기 보시면 컨트롤러가 @GetMapping 이라고 되어 있죠.
그러니까 /members/new로 GET 방식으로 오면, 이 컨트롤러를 타서 저 createMemberForm이 열리는 겁니다.

자 이게 열려서 렌더링이 되는 거구요.

그런데 렌더링이 될 때 제가 이 model.addAttribute에 memberForm을 넘겼단 말이에요. 이걸 넘겼기 때문에 이제 화면에서 이 MemberForm 객체에 접근할 수 있게 됩니다.

자 그러면 이거는 Thymeleaf에 있는 문법인데 th가 붙은 건 다 Thymeleaf 문법이에요. th:object="${memberForm} 라고 되어있는데, 이 form 안에서는 이 객체를 계속 쓰겠다는 거예요.

그 다음에 th:field="*{name} 라고 되어있죠. 이것도 Thymeleaf 문법인데, Thymeleaf에 대해서 좀 궁금하신 분들은 Thymeleaf 메뉴얼을 한번 보세요.
참고로 이 * 표시가 되어 있으면,

이 object를 참고하는 겁니다.

이 ${memberForm}에 가보면 지금 object가 MemberForm 이죠?

MemberForm에는 name이 있죠.

자 그러면 *{name}은 기본적으로 getter, setter를 통한 접근법인 property 접근법이라고 하는데, 그걸 가지고 접근을 합니다.
자 그래서 여기서 핵심이 뭐냐면,

input type은 text고 그 다음에 뭐 뒤에 쭉 있는 거는 제가 다음에 설명을 뒤에서 드릴게요.

그리고 도시는 input type이 text가 아니고 th:field라고 되어 있죠.

이 th:field가 뭐냐면, 어쨌든 city, street, zipcode로 되어있는데,

여기서 페이지 소스 보기를 하면,

도시를 가지고 얘기를 드릴게요.

지금 input type은 text고 class나 placeholder는 넘어가고요.

이 id랑 name 있죠. 보통 form을 만들 때 id랑 name을 넣어주거든요. 관례상 id랑 name을 둘 다 똑같은걸 보통 적는단 말이에요. 그런데 약간 중복이잖아요. "city" 이걸 두 번 작업을 해야 되니까 Thymeleaf에서 뭐가 있냐면,

이렇게 th:field라고 적어주시면 이거의 이름을 가지고 그냥 이렇게

generate를 해줘요. id랑 name을 다 맞춰줍니다.

그래서 여기 있는 이 memberform object에 있는

이 city, field라고 할 수 있죠. 그래서 이 city field 라는 걸 가지고

이렇게 해주면 방금처럼 이게 렌더링이 될 때,

id, name을 한꺼번에 다 똑같은 이름으로 처리를 해줍니다.

이렇게 하면, 아무래도 다음에 고칠 때 얘만 고치면 되니까 좀 편해지겠죠. 아니면 귀찮으면 th:field="*{city}를 그냥 빼고

뭐 그냥 이렇게 적으셔도 돼요. 상관없습니다. 근데 아무래도 이거보다는 이왕 th:field="*{city} 를 통해서 타임리프로 간결하게 쓰는게 낫겠죠.

그래서 결과적으로 text에 field가 name, city, street, zipcode를 다 하면 이 form 데이터가

제가 여기 버튼 submit을 딱 누르면 어떻게 되겠습니까?

여기 있는 /members/new의 method가 post로 넘어가는 거죠.
자 그럼 이제 post 컨트롤러를 만들면 되겠죠.

혹시 여기 첫번째 fields 뒤에 좀 복잡한데요! 하실 수 있는데 이거는 다음에 설명을 해드릴게요.

자 똑같이 MemberController에서 post를 받아야 되겠죠.
@PostMapping은 보통 데이터를 Form 같은 데에 넣어서 전달할 때 POST를 씁니다. 반면 GET은 주로 조회할 때 쓰죠.
자 똑같이 /members/new로 가고 url 경로가 똑같지만, Get은 form 화면을 열어보고, Post는 이 데이터를 실제 등록하는 게 목표인거죠.

create 메서드에서 MemberForm이 Parameter로 넘어오게 돼요. 이렇게 넘어오는데 이제 뭐가 있냐면,

이 Annotation을 Validation(제대로 되는지 확인) 할 수가 있어요. 지금 회원 이름을 제가 필수로 쓰고 싶단 말이에요.

그러면 여기서 @Valid라고 적어줍니다. 이렇게 하면 @Valid 라는 jakarta의 표준 어노테이션을 쓸 수 있거든요.

그러면 이 MemberForm에 있는 어노테이션을 보고, 스프링이 '어? jakarta.validation 기능을 쓰는구나' 라고 인지를 해서, 이 form에 대해서 @NotEmpty 라든가,

패키지를 가보면 기능이 참 많거든요. 그래서 이런 것들을

Spring에서 잘 되는지 Validation 해줍니다.
자 그래서 Validation 기능을 굉장히 편하게 Annotation 기반으로 쓸 수가 있죠.

자 그 다음에 이제 데이터를 가지고 new address를 만들 거에요. 이것도 아마 되게 편하게 하는 방법이 있을 텐데 저는 그냥 이렇게 하겠습니다. form에 있는 걸 꺼내서 변환하는 걸 보여드리려고 이렇게 예시를 만들었구요. form에서 꺼내서 city, street, zipcode를 가지고 우리의 value object인 address를 만들었죠.

사실 이 MemberForm 안에다가 이렇게 푸셔도 될 거에요. 아무튼 여기서 예제를 보여드리기 위해서 이렇게 했구요.

그 다음에 member.setname 해서 form에서 name 가져오면 되죠. 그리고 member.setaddress를 해서 위에서 만든 address를 넣으면 되겠죠.

그 다음에 memberService에서 join 만드셨던거 기억나죠. 이거를 호출하면 이제 저장이 딱 되는 거에요.
그럼 저장이 끝나면 어디로 가야될까요?
보통 이런 행위들은 뭔가 딱 저장이 되고나면 다시 재로딩 되거나 하면 안좋기 때문에 보통 redirect를 많이 씁니다.

저는 redirect로 홈에 보내버릴게요. 이렇게 하면 첫 번째 페이지로 넘어가게 됩니다.
자 그러면 잘 동작하는지 한번 테스트 해봐야 되겠죠.
자 다시 회원가입을 해볼게요.

이렇게 한 다음에 submit을 딱 해보면,

지금 뭔가 메인으로 돌아왔죠?
자 잘 저장됐는지를 확인해 보겠습니다.

여기 보면 insert into 해서 쿼리가 날라간 거 보이시죠? DB를 까보면 데이터가 있을 거예요.

자 Member 쿼리를 날려보면 방금 제가 저장한 데이터가 잘 넘어가 있죠.
자 이번에는 실패하는 걸 만들어 보겠습니다.

이름이 없으면 어떻게 될까요? 이 상태로 submit을 하면,

일단 스프링 부트가 기본적으로 Whitelabel Error Page를 제공 하는데요. 에러가 나면 기본적으로 /error로 가게 되어있어요. 근데 /error가 없으면 디폴트로 이 Whitelabel Error Page가 나오는데,

보시면 지금 뭔가 에러가 나죠. memberForm에 에러가 있는데,

이 에러는 보나마나 지금 보면 NotEmpty가 걸렸다고 나오죠. 그러니까 우리가 파라미터에 적은 @Valid가 정확하게 인지가 되어서 동작했다는 얘기입니다.
그러면 이렇게 Whitelabel Error Page가 나오면 보기가 좀 그렇잖아요. 제일 간지나는 거는 form으로 돌아가서 그 form에 '사용자 분께서 이름을 입력 안 하셨어요' 라고 해주는 게 제일 깔끔하겠죠.
여기서 이제 스프링을 공부 많이 하신 분들은 아실 텐데,

BindingResult라는 기능이 있습니다. 이게 뭐냐면

이렇게 코드를 짜게 되면 이 form에서 뭔가 오류가 있잖아요. 그럼 원래는 컨트롤러에서 코드가 안 넘어가고 그 오류가 튕겨버리거든요.

근데 뭔가 Validate 한 거 다음에 BindingResult가 있으면 오류가 result에 담겨서,

이 코드가 실행이 돼요.
자 그럼 뭘 할 수 있냐?

만약 result에서 hasError가 있으면, 그러니까 뭔가 error가 있으면 나는 return을 "members/createMemberForm"로 할 거야. 이렇게 해버리면 Spring이랑 thymeleaf가 인티그레이션이 되게 잘 돼있어요.

그래서 지금 라이브러리에 뭐가 있냐면 저희가 처음에 타임리프 스프링 부터 스타터 타임리프를 받았죠. 거기서 또 뭐가 있냐면 타임리프도 있는데, 추가로 thymeleaf-spring 이라는 게 있어요. 그래서 이게 스프링이랑 인티그레이션이 정말 강하게 되어있거든요.

자 그러면은 스프링이 이 BindingResult를 이 화면까지 끌고 가줘요. 그래서 어떤 에러가 있는지를 이 화면에다가 뿌릴 수가 있습니다.
Html을 다시 가볼게요. 아무튼 여기서 result 에러가 지금 생겼죠? 회원명을 제가 입력 안 했으니까.

그러면 코드가 if (result.hasErrors())로 와서 에러가 있다고 인지를 하고 다시 createMemberForm으로 갑니다.
자 결국 화면은 Post로 보냈지만,

화면이 다시 여기로 가버린 거죠. 일단 자바 코드를 수정했기 때문에 다시 돌려볼게요. 참고로 DevTools가 있어서 자바 코드만 리컴파일 해도 스프링이 다시 떠서 정상 동작하는데 그냥 깔끔하게 다시 서버를 띄우겠습니다.
회원 가입으로 가서 아무것도 입력 하지 않고 Submit을 하면

보이시나요? 지금 보면 서버 사이드에서 Validation을 컨트롤러에서 한 건데, 빨간 테두리가 나오고 "회원 이름은 필수 입니다" 메시지가 나왔죠.

MemberForm에서 적어 놨던 바로 그겁니다. 어떻게 된 건지 설명해드릴게요.

자 서버 사이드에서 @Valid로 Validation을 하고 문제가 생겼으니까 여기 BindingResult에 데이터가 하나 들어왔을 거에요.

이 result. 하고 보면 error에 대한 데이터를 찾을 수 있는 메서드가 되게 많아요. 그래서 어쨌든 error가 있으면 다시 createMemberForm으로 보내버린 거에요. 근데 Spring이 뭘 해주냐면 타임리프랑 스프링 부트가 다 인티그레이션 되어 있기도 하고 스프링이 createMemberForm으로 보내면 기본적으로 이 BindingResult를 createMemberForm에 끌고 가서 쓸 수 있게 도와줘요.

그럼 여기가 포인트인데 자 input type은 텍스트인데 field는 name이죠. city, street, zipcode를 다시 여기서 다 뿌려요. th:field="*{name}"인데,

자 여기서 fields라고 해서 방금 그 에러를 쓸 수가 있어요. 'name' 이라는 거에 에러가 있으면 어떻게 합니까?

제가 이 css를 fieldError 빨간 색깔로 껍데기를 만들어버렸습니다.
자 그리고 또 뭘 하냐?

name 필드에 대해서 에러 메시지를 뽑아서 출력을 해주는 거에요.

그렇게 해서 회원 이름은 필수입니다 라고 렌더링이 됩니다.
자 그래서 error가 있으면,

클래스를 css를 fieldError에 빨간 색깔을 반영하기 위해서

이 코드를 넣은 거고요. 그 다음에 메시지를 넣기 위해서

밑에 p 태그를 하나 넣었습니다.

그래서 error가 있으면 if로 그 메시지 에러 중에서 name과 관련된 속성의 에러를 출력해라! 라고 타임 리프에서 이런 기능들 다 제공을 해줍니다.

자 그러면 정말 깔끔하게 이 validation을 만들 수 있죠.

사실 도시, 거리, 우편번호가 다 있는데 이름만 없으면 Submit 하면,

이 데이터가 사라지지 않고 유지가 되죠. 왜냐면 이거는

여기 올 때 form에서 데이터가 다 들어오거든요. 에러가 있더라도 이 form에 있는 데이터도 다시 그대로 createMemberForm으로 가져가게 돼요.

그래서 데이터가 있는 걸 가지고 전부 다 다시 뿌리게 됩니다.
그래서 여기까지 Error를 validation 하는 것까지 한번 쭉 설명을 드렸습니다.
주문 검색을 더 공부해 보고 싶으면,

여기 @NotEmpty 이런거는 jakarta.validation을 검색하면,

이런 자료들이 되게 많아요.

그 다음에 타임리프에 대한 것도 Documents에 가셔서 Spring으로 검색하면 타임리프 + 스프링이 있거든요. 메뉴얼이 엄청 잘 돼 있어서 읽어보시면 여러분들이 원하는 웹페이지가 정말 친절하게 나와있어요.
Validation & Error Message를 찾아보면,

방금 저희가 했던 필드 에러같은 건 어떻게 할 건지, 클래스 어떻게 적용할 건지 다 나와있습니다.
자 그냥 메뉴얼 보고 이것들 하나하나씩 적용하시면 돼요. 자 이제 여기까지 하고요. 이제 Validation 이런 거는 이제 안 할 거예요. 이미 Spring MVC 쪽 잘 쓰시는 분들은 이런 기능 잘 쓰시기 때문에 제가 말씀드린 게 크게 의미가 없을 수 있어요.
자 그런데 이제 이게 궁금하실 수 있어요.

'어 여기에다가 Member 엔티티를 그대로 넣으면 되지 않나요?',

'이런 식으로 넣으면 왜 안되죠? 왜 form을 굳이 만들어서 넣나요?'

왜냐하면 Member 엔티티를 보면 비슷하거든요.
근데 차이가 있어요. 여기 보면, id도 있고 name도 있고 address도 있고 orders도 있잖아요. 그러니까 무슨 얘기냐면 두 개가 딱 안 맞는 거예요.

그리고 Member 엔티티에다가 Validation을 어떻게 할 건가요? 여기에다가도 @NotEmpty 써야하는데 이러면 점점 지저분해진단 말이에요.
정리하면, 컨트롤러에서 화면으로 넘어올 때 Validation이랑 실제 도메인이 원하는 Validation이 다를 수 있단 말이에요. 이 엔티티에다가 막 화면에 왔다 갔다 하는 이런 form 데이터를 쓰기 시작하면 뭔가 이게 안 맞아요. 안 맞으면 억지로 맞춰야 되기 때문에 차라리 깔끔하게 그냥 화면에 딱 fit한 form 데이터를 만들고 그걸로 데이터를 받으시는 게 나아요. 애플리케이션이 진짜 심플하면 방금처럼 엔티티를 바로 데이터를 바인딩 받으셔도 되거든요. 근데 여러분이 실무에서 하실 때는 그렇게 단순한 form 화면이 거의 없어요. 실무에서는 거의 엔티티를 가져다가 그대로 파라미터로 받아서 하기에는 차이가 너무 많아요. 예를 들면, Member에 있어서 name은 꼭 필수인 건 아니죠. name이 없어도 new Member()로 만들어질 수는 있잖아요. 그런데 MemberForm은 이름이 없는 게 용납이 안 되는 거예요.

그래서 form으로 받으시고 컨트롤러 같은 데서 한번 중간에 이렇게 한번 좀 정제를 하신 다음에 딱 필요한 데이터만 채워가지고 이렇게 넘기시는 거를 권장을 합니다.
자 여기까지 해봤고 이제 다음 시간에는 회원목록을 조회하는 기능을 한번 개발해 보겠습니다.
'스프링 > 실전! 스프링 부트와 JPA 활용1' 카테고리의 다른 글
| 상품 등록 (0) | 2024.05.14 |
|---|---|
| 회원 목록 조회 (0) | 2024.05.14 |
| 홈 화면과 레이아웃 (0) | 2024.05.12 |
| 주문 검색 기능 개발 (0) | 2024.05.11 |
| 주문 기능 테스트 (0) | 2024.05.11 |