실전 활용1 JPA _7.웹 계층 개발

링딩·2021년 7월 5일
0

Spring

목록 보기
5/5

※웹 계층 개발

1. 홈 화면과 레이아웃

😶HomeController

@Controller
@Slf4j //lombok 써서 log임
public class HomeController {


    @RequestMapping("/")
    public  String home(){
        log.info("home controller");
        return "home"; //home.html파일로
    }

}


home.html 폼 작성 등을 하고 잠시 결과를 보면 이렇게 HomeController가 구동되었다고 뜬다

※ 템플릿

  1. 상단의 home.html의 th: replace ="fragments/헤더위치 :: 이름 " 에서 해당 위치를 찾아 'header'이름의 fragments 형태를 찾아 리플래쉬 된다.
    => 그러나 (header , footer) 같은 템플릿 파일을 반복해서 포함하기 때문에 하단의 링크처럼 Hierarchical-style layouts을 참고하여 이런 부분도 중복을 제거할 수 있고 여러 방법이 있다.

+ 참고)

build.gradle 파일에서 이렇게 devtools를 찾을 수 있는데 이것 덕분에 html파일에서 내용을 수정하고 '리컴파일(ctrl + shift + F9)'를 해주면 변경된다.

※ 홈화면 세팅

1.부트 스트랩 설치 (디자인을 세팅)

[bootStrap 설치]((https://getbootstrap.com/)를 해서 압축을 풀어준다.

2. 압축을 푼 css, js 파일을 static 하위폴더에 복사해서 붙여줌

3. css 파일 세팅해주기

resources/static/css/jumbotron-narrow.css 추가



2. 회원 등록

=> 이곳에서는 회원기능 중 회원 가입을 눌렀을때 발생될 웹계층을 제작하는 것이 목적.

😉MemberController


@Controller
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;

    @GetMapping("/members/new")
    public String createForm(Model model){

        //여기서 model은 controller에서 View로 넘어갈때 데이터를 실어 넘겨줌
        //곧 memberForm 컨트롤러에서 View로 넘어갈때 new MemberForm()을 실어서 넘겨줌
        model.addAttribute("memberForm" ,new MemberForm());
        return "members/createMemberForm";
    }

    //createMemberForm 이름에서 post타입으로 넘어옴
    //url이 같아도-> get은 화면을 열어보는 것이고 , post는 실제로 등록하는 것이 서로 다름
    //@Valid 해당하는 클래스에 가서 애노테이션들을 다 Validation해준다.
    //앞에 @Valid와 BlindingResult가 같이 뒤에 쓰이게 되면 result에 오류가 잇을경우 이곳에 담김
    @PostMapping(value = "/members/new")
    public String create(@Valid MemberForm form, BindingResult result) {

        if (result.hasErrors()) {//result에 에러 있냐
            return "members/createMemberForm";
        }

        Address address = new Address(form.getCity(), form.getStreet(), form.getZipcode());
        Member member = new Member();
        member.setName(form.getName());
        member.setAddress(address);

        memberService.join(member); //저장
        return "redirect:/"; //재로딩(redirect)해서 홈(/)으로
    }


}

😶MemberForm.java

@Getter @Setter
//폼 계층으로 화면과 서비스 계층을 분리할 것
public class MemberForm {

    @NotEmpty(message = "회원 이름은 필수 입니다")
    private  String name; //name을 필수로 받겠다. -> 값이 비어있으면 오류가 남

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

}

1. '회원가입'의 틀을 제작

-> @GetMapping("/members/new")의 url을 찾아 창을 띄운다

  • model은 현 "Member"컨트롤러에서 View(리턴값)로 넘어갈때 'new MemberForm()'을 담아서 보내준다.
    곧, model에는 createMemberForm.html로 Member 컨트롤러에서 'MemberForm'을 챙겨서 보낸다.

. .

✔해당 MemberForm에서 name필드는 @NotEmpty가 설정되어 있다.

_1) @NotEmpty

[이 블로그에서 참조하여 작성하였습니다.]

값이 null 혹은 "" 빈 문자열이 들어와선 안된다.
=> 곧 'name' 필드는 값이 들어가야 한다.


2. @PostMapping()

이곳에서 회원가입에서 값들을 입력을 받아 저장해준다.
1. @Post 타입으로 받았다
2. @Valid와 BlindingResult 가 함께 있을때
3. 값을 받지 못한 경우에 에러를 어떻게 처리하는가

_1) post와 get의 차이

둘다 같은 url "/members/new" 을 갖고 있찌만 get방식은 화면을 열어본다면 post방식은 실제로 그것을 등록한다는 것에서 다르다.

