기능 예시

기능 목록

  • 회원 기능
    • 회원 등록
    • 회원 조회
  • 상품 기능
    • 상품 등록
    • 상품 수정
    • 상품 조회
  • 주문 기능
    • 상품 주문
    • 주문 내역 조회
    • 주문 취소
  • 기타 요구사항
    • 상품은 재고 관리가 필요하다.
    • 상품의 종류는 도서, 음반, 영화가 있다.
    • 상품을 카테고리로 구분할 수 있다.
    • 상품 주문시 배송 정보를 입력할 수 있다.

다대다

  • 관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없음
  • 연결 테이블을 추가해서 일대다, 다대일 관계로 풀어내야함
자체적으로 다대다를 할 수는 있지만 @ManyToMany 하지만 실무에서는 권장하지 않음

회원 한명이 여러 개의 상품을 구매했다고 가정하자. 그렇다면

  • 회원 입장
    • 한 명의 회원이 다양한 상품을 구매할 수있다. 1:n 관계
  • 상품 입장
    • 하나의 상품이 다양한 회원에게 판매 되어 질 수있다.

결론 관계형 데이터 베이스에선 정규화된 테이블 2개로 다대다 관계를 표현할 수 없다. 그렇지만 객체에서는 가능하다.

  • Member 쪽에서 products쪽과 ManyToMany로 연결해준다.

  • Product 쪽에서 JoinTable(Member_product)을 선언하여 링크테이블을 생성해주는 어노테이션을 단다.

  • 자동으로 1:n, n:1 관계를 만드는 것과 같은 원리이다.

오! 편리하다. 생각하고 사용하면 안된다.
- 연결 테이블이 단순히 연결만 하고 끝나지 않는다. (무슨말이지? 단순히 연결만 하고 끝나지 않고 추가 데이터가 들어간다. 별에별게 들어간다. 또한 중간 테이블이 예상할 수가 없다.)
- 주문시간, 수량 같은 데이터가 추가로 들어올 수 있다. (Member_Product에 들어감)

리팩토링

연결 테이블용 엔티티 추가 (연결 테이블을 엔티티로 승격!)

  • Member : 링크 테이블과 연결

  • Product : 링크 테이블과 연결

  • MemberProduct : 링크 테이블 추가
    • count, price, OrderDateTime을 넣을 수 있음

결론
양방향으로 여러개를 가질 수 있을 때 고민해야된다. 주인 한명에 강아지 2개 반대로 강아지 한마리에 주인이 여러명일 경우 우리가 그렇네! 그럴 경우 링크테이블을 걸어야한다.

도메인 모델과 테이블 설계

  1. 회원 <-> 주문 : 회원은 주문을 여러 건 할 수 있기 때문에 1:n의 관계이다.
  2. 주문<-> 배송 : 주문과 배송은 1:1 의 관계이다.
  3. 주문<->상품 : 주문과 상품은 n:n의 관계이다. 왜냐하면 한 번 고객이 한 번 주문 할때 여러 상품을 선택할 수 있기 때문이다.  이런 다대다 관계는 관계형 데이터베이스는 물론이고 엔티티에서도 거의 사용하지 않는다. 따라서 주문상품을 추가하여 주문 <-> 주문상품 <->상품(물품)으로 1:n, n:1로 풀어냈다.

엔티티 설계

  1. 회원(Member) : 이름과 임베디드 타입인 주소(Address), 그리고 주문(orders)리스트를 가진다.
  2. 주문(Order) : 한 번 주문시 여러 상품을 주문할 수 있으므로 주문과 주문상품(OrderItem)을 일대다 관계다.
  3. 주문상품(OrderItem) : 주문한 상품정보와 주문 금액(OrderPrice), 주문수량(count) 정보를 가지고 있다.
  4. 상품(Item) :이름, 가격, 재고수량(stockQuantity)을 가지고 있다. 상품을 주문하면 재고수량이 줄어든다.

테이블 설계

'Back-end > JPA' 카테고리의 다른 글

객체지향 쿼리 언어1 - 기본 문법  (0) 2022.09.24
연관관계 매핑 기초  (0) 2022.08.22
엔티티 매핑  (0) 2022.08.06
[JPA] JPA 영속성 컨텍스트  (0) 2022.05.05
[JPA] Batch Insert  (0) 2022.05.05

