Spring 입문반 - 2주차 (2)

ayboori·2023년 6월 19일
0

Spring

목록 보기
4/24

Entity

  • JPA에서 관리되는 클래스 즉, 객체를 의미합니다.
  • Entity 클래스는 DB의 테이블과 매핑되어 JPA에 의해 관리됩니다.

Entity 클래스

java 환경에서 JPA 다뤄보기

@Entity // JPA가 관리할 수 있는 Entity 클래스 지정
@Table(name = "memo") // 매핑할 테이블의 이름을 지정, 안 하면 Entity class와 동명의 테이블
public class Memo {
    @Id // 기본키를 지정, PK는 필수적으로 달아 주자
    private Long id;

    // nullable: null 허용 여부
    // unique: 중복 허용 여부 (false 일때 중복 허용)
    // 데이터베이스와 필드명을 매핑
    @Column(name = "username", nullable = false, unique = true)
    private String username;

    // length: 컬럼 최대 길이 지정 (문자열 길이 지정)
    @Column(name = "contents", nullable = false, length = 500)
    private String contents;
}
  • Entity와 연동 시 기본 생성자가 있어야 한다

  • 이미 만들어있는 테이블과 매핑할 때는 서로 충돌하지 않게 더더욱 주의하자

  • java 파일을 보고 자동으로 테이블을 생성한다

    create table memo (
       id bigint not null auto_increment,
        contents varchar(500) not null,
        username varchar(255) not null,
        primary key (id)
    ) engine=InnoDB

java - JPA 연동 시 persistence.xml에 설정이 필요하다.

src > main > resources > META-INF > persistence.xml

<?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="memo">
        <class>com.sparta.entity.Memo</class>
        <properties>
            <property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
            <property name="jakarta.persistence.jdbc.user" value="root"/>
            <property name="jakarta.persistence.jdbc.password" value="{비밀번호}"/>
            <property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/memo"/>

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

            <property name="hibernate.show_sql" value="true"/>
            <property name="hibernate.format_sql" value="true"/>
            <property name="hibernate.use_sql_comments" value="true"/>
        </properties>
    </persistence-unit>
</persistence>

@GeneratedValue(strategy = GenerationType.IDENTITY)

  • auto_increment, 사용자가 직접 id 값을 넣지 않아도 자동으로 추가된다
  • id bigint not null auto_increment : auto_increment 조건이 추가된 것을 확인할 수 있습니다.

영속성 컨텍스트

  • 영속성 : 객체가 생명(객체가 유지되는 시간)이나 공간(객체의 위치)을 자유롭게 유지하고 이동할수 있는 객체의 성질
  • 영속성 컨텍스트 : Entity 객체를 효율적으로 쉽게 관리하기 위해 만들어진 공간
  • JPA는 영속성 컨텍스트에 Entity 객체들을 저장하여 관리하면서 DB와 소통

EntityManager

  • 클래스들이 테이블과 매핑, 클래스 내에 Entity 객체들 들어있고, Manager가 소통
  • 이름 그대로 Entity를 관리하는 관리자
  • 개발자들은 EntityManager를 사용해서 Entity의 CRUD 조작
  • EntityManagerFactory를 통해 생성

EntityManagerFactory

  • 일반적으로 DB 하나에 하나만 생성되어 애플리케이션이 동작하는 동안 사용된다.
  • 정보를 전달하기 위해서는 /resources/META-INF/ 위치에 persistence.xml 파일을 만들어 정보를 넣어두면 됩니다.
    <?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="memo">
            <class>com.sparta.entity.Memo</class>
            <properties>
                <property name="jakarta.persistence.jdbc.driver" value="com.mysql.cj.jdbc.Driver"/>
                <property name="jakarta.persistence.jdbc.user" value="root"/>
                <property name="jakarta.persistence.jdbc.password" value="{비밀번호}"/>
                <property name="jakarta.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/memo"/>
    
                <property name="hibernate.hbm2ddl.auto" value="create" />
    
                <property name="hibernate.show_sql" value="true"/>
                <property name="hibernate.format_sql" value="true"/>
                <property name="hibernate.use_sql_comments" value="true"/>
            </properties>
        </persistence-unit>
    </persistence>
