Backend

[JPA] Java Persistence API 등장배경, 사용방법

연_우리 2022. 1. 28. 23:42
반응형

목차

     

     

    JPA 등장배경

     

    [MyBatis] 동작원리, 사용방법 정리

    목차 MyBatis 등장배경 [JDBC] 사용방법 JDBC : JAVA DataBase Connectivity 기존 자바에서는 DB를 조작하기 위해서 JDBC API를 사용했다. JDBC는 데이터베이스 종류에 상관없이 JDBC만 알면 어떤 데이터베이스를..

    lotuus.tistory.com

     

    JDBC에서 MyBatis로 넘어오면서 일정부분 편리해졌지만 아직도 불편함은 존재했다

     

     

    1. 객체마다 반복되는 CRUD, 맵핑코드 작성

    @Mapper
    @Repository
    public interface MemberRepository {
    
        @Insert("insert into member(name, age) values(#{name}, #{age})")
        @Options(useGeneratedKeys = true, keyProperty = "id")
        void insertAnno(Member member);
    
        @Select("select * from member where id=#{id}")
        Member selectAnno(Long id);
    }

    객체마다 insert, select, update, delete.... 지루하게 반복되는 CRUD코드들을 작성해주어야한다

    DB에 저장할때도 객체에서 SQL문으로 변환해주고, DB에 값을 받아올때도 SQL문에서 객체로 변환해주고,,

    그나마 MyBatis에서는 간편해지긴했지만 객체마다 비슷한 SQL문을 "직접" 반복해서 작성해주고 맵핑해야한다.

     

     

    2. 엔티티 신뢰 문제

    public void process(String id){
        Member member = memberRepository.find(id);
        member.getTeam();			//Team을 가져올 수 있을까?
        member.getOrder().getDelivery();	//Delivery를 가져올 수 있을까?
    }

    member.getTeam();
     > 이것이 가능하려면 find메소드가 member와 team을 join해서 가져오는 것을 보장해야한다.
        
    member.getOrder().getDelivery();
     > 이것이 가능하려면 find메소드가 member, order, delivery를 모두 join해서 가져오는 것을 보장해야한다.
     > select문의 join에 따라 탐색범위가 결정되어버린다. 

     

     

    3. 객체와 RDB의 패러다임 불일치

    public class Member{
    	private Long id;
    	private Long teamId;
    	private String username;
    }
    
    private class Team{
    	private Long id;
    	private String name;
    }

     

    create table member(
    	member_id bigint primary key,
    	team_id bigint,
    	username varchar,
        foreign key (team_id) references team(id)
    );
    
    create table team(
    	team_id bigint primary key,
    	name varchar
    );

    객체를 SQL로 변환 시 편리함을 위해 데이터베이스의 테이블 구조를 객체에 그대로 반영했다.

    객체에 연관관계가 있는가? NO

    테이블에 연관관계가 있는가? YES

    객체를 테이블에 맞추어 모델링했기 때문에 객체 간의 연관관계는 있을수가 없다!

     

    객체지향 관점에서 teamId대신 Team을 넣으면 어떨까? SQL에서 Team을 join해서 가져오는 것이 보장되어야한다.

    join해서 가져왔는데 Team내용이 사용되지 않는다면?? Member만 가져오는 쿼리문을 써서 Team은 null로 놔야하나??? 🤔🤔

     

    이런 고민에서 나온 것이 ORM 기술이다

     

     

     

    [ORM] Object-Relational Mapping : 객체-관계 맵핑

    객체는 객체대로 설계하고, DB는 DB대로 설계하자 

    너네 중간에 내가(=ORM) SQL문 알아서 작성하고 객체로 맵핑해줄게!!!

    데이터 삽입 시
     > Entity 분석
     > Insert SQL 생성
     > JDBC API 사용
     > 패러다임 불일치 해결
    데이터 조회 시
     > Select SQL 생성
     > JDBC API 사용
     > ResultSet 매핑
     > 패러다임 불일치 해결

     

     

     

    [JPA] Java Persistence API : 자바의 ORM 기술 표준

    자바 애플리케이션 ↔ [JPA] ↔ JDBC API ↔ DB

    JPA는 애플리케이션과 JDBC 사이에서 동작한다.

     

    Member 테이블에 컬럼이 추가되면??

     - 기존 : 모든 SQL문을 찾아서 필드를 추가해주어야한다.

     - JPA : JPA가 SQL문을 알아서 작성해주기때문에, Member 클래스에 필드만 추가해주면된다.

     => 유지보수성 증가

     

    같은 Member를 가지고 온다면??

    Long memberId = 100L;
    Member member1 = repository.find(Member.class, memberId);
    Member member2 = repository.find(Member.class, memberId);

     - 기존 : member1 != member2

     - JPA : member1 == member2

    => 동일한 트랜잭션에서 조회한 엔티티는 같음을 보장해준다.

     

    JPA의 성능 최적화

    1차 캐시와 동일성(identity) 보장 위 예시처럼 같은 100번의 Member를 조회할 때,
    1번째 find는 DB에서 값을 가져오고,
    2번째 find는 캐시에서 값을 가져온다. => 조회 성능 향상
    트랜잭션을 지원하는 쓰기 지연
    (transactional write-behind)
    트랜잭션을 커밋할때까지는 insert SQL문들을 모아놓는다
    JDBC BATCH SQL 기능을 사용해서 한번에 SQL문을 전송한다
    지연 로딩(Lazy Loading) 아래 참고
    //지연로딩 : 객체가 실제 사용될 때 로딩
    Member member = repository.find(memberId);		// select * from member;
    Team team = member.getTeam();
    String teamname = team.getName();			// select * from team;
    
    //즉시로딩 : JOIN을 사용하여 연관된 객체까지 미리 조회
    Member member = repository.find(memberId);		// select member.*, team.* from member join team..
    Team team = member.getTeam();
    String teamname = team.getName();

     

     

     

    JPA 환경설정

    maven 사용 시

    pom.xml에 디펜던시 추가

    <dependency>
        <groupId>org.hibernate</groupId>
        <artifactId>hibernate-entitymanager</artifactId>
        <version>5.6.4.Final</version>
    </dependency>

     

    resources/META-INF/persistence.xml 작성

    <?xml version="1.0" encoding="UTF-8"?>
    <persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence" version="2.2">
        <persistence-unit name="persistenceConfig">
            <properties>
            
                <!-- datasource 속성 -->
                <property name="javax.persistence.jdbc.driver" value="org.h2.Driver"/>
                <property name="javax.persistence.jdbc.user" value="sa"/>
                <property name="javax.persistence.jdbc.password" value=""/>
                <property name="javax.persistence.jdbc.url" value="jdbc:h2:tcp://localhost/~/test"/>
                <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
                
                <!-- 옵션 -->
                <!-- SQL문 로그 출력 -->
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="hibernate.use_sql_comments" value="true"/>
                
                <!-- JPA 표준에 맞춘 새로운 키 생성 전략 사용 -->
                <property name="hibernate.id.new_generator_mappings" value="true"/>
                
                <!-- 애플리케이션 실행 시점에 데이터베이스 테이블 자동 생성 -->
                <!-- <property name="hibernate.hbm2ddl.auto" value="create"/> -->
                
            </properties>
        </persistence-unit>
    </persistence>

     

     

     

    gradle 사용 시

    build.gradle에 디펜던시 추가

    implementation 'org.hibernate:hibernate-entitymanager:5.6.4.Final'

     

    resources/application.properties에 내용 작성

    spring.datasource.driverClassName=org.h2.Driver
    spring.datasource.username=va
    spring.datasource.password=
    spring.datasource.url=jdbc:h2:tcp://localhost/~/test
    
    spring.jpa.database-platform=org.hibernate.dialect.H2Dialect  
    spring.jpa.show-sql=true
    spring.jpa.properties.hibernate.format_sql=true
    spring.jpa.hibernate.ddl-auto=create

     

     * hibernate.hbm2ddl.auto : DDL을 애플리케이션 실행 시점에 자동으로 생성해준다.

     - create : 기존 테이블 삭제 후 다시 생성

     - create-drop : create와 같으나 종료시점에 테이블 drop

     - update : 변경분만 반영한다

     - validate : 엔티티와 테이블이 정상 매핑되었는지만 확인한다

     - none : 사용하지 않는다.

     

    실제 서버에서는 반드시 validate, none만 사용하도록 한다.

     

     

     

    EntityManager

    JPA의 persistence클래스는 persistence.xml 또는 application.properties 설정정보를 조회한 다음,

    EntityManagerFactory를 생성한다.

     

    EntityManagerFactory는 EntityManager를 만들어주는 객체이다.

    EntityManager는 트랜잭션을 처리할 때 항상 새로 생성하는 객체이다

     

    주의!

    EntityManagerFactory는 DB와 연결하기때문에 애플리케이션 전체에 하나만 생성해서 공유해야한다.

    EntityManager는 쓰레드간에 공유하지 않고 사용 후 버려야한다.

    jpa의 모든 데이터 변경은 트랜잭션 안에서 실행되어야한다.

    public static void main(String args[]){
    
        //EntityFactory 생성
        //설정정보 persistence-unit 태그의 name명입력
        EntityManagerFactory emf = 
            Persistence.createEntityManagerFactory("persistenceConfig")
            
        //EntityManager 생성
        EntityManager em = emf.createEntityManager();
    
        //EntityTransaction 생성
        EntityTransaction tx = em.getTransaction();
    
        //트랜잭션 시작
        tx.begin();
    
        try{
            Member member = new Member(1, "hello");
            em.persist(member);
        } catch(Exception e){
            tx.rollback();
        }
    
        em.close();
        emf.close();
    
    }

     

    JPA의 CRUD 메서드

      개발자 작성 JPA 처리
    저장 member.setTeam(team);
    jpa.persistent(member);
    insert into team ...
    insert into member ...
    조회 Member member = jpa.find(Member.class, memberId);
    member.getTeam();    //가능
    select member.*, team.*
    from member join team ...
    수정 member.setName("변경할 이름");  
    삭제 jpa.remove(member);  

     

     

     

    필드와 컬럼 매핑

    @Entity								//jpa가 관리할 객체
    public class Member{
        @Id								//DB의 PK와 매핑할 필드
        @GeneratedValue(strategy = GenerationType.AUTO)		//자동 증가하는 필드의 값을 채워준다.
        private Long id;
        
        @Column(name = "USERNAME")		//DB의 컬럼명과 객체의 필드명이 다를때 매핑해줄 수 있다.
        private String name;
        
        private int age;				
        
        @Temporal(TemporalType.TIMESTAMP)	//시간 타입을 매핑한다
        private Date regDate;
        
        @Enumerated(EnumType.STRING)	//Enum타입을 매핑한다
        private MemberType memberType;
        
        @Lob				//CLOB, BLOB을 매핑한다
        private String blob;
    }

    어노테이션은 javax.persistence에 있는 것을 사용하자

     

    @GeneratedValue

     - GenerationType.IDENTITY : MySQL일때 사용한다.

     - GenerationType.SEQUENCE: ORACLE일때 사용한다.

     - GenerationType.TABLE : 모든 DB에 사용 가능하다. 키 생성용 테이블일때 사용한다.

     - GenerationType.AUTO: 방언에 따라 자동 지정한다 (기본값)

     

    @Enumerated 의 기본값은 EnumType.ORDINAL이다.

    이것은 Enum의 index값으로 매핑하기때문에 중간에 enum이 추가되면 숫자가 다 꼬여버린다.

    반드시 String으로 설정해서 enum의 글자를 넣어주자.

     

     

     

    연관관계 매핑

    단방향 매핑 A → B

    @Entity
    public class Member{
    	@Id @GeneratedValue
    	private Long id;
        
    	@ManyToOne
    	@JoinColumn(name = "TEAM_ID")
    	private Team team;
        
    	@Column(name = "USERNAME")
    	private String username;
    }
    
    @Entity
    private class Team{
    	@Id @GeneratedValue
    	private Long id;
        
    	@Column(name = "NAME")
    	private String name;
    }

    Member와 Team의 관계가 N : 1 이므로 Member의 team에 @ManyToOne을 작성한다.

    Team 테이블의 TEAM_ID 컬럼과 연결될 것이니 @JoinColumn에 TEAM_ID를 작성한다.

     

    실제로 Member를 조회하면 JPA가 DB에 "select ~ from member left outer join team ~" SQL문장을 전송한다.

     

    @ManyToOne(fetch = FetchType.LAZY) 로 설정하면

    Member 조회 시 JPA가 Member에 대한 내용만 select해서 결과를 가져온다.

    이후 Team객체를 조회하면 그 순간에 JPA가 Team에 대해서만 select문을 전송한다.

    //지연로딩 : 객체가 실제 사용될 때 로딩
    Member member = repository.find(memberId);		// select * from member;
    Team team = member.getTeam();
    String teamname = team.getName();			// select * from team;

     

     

     

    양방향 매핑 A ↔ B

    @Entity
    public class Member{
    	@Id @GeneratedValue
    	private Long id;
        
    	@ManyToOne
    	@JoinColumn(name = "TEAM_ID")
    	private Team team;
        
    	@Column(name = "USERNAME")
    	private String username;
    }
    
    @Entity
    private class Team{
    	@Id @GeneratedValue
    	private Long id;
        
    	@Column(name = "NAME")
    	private String name;
        
    	@OneToMany(mappedBy = "team")	//★★★★★
    	private List<Member> members = new ArrayList<Member>();
    }

    테이블은 연결관계에 방향성이 없기때문에 그 자체로 양방향 관계가 성립된다. Member ↔ Team

    객체의 양방향 관계는 Member → Team 1개, Member ← Team 1개 = 단방향 연간관계 2개가 연결된 것이다.

     

    그렇다면 Member의 team 값을 수정한 후에 Team의 members를 수정하면..?

    둘다 업데이트가 되어야하는데 그럼 데이터의 신뢰성이 떨어지게된다

     

    그래서 둘 중 하나로 외래키를 관리하자! 하고 규칙이 생기게 된다

    양방향 매핑 규칙
     - 객체의 두 관계 중 하나를 연간관계의 주인으로 지정
     - 연관관계의 주인만이 외래키를 관리(등록, 수정)
     - 주인이 아닌 쪽은 읽기만 가능
     - 객체 둘 다 수정하면 주인쪽 데이터만 수정됨.
     - 주인은 @JoinColumn을 작성하고, 주인이 아닌 쪽은 mappedBy 속성을 작성한다.

    누구를 주인으로?
     - 외래키가 있는 곳을 주인으로! 
       (기존 코드에서는 Member안에 Long teamId 가 존재했었다.)

    주의!!!

     

     

    JPA의 내부구조

    영속성 컨텍스트 : 엔티티를 영구 저장하는 환경

    영속성 컨텍스트는 EntityManager를 통해 접근할 수 있다.

     

    영속성 컨텍스트를 사용하면

     - 1차 캐시를 사용할 수 있고

     - 동일성을 보장받고

     - 트랜잭션을 지원하는 쓰기 지연이 가능하고

     - 변경을 감지할 수 있고(Dirty Checking)

     - 지연을 로딩할 수 있다(Lazy Loading)

     

    엔티티의 생명주기

     - 비영속(new/transient)

       : 영속성 컨텍스트와 전혀 관계가 없는 상태

       : Member member = new Member(); 

        

     - 영속(managed)

       : 영속성 컨텍스트에 저장되어 영속성 컨텍스트가 관리하는 상태

       : em.persist(member); 

     

     - 준영속(detached)

       : 영속성 컨텍스트에 저장되었다가 분리된 상태

       : em.detach(member); 

     

     - 삭제(removed)

       : 삭제된 상태

       : em.remove(member);  객체를 DB에서 삭제한 상태

     

     

     

    JPA 동작방식

    조회

    Member member = new Member();
    member.setId("member1");
    member.setUsername("회원1");
    
    em.persist(member); 
    //영속성 컨텍스트의 1차 캐시에 key는 member1로, value는 Entity자체가 저장된다
    //(key는 @Id가 붙은 값이 된다)
    
    Member findM1 = em.find(Member.class, "member1")
    //1차 캐시에서 조회
    
    Member findM2 = em.find(Member.class, "member2");
    //1차 캐시에 없으면 DB에서 조회
    //DB조회 결과값을 1차 캐시에 저장한 후, findM2에 결과를 반환해준다

     

    작성

    Member member1 = new Member("Lisa");
    Member member2 = new Member("Happy");
    
    em.persist(member1);
    em.persist(member2);
    // member1, member2 1차 캐시에 저장
    // 해당 insert문 2개를 쓰기지연SQL저장소에 저장해둔다
    
    tx.commit();
    // insert문 2개를 DB에 전송한다 (flush)
    // DB에 commit을 실행한다

     

    수정

    Member insertMember = new Member();
    insertMember.setUsername("stark");
    member.setAge(20);
    
    em.persist(insertMember);
    // 영속성 컨텍스트의 1차 캐시에 저장된다.
    // 이때, insertMember의 스냅샷을 찍어서 보관한다.
    
    Member member = em.find(Member.class, "stark");
    // insertMember == member
    
    member.setUsername("tony");
    member.setAge(10);
    
    tx.commit();
    // 영속성 컨텍스트의 insertMember의 스냅샷과 변경된 member를 비교해서
    // 자동으로 update문을 DB에 전송한다.

    왜 이렇게 동작할까? 자바의 List를 생각해보자.

    List<Member> 에서 Member를 하나 꺼낸 후 값을 수정하고 다시 List에 담는가?? NO

     => 객체지향처럼 동작하게 하기위해서 em.update() 이런 메소드가 없는 것이다!

     

    삭제

    Member member = em.find(Member.class, "memberA");
    
    em.remove(member);
    
    tx.commit();
    //커밋 시점에 delete문 DB에 전달된다

     

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

    flush : SQL문을 DB에 전송해서 실제로 적용하는 것 = 데이터베이스에 동기화하는 것이 목적이다.

     - em.flush() : 직접 호출

     - transaction.commit() : flush 자동호출

     - JPQL 쿼리 실행 : flush 자동호출

     

     

    지연로딩 LAZY를 사용해서 프록시로 조회

    주의!! 지연로딩하려면 영속성 컨텍스트가 활성화되어있어야한다!!

    Member member = em.find(Member.class, 1L);
    //이때 Member의 Team에는 프록시객체(가짜객체)가 채워져있다.
    
    Team team = member.getTeam();
    //아직 Team은 프록시객체
    
    team.getName();
    //실제 team을 사용하는 시점에 DB조회하여 Team이 채워진다

     

     

    JPA와 객체지향 쿼리

    JPQL : SQL을 추상화하여 객체를 대상으로하는 객체지향 쿼리 언어

    SQL과 문법 유사하다. 

    JPQL은 엔티티 객체를 대상으로 쿼리를 작성하고, SQL은 테이블을 대상으로 쿼리를 작성하는 것이다.

    from 뒤에는 테이블 이름이 아닌 엔티티 이름을 사용한다

    대소문자를 구분한다

    별칭이 필수이다(m)

    String jpql = "select m from Member m where m.name like '%hello%'";
    // String jpql = "select m from Member m where m.age > 18";
    
    
    List<Member> result = em.createQuery(jpql, Member.class).getResultList();
    // getResultList() : 결과가 리스트로 반환된다.
    
    Member result = em.createQuery(jpql, Member.class).getSingleResult();
    // getSingleResult() : 결과가 정확히 하나 반환된다. (하나가 아니면 예외가 발생된다)
    
    
    String jpql = "select m from Member m where m.username =: username";
    query.setParameter("username", usernameParam);
    // 쿼리에 파라미터가 입력된다
    
    
    String jpql = "select m.username from Member m";
    String username = em.createQuery(jpql, String.class);
    // member의 필드에 해당되는 값만 가져올 수 있다.
    
    
    String jpql = "select new com.example.dto.UserDTO(m.username, m.age) from Member m";
    UserDTO user = em.createQuery(jpql, UserDTO.class);
    // 객체를 생성하는 것처럼 new 연산자를 통해 DTO로 바로 조회할 수 있다.
    
    
    String paging = "select m from Member m order by m.name desc";
    List<Member> result = em.createQuery(jpql, Member.class)
                            .setFirstResult(int startPosition)	//조회 시작 위치(0부터)
                            .setMaxResults(int maxResult)		//조회할 데이터 수 (N개 가져와)
                            .getResultList();
    // 페이징 처리 가능
    
    
    String innerJoin = "select m from Member m join m.team t";
    String outerJoin = "select m from Member m left [outer] join m.team t";
    // join
    
    
    String fetchJoin = "select m from Member m join fetch m.team";
    // fetch join : 엔티티 객체 그래프를 한번에 조회할 수 있다.
    // 별칭 사용 불가
    // 모두 Lazy 로딩일때 이때만큼은 무조건 join해서 같이 데이터를 가져와야한다! 할때 사용한다.
    // SQL문으로는 select m.*, t.* from Member m inner join team t on m.team_id = t.id

     

     

     

     

     

     

     

     

     

     

     

    참고

     

    토크ON 41차. JPA 프로그래밍 기본기 다지기 | T아카데미

    T아카데미 온라인 강의- [토크ON세미나] JPA 프로그래밍 기본기 다지기 (총8강) https://tacademy.skplanet.com/live/player/onlineLectureDetail.action?seq=149 [과정 소개] 마흔 한번째 세미나 주제는 ‘JPA(Java...

    www.youtube.com

     

    반응형
    • 네이버 블러그 공유하기
    • 페이스북 공유하기
    • 트위터 공유하기
    • 구글 플러스 공유하기
    • 카카오톡 공유하기