목차

    JPA는 다양한 쿼리 방법을 지원

    • JPQL (실무에서는 대부분 여기서 해결 됨)
    • JPA Criteria (망한 스펙 절대 안씀)
    • QueryDSL (실무 사용 권장)
    • 네이티브 SQL
    • JDBC API 직접 사용, MyBatis, SpringJdbcTemplate 함께 사용

    JPQL 소개

    • 가장 단순한 조회 방법
      • EntityManager.find()
      • 객체 그래프 탐색(a.getB().getC())
    • JPA를 사용하면 엔티티 객체를 중심으로 개발
    • 문제는 검색 쿼리
    • 검색을 할 때도 테이블이 아닌 엔티티 객체를 대상으로 검색
    • 모든 DB 데이터를 객체로 변환해서 검색하는 것은 불가능
    • 애플리케이션이 필요한 데이터만 DB에서 불러오려면 결국 검색 조건이 포함된 SQL이 필요

    JPQL 문법

    String jpql = "select m From Member m where m.name like '%hello%'";
    	List<Member> result = em.createQuery(jpql, Member.class).getResultList();
    List<Member> result = em.createQuery("select m From Member m where m.username like '%hello%'"
                Member.class).getResultList();

    JPQL의 문제점(동적 쿼리)

    *동적 쿼리 : 필요에 의해 직접 붙히거나 짜르는 쿼리

    String qlString = "select m From Member m";
    
    String username;
    if(username != null){
    	String where ="where m.username like '%kim%'";
        qlString + where;
    }

    위의 코드의 문제점

    • 복잡하다.
    • 휴먼에러가 발생하기가 쉽다.
      • qlString 과 where 의 문자열이 띄어쓰기가 된 건지 여부까지 체크해줘야한다. (오타는 당연하고)

    QueryDSL 소개

    다음과 같이 작성할 수 있다. 위의 단점을 해결 할 수 있다.

    • 기본 코드
    QMember m = QMember.member;
    List<Member> result = queryFactory
    					  .select(m)
                          .from(m)
                          .where(m.name.like("kim")
                          .fetch();
    • 동적 쿼리 사용 코드
    public List<Order> findAllByQuerydsl(OrderSearch ordersearch){
    	return queryFactory
        	   .select(order)
               .from(order)
               .join(order.member, member)
               .where(statusEq(orderSearch), memberNameEq(orderSerarch))
               .fetch();           
    }
    private BooleanExpression memberNameEq(OrderSearch oderSearch){
    	return hasText(orderSearch.getMemberName()) ? member.name.eq(orderSearch.getMemberName)): null;
    }
    
    private BooleanExpression statusEq(OrderSearch oderSearch){
    	return orderSearch.getMemberName() != null ? order.status.eq(orderSearch.getOrderStatus()):null;
    }

    'Back-end > JPA' 카테고리의 다른 글

    도메인 분석 설계 (N:N 뿌시기)  (0) 2022.11.19
    연관관계 매핑 기초  (0) 2022.08.22
    엔티티 매핑  (0) 2022.08.06
    [JPA] JPA 영속성 컨텍스트  (0) 2022.05.05
    [JPA] Batch Insert  (0) 2022.05.05

    목차

      연관관계 매핑

      객체와 테이블 연관관계 차이를 이해

      객체의 참조와 테이블의 외래 키 매핑

      용어 이해

      • 방향(Direction) : 단방향, 양방향
      • 다중성(Multiplicity) : 다대일(N:1), 일대다(1:N), 일대일(1:1), 다대다(N:M) 이해
      • 연관관계의 주인(Owner) : 객체 양방향 연관관계는 관리가 필요(가장 까다로움)

      연관관계가 필요한 이유

      객체지향 설계의 목표는 자율적인 객체들의 협력 공동체를 만드는 것이다.

      예제 시나리오

      • 회원과 팀이 있다.
      • 회원은 하나의 팀에만 소속될 수 있다.
      • 회원과 팀은 다대일 관계이다. (회원은 여러명이고, 팀은 1개이다.)

      객체를 테이블에 맞추어 모델링

      (연관관계가 없는 객체)

      Member는 N이며 TEAM은 1이다.

      @Entity
      public class Member {
      	@Id
      	@Column(name = "MEMBER_ID")
      	private String id;
      	private String username;
      	
      	//연관관계 매핑
      	@ManyToOne
      	@JoinColumn(name="TEAM_ID")
      	private Team team;
      	
      	//연관관계 설정
      	public void setTeam(Team team) {
      		this.team = team;
      	}
      	//Getter, Setter ...
      }
      @Entity
      public class Team {
      	@Id
      	@Column (name = "TEAM_ID")
      	private String id;
      	
      	private String name;
      	//Getter, Setter ...
      }
      Team team = new Team();
      team.setName("TeamA");
      em.persist(team);
      
      //Member
      Member member = new Member();
      member.setUsername("member1");
      member.setTeamId(team.getId()); //여기서 이상함
      em.persist(member);
      
      //문제가 많은 find하기
      Member findMember = em.find(Member.class, member.getId());
      Long findTeamId = findMember.getTeamId();
      Team findTeam = em.find(Team.class, findTeamId);

      위의 코드에서 문제점은 findMember를 memberId로 조회하고 그 조회한 내용 중에 TeamId를 가져와서 Team을 또 조회한다.

      Join으로 협력관계가 있는게 아닌 Select를 두 번 하는 꼴

      객체를 테이블에 맞추어 데이터 중심으로 모델링하면, 협력 관계를 만들 수 없다.

      객체 지향 모델링

      위의 모델링과 다른 점은 TeamId가 아닌 Team을 모델링 했다는 점이다.

      @Entity
      public class Member {
      	@Id
      	@Column(name = "MEMBER_ID")
      	private String id;
      	private String username;
      	
      	//연관관계 매핑
      	@ManyToOne
      	@JoinColumn(name="TEAM_ID")
      	private Team team; //가장 중요
      	
      	//연관관계 설정
      	public void setTeam(Team team) {
      		this.team = team;
      	}
      	//Getter, Setter ...
      }

      @ManyToOne

      Member 입장 에서 N이고 Team이 1이기 때문에 ManyToOne이다. 

      Team team = new Team();
      team.setName("TeamA");
      em.persist(team);
      
      //Member
      Member member = new Member();
      member.setUsername("member1");
      //member.setTeamId(team.getId()); //여기서 이상함
      member.setTeam(team);
      em.persist(member);
      
      //문제가 많은 find하기
      Member findMember = em.find(Member.class, member.getId());
      //Long findTeamId = findMember.getTeamId();
      //Team findTeam = em.find(Team.class, findTeamId);
      Team findTeam = findMember.getTeam();

      위의 findMember는 select 문이 join문으로 알아서 해준다.

      'Back-end > JPA' 카테고리의 다른 글

      도메인 분석 설계 (N:N 뿌시기)  (0) 2022.11.19
      객체지향 쿼리 언어1 - 기본 문법  (0) 2022.09.24
      엔티티 매핑  (0) 2022.08.06
      [JPA] JPA 영속성 컨텍스트  (0) 2022.05.05
      [JPA] Batch Insert  (0) 2022.05.05

      목차

      JPA를 사용하는 데 가장 중요한 일은 엔티티와 테이블을 정확히 매핑하는 것이다.
      • 객체와 테이블 매핑 : @Entity, @Table
      • 기본 키 매핑 : @Id
      • 필드와 컬럼 매핑 : @Column
      • 연관관계 매핑 : @ManyToOne, @JoinColumn

      객체와 테이블 매핑

      @Entity

      • @Entity가 붙은 클래스는 JPA가 관리, 엔티티라 한다.
      • JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 필수이다.
      • 주의
        • 기본 생성자 필수(파라미터가 없는 public 또는 protected 생성자)
        • final 클래스, enum, interface, inner 클래스 --> 사용x
        • 저장할 필드에 final 사용 x

      @Table, enumtype

      • @Table은 엔티티와 매핑할 테이블을 지정한다. 생략하면 매핑한 엔티티 이름을 테이블 이름으로 사용한다.
      • createdDate, lastModifiedDate : 자바의 날짜 타입은 @Temporal을 사용해서 매핑한다.
      • roleType : 자바의 enum을 사용하려면 @Enumerated 어노테이션으로 매핑해야한다.
      • 회원을 설명하는 필드는 길이 제한이 없다. 따라서 데이터 베이스의 VARCHAR 타입 대신 CLOB 타입으로 저장해야 한다. @Lob을 사용하면 CLOB, BLOB 타입을 매핑할 수 있다.
      package jpabook.start; 
      import javax.persistence.*; 
      import java.util.Date; 
      
      @Entity 
      @Table (name="MEMBER") 
      public class Member { 
      	
      	@Id 
      	@Column (name = "ID") 
      	private String id; 
      	
      	@Column (name - "NAME") 
      	private String username; 
      	
      	private Integer age; 
      
      	//== 추가 == 
      	@Enumerated (EnumType. STRING) 
      	private RoleType roleType; // 1
      
      	@Temporal (TemporalType. TIMESTAMP) 
      	private Date createdDate; // 2
      
      	@Temporal (TemporalType. TIMESTAMP)  
      	private Date lastModifiedDate; // 2
       
      	@Lob 
      	private String description; //3
      }
      
      //Getter, Setter 
      package jpabook.start; 
      
      public enum RoleType { 
      	ADMIN, USER
      }

      DDL 생성 기능

      • 제약조건 추가 : 회원 이름 필수, 10자 초과 X
      @Column(nullable = false, length =10)
      • 유니크 제약조건 추가
      @Table(uniqeConstraints = (@UniqueConstraint(name = "NAME_AGE_UNIQUE", columnNames ={"NAME", "AGE"})))
      • DDL 생성 기능은 DDL을 자동 생성할 때만 사용되고 JPA의 실행 로직에는 영향을 주지 않는다.

      @Column

      옵션
      설명
      name 필드와 매핑할 테이블의 컬럼 이름
      insertable, updatable 등록, 변경 가능 여부 (기본 값 : TRUE)
      nullable(DDL) null 가능 여부, false 설정하면 DDL 생성 시에 Not Null 조건이 붙다.
      unique(DDL) @Table의 uniqueConstraints와 같지만 한 컬럼에 간단한 유니크 제약조건을 걸 때 사용한다.
      columnDefinition(DDL) 데이터베이스 컬럼 정보를 직접 줄 수 있다.
      ex) varchar(100) default 'EMPTY'
      length(DDL) 문자 길이 제약 조건, String 타입에만 사용 (기본 값 : 255)
      precision, scale(DDL) 소수의 자릿수 설정 (기본 값 : precision=19)

      @Enumerated

      • 자바 enum 타입을 매핑 할 때 사용
      • 주의 ORDINAL 사용 X
        • EnumType.ORDINAL : enum 순서를 데이터 베이스에 저장
          • enum의 순서대로 0,1,2,3... 저장 된다.(Integer값 저장)
            • 따라서 엄청난 혼란을 줄 수 있다. 예를 들면 ADMIN이 0번 이었는데, GUEST가 0번으로 바뀌면 이전에 저장 되어있던 0번 값(=ADMIN)과 새로 저장된 값 GUEST(=0번 값)이 헷갈리게 된다.
        • EnumType.STRING : enum 이름을 데이터베이스에 저장 (*항상 이렇게 사용)
          • enum 설정 값 이름 그대로를 저장한다.

      @Temporal

      날짜 타입(java.util.Date, java.util.Calendar) 매핑할 때 사용한다.

      • 참고 : LocalDate, LocalDateTime을 사용할 때는 생략 가능(최신 하이버네이트 지원)
      • TemporalType.DATE : 날짜, 데이터베이스, date 타입과 매핑 (예 : 2013-10-11)
      • TemporalType.TIME : 시간, 데이터베이스, time 타입과 매핑 (예 : 11:11:11)
      • TemporalType.TIMESTAMP : 날짜와 시간, 데이터베이스, timestamp 타입과 매핑(예 : 2013-10-11 11:11:11)

      아래와 같이 적으면 어노테이션이 필요없다!

      private LocalDate localDate;
      private LocalDateTime localDateTime;

      @Lob

      데이터베이스 BLOB, CLOB 타입과 매핑

      @Lob에는 지정할 수 있는 속성이 없고, 대신에 매핑하는 필드 타입이 문자면, CLOB으로 매핑하고 나머지는 BLOB로 매핑한다.

      @Transient

      객체 임시로 어떤 값을 넣고 싶을 때 사용하고 데이터베이스에는 반영이 안된다.

      데이터 베이스 스키마 자동 생성

      사실상 운영에선 사용하진 않고, 개인적으로 개발할때 정도 사용한다.
      • DDL을 애플리케이션 실행 시점에 자동 생성
      • 테이블 중심 -> 객체 중심
      • 데이터베이스 방언을 활용해서 데이터베이스에 맞는 적절한 DDL 생성

      xml 다음과 같이 입력하면 초기 실행 시 자동으로 테이블을 생성한다.

      <property name="hibernate.hbm2ddl.auto" value ="create"/>

      위의 value 속성은 개발 단계마다 다르게 생성할 수있다.

      • 개발 초기 단계에는 create 또는 update
      • 테스트 서버는 update 또는 validate(엔티티와 테이블이 정상 매핑되었는지만 확인)
      • 스테이징과 운영서버는 validate 또는 none
      • 운영 장비에는 절대 create, create-drop, update 사용하면 안된다.

      기본 키 매핑

      기본 키 매핑 어노테이션

      • @Id
      • @GeneratedValue
      @Id @GeneratedValue(strategy = GenerationType.AUTO)
      private Long id;

      기본 키 매핑 방법

      데이터베이스마다 기본 키를 생성하는 방식이 서로 다르므로 이 문제를 해결하기는 쉽지 않다. JPA는 이런 문제들을 어떻게 해결하는지 알아보자
      • 직접 할당 : @Id만 사용
      • 자동 생성(@GeneratedValue)
        • IDENTITY : 데이터베이스에 위임(DB에 따라서 알아서(임의로) 해줌), MYSQL, PostgreSQL, SQL Server, DB2에서 사용
          • MySQL의 AUTO_INCREMENT
        • SEQUENCE : 데이터베이스 시퀀스 오브젝트 사용, ORACLE
          • @SequenceGenerator 필요
        • TABLE : 키 생성용 테이블 사용, 모든 DB에서 사용
          • @TableGenerator 필요

      기본 키 직접 할당 전략

      @Id 적용 가능 자바 타입

      • 자바 기본형
      • 자바 래퍼wrapper형
      • String
      • java.util.Date
      • java.sql.Date
      • java.math.BigDecimal
      • java.math.BigInteger
      SEQUENCE 전략

      sequenceName 속성의 이름으로 BOARD_SEQ 를 지정했는데 JPA는 이 시퀸스 생성기를 실제 데이터베이스 BOARD_SEQ 시퀀스와 매핑한다. sequenceName을 따로 설정하지 않으면 hibernate_sequence와 같이 자동으로 설정된다.

      @Entity
      @SequenceGenerator(
      	name = "BOARD_SEQ_GENERATOR".
      	sequenceName = ”BOARD_SEQ”, //매핑할 데이터베이스 시퀀스 이름
      	initialvalue = 1, 
      	allocationsize = 1)
      public class Board {
      
      	@IdQGeneratedValue(
      	strategy = GenerationType.SEQUENCE,
      	generator = "BOARD_SEQ_GENERATOR")
      	private Long id;
      	...
      }

      call next value for MEMBER_SEQ란?
      영속성 컨텍스트에 의해 JPA는 항상 PK값을 알아야한다. 그래서 MEMBER_SEQ값에서 id값을 조회한다. 그러고 나서 em.persist를 해준다.

      commit 하는 시점에 insert쿼리가 날라간다.
      SquenceGenerator.allocationSize의 기본값이 50인 이유는 최적화 때문이다 allocationSize 값이 50이면 시퀀스를 한 번에 50 증가 시킨 다음에 1~50까지는 메모리에서 식별자를 할당한다. 이 최적화 방법은 시퀀스 값을 선점 하므로 여러 JVM이 동시에 동작 해도 기본 키 값이 충돌하지 않는 장점이 있다. 반면에 데이터베이스에 직접 접근해서 데이터를 등록할 때 시퀀스 값이 한번에 많이 증가한다는 점을 염두해 두어야 한다. 참고로 앞서 설명한 hibernate.id.new_generator_mappings 속성을 true로 설정해야 지금까지 설명한 최적화 방법이 적용된다.

      TABLE 전략(운영에서 잘 쓰이지 않음)

      TABLE 전략은 키 생성 전용 테이블을 하나 만들고 여기에 이름과 값으로 사용할 컬럼을 만들어 데이터베이스 시퀀스를 흉내내는 전략이다.
      • 장점 : 모든 데이터베이스에 적용 가능
      • 단점 : 성능
      @Entity
      @TableGenerator(
      	name = "BOARD_SEQ_GENERATOR",
      	table = ”MY_SEQUENCES",
      	pkColumnValue = ”BOARD_SEQ”, 
      	allocationsize = 1)
      public class Board {
      	@Id
      	@GeneratedValue(
      		strategy = GenerationType.TABLE,
      		generator = '' BOARD_SEQ_GENERATOR''
      	)
      	private Long id;
      	...
      }
      TABLE 전략과 최적화 TABLE 전략은 값을 조회하면서 SELECT 쿼리를 사용하고 다음 값으로 증가시키기 위해 UPDATE 쿼리를 사용한다. 이 전략은 SEQUENCE 전략과 비교해서 데이터베이스와 한번 더 통신하는 단점이 있다. TABLE 전략을 최적화하려면 TableGenerator.allocationSize를 사용하면 된다.

      권장하는 식별자 전략

      • 기본 키 제약 조건 : null 아님, 변하면 안된다.
      • 미래까지 이 조건에 만족하는 자연키는 찾기 어렵다. 대리키(대체키)를 사용하자.
      • 예를 들어 주민등록번호도 기본 키로 적절하지 않다.
      • 권장 : Long형 + 대체키 + 키 생성전략 사용

      AUTO 전략

      GenerationType.AUTO는 선택한 데이터베이스 방언에 따라 IDENTITY, SEQUENCE, TABLE 전략 중 하나를 자동으로 선택한다. AUTO 전략의 장점은 데이터베이스를 변경해도 코드를 수정할 필요가 없다는 것이다. AUTO를 사용할 때 SEQUENCETABLE 전략이 선택되면 시퀀스나 키 생성용 테이블을 미리 만들어 두어야 한다.

      IDENTITY 전략

      기본 키 생성을 데이터베이스에 위임

      IDENTITY 전략은 지금 설명한 AUTO INCREMENT를 사용한 예제처럼 데이터베이스에 값을 저장하고 나서야 기본 키 값을 구할 수 있을 때 사용한다.

      private static void logic (EntityManager em) { 
      	Board board = new Board(); em.persist (board); 
      	System.out.println("board.id = " + board.getId()); 
      }
      //출력: board.id = 1
      문제점 : IDENTITY 전략은 데이터를 데이터베이스에 INSERT한 후에 기본 키 값을 조회할 수 있다.
      티티가 영속 상태가 되려면 식별자가 반드시 필요하다. 그런데 IDENTITY 식별자 생성 전략은 엔티티를 데이터베이스에 저장해야 식별자를 구할 수 있으므로 em.persist()를 호출하는 즉시 INSERT SQL 이 데이터베이스에 전달된다. 따라서 이 전략은 트랜잭션을 지원하는 쓰기 지연이 동작하지 않는다. 왜냐하면 JPA는 보통 트랜잭션 커밋 시점에 INSERT SQL 실행합니다. 그리고 AUTO_INCREMENT는 데이터베이스에 INSERT SQL을 실행한 이후에 ID 값을 알 수 있습니다.

      'Back-end > JPA' 카테고리의 다른 글

      객체지향 쿼리 언어1 - 기본 문법  (0) 2022.09.24
      연관관계 매핑 기초  (0) 2022.08.22
      [JPA] JPA 영속성 컨텍스트  (0) 2022.05.05
      [JPA] Batch Insert  (0) 2022.05.05
      JPA 소개  (0) 2022.03.14

      목차

        JPA에서 가장 중요한 2가지

        • 객체와 관계형 데이터 베이스 매핑하기 (Object Relational Mapping)
        • 영속성 컨텍스트

        영속성 컨텍스트

        • JPA를 이해하는데 가장 중요한 용어
        • "엔티티를 영구 저장하는 환경" 이라는 뜻
        • EntityManager.persist(entity);

        EntityManager.persist(entity)

        우리는 .persist를 해당 entity를 DB에 저장한다고 이해했다. 그렇지않고 우리는 entity를 DB에 저장하는 것이 아닌 영속성 컨텍스트에 저장하는 것이다.

        • 영속성 컨텍스트는 논리적인 개념이다.
        • 눈에 보이지 않는다.
        • 엔티티 매니저를 통해서 영속성 컨텍스트에 접근

        영속성 컨텍스트 생성 과정

        EntityManasger를 생성하면 위 그림 처럼 영속성 컨텍스트가 생성됩니다.

        엔티티의 생성주기

        • 비영속(new/transient)
          • 영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
        • 영속(managed)
          • 영속성 컨텍스트에 관리되는 상태
        • 준영속(detached)
          • 영속성 컨텍스트에 저장되었다가 분리된 상태
        • 삭제(removed)
          • 삭제된 상태

        비영속 상태(JPA랑 전혀 관계 없는 상태)

        영속

        DB에는 언제 저장하는가?

        em.persist(member);

        위의 상태에서는 DB에 저장한 것이 아닌 영속성 컨텍스트에 저장한 것이다. 그렇다면 언제 DB에 저장하는 것 인가?

        확인 코드 

        System.out.print("=== before ===");
        em.persist(member);
        System.out.print("=== after ===");

        결과 : before와 after사이에 insert문이 날라가지 않는다.

        이유

        tx.commit;

        트랜잭션 커밋 상태에서 결국 query가 날라가게 된다.

        준영속, 삭제

        회원 엔티티를 영속성 컨텍스트에서 분리, 준영속 상태

        em.detach(member);

        객체를 삭제한 상태

        em.remove(member);

        영속성 컨텍스트의 이점

        바로 db에 저장하지 않고 중간과정을 거친다. 그래서 얻는 것은 다음과 같다.

        • 1차 캐시
        • 동일성 보장
        • 트랜잭션을 지원하는 쓰기 지연
        • 변경 감지
        • 지연 로딩

        엔티티 조회, 1차 캐시

        1차 캐시로 저장하고 db에 저장한다.

        어느 시점에? 트랜잭션

         데이터베이스에서 조회

        단, 한 트랜잭션안에서만 동작하기 때문에 큰 이점은 없다. 즉, 트랜잭션이 끝나면 휘발성으로 날라간다. 우리가 아는 캐싱을 적용하려면 2차캐싱을 적용해야한다. 

        영속 엔티티의 동일성 보장

        1차 캐시로 반복 가능한 읽기(Repeatable read) 등급의 트랜잭션 격리 수준을 데이터베이스가 아닌 애플리케이션 차원에서 제공

        엔티티 등록 트랜잭션을 지원하는 쓰기 지연

        1차 캐시에 저장과 동시에 INSERT SQL 생성하여 쓰기 지연 SQL 저장소에 저장한다.

        그리고 transactio.commit();을 만나면 flush가 되면서 db에 날라가고 commit이 된다.

        • jpa batch 라는 것으로 size를 관리한다.
        • 약간 bulk insert 같은 느낌으로 버퍼를 모으는 느낌

        ★엔티티 수정 변경 감지(Drity Checking)★

        ​EntityManager em = emf.createENtityManager();
        EntityTransaction tr = em.getTransaction();
        tr.begin(); //트랜잭션 시작
        
        //영속 엔티티 조회
        Member memberA = em.find(Member.class, "memberA");
        //영속 엔티티 데이터 수정
        MemberA.setUserName("hi");
        MemberA.setAge(10);
        
        //em.update(member) 이런 코드가 있어야 하지 않을까?
        tr.commit(); //트랜잭션 커밋

        위의 코드를 보면 memberA라는 객체에 setName, setAge를 해주고 update 쿼리를 해주지 않았는데도 자동으로 update가 된다. 왜그럴까? 뭔가 set을 해주고 persist를 해줘야 반영이 될 것 같은데 말이다.

        1차 캐시안에는 @Id, Entity, 스냅샷이라는 것이 있다. 스냅샷은 최초로 영속성 컨텍스트에 들어온 값을 임시 저장하는 것이다. 그러고 Entity(새로 들어온 값) 스냅샷과 변경이 감지되면 update 쿼리를 쓰기 지연 SQL 저장소에 저장해버리고 DB에 반영한다.

        플러시

        영속성 컨텍스트의 변경내용을 데이터베이스에 반영

        플러시 발생

        • 변경 감지
        • 수정된 엔티티 쓰기 지연 SQL 저장소에 등록
        • 쓰기 지연 SQL 저장소의 쿼리를 데이터베이스에 전송
          • 등록, 수정, 삭제 쿼리

        영속성 컨텍스트를 플러시 하는 방법

        • em.flush() - 직접 호출 트랜잭션 커밋 전에 강제 느낌 (거의 사용x)
        • 트랜잭션 커밋 - 플러시 자동 호출
        • JPQL 쿼리 실행 - 플러시 자동 호출
        em.persist(memberA); 
        em.persist(memberB); 
        em.persist(memberC);
        // 중간에 JPQL 실행 
        query = em.createQuery("select m from Member m", member.class);
        List<Member> members = query.getResultList();
        • em.persist의 memberA,B,C는 아직 데이터베이스에 Insert 전이다.
        • 따라서 select 문에서 조회가 안될 것 같지만, JPQL은 flush가 자동으로 된다. 그래서 조회가 된다.
          • flush 여부 설정 가능
            • FlushModeType.AUTO... or COMMIT

        준영속 상태

        • 영속 -> 준영속
        • 영속 상태의 엔티티가 영속성 컨텍스트에서 분리(detached)
        • 영속성 컨텍스트가 제공하는 기능을 사용 못함
        • 거의 비영속 상태에 가깝다.
          영속성 컨텍스트가 관리하지 않으므로 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩을 포함한 영속성 컨텍스트가 제공하는 어떤한 기능도 동작하지 않는다.
        • 식별자 값을 가지고 있다.
          비영속 상태는 식별자 값이 없을 수도 있지만 준영속 상태는 이미 한 번 영속상태였으므로 반드시 식별자 값을 가지고 있다
        • 지연 로딩을 할 수 없다.
        //1 : 떼어내기
        em.detach(member); 
        tx.commit();//commit을 해도 em은 반영되지 않는다.
        
        //2 1차 캐시를 조회함 그래서 같은 내용 또 조회해도 select 쿼리가 또 나감
        em.clear();
        
        //3. em.close() : 종료

         

        'Back-end > JPA' 카테고리의 다른 글

        연관관계 매핑 기초  (0) 2022.08.22
        엔티티 매핑  (0) 2022.08.06
        [JPA] Batch Insert  (0) 2022.05.05
        JPA 소개  (0) 2022.03.14
        자바 ORM 표준 JPA 프로그래밍  (0) 2022.03.14

        성능 향상을 위해서 Batch Insert를 도입하는 과정 중 JPA, Mysql 환경에서의 Batch Insert에 대한 방법과 제약사항들에 대해서 정리했습니다. 결과적으로는 다른 프레임워크를 도입해서 해결했으며 본 포스팅은 JPA Batch Insert의 정리와, 왜 다른 프레임워크를 도입을 했는지에 대해한 내용입니다.

        Batch Insert 란 ?

         
        # 단건 insert
        insert into payment_back (amount, order_id) values (?, ?)
        
        # 멀티 insert
        insert into payment_back (amount, order_id)
        values 
               (1, 2),
               (1, 2),
               (1, 2),
               (1, 2)

        insert rows 여러 개 연결해서 한 번에 입력하는 것을 Batch Insert라고 말합니다. 당연한 이야기이지만 Batch Insert는 하나의 트랜잭션으로 묶이게 됩니다.

        Batch Insert With JPA

        위 Batch Insert SQL이 간단해 보이지만 실제 로직으로 작성하려면 코드가 복잡해지고 실수하기 좋은 포인트들이 있어 유지 보수하기 어려운 코드가 되기 쉽습니다. 해당 포인트들은 아래 주석으로 작성했습니다. JPA를 사용하면 이러한 문제들을 정말 쉽게 해결이 가능합니다.

        // 문자열로 기반으로 SQL을 관리하기 때문에 변경 및 유지 보수에 좋지 않음
        val sql = "insert into payment_back (id, amount, order_id) values (?, ?, ?)"
        val statement = connection.prepareStatement(sql)!!
        
        fun addBatch(payment: Payment) = statement.apply {
            // code 바인딩 순서에 따라 오동작 가능성이 높음
            // 매번 자료형을 지정해서 값을 입력해야 함
            this.setLong(1, payment.id!!)
            this.setBigDecimal(2, payment.amount)
            this.setLong(3, payment.orderId)
            this.addBatch()
        }
        
        // connection & statement 객체를 직접 close 진행, 하지 않을 경우 문제 발생 가능성이 있음
        fun close() {
            if (statement.isClosed.not())
                statement.close()
        }

        쓰기 지연 SQL 지원 이란 ?

        EntityMaanger em  = emf.createEnttiyManager();
        ENtityTranscation transaction = em.getTransaction();
        // 엔티티 매니저는 데이터 변경 시 트랜잭션을 시작해야 한다.
        
        transaction.begin();
        
        em.persist(memberA);
        em.persist(memberB);
        
        // 여기까지 Insert SQL을 데이터베이스에 보내지 않는다.
        // Commit을 하는 순간 데이터베이스에 Insert SQL을 보낸다
        transaction.commit();
         

        엔티티 매니저는 트랜잭션을 커밋 하기 직전까지 데이터베이스에 엔티티를 저장하지 않고 내부 쿼리 저장소에 INSERT SQL을 모아둔다. 그리고 트랜잭션을 커밋 할 때 모아둔 쿼리를 데이터베이스에 보내는데 이것을 트랜잭션을 지원하는 쓰기 지연이라 한다.

        회원 A를 영속화했다. 영속성 컨텍스트는 1차 캐시에 회원 엔티티를 저장하면서 동시에 회원 엔티티 정보로 등록 쿼리를 만든다. 그리고 만들어진 등록 쿼리를 쓰기 지연 SQL 저장소에 보관한다.


        다음으로 회원 B를 영속화했다. 마찬가지로 회원 엔티티 정보로 등록 쿼리를 생성해서 쓰지 지연 SQL 저장소에 보관한다. 현재 쓰기 지연 SQL 저장소에는 등록 쿼리가 2건이 저장되어 있다.

        마지막으로 트랜잭션을 커밋 했다. 트랜잭션을 커밋 하면 엔티티 매니저는 우선 영속성 컨텍스트를 플러시 한다. 플러시는 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업인데 이때 등록, 수정, 삭제한 엔티티를 데이터베이스에 반영한다. 이러한 부분은 JPA 내부적으로 이루어지기 때문에 사용하는 코드에서는 코드의 변경 없이 이러한 작업들이 가능하다.

        JPA With Batch Insert Code

        spring:
            jpa:
                database: mysql
                properties:
                    hibernate.jdbc.batch_size: 50
                    hibernate.order_inserts: true
                    hibernate.order_updates: true
                    hibernate.dialect: org.hibernate.dialect.MySQL5InnoDBDialect
                    hibernate.show_sql: true
        
            datasource:
                url: jdbc:mysql://localhost:3366/batch_study?useSSL=false&serverTimezone=UTC&autoReconnect=true&rewriteBatchedStatements=true
                driver-class-name: com.mysql.cj.jdbc.Driver
         

        addBatch 구분을 사용하기 위해서는 rewriteBatchedStatements=true 속성을 지정해야 합니다. 기본 설정은 false이며, 해당 설정이 없으면 Batch Insert는 동작하지 않습니다. 정확한 내용은 공식 문서를 참고해 주세요.

        MySQL Connector/J 8.0 Developer Guide : 6.3.13 Performance Extensions
        Stops checking if every INSERT statement contains the “ON DUPLICATE KEY UPDATE” clause. As a side effect, obtaining the statement’s generated keys information will return a list where normally it wouldn’t. Also be aware that, in this case, the list of generated keys returned may not be accurate. The effect of this property is canceled if set simultaneously with ‘rewriteBatchedStatements=true’.

        hibernate.jdbc.batch_size: 50 Batch Insert의 size를 지정합니다. 해당 크기에 따라서 한 번에 insert 되는 rows가 결정됩니다. 자세한 내용은 아래에서 설명드리겠습니다.

        @Entity
        @Table(name = "payment_back")
        class PaymentBackJpa(
            @Column(name = "amount", nullable = false)
            var amount: BigDecimal,
        
            @Column(name = "order_id", nullable = false, updatable = false)
            val orderId: Long
        ){
            
            @Id
            @Column(name = "id", updatable = false) // @GeneratedValue를 지정하지 않았음
            var id: Long? = null
        }
        
        interface PaymentBackJpaRepository: JpaRepository<PaymentBackJpa, Long>
         

        엔티티 클래스는 간단합니다. 중요한 부분은 @GeneratedValue을 지정하지 않은 부분입니다.

        @SpringBootTest
        @TestConstructor(autowireMode = TestConstructor.AutowireMode.ALL)
        internal class BulkInsertJobConfigurationTest(
            private val paymentBackJpaRepository: PaymentBackJpaRepository
        ) {
        
            @Test
            internal fun `jpa 기반 bulk insert`() {
                (1..100).map {
                    PaymentBackJpa(
                        amount = it.toBigDecimal(),
                        orderId = it.toLong()
                    )
                        .apply {
                            this.id = it.toLong() // ID를 직접 지정
                        }
                }.also {
                    paymentBackJpaRepository.saveAll(it)
                }
            }
        }
         

        paymentBackJpaRepository.saveAll()를 이용해서 batch inset를 진행합니다. JPA 기반으로 Batch Insert를 진행할 때 별다른 코드가 필요 없습니다. 컬렉션 객체를 saveAll()으로 저장하는 것이 전부입니다. hibernate.show_sql: true으로 로킹 결고를 확인해보겠습니다.

        로그상으로는 Batch Insert가 진행되지 않은 것처럼 보입니다. 결론부터 말씀드리면 실제로는 Batch Insert가 진행됐지만 hibernate.show_sql: true 기반 로그에는 제대로 표시가 되지 않습니다. Mysql의 실제 로그로 확인해보겠습니다.

        show variables like 'general_log%'; # general_log 획인
        set global general_log = 'ON'; # `OFF` 경우 `ON` 으로 변경
         

        해당 로그 설정은 성능에 지장을 줄 수 있기 때문에 테스트, 개발 환경에서만 지정하는 것을 권장합니다. 해당 기능은 실시간으로 변경 가능하기 때문에 설정 완료 이후 /var/lib/mysql/0a651fe44d20.log 파일에 로그를 확인할 수 있습니다.

        batch size

        Query	SELECT @@session.transaction_read_only
        Query	insert into payment_back (amount, order_id, id) values (1, 1, 1),(2, 2, 2),(3, 3, 3),(4, 4, 4),(5, 5, 5),(6, 6, 6),(7, 7, 7),(8, 8, 8),(9, 9, 9),(10, 10, 10),(11, 11, 11),(12, 12, 12),(13, 13, 13),(14, 14, 14),(15, 15, 15),(16, 16, 16),(17, 17, 17),(18, 18, 18),(19, 19, 19),(20, 20, 20),(21, 21, 21),(22, 22, 22),(23, 23, 23),(24, 24, 24),(25, 25, 25),(26, 26, 26),(27, 27, 27),(28, 28, 28),(29, 29, 29),(30, 30, 30),(31, 31, 31),(32, 32, 32),(33, 33, 33),(34, 34, 34),(35, 35, 35),(36, 36, 36),(37, 37, 37),(38, 38, 38),(39, 39, 39),(40, 40, 40),(41, 41, 41),(42, 42, 42),(43, 43, 43),(44, 44, 44),(45, 45, 45),(46, 46, 46),(47, 47, 47),(48, 48, 48),(49, 49, 49),(50, 50, 50)
        Query	SELECT @@session.transaction_read_only
        Query	insert into payment_back (amount, order_id, id) values (51, 51, 51),(52, 52, 52),(53, 53, 53),(54, 54, 54),(55, 55, 55),(56, 56, 56),(57, 57, 57),(58, 58, 58),(59, 59, 59),(60, 60, 60),(61, 61, 61),(62, 62, 62),(63, 63, 63),(64, 64, 64),(65, 65, 65),(66, 66, 66),(67, 67, 67),(68, 68, 68),(69, 69, 69),(70, 70, 70),(71, 71, 71),(72, 72, 72),(73, 73, 73),(74, 74, 74),(75, 75, 75),(76, 76, 76),(77, 77, 77),(78, 78, 78),(79, 79, 79),(80, 80, 80),(81, 81, 81),(82, 82, 82),(83, 83, 83),(84, 84, 84),(85, 85, 85),(86, 86, 86),(87, 87, 87),(88, 88, 88),(89, 89, 89),(90, 90, 90),(91, 91, 91),(92, 92, 92),(93, 93, 93),(94, 94, 94),(95, 95, 95),(96, 96, 96),(97, 97, 97),(98, 98, 98),(99, 99, 99),(100, 100, 100)
        Query	commit
        Query	SET autocommit=1
         

        실제 mysql 로그에서는 Batch Insert를 확인할 수 있습니다. 그런데 왜 2번에 걸쳐서 Batch Insert가 진행되었을까요? hibernate.jdbc.batch_size: 50설정으로 Batch Insert에 대한 size를 50으로 지정했기 때문에 rows 100를 저장할 때 2번에 걸쳐 insert를 진행하는 것입니다. 만약 hibernate.jdbc.batch_size: 100이라면 1번의 insert로 저장됩니다.

         
         

        위 쿼리는 hibernate.jdbc.batch_size: 100으로 지정한 결과입니다. 그렇다면 왜 batch_size 옵션을 주어서 한 번에 insert 할 수 있는 데이터의 크기를 제한하는 것일까요? 아래 코드에서 해답을 찾을 수 있습니다.

        Hibernate User Guide: 12.2.1. Batch inserts

        When you make new objects persistent, employ methods flush() and clear() to the session regularly, to control the size of the first-level cache.

         

        하이버네이트 공식 가이드의 내용입니다. batchSize 값을 기준으로 flush();, clear();를 이용해서 영속성 컨텍스트를 초기화 작업을 진행하고 있습니다. batchSize에 대한 제한이 없으면 영속성 컨텍스트에 모든 엔티티가 올라가기 때문에 OutOfMemoryException 발생할 수 있고, 메모리 관리 측면에서도 효율적이지 않기 때문입니다. 하이버네이트의 공식 가이드에서도 해당 부분의 언급이 있습니다.

        Hibernate User Guide: 12.2. Session batching

        1. Hibernate caches all the newly inserted Customer instances in the session-level cache, so, when the transaction ends, 100 000 entities are managed by the persistence context. If the maximum memory allocated to the JVM is rather low, this example could fail with an OutOfMemoryException. The Java 1.8 JVM allocated either 1/4 of available RAM or 1Gb, which can easily accommodate 100 000 objects on the heap.
        2. long-running transactions can deplete a connection pool so other transactions don’t get a chance to proceed
        3. JDBC batching is not enabled by default, so every insert statement requires a database roundtrip. To enable JDBC batching, set the hibernate.jdbc.batch_size property to an integer between 10 and 50.

        쓰기 지연 SQL 제약 사항

        batchSize: 50 경우 PaymentBackJpa 객체를 50 단위로 Batch Insert 쿼리가 실행되지만, 중간에 다른 엔티티를 저장하는 경우 아래처럼 지금까지의 PaymentBackJpa에 대한 지정하기 때문에 최종적으로 batchSize: 50 단위로 저장되지 않습니다.

        em.persist(new PaymentBackJpa()); // 1
        em.persist(new PaymentBackJpa()); // 2
        em.persist(new PaymentBackJpa()); // 3
        em.persist(new PaymentBackJpa()); // 4
        em.persist(new Orders()); // 1-1, 다른 SQL이 추가 되었기 때문에  SQL 배치를 다시 시작 해야 한다.
        em.persist(new PaymentBackJpa()); // 1
        em.persist(new PaymentBackJpa()); // 2
         

        이러한 문제는 hibernate.order_updates: true, hibernate.order_inserts: true 값으로 해결 할 수 있습니다.

        JPA Batch Insert의 가장 큰 문제…

        위에서 설명했던 부분들은 Batch Insert에 필요한 properties 설정, 그리고 내부적으로 JPA에서 Batch Insert에 대한 동작 방식을 설명한 것입니다. 실제 Batch Insert를 진행하는 코드는 별다른 부분이 없고 컬렉션 객체를 saveAll() 메서드로 호출하는 것이 전부입니다. 이로써 JPA는 Batch Insert를 강력하게 지원해 주고 있습니다. 하지만 가장 큰 문제가 있습니다. @GeneratedValue(strategy = GenerationType.IDENTITY) 방식의 경우 Batch Insert를 지원하지 않습니다.

        Hibernate User Guide: 12.2. Session batching

        Hibernate disables insert batching at the JDBC level transparently if you use an identity identifier generator.

        공식 문서에도 언급이 있듯이 @GeneratedValue(strategy = GenerationType.IDENTITY) 경우 Batch Insert를 지원하지 않습니다. 정확히 어떤 이유 때문인지에 대해서는 언급이 없고, 관련 내용을 잘 설명한 StackOverflow를 첨부합니다.

        제가 이해한 바로는 하이버네이트는 Transactional Write Behind 방식(마지막까지 영속성 컨텍스트에서 데이터를 가지고 있어 플러시를 연기하는 방식)을 사용하기 때문에 GenerationType.IDENTITY 방식의 경우 JDBC Batch Insert를 비활성화함. GenerationType.IDENTITY 방식이란 auto_increment으로 PK 값을 자동으로 증분 해서 생성하는 것으로 매우 효율적으로 관리할 수 있다.(heavyweight transactional course-grain locks 보다 효율적). 하지만 Insert를 실행하기 전까지는 ID에 할당된 값을 알 수 없기 때문에 Transactional Write Behind을 할 수 없고 결과적으로 Batch Insert를 진행할 수 없다.

        Mysql에서는 대부분 GenerationType.IDENTITY으로 사용하기 때문에 해당 문제는 치명적입니다. 우선 GenerationType.IDENTITY 으로 지정하고 다시 테스트 코드를 돌려 보겠습니다.

         
        @Entity
        @Table(name = "payment_back")
        class PaymentBackJpa(
            @Column(name = "amount", nullable = false)
            var amount: BigDecimal,
        
            @Column(name = "order_id", nullable = false, updatable = false)
            val orderId: Long
        ){
            @Id
            @GeneratedValue(strategy = GenerationType.IDENTITY) // GenerationType.IDENTITY 지정
            var id: Long? = null
        }
        
        internal class BulkInsertJobConfigurationTest(
            private val paymentBackJpaRepository: PaymentBackJpaRepository
        ) {
        
            @Test
            internal fun `jpa 기반 bulk insert`() {
                (1..100).map {
                    PaymentBackJpa(
                        amount = it.toBigDecimal(),
                        orderId = it.toLong()
                    )
                        .apply {
        //                    this.id = it.toLong() // ID를 자동 증가로 변경 했기 때문에 코드 주석
                        }
                }.also {
                    paymentBackJpaRepository.saveAll(it)
                }
            }
        }
        Query	insert into payment_back (amount, order_id) values (1, 1)
        Query	insert into payment_back (amount, order_id) values (2, 2)
        Query	insert into payment_back (amount, order_id) values (3, 3)
        Query	insert into payment_back (amount, order_id) values (4, 4)
        Query	insert into payment_back (amount, order_id) values (5, 5)
        Query	insert into payment_back (amount, order_id) values (6, 6)
        Query	insert into payment_back (amount, order_id) values (7, 7)
        Query	insert into payment_back (amount, order_id) values (8, 8)
        Query	insert into payment_back (amount, order_id) values (9, 9)
        Query	insert into payment_back (amount, order_id) values (10, 10)
        Query	insert into payment_back (amount, order_id) values (11, 11)
        Query	insert into payment_back (amount, order_id) values (12, 12)
        ...
         

        GenerationType.IDENTITY의 경우에는 Batch Insert가 진행되지 않습니다. 그래서 다른 대안을 찾아야 했습니다. 이 부분부터는 다음 포스팅에서 이어가겠습니다.

        'Back-end > JPA' 카테고리의 다른 글

        엔티티 매핑  (0) 2022.08.06
        [JPA] JPA 영속성 컨텍스트  (0) 2022.05.05
        JPA 소개  (0) 2022.03.14
        자바 ORM 표준 JPA 프로그래밍  (0) 2022.03.14
        모든 연관관계는 지연로딩으로 설정하자. (N+1문제)  (0) 2022.03.13

        목차로 돌아가기

        JPA 장점
        - 우선 CRUD SQL을 작성할 필요가 없다.
        - JPA가 제공하는 네이티브 SQL 기능을 사용해서 직접 SQL을 작성할 수도 있고, 데이터베이스 쿼리 힌트도 사용할 수 있는 방법이 있다.(성능걱정x)
        - 개발 단계에서 MySQL DB를 사용하다가 오픈 시점에 갑자기 Oracle로 바꿔도 코드를 거의 수정할 필요가 없다.

        목차

            강의 : JPA 소개 - SQL 중심적인 개발의 문제점

            SQL 직접 다룰 때 발생하는 문제점

            반복 또 반복
            // 1. 회원 등록용 SQL 작성
            String sql = "INSERT INTO MEMBER(MEMBER_ID,NAME) VALUES(?,?)";
            //2. 회원 객체의 값을 꺼내서 등록 SQL에 전달한다.
            pstmt.setString(1, member.getMemberId();
            pstmt.setString(2, member.getName());
            //3.JDBC API를 사용해서 SQL을 실행한다.
            pstmt.executeUpdate(sql);
            • 객체를 데이터베이스에 CRUD 하려면 너무 많은 SQL과 JDBC API를 코드로 작성해야 한다는 점이다.  
            • 그리고 테이블마다 이런 비슷한 일을 반복해야 하는데, 개발하려는 애플리케이션에서 사용하는 데이터베이스 테이블이 100개라면 무수히 많은 SQL을 작성해야 하고 이런 비슷한 일은 100번은 더 반복해야 한다.
            • 데이터 접근 계층 (DAO)을 개발하는 일은 이렇듯 지루함과 반복의 연속이다.

            SQL에 의존적인 개발

            갑자기 회원의 연락처도 함께 저장해달라는 요구사항이 추가 되었다. = 새롭게 쿼리를 다 짜야한다...

            등록코드 변경

            public class Member{
                private String memberId;
                private String name;
                private String tel; //추가
                ...
             }

            연락처를 저장할 수 있도록 INSERT SQL을 수정했다.

            String sql = "INSERT INTO MEMBER (MEMBER_ID, NAME, TEL) VALUE(?,?,?)";

            그 다음 회원 객체의 연락처 값을 꺼내서 등록 SQL에 전달했다.

            pstmt.setString(3, member.getTel());

            조회 코드 변경

            다음 처럼 회원 조회용 SQL을 수정한다.

            SELECT MEMBER_ID, NAME, TEL FROM MEMBER WHERE MEMBER_ID = ?
            //조회 결과를 Member 객체에 추가로 매핑한다.
            String tel = rs.getString("TEL");
            member.setTel(tel); //추가

            JPA와 문제해결

            JPA를 사용하면 객체를 데이터베이스에 저장하고 관리할 때, 개발자가 직접 SQL을 작성하는 것이 아니라 JPA가 제공하는 API를 사용하면 된다. 그러면 JPA가 개발자 대신에 적절한 SQL을 생성해서 데이터베이스에 전달한다.

            저장 기능

            jpa.persist(member); //저장

            조회 기능

            String memberId = "helloId";
            Member member = jpa.find(Member.class, memberId); //조회

            수정 기능

            Member member = jpa.find(member.class, memberId);
            member.setName("이름변경") // 수정
            • JPA는 별도의 수정 메서드를 제공하지 안흔다. 대신에 객체를 조회해서 값을 변경만 하면 트랜잭션을 커밋할 때 데이터베이스에 적절한 UPDATE SQL이 전달된다. (마법 같은 일-> 추후에 공부)

            연관된 객체 조회

            Member member = jpa.find(Member.class. memberId);
            Team team = member.getTeam(); //연관된 객체 조회

            JPA는 연관된 객체를 사용하는 시점에 적절한 SELECT SQL을 실행한다. 따라서 JPA를 사용하면 연관된 객체를 마음껏 조회할 수 있다.(마법 같은 일2->추후에 공부)

            패러다임의 불일치

            비즈니스 요구사항을 정의한 도메인 모델도 객체로 모델링하면 객체지향 언어가 가진 장점들을 활용할 수 있다. 문제는 이렇게 정의한 도메인 모델을 저장할 때 발생한다.
            • 객체의 기능은 클래스에 정의되어 있으므로 객체 인스턴스의 상태인 속성만 저장 했다가 필요할 때 불러 와서 복구하면 된다.
            • 관계형 데이터베이스는 데이터 중심으로 구조화되어 있고, 집합적인 사고를 요구한다. 그리고 객체 지향에서 이야기하는 추상화, 상속, 다형성 같은 개념이 없다.
            객체와 관계형 데이터베이스는 지향하는 목적이 서로 다르므로 둘의 기능표현 방법도 다르다. 이것을 객체와 관계형 데이터베이스의 패러다임 불일치 문제라 한다. 따라서 객체 구조테이블 구조에 저장하는 데는 한계가 있다.
            문제는 이런 객체와 관계형 데이터베이스 사이의 패러다임 불일치 문제를 해결하는 데 너무 많은 시간코드소비하는 데 있다.

            상속

            객체는 상속 이라는 기능을 가지고 있지만 테이블은 상속 이라는 기능이 없다. 따라서 패러다임의 불일치가 시작한다.
            객체 상속 모델
             
            위의 관계형데이터에서 만약 Album을 조회한다.
            1. 각각의 테이블에 따른 조인 SQL 작성
            2. 각각의 객체 생성
            3. 상상만 해도 복잡...
            4. 더 이상 설명은 생략한다.
            5. 그래서 DB에 저장할 객체에는 상속 관계 안쓴다!!!

            만약 자바 컬렉션에 저장하면? 
            list.add(album);

            자바 컬렉션을 조회하면?

            Album album = list.get(albumId);
            
            //부모 타입으로 조회 후 다형성 활용
            Item item = list.get(albumId);

            JDBC를 이용한 상속 구현

            JDBC API를 사용해서 상속 관계의 객체를 저장 하려면 부모 객체에서 부모 데이터만 꺼내서 INSERT SQL을 작성하고 자식 객체에서 자식 데이터만 꺼내서 INSERT SQL을 별도로 작성해야 한다.
            INSERT INFO ITEM ...
            INSERT INTO ALBUM ...​
             

            JPA를 이용한 상속 구현

            JPA 상속과 관련된 패러다임의 불일치 문제를 개발자 대신 해결해준다. 개발자는 마치 자바 컬렉션에 객체를 저장 하듯이 JPA에게 객체를 저장하면 된다.

            jpa.persist(album);

            연관관계

            • 객체는 참조를 사용 : member.getTeam()
            • 테이블은 외래 키 사용 : JOIN ON M.TEAM_ID = T.TEAM_ID

            • 테이블에 맞춘 객체 모델
            class Member {
              String id; // MEMBER_ID 컬럼 사용
              Long temId; // TEAM_ID FK 컬럼 사용
              String username; // USERNAME 컬럼 사용
            }
            
            class Team {
              Long id; // TEAM_ID PK 사용
              String name; // NAME 컬럼 사용
            }

             

            그런데 여기서 TEAM_ID 외래 키의 값을 그대로 보관하는 teamId 필드에는 문제가 있다. 객체는 연관된 객체의 참조를 보관해야 다음처럼 참조를 통해 연관된 객체를 찾을 수 있어야 하는데 하지 못하게 된다.

            이런 방식을 따르면 좋은 객체 모델링은 기대하기 어렵고 결국 객체 지향의 특징을 잃어버리게 된다.
            엔티티를 직접 참조하는 것과 간접 참조하는 것에 대한 장단점이 무엇이 있을까?
            직접 참조
            장점 : 연관된 데이터를 한번에 추출할 수 있다.단점 : 연관된 데이터에 대한 수정이 발생할 경우 영향의 범위가 커질 수 있다.
            간접 참조
            장점 : 복잡도를 낮출 수 있고, 응집도를 높이고 결합도를 낮출 수 있다.단점 : 연관된 데이터를 한번에 추출 하려면 구현해야 하는 로직이 복잡하다.
            • 참조를 사용하는 객체 모델
            class Member {
              String id; // MEMBER_ID 컬럼 사용
              Long temId; // TEAM_ID FK 컬럼 사용
              Team team; // 참조로 연관관계를 맺는다.
            }
            
            class Team {
              Long id; // TEAM_ID PK 사용
              String name; // NAME 컬럼 사용
            }

            JDBC를 이용한 연관 관계 구현

            만약 JDBC로 저장하는 로직을 만들기 위해서는 team 필드를 TEAM_ID 외래 키 값으로 변환해야 한다.

            member.getId(); // MEMBER_ID PK에 저장
            member.getTeam().getId(); // TEAM_ID FK에 저장
            member.getUsername(); // USERNAME 컬럼에 저장

            또는 조회하는 로직에도 객체를 생성하고 연관관계를 설정해서 반환하는 로직이 필요하다.

            public Member find(String memberId) {
              // SQL 실행
              ...
              Member member = new Member();
              ...
            
              // 데이터베이스에서 조회한 회원 관련 정보를 모두 입력
              Team team = new Team();
              ...
              // 데이터베이스에서 조회한 팀 관련 정보를 모두 입력
            
              // 회원과 팀 관계 설정
              member.setTeam(team);
              return member;
            }

            JPA와 연관관계 구현

            개발자는 회원의 관계를 설정하고 회원 객체를 저장하면 된다. JPAteam의 참조를 외래 키로 변환해서 적절한 INSERT SQL을 데이터베이스에 전달한다.

            member.setTeam(team);
            jpa.persist(member);

            객체를 조회할 때 외래 키를 참조로 변환하는 일도 JPA가 처리 해준다.

            Member member = jpa.find(Member.class, memberId);
            Team team = member.getTeam();

            객체 그래프 탐색

            SQL을 직접 다루면 처음 실행하는 SQL에 따라 객체 그래프를 어디까지 탐색할 수 있는지 정해진다. 이것은 객체지향 개발자에겐 너무 큰 제약이다. 왜냐하면 비즈니스 로직에 따라 사용하는 객체 그래프가 다른데 언제 끊어질지 모를 객체 그래프를 함부로 탐색할 수는 없기 때문이다.
            결국, 어디까지 객체 그래프 탐색이 가능한지 알아보려면 데이터 접근 계층인 DAO를 열어서 SQL을 직접 확인해야 한다.
             
            class MemberService {
              ...
              public void process() {
                Member member = memberDAO.find(memberId);
                member.getTeam(); // member->team 객체 그래프 탐색이 가능한가?
                member.getOrder().getDelivery(); // ???
              }
            }

            위의 코드에서 memberDAO.find에 주목하자. 그것은 무엇을 조회했을까? 무엇을 조회했길래 '팀 정보(getTeam)', '배송+주문 정보'를 가져올수 있을까? 알수없다. 즉, find구현한 Repo를 가서 무슨 쿼리인지를 까봐야한다. 즉 위의 코드는 신뢰가 안간다.

            따라서 신뢰를 갖기 위해서는 다음과 같은 코드가 탄생한다.(복잡)

            memberDAO.getMember(); //Member만 조회
            memberDAO.getMemberWithTeam(); //Member+Team 조회
            member.DAO.getMemberWithOrderWIthDelivery(); //Member, Order, Delivery

            JPA와 객체 그래프 탐색

            JPA를 사용하면 연관된 객체를 신뢰하고 마음껏 조회할 수 있다. 이 기능은 실제 객체를 사용하는 기점까지 데이터베이스 조회를 미룬다고 해서 지연 로딩이라 한다.

            // 처음 조회 시점에 SELECT MEMBER SQL
            Member member = jpa.find(Member.class, memberId);
            
            Order order = member.getOrder();
            order.getOrderDate(); // Order를 사용하는 시점에 SELECT ORDER SQL

             

            여기서 마지막 줄의 order.getOrderDate() 같이 실제 Order 객체를 사용하는 시점에 JPA는 데이터베이스에서 ORDER 테이블을 조회한다.
             

            JDBC로 구현한 비교

            데이터베이스와 같은 로우로 조회했지만 객체의 동일성 비교에는 실패한다.

            class MemberDAO {
              public Member getMember(String memberId) {
                String sql = "SELECT * FROM MEMBER WHERE MEMBER_ID + ?";
                ...
            
                // JDBC API, SQL 실행
                return new Member(...);
              }
            }
            
            String memberId = "100";
            Member member1 = memberDAO.getMember(memberId);
            Member member2 = memberDAO.getMember(memberId);
            
            member1 == member2; // false
             

            JPA로 구현한 비교(=자바 컬렉션)

            String memberId = "100";
            Member member1 = jpa.find(Member.class,memberId);
            Member member2 = jpa.find(Member.class,memberId);
            
            member1 == member2; // true​

             정리

            객체 모델관계형 데이터베이스 모델은 지향하는 패러다임이 서로 다르다. 더 어려운 문제는 객체 지향 애플리케이션 답게 정교한 객체 모델링을 할수록 패러다임의 불일치 문제가 더 커진다는 점이다. 이는 결국 객체 모델링은 힘을 잃고 점점 데이터 중심의 모델로 변해간다. JPA는 패러다임의 불일치 문제를 해결 해주고 정교한 객체 모델링 유지하게 도와준다.  
             
            데이터 주도 설계와 도메인 주도 설계의 장단점은 무엇일까?
             

            강의 JPA소개 - JPA소개

            JPA란 무엇인가?

            JPA(Java Persistence API)는 자바 진영의 ORM 기술 표준이다.
            그렇다면 ORM이란 무엇일까?

            ORM?

            ORM(Object-Relational Mapping)은 이름 그대로 객체와 관계형 데이터베이스를 매핑 한다는 뜻이다. ORM 프레임워크는 객체와 테이블을 매핑 해서 패러다임의 불일치 문제를 개발자 대신 해결해준다.
            따라서 객체 측면에서는 정교한 객체 모델링을 할 수 있고 관계형 데이터베이스는 데이터베이스에 맞도록 모델링하면 된다. 그리고 둘을 어떻게 매핑 해야 하는지 매핑 방법만 ORM 프레임워크에게 알려주면 된다

            왜 JPA를 사용해야 하는가?

            • 생산성

            이전에 DAO에서 작업하던 지루하고 반복적인 일은 JPA가 대신 처리 해준다. 이런 기능들을 사용하면 데이터베이스 설계 중심의 패러다임을 객체 설계 중심으로 역전시킬 수 있다.

            • 유지보수

            이전엔 엔티티 필드 하나만 수정해도 관련된 DAO 로직의 SQL문을 모두 변경해야 했다. 반면에 JPA는 대신 처리해주므로 필드를 추가하거나 삭제해도 수정해야 할 코드가 줄어든다.

            • 패러다임의 불일치 해결
            • 성능

            JPA는 애플리케이션과 데이터베이스 사이에 동작하여 최적화 관점에서 시도해 볼 수 있는 것들이 많다. 예를 들어 동일한 조건으로 조회 했을 경우엔 SELECT SQL을 한 번만 데이터베이스에 전달하고 두 번째 조회한 회원 객체는 재사용할 수 있다.

            • 데이터 접근 추상화와 벤더 독립성

            관계형 데이터베이스는 같은 기능도 벤더 마다 사용법이 다른 경우가 많다. 단적인 예로 페이징 처리는 데이터베이스마다 달라서 사용법을 각각 배워야 한다. 결국 애플리케이션은 데이터베이스에 종속되어 변경 하기는 매우 어렵다. **JPA 추상화된 데이터 접근 계층을 제공해서 애플리케이션이 특정 데이터베이스 기술에 종속되지 않도록 있다.**

            생산성

            저장 : jpa.persist(member)

            조회 : Member member = jpa.find(memberId)

            수정 : member.setName("변경할 이름")

            삭제 : jpa.remove(member)

            JPA의 성능 최적화 기능

            1. 1차 캐시와 동일성 보장
            2. 트랜잭션을 지원하는 쓰기 지연 - INSERT
            3. 지연 로딩과 즉시 로딩

            지연로딩

            Member member = memberDAO.find(memberId); //selcet * from member
            Team team = member.getTeam();
            String teamName = team.getName(); //select * from team

            즉시로딩

            Member member = memberDAO.find(memberId); //select M.*, T.* from member join team
            Team team = member.getTeam();
            String teamName = team.getName();

            JPA, Hibernate, Spring Data JPA 차이점

            JPA(=껍데기)

            JPA공부를 시작함에 있어서 가장헷갈렸던 부분이 JPA와 Hibernate와의 관계였다.
            동영상강의에서는 처음에 EntityManager를 활용하여 Data를 삭제 저장 업데이트를 하지만, 실제 실무에서는 EntityManager를 사용하지 않고 Repository 인터페이스 만을 이용해서 JPA를 사용한다.

            JPA는 Java Persistence API의 약자로, 자바 어플리케이션에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스이다. 여기서 중요하게 여겨야 할 부분은, JPA는 말 그대로 인터페이스라는 점이다. JPA는 특정 기능을 하는 라이브러리가 아니다. 마치 일반적인 백엔드 API가 클라이언트가 어떻게 서버를 사용해야 하는지를 정의한 것처럼, JPA 역시 자바 어플리케이션에서 관계형 데이터베이스를 어떻게 사용해야 하는지를 정의하는 한 방법일 뿐이다.

             

            JPA는 단순히 명세이기 때문에 구현이 없다. JPA를 정의한 javax.persistence 패키지의 대부분은 interface, enum, Exception, 그리고 각종 Annotation으로 이루어져 있다. 예를 들어, JPA의 핵심이 되는 EntityManager는 아래와 같이 javax.persistence.EntityManager 라는 파일에 interface로 정의되어 있다.

            package javax.persistence;
            
            import ...
            
            public interface EntityManager {
            
                public void persist(Object entity);
            
                public <T> T merge(T entity);
            
                public void remove(Object entity);
            
                public <T> T find(Class<T> entityClass, Object primaryKey);
            
                // More interface methods...
            }

            Hibernate(JPA를 구현한 구현체)

            Hibernate는 JPA 명세의 구현체이다. javax.persistence.EntityManager와 같은 JPA의 인터페이스를 직접 구현한 라이브러리이다.

            위 사진은 JPA와 Hibernate의 상속 및 구현 관계를 나타낸 것이다. JPA의 핵심인 EntityManagerFactory, EntityManager, EntityTransaction을 Hibernate에서는 각각 SessionFactory, Session, Transaction으로 상속받고 각각 Impl로 구현하고 있음을 확인할 수 있다.

            “Hibernate는 JPA의 구현체이다”로부터 도출되는 중요한 결론 중 하나는 JPA를 사용하기 위해서 반드시 Hibernate를 사용할 필요가 없다는 것이다. Hibernate의 작동 방식이 마음에 들지 않는다면 언제든지 DataNucleus, EclipseLink 등 다른 JPA 구현체를 사용해도 되고, 심지어 본인이 직접 JPA를 구현해서 사용할 수도 있다. 다만 그렇게 하지 않는 이유는 단지 Hibernate가 굉장히 성숙한 라이브러리이기 때문일 뿐이다.

            Spring Data JPA

            필자는 Spring으로 개발하면서 단 한 번도 EntityManager를 직접 다뤄본 적이 없다. DB에 접근할 필요가 있는 대부분의 상황에서는 Repository를 정의하여 사용했다. 아마 다른 분들도 다 비슷할 것이라 생각한다. 이 Repository가 바로 Spring Data JPA의 핵심이다.

            Spring Data JPA는 Spring에서 제공하는 모듈 중 하나로, 개발자가 JPA를 더 쉽고 편하게 사용할 수 있도록 도와준다. 이는 JPA를 한 단계 추상화시킨 Repository라는 인터페이스를 제공함으로써 이루어진다. 사용자가 Repository 인터페이스에 정해진 규칙대로 메소드를 입력하면, Spring이 알아서 해당 메소드 이름에 적합한 쿼리를 날리는 구현체를 만들어서 Bean으로 등록해준다.

            Spring Data JPA가 JPA를 추상화했다는 말은, Spring Data JPA의 Repository의 구현에서 JPA를 사용하고 있다는 것이다. 예를 들어, Repository 인터페이스의 기본 구현체인 SimpleJpaRepository의 코드를 보면 아래와 같이 내부적으로 EntityManager을 사용하고 있는 것을 볼 수 있다.





            'Back-end > JPA' 카테고리의 다른 글

            엔티티 매핑  (0) 2022.08.06
            [JPA] JPA 영속성 컨텍스트  (0) 2022.05.05
            [JPA] Batch Insert  (0) 2022.05.05
            자바 ORM 표준 JPA 프로그래밍  (0) 2022.03.14
            모든 연관관계는 지연로딩으로 설정하자. (N+1문제)  (0) 2022.03.13
            2022.07.30 부터의 자바 ORM 표준 JPA 프로그래밍 학습 기록
            챕터 강의 수강날짜 완료 여부
            세션 1. JPA 소개
            SQL 중심적인 개발의 문제점
            7.30(토) OK
            JPA 소개
            7.30(토)
             OK
            섹션 2. JPA 시작하기
            Hello JPA - 프로젝트 생성
            8.6(토)
             OK
            Hello JPA - 애플리케이션 개발
               
            섹션 3. 영속성 관리 - 내부 동작 방식
            영속성 컨텍스트 1
            8.21(일) OK
            8.21(일) OK
            8.21(일) OK
            8.21(일) OK
            8.21(일) OK
            섹션 4. 엔티티 매핑 (발표) 8.6(토) OK
            8.6(토) OK
            8.6(토) OK
            8.7(일) OK
            8.8(월) OK
            섹션 5. 연관관계 매핑 기초 8.23(화) OK
               
               
               
            섹션 6. 다양한 연관관계 매핑    
               
               
               
               
            섹션 7. 고급 매핑    
               
               
            섹션 8. 프록시와 연관관계 관리    
               
               
               
            섹션 9. 값 타입

               
               
               
               
               
               
            섹션 10. 객체지향 쿼리 언어1 - 기본 문법 9/24(토) OK
            9/24(토) OK
            9/24(토) OK
               
               
               
               
               
               
            섹션 11. 객체지향 쿼리 언어2 - 중급 문법    
               
               
               
               
               
               

             

            'Back-end > JPA' 카테고리의 다른 글

            엔티티 매핑  (0) 2022.08.06
            [JPA] JPA 영속성 컨텍스트  (0) 2022.05.05
            [JPA] Batch Insert  (0) 2022.05.05
            JPA 소개  (0) 2022.03.14
            모든 연관관계는 지연로딩으로 설정하자. (N+1문제)  (0) 2022.03.13

            + Recent posts