[스프링 숲] JPA

byeol·2023년 4월 16일
0

최주호님의 스프링 부트 개념 정리를 통해서 JPA를 정리해보려고 합니다.
추가적으로 공부하면서 의문이 생겼던 부분(✅)인 API와 RestAPI의 개념과 차이점도 함께 정리합니다.

JPA(Java Persistence API)

Java + 영속성 + API

하나씩 살펴보겠습니다.

  • 영속성이란

    데이터를 생성한 프로그램의 실행이 종료되더라도 사라지지 않는 데이터의 특성을 의미합니다. 영속성은 파일 시스템, 관계형 데이터베이스 혹은 객체 데이터베이스 등을 활용해서 구현합니다.

    즉 RAM인 휘발성 메모리에 저장하는 것이 아니라 하드디스크에 저장하여 영구적으로 보관하는 것을 의미합니다.

  • API(Application Programming Interface)
    API는 Application A가 있다고 가정했을 때 A를 프로그래밍을 실행할 수 있는 인터페이스를 의미합니다.

    인터페이스는 하나의 약속입니다. 프로토콜과 비교하면 프로토콜은 동등한 여러개의 프로그램이 통신할 수 있는 약속이라면 인터페이스는 갑을 관계가 존재합니다. 즉 A라는 프로그램은 갑이고 A라는 프로그램을 실행시키는 프로그램은 을이 됩니다. 갑은 자신의 프로그램을 이용할 수 있는 약속을 만들고 그 약속을 지키지 않는 프로그램은 A라는 프로그램을 이용할 수 없습니다.

    ✅ 그렇다면 REST API는 무엇일까요?
    REST API(REpresntational State Transfer API)는 웹 상에서 사용되는 여러 리소스를 HTTP URI로 표현하고 해당 리소스에 대한 행위를 HTTP Method로 정의하는 방식입니다.

    ✅ URI(Uniform Resource Identifier)와 URL(Uniform Resource Locator)은 무슨 차이가 있을까?

    • URL(Locator)은 위치를 통해서 접근합니다. 따라서 끝에 /a.html 혹은 /a.jsp와 같은 파일명이 들어가게 됩니다. 만약에 정적인 파일(html, javacript, css. avi)욜 요청하는 /a.html의 경우는 Web Server(아파치)를 통해서 파일을 보내주면 됩니다. 그러나 정적인 파일이 아닌 동적인 파일을 요청하는 /a.jsp의 경우는 WAS 서버(톰캣)을 통해서 .jsp파일의 java를 컴파일 하여 html인 정적인 파일로 만들어 주는 작업이 필요합니다.
    • URI(Identifier)는 통합자원 식별자를 통해서 접근합니다. 따라서 무조건 Was Server(톰캣)를 이용해야 합니다. 스프링 부트는 URI 접근만을 가능하도록 하기 때문에 REST API 설계가 필요합니다.

즉 정리하면 JPA는 자바의 DB를 영속성을 가지도록 하는 갑을 관계가 존재하는 약속입니다.

⏺️ JPA는 ORM(Object Relational Mapping) 기술입니다.
ORM은 객체와 DB를 매핑할 때
객체를 통해서 DB의 테이블을 만드는 것을 의미합니다.
ORM을 이용하면 SQL Query가 아닌 직관적인 코드로서 데이터를 조작할 수 있습니다.

⏺️ JPA는 반복적인 CRUD 작업을 생략하게 해줍니다.

Java 프로그램은 조건에 맞는 하나의 데이터나 여러 개의 데이터를 검색하는 SELECT(Read) 작업이나 삭제하는 DELETE, UPDATE, INSERT(Create)등의 쿼리를 DB에 자주 보냅니다. 그러나 이러한 쿼리 작업을 하기 위해서는 아래와 같은 단순 작업이 반복되게 됩니다.

1차 : Java 프로그램이 DB에 커넥션 요청하면 DB는 Java 프로그램을 확인하고 세션을 오픈합니다.
2차 : Java는 커넥션을 가지고 Query를 보낼 수 있습니다. DB는 어떤 작업 수행하여 Data를 Java 프로그램에 보내줍니다.
3차 : Java가 받은 Data는 DB의 Data와 형태가 다르기 때문에 DB의 Data를 Java의 Data 형식으로 바꾸는 작업을 합니다.
4차 : 연결된 세션과 커넥션을 끊습니다.

이런 일을 줄이도록 해주는 것이 JPA입니다. JPA는 기본적으로 반복되는 CRUD 작업을 단순하게 만들어 줍니다.

⏺️ 영속성 컨텍스트를 가지고 있다.
컨텍스트란? 대상에 대한 모든 정보입니다.

