Back/JPA

[김영한, inflearn] 실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발

TimeSave 2023. 1. 9. 00:23

목차


    학습목표 : 실무에 가까운 복잡한 예제로 애플리케이션 개발 해보는 것이 목표


    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. 회원 엔티티

    회원엔티티(좌), 회원테이블(우), 엔티티에서 Order와 Delivery가 단방향 관계로 잘못 그려져 있다. 양방향 관계가 맞다. 

    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) 사용 이쁘게 만들기

    > https://getbootstrap.com/ 

    All releases 이동
    downloads 페이지 링크 클릭
    다운로드

    > 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