Per ardua ad astra !
I'm On My Way
Per ardua ad astra !
전체 방문자
오늘
어제
  • 분류 전체보기 (126)
    • Algorithm (50)
      • 백준 (30)
      • SWEA (3)
      • JUNGOL (3)
      • Programmers (5)
      • LeetCode (2)
    • 안드로이드 개발 (6)
      • Java로 개발 (0)
      • Kotlin으로 개발 (3)
    • Spring (41)
      • Spring기본 (17)
      • JPA기본 (15)
      • JPA활용 SpringBoot 기본 (9)
      • API 개발 기본 (0)
    • 네트워크 (3)
    • 운영체제 (0)
    • Life (3)
      • 책 (0)
      • 자기계발 (1)
      • 일상 (2)
    • 노마드코더 (3)
      • python으로 웹 스크래퍼 만들기 (3)
    • 프로그래밍 언어 (17)
      • Java 기본 (2)
      • 코틀린 기본 (15)

블로그 메뉴

  • 홈
  • 방명록

인기 글

hELLO · Designed By 정상우.
Per ardua ad astra !

I'm On My Way

Spring/JPA활용 SpringBoot 기본

MVC 계층 개발 1. 홈화면, 회원등록 및 목록 조회 (Spring-boot-starter-validation 사용) (thymeleaf 배우기) (BindingResult, @Valid)

2021. 8. 23. 12:09

1. 홈회면과 레이아웃 설정

- 화면 레이아웃 구성을 더 쉽게 하도록 도와주는 프레임워크인 bootstrap을 설치해서 추가하자

 -> https://getbootstrap.com/ 접속 ! 설치 !

 -> resources/static 폴더에 다운받은 css, js 폴더 전체를 넣어주자

 

- 스프링 부트 타임리프 기본 설정(default)

spring:
 thymeleaf:
 prefix: classpath:/templates/
 suffix: .html

 

스프링 부트 타임리프 viewName 매핑
resources:templates/ +{ViewName}+ .html
반환한 문자(ex. home )과 스프링부트 설정 prefix , suffix 정보를 사용해서 렌더링할 뷰( html )를 찾는다.

 

SpringBoot를 사용하지 않았을 때 엄청 불편함을 느꼈던 것이 기억난다. 

MvcConfig.java 파일을 만들고 WebMvcConfigurer을 implements하여 
configureViewResolvers()메소드를 통해서 prefix와 suffix를 지정해줬던 기억이 있다. 
그리고 이 config를 web.xml에 등록해야했다. 

참조: https://ggp03016.tistory.com/29?category=826311

 

 

2. 폼 객체 생성

 

MemberForm

MemberForm

package jpabook.jpashop.domain.controller;

import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.NotEmpty;

@Getter
@Setter
public class MemberForm {

    @NotEmpty(message = "회원 이름은 필수 입니다.")
    private String name;

    private String city;
    private String street;
    private String zipcode;
}

 

  • 폼 객체를 사용해서 화면 계층과 서비스 계층을 명확하게 분리한다. (실무에선 엔티티를 그대로 받는경우 거의X)
    => 데이터를 주고받는 커맨드 객체로써 폼객체를 생성하고 데이터 전달을 목적으로 사용하면 편리하다.

  • spring-boot-starter-validation 

    springboot 2.3.0 부터는 spring-boot-starter-validation api를 자동으로 제공하지 않기 때문에
    별도로 의존을 추가하여 사용해야한다. 

    의존 추가

    gradle의 경우
    dependencies {
    	implementation 'org.springframework.boot:spring-boot-starter-validation'
    }


    Maven의 경우
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>


    추가적인 validation 어노테이션을 정리해 놓으셨다. ↓
    https://gaemi606.tistory.com/entry/Spring-Boot-ResponseBody-%EA%B0%81-%ED%95%AD%EB%AA%A9%EC%97%90-%ED%81%AC%EA%B8%B0-%ED%95%84%EC%88%98-%EA%B0%92-%EC%84%A4%EC%A0%95-spring-boot-starter-validation  

    영상을 참고했다.
    https://www.youtube.com/watch?v=cP8TwMV4LjE 

3. Controller 생성

 

MemberController

package jpabook.jpashop.domain.controller;

import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.service.MemberService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.validation.Valid;

