image

날씨가 벌써 쌀쌀해졌다.
아침에 일어나면 으슬으슬하다…
이제 진짜 가을은 얼마 없고 바로 겨울이되는 느낌

어제는 처음으로 게임을 한번도안했다.
나에게는 엄청난 성과다 그 시간에 공부를 했으니..

이 마음가짐으로 쭉 열심히 공부해보자 !


JPA

Java Persistence API의 약자다.
현재는 Jakarta Persistence 라고도 부른다고한다.

JPA는 Java 진영에서 사용하는
ORM(Object-Relational Mapping) 기술의 표준 사양이다.
즉, JPA는 Java의 인터페이스로 사양이 정의가 되어있어
구현하는 구현체가 따로 있다.

Hibernate ORM

JPA의 구현체의 하나로 EclipseLink, DataNucleus 등이 있다.
우리가 주로 배우게될 JPA 구현체는 Hibernate ORM이다.

JPA와 Hibernate ORM은
웹 애플리케이션 3계층에서 데이터 엑세스 계층에 위치한다.

Persistence Context

직역을하자면, 영속성 컨텍스트라고 부른다.
JPA에서 P에 해당하는 부분이고, 지속한다는 의미를 가진다.

위에서 얘기했듯이 ORM은 객체와 DB 테이블의 맵핑을 통해
Entity 클래스 객체 안에 포함된 정보를 테이블에 저장하는 기술이다.
JPA에서는 테이블과 맵팽되는 Entity 객체 정보를
영속성 컨텍스트라는 곳에 보관해서 애플리케이션 내에서 오래 지속되도록 한다.

그리고 보관된 Entity 정보는 DB 테이블에
데이터를 CRUD하는데 사용하게된다.

  1. Entity를 영구 저장하는 환경
  2. 애플리케이션과 DB 사이에서 객체를 보관하는 논리적 개념

image

영속성 컨텍스트을 그림을 표현한 모습이다.
보는 것과 같이 1차 캐시 ,쓰기 지연 SQL 저장소가 있다.

프로그램으로 문법과 1차캐시, 쓰기 지연 SQL 저장소가 어떻게
운영되는지 알아보자

@Getter
@Setter
@NoArgsConstructor
@Entity 
public class Member {
    @Id 
    @GeneratedValue  
    private Long memberId;

    private String email;

    public Member(String email) {
        this.email = email;
    }
}
@Configuration
public class JpaExample {
    private EntityManager em;
    private EntityTransaction tx;

    @Bean
    public CommandLineRunner testJpaBasicRunner(EntityManagerFactory emFactory) { // (1)
        this.em = emFactory.createEntityManager();  // (2)
        this.tx = em.getTransaction();
        
        return args -> {
            jpatest();
        };
    }

    private void jpatest() {
        Member member = new Member("dhfif718@gmail.com");
        em.persist(member);
        Member resultMember = em.find(Member.class, 1L);
    }
}


첫번째 코드 블럭은 Member라는 데이터를
우리가 데이터베이스와 연동하기위해 만들어둔 클래스이다.
@Entity로 엔티티클래스를 지정하고 @Id로 기본키를 지정했다.
할당전략은 GenerateValue로 값이 자동적으로 증가하게된다


두번째 코드 블럭부터 중요하다.

EntityManager 클래스
-> JPA의 영속성 컨텍스트를 관리함.
-> EntityManagerFactory 클래스 객체를 DI 받을 수 있다.
-> 주입 받은 객체를 통해서 JPA의 API 메서드를 사용할 수 있다.

.persist();
-> 해당객체를 넣으면 영속석 컨텍스트에서 객체의 정보들이 저장된다.
-> 객체는 1차 캐시에 저장되고, 이 객체는 쓰기 지연 SQL 저장소에 INSERT 쿼리 형태로 등록된다

.find();
-> 컨텍스트 안의 조회할 엔티티를 불러오는 방법 (클래스타입, 식별자값)

