주문목록검색 취소

1. OrderController 클래스에서

 

@GetMapping 작성

@PostMapping작성

사진

 

2. OrderList.html 파일 작성

 

 

3. 특정이름으로 검색이

마주한 에러 및 문제해결

-OrderRepositoryJPA Criteria 구문 작성

-OrderService 클래스에 findAllCriteria로 변경

 

실습

화면출력

정리

1.orderList.html파일

-검색조건 양식(form)이 있어야 한다.

 

회원명 구문

주문상태 구문

검색 구문

->검색 버튼을 누르면

 

위에 구문에서 작성하고 선택한 옵션값들을 보낸 값들이

OrderController 클래스에 OrderSearchmemberNameOrderStatus에 바인딩이 된다.

다시 OrderController에 와서 바인딩 된 상태로 return에 지정한 order/orderList로 넘어가게 된다.

 

enum typeorder,cancel을 둘다 값을 OrderStatus 루프를 통해 뿌리고 select를 해야 한다.

타임리프의 문법을 사용해서 값을 뿌릴 수 있다.

, enum에 잇는 values를 가져와서 값을 뿌리게 된다.

 

 

<tr th:each="item : ${orders}"> 구문 설명

<td th:text="${item.id}"></td>

<td th:text="${item.member.name}"></td>

<td th:text="${item.orderItems[0].item.name}"></td>

<td th:text="${item.orderItems[0].orderPrice}"></td>

<td th:text="${item.orderItems[0].count}"></td>

<td th:text="${item.status}"></td>

<td th:text="${item.orderDate}"></td>

<td>

 

-orders를 주문으로 돌려서 나오게 한다.

 

 

<a th:if="${item.status.name() == 'ORDER'}" 구문설명

href="#" th:href="'javascript:cancel('+${item.id}+')'"

class="btn btn-danger">CANCEL</a>

 

-상태가 order이면 cancel버튼이 나오도록 자바 스크립트로 작성을 했다.

cancel을 누를 시에 자바스크립트 캔슬이 호출되어 하기구문으로 넘어간다.

 

<script>

function cancel(id) {

var form = document.createElement("form");

form.setAttribute("method", "post");

form.setAttribute("action", "/orders/" + id + "/cancel");

document.body.appendChild(form);

form.submit();

}

</script>

 

- 구문에서 form 내에 해당하는 내용들을 위해 구문작성을 만들어야 한다.

 

-@PostMapping 방식으로 cancelOrder을 받아와서

redirect으로 화면에 cancel상태의 화면을 출력한다.

 

 

cancel버튼클릭시 화면 및 cancel상태의 목록검색

 

특정이름없이 전체목록출력

상품주문

1. OrderController 클래스 생성 및 세팅

 

사진

 

화면출력

 

*번외로 알아본것들

ordercontroller에서 memberid를 직접찾아서 보내면 되지 않을까에 대한 고민들

=>controller 로직도 지저분해진다.

 

커맨드성

외부에서 식별자만 넘기고

서비스에서 엔티티를 찾는 것부터 거기서 하면

엔티티의 값들도 엔티티를 조회해야 영속상태로 진행을 한다.

 

가급적이면 핵심비즈니스 로직이 있는 경우엔

밖에서 엔티티를 찾아서 넣는 것 보다 식별자만 넘겨주고

핵심비즈니스 로직을 안에서 찾게 되면 영속성 컨텍스트가 존재하는 상태에서 조회가 가능하다.

주문하면서 멤버가 바뀌게 되더라도 더티체킹이 되어 자연스럽게 적용이 된다.

밖에서 가지고 오게 되면 트랜잭션 없이 이루어지는 것이라 더티체킹이 되지 않는다.

 

마주한문제

상품주문버튼 클릭시 에러

코드수정

 

에러가 발생하지 않는다.

상품수정

 

등록이나 조회는 쉬운편이지만, 수정은 복잡하다.

jpa에서 어떠한 방법으로 수정하는 것이 좋을 것인지(정석적인지)에 대해

고민해서 선택해야 한다. 방법중엔 변경감지, 병합이라는 방법이 있다고 한다.

 

jpa의 가이드는 변경감지를 best practice라고 권한다.

 

사진(item controller 수정구문)

 

updateItemForm.html 파일

-createItemForm 파일과의 차이점은 기존에 있는 데이터를 보유하고 있다의 정도이다.

 

*form-control : formcontrol로 명시할 경우 form이 제대로 설정이 되지 않는다.

 

사진

 

사진(수정버튼)

사진(수정화면)

 

*itemId 취약점

