내일배움캠프

[내일배움캠프] JPA와 Entity, 영속성 컨텍스트

munsik22 2026. 4. 6. 20:42

스파르타클럽 | AI시대, 미래를 돌파하는 힘

누구나 잠재력을 깨워 나아가도록. IT 커리어의 모든 성장 과정을 스파르타클럽에서

academia.spartaclub.kr

📚 목차

    🧩 JPA란?

    DB를 직접 다룰 때의 문제점

    public class Memo {
        private Long id;
        private String username;
        private String contents;
    }
    • 위 형태의 객체 데이터를 DB에 저장해서 관리하려면
      1. DB 테이블을 만들고
      2. 애플리케이션에서 SQL문을 작성하고
      3. SQL을 JDBC를 사용해서 직접 실행하고
      4. SQL 결과를 객체로 직접 만들어줘야 한다
    • SQL 의존적이라 변경에 취약하다
      • 메모 데이터에 비밀번호를 추가해야 한다면
        1. SQL을 직접 수정해야 하고
        2. MemoResponseDto 객체에 값을 넣어주는 부분도 추가해야 한다
      • 단순히 비밀번호 하나 더 추가했는데 해야하는 일이 엄청 늘어남
      • 자동으로 퇴근 시간도 늦어지고 삶의 질도 낮아짐

    ORM

    • ORM(Object-Relational Mapping): 객체와 DB를 매핑해주는 도구
    • 반복적이고 번거로운 애플리케이션 단에서의 SQL 작업을 줄여주기 위해 등장함

    JPA

    • JPA(Java Persistence API): 자바 ORM 기술의 대표적인 표준 명세
    • 애플리케이션과 JDBC 사이에서 동작함
    • JPA를 사용하면 DB 연결 과정을 직접 개발하지 않아도 자동으로 처리해줌
    • 객체를 통해 간접적으로 DB 데이터를 다룰 수 있기 때문에 매우 쉽게 DB 작업을 처리 가능

    Hibernate

    • Hibernate(하이버네이트): JPA를 실제 구현한 프레임워크 중 사실상 표준(de facto)
    • Spring Boot에서는 기본적으로 Hibernate 구현을 사용 중

    🧩 Entity 이해하기

    Entity란?

    • JPA에서 관리되는 클래스 즉, 객체
    • Entity 클래스는 DB의 테이블과 매핑되어 JPA에 의해 관리됨

    프로젝트 설정

    • /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>
    • build.gradle 의존성 추가 및 빌드
        implementation 'org.hibernate:hibernate-core:6.1.7.Final'
        implementation 'com.mysql:mysql-connector-j:8.2.0'
    • memo 데이터베이스 연결 및 memo 테이블 삭제(drop)
    • /test/java/EntityTest.java
    public class EntityTest {
    
        EntityManagerFactory emf;
        EntityManager em;
    
        @BeforeEach
        void setUp() {
            emf = Persistence.createEntityManagerFactory("memo");
            em = emf.createEntityManager();
        }
    
        @Test
        void test1() {
    
        }
    }
    • /com/sparta/entity/Memo.java
      • @Id: 테이블의 기본키를 지정, 영속성 컨텍스트에서 Entity를 구분할 때 사용
    @Entity
    @Table(name = "memo")
    public class Memo {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
    
        @Column(name = "username", nullable = false, unique = true)
        private String username;
    
        @Column(name = "contents", nullable = false, length = 500)
        private String contents;
    }

    JPA가 자동으로 테이블을 만들었다

    • EntityTransaction 성공 테스트
        @Test
        @DisplayName("EntityTransaction 성공 테스트")
        void test1() {
            EntityTransaction et = em.getTransaction();
    
            et.begin();
    
            try {
    
                Memo memo = new Memo();
                memo.setId(1L);
                memo.setUsername("Robbie");
                memo.setContents("영속성 컨텍스트와 트랜잭션 이해하기");
    
                em.persist(memo);
    
                et.commit();
            } catch (Exception ex) {
                ex.printStackTrace();
                et.rollback();
            } finally {
                em.close();
            }
    
            emf.close();
        }

    디버그 중단점 설정
    "스텝오버"로 디버깅 진행
    persistenceContext(영속성 컨텍스트)에 memo 객체가 저장되어있음을 확인할 수 있다 (Identifier #1로 구분됨)

    🧩 영속성 컨텍스트의 기능

    영속성 컨텍스트는 Entity 객체를 효율적으로 쉽게 관리하기 위해 만들어진 공간이다.

    1차 캐시

    • 영속성 컨텍스트는 내부적으로 Map 자료구조 형태의 캐시 저장소를 가지고 있다.
      • key에는 @Id로 매핑한 기본키 = 식별자 값을 저장한다.
      • value에는 해당 Entity 클래스의 객체를 저장한다.
      • 영속성 컨텍스트는 캐시 저장소 Key에 저장한 식별자 값을 사용해서 Entity 객체를 구분하고 관리한다.
    • Entity 저장 테스트 코드
        @Test
        @DisplayName("1차 캐시 : Entity 저장")
        void test1() {
            EntityTransaction et = em.getTransaction();
    
            et.begin();
    
            try {
    
                Memo memo = new Memo();
                memo.setId(1L);
                memo.setUsername("Robbie");
                memo.setContents("1차 캐시 Entity 저장");
    
                em.persist(memo);
    
                et.commit();
    
            } catch (Exception ex) {
                ex.printStackTrace();
                et.rollback();
            } finally {
                em.close();
            }
    
            emf.close();
        }

    <code>persist()</code> 실행 전: <code>entitiesByKey</code>가 비어 있다
    <code>persist()</code> 실행 후: 새로운 Memo#1 객체가 저장되었다
    key-value 구조로 저장되어 있다

    • Entity 조회 테스트 코드 (조회 Id가 존재하지 않는 경우)
    @Test
    @DisplayName("Entity 조회 : 캐시 저장소에 해당하는 Id가 존재하지 않은 경우")
    void test2() {
        try {
    
            Memo memo = em.find(Memo.class, 1);
            System.out.println("memo.getId() = " + memo.getId());
            System.out.println("memo.getUsername() = " + memo.getUsername());
            System.out.println("memo.getContents() = " + memo.getContents());
    
    
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            em.close();
        }
    
        emf.close();
    }

    em.find() 호출 전
    em.find() 호출 후
    1차 캐시에 없기 때문에 SELECT 쿼리를 날려서 찾아와서 1차 캐시에 저장했다.

    • Entity 조회 테스트 코드 (조회 Id가 존재하는 경우)
    @Test
    @DisplayName("Entity 조회 : 캐시 저장소에 해당하는 Id가 존재하는 경우")
    void test3() {
        try {
    
            Memo memo1 = em.find(Memo.class, 1);
            System.out.println("memo1 조회 후 캐시 저장소에 저장\n");
    
            Memo memo2 = em.find(Memo.class, 1);
            System.out.println("memo2.getId() = " + memo2.getId());
            System.out.println("memo2.getUsername() = " + memo2.getUsername());
            System.out.println("memo2.getContents() = " + memo2.getContents());
    
    
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            em.close();
        }
    
        emf.close();
    }

    memo2는 1차 캐시에 있기 때문에 SELECT 쿼리를 날리지 않았다.

    • 1차 캐시 사용의 장점
      • DB 조회 횟수를 줄인다
      • 객체 동일성 보장: DB row 1개 당 객체 1개가 사용되는 것을 보장
    • 객체 동일성 보장 테스트 코드
    @Test
    @DisplayName("객체 동일성 보장")
    void test4() {
        EntityTransaction et = em.getTransaction();
    
        et.begin();
        
        try {
            Memo memo3 = new Memo();
            memo3.setId(2L);
            memo3.setUsername("Robbert");
            memo3.setContents("객체 동일성 보장");
            em.persist(memo3);
    
            Memo memo1 = em.find(Memo.class, 1);
            Memo memo2 = em.find(Memo.class, 1);
            Memo memo  = em.find(Memo.class, 2);
    
            System.out.println(memo1 == memo2);
            System.out.println(memo1 == memo);
    
            et.commit();
        } catch (Exception ex) {
            ex.printStackTrace();
            et.rollback();
        } finally {
            em.close();
        }
    
        emf.close();
    }

    <code>memo1 == memo2</code>가 true라고 나왔다.

    • Entity 삭제 테스트 코드
    @Test
    @DisplayName("Entity 삭제")
    void test5() {
        EntityTransaction et = em.getTransaction();
    
        et.begin();
    
        try {
    
            Memo memo = em.find(Memo.class, 2);
    
            em.remove(memo);
    
            et.commit();
    
        } catch (Exception ex) {
            ex.printStackTrace();
            et.rollback();
        } finally {
            em.close();
        }
    
        emf.close();
    }

    Memo#2의 상태가 MANAGED로 설정되어 있다
    Memo#2의 상태가 DELETED로 변경되었다.
    <code>commit()</code> 호출 이후 삭제되었다.
    이후 DELETE 쿼리가 날라갔다.

    쓰기 지연 저장소

    • JPA는 트랜잭션처럼 SQL을 모아서 한번에 DB에 반영한다.
    • JPA는 이를 구현하기 위해 쓰기 지연 저장소를 만들어 SQL을 모아두고 있다가 트랜잭션 commit 후 한번에 DB에 반영한다.
    • ActionQueue 테스트 코드
    @Test
    @DisplayName("쓰기 지연 저장소 확인")
    void test6() {
        EntityTransaction et = em.getTransaction();
    
        et.begin();
    
        try {
            Memo memo = new Memo();
            memo.setId(2L);
            memo.setUsername("Robbert");
            memo.setContents("쓰기 지연 저장소");
            em.persist(memo);
    
            Memo memo2 = new Memo();
            memo2.setId(3L);
            memo2.setUsername("Bob");
            memo2.setContents("과연 저장을 잘 하고 있을까?");
            em.persist(memo2);
    
            System.out.println("트랜잭션 commit 전");
            et.commit();
            System.out.println("트랜잭션 commit 후");
    
        } catch (Exception ex) {
            ex.printStackTrace();
            et.rollback();
        } finally {
            em.close();
        }
    
        emf.close();
    }

    actionQueue의 insertions에 2개가 저장되었다.
    <code>commit()</code> 호출 이후 actionQueue의 insertions가 0이 되었다.
    commit 후 한 번에 INSERT SQL 2개가 순서대로 요청되었다.

    • flush() 호출 테스트 코드
    @Test
    @DisplayName("flush() 메서드 확인")
    void test7() {
        EntityTransaction et = em.getTransaction();
    
        et.begin();
    
        try {
            Memo memo = new Memo();
            memo.setId(4L);
            memo.setUsername("Flush");
            memo.setContents("Flush() 메서드 호출");
            em.persist(memo);
    
            System.out.println("flush() 전");
            em.flush(); // flush() 직접 호출
            System.out.println("flush() 후\n");
            
    
            System.out.println("트랜잭션 commit 전");
            et.commit();
            System.out.println("트랜잭션 commit 후");
    
        } catch (Exception ex) {
            ex.printStackTrace();
            et.rollback();
        } finally {
            em.close();
        }
    
        emf.close();
    }

    <code>flush()</code>가 호출되자 바로 쓰기 지연 저장소의 SQL문이 요청되었다.

    • 트랜잭션을 설정하지 않고 flush 메서드를 호출하면 TransactionRequiredException 오류가 발생한다.
      • 데이터 변경 SQL(INSERT, UPDATE, DELETE)을 DB에 요청 및 반영하기 위해서는 트랜잭션 환경이 필요하다.
      • SELECT는 변경이 아니라 조회이므로 트랜잭션이 필수는 아님

    변경 감지 (Dirty Checking)

    • 영속성 컨텍스트에 저장된 Entity가 변경될 때마다 UPDATE SQL이 쓰기 지연 저장소에 저장된다면, 하나의 UPDATE SQL로 처리할 수 있는 상황을 여러번 UPDATE SQL을 요청하게 되기 때문에 비효율적이다.
    • em.update(entity); 같은 메서드는 지원하지 않는다.
    • JPA는 영속성 컨텍스트에 Entity를 저장할 때 최초 상태(LoadedState)를 저장한다.
    • em.fulsh(); 가 호출되면 Entity의 현재 상태와 최초 상태를 비교하고, 변경 내용이 있다면 UPDATE SQL을 생성하겨 쓰기 지연 저장소에 저장하고 모든 쓰기 지연 저장소의 SQL을 DB에 요청한다.
    • 변경 감지: 변경하고 싶은 데이터를 조회하고 해당 Entity 객체의 데이터를 변경하면 자동으로 UPDATE SQL이 생성되고 DB에 반영된다.
    • 변경 감지 테스트 코드
    @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();
            System.out.println("트랜잭션 commit 후");
    
        } catch (Exception ex) {
            ex.printStackTrace();
            et.rollback();
        } finally {
            em.close();
        }
    
        emf.close();
    }

    <code>find()</code> 호출 후
    entityInstance에 현재 상태, loadedState에 최초 상태가 저장되어 있다.
    commit 후 UPDATE SQL이 요청되었다.

    🧩 Entity의 상태

    비영속 (Transient)

    Memo memo = new Memo();
    • new 연산자를 통해 인스턴스화된 Entity 객체
    • 아직 영속성 컨텍스트에 저장되지 않았기 때문에 JPA의 관리를 받지 않음
    • 객체의 데이터를 변경해도 변경 감지가 이루어지지 않음

    영속 (Managed)

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

    준영속 (Detached)

    • 영속성 컨텍스트에서 저장되어 관리되다가 분리된 상태
    • 영속 상태 → 준영속 상태로 바꾸는 방법
      • em.detach(memo); : 특정 Entity만 준영속 상태로 전환
      • em.clear(); : 영속성 컨텍스트를 완전히 초기화 (컨텍스트 내 모든 Entity를 준영속으로 전환)
      • em.close(); : 영속성 컨텍스트를 종료
    • 준영속 상태 → 영속 상태
      • em.merge(memo); : 전달받은 Entity를 사용해 새로운 영속 상태의 Entity를 반환함

    삭제 (Removed)

    em.remove(memo);
    • 삭제하기 위해 조회해 온 영속 상태의 Entity를 파라미터로 전달받아 삭제 상태로 전환함

    🧩 Spring Boot의 JPA

    메모장 프로젝트 JPA 설정

    • build.gradle
        // implementation 'org.springframework.boot:spring-boot-starter-data-jdbc'
        implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    • application.properties
    spring.jpa.hibernate.ddl-auto=update
    spring.jpa.properties.hibernate.show_sql=true
    spring.jpa.properties.hibernate.format_sql=true
    spring.jpa.properties.hibernate.use_sql_comments=true
    • ddl-update의 종류
      • create: 기존 테이블 삭제 후 다시 생성 (DROP + CREATE)
      • create-drop: create와 같으나 종료시점에 테이블을 DROP함
      • update: 변경된 부분만 반영
      • validate: Entity와 테이블이 정상 매핑되었는지만 확인함
      • none: 아무것도 하지 않음
    • Memo 엔티티 수정
    package com.sparta.memo.entity;
    
    import com.sparta.memo.dto.MemoRequestDto;
    import jakarta.persistence.*;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    import lombok.Setter;
    
    @Entity
    @Getter @Setter
    @Table(name = "memo")
    @NoArgsConstructor
    public class Memo {
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        @Column(name = "username", nullable = false)
        private String username;
        @Column(name = "contents", nullable = false, length = 500)
        private String contents;
    
        public Memo(MemoRequestDto requestDto) {
            this.username = requestDto.getUsername();
            this.contents = requestDto.getContents();
        }
    
        public void update(MemoRequestDto requestDto) {
            this.username = requestDto.getUsername();
            this.contents = requestDto.getContents();
        }
    }
    • Spring Boot 환경에서는 EntityManagerFactory와 EntityManager를 자동으로 생성한다.
      • application.properties에 DB 정보를 전달해 주면 이를 토대로 EntityManagerFactory가 생성됨
      • @PersistenceConext 어노테이션을 사용하면 자동으로 생성된 EntityManager를 주입받아 사용 가능
    @PersistenceContext
    EntityManager em;

    Spring의 트랜잭션

    Spring 프레임워크에서는 DB의 트랜잭션 개념을 애플리케이션에 적용할 수 있도록 트랜잭션 관리자를 제공한다.

    @Transactional(readOnly = true)
    public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> {
        /* ... */
    			
        @Transactional
        @Override
            public <S extends T> S save(S entity) {
    		
            Assert.notNull(entity, "Entity must not be null");
    		
            if (entityInformation.isNew(entity)) {
                em.persist(entity);
                return entity;
            } else {
                return em.merge(entity);
            }
        }
    
        /* ... */
    }
    • @Transactional 어노테이션을 클래스나 메서드에 추가하면 쉽게 트랜잭션 개념을 적용할 수 있다.
      • 메서드가 호출되면 해당 메서드 내에서 수행되는 모든 DB 연산 내용은 하나의 트랜잭션으로 묶임
      • 해당 메서드가 정상적으로 수행되면 트랜잭션을 커밋하고 예외가 발생하면 롤백함
      • 클래스에 선언한 @Transactional은 해당 클래스 내부의 모든 메서드에 트랜잭션 기능을 부여함
      • save 메서드는 @Transactional가 추가되어있기 때문에 readOnly = true 옵션인 @Transactional을 덮어쓰게 되어 readOnly = false 옵션으로 적용됨
    • 트랜잭션 테스트 코드
    @SpringBootTest
    public class TransactionTest {
        
        @PersistenceContext
        private EntityManager em;
    
        @Test
        @Transactional
        @Rollback(value = false)
        @DisplayName("메모 생성 성공")
        void test1() {
            Memo memo = new Memo();
            memo.setUsername("Robbert");
            memo.setContents("@Transactional 테스트 중!");
    
            em.persist(memo);
        }
    }

    손쉽게 Transaction이 실행되었다.

    영속성 컨텍스트와 트랙잭션의 생명 주기

    • Spring Container 환경에서는 영속성 컨텍스트와 트랜잭션의 생명주기가 일치한다.
    • 트랜잭션이 유지되는 동안은 영속성 컨텍스트도 계속 유지가 되기 때문에 영속성 컨텍스트의 기능(1차 캐시, 변경 감지 등...)을 사용할 수 있다.
    • Spring은 트랜잭션 전파 기능을 통해 Service부터 Repository 까지 Transaction을 유지한다.

    트랜잭션 전파

    • MemoRepository
        @Transactional
        public Memo createMemo(EntityManager em) {
            Memo memo = em.find(Memo.class, 1);
            memo.setUsername("Robbie");
            memo.setContents("@Transactional 전파 테스트 중!");
    
            System.out.println("createMemo 메서드 종료");
            return memo;
        }
    • TransactionTest
      • test3()이 부모 메서드, createMemo(em)이 자식 메서드가 된다.
    @SpringBootTest
    public class TransactionTest {
    
        @PersistenceContext
        EntityManager em;
    
        @Autowired
        MemoRepository memoRepository;
    
        /* ... */
    
        @Test
        @Transactional
        @Rollback(value = false)
        @DisplayName("트랜잭션 전파 테스트")
        void test3() {
            memoRepository.createMemo(em);
            System.out.println("테스트 test3 메서드 종료");
        }
    }

    부모 메서드가 끝난 이후에야 Transaction이 commit되었다!

    • 기본적으로 @Transactional의 Propagation은 REQUIRED가 default로 설정되어 있기 때문에, @Transactional만 적어도 (부모 메서드에 트랜잭션이 존재하면) 자동으로 자식 메서드의 트랜잭션이 부모의 트랜잭션과 합쳐진다.

    🧩 Spring Data JPA란?

    Spring Data JPA

    • Spring Data JPA: JPA를 쉽게 사용할 수 있게 만들어놓은 하나의 모듈
      • JPA를 추상화시킨 Repository 인터페이스를 제공함
      • Repository 인터페이스는 Hibernate와 같은 JPA 구현체를 사용해서 구현한 클래스를 통해 사용됨
    • Spring Data JPA에서는 JpaRepository 인터페이스를 구현하는 클래스를 자동으로 생성해줌
      • Spring 서버가 뜰 때 JpaRepository 인터페이스를 상속받은 인터페이스가 자동으로 스캔이 되면 해당 인터페이스의 정보를 토대로 자동으로 SimpleJpaRepository 클래스를 생성해 주고, 이 클래스를 Spring Bean으로 등록함
      • 따라서 인터페이스의 구현 클래스를 직접 작성하지 않아도 JpaRepository 인터페이스를 통해 JPA의 기능을 사용할 수 있음
    • 사용 방법: JpaRepository<{@Entity 클래스}, {@Id의 데이터 타입}>을 상속받는 interface로 선언

    메모장 프로젝트 Spring Data JPA 적용

    • MemoRepository
    public interface MemoRepository extends JpaRepository<Memo, Long> {
    }
    • MemoService
    @Service
    public class MemoService {
    
        private final MemoRepository memoRepository;
    
        public MemoService(MemoRepository memoRepository) {
            this.memoRepository = memoRepository;
        }
    
        public MemoResponseDto createMemo(MemoRequestDto requestDto) {
            Memo memo = new Memo(requestDto);
    
            Memo saveMemo = memoRepository.save(memo);
            
            return new MemoResponseDto(saveMemo);
        }
    
        public List<MemoResponseDto> getMemos() {
            return memoRepository.findAll().stream().map(MemoResponseDto::new).toList();
        }
    
        @Transactional
        public Long updateMemo(Long id, MemoRequestDto requestDto) {
            Memo memo = findMemo(id);
    
            memo.update(requestDto);
    
            return id;
        }
    
        public Long deleteMemo(Long id) {
            Memo memo = findMemo(id);
    
            memoRepository.delete(memo);
    
            return id;
        }
    
        private Memo findMemo(Long id) {
            return memoRepository.findById(id).orElseThrow(() ->
                    new IllegalArgumentException("선택한 메모는 존재하지 않습니다.")
            );
        }
    }

    JPA Auditing 적용하기

    JPA Auditing이란 자동으로 시간을 저장할 수 있는 기능이다.

    • Timestamped 엔티티 생성
      • @MappedSuperclass: JPA Entity 클래스들이 해당 추상 클래스를 상속할 경우 createdAt, modifiedAt 처럼 추상 클래스에 선언한 멤버변수를 컬럼으로 인식할 수 있다.
      • @EntityListeners(AuditingEntityListener.class): 해당 클래스에 Auditing 기능을 포함시킨다.
      • @CreatedDate: Entity 객체가 생성되어 저장될 때 시간이 자동으로 저장된다. (updatable = false)
      • @LastModifiedDate: 조회한 Entity 객체의 값을 변경할 때 변경된 시간이 자동으로 저장된다.
      • @Temporal: 날짜 타입을 매핑할 때 사용한다.
    package com.sparta.memo.entity;
    
    import jakarta.persistence.*;
    import lombok.Getter;
    import org.springframework.data.annotation.CreatedDate;
    import org.springframework.data.annotation.LastModifiedDate;
    import org.springframework.data.jpa.domain.support.AuditingEntityListener;
    
    import java.time.LocalDateTime;
    
    @Getter
    @MappedSuperclass
    @EntityListeners(AuditingEntityListener.class)
    public abstract class Timestamped {
    
        @CreatedDate
        @Column(updatable = false)
        @Temporal(TemporalType.TIMESTAMP)
        private LocalDateTime createdAt;
    
        @LastModifiedDate
        @Column
        @Temporal(TemporalType.TIMESTAMP)
        private LocalDateTime modifiedAt;
    }
    • MemoApplication
    @EnableJpaAuditing // ← ★추가★
    @SpringBootApplication
    public class MemoApplication {
        public static void main(String[] args) {
            SpringApplication.run(MemoApplication.class, args);
        }
    }
    • Memo 엔티티 수정: Timestamped 상속
    @Entity
    @Getter @Setter
    @Table(name = "memo")
    @NoArgsConstructor
    public class Memo extends Timestamped { // ← 여기 수정
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long id;
        @Column(name = "username", nullable = false)
        private String username;
        @Column(name = "contents", nullable = false, length = 500)
        private String contents;
    
        public Memo(MemoRequestDto requestDto) {
            this.username = requestDto.getUsername();
            this.contents = requestDto.getContents();
        }
    
        public void update(MemoRequestDto requestDto) {
            this.username = requestDto.getUsername();
            this.contents = requestDto.getContents();
        }
    }
    • MemoResponseDto 수정
    public class MemoResponseDto {
        private final Long id;
        private final String username;
        private final String contents;
        private final LocalDateTime createdAt; // ← 추가
        private final LocalDateTime modifiedAt; // ← 추가
    
        public MemoResponseDto(Memo memo) {
            this.id = memo.getId();
            this.username = memo.getUsername();
            this.contents = memo.getContents();
            this.createdAt = memo.getCreatedAt(); // ← 추가
            this.modifiedAt = memo.getModifiedAt(); // ← 추가
        }
    }

    시간이 추가되었다

    Query Method 적용하기

    Spring Data JPA에서는 메서드 이름으로 SQL을 생성할 수 있는 Query Method 기능을 제공한다.

    • MemoRepository
    public interface MemoRepository extends JpaRepository<Memo, Long> {
        List<Memo> findAllByOrderByModifiedAtDesc();
    }
    • MemoService: getMemos() 수정
        public List<MemoResponseDto> getMemos() {
            return memoRepository.findAllByOrderByModifiedAtDesc().stream().map(MemoResponseDto::new).toList();
        }