웹 계층 개발
Reference : Inflearn 실전 스프링부트와 JPA 활용1 (김영한님 강의)
홈 컨트롤러 등록
로그를 찍을 때
@Slf4j 어노테이션을 사용하면
다음과 같이 작성하는 것과 동일하다.
(Log4j 와 비슷한 기능을 제공하는 듯 하다)
위와 같은 코드를 짜면, home.html 을 찾아서 그 화면이 / 을 받았을 때 뿌려지게 된다.
resources/templates/home.html
위와 같은 화면이 / 에서 뿌려지게 되고, 이 코드에서는 th:replace 를 통해 fragments 폴더 아래 header.html 이라는 파일, bodyHeader.html, footer.html 이라는 파일이 각각 대체하여 화면에 보여진다.
따라서 각각의 파일들을 작성해준다.
getbootstrap.com 을 통해 view 리소스를 등록할 수 있다. 예쁜 화면 가능!
resources/static 아래에 css, js 추가 가능.
회원 등록 (회원 가입)
폼 객체를 사용해 화면 계층과 서비스 계층을 분리해보자.
★ <a> 태그는 Link Page, 즉 일반적으로 페이지 연결을 위해 사용되며 연결 대상이 되는 페이지는 <a> 태그의 href 속성을 통해 정의한다.
회원 등록 컨트롤러 (MemberController)
@NotEmpty : java 에서 validation을 통해 spring이 validation을 해준다. (값이 비어있는지 확인)
Model 이란 : model.addAttribute("memberForm", new MemberForm); 으로 작성하면 컨트롤러에서 뷰로 넘어갈 때 데이터를 실어서 넘기게 된다.
/members/new 를 post로 받은 경우, 멤버를 새로 join 하면서 저장한 후, 재로딩되면 좋지 않기 때문에 redirect해서 home으로 보낸다. (return "redirect:/")
오류가 난 경우는 BindingResult 를 이용한다. validate 한 뒤 BindingResult가 있는 경우, 오류가 result에 담겨서 코드가 실행된다.
따라서 result에 에러가 있다면 return 하도록 작성.
위와 같이 작성하면 이런 화면이 나타난다.
Spring이 에러에 대한 데이터를 찾는데, 에러가 있을 경우 다시 createMemberForm으로 보내고, 그렇게 되면 폼에서 에러가 있는지 확인한다. (BindingResult를 통해 에러를 같이 보내게 됨)
회원 등록 폼 화면
컨트롤러에서 memberForm 을 넘겨주었으므로, 화면에서는 해당 객체에 접근할 수 있게 된다.
th:object -> 해당 객체를 게속 사용하겠다는 뜻.
th:field="*{name}" 이면 위의 object를 참고하겠다는 뜻.
위의 폼 데이터가, <button> submit 을 누르면 /members/new 에 method가 post로 넘어가겠다는 뜻.
그럼 이 post로 넘어온 데이터를 받는 내용을 컨트롤러에 작성.
회원 목록 조회
회원 목록 컨트롤러 추가 (MemberController에 추가)
memberService.findMembers() 하면 JPA에서 JPQL로 짜서 모든 멤버를 조회해줌
가져와서 모델에 담아서, 화면에 넘겨줌.
즉, 조회한 상품을 뷰에 전달하기 위해 스프링 MVC가 제공하는 모델 (Model) 객체에 보관.
memberList.html
table로 쭉 돌리면서 뿌림
tr th : html 태그를 그대로 가져다 쓴다. 즉, 모델에서 넘긴 members list를 그대로 가져와서 바인딩이 된다.
루프로 돌리면서 그대로 찍기만 하면 나옴
타임리프에서 ? 를 사용하면 null. (null 이 있으면 더이상 진행하지 않음)
폼 객체 사용 vs 엔티티 직접 사용
요구사항이 단순한 경우에는 폼 없이 엔티티를 그대로 사용해도 된다.
그러나 실무에서는 1:1로 매칭되는 경우가 거의 없기 때문에, 엔티티가 화면을 처리하기 위한 기능이 너무 많아져서 유지보수하기 어려워진다. (엔티티가 화면에 종속적으로 변하게 됨)
엔티티는 최대한 순수하게 설계해야함! 즉, 엔티티는 핵심 비즈니스 로직만 가지고 있고, 화면을 위한 로직은 없어야 한다.
화면이나 API에 맞는 폼 객체나 DTO를 사용하자.
API를 만들때는, 이유불문하고 절대 엔티티를 넘기면 안된다!!
그렇게 하면 엔티티에 로직을 추가하면 api 스펙이 변화해버림.
엔티티는 절대 외부로 노출해서는 안된다.
템플릿 엔진에서는 서버사이드에서 돌기 때문에 선택적으로 사용은 가능함.
상품 등록
상품 등록 컨트롤러
/controller/ItemController
멤버 컨트롤러와 동일하게 작동한다. Model을 통해 새로운 폼을 등록하고, 실제로 post 를 받았을 때 create 함수가 작동하여 생성한다.
그리고 작업이 끝나면 itemService를 통해 book 아이템을 저장하고, 아이템 목록을 보여주는 화면으로 redirect 한다.
위의 코드처럼 다 Set 으로 하는것보다, createBook() 같은 함수를 통해 한번에 parameter를 넘겨 생성해서 setter를 다 제거하는 것이 좋은 설계이다.
상품 등록 뷰 코드를 작성한다.
상품 목록
상품 목록 컨트롤러
위의 코드에서 하단에 추가
items를 모델에 담아 폼 형태로 넘겨주고, 상품 목록 뷰 html 코드에서는 해당 아이템을 쭉 돌면서 내용을 출력한다.
상품 목록은 다음과 같이 확인 가능하다.
여기서 상품 수정할 수 있는 기능을 추가해보자.
상품 수정
상품 수정과 관련된 컨트롤러
@ModelAttribute("form") 어노테이션 : updateItemForm 의 object 이름을 넘겨주는 것으로 사용
itemId를 누군가가 조작해서 넘겨서, 마음대로 수정할 수 있으므로 조심해야한다. (보안상으로 위험)
유저가 아이템에 대해서 권한이 있는지 체크하는 로직을 서버에 넣어야한다.
saveItem을 호출하면 transactional 이 걸린 상태로 itemRepository.save를 다시 호출한다.
이 save는 item Id가 null 이면 새로운 object이므로 persist를 하고, 아니면 수정 목적으로 em.merge를 호출하게 된다.
변경 감지와 병합 (merge)
준영속 엔티티란?
JPA 영속성 컨텍스트가 더이상 관리하지 않는 엔티티를 말한다.
JPA에 한번 들어갔다 나와서 (DB에 한번 저장된) 식별자가 있는 것을 준영속 상태의 객체라고 한다.
임의로 만들어낸 엔티티도 기존 식별자를 가지고 있으면 준영속 엔티티로 볼 수 있다.
준영속 엔티티의 문제점 -> JPA가 관리를 하지 않는다. 따라서 값을 변경해도 DB에 업데이트가 자동으로 일어나지 않는다.
그렇다면 준영속 상태의 엔티티를 어떻게 변경할 수 있을까? -> 2가지 방법
변경 감지 기능 사용
병합 (merge) 사용
변경 감지 기능
ItemService.java
findItem으로 찾아온 영속 상태.
이런 경우, 값을 다 setting 한 다음에 @Transactional 에 의해 커밋되고, 커밋된 후에는 flush를 날린다. 즉, 영속성 컨텍스트에서 변경된 것이 무엇인지를 찾아낸다. 그리고 바뀐 값을 update해서 DB에 반영한다.
즉, 영속성 컨텍스트에서 엔티티를 다시 조회한 후에 데이터를 수정하는 방법.
트랜잭션 안에서 엔티티를 다시 조회, 변경할 값 선택 -> 트랜잭션 커밋 시점에 변경 감지 (Dirty Checking)
이 동작을 한 뒤 데이터베이스에 UPDATE SQL 실행
병합 사용
merge는 직접 하나하나 입력해서 수정하던것이 한번에 바뀌는 방식.
병합 동작 방식
merge() 를 실행한다.
파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시 (영속성 컨텍스트 entityManager)에서 엔티티를 조회한다.
만약 1차 캐시에 엔티티가 없으면 데이터베이스에서 엔티티를 조회하고, 1차 캐시에 저장한다. (db에서 가져옴)
조회한 영속 엔티티 (위 그림에선 mergeMember)에 member 엔티티의 값을 채워 넣는다. (member 엔티티의 모든 값을 mergeMember에 밀어 넣는다. 기본적인 필드를 다 바꿔치기 함. 이 때 mergeMember의 "회원1" 이라는 이름이 "회원명 변경"으로 바뀐다.)
영속 상태인 mergeMember를 반환한다.
즉, 아래 코드와 같은 내용이 된다.
기존의 parameter로 넘어온 것은 영속성 컨텍스트로 변경되지 않고, 반환된 내용만 영속성 컨텍스트에서 관리되는 객체이다.
다시 한번 병합 시 동작 방식을 간단히 정리하자면
1. 준영속 엔티티의 식별자 값으로 영속 엔티티를 조회한다.
2. 영속 엔티티의 값을 준영속 엔티티의 값으로 모두 교체한다. (병합한다.)
3. 트랜잭션 커밋 시점에 변경 감지 기능이 동작해서 데이터베이스에 UPDATE SQL이 실행
주의 할 점
변경 감지 기능을 사용하면 원하는 속성만 선택해서 변경할 수 있지만, 병합을 사용하면 모든 속성이 변경된다. 병합시 값이 없으면 null 로 업데이트 할 위험도 있다. (병합은 모든 필드를 교체한다.)
실무에서는 보통 업데이트 기능이 매우 제한적이다. 그런데 병합은 모든 필드를 변경해버리고, 데이터가 없는 경우 null로 업데이트해버린다. 병합을 사용해서 문제를 해결하기 위해서는 변경 폼 화면에서 모든 데이터를 항상 유지해야한다.
실무에서는 보통 변경 가능한 데이터만 노출하기 때문에, merge 를 사용해서 데이터를 다 갈아치우는 방법보다는, 귀찮더라도 변경감지를 하는 방법을 권장한다.
좋은 해결 방법
컨트롤러에서 어설프게 엔티티를 생성하지 말자.
트랜잭션이 있는 서비스 계층에서 식별자(id)와 변경할 데이터를 명확하게 전달한다. (파라미터 or dto)
트랜잭션이 있는 서비스 계층에서 영속 상태의 엔티티를 조회하고, 엔티티의 데이터를 직접 변경한다.
트랜잭션 커밋 시점에 변경감지가 실행된다.
ex ) ItemController
entity를 파라미터로 쓰지 말고 정확하게 필요한 데이터만 받아서 사용한다.
(다 전달하지 않고 UpdateItemDto 등을 사용해서 dto를 넘겨줘도 된다.)
+setter 없이 엔티티안에서 바로 추적할 수 있도록 메소드를 만드는 것이 좋다.
더 공부해보고 싶은 것
Slf4j 와 Log4j의 차이가 있는지?
타임리프(Thymeleaf)란?
계층형 레이아웃 사용해보기 (Hierarchical-style layouts)
Last updated