_2) @Valid와 BlindingResult가 같이 쓰였을때...

이 블로그를 참조하였습니다.

  • @Valid에 해당되는 객체인 MemberForm에서 쓰인 애노테이션들을 Validation 해준다.
  • BlindingResult가 함께 쓰인다면, 이곳에서는 방금 전 객체를 검사하며 바인딩 중 생긴 에러가 담긴다.
  • result.hasErrors()로 에러가 담겨있다면 createMemberForm.html로 날린다.
  • if문에서 에러가 발생되어 createMemberForm 에서는
  • object에 담긴 객체의 프로퍼티를 filed *{필드이름} 형식으로 사용
  • class="${ #fields.hasErrors('name')}? 로 memberForm의 'result'와 같이 'name' 에러가 있다면, 상단의 'fieldError'에서 설정되있는 빨간색 테두리로 바뀌게 된다.
  • if="${#fields.hasErrors('name')}" 에서 에러가 발생되면 object에서 참고하는 객체에서 해당 'name'의 메세지를 가지고 출력

<< name 오류가 떴을때 화면 결과>>




3. 회원 목록 조회

1. MemberController에 회원목록 추가

MemberController.java

@Controller
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;
	.
    .
    .
    (중략)

   //회원목록 눌렀을때
    //주의! 여기서는 어쩔 수 없이 엔티티를 넘겨주었으나
    //API를 이용할때는 절대 엔티티를 외부로 넘겨주거나 노출하면 안된다. ->ex 패스워드 노출 등
    
    @GetMapping("/members")
    public String list(Model model){
        List<Member> members = memberService.findMembers();
        //members객체를 가지고 넘겨줌
        model.addAttribute("members", members);
        return "members/memberList";
    }

주의)

이곳에선 어쩔 수 없이 엔티티를 그대로 넘겨주었으나,
=> Mock객체를 받았지만 List<Member> members=로 엔티티를 다시 이용함.
실제 API를 이용할때는 절대 '엔티티'를 외부로 넘겨주거나 노출해선 안된다 ex)패스워드 노출 등

2. memberList.html 생성

'회원목록'의 html을 templates/members/에 생성해준다.

+)추가 문법

  • th:each="member : ${members}" Get 매핑에서 Mock에 넘겨준 members를 참조하여 바인딩하겠다.
  • ${member.address?.city} 에서 '?'는 null이면 더 진행하지 않겠다 는 의미.



4. 상품등록

Book 외에도 Movie, Album 등이 있지만 Book만 상품으로 만들겠습니다.

1. controller 폴더에 BookForm 생성

@Getter @Setter
public class BookForm {

    private Long id;

    private String name;
    private int price;
    private int stockQuantity;

    private String author;
    private String isbn;


}

2. ItemController 생성하여 Get매핑과 Post 매핑을 각각 만들어 준다.

@Controller
@RequiredArgsConstructor
public class ItemController {

    private final ItemService itemService;

    @GetMapping("/items/new")
    public String createForm(Model model) {
        model.addAttribute("form", new BookForm());
        return "items/createItemform";
    }

    //Validation 안써줌 (여기서 단순하게 해주려고)
    @PostMapping(value = "/items/new")
    public String create(BookForm form) {

       //웬만해서 이처럼 setter 안쓰길 바람..
        // ->담엔 createBook()처럼 파라미터로 넘기도록 해라
        Book book = new Book();
        book.setName(form.getName());
        book.setPrice(form.getPrice());
        book.setStockQuantity(form.getStockQuantity());
        book.setAuthor(form.getAuthor());
        book.setIsbn(form.getIsbn());

        itemService.saveItem(book); //저장
        return "redirect:/";
    }

}

_1. GetMapping()

  • model.addAttribute("form", new BookForm()); 에서 'new BookForm()'을 가지고 'createItemform으로 넘겨주었다.

_2. PostMapping()

  • 예제를 단순하게 하기 위해서 Validation은 해주지 않았다.
  • 위와 같이 입력받은 정보를 가지고 'form'에서 입력받은 값으로, book 객체의 name, price, stockQuantity, author, isbn 에 값을 넣어준다.
  • 웬만해서 이처럼 setter로 값을 입력받는 것보다 createBook() 처럼 따로 메서드를 만들어 파라미터로 입력받는 것을 더 추천.

3. createItemform.html

상단의 Get매핑에서 model객체로 함께 넘어온 'new Book()'의 'form'객체로 값이 저장된다.