EntityManagerFactory emf = Persistence.createEntityManagerFactory("memo");
EntityManager em = emf.createEntityManager();
  • EntityManagerFactory emf = Persistence.createEntityManagerFactory("memo");
  • 해당 코드를 호출하면 JPA는 persistence.xml 의 정보를 토대로 EntityManagerFactory를 생성합니다.
  • EntityManager em = emf.createEntityManager(); 코드를 호출하면 EntityManagerFactory를 사용하여 EntityManager를 생성할 수 있습니다.

트랜잭션

  • 쪼갤 수 없는 (데이터 베이스) 업무 처리의 최소 단위
  • 데이터베이스의 상태를 변화시키기 휘해서 수행하는 작업의 단위
  • 여러 개의 SQL이 하나의 트랜잭션에 포함될 수 있다.
  • 속성 : 원자성 / 일관성 / 독립성 / 영속성

트랜색션 내에서 commit을 하기 전까지는 DB에 작업이 반영되지 않는다.

영속성 컨텍스트 내에서도 트랜잭션처럼 한 번에 작업을 반영한다

JPA에서도 영속성 컨텍스트로 관리하고 있는 객체들의 정보 (변경이 발생한 것들)을 지연 저장소에 가지고 있다가 SQL을 한 번에 DB에 요청해 변경을 반영한다.

@Test
@DisplayName("EntityTransaction 성공 테스트")
void test1() {
    EntityTransaction et = em.getTransaction(); // EntityManager 에서 EntityTransaction 을 가져옵니다.

    et.begin(); // 트랜잭션을 시작합니다.

    try { // DB 작업을 수행합니다.

        Memo memo = new Memo(); // 저장할 Entity 객체를 생성합니다.
        memo.setId(1L); // 식별자 값을 넣어줍니다.
        memo.setUsername("Robbie");
        memo.setContents("영속성 컨텍스트와 트랜잭션 이해하기");

        em.persist(memo); // EntityManager 사용하여 memo 객체를 영속성 컨텍스트에 저장합니다.

        et.commit(); // 오류가 발생하지 않고 정상적으로 수행되었다면 commit 을 호출합니다.
        // commit 이 호출되면서 DB 에 수행한 DB 작업들이 반영됩니다.
    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback(); // DB 작업 중 오류 발생 시 rollback 을 호출합니다.
    } finally {
        em.close(); // 사용한 EntityManager 를 종료합니다.
    }

    emf.close(); // 사용한 EntityManagerFactory 를 종료합니다.
}

영속성 컨텍스트의 기능

1차 캐시

  • 우리가 저장하는 Entity 객체들은 1차 캐시(=캐시 저장소)에 저장된다.
  • 캐시 저장소는 Map 형태이다 (id, 클래스 객체)

쓰기 지연 저장소 (ActionQueue)

JPA가 트랜잭션처럼 SQL을 모아서 한 번에 DB에 반영할 때 사용하는 저장소

  • 디버깅 시 em > actionQueue를 확인해보면 반영 이전의 객체를 확인할 수 있고, commit이나 flush 이후에는 사라진다.
  • em.flush() : 영속성 컨텍스트의 변화를 DB에 반영, et.commit() 시 자동 수행됨

트랜잭션 환경

 EntityTransaction et = em.getTransaction();

    et.begin();

하지 않고 플러시 메서드를 호출하면 no transaction is in progress 메시지와 함께 TransactionRequiredException 오류가 발생합니다.
- Insert, Update, Delete 즉, 데이터 변경 SQL을 DB에 요청 및 반영하기 위해서는 트랜잭션이 필요합니다.
- Select 시에는 특정 케이스 이외에 필수적이진 않다.

Entity 저장

em.persist(객체); 메서도 호출 시 캐시 저장소에 저장

Entity 조회