-브라우저창에서 상품수정시에 format이 넘어올 때에 인위적으로 아이디를 조작해서 넘길 수 있다.

 서비스계층이든 뒷단이든 유저가 item에 대해 권한이 있는지 체크하는 로직이 있어야 한다.

 

 

2. 변경감지와 병합

JPA에서 변경감지를 모르면 시간을 많이 낭비할 수 있다.

 

JPA기본 메커니즘

a. JPA값 변경시 예제

 

@RunWith(SpringRunner.class)

@SpringBootTest

public class ItemUpdateTest {

 

@Autowired

EntityManager em;

 

@Test

public void updateTest() throws Exception{

Book book = em.find(Book.class, primarykey:1L); //1번 데이터베이스에 있다고 가정한다.

 

//트랜잭션(TX)

book.setName("asdfasdf");

->트랜잭션안에서 이름을 변경하고 트랜잭션 커밋할때에 JPA가 자동으로 변경된구문을 찾아서

업데이트 쿼리를 자동 생성해서 데이터베이스에 생성한다.

이것을 변경감지 == dirty checking이라고 한다.

이 매커니즘을 기본으로 jpa 의 엔티티를 바꿀 수 있다.

 

이것과 비슷한 예제가 했었던 실습구문에 있다.

setStatus의 상태만 바꾸고 jpa가 트랜잭션 커밋시점에 바뀐 것을 찾아서 업데이트 쿼리를 이용해 디비에 전달하여 커밋을 완료한다. 엔티티매니저 merge와 같은 행위를 따로 해주지 않아도 된다.

 

정리

-엔티티가 영속성상태로 관리 되면 값만 바꾸면 변경된 JPA가 트랜잭션 커밋시점에 변경된값을 알고,

 디비에 반영시킨다.

 

문제는 준영속 엔티티를 이해해야 한다.

-jpa 영속성 컨텍스트가 더 이상 관리하지 않는 엔티티를 말한다.

 

새로운 객체는 새로운 객체이지만 아이디는 세팅이 된 상태이다.

JPA에 작업을 한번 거친 식별자(itemId)가 있는 것은 준영속 엔티티라고 한다.

데이터베이스에 들어갔다 나온 데이터라 JPA가 식별할 수 있는 아이디를 가지고 있다.

여기에서 준영속 엔티티는 book이다.

포트폴리에서는 내가 직접 만든 new Book이고, JPA가 관리하지 않는 준영속 엔티티이다.

jpa 영속성 컨텍스트가 관리하는 엔티티는 변경된 것을 알지만 준영속 엔티티는 그렇지 않다.

아무리 준영속엔티티에 정보를 변경해도 변경이 되지 않는다.

 

어떻게 하면 준영속 엔티티를 수정할 수 있을까에 대해 알아보기로 한다.

 

준영속 엔티티를 수정하는 2가지 방법

-변경감지기능사용(dirty checking)

-병합(merge)사용

 

변경감지 기능 사용

@Transactional

void update(Item itemParam) { //itemParam: 파라미터로 넘어온 준영속 상태엔티티이다.

 

Item findItem = em.find(Item.class, itemParam.getId()); 다시 동일한 엔티티를 조회한다.

 

findItem.setPrice(itemParam.getPrice()); //데이터를 수정한다.

}

 

== 비슷한 예제

@Transactional

public void updateItem(Long itemId, Book param) {

 

private final ItemRepository itemRepository;

 

@Transactional

public void saveItem(Item item) {

itemRepository.save(item);

}

 

@Transactional

public void updateItem(Long itemId, Book param) {

//파라미터로 넘어온 준영속 상태엔티티이다.

 

Item findItem = itemRepository.findOne(itemId);

//다시 동일한 엔티티를 조회한다.

 

findItem.setPrice(param.getPrice());

findItem.setName(param.getName());

findItem.setStockQuantity(param.getStockQuantity());

//데이터를 수정한다.

}

 

 

설명

-@Transactional 안에서 파라미터로 넘어온 준영속 상태엔티티를 부른다.

-다시 동일한 엔티티를 조회한다.(findItem)

-넘어온 준영속상태의 엔티티의 값을 다시 재세팅(변경/수정)을 한다.

-트랜잭션이 커밋할 시에, flush를 보내어, 변경된 값을 찾아( 변경감지 : dirty checking)내어 감지한다.

-데이터베이스 update sql 실행하여 변경된 값이 전달 된다.(데이터를 조회 한다.)

 

->이것이 더 나은 수정방법이다.

 

 

2. 병합방법

-포트폴리오에 사용한 방법은 병합방법이다.

-준영속 상태 엔티티를 영속 상태로 변경해주고자 할 때 사용하는 기능 방법이다.

 

예제1)

@Transactional

vodi update(Item itemParam) { //itemParam : 파라미터로 넘어온 준영속 상태의 엔티티

Item mergeItem = em.merge(item);

}

 

병합 : 기존에 있는 엔티티

병합 동작 방식 정리

1. 준속 엔티티의 식별자 값으로 속 엔티티를 조회한다.

2. 속 엔티티의 값을 준속 엔티티의 값으로 모두 교체한다.(병합한다.)

3. 트랜잭션 커밋 시점에 변경 감지 기능이 동작해서 데이터베이스에 UPDATE SQL이 실행

 

주의

-변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경할 수 있지만,

 병합을 사용하면 모든 속성이 변경된다. 병합시 값이 없으면 null로 업데이트 할 위험도 있다.

 (병합은 모든 필드를 교체한다.) -> 그래서 실무에서 병합기능은 위험하다.

 

예를들어

 

Book book = new Book();

book.setId(form.getId());

book.setName(form.getName());

book.setPrice(form.getPrice()); -> 이부분을 빠뜨리면 pricenull로 업데이트가 된다. book.setStockQuantity(form.getStockQuantity());

book.setAuthor(form.getAuthor());

book.setIsbn(form.getIsbn());

 

itemService.saveItem(book);

return "redirect:/items";

}

 

*자세한 원리는 매뉴얼에 보면 있다.

 

유지보수성 측면에서

@Transactional

public void updateItem(Long itemId, Book param) {

//파라미터로 넘어온 준영속 상태엔티티이다.

 

Item findItem = itemRepository.findOne(itemId);

//다시 동일한 엔티티를 조회한다.

//findItem.change(name, price, stockQuantity);

->이렇게 해주어야 change만 확인하면 어디가 변경된지 쉽게 추적할 수있다.

 

findItem.setPrice(param.getPrice());

findItem.setName(param.getName());

findItem.setStockQuantity(param.getStockQuantity());

//데이터를 수정한다.

}

 

가장 좋은 해결 방법

엔티티를 변경할 때는 항상 변경 감지를 사용할 것.

컨트롤러에서 어설프게 엔티티를 생성하지 마세요.

트랜잭션이 있는 서비스 계층에 식별자(id)와 변경할 데이터를 명확하게 전달할것.

상품목록

1. itemController 클래스에서 추가 작성

 

ItemController에서 권장사항

public String create(BookForm form) { ~구문에서

create book을 해서 파라미터를 넘기는 것이 좋은 설계이다.

set을 간소화할 수 있다.

static 생성자 메서드를 가지고 의도에 맞게 사용하는 것이 좋다.

 

사진

2.itemList.html 생성 및 작성

 

사진

 

에러해결과정

1.return의 경로가 “items/itemsList“로 되어 있었다.

items/itemList로 변경하니 상품목록이 나온다.

 

에러화면

수정

출력화면

상품등록

 

1.BookForm 클래스 생성

 

 

2.ItemController 클래스 생성

 

ItemController에서 권장사항 3

public String create(BookForm form) { ~구문에서

static 생성자 메서드를 가지고 의도에 맞게 사용하는 것이 좋다. set을 간소화할 수 있다.

create book을 해서 파라미터를 넘기는 것이 좋은 설계이다.

 

private final MemberService memberService

Controller가 서비스(MemberService)를 사용한다.

 

*Model model이란?

model.addAttribute(“정보”)

-> controller로 뷰로 넘어갈 때 데이터를 실어서 정보를 넘긴다.

return "items/createItemForm“ ->여기로 화면이 출력이 된다.

createItemForm을 위한 html을 만들어야 한다.

controller가 화면을 이동할 때 빈 BookForm을 가지고 간다,.

validation같은 역할을 해주기 때문에 빈 BookForm을 가지고 가도록 한다.

데이터가 넘어가니 데이터의 화면출력을 위한 createItemForm.html 생성이 필요하다.

 

*필드값도 추적이 가능하다.

model.addAttribute(“정보”) 가 데이터를 실어서 정보를 넘길 때

controller가 화면을 이동할 때 빈 BookForm을 넘겨주므로,

name, price 등의 필드값도 추적이 가능하다.

 

사진

 

 

3. createItemForm.html 생성

 

실습

 

마주한문제

에러화면사진

 

에러문제해결절차

1. ItemController에서 받아오는 modelattribute이름 확인

2. return 되는 경로 확인

3. createItemForm.html 코드확인

 

4. Book클래스 및 연관된 클래스들 한번씩 체크..

 

5. 경로재확인

-fragments 하위에 items디렉터리가 있어

members와 동일하게 items 디렉터리 위치 templates 하위에 디렉터리위치를 변경

상품등록화면 출력확인

 

데이터 입력확인

*싱글테이블 전략으로 인해 Artist 등 전부 null으로 된 것을 확인할 수 있다.

 

 

 

회원목록조회

 

1.MemberController클래스에서 멤버조회하도록 MemberList.html구문작성

 

사진

 

사진, *변수를 합쳐 단축시켜주는 refactor inline 기능 (ctrl + alt + n)

*타임리프의 장점

-html 태그를 그대로 가져다 사용한다.

-MemberController클래스에서 작성한 List<member>구문에 있는 model에서 members를 담아서 사용한 것을 그대로 

 memberList.html파일로 가져와서 binding이 된다. 그래서 그대로 출력하는 구문만 작성하면 된다.

 

타임리프에서 address?

-null이면 그대로 진행 안한다는 의미

 

마주한 문제(회원목록화면 출력이 안된다.)

에러메세지 발생

-에러메시지만 보았을 때는 이해가 안된다..

가설1

MemberControll클래스에서 return "user/userList"으로 해주어서 출력이 안되었을 것이다.

 

결과- 그대로

 

가설 2

회원이름 입력을 하지 않았을 때에 submit을 누르면

필수항목입니다 메시지가 나오는지 재확인 및 재작동됨을 확인했다.

 

이름을 우리나라말로 바꾸어 보았다. submit이 되고 처음화면으로 redirect가 된다.

처음엔 hello1 로 했었는데. 영어 또는 숫자 입력이 안되게 구문을 작성한 것으로 짐작했지만

h2.database에 들어가니 정보가 입력이 되어 있다.

 

가설 3

회원목록이 나오지 않는다는 것은 못받아 온다는 뜻이라고 가설을 세웠다.

GetMapping(“ members/”)에서 /members로 바꾸어 보았다...

 

결과-동일한 에러

 

가설 4

회원목록이 나오지 return 으로 memberList가 받아 오지 못한다고 가설을 세웠다.

확인해보니 return("members/memberList")로 괄호안에 설정 되어 있었다.

return "member/memberList"; 로 수정완료

 

그래도 에러가 나온다.

 

가설5

다시 회원목록조회 구문을 잠시 주석처리하니,

회원가입시 작성하였던 데이터가 Member 테이블에 잘 나오고 에러도 없다.

 

회원가입 항목을 작성하니 에러가 나오고

member_id를 처음 데이터 작성한 첫행으로 인식을 하여

현재 보유한 데이터가 7개이면 회원가입작성 및 submit후에 에러가 나오고 8번을 새로고침해야 작성한 내용이 데이터베이스에 들어가있음을 확인함.

 

현재 application.yml에서 ddl-auto(초기화옵션)은 create이고,

create는 엔티티자체를 전부 없애고 다시 새로 만들어주는 역할이니 그러한 현상이 발생하는 것으로 짐작하여 update로 변경해주었다.

 

결과-회원가입작성은 잘된다.

 

가설6

-회원목록을 클릭하면 회원가입작성페이지가 나온다.

-home.html 파일에 들어가서 확인을 했다.

<p class="lead">회원 기능</p>

<p>

<a class="btn btn-lg btn-secondary" href="/members/new">회원가입</a>

<a class="btn btn-lg btn-secondary" href="/members/new">회원목록</a>

</p>

로 회원가입과 동일하게 하이퍼텍스트 리퍼런스가 동일하게 되어 있다.

-<a class="btn btn-lg btn-secondary" href="/members">회원목록</a> 으로 수정

 

회원목록출력성공 완료

 

사진

 

권장사항

엔티티를 폼으로 사용하면 화면 종속적 기능이 되어버려서

엔티티가 지저분해지게 된다. 유지보수가 하기 어려워진다.

핵심 로직을 수정하니 화면이 나오지 않는다라는 상황이 발생한다.

엔티티는 순수하게 유지하라는 얘기가 있음.

 

jpa 사용할 때 조심해야 하는 것은 순수하게 간결하게 엔티티를 작성해야 한다.

핵심 비즈니스로직에만 디펜던시가 있도록 작성해야 한다.

그래야 어플리케이션을 만들어도 더 커지지 않는다. 유지보수하기가 좋아진다.

 

엔티티 대신 폼객체나 DTO(Data Transfer Object)권장을 한다.

회원등록할때에는 MemberForm객체를 사용했는데 데이터를 memberList로 보낼 때에는 Member로 보낸다.

 

권장사항2

api를 만들 때에는 엔티티를 절대 외부로 반환하면 안된다. api는 스펙이기 때문이다.

 

예를 들어 패스워드가 그대로 노출(private String userpassword;)될 수도 있고,

엔티티의 로직을 변경했는데 api스펙이 변하게 된다. 그래서 api를 만들 때에는 엔티티를 절대 외부로 반환하면 안된다.

 

사진

회원등록

-현재 회원가입 버튼을 누르면 주소창에 members/new처럼 이동 한다.

 

*<a href = ...>

-a라는 태그가 href라는 속성을 가지고 있다.

-hrefhypertext reference의 줄임말이다.

 

1.memberForm 만들기(입력란 만들기)

-여기에서 members/new 회원가입 폼화면이 나와야 한다.

-회원가입 폼화면이 나올 때를 위해 폼객체를 만든다.

 

사진1(실제는 이름만 필수항목 적용)

 

public class MemberForm {

 

@NotEmpty -> javax.validation.constraints

값이 비어있으면 오류메시지를 보여주는 기능

}

 

private final MemberService memberService

Controller가 서비스(MemberService)를 사용한다.

 

*Model model이란?

model.addAttribute(“정보”)

-> controller로 뷰로 넘어갈 때 데이터를 실어서 정보를 넘긴다.

 

return "members/createMemberForm“

->여기로 화면이 출력이 된다.

createMemberForm을 위한 html을 만들어야 한다.

controller가 화면을 이동할 때 빈 memberForm을 가지고 간다,.

validation같은 역할을 해주기 때문에 빈 memberForm을 가지고 가도록 한다.

데이터가 넘어가니 데이터의 화면출력을 위한 createMemberForm.html 생성이 필요하다.

 

2.templates내에 members 디렉터리생성 및 createMemberForm.html 파일생성을 한다.

 

구문사진

화면사진

html 구문 설명

첫구문

a.<head> include style로 작성

 

b.form 태그가 존재한다.

-action ="members/new"로 넘어간다.

-앞서 작성한 @GetMapping members/newget방식으로 오면

-controller에 있는 createform 구문을 통해 createform html이 열린다.

-열려서 렌더링이 된다. 렌더링이 될 때 model addAttributemembeform을 넘겼다.

 때문에 화면에서 이 객체(new member())에 접근할 수 있게 된다.

-이 객체는 createMemberForm.html 파일 내에

<form ~~~~~~th:object="${memberForm}" 과 연결 되어 form내에서는

memberForm 객체를 사용하겠다는 타임리프의 문법적용이 된다.

 

c.th:field=*{name}

-*가 있는 경우는 object(memberform)를 참고하게 되고

MemberControllercreateform구문의 memberform을 참고하게 되고

memberformmemberform 클래스를 참고하여 해당 필드인 name을 참고하게 된다.

 

회원가입작성시 흐름

th:field=*{필드항목}

->th:object="${객체}

->MemberController클래스의 memberform 객체

->memberform 클래스의 해당 필드 private ~필드

 

d.필드항목 구문

-input type 텍스트

-input후 클래스와 placeholder로 넘어간다.

-필드항목 작성시 idname을 각각 항목내용을 똑같은 이름으로 처리 해준다.

-다 작성후 폼 데이터가 button submit을 누르면

members/newmethodpost로 넘어간다.

 

e. 회원가입 클릭 및 필드항목작성후 첫화면으로 돌아가도록 구문 작성

 

*@NotEmpty 어노테이션으로 인하여 회원이름을 빈칸으로 작성시 에러페이지가 나온다.

구문작성

설명

controller의 서버사이드에서 validation을 했는데 필수항목입니다 메시지가 뜬다.

MemberForm에 작성된 메시지임을 확인할 수 있다.

 

MemberFormController 흐름정리

-서버사이드에서 validation을 하고 문제가 생겨서 BindingResult의 데이터가 하나 들어왔다.

result.를 보면 에러에 대한 데이터를 찾아 낼 수 있는 메서드가 많다.

에러가 있으면 다시 createMemberForm으로 보낸다.

타임리프와 스프링부트가 integration이 되어 있기도 하고, 스프링이 바인딩 result를 기본

적으로 createMemberform에 긁어 와서 사용할 수 있도록 작동한다.

 

createformmember.html 흐름정리

th:class="${#fields.hasErrors('name')}? 'form-control fieldError' : 'form-control'">

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

 

-createMemberform.htmlfields.hasError에서 name에 에러가 있으면 css를 빨간 테두리(form-control fieldError)로 변경하게 해준다.

 

-fields.haserrors의 이름이 있으면 네임필드에 대해서 에러메세지를 출력해준다.

그래서 필수항목이라는 메시지가 렌더링이 된다.

 

사진

 

*회원이름을 작성안해서 에러가 있더라도 다른항목란에 값을 입력했다면 값을 그대로 가지고 온다.

*javax.validation 개념 구글링 통해 공부 필요

*thymleaf도 홈페이지를 통해 공부가능

 

 

웹 계층 개발

 

1.홈 화면

 

2.회원 기능

-회원 등록

-회원 조회

 

3.상품 기능

-상품 등록

-상품 수정

-상품 조회

 

4.주문 기능

-상품 주문

-주문 내역 조회

-주문 취소

 

5.기타

-상품 등록

-상품 목록

-상품 수정

-변경 감지와 병합

-상품 주문

 

홈 화면과 레이아웃

1. Controller 패키지 생성 및 HomeController 클래스 생성

 

2. resources 패키지 -> templates 패키지 -> home.html 파일 생성

-home.html 파일에 타임리프 템플릿을 등록해야 한다.

 

<div th:replace="fragments/bodyHeader :: bodyHeader" />

- jsp include처럼 렌더링 될 때에 fragmentsheader로 바꾼다.

*th라고 타임리프를 관례상 사용한다고 함


사진

설명

-로고가 찍혔음을 확인했고, 화면으로 넘어가서 오류가 났다는 것을 확인할 수 있다.

 

사진2

설명

-header, bodyHeader, footer이름으로 fragments 디렉터리 내에 html파일을 생성하지 않을 경우 에러발생한다.

 

3. fragment 각 대체파일 생성 및 화면출력확인

 

화면출력

*Include-style layouts

-현재는 Include-style layouts으로 작성되었다. 좀 더 단순하게 예제를 만들어 익히는데 중점을 두기 위해서이다.

-좀 더 편하고 실용적으로 작성하길 원한다면 Hierarchical-style layouts로 작성할 것

 

*dev-tools

글자 변경과 같이 수정 된 사항은 앞서 환경설정에서 build.gradle

설치된 dev-tools에 의해 build메뉴->Recompile 실행으로 인해

자동으로 수정변경완료가 되고 재실행 없이, 브라우저화면에서 F5를 누르면 된다.

 

4.view 리소스 등록

a. getbootstrap 사이트에 들어가서 파일을 다운로드 받는다.

b. 부트스트랩 파일을 static에 복사 및 붙여넣기를 한다.

 

c. resources 우클릭 후 Sync(Reload from DISK) 클릭

Build 메뉴 -> Build Project 클릭

*강제로 복사 및 붙여넣기를 했기 때문에 위와 같이 해주어야 한다.

*그래도 되지 않을 경우 부트스트랩 버전을 확인하고 버전에 맞는 integrity 값을 사용해야 한다.

부트스트랩 CDN 및 복붙

d. jumbotron-narrow.css staticcss폴더에 생성후 다시 재실행

 

요약정리

home controller에서 request mapping/로 들어가고

출력된 “home controller”에서 다시 뷰(home)으로 넘어간다.

include 스타일로 리소스들을 include 했고 header는 부트스트랩, 점보트론 css를 사용했다.

 

home.html에 있는 header,bodyHeader, footerfragments에 만들어놓은 html파일로 돌아간다.

부트스트랩(css)를 이용한 홈화면 및 레이아웃 완성!

 

1. 회원 기능 테스트

 

테스트 내용

-회원가입기능이 작동해야 한다.

-회원가입할 때 같은 이름이 있으면 예외가 발생해야 한다.

 

테스트케이스

1.MemberService 클래스에서

a.하기 MemberService에서 ctrl + shift + t < create test < ok를 한다.

 

public MemberService {

......

}

 

b. public void 회원가입( ) throws Exception{

//given(이렇게 주어졌을 때)

 

//when(이렇게 하면)

 

//then(이렇게 된다)

}

 

public void 회원_중복_예외() throws Exception{

//given

 

//when

 

//then

}

 

 

테스트 범위

-정말 순수한 단위테스트 보다 jpa가 메모리모드로 실제 디비까지 돌아가는 것을 보기 위해

 완전히 스프링과 integration해서 테스트를 할 것이다.

 

그래서 하기 두가지를 세팅할 것이다.

 

@RunWith(SpringRunner.class)

@SpringBootTest

@Transactional 데이터를 변경해야 하기 때문에 하는 설정,

public class MemberServiceTest {

 

//테스트라 참조해서 테스트할 것이 없기 때문에 하기처럼 작성

@Autowired MemberService memberService;

@Autowired MemberRepository memberRepository;

}

////alt + enter를 누르고 suppress인가를 누르니 memberRepository가 보라색으로 변경됨

 

*@Transanctional

-데이터를 변경해야 하기 때문에 하는 설정

-같은 Transaction안에서 같은 엔티티에서 id(pk)이 같으면 같은 영속성 컨텍스트에서

 똑같은 값이 관리가 된다.

-Transactional이 있어야 롤백이 가능하다.

-insert SQLDB에 날라가지 않는다. 영속성 컨텍스트가 flush가 되지 않는다.

 

참고: 테스트 케이스 작성

Given, When, Then (http://martinfowler.com/bliki/GivenWhenThen.html) >

필수는 아니지만 위에 것을 기본으로 해서 다양하게 응용하는 것을 권장한다고 한다.

 

 

*축약어를 통해 코드구문생성하기

설정경로

- File->Setting->Editer->Live Templates -> tdd 단축단어 생성 -> tdd

 

내용설정

@Test

public void () throws Exception {

//given

 

//when

 

//then

}

 

=> tdd를 사용하면 위에 구문이 자동생성된다.

 

 

@Rollback(false)

-Transacntional을 롤백이 기본적으로 되므로 직접 눈으로 확인하고 싶고

 DB에 값이 들어가는 것을 확인하고 싶을 때에는

 @Rollback(false)public void 회원가입() throws Exception 에 적용하자.

 

-데이터베이스 트랜잭션이 커밋을 하는 순간 flush가 되면서

  jpa 영속성 컨텍스트가 있는 member객체가 insert문이 만들어지면서 DB insert가 나가게 된다.

 

사진

 

em.flush();

assertEquals(member, memberRepository.findOne(savedId));

-insert문이 db로 가는 것을 실행시킬 때 눈으로 볼 수 있고,

 transactionrollback 되도록 하는 설정

 

try catch return

-중복회원 및 회원가입을 다시 테스트

*데이터베이스에 앞서 insertDB로 날려 값(PRIMARY KEY)이 생성 되었다면

 실행시 중복회원에서 에러가 발생하니 조심할 것

 

사진

회원 기능 테스트 완료

 

 

 

 

2. 회원기능테스트 기술적 정리

a.RunWith(SpringRunner.class)

-jUnit 실행할 때 스프링이랑 엮어서 실행을 하겠다.

 

@SpringBootTest

-스프링부트위에서 테스트를 하려면 꼭 필요하다.

-@Autowired 실행이 실패한다.

-컨테이너 안에서 테스트를 한다.

 

b.@Transactional

-테스트가 끝나면 Rollback을 한다.

-서비스클래스나 레포지터리에 붙여서 돌리면 Rollback하지 않는다.

 

c. tdd 단축키 만들어서 테스트구문 생성하기

 

d. 테스트를 완전 격리된 환경에서 하는 방법

-실제 외부에 있는 데이터베이스를 사용했다.

-디비를 외부에 설치해서 테스트해야 할 경우가 생긴다. 테스트가 끝나고

 데이터가 초기화 되는 것이 좋다.

-테스트를 완전 격리된 환경에서 하는 방법이 있다.

-자바안에 데이터베이스를 만들어 테스트 하는 방법이 있는데,

 메모리 데이터베이스를 사용하는 방법이다. 안에 않고 가상의 데이터베이스를 만들어서

 스프링부트를 사용하면 이러한 방법을 무료로 이용할 수 있다.

 

 

종류

1. main

- 실제 개발하는 운영소스 (java, resouces)

-기본적으로 운영로직은 java, resources가 운영권을 가진다.

 

2. test

-테스트하는 운영소스 (java, resources:directory resources생성해주어야 함)

-테스트 운영로직의 우선권은 test안에 java, resources가 우선권을 가진다.

-application.yml을 java->resource로부터 가져온다면 application.yml이 우선권을 가져

 main에 있는 application.yml은 무시되고 test안에 있는 application.yml이 실행된다.

 

(application.yml 복사 test->resources(directory)->application.yml(복사 from main:java)

 

 

 

JVM내에서 실행 및 메모리모드로 데이터베이스실행 테스트

-jvm 안에서 실행되고 memory모드로 데이터베이스를 띄워주기 위해

 application.ymlspingurl을 인메모리 url로 바꾸어 준다.

 

주소는 h2.database->cheat sheet->In-Memory->첫번째 라인

 

사진 h2.database 사이트

 

사진

 

*하지만, spring, jpa 설정 내용이 전혀 없고 logging,level 설정 내용만으로도

 스프링부트의 기능으로 인해 인메모리 모드로 실행이 된다.

 극단적으로 얘기해서 내용이 전혀 없어도 인메모리 모드로 실행이 된다.

회원서비스(MemberService)

 

순서별로 설명

a.@Service

- @service 어노테이션에 ctrl + b로 확인을 해보면 @component가 설정이 되어 있다.

@component의 스캔의 대상이 되어서 자동으로 스프링빈으로 등록이 된다.

 

b.@Transactional(readOnly = true)

-class 전체적용 여기에선 읽기가 많으므로, readOnly으로 설정

-jpa의 모든 데이터변경이나 모든 로직들은 Transactional 안에서 실행 되어야 한다.

 Transactional 설정으로 lazy loading이 가능해진다.

-클래스레벨의@Transacntional 사용시 public method가 적용범주 안에 속한다.

 

*Transactional 종류2가지 javax,

-스프링에 dependency한 로직이많아서 스프링이 제공하는

Transactional 어노테이션사용권장한다. 사용할수있는옵션들이많기 때문에

 

c.@Autowired

//MemberRepository 클래스를 Autowired로 스프링이 스프링빈에 등록되어 있는 MemberRepository를 인젝션해준다.

____________________________________

 

현재구문에 @Autowired 단점

-일단 테스트를 위해 변경을 못하고 액세스할 수 있는 방법이 없다. 그래서 setter 인젝션을 사용한다.

 

단점보완책

1.예시(setter injection)

@Autowired

public void setMemberRepository(MemberRepository memberRepository) {

this.memberRepository = memberRepository;

}

 

-setter injection을 사용, 테스트할때에 memberRepository 목에 직접 입력(임의의 가짜 repository를 주입)해줄 수 있다.

-단점은, 실제 서비스실행시 누군가 바꿀 수있다.

-실제 동작하고 있는데 사실 바꿀일은 없다. 그래서 이보완책은 좋지 않다.

-스프링데이터 jpa에 의해서 다음과 같이 줄이고 보완할 수 있다.

 

2.최적의 보완책 예시(생성자 injection)

@RequiredArgsConstructor 와 private final MemberRepository 설정

-private final MemberRepository memberRepository 권장

-MemberService 에 값을 작성안해주면 위의 memberRepository에 빨간줄이 생긴다.

-, compile 시점에 한번 더 점검 할 수 있다.

*@RequiredArgsConstructor (lombok 참고)

-final에 있는 필드만 가지고 생성자를 만들어준다. 권장.

________________________________________

 

d.@Transactional

-@Transactional defaultTransactional=false 이다. class전체적용이 있어도

 현재 지금의 어노테이션이 우선권을 가져 이것만 적용이 된다.

-읽기전용아닌 곳에서는 @Transactional(readOnly=true)를 하면 안된다.

 

회원가입

validateDuplicateMember(member); 중복 회원 검증 함수이름=Exception

-MemberRepository클래스에서 jpa에서 em.persist를 하면 순간에 영속성컨텍스트에 Member를 올린다.

-@GeneratedValue를 생성하면 id가 항상생성된 것을 보장한다.

 영속성 컨텍스트에 값을 넣어야 하는데 db와 매핑한 키값이 id(pk)값이 된다.

-db에 들어가지 않아도 그래서 memberid도 채워준다는 얘기

 

f.Exception(validateDuplicateMember (Member member)

List<Member> findMembers = memberRepository.findByName(member.getName());

-받아온 이름을 찾아볼 때에 반환값 설정을 findByName에서 단축키ctrl+alt+v-member이름을

 유니크제약조건을 적용해주어야한다. (현재는 적용이 안되어 있음)

 MemberA라는 이름으로 동시에 INSERT를 하면 같은 이름으로 가입이 가능하기 때문에

 

if (!findMembers.isEmpty()) {

throw new IllegalStateException("이미 존재하는 회원입니다.");

}

 

-member이름을 찾아(!findMembers)보고 없으면(isEmpty)이면 throw new

이미존재하는 회원이라고 출력한다.

 

g.회원전체조회 작성

public List<Member> findMembers() and public Member findOne(Long memberId)

-@Transactional(readOnly=true)로 줄 경우 멤버를 찾을 때에 읽기전용으로만 사용하므로

 jpa가 사용되는 곳에서는 성능을 최적화 한다. 리소스를 적게사용한다.

-읽기전용아닌 곳에서는 @Transactional(readOnly=true)를 하면 안된다.

 변경이 안된다.

 

h. MemberRepository 클래스에서

-스프링부트 라이브러리, 스프링데이터 jpa의 지원으로 하기와 같이 가능해져

@Autowired

private EntityManager em;

public MemberRepository(EntityManager em) {

// Alt + Insert 키 이용해서 Generator사용

this.em = em;

}

 

위의 내용을 @RequiredArgsConstructorfinal 조합으로 사용할 수 있다.

*원래는 @Autowired@PersistentContext(이것도 잇어야 하지만, 생략가능하다.

스프링부트에서 있는 것으로 지원을 해주기때문(현재 버전에서 지원가능한지를 체크해야 함)

 

 

@Repository

@RequriedArgsConstructor

public class MemberRepository {

 

private final EntityManager em;

}

으로 줄일 수 있다.

 

사진

+ Recent posts