스파르타클럽 | AI시대, 미래를 돌파하는 힘
누구나 잠재력을 깨워 나아가도록. IT 커리어의 모든 성장 과정을 스파르타클럽에서
academia.spartaclub.kr
📚 목차
🧩 JPA란?
DB를 직접 다룰 때의 문제점
public class Memo {
private Long id;
private String username;
private String contents;
}
- 위 형태의 객체 데이터를 DB에 저장해서 관리하려면
- DB 테이블을 만들고
- 애플리케이션에서 SQL문을 작성하고
- SQL을 JDBC를 사용해서 직접 실행하고
- SQL 결과를 객체로 직접 만들어줘야 한다
- SQL 의존적이라 변경에 취약하다
- 메모 데이터에 비밀번호를 추가해야 한다면
- SQL을 직접 수정해야 하고
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;
}

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();
}



🧩 영속성 컨텍스트의 기능
영속성 컨텍스트는 Entity 객체를 효율적으로 쉽게 관리하기 위해 만들어진 공간이다.
1차 캐시
- 영속성 컨텍스트는 내부적으로 Map 자료구조 형태의 캐시 저장소를 가지고 있다.
- key에는
@Id로 매핑한 기본키 = 식별자 값을 저장한다. - value에는 해당 Entity 클래스의 객체를 저장한다.
- 영속성 컨텍스트는 캐시 저장소 Key에 저장한 식별자 값을 사용해서 Entity 객체를 구분하고 관리한다.
- key에는
- 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();
}



- 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();
}



- 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();
}

- 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();
}

- 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();
}




쓰기 지연 저장소
- 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();
}



- 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();
}

- 트랜잭션을 설정하지 않고
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();
}



🧩 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);
}
}

영속성 컨텍스트와 트랙잭션의 생명 주기
- 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;
}
TransactionTesttest3()이 부모 메서드,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 메서드 종료");
}
}

- 기본적으로
@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();
}
'내일배움캠프' 카테고리의 다른 글
| [내일배움캠프] JWT을 사용한 회원가입과 로그인 구현 (0) | 2026.04.08 |
|---|---|
| [내일배움캠프] Bean 수동 등록, 쿠키와 세션 (0) | 2026.04.07 |
| [내일배움캠프] IoC, DI, Bean (0) | 2026.04.06 |
| [내일배움캠프] Spring 입문 1주차 (0) | 2026.04.06 |
| [내일배움캠프 사전캠프] 동시성 이슈 해결하기 (0) | 2026.04.03 |