데이터의 변경이 발생하지 않으므로 트랜잭션이 없어도 조회 가능

  1. 캐시 저장소 조회 (id 값을 이용)
  2. DB SELECT 조회 후 캐시 저장소에 저장
  • em.find(Memo.class, 1); 호출 시 캐시 저장소를 확인 한 후 해당 값이 없다면?
  • DB에 SELECT 조회 후 해당 값을 캐시 저장소에 저장 후 반환
  • '1차 캐시' 사용의 장점
    1. DB 조회 횟수를 줄임
    2. '1차 캐시'를 사용해 DB row 1개 당 객체 1개가 사용되는 것을 보장 (객체 동일성 보장)
    • 객체 동일성 보장 (같은 id 값으로 조회할 경우 같은 객체가 리턴됨)

Entity 삭제

  1. Entity 조회 후 해당 객체를 저장
    2.em.remove(객체); 를 수행

Entity 업데이트 - 변경 감지 (DirtyChecking)

  • 데이터를 가져온 뒤 setMethod로 값 변경만 해 주어도 flush 시 자동으로 변경을 확인하고 반영시킨다 / Managed일 때만 반영된다.
@Test
@DisplayName("변경 감지 확인")
void test8() {
    EntityTransaction et = em.getTransaction();

    et.begin();

    try {
        System.out.println("변경할 데이터를 조회합니다.");
        Memo memo = em.find(Memo.class, 4);
        System.out.println("memo.getId() = " + memo.getId());
        System.out.println("memo.getUsername() = " + memo.getUsername());
        System.out.println("memo.getContents() = " + memo.getContents());

        System.out.println("\n수정을 진행합니다.");
        memo.setUsername("Update");
        memo.setContents("변경 감지 확인");

        System.out.println("트랜잭션 commit 전");
        et.commit(); // 이때 DB에 변경 사항 반영된다
        System.out.println("트랜잭션 commit 후");

    } catch (Exception ex) {
        ex.printStackTrace();
        et.rollback();
    } finally {
        em.close();
    }

    emf.close();
}
  • commit이나 flush 이후 현재 상태와 최초 상태를 비교하고 변경이 있다면 Update SQL을 생성하여 쓰기 지연 저장소에 저장한 후 DB에 요청한다.

Entity의 상태

  • Transient : 아직 영속성 컨텍스트에 저장되지 않았기 때문에 JPA의 관리를 받지 않습니다. (객체 생성 new, 값 세팅만 했을 때)

    debugging 조회 시 entitiesByKey=null
    JPA가 관리하지 못해서 해당 데이터 변경해도 변경 감지 X

  • Managed : em.persist(memo); 비영속 Entity를 EntityManager를 통해 영속성 컨텍스트에 저장하여 관리되고 있는 상태로 만듭니다.

  • Detached (= 준영속 상태) : 영속성 컨텍스트에 저장되어 관리되다가 분리된 상태를 의미합니다.
    -em.detach(memo); 특정 Entity만 준영속 상태로 전환, 영속성 컨텍스트의 기능 사용 불가
    -em.clear(); : 영속성 컨텍스트를 완전히 초기화, 틀은 유지되어 계속해서 이용 가능
    -em.close(); : 영속성 컨텍스트를 종료, 이후 사용 불가

  • Merge : em.merge(memo); 전달받은 Entity를 사용하여 새로운 영속 상태의 Entity를 반환
    준영속 상태에서 다시 영속 상태로 변경 가능, 비영속 상태도 파라미터로 받을 수 있다.

  • 파라미터로 전달된 Entity의 식별자 값으로 영속성 컨텍스트를 조회
    1. 해당 Entity가 영속성 컨텍스트에 없다면?
      1. DB에서 새롭게 조회합니다.
      2. 조회한 Entity를 영속성 컨텍스트에 저장합니다.
      3. 전달 받은 Entity의 값을 사용하여 병합합니다.
      4. Update SQL이 수행됩니다. (수정)
    2. 만약 DB에서도 없다면 ?
      1. 새롭게 생성한 Entity를 영속성 컨텍스트에 저장합니다.
        즉, 해당 entity는 계속 비영속 상태, 새로운 entity만 영속
      2. Insert SQL이 수행됩니다. (저장)

