Back/JPA

실전! 스프링 Data JPA - 김영한

TimeSave 2022. 12. 26. 18:04
JPA Entity를 대상으로 먼가 지지고 볶는 것.

 

 

  • 1. 프로젝트 환경설정
    • IntelliJ Settings
      • BuildTools > Gradle > change Build+Test : IntelliJ
      • Compiler > Annotation Processors > Enable
    • 프로젝트 생성 
      • start.spring.io  +  Gradle + Java + ADD web, jpa, h2, lombok
    • 라이브러리 살펴보기
      • dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
        implementation 'org.springframework.boot:spring-boot-starter-web'
        compileOnly 'org.projectlombok:lombok'
        runtimeOnly 'com.h2database:h2'
        annotationProcessor 'org.projectlombok:lombok'
        testImplementation 'org.springframework.boot:spring-boot-starter-test'
        }
      • Transparent Dependancy : 추가적인 연관 dependancy
      • HikariCP : spring boot 2.x 대 부터 DB Connection pool 을 Hikari로 사용
        • 매우 빠름. 실무에서 성능 극한을 다루려면 메뉴얼 봐야 함.
      • AssertJ : 기존 대비 chainning 방식으로 짤 수 있게 개선된 라이브러리.
      • 버전 명기가 안 된 것 : spring boot가 맞는 버전을 사전에 세팅 해놔서 생략한 것.
        • gradle download 된 것 확인하여, 관련 클라이언트 설치하면 됨(h2...)
      • 핵심 라이브러리
        • 스프링 MVC
        • 스프링 ORM
        • JPA, 하이버네이트
        • 스프링 데이터 JPA
      • 기타 라이브러리
        • H2 데이터베이스 클라이언트
        • 커넥션 풀: 부트 기본은 HikariCP
        • 로깅 SLF4J & LogBack
        • 테스트 
    • H2 데이터베이스 설치
      • https://www.h2database.com
      • 파일로 접근하기 : jdbc:h2:~/datajpa (첫 한번만, 파일로 접근하면 lock 생겨서 곤란해짐)
      • 원격접근 :  jdbc:h2:tcp://localhost/~/datajpa
    • 스프링 데이터 JPA와 DB 설정, 동작확인
      • profile 파일 변경하기
        • application.properties(삭제) -> application.yml 생성
        • 아래처럼 spring, jpa, logging 세가지 설정 넣어준다.
        • ddl-auto: create
        • hibernate:
          • show_sql = 콘솔에 찍기
          • format_sql = 쿼리를 이쁘게 틀 잡아서 출력
          • logging level : debug = log 파일로 sql 남기기
          • org.hibernate.type: trace = binding 된 파라미터 까지 볼 수 있는 옵션
      • Entity 생성(JPA 필수)
        • @Entity, @Getter
        • field 추가
      • Repository 생성
        • @Repository
        • @PersistenceContext 추가(EntityManager)
        • CRUD 메소드 추가
        • Spring Data Repository 생성을 위해선 extends JpaRepository 선언
      • Test 생성 : cmd(ctrl) + shift + t
        • JPA는 entity의 default 생성자(parameter 없는)가 필요하다.
          • protected level로 : JPA proxy 방법 접근을 위해 
            • protected void Member(){} 
            • 상단에 @NoArgsConstructor(access = AccessLevel.PROTECTED) 선언으로 대체 할 수 있다.
          • assertThat().isEqualTo : == 연산과 동일
          • 동일 transaction 안에선 동일성 보장된다.(== 1차 caching)
          • 공부를 위해 query parameter 보는 설정(현업에서 쓰지 말 것)
            • gradle dependancy에 implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.7' 추가
spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/datajpa
    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
  # org.hibernate.type: trace

 

 

 

  • 2. 예제 도메인 모델(Member : Team = n:1)
    • 예제 도메인 모델과 동작확인
      • DB에서는 foreign key가 1:다 에서 다에 들어가야 한다.
      •  @Column(name = "member_id")
        • 실무style : 관리상 "{table명}_id" 로 쓴다. join할 때 편하다.
      • 연관 column field 만들기 전에 해당 Entity class가 먼저 생성되어 있어야 한다(편함)
      • 관계선언  
        • @ManyToOne
              @JoinColumn(name = "team_id")
              private Team team; //foreign key 설정. 단일 객체임을 주의깊게 보자.
          • ManyToOne의 fetch 기본 타입이 EAGER다. LAZY로 직접 선언해주자
            • EAGER는 성능 최적화 어렵다.
            • member조회 끝 -> team 조회 에서 member+team 조회 iteration이 된다.
            • LAZY : 해당 Entity 호출할 그 시점에 쿼리
        • @OneToMany
              private List<Member> members = new ArrayList<>(); //List임을 주의
        • 이렇게 양쪽 설정하는 건, mapped by 설정 필요함. FK 없는 쪽에 거는 것이 좋다!
      • @ToString(of = {"id", "username", "age"}) : 객체 바로 찍을 때 출력 되는 것. 선언한 field 타고가서 출력을 해주며, 연관관계 field는 무한loop 유발 할 수 있으므로 적지 말 것.
      • 연관관계 세팅 메소드(Member Class에)
        •  public void changeTeam(Team team) {
                  this.team = team;
                  team.getMembers().add(this);
              }
          • 팀이 변경 되면, team의 members도 같이 수정. 이걸로 생성자에서 team 세팅을 한다?
      • EntityManger.flush() : 강제로 DB에 모아놓은 쿼리 날림
      • EntityManager.clear() : JPA persistence context에 있는 cache clear

 

 

 

  • 3. 공통 인터페이스 기능
    • 순수 JPA 기반 리포지토리 만들기
      • JPQL : select m from Member m, 객체를 대상으로 하는 쿼리(Table X)
        • JPQL -> SQL 번역 -> DB에서 반환
      • JPA는 update가 없다.
        • collection과 동일한 형태로 다룸. entity 수정하고 commit하면 자동으로 인지해서 update query날림
        • = dirty checking
      • findAll()등 CRUD 메소드 반복 => 해결해보자.. generic... -> Interface로 해결(오픈소스) -> Spring Data JPA로 발전.
    • 공통 인터페이스(JpaRepository) 설정
      • implements가 없는데 어떻게 사용 되나
        • Injection 주입 받는 것 print 찍어보면, com.sun.proxy.$XXX : Spring Data JPA가 implements class를 알아서 만들어 주입.
      • @Repository annotation이 없다?
        • JpaRepository Extends 한 건 없어도 된다.
        • extends JpaRepository<{EntityType},{id type}> 형식으로 표기한다.
          • 여기서 id란 model의 id 이다.(the type of the id of the entity the repository manages)
    • 공통 인터페이스(JpaRepository) 적용
      • TestCase를 하나 만들어서, @Autowired로 interface Repository에 주입을 시키고, 돌려본다.
        • 잘돌아가면, 어디선가 implements가 잘 만들어졌다는 뜻이고, Spring Data JPA가 잘 넣어 준 것이다.
    • 공통 인터페이스 분석(개발자가 상상할 수 있는 모든 기능 제공)
      • spring-data-jpa
        • JpaRepository
          • org.springframework.data.jpa.repository, spring-data-jpa
          • Package 경로를 보면, Spring data의 일부로, jpa 특화의 느낌을 알 수 있다.
      •   spring-data-commons : Mongo, Redis, JPA 등 공통으로 다 쓰는 것.
        •  PagingAndSortingRepository
          • org.springframework.data.repository, spring-data-commons
        • CrudRepository
          • org.springframework.data.repository, spring-data-commons
        • Repository : marker interface. classpath scanning for easy Spring bean creation. bean 스캔용.
      • T findOne(Id) -> Optional<T> findById(Id) 로 변경됨 주의
      • T = 엔티티, Id = 엔티티의 식별자 타입, S = 엔티티와 그 자식 타입
      • 주요메서드
        • Save(S) : save & merge.
        • delete(T) : delete 1 entity. EntityManager.remove()
        • findById(ID) : get 1 entity. EntityManager.find()
        • getOne(ID) : entity를 proxy로 조회 -> reference 반환(가짜 프록시 개체). EntityManager.getReference()
          • proxy를 touch해서 실제로 값을 꺼낼 때, query가 날라가서 값을 가지고 옮(프록시가 초기화 된다?)
        • findAll(_) : Sort, Paging 조건을 제공. 쿼리나갈때 sort 해주고, page 쿼리도 나가고.
      • 도메인 특화 기능 개발해야한다면??
        • findByUserName? => implements 구현 필요없다
          • 쿼리메소드 기능 사용
  • IntelliJ Tip) 이전 것 왔다갔다 하기 : Cmd+E+Enter
  • 4. 쿼리 메소드 기능 
    • 쿼리를 어떤식으로 해결할지 제공하는 강력한 3가지 기능
      • 메소드 이름으로 쿼리 생성
        • 관례를 가지고 String분석하여 생성
          • 이름, 즉 문법이 중요!. 엔티티의 필드명이 변경되면 인터페이스에 정의한 메서드 이름도 꼭 함께 변경
        • ex) List<Member> findByUsernameAndAgeGreaterThan(String username, int age)
        •  UsernameAndAge :Username=Username,  And 조건, AgeGreaterThan= Age> ?
        • Spring Data JPA - Reference Documentation
        • Spring Data JPA - Reference Documentation
        • Parameter 최대 2개정도가 적당하며, 짤막한 쿼리 필요할 때 아주 유용하다.
      • JPA NamedQuery
        • 선언 : Entity에 @NamedQuery(name="Entity.methodName", query="{JPQL}") 추가
        • 쿼리에 이름을 부여하고 호출하는 기능
          • repository에 em.createQuery("Entity.methodName", Member.class)..... 혹은
          • @Query(name = "Entity.methodName") List<Member> methodName(@Param() String "");
          • 혹은 @Query도 생략 가능 => Entity.methodName으로 작성하면 알아서 찾음.
          • 우선순위 Named query > Method query
        • 실무에서 거의 안 씀. 장점 : app loading 시점에 파싱해서 문법 오류 알려줌
      • @Query, 리포지토리 메소드에 쿼리 정의하기** : 실무 활용형태
        • @ Query("select m from Member m where m.username = : ~!@#!) 
        • 인터페이스 메소드위에 바로 annotation으로 선언
        • 복잡한 쿼리 핸들링
        • app loading 시점에 파싱해서 문법 오류 알려줌(정적쿼리)
        • 동적쿼리는 queryDSL 권장(깔끔 + 유지보수성)
    • @Query, 값, DTO로 조회하기 : 단순히 값 하나를 조회
      • 값 조회 : 상단에 @Query 및 method 선언
      • DTO 조회
        • @Data는 Getter Setter 포함이라 Entity에는 웬만하면 선언 하지 말 것
        • @Query("select new study.datajpa.dto.MemberDto{m.id, m.username, t.name} from Member m join m.team t")
          • 생성자에 매칭하여 new Package경로{column} 적는 것이 포인트 == 생성 후 반환해주는 개념
          • new operation이라고 함. 
  • IntelliJ Tip) 코드블럭 위아래 옮기기 사용 용도 : 코드 선행관계 조정할 때 
    • 파라미터 바인딩(이름기반)
      • 위치기반
        • select m from Member m where m.username = ?0 //위치 기반. 거의 안씀
      • 이름기반
        • select m from Member m where m.username = :name //이름 기반(권장)
      • 컬렉션 파라미터 바인딩 : 인자를 이쁘게 만들어 줌. 괄호 쉼표 등등...
        • @Query("select m from Member m where m.username in :names")
        • in :names 
        • -> from member where username in(?,?) 형태로 변환
    • 반환 타입 : 알아서 잘 반환. 유연하게 사용가능
      • List<Member> findByUsername(String name); //컬렉션
        Member findByUsername(String name); //단건
        Optional<Member> findByUsername(String name); //단건 Optional 
      • collection
        • 없다면 empty collection return
      • 단 건
        • 메서드를 호출하면
          • 순수 JPA : 내부에서 JPQL의 Query.getSingleResult() 메서드를 호출.조회 결과가 없으면 javax.persis tence.NoResultException 예외가 발생
          • 스프링 Data JPA : 단건을 조회할 때 NoResultException 발생하면 무시하고 null 을 반환한다.
          • JPA와 동작 방식이 다른 사례.
          • 옵셔널로 받을 것. 
      • 그 외, 공식 문서 참고 : Spring Data JPA - Reference Documentation
    • 순수 JPA 페이징과 정렬
      • 페이징 : DB에 있는 데이터를 적절한 size로 끊어서 끌어오는 것. ex) 100만건 load 불가...
        • 페이징으로 끊어서 화면,API에 전달.
        • SQL로 페이징 하면 너무 어려움 ex) row num을 3번넘게 넣어서 select 막 감싸고..
        • int offset, int limit : 페이징 쿼리 condition. 몇번째 부터 시작해서 몇개를 가져와라 명령을 위한 변수
        • .setFirstResult(offset) : 어디서 부터 가져올 것이냐 = 페이징 쿼리로 설정
        • .setMaxResults(limit) : 가져올 갯 수 설정
        • Total Count query 추가 : 몇번쨰 페이지인지 알아야 함.
        • 페이지는 0부터 시작한다.
        • db가 바뀌어도 JPA가 알아서 쿼리를 날린다 : dialect(방언)

      •  
      • 스프링 데이터 JPA 페이징과 정렬
        • interface 2개로 페이징 표준화,공통화(감동적..)
          • 모든 DB는 페이징을 제공하는데, 이것을 공통화 처리 해버렸다. 
          • org.springframework.data.domain.Sort : 정렬 기능
            org.springframework.data.domain.Pageable : 페이징 기능 (내부에 Sort 포함)
        • 작성조건
          • Parameter에 Pageable : 인터페이스, 쿼리에 대한 조건, 쿼리 리미트를 걸기 위해 : 현재 1페이지야..
          • 반환타입 Page/Slice/List 선언 : total count 등을 결정
        • 반환타입(Page/Slice)
          • 반환 타입 Page : TotalCount 쿼리 같이 날림. 필요하니까
            • org.springframework.data.domain.Page : 추가 count 쿼리 결과를 포함하는 페이징
            • Contents + totalCount query 같이나감
            •  Page<Member> findByAge(int age, Pageable pageable) 형태로 쓴다.
              • 사용
                • PageRequest pageRequest = PageRequest.of(0, 3, Sort.by(Sort.Direction.DESC, "username"));
                        
                • Page<Member> members = memberRepository.findByAge(age, (Pageable) pageRequest); //PageRequest -> AbstractPageRequest -> Pageable
                • page.getContent() : page 내부 실제 데이터 가져오기(contents)
                • page.getNumber() : 해당 page의 번호
                • page.getTotalElements(): 전체 element 갯수
                • page.getTotalPages() : 총 page 갯수
                • page.isFirst() : 첫페이지인지 아닌지 리턴
                • page.hasNext() : 다음페이지 있는지 리턴
          •  TotalCount 불필요 : slice (더보기 버튼 style.. 표시를 숨겨놓기)
            • org.springframework.data.domain.Slice : 추가 count 쿼리 없이 다음 페이지만 확인 가능
              • 다음페이지 확인 원리 : limit + 1 조회하여 contents 갖고 옴.
                • 기준 : 다음페이지가 있냐/없냐 정도로 푸는 것..?
                • 전체 count 가져오지 않음
                  • Contents query만 나감
                • 따라서 아래 두개는 존재하지 않는다.
                  • slice.getTotalElements(): 전체 element 갯수
                  • slice.getTotalPages() : 총 page 갯수
              • slice.getContent() : page 내부 실제 데이터 가져오기(contents)
              • slice.getNumber() : 해당 page의 번호
              • slice.isFirst() : 첫페이지인지 아닌지 리턴
              • slice.hasNext() : 다음페이지 있는지 리턴
            • Slice extends Streamable
          • cf) List (자바 컬렉션): 추가 count 쿼리 없이 결과(List)만 반환. 정확히 몇개만 끊어받기
        • Paging 안 쓰려고 하는 이유 : total count가 전체조회라, 성능을 잡아먹기 때문
          • total count query를 잘 짜야 함.(ex) left outer join 같은 경우 count에서 똑같이 join 쓸 필요가 없다..)
            • 그래서 count query 보통 분리한다
              • 분리하는 법 : 쿼리 정의시 분리해서 쓸 수 있도록 되어 있음.
                • @Query(value = "select m from Member m left join m.team t",countQuery = "select count(m.username) from Member m")
        • pagable 등이 등장하면서, JPQL을 직접 짜더라도, totalcount 같은 핵심비즈니스 외 관심사,잡무에서 벗어날 수 있게 되었다.
        • API, Controller에서 Page<Entity> 그대로 반환하면 큰일 난다.
          • Entity는 외부에 절대 노출하면 안됨. 무조건 DTO로 변환해서 노출해야 함.
            • Entity 바꾸면 API spec이 바뀌고 API들 다 장애가 난다.
          • Page<Entity> 를 Page<DTO>로 변환하기 : map
            • page.map(member -> new MemberDto(member.getId(),....);
          • Page를 API로 반환해도 좋은 이유
            • 데이터가 모두 JSON으로 반환, content도 JSON으로 전송, response body JSON으로 보내면 깔끔 -> front에서 알아서
      • 벌크성 수정 쿼리
        • JPA dirty checking은 단건이다, 한번에 수정할 필요가 있다 = 벌크성
        • entity 중심이라 query와 다르게 분리되어 있다.
        • em.createQuery().executeUpdate() : 응답 값의 갯수를 반환
        • @Modifying : modifying이 있어야 select가 아니라 executeUpdate를 실행함
        • JPA 벌크성 업데이트 주의사항(transaction에 bulk 연산만 있다면 무관)
          • PersistenceContext에서 Entity가 관리가 되어야하는데, 무시하고 DB에 강제 Update하는 것
            • PersistenceContext (1차 캐시)에 있는 엔티티의 상태와 DB에 엔티티 상태가 다를 수 있다.
              • DB 반영 타이밍 전일 수가 있다.
              • 권장하는 방안
                • 1. 영속성 컨텍스트에 엔티티가 없는 상태에서 벌크 연산을 먼저 실행.
                • 2. 영속성 컨텍스트에 엔티티가 있으면, 벌크 연산 직후 영속성 컨텍스트를 초기화
                  • 같은 transaction이면 같은 EntityManager가 올라와서 동작
                  • em.clear(); 추가 or @Modifying(clearAutomatically = true) 로 설정
                  • 깔끔한상태에서 DB에서 다시 조회해 가져 옴.
            • MyBatis, JDBC Template등 연동할 때도 동일 문제 발생 가능하다. 
        • cf) JPQL 수행 전에, 항상 영속성 컨텍스트의 Data를 DB에 보내는 작업이 있다(flush)
      • @EntityGraph : fetch join 편하게 쓰는 문법. 연관된 엔티티들을 SQL 한번에 조회하는 방법
        • fetch join : 지연로딩과 발생하는 연관 문제를 먼저 이해할 것
          • member1 -> teamA, member2 -> teamB
            member - team은 지연로딩 관계이다. select member만 했을 때는 가짜 개체(proxy)만 가져온다.
            • getClass()로 중간에 print 해보면 확인 가능
          • team의 세부정보를 알려고 할 때, read해 놓은 데이터가 없으므로 가져올 때(proxy 초기화)마다 쿼리가 실행된다. (N+1 문제 발생) 
            • N+1
              • 1 : 원래 select 한 쿼리
              • N : 1에 연계된 n개의 추가 쿼리
          • 결국 fetch join은 N+1 문제 해결에 사용
            • @Query("select m from Member m left join fetch m.team") 
            • 조회 할 때 연관된 것을 같이 조회해버린다(or 다 받아버린다, fetch)는 뜻.  
              • proxy가 아니므로 proxy 초기화 할 필요가 없어진다.
              • 연관관계가 있는 것 = 객체그래프라고 함
              •  select 할 때 data를 다 넣고 join 함(기본적으로 left outer join)
              •  "한번의 쿼리로 끝내!"
        • Spring Data JPA는 fetch join 때문에 JPQL을 사용할 수 밖에 없는 상황(네임 쿼리 등 불가..)
          • @EntityGraph를 도입하여 처리하도록 개발 됨(JPQL 짜기 싫어서)
          • 메소드 위에 @EntityGraph(attributePaths ={"team"}) 으로 선언하면 됨.
          • fetch join을 annotation으로 바꾼것.
          • JPQL 없이도 객체그래프를 엮어, 성능최적화 해서 한번에 가져오는게 된다
          • JPQL 짰는데 fetch join 추가 하고 싶을 때도, @EntityGraph(attributePaths ={"team"}) 사용 가능
            • 쿼리문 안에 join 필요 없음.
          • 적절한사용 : 연관된 것이 매우 잦은 빈도로 같이 사용할 때.
        • @NamedEntityGraph 사용방법
          • Entity에 선언 : @NamedEntityGraph(name = "Member.all", attributeNodes = @NamedAttributeNode("team"))
          • Repository에 선언 : @EntityGraph("Member.all")
          • Entity에 name tag를 달고, 연관 node 명시하여 같이 조회하도록 하는 형태인 듯.
          • 보통 안 씀
      • JPA Hint & Lock
        • JPA Hint : SQL Hint 아님, Hibernate(or 다른 구현체..)에게 알려주는 힌트
          • Interface 이상으로 Hibernate가 뭔가 따로 쓸 때 필요함
          • 대표적으로 ReadOnly에 쓴다.
          • flush() 까지는 정보가 영속성 context에 남아있다.
          • update시 dirtychecking을 위해선 원본이 있어야 한다.(메모리 먹는다)
          • dirtychecking 때문에 원본을 따로 만드는 로직이 있다
            • 변경을 안하는 상황에선 매우 불필요
            • Hibernate에서 이 상황 해결 기능 제공(최적화 기능, 구멍을 만들어 놨다)
            • 메소드 위에 @QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true")) 선언하면 됨
              • readonly = true 세팅 시 원본/변경본의 snapshot을 안 만든다. dirty checking 안 함
        • Lock
          • select for update : DB에 lock 거는 것. JPA도 지원 함.
          • 메소드 상단에 @Lock(LockModeType.PESSIMISTIC_WRITE) 선언
          • 쿼리끝에 for update 가 추가 됨.
          • 공식문서 참고 할 것.
          • 실시간 많은 서비스에서는 사용금지. 특히 PESSIMISTIC Lock
            • 차라리 Optimistic Lock이 나음(versioning 방식)
            • 금융 관련, 원장 맞추는 스타일은 사용 하긴 함.
  • 5. 확장 기능
    • 사용자 정의 리포지토리 구현(쿼리 커스텀 필요할 때 사용 ex)query dsl, 기타 동적쿼리.. )
      • 내 Custom기능 몇 몇개만 넣고 싶을 때, 인터페이스 메소드 직접 구현할 때 사용
        • extends interface를 추가하는 원리.
        • JAVA가 해주는 것은 아니고 Spring Data JPA가 해주는 것이다.
        • JPA 직접 사용(Entity Manager), JDBC Template 사용, Querydsl 사용, Mybatis 사용....
        • Spring Data JPA를 활용 위해 상속을 받아야 하는데, super class의 모든 것을 구현하는 건 현실적이지 않다.
      • 순서 : Custom Interface 생성 > target Repository Impl 생성{EntityManger} ->  target repository의 extends에 추가
        • Impl 구현
          • 1. @PersistenceContext 선언(생성자 1개면 생략가능) : EntityManager 주입
          • 2. @RequiredArgsConstructor : 생성자 대체 가능
          • 3. interface 함수 override -> em.createQuery()
            • JDBC로 DB 직접 native query 날리고 싶다면 connection 얻는 형태로 가면 됨.
          • 4. target repository의 extends에 추가
      • 주의 규칙 : impl은 꼭 Target Repository로 세팅해야 한다. ex)MemberRepositoryImpl
        • Impl이 싫다면, 아래 설정(근데... 관례 그냥 따르자)
          • xml설정의 경우 : <repositories repository-impl-postfix="Impl" /> 변경
          • javaConfig 설정의 경우 : @EnabeJpaRepositories( repositoryImplementationPostfix = "")
      • 핵심 비즈니스 로직 & 화면에 맞춘 쿼리(Dto..)를 분리할 필요가 있음.
        • Repository를 그냥 분리해라! custom해서 쓰지 말고, Class를 따로 만들어서 쿼리 날리자
          • 분리할 때 고민할 점 : 핵심 비즈니스 & 그외 lifecycle 관점에서, command & query 분리 ...
    • Auditing
      • entity 이력 추적 관리를 편하게 하는 방법 
      • 테이블 생성시, 등록일, 수정일, 등록자&수정자(세션정보 기반) 필드 꼭 포함.
      • 순수 JPA 사용
        • Java : LocalDateTime
        • @Column(updatable = false) 추가 : 수정불가 선언. created Date 필드
        • @PrePersist 추가 : 이벤트 어노테이션. persist(저장) 전에 이벤트 발생. 메소드에 추가
          • this. 생략 이유 : 요즘은 id를 다 사용하므로. 강조의 용도 외에는 표기 안 함.
          • LocalDateTime.now()로 데이터 미리 세팅하는 이유 : 미리 해놔야 쿼리 날릴 때 편하다.
            • null이 있으면 쿼리 지저분 하다.
            • 등록일 수정일 미리 맞춰두기. 최초 등록된 데이터구나~
        • @PreUpdate 추가 : 이벤트 어노테이션. update 전에 이벤트 발생. 메소드에 추가
          • updateDate 갱신
        • 대상 Entity에 윗 단계에서 생성한 class를 extends 처리
          • 날짜가 없을 때
            • JPA 진짜 상속 : 따로 있음. 추후 추가
            • 속성만 상속 : @MappedSuperClass, 데이터만 공유하는 것
          • 쿼리 날릴 때 같이 추가 되는 것 확인 가능
        • 장점 : 여기저기 extends로 받으면 등록일,수정일 자동화(공통관심사)
      • Spring DATA JPA 활용
        • 스프링 부트 설정 클래스(Application main class)에
          • @EnableJpaAuditing(AuditingEntityListener.class) 설정 추가
            • 변경 마다 값 채우는것 null 로 두고 싶은 경우(ModifyOnCreate = false) 로 설정 추가
          • CurrentAuditor 추가 : @Bean public AuditorAware<String> autditorProvider()
            • SpringSecurity 등등 경우 : Session정보 가져와서 User Id 꺼냄
              • 등록자/수정자에 넣음
        • Entity에 @EntityListeners(AuditingEntityListener.class) 추가
          • Event 기반으로 동작하도록 설정
        • @CreatedDate 사용 : PrePersist 사라짐
        • @LastModifiedDate 사용 : PreUpdate 사라짐
        • @CreatedBy/@LastModifiedBy :등록자. 수정자
          • 이벤트 감지시, AuditorAware 호출해서, 그때마다 결과물 꺼내서 채움
          • 등록자/수정자는 안쓰는 경우도 많아서, 상위 클래스로 등록일,수정일 만든 후 상속 받아 추가하는 형태로 구현한다.
        • Entity Listener 관련 설정(@EnableJpaAuditing, @EntityListeners..)생략하고 싶은 경우
          • META-INF/orm.xml에 <entity-listeners> 설정 등록.
      • Web 확장 - 도메인 클래스 컨버터
        • PK일 때, domain class converter 사용 가능. Spring Data JPA가 repository query통해서 return을 위한 parameter 변환 과정을 해 줌. 권장하진 않는 기능. 단순 조회 용만 쓸 수 있다. 트랜잭션 없이 조회하여, Entity 변경시 DB반영이 안 됨.
          • AsIs : public String findMember(@PathVariable("id") Long id){ repository~~ > return }
            • ToBe : public String findMember(@PathVariable("id") Member member) {return }
        • @RestController 추가
        • @PathVariable 설정
        • @PostConstruct 설정 : Spring Application 올라올 때 실행.
      • Web 확장 - 페이징과 정렬
        • 페이징이 웹에서 바로 바인딩 되록 함.
          •  Controller > repository call(parameter pageable) -> PagingAndSortRepository
          • page & size 지원
            • "/members" mapping시, http://localhost:8080/members?page=1&size=3&sort=id,desc&sort=username,desc 의 형태 사용가능
            • Parameter에 Pageable이 있으면,
            • PageRequest 객체를 생성해서, 값을 채워서,
            • Pageable에 inject 해 줌 
            • Sort
              • 계속 url뒤에 추가 가능
        • 요청파라미터 : page/size/sort
          • 기본값 변경 원할 때
            • 글로벌 설정 : application.yml 설정에 추가. Spring Boot 매뉴얼 확인하기
              • ex) data: web: pageable: default-page-size: 10 max-page-size:2000
              • 특별한 설정 : 메소드 파라미터 넣을 때 annotation 추가. 최우선으로 적용됨
              • AsIs : (Pageable pageable)
              • ToBe : (@PageableDefault(size=5, sort="username") Pageable pageable)
        • Postman 툴: http return 값 이쁘게 보기 위해 사용(ex) JSON return)
        • Page반환시는 항상 total Count query가 나간다.
        • 접두사 : 한 api에 페이징 정보 둘 이상시 접두사로 구분
          • @Qualifier 사용. ex) @Qualifier("member) , @ Qualifier("order) ...
            • 결과 : /members?/member_page=0&order_page=1
        • Page를 DTO 변환
          • DTO 반환해야 하는 이유
            • Entity는 외부에 노출하면 절대 안된다.
            • MVC 나눈 의미가 사라짐.
            • Entity 손대는 순간 API spec이 변경 됨
          • map을 통해서 내부 변경
            • page.map(member -> new MemberDto(param1,param2...))
            • 옛날엔 map이 없어서 직접 loop돌리면서 객체 새로 생성해서 담았었음..
          • DTO는 Entity 봐도 됨
            • public MemberDTO(Member member){ this.id = member.getId(), this.username...) 이렇게 변경 가능하고
            • (member -> new MemberDto(param1,param2...)) 이것이 MemberDto::new 로 변경가능
              • = 메소드 레퍼런스
              • :: = package java.util.function;
          • Page 1부터 시작하기
            • Pageable, PageRequest 직접 구현 : PageRequest.of(page:1, size:2)
            • spring.data.web.pageable.one-indexed--parameters = true로 설정
              •   web에서 파라미터를 -1로 바꿔서 처리하는 형태.
                • pageable 객체의 data는 요청과 안맞는 문제 발생!

 

  • 6. 스프링 데이터 JPA 분석
    • 스프링 데이터 JPA 구현체 분석(code level)
      • JPA 공통 인터페이스 구현체 : SimpleJpaRepository
        • findbyId,findAll... : 결국 EntityManager 통해서 다 작업. JPA 내부기능 활용
        • Query String을 미리 들고 있는 것을 볼 수 있다.
        • @Repository가 붙어있다
          • Spring Bean의 scan 대상
          • Exception을 Spring에 정의된 Exception 사용가능(매핑표 있음)
            • 하부 구현기술을 JDBC -> JPA로 바꿔도, exception 처리 매커니즘 동일(스프링 강점)
        • @Transational
          • @Transcational은 별다른 옵션 없으면 Transaction 이어 받아 동작한다.
          • Spring Data JPA의 모든 기능은 일단 Transaction 걸고 시작한다.
            • 즉, Transcation 표기 없어도, 알아서 Transaction 시작 한다.
              • 서비스 계증에서 시작 까먹어도 시작한다.
              • 모든 JPA 데이터 변경은 Transaction 안에서 일어나야 한다.
                • Spring Data JPA 사용 시 자동으로 됨!
                • Transcation 안에서 변경 안하면 예외 던짐
          • DatabaseConnection(set autocommit = false)로 넘기는 과정 포함 됨
          • readOnly = true 설정 : flush를 안하도록 하는 설정 -> 성능 개선 효과
            • read only니까 db 변경 필요없어서(dirty checking 등..) flush 안해도 됨.
        • save 메서드
          • isNew -> Persist 한다.
          • else(=DB에 이미 있던 것) -> merge 한다.
            • DB에서 read -> 교체 -> 반영 
          • 위 형태 때문에, DB Select 쿼리를 한번 하는 단점이 있다.
          • update시 save의 merge는 가급적 하지 말고, entity 값만 바꾸는 것을 사용해야 한다.
            • merge는 persist 상태를 벗어난 entity가 다시 persist 상태가 되어야 할 때 쓰는 것.
    • 새로운 엔티티를 구별하는 방법 : @Id @GeneratedValue
      • 전략
        • @Id가 객체 일 때 : null로 봄
        • @Id가 자바 primitive일 때 : 0으로 봄(객체가 아니기 때문)
      • @GeneratedValue
        • JPA에서 em.persist() 할 때 생성 함. JPA서 read해서 만들어 주입.
        • GeneratedValue를 안 쓴다면 : merge가 실행된다(select 문 + insert문 실행)
          • 데이터 저장은 꼭, persist로 해야한다. merge는 entity detached 상태에 쓴다.
          • Id를 직접 넣어 줄 것이다.
            • PK 값을 미리 세팅되어 있다.
              • persist 호출이 안 된다. null이어야 새걸로 판단하는데 pk가 null이 아니다.
                • 따라서, merge(DB에 값이 있다는 것이 가정된 메소드) 동작을 호출한다.
                  select문을 먼저 호출해서 read시도를 하고, 없으면 없다는 판단을하여
                  insert문을 추가로 호출.
          • 아래처럼 persist할 때, isNew판단 시(entityInformation.isNew(entity) = false)
            ex) public void save(){
            Item item = new Item("A");
            itemRepository.save(item);
      • Id를 직접 생성하고 싶은데... " : entity에 implements Persistable<String>을 사용하면 됨
        • @Override isNew(){} 해서 직접 로직 정의
          • step1. @EntityListener(AuditingEntityListener.class) 선언 
          • step2. @CreatedDate 사용해서 new 판단 : CreatedDate는 persist전에 호출 됨
            @CreatedDate
            private LocalDataTime createdDate;
          • step3. @Override boolean isNew() {
                   return createdDate == null;
            }
    • cf) JPA는 기본생성자가 꼭 있어야 한다.
    • data JPA 나머지 기능 : JOIN일부 구현 불가하여..네이티브 쿼리 빼고, 실무에서 안 씀...
      • => Query dsl로 다 처리 가능. 
      • Specifications (명세)
        • 도메인주도설계에서 소개 된 Specification 개념
        • where문 등 조건을 조립해서 쓸 수 있도록 하는 추상화 개념
        • JPA Criteria 활용해서 사용 할 수 있도록 지원
        • JPA에서 가장 잘못 설계되었다고 생각함..(영한's 의견)
          • 가독성 떨어지고, 복잡해 지면 이해불가. 사용하면 분명 후회할 것
        • predicate(술어)
          • 참,거짓으로 평가. AND OR 등 연산자로 검색조건 쉽게 생성(composite 패턴)
          • org.springframework.data.jpa.domain.Specification 클래스로 정의
        • 사용방법 : extends JpaSpecificatonExecutor<Entity> : findAll, findOne...
          • 파라미터에 Specification을 넣음
      • Query By Example : springframework.data.commons
        • . eEntity(도메인객체)가 검색 조건이 됨.
          • NoSQL변경해도, 코드 변경 필요 없음
        • 사용방법
          • Example<Member> example = Example.of(member);
            repository.findAll(example);
          • 연관관계 설정한 것도 조건으로 걸림.
        • Probe : 실제 도메인 객체
        • ExampleMatcher : 특정 필드를 일치시키는 상세정보. 구체적인 검색조건
          • 하지만, 결국 equal 조건 선에서만 찾을 수 있다.
        • Example : 쿼리 생성에 사용
        • INNER  JOIN만 가능하고 LEFT JOIN안 됨
      • Projections
        • 특정 필드, 특정 데이터만 조회하고 싶을 때 쓰면 좋음.
        • Interface기반 : AOP Proxy 이용 Spring Data JPA가 Implements등 구현하여 반환 해 줌.
          • Close Projection
            • Interface로 반환 타입 생성
            • repository 메소드에 반환타입 입력
              List<Inerface Model> findProjectionsByUserName("somename");
            • AOP Proxy 이용 Spring Data JPA가 Implements등 구현하여 반환 해 줌.
          • Open Projection
            • username + age + team.name 조회
            • @Value("#{target.username + ' ' + target.age + ' ' + target.team.name}") : spEL문법이라고 함
            • targetEntity를 전부 가져 와서, 원하는 데이터만 표출.(최적화 X)
        • Class 기반 Projection
          • 생성자의 parameter 이름으로 matching 시켜 projection
          • class이므로 proxy 안 씀.
        • 동적 Projection
          • 반환 타입에 <T>로 Generic 선언하고 Service에서 구체적으로 Class 타입 명시하면 됨
        • 중첩구조(연관관계, Interface 안에 Interface 선언)
          • public interface NestedClosedProjection {
                String getUsername();
                TeamInfo getTeam();
                interface TeamInfo {
                    String getName();
                }
            }
          • root 모델은 명시한 것만 가져오나, 내부의 연관모델은 전부 다 가져온다.
            • left join 사용
            • 비효율 임
      • 네이티브 쿼리
        • sql 직접 짜는 것. JPA 사용시 가급적 쓰지 말고, 최후의 수단으로 사용 할 것
        • @Query(value = "select * from member where username = ?", nativeQuery =
          true)
        • 반환타입이 정해져있고, 엔티티 일부만 보고 싶을 경우 난감해 진다.
          • 반환타입 : Object[], Tuple, DTO
        • Sort 정렬이 정상 동작하지 않을 수 있음(직접 처리 할 것)
        • app 로딩시점에 문법 확인 불가(JPQL은 로딩시 파싱과정 통해서 알 수 있는데..)
        • 동적쿼리 불가.
          • 동적 네이티브 쿼리 : 하이버네이트(외부 라이브러리 쓰는 것 권장)
          • String sql = "select m.username as username from member m";
            List<MemberDto> result = em.createNativeQuery(sql)
            .setFirstResult(0)
            .setMaxResults(10)
            .unwrap(NativeQuery.class)
            .addScalar("username")
            .setResultTransformer(Transformers.aliasToBean(MemberDto.class))
            .getResultList();
            }
        • 보통 Join등을 해서 DTO로 가져오고 싶을 때 쓰는 경향이 있는데, JdbcTemplate, myBatis 사용 권장
        • Projections 활용 할 때는 쓸 만 하다.
          • @Query(value = "SELECT m.member_id as id, m.username, t.name as teamName " +
            "FROM member m left join team t ON m.team_id = t.team_id",
            countQuery = "SELECT count(*) from member",
            nativeQuery = true)
            Page<MemberProjection> findByNativeProjection(Pageable pageable);
        • count 쿼리 별도로 짜야 한다.
        • 수백줄 쿼리는 쿼리 3개로 나눠서 수십줄로 처리하는 방법 권장