@Controller
@Slf4j
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;

    @GetMapping("/members/new")
    public String createForm(Model model) {
        log.info("Member Controller");
        model.addAttribute("memberForm", new MemberForm()); // validation 확인도 가능
        return "members/createMemberForm";
    }

    @PostMapping("/members/new")
    public String finishFrom(@Valid MemberForm memberForm, BindingResult bindingResult) {
        if(bindingResult.hasErrors()){
           return "members/createMemberForm";
        }
        Member member = new Member();
        member.setName(memberForm.getName());
        member.setAddress(new Address(memberForm.getCity(), memberForm.getStreet(), memberForm.getZipcode()));

        memberService.join(member);
        return "redirect:/";
    }

}

 

4. view

 

createMember.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header"/>
<style>
 .fieldError {
 border-color: #bd2130;
 }

</style>
<body>
<div class="container">
    <div th:replace="fragments/bodyHeader :: bodyHeader"/>
    <form role="form" action="/members/new" th:object="${memberForm}"
          method="post">
        <div class="form-group">
            <label th:for="name">이름</label>
            <input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력하세요"
                   th:class="${#fields.hasErrors('name')}? 'form-control fieldError' : 'form-control'">
            <p th:if="${#fields.hasErrors('name')}"
               th:errors="*{name}">Incorrect date</p>
        </div>
        <div class="form-group">
            <label th:for="city">도시</label>
            <input type="text" th:field="*{city}" class="form-control"
                   placeholder="도시를 입력하세요">
        </div>
        <div class="form-group">
            <label th:for="street">거리</label>
            <input type="text" th:field="*{street}" class="form-control"
                   placeholder="거리를 입력하세요">
        </div>
        <div class="form-group">
            <label th:for="zipcode">우편번호</label>
            <input type="text" th:field="*{zipcode}" class="form-control"
                   placeholder="우편번호를 입력하세요">
        </div>
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
    <br/>
    <div th:replace="fragments/footer :: footer"/>
</div> <!-- /container -->
</body>
</html>

 

jsp <-> thymeleaf

스프링의 내용을 다루는 부분은 옛날에 학습했던 내용과 같지만
(참고: https://ggp03016.tistory.com/33?category=826311)
jsp <-> thymeleaf 의 내용이 다름으로 thymeleaf가 얼마나 편리하고 좋은지 확인해보는 것도 좋을 듯하다.

th:object는 폼에서 서버단으로 값을 넘길 때 object 에 지정한 객체에 값을 담아 넘겨줄수 있다.
th:field="*{필드명}"으로 쉽게 필드에 접근할 수 있다. 

 

 

5. Error처리


@Valid와 BindingResult를 활용하여 오류시 문제 처리하기


MemberForm에서 @NotEmpty를 걸어놓은 name 필드의 값이 서버 단에서 값이 없는 상태로 넘어왔다면 오류를 체크하도록 하는 것이 좋다. 처리과정은 다음과 같다.

1. MemberForm 클래스에는 validation의 @NotEmpty 어노테이션이 있다. 만약 값이 없을 경우 오류를 발생시키는 어노테이션인데 일단 이 어노테이션의 제약조건 위반 오류가 발생했다고 가정해보자.

2. Controller의 매개변수 @Valid memberForm에 문제가 발생하면 bindingResult에 결과값에 에러가 담겨서 왔을 것이다.

3. bindingResult에 에러가 있으면 다시 members/createMemberForm 화면으로 돌아가도록 설정한다.

4. 해당 화면으로 돌아왔을 때, 똑같은 화면이지만 bindingResult의 에러 데이터가 함께 날아간다.

5. 타임리프의 코드를 확인해보면 fields에 에러데이터를 보낸것을 알 수 있고 ${fields.hasErrors('name')}? 로 접근한 것을 확인할 수 있다.

따라서,
css로 에러가 발생했을 때 빨간색을 입히기 위한 코드:

<input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력하세요"
 th:class="${#fields.hasErrors('name')}? 'form-control fieldError' : 'form-control'">


경고 메세지를 띄우기 위한 코드:

<p th:if="${#fields.hasErrors('name')}" th:errors="*{name}">Incorrect date</p>​

 

 

+ 추가적인 공부

 

MemberService에서 

    private void validateDuplicateException(Member member) {
        List<Member> findMembers = memberRepository.findByName(member.getName());
        if(!findMembers.isEmpty()){
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        }
    }

메소드를 회원가입시 실행시켜서, 이름으로 회원을 조회해서 이미 존재한다면 exception을 발생시키게끔 유도했었다.

클라이언트가 홈페이지를 이용할 때 익셉션화면을 마주하지 않도록 위처럼 똑같이 에러처리를 해야한다. 

 

MemberController

    @PostMapping("/members/new")
    public String finishForm(@Valid MemberForm memberForm, BindingResult bindingResult) {
        if(bindingResult.hasErrors()){
           return "members/createMemberForm";
        }
        Member member = new Member();
        member.setName(memberForm.getName());
        member.setAddress(new Address(memberForm.getCity(), memberForm.getStreet(), memberForm.getZipcode()));

        try {
            memberService.join(member);
        } catch (IllegalStateException e){
            bindingResult.addError(new FieldError("memberForm", "name", "이미 존재하는 이름입니다."));
            return "members/createMemberForm";
        }
        return "redirect:/";
    }

try-catch 문으로 exception이 발생할 수 있는 join문을 담고, 익셉션이 발생했을 때 에러 처리를 해준다.

 

BindingResult의 addError 함수를 이용한다. addError안에 FieldError 객체를 생성해서 넣어주자.

FieldError( "오류 발생가능한 objectName", "오류 처리해줘야할 필드", "deafult Message") 

 

ex)