.


## SpringBoot의 JPA

Spring의 트랜잭션

Entity Transaction을 따로 부여할 필요 없이 Method나 Class에 걸어주면 된다

  • 애플리케이션에서 트랜잭션 개념을 적용할 수 있다.
  • 적용하지 않은 경우 em.persist(객체); 사용이 되지 않아 DB에 저장되지 않는다.

@Transactional(readOnly = true)

  • 읽기 전용 최적화, 이게 걸린 상태에서 수정 추가 불가
  • 클래스에 위 문장이 걸려있으면 메소드 위에 @Transactional 을 걸어서 편집 가능

@PersistenceContext

  • SpringBoot가 만들어주는 Entity Manager 쉽게 받아올 수 있다

트랜잭션 전파

메소드 뿐만 아니라 Service, Repsitory까지 제어할 수 있도록 트랜잭션 전파 기능을 제공하고 있다

  • 자식 method의 Transaction이 부모의 것에 합쳐지는 것이 Default이다.


@SpringBootTest
public class TransactionTest {

    @PersistenceContext
    EntityManager em;

    @Autowired
    MemoRepository memoRepository;

    @Test
    @Transactional
    @Rollback(value = false) // 테스트 코드에서 @Transactional 를 사용하면 테스트가 완료된 후 롤백하기 때문에 false 옵션 추가
    @DisplayName("메모 생성 성공")
    void test1() {
        Memo memo = new Memo();
        memo.setUsername("Robbert");
        memo.setContents("@Transactional 테스트 중!");

        em.persist(memo);  // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
    }

    @Test
    @Transactional
    @Rollback(value = false)
    @DisplayName("트랜잭션 전파 테스트")
    void test3() {
        memoRepository.createMemo(em);
        System.out.println("테스트 test3 메서드 종료");
    }
}

Spring Data JPA

  • JPA를 쉽게 사용할 수 있게 만들어둔 하나의 모듈
  • JPA를 추상화시킨 Repository 인터페이스를 제공

JpaRepository 등록

Screen Shot 2023-05-17 at 6.15.27 PM.png

public interface MemoRepository extends JpaRepository<Memo, Long> {

}
  • JpaRepository<"@Entity 클래스", "@Id 의 데이터 타입">를 상속받는 interface 로 선언합니다.
    - Spring Data JPA에 의해 자동으로 Bean 등록이 되었습니다.
    - 제네릭스의 @Entity 클래스 위치에 Memo Entity를 추가했기 때문에 해당 MemoRepository는 DB의 memo 테이블과 연결되어 CRUD 작업을 처리하는 인터페이스가 되었습니다.

제공된 interface의 클래스들을 사용하여 원하는 값을 얻을 수 있다.
대신 return 타입을 잘 확인해야 한다.

저장

public MemoResponseDto createMemo(MemoRequestDto requestDto) {
    // RequestDto -> Entity
    Memo memo = new Memo(requestDto);

    // DB 저장 > 이 메서드에 @Transactional 적용되어 있다.
    Memo saveMemo = memoRepository.save(memo);

    // Entity -> ResponseDto
    MemoResponseDto memoResponseDto = new MemoResponseDto(saveMemo);

    return memoResponseDto;
}

조회

1) findAll

public List<MemoResponseDto> getMemos() {
    // DB 조회
    return memoRepository.findAll().stream().map(MemoResponseDto::new).toList();
}

2) findById

private Memo findMemo(Long id) {
    return memoRepository.findById(id).orElseThrow(() ->
            new IllegalArgumentException("선택한 메모는 존재하지 않습니다.")
    );
}

find 메서드의 반환 타입은 Optional이다.
처리 방안은
1) Optinal<Entity타입> 을 return 타입으로 받은 뒤 null에 대한 사항 따로 체크
2) orElseThrow를 사용하여 return 값이 null일 경우 예외를 던지도록 처리