.getTransaction();
-> EntityTransaction 클래스 타입을 반환함
-> JPA에서는 Transcation 객체를 기준으로 DB의 Table에 데이터를 저장한다.
-> EntityManger 객체에서 가져올 수 있다.

.getTransaction().begin();
-> transaction 시작하기 위한 메서드

.getTransaction().commit();
-> 메서드 호출시점에 영속성 컨텍스트에 저장되어있는 객체를 DB의 테이블에 저장함
-> 쓰기 지연 SQL 저장소에 INSERT 쿼리가 실행되어 저장소에서 사라진다.
-> 내부적으로 flush(); 메서드가 호출되어 영속성 컨택스트의 변경 내용을 반영한다.

EntityManagerFactory 클래스
.createEntityManager();
-> EntityManager 타입의 변수에 해당 메서드로 만든 객체를 넣어줘야함
-> 만들어진 영속성 컨텍스트로 EntityManager 클래스의 기능 사용가능

이상 간단한 구문들만 먼저 정리해 보았고 두번째 코드블럭을 분석해보면

Member 클래스 타입의 객체인 member 라는 새로운 객체를
EntityManager 클래스 타입의 em이라는 참조변수의 메서드인
em.persist(member);를 실행 시켜 1차 캐시에 객체를 저장하고
쓰기 지연 SQL 저장소에 INSET 쿼리 형태로 등록한다.

영속성 컨택스트 안에 객체가 등록되었기 때문에
DB애 데이터가 저장되지 않았어도
.find();라는 메서드를 이용해 객채를 찾을 수 있는 것이다.

image

결과: 영속성 컨텍스트 저장 O, 데이터베이스 저장 X


이제 영속성 컨택스트에 등록하는 법을 알았다.
그러면 DB에는 어떻게 저장할까?

우선 우리는 Transaction 객체를 기준으로 데이터베이스의
테이블에 데이터를 저장해야되서 .begin(); .commit();로
시작과 데이터를 써줄 수 있게되어진다.

private void jpatest() {
    tx.begin();
    Member member = new Member("dhfif718@gmail.com");
    em.persist(member);
    tx.commit();
    
    Member resultMember = em.find(Member.class, 1L);
}

위에서 얘기했듯이 .commit();라는 메서드를 호출해주면
영속성 컨텍스트의 변경 사항을 테이블에 반영해주는 메서드 flush();가
내부적으로 호출되어 DataBase에 데이터가 저장되고
기존에 있던 쓰기 지연 SQL 저장소에는 데이터가 지워지게된다.

image

결과: 쓰기 지연 SQL 저장소 삭제, 데이터베이스 저장 O


만약 여러개 의 데이터를 .persist() 하여 .commit();을 하게된다면

private void jpatest() {
    tx.begin();
    Member member1 = new Member("dhfif718@gmail.com");
    Member member2 = new Member("tmdghwlq@naver.com");
    Member member3 = new Member("bomjin128@gmail.com");
    em.persist(member1);
    em.persist(member2);
    em.persist(member3);
    tx.commit();
    
    Member resultMember1 = em.find(Member.class, 1L);
    Member resultMember2 = em.find(Member.class, 2L);
    Member resultMember3 = em.find(Member.class, 3L);
    System.out.println(resultMember1.getEmail());
    System.out.println(resultMember2.getEmail());
    System.out.println(resultMember3.getEmail());
}

아래와 같이 쓰기 지연 SQL 저장소의 데이터는 모두 지워진다.

image

그리고 시스템 출력으로 찍어보 았을때
image

여전히 영속성 컨택스트 안에 객체의 값을 가져올 수 있었고

아래와 같이 데이터 베이스에서도 값이 저장된 모습을 확인할 수 있다.

image


추가하는법과 DB에 데이터를 저장하는 법까지 알았으니
영속성 컨텍스트안에 객체를 삭제한는 방법도 있다.

.remove(); 메서드를 이용하면 된다.

