• [인프런 JPA]자바 ORM 표준 JPA 프로그래밍 정리1

    2023. 1. 28.

    by. 웰시코더

    반응형

    -인프런 김영한님 JPA 강의를 듣고 정리한다.

    -이미지 출처는 인프런 강의 자료를 참고하였다.

    -가물가물한 상태에서 재정리하고 있는 상태라서 중간중간 틀린 개념이나 어설픈 설명이 많을 수 있다.


    1.JPA 소개

     

    1.JPA 소개

     

    JPA의 특징들은 다음과 같다.

    • JPA는 자바애플리케이션과 JDBC API 사이에서의 역할을 해줌
    • JPA가 JDBC API를 사용하여 DB에 접근
    • 쿼리를 JPA가 만들어줌
    • JPA는 패러다임 불일치를 해결해 줌. 자바는 객체지향적이고 DB는 데이터중심적이기 때문에 패러다임이 다른데 이 부분을 어느정도 해결해줌

     

    2.JPA 시작하기

     

    1.프로젝트 생성 및 애플리케이션 개발

     

    예제 진행하며 개념들을 간단하게 정리한다.

     

    1)EntityManagerFactory

     

    해당 객체는 EntityManager를 만드는 역할을 한다. EntityManagerFactory는 여러 쓰레드에서 동시에 접근해도 안전하며 생성비용이 비싸기 때문에 DB당 1개만 생성하여 사용한다.

     

    2)EntityManager

     

    해당 객체는 엔티티를 관리(생성, 삭제, 수정 등)하는 역할을 한다. 엔티티 매니저는 내부에 '영속성 컨텍스트'를 두어서 엔티티를 관리한다. 영속성 컨텍스트는 엔티티를 영구히 저장하는 환경이다. EntityManager는 요청당 1개씩 만들어지는데 트랜잭션 단위 안에서 생성되며 트랜잭션 종료(commit)시 DB에 쿼리가 날라가 반영이 된다. 엔티티 매니저는 동시성 이슈로 쓰레드간 공유되면 안 된다.

     

    아래부터 변수명 em으로 계속 작성한다. 

     

     

    3.영속성 컨텍스트 - 내부 동작 방식

     

    1.영속성 컨텍스트

     

    JPA에서 가장 중요한 2가지는 다음과 같다.

    • 객체의 매핑
    •  
    • 영속성 컨텍스트(내부동작)

    EntityManager 안에 영속성 컨텍스트가 존재한다. 영속에 따라 다음예제와 같이 분류된다.

    • 영속 : ex) em.persist(member); //영속성 컨텍스트에 반영
    • 준영속 : ex) em.detach(member); //영속성 컨텍스트에 있다가 제거 됨
    • 비영속 : ex) Member member = new Member(); //단순히 객체 생성

     

    em.persist(member) 시에는 DB에 반영되지 않고 영속성 컨텍스트에 반영되는데 이 때 영속상태라고 한다. 1차캐시에 저장하는 em.persist()를 사용하면 영속성컨텍스트 내부의 쓰기지연SQL저장소에 엔티티가 저장된다. 이후 트랜잭션 commit() 시점에 DB로 쿼리가 날라간다.

     

    이 때 em.find()를 이용하여 엔티티를 조회하면, 1차캐시에 있는경우 DB에 접근 안하고 1차캐시에서 엔티티를 가져온다. 즉, 조회 쿼리가 나가지 않는다.

     

    추가로 1차 캐시에는 스냅샷이 있어서 update시에는 스냅샷과 비교하여 내용이 바뀐 경우, 쓰기지연SQL저장소에 저장 후 commit() 시점에 DB 업데이트를 날린다. 

     

    2.플러시(flush)

     

    em.flush()로 영속성 컨텍스트의 내용을 DB에 반환한다. 테스트 때 쿼리 확인하기 위해 유용하다. 영속성 컨텍스트를 비우는 것이 아님에 주의해야하며 DB와 동기화한다는 개념이다.

     

    3.준영속 상태

     

    em.detach(member) 등으로 준영속상태를 만든 후 tx.commit()을 하면 

     

    4.엔티티 매핑

     

    1.객체와 테이블 매핑

     

    클래스에 @Entity가 붙어야 JPA가 관리하는 엔티티가 된다. 기본생성자는 필수다.

     
    2.데이터베이스 스키마 자동 생성
     
    DDL을 애플리케이션 생성 시점에 생성할 수 있다. 개발에서만 사용해야 한다. 컬럼 추가시에 ALTER SQL을 사용할 필요 없이 객체에 프로퍼티만 추가하면 된다.
    스키마 자동 생성은 persistence.xml에서 설정하며 다음 <property..>를 <persistence-unit..>에 넣어준다.
     
    <?xml version="1.0" encoding="UTF-8"?>
    <persistence version="2.2"
                 xmlns="http://xmlns.jcp.org/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_2.xsd">
        <persistence-unit name="hello">
            <properties>
                <property name="hibernate.hbm2ddl.auto" value="create"/>
            </properties>
        </persistence-unit>
    </persistence>

     

    위 프로퍼티의 value 속성 값에 따라 동작이 달라진다.

    • create : 기존 테이블 삭제 후 다시 생성
    • create-drop : create와 동일하나 애플리케이션 종료 시점에 테이블을 제거
    • update : 변경 내용만 반영
    • validate : 엔티티와 테이블이 정상 매핑되었는지만 확인
    • none : 사용하지 않음

     

    3.필드와 컬럼 매핑

     

    엔티티 프로퍼티에 어노테이션들을 사용하여 컬럼에 여러가지 방법으로 매핑할 수 있다. 몇가지를 소개하면 다음과 같다.

    • @Temporal은 시간관련. 최신 하이버네이트는 java8의 LocalDate, LocalDateTime 타입쓰면 DB의 timestamp타입과 자동 매핑
    • @Lob은 String쓰면 CLOB으로 생성
    • @Enumerated는 Enum타입의 프로퍼티를 매핑할 때 사용. EnumType.ORDINAL 사용 금지(순서를 저장함)

     

    4.기본 키 매핑

     

    기본키(Primary Key) 매핑 전략을 위한 어노테이션도 제공한다.

    • @Id : 식별자로 사용할 필드에 붙여주면 pk로 매핑
    • @GenerateValue : auto_increment 또는 시퀀스 기능을 제공하며 strategy 옵션을 통해 여러가지 생성 전략을 지정가능
      • IDENTITY : DB에게 생성방법 위임. em.persist(object) 시점에 DB에 insert 쿼리 반영.거기서 반환받은 식별자 값을 갖고 1차 캐시에 엔티티를 등록하여 관리. MySQL, PostgreSQL 등에서 사용
      • SEQUENCE : DB의 시퀀스 값을 활용하여 ID 값을 증가시킴.오라클 등에서 사용.

     

    SEQUENCE 전략을 사용할 때 테이블마다 시퀀스 객체를 따로 쓰고 싶으면 @SequenceGenerator를 사용한다. persist때마다 db에서 시퀀스를 가져오면 성능문제가 발생하기 때문에 allocationSize를 사용하여 할당한걸 미리 땡겨와서 처리한다.

     

    엔티티 및 프로퍼티 매핑과 관련된 연습 코드는 다음과 같다.

     

    @Entity
    @Table(name = "MBR") //DB의 MBR이라는 TB랑 매핑
    @SequenceGenerator(name = "member_seq_generator", sequenceName = "member_seq", allocationSize = 60)
    public class Member {
        //@Id만 쓰면 내가 직접 ID를 만듦(직접할당)
        //@GeneratedValue : 값을 자동으로 할당
        //GeneratedValue 전략은 여러개 설정 가능(AUTO, IDENTITY, SEQUENCE..)
        //IDENTITY는 생성 전략을 DB에 위임
        @Id
        @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "member_seq_generator")
        private Long id;
    
        //insertable = true, updatable = true 등 DB에 insert, update 가능여부
        //nullable = false -> not null 제약조건 생성
        @Column(name = "name", nullable = false)
        private String username;
    
        private Integer age;
    
        //DB에는 enum타입이 없기때문에 매칭시 필요
        //EnumType.ORDINAL : default. enum의 순서를 DB에 저장(사용X)
        //EnumType.STRING : enum의 이름을 DB에 저장
        @Enumerated(EnumType.STRING)
        private RoleType roleType;
    
        //Temporal은 날짜 매핑
        @Temporal(TemporalType.TIMESTAMP)
        private Date createdDate;
    
        //DB기준으로 DATE : 날짜 / TIME : 시간 / TIMESTAMP : 날짜,시간
        @Temporal(TemporalType.TIMESTAMP)
        private Date lastModifiedDate;
    
        //최신 하이버네이트 사용시에는 @Temporal을 사용할 필요가 없다.
        private LocalDate testLocalDate;
        private LocalDateTime testLocalDateTime;
    
        //문자타입이면 CLOB으로 생성(cf : BLOB)
        @Lob
        private String description;
    
        //DB랑 얘는 신경쓰고싶지않을 때, 메모리 등에서만 쓰고 싶을 때 사용
        @Transient
        private int temp;
    
        //JPA는 내부적으로 리플렉션 사용으로 기본생성자 필요
        public Member() {
        }
    }

     

     

    5.연관관계 매핑 기초

     

    기본적인 용어 정리는 다음과 같다.

    • 방향 : 단방향, 양방향
    • 다중성 : 다대일, 일대다, 일대일, 다대다
    • 연관관계의 주인 : 객체 양방향 연관관계는 관리 주인이 필요(보통 fk가 있는 쪽)

     

    1.연관관계가 필요한 이유

     

    다음과 같은 객체 간의 관계가 있다고 가정한다.

    • 회원(Member)과 팀(Team)이 있다.
    • 회원은 하나의 팀에만 소속될 수 있다. 
    • 회원과 팀은 다대일(N:1) 관계다

     

    참고 : 김영한 JPA 강의 PDF 일부

     

    위 그림과 같이 회원 엔티티가 팀 엔티티를 참조할 때 참조설정을 객체로 안 하고 외래키로 하면, 테이블 중심적인 모델링을 한 것으로 객체지향적인 설계라고 보기 어렵다. 팀을 찾으려면 회원 테이블을 em.find()로 조회하고 getTeamId()를 가져와서 Team을 조회해야 한다. 이러면 연관관계가 있다고 보기 어렵다.

     

     

    2.단방향 연관관계

     

    아래 그림과 같이 회원이 팀을 참조할 때 객체 참조타입으로 참조하면 연관관계가 설정된다. 

    참고 : 김영한 JPA 강의 PDF 일부

     

    [Member.java 일부]

    //    @Column(name = "TEAM_ID")
    //    private Long teamId;
    
        @ManyToOne
        @JoinColumn(name = "TEAM_ID")
        private Team team;

     

    N:1관계 설정시에는 @ManyToOne으로 설정 가능하다.

    다음과 같이 테스트를 하면 팀을 바로 가져올 수 있다.

     

    [JpaTest.java 일부]

    //연관관계 매핑 기초 - 객체참조매핑으로 변경
    Team team = new Team();
    team.setName("TeamA");
    em.persist(team);
    
    Member member = new Member();
    member.setUsername("member1");
    member.setTeam(team);
    em.persist(member);
    
    //조회-객체 그래프 탐색(바로끄집어낼 수가 있다)
    Member findMember = em.find(Member.class, member.getId()); //영속성 컨텍스트 상태라서 DB접근 안 함
    Team findTeam = findMember.getTeam();
    System.out.println("findTeam = " + findTeam);

     

    3.양방향 연관관계와 연관관계의 주인 1-기본

     

    엔티티가 서로를 참조하도록한다. DB는 기본적으로 양방향이지만 객체는 아니기 때문이다. 다음과 같은 그림으로 표현가능하다.

    참고 : 김영한 JPA 강의 PDF 일부

     

     

    테이블 간의 연관관계는 기본적으로 양방향이다. 객체지향은 기본적으로 단방향이다. Member에 team이 있어서 team을 가져올 수 있다. 그 반대는 기본적으로 불가능하다. 그래서 Team에 List 타입의 members를 넣어줘야 양방향이 가능하다.  기본적으로 양방향 의존성은 추천하지 않는다고 한다.

     

    [Team.java 일부]

    //양방향 매핑을 위함 / 1:N 매핑시(주인이 1)
    @OneToMany
    @JoinColumn(name = "TEAM_ID")
    private List<Member> members = new ArrayList<>();

     

    기본적으로 객체는 양방향의 의미가 단방향2개가 설정된 것으로 보는 것이 좋다. 회원이 팀쪽으로 단방향 1개, 팀이 회원쪽으로 단방향 1개라는 의미이다.

     

    이때 member에서 team을 바꾸고 싶으면 member 엔티티를 수정해야하는지, team의 members 리스트를 수정해야 하는지 딜레마가 생긴다. 그렇기 때문에 연관관계의 주인(owner)를 정해야 한다.

     

    주인이 아닌 쪽은 읽기만 가능하며 mappedBy 속성을 사용해준다. 읽기만 가능하기 때문에 Team.members에 값을 넣어도 변경이 일어나지 않는다. 

     

    주인을 정할 때에는 외래키(fk)가 있는 곳을 주인으로 정하면 된다. MEMBER TB에 TEAM_ID(FK)가 있으니 MEMBER를 주인으로 정하면 된다. 즉, @ManyToOne이 붙은 쪽이 항상 주인이 된다.

     

    4. 양방향 연관관계와 연관관계의 주인 2-주의점, 정리

     

    DB 반영시 연관관계의 주인(member)쪽에 team을 넣어주는 것이 기본적으로 맞지만, 양방향에서는 그냥 team쪽(주인이 아닌쪽)에도 member를 넣어주는 것이 객체지향적으로 더 맞다.

     

    안 넣을 때 다음과 같은 문제가 발생한다.

    • Team을 저장할때 member관련 된거 없이 그냥 1차캐시에 저장됨
    • 1차 캐시에 단순저장이라서 em.find(Team.class, team.getId())를 할 때 em.flush(), em.clear()가 없으면 캐시에서 단순객체로 가져온다.
    • 이상태에서 team.getMembers()를 하면 아무것도 못가져온다.
    • 그래서 이런상황방지를 위해 team.getMembers().add(member)같은 처리를 해주는 것이 좋다.

     

    양방향 매핑시 양쪽에 넣어줘야 하는데 한쪽에만 넣어주는 실수 방지를 위해 '양방향 편의 메소드'를 만들어서 사용할 수 있다. 다음과 같다.

     

    [Member.java 일부]

    //반대로 team쪽에 addMember(Member member)를 만들어 줘도 된다.
    public void setTeam(Team team) {
        this.team = team;
        team.getMembers().add(this);
    }

     

    가능하면 단방향으로 끝내고 필요시에 양방향 설계를 고민하는 것이 좋다.

     

     

    6.다양한 연관관계 매핑

     

    1.다대일[N:1]

    • 다중성 : @ManyToOne, @OneToMany 등
    • 다대다(@ManyToMany)는 쓰면 안 됨
    • DB의 테이블은 방향개념이 없다. 외래키 하나로 양쪽 조인이 된다.
    • 객체는 방향개념이 있다. 참조용 필드가 있는 쪽으로만 참조가 가능하다.
    • 객체 양방향 관계는 연관관계 주인이 필요(외래키를 관리하는 객체)
    • 주인의 반대편은 외래키에 영향을 주지 않음

     

    2.일대다[1:N]

    • 1에서 외래키를 관리할 때(권장하지 않음)

     

    • 1이 주인. 테이블에서는 N쪽에 무조건 외래키가 있기 때문에 특이한 구조가 나옴
    • 1에서 반대쪽 TB의 외래키를 매핑해줘야 함 → @JoinColumn(name = "TEAM_ID")
    • 가능하면 1:N보다 N:1 양방향을 쓰자

    3.일대일[1:1]

    • 반대도 일대일
    • 주테이블, 대상테이블 중 외래키 선택 가능
    • 외래키에 DB 유니크 제약 조건을 넣어야 함

     

    4.다대다[N:N]

    • 실무에서 사용하면 안 됨
    • DB는 다대다[N:N] 중간테이블 없이 매핑을 못함 / 객체는 컬렉션 사용하여 가능
    • 중간테이블이 자동으로 생성
    • 중간테이블에 원하는 것을 넣을 수가 없음
    • 다대다 관계를 사용해야 한다면 중간테이블을 엔티티로 만들어 1:N, N:1로..
      • ex)Member, Product의 경우 MemberProduct.java를 만든다)
      • 이렇게 하면 중간테이블에 price, count 등의 컬럼 추가가 가능

     

     

    7.고급 매핑

     

    1.상속관계 매핑

     

    객체는 상속관계가 있지만 DB에는 없다. 대신 DB에는 '슈퍼타입 서브타입' 관계라는 모델링 기법이 있다. 아래 그림의 오른쪽 모델이다.

     

    김영한님 JPA 강의 참고자료

     

    상속관계 매핑을 위한 어노테이션은 @Inheritance(strategy=InheritanceType.XXX)를 사용한다. XXX에 JOINED, SINGLE_TABLE, TABLE_PER_CLASS 전략을 각각 지정할 수 있다.

     

    그리고 @DiscriminatorColumn(name="DTYPE")과 @DiscriminatorValue("XXX")는 상속한 테이블(BOOK, MOVIE 등)을 구분할 수 있도록 해준다.

     

    조인전략(JOINED)은 다음과 같은 장단점이 있다.

    • 장점
      • 테이블 정규화
      • 외래키 참조 무결성 제약조건 활용가능
      • 저장공간 효율화
    • 단점
      • 조회시 조인을 많이 사용, 성능 저하
      • 조회 쿼리가 복잡함
      • 데이터 저장시 INSERT SQL 2번 호출

     

    단일테이블 전략(SINGLE_TABLE)은 다음과 같은 장단점이 있다.

     

    • 장점
      • 조인이 필요 없으므로 일반적으로 조회 성능이 빠름
      • 조회 쿼리가 단순함
    • 단점
      • 자식 엔티티가 매핑한 컬럼은 모두 null
      • 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있음

     

    단일테이블 전략에서는 @DiscriminatorColumn을 부모엔티티에 붙여주지 않아도 자동으로 DTYPE이 생성되어 들어간다.

     

    참고로 구현클래스마다 테이블 전략(TABLE_PER_CLASS)을 사용하면 부모엔티티(Item)들이 자식엔티티(Album, Movie, Book)에 녹아들어간다. 사용을 권장하지 않는 모델이다.

     

    2.Mapped Superclass - 매핑 정보 상속

     

    공통 매핑 정보(id, name 등)가 필요할 때 사용할 수 있다.

     

    김영한님 JPA 강의 참고자료

     

    실습코드1 - 공통정보를 담은 클래스(엔티티X)

     

    실습코드2 - 공통정보를 상속하는 엔티티

     

    테이블 생성시 공통정보가 들어감

     

    상속관계가 아니고 엔티티가 아니다. 부모클래스를 상속받는 자식클래스에 속성 정보만 제공한다. 엔티티가 아니기 때문에 BaseEntity는 em.find()가 불가능하다. 직접 생성해서 사용할 일이 없으므로 추상클래스로 권장한다.

     

     

    8.프록시와 연관관계 정리

     

    1.프록시

     

    Member라는 엔티티를 조회할 때, Team도 함께 조회해야 할까? 만약 printMemberAndTeam이라는 비지니스 로직이 있다면 함께 조회하는 것이 좋을 것이다. 그러나 printMember로 바뀌었다면 Team까지 조회하는 것은 손해일 것이다. 이런 문제를 해겨하기 위해 지연로딩프록시 개념을 알아야 한다. 특히 프록시를 더 먼저 이해해야 한다.

     

    JPA에서는 em.find()말고 em.getReference()라는 메서드가 있다.

     

    • em.find() : DB를 통해 실제 엔티티 객체를 조회
    • em.getReference() : DB조회를 미루는 가짜(프록시) 엔티티 객체 조회 / DB에 쿼리가 안나가는데 조회가 됨
      • getClass()를 해보면 class hellojpa.Member$hinbernateproxy..같이 나옴
      • getReference()로 조회를 하면 하이버네이트가 가짜(프록시) 엔티티를 줌

     

    프록시 객체는 실제 클래스를 상속 받아서 만들어지고 겉 모양이 실제 클래스와 같다. 사용자 입장에서는 진짜 객체인지 프록시 객체인지 구분하지않고 사용할 수 있다.

     

    프록시 객체는 실제 객체의 참조(target)을 보관하고 프록시 객체를 호출하면 프록시 객체는 실제 객체의 메소드를 호출한다.

     

    김영한님 JPA 강의 자료 참고

     

    영속성 컨텍스트에 초기화 요청이 필요하기 때문에 준영속상태에서 프로퍼티를 가져오려면 에러가 발생한다. 아래는 예제다.

     

    2.즉시로딩과 지연로딩

     

    Member 조회시 Team까지 조회할 필요가 없을 때 지연로딩을 사용한다. Member.java의 Team필드에 @ManyToOne(fetch = FetchType.LAZY) 어노테이션을 붙여준다.

     

    이 상태에서 Member를 em.find()로 조회하면 team 엔티티는 프록시 객체로 조회가 된다. 

    Team은 프록시 객체로 조회되고 team의 필드 중 하나를 조회하려고할 때 실제 쿼리가 나감

     

    즉시로딩은 fetch=FetchType.EAGER를 사용하면 된다. 그러면 Member, Team이 join되어 조회된다.

    실무에서는 가급적 지연로딩(LAZY)를 사용해야 JPQL에서 N+1를 방지할 수 있다.

     

    아래는 Team을 EAGER로 설정하고 MEMBER 조회시 문제가 될 수 있는 예제다.

     

     

    TEAM1과 TEAM2를 MEMBER1, MEMBER2에 각각 setTeam후 persist

     

    MEMBER 조회시 TEAM1, TEAM2도 가져온다.(N+1)

     

    즉시 로딩은 문제가 많기 때문에 JPQL fetch join이나 엔티티 그래프 기능을 사용해야 한다.

     

    3.영속성 전이(CASCADE)와 고아객체

     

    영속성 전이는 특정 엔티티를 영속 상태로 만들 때, 연관된 엔티티도 영속상태로 만들고 싶을 때 사용한다.

     

    childList에 cascade 옵션을 붙여줌
    em.persist(parent)만 해도 child가 들어간다

     

    cascade의 옵션 종류는 다음과 같다.

    • ALL : 모두 적용
    • PERSIST : 영속
    • REMOVE : 삭제

    parent라는 엔티티만 child와 관계가 있으면 cascade를 써도 되나, child의 소유자(관려)가 1개 초과시에는 쓰면 안 된다.

     

    그리고 부모엔티티와 연관관계가 끊어진 자식 엔티티(고아객체)를 제거할 때는 orphanRemoval=true 옵션을 준다. 다음과 같이 remove시 delete쿼리가 나간다.

    자식엔티티를 컬렉션에서 제거하면 부모객체와의 연관관계가 끊어지고 DELETE쿼리가 나감

    고아객체 제거기능을 활성화하면 부모 제거시 자식도 제거된다. Cascade.REMOVE처럼 동작한다.

     

     

     

    출처

    https://perfectacle.github.io/2018/01/14/jpa-entity-manager-factory/

    https://gmlwjd9405.github.io/2019/08/06/persistence-context.html

    https://devcamus.tistory.com/m/16

    https://developer-hm.tistory.com/40

    반응형

    댓글