즉 영속성 컨텍스트란

  • Java가 DB에 저장하는 데이터에 대한 모든 것들을 알고 있는 역할을 담당합니다.
  • Entity Manager가 초기화 및 종료되지 않는 한 엔티티를 영구 저장하는 환경입니다.
  • 어플리케이션과 DB 사이에서 객체를 보관하는 가상의 DB입니다.
  • 서비스 별로 하나의 Entity Manager Factory가 존재하며 Entity Manager Factory에서 DB에 접근하는 트랜젝션이 생길 때마다 쓰레드 별로 Entity Manager를 생성하여 영속성 컨텍스트에 접근합니다.

Entity = 🍩, Entity Manager = 👩‍🍳, Entity Manager Factory = 🏭

✅Entity
영속성을 가진 객체로 DB 테이블에 보관할 대상입니다.
즉 영속 컨테스트에 속한 객체입니다.
이러한 엔티티는 특정한 DB에 영향을 미치는 쿼리를 실행하게 됩니다.

엔티티를 설정하는 방법은 어노테이션을 이용한 방법과 xml 설정을 이용하는 방법이 있습니다.
그러나 주로 어노테이션을 이용하여 엔티티를 설정합니다.

  • @Entity
    JPA는 클래스 이름을 테이블 이름으로 사용하는데 아래의 Room 엔티티는 기본적으로 Room 테이블과 매핑됩니다.

    @Entity
    public class Room {
    //...
    }
  • @Table
    클래스 이름 ≠ 테이블 이름
    @Table 어노테이션을 이용하여 클래스 이름과 테이블 이름이 다르더라도 name 속성을 설정하여 테이블 이름을 지정할 수 있습니다.

    @Entity
    @Table(name="room_info")
    public class Room{
    // ...
    }
  • @Id
    DB가 레코드를 구분하기 위한 주요키를 사용하는 것처럼 JPA는 엔티티의 @Id 어노테이션을 이용하여 식별자를 정합니다.

    public Optional findRoom(int roomId) {
       EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpastart");
       EntityManager em = emf.createEntityManager();
       try {
           Room room = em.find(Room.class, roomId);  // SELECT ... FROM Room r WHERE id = ? 
           return Optional.ofNullable(room);
       } finally {
           em.close();
       }
    }

    roomId가 Room 테이블의 식별자입니다.
    find 메서드는 식별자를 통해서 찾습니다.

  • @Basic
    @Id 어노테이션을 제외한 나머지 영속 대상은 @Basic입니다. 그러나 생략이 가능하기 때문에 대부분 생략해서 사용합니다.

    @Entity
    @Table(name="room_info")
    public class Room {
       @Id
       private String number;
       private String name;
       private String description;
    }
  • @Enumerated
    열거 타입에 대한 매핑을 할 때 사용합니다.

    // 호텔 등급 열거형
    public enum Grade {
       STAR1, STAR2, STAR3, STAR4, STAR5
    }
    //호텔 엔티티
    @Entity
    public class Hotel {
       @Id
       private String id;
       private String name;
       @Enumerated(EnumType.STRING)
       private Grade grade;
    }

    Hotel 엔티티를 보면 @Enumerated의 속성으로
    EnumTypes.STRING과 EnumTypes.ODINAL을 지정할 수 있습니다.
    EnumTypes.STRING은 열거형에 나열된 String 자체입니다.
    EnumTypes.ODINAL은 열거형에 나열된 인덱스가 저장됩니다.
    STAR1은 0, STAR2은 1 순으로 저장됩니다.

  • @Column
    프로퍼티의 이름과 테이블의 칼럼 이름과 같다면 생략 가능하지만, 다를 경우에는 어노테이션을 지정해줘야 합니다.

    @Entity
    @Table(name="room_info")
    public class Room {
       @Id
       private String number;
       private String name;
           
       @Column(name="description")
       private String desc;
    }
    • JPA는 INSERT, UPDATE, DELETE의 동작이 보통과 다르기 때문에 예쌍하지 못하거나 실수를 방지하기 위해서 읽기 전용 매핍 설정이 가능합니다.
      @Entity
      @Table(name="room_info")
      public class Room {
         //...
         @Column(name="id", insertable=false, updatable=false)
         private Long dbId;
      }
      위 예시는 dbId라는 필드는 자동으로 생성되는 UPDATE문과 INSERT 문에서 접근할 수 없습니다.
  • @Transient
    영속 대상에서 제외

    @Entity
    @Table(name="room_info")
    public class Room {
       @Id
       private String number;
       //...
    
       @Transient
       private long timestamp = System.currentTimeMills();
    }
    Room find = em.find(Room.class, room.getNumber());
    //Hibernate: select room0_.number as number1_0_0_ from room_info room0_ where room0_.number=?