bindingResult.addError(new FieldError("memeberForm", "name", "이미 존재하는 이름입니다."));

 

오류처리 결과

 

 

6. 목록 조회

memberList.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>
<div class="container">
    <div th:replace="fragments/bodyHeader :: bodyHeader" />
    <div>
        <table class="table table-striped">
            <thead>
            <tr>
                <th>#</th>
                <th>이름</th>
                <th>도시</th>
                <th>주소</th>
                <th>우편번호</th>
            </tr>
            </thead>
            <tbody>
            <tr th:each="member : ${members}">
                <td th:text="${member.id}"></td>
                <td th:text="${member.name}"></td>
                <td th:text="${member.address?.city}"></td>
                <td th:text="${member.address?.street}"></td>
                <td th:text="${member.address?.zipcode}"></td>
            </tr>
            </tbody>
        </table>
    </div>
    <div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>

List<Member>를 members파라미터로 받아서 그대로 사용한다. List를 자바의 iter구문 처럼 사용하기 쉽게 설정했고 html의 태그로 대부분 해결 가능한 것이 thymeleaf의 장점인 듯 하다. 

 

MemberController

    @GetMapping("/members")
    public String findMembers(Model model){
        List<Member> members = memberService.findMembers();
        model.addAttribute("members", members);
        return "members/memberList";
    }

위 코드를 MemberController에 추가하면 끝이다. 

하지만 주의사항은 가능한 Member엔티티를 그대로 사용하지 않는게 좋다고 하셨다. 현재의 경우는 목록 조회시 사용되는 속성이 Member의 것과 완전 일치하고 그리고 화면템플릿에 전달해도 괜찮은것이 서버단에서 원하는 데이터만 찍어서 출력하는 것이기 때문에,

 

하지만 !! API를 만들때는 이유를 불문하고 절대 엔티티를 외부로 반환하면 안된다고 하셨다.

이유1: 필드가 그대로 노출될 수 있음 (ex password) 

이유2: API 스펙이 변해버린다.  

 

가장 권장하는 경우는 DTO를 만들어서 데이터를 전달하는게 낫다. !!

 

 

SpringBoot-Basic-main.zip
1.40MB

 

 

 

이 게시물은 '실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발' 강의를 수강하고 정리한 내용임을 밝힙니다.

출처: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-%ED%99%9C%EC%9A%A9-1/

 

실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 인프런 | 강의

실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다., 본

www.inflearn.com

 

 

 

    'Spring/JPA활용 SpringBoot 기본' 카테고리의 다른 글
    • MVC 계층 개발 3. 주문, 주문 취소, redirect 주의, Transaction 주의
    • MVC 계층 개발 2. 상품 개발, 변경 감지와 병합
    • 주문 도메인 개발
    • 상품 도메인 개발, @Transactional에 대한 궁금증 해소
    Per ardua ad astra !
    Per ardua ad astra !
    개발자 지망생이며 열심히 공부하고 기억하기 위한 블로그입니다.

    티스토리툴바