private void jpatest() {
    tx.begin();
    Member member1 = new Member("dhfif718@gmail.com");
    Member member2 = new Member("tmdghwlq@naver.com");
    Member member3 = new Member("bomjin128@gmail.com");
    em.persist(member1);
    em.persist(member2);
    em.persist(member3);
    tx.commit();
    
    tx.begin();
    Member resultMember1 = em.find(Member.class, 1L);
    Member resultMember2 = em.find(Member.class, 2L);
    Member resultMember3 = em.find(Member.class, 3L);
    em.remove(resultMember1);
    tx.commit();
}

이렇게 트랜젝션을 두개로 나눠 진행해보자.
첫번째는 우리가 위에서 배웠던 persist(); 메서드로 영속성 컨택스트의 등록
commit(); 메서드로 데이터 베이스에 저장 후 쓰기 지연 SQL 저장소에서 삭제.

여기서 두번째 트랜잭션은
영속성 컨텍스트의 Member타입의 클래스 객체를 가져오고
그 중에서 Id가 1에 해당하는 객체를 .remove((); 메서드에 담아주었다.

여기서 메서드를 호출하는 순간 1차 캐시에 있는 엔티티를 제거 요청을하여
쓰기 지연 SQL 저장소에 DLETE 쿼리 형태로 저장하게된다.
.commit();을 실행하면, 1차 캐시에 있는 엔티티를 제거하고, 쓰기 지연 SQL 저장소에
등록된 DELETE 쿼리가 실행 된다.

image

위와 같은 그림으로 실행이 이루어진다 생각하면 될 것 같고

그리고 DELETE 쿼리를 실행했기 때문에
데이터 베이스의 해당 엔티티의
레코드 값도 삭제되게 되어진다.

image

Entity Mapping

@Id

우선 @Id 우리가 기본키(Primary Key)로 사용하는
데이터의 여러가지 전략을 알아보자

  1. IDENTITY : 기본키 생성을 DB한테 위임함, 대표적으로 AUTO_INCREMENT
  2. SEQUENCE : DB에서 제공하는 시퀀스를 사용해서 기본키를 생성
  3. TABLE : 별도의 키 생성 테이블을 사용하는 전략임

애플리케이션 코드 상에서 기본키를 직접 할당해주는 방식을 제외 하고는
위와 같은 3가지 전략을 가지고 기본키를 할당한다.

@GenerateValue 애노테이션이 할당하는 법을 도와주며
아래와 같이 사용할 수 있다.

1). @GeneratedValue(strategy = GenerationType.IDENTITY)
-> IDENTITY 전략으로 객체를 생성하면서 별도의 기본키 값을 전달하지 않았다.
-> 예를들어 @Id만 있을 경우에는 기본적으로 해달 컬럼에 데이터를 우리가 직접 넣어줘야한다.
하지만 이놈과 같이 사용한다면 자동으로 값이 증가됨

2). @GeneratedValue(strategy = GenerationType.SEQUENCE)
-> SEQUENCE 전략으로 객체를 생성하면서 별도의 기본키 값을 전달하지 않았다.
-> DB에서 제공하는 시퀀스를 이용한 키가 생성됨

3). @GeneratedValue(strategy = GenerationType.AUTO)
-> 데이터베이스의 Dialect에 따라서 적절한 전략을 자동으로 선택한다.


@Column

그리고 @Column에 대해서 알아 보았다.
-> 엔티티 클래스의 컬럼(열)을 맵핑해주는 애노테이션이다. -> 해당 애노테이션이 없어도, 필드의 변수가 있다면 자동적으로 컬럼과 맵핑되는 필드라 간주한다. -> 속성의 기본 값은 nullable = true , updatable = true, unique = false; -> length 속성으로 문자 길이 지정가능 / default 255