JPA Auditing 적용하기

  • Spring Data JPA에서는 시간에 대해서 자동으로 값을 넣어주는 기능인 JPA Auditing을 제공하고 있습니다.
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class) // 있어야 자동 추가
public abstract class Timestamped { 
// 이 자체를 객체로 사용할 일은 없으니까 abstract로 선언

    @CreatedDate
    @Column(updatable = false) // 최초 시간만 저장됨. 수정 불가
    @Temporal(TemporalType.TIMESTAMP) // 연월일시분초 저장
    private LocalDateTime createdAt; // 시간값 저장

    @LastModifiedDate // 변경 시 적용됨
    @Column
    @Temporal(TemporalType.TIMESTAMP)
    private LocalDateTime modifiedAt; // 변경시간이 변경될 때마다 반영됨
}
  • @MappedSuperclass

    • JPA Entity 클래스들이 해당 추상 클래스를 상속할 경우 createdAt, modifiedAt 처럼 추상 클래스에 선언한 멤버변수를 컬럼으로 인식할 수 있습니다.
  • @EntityListeners(AuditingEntityListener.class)

    • 해당 클래스에 Auditing 기능을 포함시켜 줍니다.
  • @CreatedDate

    • Entity 객체가 생성되어 저장될 때 시간이 자동으로 저장됩니다.
    • 최초 생성 시간이 저장되고 그 이후에는 수정되면 안되기 때문에 updatable = false 옵션을 추가합니다.
  • @LastModifiedDate

    • 조회한 Entity 객체의 값을 변경할 때 변경된 시간이 자동으로 저장됩니다.
    • 처음 생성 시간이 저장된 이후 변경이 일어날 때마다 해당 변경시간으로 업데이트됩니다.
  • @Temporal

    • 날짜 타입(java.util.Date, java.util.Calendar)을 매핑할 때 사용합니다.
    • DB에는 Date(날짜), Time(시간), Timestamp(날짜와 시간)라는 세 가지 타입이 별도로 존재합니다.
      • DATE : ex) 2023-01-01
      • TIME : ex) 20:21:14
      • TIMESTAMP : ex) 2023-01-01 20:22:38.771000
  • 메모장 프로젝트에 JPA Auditing 적용
    1. Timestamped 생성
    2. @EnableJpaAuditing 애너테이션을 추가합니다.
    3. 적용하고자하는 Entity 클래스에서 Timestamped를 상속받습니다.

이 부분 많이 사용한다고 하니까 추후 강의 다시 들어보자


## Query Methods란? Spring Data JPA에서는 메서드 이름으로 SQL을 생성할 수 있는 Query Methods 기능을 제공합니다. > Query Methods는 개발자가 이미 정의 되어있는 규칙에 맞게 메서드를 선언하면 해당 메서드 이름을 분석하여 SimpleJpaRepository에서 구현
  • Repository에서 선언 후, Service에서 호출만 하면 사용할 수 있다.
package com.sparta.memo.repository;

import com.sparta.memo.entity.Memo;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface MemoRepository extends JpaRepository<Memo, Long> {
    List<Memo> findAllByOrderByModifiedAtDesc(); //1
    List<Memo> findAllByUsername(String username);//2
}
  • 1: Memo 테이블에서 ModifiedAt 즉, 수정 시간을 기준으로 전체 데이터를 내림차순으로 가져오는 SQL을 실행하는 메서드

  • 2: ByUsername 에 값을 전달해줘야하기 때문에 파라미터에 해당 값의 타입과 변수명을 선언해야 함

  • 즉, Query Methods 는 메서드의 파라미터를 통해 SQL에 필요한 값을 동적으로 받아 처리할 수 있습니다.

==== 강의 자료에 debugging 상세하게 나와 있음 ====

profile
프로 개발자가 되기 위해 뚜벅뚜벅.. 뚜벅초

0개의 댓글