4. 결론

  1. 이렇게 createItemform.html에서 값을 넘겨 받은 'form'은, 'ItemController'의 PostMapping()에서 'form'을 파라미터로 들어간다.
    ⇨ 곧 등록 폼에서 데이터가 입력되고 Submit 버튼을 누름과 함께 'POST방식'으로 요청된다.
  2. 'ItemController'의 PostMapping()에서 book 객체의 필드들에 값을 넣고 저장 한다.
  3. 그리고 리턴값으로 return "redirect:/items" 로 상품 목록에 보내준다.

DB 출력시

이렇게 Book을 상품등록해줘서 DB에서는 'DTYPE=B'가 뜨며, 나머지 'ACTOR'나 'DIRECTOR', 'ARTIST', 'ETC' 와 같은 경우 @Inheritance(strategy = InheritanceType.SINGLE_TABLE) 싱글 테이블로 설정해줬기 때문에 같이 나온다!



5. 상품 목록

※ ItemController의 Get매핑

  • "/items" url과 매핑 되어있음
  • model객체에 'items'를 가지고 return "items/itemList"에 위치한 파일로 보내준다.

📣 itemList에서는 model 객체에서 넘어온 'items'로 값을 text에 넣어준다.

※ 상품 수정 링크

상품 목록에서는 itemList에서는 아래와 같이 '수정' 버튼이 생기는데

이와 같이 itemList의 클래스에서 url이 id에 따라 달라진다.

ↆↆↆ



6. 상품 수정

ItemController


@Controller
@RequiredArgsConstructor
public class ItemController {

  private final ItemService itemService;

	.
  .
  (중략)

  //상품 수정 폼
  @GetMapping("items/{itemId}/edit")
  //@PathVariable 'itemId'가 변경 되기 때문
  public String updateItemForm(@PathVariable("itemId") Long itemId, Model model){

      //예제 단순화를 위해 Item 타입이지만 Book타입으로 캐스팅함
      Book item=(Book) itemService.findOne(itemId);

      //item엔티티 보내지 않고 우리는 BookForm을 보낼 것
      BookForm form = new BookForm();

      //item엔티티로부터 저장받은 필드들의 값을 다시 form에 세팅
      form.setId(item.getId());
      form.setName(item.getName());
      form.setPrice(item.getPrice());
      form.setStockQuantity(item.getStockQuantity());
      form.setAuthor(item.getAuthor());
      form.setIsbn(item.getIsbn());

      //model에 form객체를 담아서 보내줌
      model.addAttribute("form", form);
      return "items/updateItemform";

  }

  /**
   * 상품 수정
   */
  @PostMapping(value = "/items/{itemId}/edit")
  public String updateItem(@ModelAttribute("form") BookForm form) {
      
      Book book = new Book();
      
      book.setId(form.getId());
      book.setName(form.getName());
      book.setPrice(form.getPrice());
      book.setStockQuantity(form.getStockQuantity());
      book.setAuthor(form.getAuthor());
      book.setIsbn(form.getIsbn());
      
      itemService.saveItem(book);//수정된 book객체 필드들 저장
      return "redirect:/items"; //상품목록으로 반환
  }
}

1. 상품 수정 폼

_1. GetMapping()

  • url을 ("items/{itemId}/edit") 로 정해준다.
    이때 itemId는 계속 변경이 되기 때문에 @PathVariable("itemId") Long itemId 멤버필드를 넣어줘야 한다.

    (1) item 엔티티로부터 가져온 필드 값들을 'BookForm'의 form 객체에 전부 세팅해준다.
    (2) form객체에 필드들은 수정 전 값들이 들어가 있다.
    (3) form객체를 담은 'model'을 return하는 곳에 넘겨 준다.


2. 상품 수정

  1. 그렇게 form으로 값이 수정되어 updateItemform.html으로부터 넘어오고 'book'객체로 그 값들을 다시 옮겨준다.
  2. 넘겨받은 값들은 다시 db에 저장후 return 값에 의해 '상품목록'창으로 넘긴다.

_1. @ModelAttribute("?")

  • 이 form을 다시 가져온 것



3. 결과

7. 변경감지와 병합(merge)

포멜로 님 Bing9님의 jeongdalma참조하였습니다.

※ 준영속 상태?

영속 상태였었다가 연속성 컨텍스트에서 더이상 관리를 하지 않는 상태를 뜻한다.

※ 준영속 엔티티?

  • 영속성 컨텍스트가 더는 관리하지 않는 엔티티
  • 식별자 값을 소지한 상태(이미 한 번 영속상태였으므로 소지함.)

1. 준영속 엔티티를 수정하는 방법

_1) 변경감지 (강추!)

  • 영속성 컨텍스트에서 엔티티를 다시 조회한 후에 데이터를 수정하는 방법
  • 트랜잭션 안에서 엔티티를 다시 조회 , 변경할 값을 선택 -> 트랜잭션 커밋 시점에 "변경 감지"가 발생하여 수정 됨.
  • 이 동작에서 데이터베이스 UPDATE SQL 실행