특징은 위와 같고 default 설정을 다시 보면
nullable : false = null 값 불허용 / true = null 값 허용함 updatable : false = 컬럼의 값 변경 불허용 / true = 컬럼의 값 변경 허용 unique : false = 유니크 미사용 / true = 유니크 사용, 중복 데이터 컬럼에 들어올 경우 알람발생
이렇게 정리해 볼 수 있을 것 같다.

nullable은 간단하니 패스하고 updatable과 unique를 비교해보자

1). 업데이트가 불가능한 설정

@Column(nullable = false, updatable = false, unique = false)
private String email;
private void jpatest() {
        tx.begin();
        em.persist(new Member("dhfif718@naver.com"));
        em.persist(new Member("dhfif718@naver.com"));
        em.persist(new Member("dhfif718@naver.com"));
        tx.commit();

        tx.begin();
        Member member = em.find(Member.class,1L);
        member.setEmail("ddd@gmail.com");
        em.persist(member);
        tx.commit();
}

위의 처럼 1번째 엔티티의 객체를 가져와
setter로 수정 후 다시 등록하여도, DB에는 수정된 값이 반영되지 않는다.
하지만 영속석 컨텍스트의 객체를 조회해보면
변경된 객체는 persist되어있기 때문에 ddd@gmail.com 값을 가진
Member 객체가 존재한다는 것을 확인해 볼 수 있다…

그러면 이렇게 서로 상이할경우엔..? 어떻게 해결해야하지?? 라는 의문이든다..
이 부분은 오늘 아고라 스테이츠에 질문을 올려놓았다.

다음날 답변 : updatable = false이라는 뜻이 읽기전용 컬럼이기 때문에
Setter를 사용할 일이 없다고 한다 ! 예제를 위한 프로그램이었으니 참고 !

2). 유니크값 허용 설정

@Column(nullable = false, updatable = true, unique = true)
private String email;

만약 동일한 값을 해당 email 컬럼에 저장을 하게된다면
아래와 같은 알람이 컴파일 중 발생할 것이다.

Unique index or primary key violation: "PUBLIC.UK_6DOTKOTT2KJSP8VW4D0M25FB7_INDEX_4 ON PUBLIC.USERS(EMAIL NULLS FIRST) VALUES ( /* 1 */ 'dhfif718@naver.com' )"; SQL statement:




이 외에 Mapping을 하기 위한 어노테이션들 몇 가지들을
더 공부해보았다.

@Temporal
-> 날짜를 맵핑하기위한 필드에 달아주는 애노테이션
-> LocalDataTime 타입일 경우 생략이 가능하다.

@Transient
-> 이 애노테이션을 필드에 추가하면 테이블 컬럼과 매핑하지 않겠다는 의미로 JPA가 인식한다.
-> 따라서 데이터베이스에 저장도 하지않고, 조회할 때 역시 매핑되지 않는다.
-> 주로 임시 데이터를 메모리에서 사용하기위한 용도
-> 즉, DB에는 저장하고 싶지 않은데, 더미로 데이터 사용할 경우 이용하기 가능!

@Enumerated
-> enum 타입과 매핑할 때 사용하는 애너테이션이다.
-> 두가지 타입을 가질 수 있다.
1). EnumType.ORDINAL : enum의 순서를 나타내는 숫자를 테이블에 저장
2). EnumType.STRING : enum의 이름을 테이블에 저장
-> ex) @Enumerated(EnumType.STRING)


오늘의 JPA공부는 여기까지!
확살하 JDBC를 조금 공부하고 와서 JPA를 보니까
훨씬 이해가 빠르고 잘되는 것 같다.

아직 연관 관계 맵핑에 대해서는 배우지 않았지만
기본적인 테이블과 연결되어지는 과정이라든가
기본적인 개념들을 배우고 인텔리제이로 테스트해보는 과정들이
너무 재밌어서 시간이 훅훅 지나간 것 같다.

오늘 공부는 여기서 끝!!


오늘의 커피량: ☕️ ☕️ ☕️
오늘의 점심: 짜장면 곱뺴기