✅Entity Manager

  • 영속성 컨텍스트를 둘어서 엔티티들을 관리한다.

  • 엔티티를 관리하는 역할을 수행하는 클래스

         public class Join{
    	
         public void save(String name){
     	Student s = new Student();
         s.setName(name);
         
         // 엔티티 매니저가 있다고 가정.
         EntityManager em;
         EntityTransaction tx = em.getTransaction();
         
         try{
         	// 엔티티 매니저에서 수행하는 모든 로직은 트랜잭션 안에서 수행되야 한다.
             tx.begin();
             
             // 이렇게 하면 해당 엔티티 매니저의 영속성 컨텍스트에 위에서 만든 student 객체가 저장된다.
             // 이제 student 엔티티는 엔티티 매니저의 관리 대상이 되고, 영속성을 가졌다고 말할 수 있다.
             em.persist(s);
             
             // 트랜잭션을 커밋한다.
             tx.commit();
         }catch(Exception e){
         	// 오류가 났다면 트랜잭션을 롤백 시켜줘야한다.
             tx.rollback();
         }finally{
         	// 더 이상 사용하지 않는 자원이므로 엔티티 매니저를 종료시켜줘야 한다.
             em.close();
           }
         
           }
         }
  • 트랜젝션(transaction)
    = 하나의 작업 단위
    = 데이터베이스의 상태를 변화시키기 위해 수행하는 작업의 단위

    "데이터의 상태를 변화시킨다"
    간단하게 SQL 질의어를 이용하여 DB에 접근하는 것

    작업의 단위는 질의어 한문장이 아닙니다. 아래의 예시를 봅시다.

    ex) 상품 결재
    상품 결재로 예를 들면 상품 결재인 하나의 트랜잭션은 아래와 같이 5단계를 가지고 있습니다.

    1.상품 재고 조회
    2.사용자의 잔고 조회
    3.상품 재고에 -1
    4.사용자의 잔고에서 해당 금액을 뺍니다.
    5.주문 완료

    위 5단계 중에서 하나라도 오류가 발생하면 전체가 오류라고 보고 맨 처음 상태로 돌려야 합니다.
    왜냐하면 트랜젝션은 하나의 작업 단위이기 때문에 내부에 하나라도 문제이면 모든 작업을 되돌려야 합니다.(rollback)
    트랜잭션이 모두 정상 수행되었을 때 commit을 수행해서 작업 내용을 실제 DB와 엔티티 매니저에 반영합니다.

    사실 위 성격은 트랜잭션의 4가지 특징 중에서 원자성에 해당됩니다.
    트랜젝션이 데이터베이스에 모두 반영되던가 아니면 전혀 반영되지 않아야 한다는 것입니다.

  • 쓰기 지연 SQL 저장소

    public class Join{
    	public void save(String name){
       	
           Student s1 = new Student();
           s.setName(name);
           
           Student s2 = new Student();
           s.setName(name);
           
           // 엔티티 매니저가 있다고 가정
           EntityManager em;
           EntityTransaction tx = em.getTransaction();
           
           try{
           	// 엔티티 매니저에서 수행하는 모든 로직은 트랜잭션 안에서 수행되야 한다.
               tx.begin();
               
               //쿼리는 전송되지 않는다.
               em.persist(s1);
               em.persist(s2);
               
               // 커밋하는 시점에 쿼리가 전송된다.
               tx.commit();
           }catch(Exception e){
           	// 오류가 났다면 트랜잭션을 롤백 시켜줘야한다.
               tx.rollback();
           }finally{
           	// 더 이상 사용하지 않는 자원이므로 엔티티 매니저를 종료시켜줘야 한다.
               em.close();
           }
           
       }
    }

    위 코드가 트랜잭션을 사용하지 않는다고 가정해봅시다.
    1.두 번의 쿠리가 날아갑니다.
    2.그러나 s2에 오류가 발생하여 롤백을 해야한다면 날리지 않아야할 s1에 대한 삽입 쿼리가 이미 반영되었습니다.

    하지만 영속성 컨텍스트는 지연 쓰기 SQL 저장소 및 트랜잭션에 의해서 아래와 같이 동작합니다.
    따라서 아래와 같이 동작합니다.
    1.트랜젝션이 커밋 되기 전가지 모든 쿼리문은 영속성 컨텍스트 내부의 쓰기 지연 SQL 저장소에 저장됩니다.
    2.트랜잭션이 커밋이 되는 순간 모든 쿼리가 한방에 날아갑니다.
    3.만약에 트랜잭션 내부에서 오류가 나서 롤백을 해야한다면 애초에 날리지도 않을 쿼리가 날아가지 않습니다.

  • JPA에서 제공하는 interface로 spring bean으로 등록되어 있어 Autowirted로 사용할 수 있다.

    @Autowired
    private EntityManager entityManager
  • Entity Manager는 Entity Cache를 가지고 있다.

