목차
학습목표 : 실무에 가까운 복잡한 예제로 애플리케이션 개발 해보는 것이 목표
1. 프로젝트 환경설정
1.1 프로젝트 생성(https://start.spring.io/)
1.1.1 기본 : Gradle/JAVA/Spring Boot 안정화 버전/group,artifact명 설정
1.1.2 Dependancy : Spring Web/Thymeleaf(JSP대안)/Spring Data JPA/H2 Database/Lombok
A. boilerplate code : 지루하게 반복하는 코드
보일러플레이트 코드란?(Boilerplate code)
보일러플레이트란?
charlezz.medium.com
B. 생성 후, build.gradle 파일 Intellij로 열기
C. Enable Annotation processiong 설정(Build,Execution,Deployment 메뉴)
D. plugin 추가 설치 : 개발보조 용 intellij plugin
- lombok builder helper
1.2 라이브러리 살펴보기
1.2.1 스프링 부트 라이브러리
- spring-boot-starter-web
- spring-boot-starter-tomcat: 톰캣 (웹서버)
- spring-webmvc: 스프링 웹 MVC
- spring-boot-starter-thymeleaf: 타임리프 템플릿 엔진(View)
- spring-boot-starter-data-jpa
- spring-boot-starter-aop
- spring-boot-starter-jdbc
- HikariCP 커넥션 풀 (부트 2.0 기본)
- hibernate + JPA: 하이버네이트 + JPA
- spring-data-jpa: 스프링 데이터 JPA
- spring-boot-starter(공통): 스프링 부트 + 스프링 코어 + 로깅
- spring-boot
- spring-core
- spring-boot-starter-logging
- logback, slf4j
1.2.2 테스트 라이브러리
- spring-boot-starter-test
- junit: 테스트 프레임워크
- mockito: 목 라이브러리
- assertj: 테스트 코드를 좀 더 편하게 작성하게 도와주는 라이브러리
- spring-test: 스프링 통합 테스트 지원
1.2.3 핵심 라이브러리
- 스프링 MVC
- 스프링 ORM
- JPA, 하이버네이트
- 스프링 데이터 JPA
1.2.4 기타 라이브러리
- H2 데이터베이스 클라이언트
- 커넥션 풀: 부트 기본은 HikariCP
- WEB(thymeleaf)
- 로깅 SLF4J & LogBack
- 테스트
1.3 View 환경 설정
1.3.1 thymeleaf 템플릿 엔진 : markup을 깨지않는 장점.
1.3.2 스프링 부트 thymeleaf viewName 매핑
=> resources:templates/ +{ViewName}+ .html
1.3.3 View,Controller 만들기
A. Controller
@Controller
public class HelloController {
@GetMapping("hello")
public String hello(Model model) {
model.addAttribute("data", "hello!!");
return "hello";
}
}
B. View
- resources/templates/hello.html
- static/index.html
1.4 H2 데이터베이스 설치 : Version 1.4.200를 사용
1.4.1 다운로드 : https://h2database.com/html/download-archive.html
1.4.2 JDBC URL
A. 첫 접속 : jdbc:h2:~/jpashop (파일모드로 실행, 첫 실행시 db파일 생성)
B. 이후 접속 : jdbc:h2:tcp://localhost/~/jpashop로 접속(네트워크 모드 접속)
1.5 JPA와 DB 설정, 동작확인 : application.properties 삭제후 application.yml 새로 생성
1.5.1 application.yml(yml 파일은 띄어쓰기 2칸으로 계층구분)
spring:
datasource:
url: jdbc:h2:tcp://localhost/~/jpashop
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
# show_sql: true
format_sql: true
logging.level:
org.hibernate.SQL: debug
A. spring.jpa.hibernate.ddl-auto: create
=> 애플리케이션 실행 시점에 테이블을 drop 후, 재생성하는 옵션
B. show_sql :System.out 에 하이버네이트 실행 SQL을 표시.(log로 찍자..)
C. org.hibernate.SQL : logger를 통해 하이버네이트 실행 SQL 표시
1.5.2 테스트 모델, 테스트케이스 만들기
A. Member => Repository
B. 코드 작성시 command와 query 분리하는 것이 좋은 습관
em.persist(member)
return member.getId(); //저장한다면 return값을 안넘긴다.(sideEffect). id 만으로도 판별가능
C. 테스트케이스
- @SpringBootTest
@Test
@Transactional
@Rollback(false)
- Assertions import 경로 잘 확인할 것(AssertJ)
1.5.3 쿼리 파라미터 로그 남기기
A. 방법1. org.hibernate.type: trace
B. 방법2. build.gradle에 implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.6'
- database Connection을 한번 wrapping해서 출력해주는 것
2. 도메인 분석 설계
2.1요구사항 분석
2.1.1 실제 동작하는 화면 이해로 시작하기.
2.1.2 기능 목록 나열
A. 회원 기능 : Create/Read
B. 상품 기능 : Create/Read/Update
C. 주문 기능 : Create/Read/Delete
D. 기타요구사항
- 상품 재고 관리 필요
- 상품 종류 : 도서/음반/영화
- 상품은 카테고리로 구분 가능
2.2 도메인 모델과 테이블 설계
2.2.1 도메인 모델
A. 주문<>상품<>회원 관계
A.1 상품<>주문 간 다대다 관계 풀기
- 회원은 여러상품 주문 가능
(양방향 가급적 안쓰는게 좋다...예시를 위해 양방향 사용)
- 한 주문당 상품 여러개 포함 가능
- 한 상품이 여러 주문에 포함 가능
=> 주문상품 엔티티를 추가하여 일대다+다대일로 분리
B. 상품분류
- 카테고리와 상품의 다대다 관계 : 카테고리에 여러상품, 상품이 여러 카테고리
(다대다는 모든사례에서 사용하지 않아야 한다. 학습예시를 위해 사용)
- 도서/음반/영화, 종류별로 상품 생성하였고, 공통속성인 상품을 상속하도록 설계
2.2.2 도메인 모델 to Entity 변경
A. 회원 엔티티
A.1 공통속성 Id(PK) : Long
A.2 Embedded ype : 회원 Address(=Value Type)
A.3 일대다 표현 : 회원은 Orders를 List로 가짐
-> Member-Order 관계 : 1:*이 아니라, 동등하게 놓고, 사실 Order를 위해 Member가 필요하다고 보는게 맞음
-> 그래서 member의 orders는 사실 필요가 없음(학습용)
-> 쿼리 날릴 때, One 기준으로 날린다.
A.4 OrderItem : 다대다 푸는 것과 더불어, 주문의 count,price 정보를 위해서도 필요함
A.5 계층구조 : Category에 parent와 child가 있음.
B. 회원 테이블
B.1 ItemTable : 상속 표현 3가지 방법 중, Single Table 전략. DType으로 구분
B.2 Orders : OrderBy keyword와 겹쳐서 Orders로 표기
B.3 Category_Item : 다대다 표현을 위한 mapping Table. DB의 기본이론
> 객체는 Category와 Item 양쪽에 list로 다대다 만들 수 있으나 RDB는 불가능함
> 이런 mapping Table은 운영 중간에 Field 추가 등이 어렵다.(manyTomany 쓰지 말 것)
B.4 표기방법 : 실제 코드에서는 DB에 소문자 + _(언더스코어)
> 관례는 회사마다 다르지만, 보통 대문자 + _(언더스코어), 소문자 + _(언더스코어) 방식 중에 하나로 통일
> 여기서는 객체와 구별을 위해 이 부분만 대문자 사용.
B.5 외래 키가 있는 곳을 연관관계의 주인으로 설정(C언어의 Pointer 같은 내용이므로 꼭 이해 제대로 할 것)
> 연관관계의 주인은 단순히 외래 키를 누가 관리하냐의 문제
> 비즈니스상 우위에 있다고 주인으로 정하면 안된다
B.5.ex) 자동차 & 바퀴 , 항상 many 쪽에 FK가 있으므로 바퀴를 연관관계의 주인으로 설정
> 주인 쪽에 값이 세팅되어야 update 된다.
> 자동차가 주인이 되면, 바퀴 테이블의 FK 값이 업데이트 됨
> 결국, 자동차 Table에서 관리하지 않아, App의 코드관리와 유지보수가 어렵다.
> 또한, 별도의 업데이트 쿼리 발생 때문에 성능 문제 발생
>onetomany에서 one은 단순한 거울(단순 read)이 되는 것이 좋다.
2.3 엔티티 클래스 개발(@Entity) : 설계를 코드로 그대로 옮기면 된다.
2.3.1 Member, Address, Order, Item, Category... 등등 구현한다.
2.3.2 @Column("field명) : 표시 안하면, class 변수명 그대로 인식함
> id column은 "{table명}_id" 이런식으로 DBA들이 표기하는 편
> join할 때 편함(foreign key와 이름 맞추기)
> 단순히 id로 작성하면 나중에 찾기 힘듦.
2.3.3 @Embeddable, @Embeded : JPA 내장 타입 표시. 한쪽만 있어도 되지만 보통 둘 다 표시하는 편
> 내장타입 @Getter만 사용, @Entity도 붙이면 안된다.
> 값 type이라고 하며 immutable 이다. 그래서 @Setter 도 없어야 한다.
2.3.4 Order table은 @Table(name="orders") public class Order{} 로 작성한다.
2.3.5 @JoinColumn(name="member_id") : 매핑을 뭘로 할거냐, member_id가 FK의 이름이 됨.
2.3.6 @ManytoOne, @OnetoMany 양방향 : FK는 어떤 Entity에서 update를 실행해야 하는지...
> 관계의 주인은 그대로 두고, 반대쪽만, 즉 @OneToMany에 (mappedBy="변수명")를 넣어준다.
2.3.7 상속관계 전략을 부모Class에 정해줘야함 @Inheritance (strategy = InheritanceType.SINGLE_TABLE)
>SINGLE_TABLE : 한 테이블에 다 때려박기
> JOINED : 정규화 된 스타일
> TABLE_PER_CLASS : 테이블당 class 하나??
2.3.8 @Discriminator
2.3.9 @Enumerated(EnumType.STRING)
> EnumType.ORDINAL: column값이 숫자로 들어감 => 중간에 다른 상태가 생기면 망함.
> EnumType.STRING: STRING로 들어감
2.3.10 1:1관계에서 FK 설정할 곳 : 주로 Access를 하는 곳에 설정
2.3.11 @JoinTable : 다대다 관계를 풀 때, 일대다<-> 다대일 사이의 매핑테이블 생성
ex) JoinColumn명을 설정해주고, join&inverse 순서는 n:n에서는 크게 상관 없는 듯
@JoinTable(name = "category_item",
joinColumns = @JoinColumn(name = "category_id")
, inverseJoinColumns = @JoinColumn(name = "item_id"))
2.3.12 self 연관하는 법 : 이름만 self고 다른 연관관계 설정과 동일
@ManyToOne
@JoinColumn(name = "parent_id")
private Category parent;
@OneToMany(mappedBy = "parent")
private List<Category> child = new ArrayList<>();
2.3.13 JPA는 alter 명령어로 foreign key 걸어준다.
2.3.14 Setter의 문제점. 여기저기서 call하여 변경하면 어디서 왜 변경되는지 추적이 점점 어려워진다.
> 변경지점이 명확하도록 비즈니스 메소드를 구현해 Setter 대신 사용한다.
2.4 엔티티 설계시 주의점
2.4.1 Entity에 Setter를 사용금지
> 변경 포인트가 너무 많아서, 유지보수 어렵다.
2.4.2 모든 연관관계는 (fetch = FetchType.LAZY) 설정!(암기)
A. EAGER(즉시로딩)은 모든 연관데이터를 바로 다 read한다.
>> EAGER는 예측이 어렵고, 어떤 SQL이 실행될지 추적하기 어렵다. 특히 JPQL을 실행할 때 N+1문제가 자주 발생한다. 따라서, 실무의 모든 연관관계는 LAZY로 해야 함
B. LAZY 세팅 후 필요시 EAGER로 변경
C. 연관된 엔티티를 함께 DB에서 조회해야 하면, fetch join 또는 entity graph를 사용한다.
D. @XToOne(OneToOne, ManyToOne)는 default = EAGER이므로, LAZY 명시 필수
2.4.3 컬렉션은 필드에서 초기화 하자.
A. 컬렉션은 필드에서 바로 초기화 하는 것이 안전하다.
> null 문제에서 안전하다.
> hibernate는 entity를 persist 할 때, collection을 wrapping하여 hibernate 내장 collection으로 변경한다.
> getOrders()같은, 어떤 method에서 collection을 잘못 생성하면 hibernate 내부 메커니즘에 문제가 발생할 수 있다.
>> 기껏 wrap했는데, 다른곳에서 set하여 collection 바꾸면 hibernate가 이상동작 함.
2.4.4 테이블, 컬럼명 생성 전략 : name="" 안쓸 때 default로 생성하는 방식
A. SpringBoot에서 hibernate 기본 매핑 전략을 변경해서 실제 테이블 필드명은 다름
ex) 변수명 orderDate -> 컬럼명 order_date
A.1 과거 하이버네이트 방식
> 엔티티의 필드명을 그대로 테이블의 컬럼명으로 사용 ( SpringPhysicalNamingStrategy )
A.2 스프링 부트 신규 방식 (엔티티(필드) 테이블(컬럼))
1. 카멜 케이스 언더스코어(memberPoint member_point)
2. .(점) _(언더스코어)
3. 대문자 소문자
B. 이름 바꾸고 싶을때 방법
B.1. 논리명 생성
> 컬럼, 테이블명을 명시하지 않으면 ImplicitNamingStrategy 사용
> spring.jpa.hibernate.naming.implicit-strategy
2. 물리명 적용: 모든 논리명, 실제 테이블에 적용
> spring.jpa.hibernate.naming.physical-strategy : username usernm 등 회사 룰로 바꿀 수 있음
C. 스프링 부트 기본 설정
spring.jpa.hibernate.naming.implicit-strategy:
org.springframework.boot.orm.jpa.hibernate.SpringImplicitNamingStrategy
spring.jpa.hibernate.naming.physical-strategy:
org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
2.4.5 Cascade
> Cascade 옵션은 persist를 전파하도록 한다. 연관관계 항목들까지 같이 persist한다.
> 즉, 같이 persist 해달라는 표시
> 여기서는 order/orderItem/deliver에 사용
2.4.6 연관관계 (편입) 메소드
> 연관관계 같이 set 하는 것 까먹을 수 있다. 이를 해결하기 위한 메소드
> controll하는 쪽에 작성하는 것이 좋음. 특히 양방향에선 메소드 만들자
> ex)
public void setMember(Member member){
this.member = member;
member.getOrders().add(this);
}
3. 애플리케이션 구현 준비
3.1 구현 요구사항
3.1.1 접근방식
> 핵심 비즈니스 메소드를 먼저 구현한다 => 이후 화면 구현
A. 회원 기능 : 회원 등록, 회원 조회
B. 상품 기능 : 상품 등록, 상품 수정, 상품 조회
C. 주문 기능 : 상품 주문, 주문 내역 조회, 주문 취소
D. 구현 안 할 기능 : 로그인/권한 관리, 파라미터 검증/예외 처리 단순화, 상품은 도서만 사용, 카테고리 사용X, 배송 정보 사용X
3.2 애플리케이션 아키텍처
3.2.1 계층형 아키텍처 사용
A.controller, web 계층 : 웹 계층
B. service 계층 : 비즈니스 로직, 트랜잭션 처리
C. repository 계층 : JPA를 직접 사용하는 계층, 엔티티 매니저 사용
D. domain 계층: 엔티티가 모여 있는 계층, 모든 계층에서 사용
> controller 가 repository 바로 접근 될 수도 있도록 개발 예정(간단한 조회용)> 모든 것이 service 타는 것도 실용적이진 않다.
3.2.2 패키지 구조
A. jpabook.jpashop
> domain
> exception : 공통예외 모음
> repository
> service
> web
3.2.3 개발 순서
: service > repository > test case 검증 > 웹 계층
4. 회원 도메인 개발
순서 : 엔티티 코드 확인 > 리포지토리 개발 > 서비스 개발 > 기능 테스트
4.1회원 리포지토리 개발
> @Repository : bean등록
> EntityManager 선언 후 @PersistenceContext(JPA가 injection)
> EntityManagerFactory를 갖고 싶으면, @PersistenceUnit 사용하면 됨.
4.1.1 Create : em.persist
4.1.2 Read
A. 단건조회 : em.find
B. list 조회 (JPQL) : em.createQuery
> JPQL은 Entity 대상으로 query를 한다.
C. Where 조회: where m.name = :name + .setParameter("name", name)
4.2 회원 서비스 개발
A. @Service
B. @Transactional : 트랜잭션, 영속성 컨텍스트
> readOnly=true : 데이터의 변경이 없는 읽기 전용 메서드에 사용, 영속성 컨텍스트를 플러시 하지
않으므로 약간의 성능 향상(읽기 전용에는 다 적용)
> 데이터베이스 드라이버가 지원하면 DB에서 성능 향상
C. @Autowired
> Field, Setter Injection 보다 생성자 Injection 사용하기
> 생성자가 하나면 생략 가능
> final 선언: compile 시점에 Injection 안되어 있으면 error로 알려줌 (기본 생성자를 추가할 때 발견)
D. @RequiredArgsConstructor : final 선언 된 것에 constructor 자동 생성
4.2.1 회원가입
A. 중복검증 : IllegalStateExcpetion
B. repository.save()
C. return Id : 어떤 것이 저장되었는지 확인할 수 있도록 ACK
4.2.2 회원조회
A. 단건조회 : repository.findOne()
B. 전체조회 : repository.findAll()
4.3 회원 기능 테스트
> 테스트 요구사항 : 회원가입 성공, 같은이름가입 예외
A. 회원가입
> JPA 같은 transaction안에서, PK 값이 같으면 같은 persistence Context에서 같은 entity로 인식한다.
B.중복회원 예외(expected 부분은 JUnit4, JUnit5 에서는 assertThrows로 변경되었다)
try {
memberService.join(member2);
}
catch (IllegalStateException e ){
return ; //성공
}
를 아래 선언으로 변경 가능하다.
@Test(expected = IllegalStateException.class)
C. Memory DB(in-memory) 세팅 : Spring Boot 기능. h2를 JVM안에서 memory모드로 DB를 띄울 수 있다.
> 테스트 시 매번 DB 띄우고, 끝나면 매번 data 지워야 하는 불편함 해소
> 즉, DB 안 띄워도 됨
> test yml 생성 후 값 세팅
C.1. test폴더에 resources 폴더 추가하기
C.2. test/resources/application.yml 생성
C.3. h2 db url을 memory로 변경하기
> https://www.h2database.com/html/cheatSheet.html > In-Memory
> jdbc:h2:mem:test
C.4 Spring boot는 기본 설정이 메모리 모드이다. url 따로 입력 안해도 됨 but 설정을 따로 가져가는게 맞다.
> mockknig 등의 이유
C.5 table drop도 기본 값이다.
5.상품 도메인 개발
> 비즈니스 로직 생각할 때, 화면과 연계해서 생각하는게 간편하긴 하다.
> 순서 : 엔티티 코드 확인 > 리포지토리 개발 > 서비스 개발 > (기능 테스트는 생략)
5.1상품 엔티티 개발(도메인 자체에 비즈니스 로직 추가)
5.1.1 비즈니스 로직
> 도메인인 주도 설계 관점에서, 엔티티 자체적인 해결 되는건 엔티티에 비즈니스 로직 넣는 것이 좋다.
> 데이터 가진 곳에 비즈니스 메소드가 있는것이 가장 좋음. 응집도 붙이기
A. 재고 증가
> addStock(int quantity)B.재고 감소
> removeStock(int quantity) : 양의 정수 체크로직 필요
> runtime exception extend 해서 새로 정의.
> 값 변경은 setter 사용이 아니라, 핵심 비즈니스 메소드 사용해서 변경해야 함.(객체지향적인 방식)
5.2상품 리포지토리 개발
public void save(Item item){
if(item.getId() == null){
em.persist(item);
}else{
em.merge(item);
}
}
5.2.1 Create> Item 은 처음에 id가 없어서, persist로 save하고> JPA 저장 전까지는 ID 값이 없다. @GeneratedValue 해도. 완전 새로 생성한 값이라는 뜻> 있는 item이면 merge를 한다.(= update의 의미)
5.2.2. Read
A. 단건 조회 : findOne()
B. 전체 조회 : findAll()
5.3 상품 서비스 개발
> @Save선언, Transcational, repository 사용
5.3.1 Create : repository.save
5.3.2 Read : repository.find()...
A. 단건조회
B. 전체조회
6. 주문 도메인 개발
> 제일 중요한 비즈니스. 비즈니스 로직 얽힌 것을 JPA가 푸는 것에 대한 예시.
> 도메인 모델 패턴이라고 한다 = 엔티티에 많은 것을 위임(엔티티에서 로직처리). 서비스는 엔티티에 요청만 전달
>VS transcation-script 패턴 : 서비스 계층에서 로직 처리
순서 : 엔티티 코드 확인 & 비즈니스로직 추가 > 리포지토리 개발 > 서비스 개발 > 기능 테스트 > 추가 기능 개발(검색 )
6.1 주문, 주문상품 엔티티 개발
6.1.1 핵심 비즈니스 로직 넣기
A. 생성 메서드 - Order
> order에 Item, delivery등이 연계된다. 이런 복잡한 것들은 별도 생성메서드 있으면 좋음.
> "이렇게 작성하면, 생성 지점의 변경이 필요할 때 이 메서드만 변경하면 된다."
//생성메서드
public static Order createOrder(Member member, Delivery delivery, OrderItem... orderItems){
Order order = new Order();
order.setMember(member);
order.setDelivery(delivery);
for(OrderItem orderItem: orderItems){
order.addOrderItem(orderItem);
}
order.setStatus(OrderStatus.ORDER);
order.setOrderDate(LocalDateTime.now());
return order;
}
> ...문법 : https://godnr149.tistory.com/150
B. 생성 메서드 - OrderItem
> orderItem의 가격은 할인 등에 따라 바뀔 수 있으므로, Item 자체가격 사용하지 않는다.
//생성메서드
public static OrderItem createOrderItme(Item item, int orderPrice, int count){
OrderItem orderItem = new OrderItem();
orderItem.setItem(item);
orderItem.setOrderPrice(orderPrice);
orderItem.setCount(count);
item.removeStock(count);
return orderItem;
}
B. 비즈니스 로직
B.1 주문취소 > 예외처리 : 배송완료시 => 취소불가 exception> 상태변경 : setStatus(CANCEL)> 재고수량원복 : orderItem List cancel(), OrderItem Entity에도 cancel추가(재고수량 원복)
B.2 전체 주문가격 조회> 가격 계산 : Total Price += orderItem.getTotalprice();> 개별가격 계산 : getTotalPrice = getOrderPrice * getCount;
6.2 주문 리포지토리 개발
A. 저장 : saveB. 조회> 단건조회 : findOne> 전체조회 : 이 기능은 검색 기능 할 때 개발.
6.3 주문 서비스 개발
> 다른 사람들이 정해진 룰대로 생성하지 않는 경우를 막는 법
> setter에 protected 선언하기(JPA에서는 쓰지 말라는 것과 동일의미)
> 상단에 @NoArgsConstructor(access = AccessLevel.PROTECTED) 선언하는 것도 같은 효과를 볼 수 있다.
6.3.1 주문생성 : member, item, item 몇 개
A.엔티티조회
> member값을 꺼내기 위해 memberRepository
> item값을 꺼내기 위해 itemRepository
B. 배송정보 생성
> Delivery, setAddress
C. 주문상품 생성
> OrderItem.createOrderItem()
D. 주문 생성
> Order.createOrder
E. 주문 저장
> orderRepository.save(order);
>> Casecade 옵션이 있어서, OrderItem, Delivery를 따로 repository통해 persist 안 하고 save 한번만 해보면 된다.
>>> 어디까지 cascade해야하는가? : 주인이 private Owner인 경우에만 써야 함. 그 외는 위험할 수 있음
>>>> ex) order<-delivery, order<-orderItem. order만 참조해서 쓴다. 같은 lifecycle 이므로 order에서 cascade처리해도 된다.
6.3.2 주문취소
> JPA 장점! , update 쿼리 따로 안쳐도 된다. entity data만 바꾸면 알아서 해준다.
A. 주문 엔티티 조회 : findOne(Id). id값만 필요함
B. 주문 취소 : order.cancel()
6.4 주문 기능 테스트
> 주문기능 동작, 예외처리(재고 수량 초과),주문 취소
6.4.1 주문기능
A. 주문의 상태
B. 주문 수
C. 주문의 가격 계산 로직
D. 재고 수량 감소
6.4.2.예외테스트 : 재고 수량 초과 주문
6.4.3 주문 취소
>> 도메인 모델의 장점 : 모델 entity에 대해 바로 Test 작성.
6.5 주문 검색 기능 개발
> JPA의 동적쿼리 처리방법 설명 목적인 챕터. where절 값이 막 바뀐다.
6.5.1 OrderSearch Class 생성하기
6.5.2 OrderRepository에 findAll 추가하기
6.5.3 검색 로직(동적쿼리)
A. JPQL 사용, . String += 형태의 노가다 : 사용금지
B. JPA Criteria : 현재는 우선 사용. 실무에서 사용금지, JQPL 추측불가. 유지보수 불가
C. QueryDsl : 사용 할 것
7. 웹 계층 개발
7.1 홈 화면과 레이아웃
7.1.1 controller/HomeController 선언
A.@Slf4j : Logger 따로 선언 안해도 되게 해줌
7.1.2 resource/template/home.html 생성
A. namespace만 잘 보면 됨 : <html xmlns:th="http://www.thymeleaf.org">
B. <head th:replace="target :: header"> : rendering 해줄 때 target부분을 바꿔치기 해주는 부분
> 자주사용 하는 것을 header,bodyheader,footer로 설정해 import해서 사용한다. == frgment
>> Hierarchical-style layouts
>>> header , footer 같은 템플릿 파일을 반복해서 포함하는 중복을, Hierarchical-style layouts로 제거할 수 있다.
>>> https://www.thymeleaf.org/doc/articles/layouts.html
C. 뷰 템플릿 변경사항을 서버 재시작 없이 즉시 반영하는 설정
> 1. spring-boot-devtools 추가
> 2. html 파일 build-> Recompile
7.1.3 fragment : fragment /header.html, fragment /bodyheader.html, fragment /footer.html 파일을 생성하기
7.1.4 view 리소스 등록 : 부트스트랩(v4.3.1) 사용 이쁘게 만들기
> resources/static 하위에 다운로드한 css , js를 디렉토리 채로 복붙해서 추가
> resources/static/css/jumbotron-narrow.css 추가
> 결과
7.2 회원 등록 (회원 가입)
> 화면에서 회원 가입 버튼에 href="/members/new" 로 매핑해놨다
> form을 써야하는 이유
>> entity와 화면이 동일하지 않다.
>> 그래서 화면이 원하는 validation과 domain이 원하는 validation이 다를 수 있다.
>> 그래서, entity를 그대로 쓰게 된다면 not Empty등을 entity에 추가하게 되고, entity가 변질되게 된다.
>> 결국, FormData를 활용해, entity를 보호하고 화면에 fit하게 구성하는 것이 좋다.
>> 실무에서 엔티티는 핵심 비즈니스 로직만 가지고 있고, 화면을 위한 로직은 없어야 한다.
>> 화면이나 API에 맞는 폼 객체나 DTO를 사용하자. 그래서 화면이나 API 요구사항을 이것들로 처리하고, 엔티티는 최대한 순수하게 유지하자.
7.2.1 form 생성 : 회원가입 버튼을 누르면, 회원가입 form이 떠야 함
A. form 객체 생성 : @Getter @Setter public class MemberForm{ }
> 필드에 @NotEmpty : import javax.validation.constraints.NotEmpty, 값이 비어있으면 오류가 나도록 spring이 validation
7.2.2. Controller 생성
> @Controller @RequiredArgsConstructor public class MemberController{}
> 서비스 선언 : private final Memberservice memberservice
> Form 생성 메소드 선언 : @GetMapping(value = "/members/new") public String createForm(Model model){}
>> Model : controller에서 view로 넘어갈 때 model에 데이터 실어 넘김. Spring MVC가 제공하는 Model객체
>>> model.addAttribute("memberForm", new MemberForm()) return "members/createMemberForm";
>>> memberForm의 빈 껍데기 가져가는 이유 : validation 등등을 해 줌
7.2.3. 화면생성 : 위에서 선언한 createMemberForm.html 생성(코드 생략)
> header include한다.
> <form role="form" action="/members/new" th:object="${memberForm}" method="post">
> th:field = rendering 시에 thymeleaf가 표기한 id, name으로 바꿔서 자동 생성 해준다.
> submit 누르면 위에 명시한 post action으로 전달된다.
7.2.4 Submit동작 추가
A. Controller에 @Postmapping("/members/new") 추가
@PostMapping("/members/new")
public String create(@Valid MemberForm form){
Member member = new Member();
Address address = form.getCity(), form.getStreet(), form.getZipcode());
member.setName(form.getName());
member.setAddress(address);
memberService.join(member);
return "redirect:/";
}
> @Valid를 붙이면, javax validation을 수행해준다(위에 @Not Empty)
> return "redirect:/" : 저장 후 재 로딩되면 안좋아서 home으로 보내버린다.
B. PostMapping에 BindingResult result 추가
> 원래 오류가 있으면 튕기는데, (@Valid form, BindingResult result)형태로 선언하면 오류가 result에 담긴 후 코드는 실행
> if(result.hasErrors()){ return "members/createMemberForm";} 을 본문에 추가하면 bindingResult를 화면에 가져가 에러페이지가 아닌, 화면에서 에러를 표기할 수 있다.
> 화면에 th:class="${#fields.hasErrors('name')}? 'form-control fieldError' : 'form-control'"><p th:if="${#fields.hasErrors('name')}" th:errors="*{name}">Incorrect date</p>는 추가되어 있어야 함
>> 에러가 있으면 .fieldError로 정의한 것으로 css 표시한다.
>> hasErrors에 name이 있으면, th:errors가 name field에 대해 error message를 뽑아서 출력 해 줌
7.3 회원 목록 조회
7.3.1 Controller에 요청 받는 부분, GetMapping("/members") 추가
> JPA에서 조회 > model에 담아서 > 화면에 넘김
>> 아래 코드는 사실 Member 그대로가 아니라, DTO로 변환해서 넘겨야 함.
>> template engine이라 괜찮지, API는 entity 반환하면 큰일남.
>> api는 spec임. 이렇게 하면 entity 수정 시 api spec변경 되어버림
@GetMapping("/members")
public String list(Model model){ List<Member> members = memberService.findMembers();
model.addAttribute("members",members);
return "members/memberList";
}
7.3.2 화면추가 : memberList.html
> 루프 돌면서 데이터 뿌리기
> header include한다.
> <tr th:each ><td th:text></td></tr> 로 binding
7.4 상품 등록
상품 등록 폼에서 데이터를 입력하고 Submit 버튼을 클릭하면 /items/new 를 POST 방식으로 요청
상품 저장이 끝나면 상품 목록 화면( redirect:/items )으로 리다이렉트
7.4.1상품 등록 폼
> @Getter @Setter public class BookForm{ variables.... }
7.4.2 상품 등록 컨트롤러
아래 PostMapping 에서, createBook 형태의 생성자 method 사용해, setter 다 날리도록 수정하는 것이 좋은 설계
@Controller
@RequiredArgsConstructor
public class ItemController{
private final ItemService itemservice;
@GetMapping("/items/new")
public string createForm(Model model){
model.addAttribute("form", newBookForm());
return "items/createItemForm";
}
@PostMapping("items/new")
public String create(BookForm form){
Book book = new Book();
book.setName(form.getName());
book.setPrice(form.getPrice());
....
itemService.,saveItem(book);
return "redirect:/items";
}
}
7.4.3 상품 등록 뷰 : items.html
> BookForm이 넘어가면 위에 회원등록 처럼 동일하게 rendering
7.5 상품 목록
7.5.1 컨트롤러 : 요청 받고, 조회해서 model에 담아서 return
@GetMapping("/items")
public String list(Model model){
List<Items> = itemService.findItems();
modle.addAttribute("items",items);
return "items/itemList";
}
7.5.2 화면 : itemList.html
> 루프 돌면서 데이터 뿌리기
7.6 상품 수정
> 상품 리스트 옆에 수정버튼 추가
> JPA에서 어떤 방법으로 수정하는 것이 옳을 까... 정석적인 방법(변경감지. JPA에서 가이드하는 방법) 설명
7.6.1 컨트롤러 - Get 처리 추가
@GetMapping("items/{itemId}/edit")
public String updateItemForm(@PathVariable("itemId") Long itemId, Model model){
Book item = (Book) itemService.findOne(itemId);
BookForm form = new BookForm();
form.setId(item.getId());
......
model.addAttribute("form", form);
return "items/updateItemForm";
}
tip) intelliJ : Cmd 두번 누르면 multiLine select 된다. shfit & option 버튼 연계해서 사용하기
7.6.2 화면 : updateItemForm.html
> <form th:object="${form}" method = "post"> 에서 submit누르면 object를 반환한다.
7.6.3 컨트롤러 - Post 처리 추가
A. @ModelAttirbute : form에서 반환 받는 것 매핑
B. itemService에서 save하는 것으로 마무리
> Controller > itemService.saveItem > @Transactional itemRepository.save > item.getId != null 이므로 em.merge로 동작.
> 컨트롤러에 파라미터로 넘어온 item 엔티티 인스턴스는 준영속 상태다.
> 따라서, Persistence Context의 지원을 받을 수 없고, 변경 감지 기능이 동작하지 않음.
C. id받는 부분에서 보안성이 취약하다 : 남이 바꿀 수 있음. 뒷단에서 권한체크 로직(or session 객체 사용) 추가 필요
@PopstMapping("items/{itemId}/edit")
public String updateItem(@PathVariable String itemId, @ModelAttribute("form") BookForm form){
Book book = new Book();
book.setId(form.getId());
......
itemService.saveItem(book);
return "redirect:items";
}
7.7 변경 감지와 병합(EntityManager.merge) - 기능이 성립하려면 맥락이 있어야 한다.
영속상태(dirty chekcing)와 준영속상태(no dirty checking) 차이를 확실히 이해해야 함.
DirtyChecking : em.flush 할때 값의 변경이 있다면, update문 자동으로 날리는 것
7.7.1 준영속 엔티티
> DB에 한번 갔다온 Entity. DB에 이미 저장되서 관리 안 함.
> 즉, Persistence Context가 더는 관리하지 않는 Entity를 말한다.
> 임의로 생성한 엔티티도 id를 가지고 있으면, 준영속 엔티티
> JPA에서 관리를 하지 않으므로 update문을 날리지 않는다.
7.7.2 준영속 엔티티 수정방법
A. Dirty Checking: save할 필요 없음. Persistence Context에서 Entity를 조회 한 후, 수정할 때 발생
A.1. Transaction 안에서 Entity 조회, 변경 할 값 선택
A.2. Transaction "Commit 시점"에 변경 감지(Dirty Checking) > 데이터베이스에 UPDATE SQL 실행
> commit 시 flush를 날린다.
@Transactional
void update(Item itemParam) { //itemParam: 준영속상태의 엔티티
Item findItem = em.find(Item.class, itemParam.getId()); //엔티티 조회
findItem.setPrice(itemParam.getPrice()); //수정.
}
B. Merge: Merge는 준영속상태의 Entity를 영속 상태로 변경할 때 발생
B.1.준영속 Entity의 id 값으로 영속 엔티티를 조회.
B.2.영속 Entity의 값을 준영속 Entity 값으로 모두 바꿔치기 한다.(merge 한다)
B.3.Transaction "Commit 시점"에 Dirty checking 기능이 동작해서 DB에 UPDATE SQL이 실행
@Transactional
void update(Item itemParam) { //itemParam: 준영속 상태 엔티티
Item mergeItem = em.merge(itemParam);
}
7.7.3 Merge 동작방식 상세설명
> 파라미터로 넘긴 것이 들어가지 않고, read한거에 값을 replace해서 저장하는 방식.
A. merge()를 호출
B. 파라미터로 넘어온 준영속 Entity의 id 값으로 1차 cache 에서 조회.
> 조회 결과가 없으면, DB에서 후 1차 cache에 저장.
C. 조회한 영속 Entity의 값을 parameter로 받은 Entity 값으로 바꿔치기 한다.
D. 영속 상태인 Entity를 반환
public item updateItem(Long itemId, Book param){
Item findItem = itemRepository.findOne(itemId);
findItem.setPrice(param.getPrice());
findItem.set~~
return findItem;
}
cf) 주의
> dirty checking : 원하는 속성만 선택해서 변경
> merge : 모든 속성이 변경.(모든 필드를 교체한다.) 값이 없으면 null 로 업데이트 한다.
7.7.4 상품 리포지토리의 저장 메서드 분석 ItemRepository
A. id 값이 없으면( null ) : 새로운 엔티티로 판단해서 영속화(persist)하고
B. id 가 있으면 : 병합(merge)
> 준영속 상태인 상품 엔티티를 수정할 때는 id 값이 있으므로 병합 수행
7.7.5 새로운 엔티티 저장과 준영속 엔티티 병합을 편리하게 한번에 처리하기
: 그냥 Dirty checking을 명확하게 해주는 것이 좋다.
save() 메서드 하나로 저장과 수정(병합)을 다 처리한다.
식별자 값이 없으면 새로운 엔티티로 판단해서 persist() 로 영속화
식별자 값이 있으면 이미 한번 영속화 되었던 엔티티로 판단해서 merge() 로 update.
결국, save의 의미 = 신규 데이터를 저장 || 변경된 데이터 저장
> 저장/수정을 구분하지 않아, 클라이언트 로직이 단순.
> 여기서 사용하는 수정(병합)은 준영속 상태의 엔티티를 수정할 때 사용한다.
> 영속 상태의 엔티티는 변경 감지(dirty checking)기능이 동작해서 트랜잭션을 커밋할 때 자동으로 수정되므로 별도의 수정 메서드를 호출할 필요가 없고 그런 메서드도 없다.
> cf) save() 메서드는 id를 자동 생성해야 정상 동작한다.
>> Item 엔티티의 id에는 @GeneratedValue 선언으로 자동 생성처리 했다. id 없이 save() 메서드를 호출하면 persist() 가 호출되면서 식별자 값이 자동으로 할당된다.
>> id를 직접 할당하도록 @GeneratedValue없이 @Id 만 선언 후, 할당 안하고, save() 메서드를 호출하면,
>> 식별자가 없는 상태로 persist()가 호출되고, 식별자가 없다는 예외가 발생한다.
7.7.6 결론 : 직접 persist된 entity 를 repository 통해 find -> 필요한 필드만 set,set,set 직접하기
A. Entity update 시, 항상 Dirty checking으로 되도록 할 것
B. Controller에서 어설프게 Entity를 생성하지 말 것(new Entity 생성해서 넘기는 형태 금지. 아래 형태 or DTO 사용)
ItemService.updateItem(id, price, name)
@Getter @Setter
public class UpdateItemDto{
}
C. Transcation이 있는 서비스 계층에 식별자( id )와 변경할 데이터를 명확하게 전달.(파라미터 or dto)
D. Transcation이 있는 서비스 계층에서 영속 상태 Entity 조회 후, 그 Entity를 직접 변경.
E. 이렇게 하면 Transcation Commit 시점에 변경 감지 실행.
7.8 상품 주문
7.8.1 Controller : 상품, 회원 선택 때문에 그동안 한거 보다 몇개 Service 추가됨
@Controller
@RequiredArgsConstructor
public class orderController{
private final OrderService orderService;
private final MemberService memberService;
private final ItemService itemService;
@GetMapping(value = "/order")
public String createForm(Model model) {
List<Member> members = memberService.findMembers();
List<Item> items = itemService.findItems();
model.addAttribute("members", members);
model.addAttribute("items", items);
return "order/orderForm";
}
}
7.8.1 화면 : orderForm.html
form 태그 : <form role="form" action="/order" method="post">
select box 추가 : <select name="memberId" id="member" class="form-control">
each 사용해서 rendering : ex) <option th:each="member : ${members}"
7.8.2 Post 처리 Controller 추가(Form > submit)
@PostMapping(value = "/order")
public String order(@RequestParam("memberId") Long memberId,
@RequestParam("itemId") Long itemId,
@RequestParam("count") int count) {
orderService.order(memberId, itemId, count);
return "redirect:/orders";
}
> id만 넘겨서 service 계층에서 찾고 여러가지 활동하는 형태가 좋음. service계층이 entity에 더 많이 의존해야 함. 할 수 있는게 더 많아짐. command성 로직은, cotroller에서는 id만 토스하고, 핵심 비즈니스 service에서 모든걸 처리.
>> 영속상태가 존재하는 상태에서 조회가능.
7.9 주문 목록 검색, 취소
7.9.1 주문내역
A. Controller
@Controller
@RequiredArgsConstructor
public class OrderController {
@GetMapping(value = "/orders")
public String orderList(@ModelAttribute("orderSearch") OrderSearch orderSearch, Model model) {
//아래 findOrders 같은 단순 위임이면 repository에서 바로 호출하도록 하기
List<Order> orders = orderService.findOrders(orderSearch);//findAllByString
model.addAttribute("orders", orders);
return "order/orderList";
}
}
> @ModelAttribute 세팅을 하면 mapping된 것이, model로 자동으로 담긴다.(addAttribute 안해도 됨)
>> Spring MVC 참조...
B. 화면 : orderList.html
> 랜더링 하기
> <form th:object="${orderSearch}" class="form-inline"> <tr th:each="item : ${orders}">
> enum 반복문으로 뿌리는 방법
<option value="">주문상태</option>
<option th:each="status : ${T(jpabook.jpashop.domain.OrderStatus).values()}"
th:value="${status}"
th:text="${status}">option
</option>
7.9.2 주문 취소
A. 화면 - cancel 버튼 추가
<td>
<a th:if="${item.status.name() == 'ORDER'}" href="#"
th:href="'javascript:cancel('+${item.id}+')'"
class="btn btn-danger">CANCEL</a>
</td>
<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>
B. Controller - Post처리
@PostMapping(value = "/orders/{orderId}/cancel")
public String cancelOrder(@PathVariable("orderId") Long orderId) {
orderService.cancelOrder(orderId);
return "redirect:/orders";
}
2편 - API 개발과 성능 최적화는 다음 강의 이어서...
'Back > JPA' 카테고리의 다른 글
실전! 스프링 Data JPA - 김영한 (2) | 2022.12.26 |
---|