_2) 병합 (merge)

*병합 동작 방식

  1. merge() 를 실행
  2. 파라미터로 넘어온 '준영속 엔티티'의 식별자 값(member)으로 1차 캐시에서 엔티티를 조회
    2-1. 만약 1차 캐시에 엔티티가 없으면 데이터베이스에서 엔티티를 조회하고, 1차 캐시에 저장한다.
  3. 조회한 영속 엔티티( mergeMember )에 member 엔티티의 값으로 바꿔줌.
    member 엔티티의 모든 값
    을 mergeMember에 밀어 넣는다. 이때 mergeMember의 “회원1”이라는 이름이 “회원명변경”으로 바뀜.
  4. '영속 상태'인 mergeMember를 반환

*간단히 설명한 동작 방식

  1. 준영속 엔티티의 식별자 값으로 영속 엔티티를 조회한다.
  2. 영속 엔티티의 값을 준영속 엔티티의 값으로 모두 교체한다(merge)
  3. 트랜잭션 커밋 시점에 변경 감지 기능이 동작해서 데이터베이스에 UPDATE SQL이 실행

√둘의 차이

(주의):
'변경 감지' 기능을 사용하면 원하는 속성만 선택해서 변경할 수 있지만, '병합'을 사용하면 모든 속성이 변경된다.
->병합시 값이 없으면 null 로 업데이트 할 위험도 있다. (병합은 모든 필드를 교체한다.)

✔결론

결론적으로는 '변경감지'를 더 추천한다!!

  • 앞으로 '변경감지'를 이용해 엔티티를 변경하는 것이 좋다.
  • 컨트롤러에서 엔티티를 생성하는 것이 아니라, 트랜잭션이 있는 '서비스 계층'에서 식별자 id와 변경할 데이터들을 파라미터로 넘겨주어 영속상태의 엔티티를 조회하고 데이터를 변경해주는 것이 best!



8. 상품 주문

1. 주문 폼 이동

메인화면에서 '상품주문' 클릭->/orderurl 로 'GET 방식으로 호출'

OrderController 의 createForm() 메서드

=> model객체에 모든 고객정보와 상품정보를 담아 View로 넘긴다.

2. 주문 실행

1. orderItemForm 은 전달받은 'members'와 'items' 객체로 인해 인자값을 전달 받는다.

2. 폼에서 입력받은 주문할 회원과 상품 그리고 수량을 선택하여 제출한다.

-> 해당 /orderurl을 'post'방식으로 호출한다.

3. 컨트롤러의 order()에서 고객 식별자( memberId ), 주문할 상품 식별자( itemId ), 수량( count ) 정보를 받아서 주문 서비스에 '주문'을 요청



9. 주문 목록 검색, 취소

1. 주문목록 검색

(1) 주문 등록 후 or 홈화면에서 주문목록을 클릭시 'GET방식'으로 url이 연결된다.

OrderController

_(1). @ModelAttribute("orderSearch")

이렇게 model 객체에 자동으로 담기며 form에서 값을 받아온다 =>model.addAttribute("orderSearch", orderSearch); (이게 생략 된 것과 같음)

(2) model객체를 그렇게 orderList View로 넘겨주면 form 태그에 th:object="${orderSearch}"에서 이름과 주문상태 검색창은 object를 통해 값을 가져와 옵션값을 비춘다.

(3) 회원이름과 주문상태를 맞춘후 '검색'버튼을 클릭시, 다시 'GET방식'에서 'orderSearch'로 받아온 검색 값을
List<Order> orders = orderService.findOrders(orderSearch); 통해 model객체에서 다시 View로 보내준다.

2.주문 취소

주문검색을 하고, 만약 주문상태가 'ORDER'이라면 자바스크립트를 통해 'POST방식'을 호출하여 주문취소를 해줄 수 있다.

(1) "'javascript:cancel('+${item.id}+')'" 에서 주문id를 이용해 cancel()메서드를 호출한 후, form객체에 'POST방식'으로 url을 넘겨준다.

PostMapping으로 전달 받은 cancelOrder()메서드


(2) 자바스크립트로부터 url로 호출되어 주문id로 cancelOrder()메서드를 실행해주고 '주문내역'으로 돌아온다.

_(1) @PathVariable()

[이 블로그를 참조하였습니다.]

@PostMapping이나 @GetMapping 등의 {변수명}
@PathVariable("변수명")뒤의 자료형과 변수에 전달받을 값으로 넣는다.

👀주문취소 결과

profile
초짜 백엔드 개린이

0개의 댓글