✅Entity Manager Factory

  • 엔티티 매니저는 여러 스레드가 동시에 접근하면 동시성 문제가 발생하므로 스레드 간에 절대 공유하면 안됩니다.

  • 동시성 (Concurrency)
    사용자가 체감하기에 동시에 수행하는 것처럼 보이지만 사실 사용자가 체감할 수 없는 짧은 시간 단위로 작업들을 번갈아가면서 수행되는 것입니다.

  • 병렬 (Parallelism)
    우리가 생각하는 진짜 동시에 실행하는 개념
    실제로 동시에 여러 작업이 수행되는 개념

✨ 내가 데이터를 수정하고 있는데 다른 스레드에서 해당 데이터를 미리 수정해버리면 안되기 때문에
엔티티 매니저는 하나를 공유하면 안되고, 상황에 따라 계속 만들어줘야 합니다.

public class Join{
	public void save(String name){
    	
        Student s1 = new Student();
        s1.setName(name);
        
        // META-INF/persistence.xml에서 이름이 db인 persistence-unit을 찾아서 엔티티 매니저 팩토리를 생성
        EntityManagerFactory emf = Persistence.createEntityManagerFactory("db");
        EntityManager em = emf.createEntityManager();
        EntityTransaction tx = em.getTransaction();
        
        try{
        	// 엔티티 매니저에서 수행하는 모든 로직은 트랜잭션 안에서 수행되야 한다.
            tx.begin();
            
            // 이렇게 하면 해당 엔티티 매니저의 영속성 컨텍스트 위에서 만든 student 객체가 저장된다.
            // 이제 student 엔티티 매니저의 관리 대상이 되고, 영속성을 가졌다고 말할 수 있다.
            em.persist(s1);
            
            // 트랜잭션을 커밋한다.
            tx.commit();
            
        }catch(Exception e){
        	// 오류가 났다면 트랜잭션 롤백 수행
            tx.rollback();
        }fianlly{
        	// 더 이상 사용하지 않는 자원이므로 엔티티 매니저를 종료시켜줘야 한다.
            em.close();
        }
        
    }
}

Entity Manager Factory는 Entity Manager와 달리 여러 스레드가 동시에 접근해도 안전합니다.
Entity Manager Factory는 비용이 크기 때문에 DB 당 하나밖에 사용하지 않습니다.

동일한 엔티티 매니저 팩토리를 공유해서 사용하려면 싱글턴 인스턴스나 의존성 주입 등등을 통해 개발자의 의식적인 노력이 필요합니다.

✅ 스레드란

  • 동작하고 있는 프로그램을 프로세스라고 합니다. 보통 한 개의 프로세스는 한 가지의 일을 하지만 스레드를 이용하며 한 프로세스 내에서 두 가지 또는 그 이상의 일을 동시에 할 수 있습니다.
public class Sample extends Thread {
    int seq;

    public Sample(int seq) {
        this.seq = seq;
    }

    public void run() {
        System.out.println(this.seq + " thread start.");  // 쓰레드 시작
        try {
            Thread.sleep(1000);  // 1초 대기한다.
        } catch (Exception e) {
        }
        System.out.println(this.seq + " thread end.");  // 쓰레드 종료
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {  // 총 10개의 쓰레드를 생성하여 실행한다.
            Thread t = new Sample(i);
            t.start();
        }
        System.out.println("main end.");  // main 메서드 종료
    }
}

10개의 스레드를 실행시켰습니다. 스레드 메서드 수행시 시작과 종료를 출력하고 그 사이에 1초의 간격이 생깁니다. 그리고 main 메서드 종료 시 "main end"를 출력합니다.

결과를 보면 스레드의 순서에 상관 없이 동시에 실행됩니다. 또한 main 메서드가 스레드가 종료되기 전에 종료되었습니다.

reference

https://victorydntmd.tistory.com/195
https://velog.io/@neptunes032/JPA-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8%EB%9E%80
https://dev-troh.tistory.com/151
https://velog.io/@seongwon97/Spring-Boot-%EC%98%81%EC%86%8D%EC%84%B1-%EC%BB%A8%ED%85%8D%EC%8A%A4%ED%8A%B8Persistence-Context
https://perfectacle.github.io/2018/01/14/jpa-entity-manager-factory/
https://wikidocs.net/230

profile
꾸준하게 Ready, Set, Go!

0개의 댓글