DETACH 호출 전까지는 현재 영속상태인것을 볼 수 있다. DETACH호출하는순간 영속상태에서 빠진것을 볼 수 있다. memo객체는 현재 준영속 상태이다.
디버그를 하지 않아도, Continas를 호출을 하면 영속상태인지 아닌지 알 수 있다 memo.getContents() = 영속성 컨텍스트와 트랜잭션 이해하기 em.contains(memo) = true detach() 호출 em.contains(memo) = false memo Entity 객체 수정 시도 트랜잭션 commit 전 트랜잭션 commit 후
또한 commit과정 중에서도 아무 일도 일어나지않는다
영속 상태일때만 dirtyChecking가 일어나며, 변경사항이 추적된다.
clear 영속성 컨텍스트를 초기화한다 현재 클리어 메서드를 통과하지 않았을때는 영속성컨텍스트(1차 캐시)에 두개의 정보가 있다.
설정 설명 DDL 설정: create: 기존 테이블을 삭제하고 다시 생성한다. create-drop: create와 동일하지만, 애플리케이션 종료 시 테이블을 삭제한다. update: 변경된 부분만 반영한다. validate: 엔티티와 테이블이 올바르게 매핑되었는지 확인한다. none: 아무 작업도 하지 않는다. SQL 출력 설정:
show_sql: Hibernate가 실행하는 SQL 문을 콘솔에 출력한다. format_sql: SQL 문을 보기 좋게 포맷하여 출력한다. use_sql_comments: SQL 문에 주석을 포함하여 출력한다. 스프링 부트는 application.properties에 설정을 명시하면 자동으로 이를 반영해준다. 과거에는 META-INF 디렉토리에 별도의 설정 파일을 두어야 했지만, 스프링 부트에서는 이 과정이 간소화되었다.
또한, 스프링 부트는 @PersistenceContext 어노테이션을 사용하여 EntityManager를 자동으로 주입해준다. 이를 통해 개발자는 엔티티 매니저를 직접 관리하지 않아도 된다.
스프링에서는 트랜잭션을 쉽게 관리할 수 있도록 @Transactional 어노테이션을 제공한다. 이 어노테이션을 통해 트랜잭션을 시작하고, 오류 발생 시 자동으로 롤백할 수 있다.
entity 변경
package com.sparta.springprepare.entity;
import com.sparta.springprepare.dto.MemoRequestDto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
public class Memo {
private Long id;
private String username;
private String contents;
public Memo(MemoRequestDto requestDto)
{
this.username = requestDto.getUsername();
this.contents = requestDto.getContents();
}
public void update(MemoRequestDto requestDto)
{
this.contents = requestDto.getContents();
this.username = requestDto.getUsername();
}
}
기존의 jdbcTemplate를 사용하던 코드에서 아래와 같이 jpa로 변경해본다.
package com.sparta.memo.entity;
import com.sparta.memo.dto.MemoRequestDto;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity // JPA가 관리할 수 있는 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();
}
}
테스트.
@SpringBootTest
public class TransactionTest
{
@PersistenceContext
private EntityManager em;
@Test
@Transactional
@Rollback(value = false) // 테스트 코드에서 @Transactional 를 사용하면 테스트가 완료된 후 롤백하기 때문에 false 옵션 추가
@DisplayName("메모 생성 성공")
void test1() {
Memo memo = new Memo();
memo.setUsername("Robbert");
memo.setContents("@Transactional 테스트 중!");
em.persist(memo); // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
}
}
@PersistenceContext를 통해 스프링부트가 엔티티 매니저를 만들어준다. @transactional을 통해 자동으로 transaction을 만들어준다 @rollback 어노테이션의 value=false를 통해 테스트코드를 실행하고 끝날시 다시 롤백해주는 것을 방지할수있다.
@Test
@DisplayName("메모 생성 실패")
void test2() {
Memo memo = new Memo();
memo.setUsername("Robbie");
memo.setContents("@Transactional 테스트 중!");
em.persist(memo); // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
}
만약@transactional을 걸지않았다면??
오류가뜬다.!
영속성 컨텍스트와 생명주기 spring에서는 어떻게 service, repository 전체에 transaction을 유지할수 있을까? spring은 트랜잭션 전파기능을 가지고 있다. service에서 repository를 호출할때, 트랜잭션도 같이 전파한다.
트랜잭션 전파 repository에 아래 메서드를 정의해보자
@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;
}
아래 테스트코드를 작성하자
Test
@Transactional
@Rollback(value = false)
@DisplayName("트랜잭션 전파 테스트")
void test3() {
memoRepository.createMemo(em);
System.out.println("테스트 test3 메서드 종료");
}
위 코드를 보면, 먼저 test3에서 memorepository의 createMemo를 출력한다 일단 test3를 부모메서드, create를 자식메서드라고하자. 위 결과를 보면 자식 메서드의 find로 인해 select쿼리가 날아간것을 볼 수 있다. 근데 createMemo를 수행한다음, update 쿼리가 날아갈 줄 알았지만 부모 메서드 종료후 날아가는것을 볼 수 있다.
왜일까?
Transactional에는 propagation의 값이 required로 되어있다. 위 옵션은, 부모 메서드에서 transactional이 존재를 한다면 자식 메서드의 transactional은 부모 메서드에 합류하게된다 라는뜻이다.
부모 메서드의 transactional을 주석 처리해보자. 자식 메서드가 종료되면서 update가 날라갔는것을 볼 수 있다.
Spring Data Jpa spring data jpa는 jpa를 쉽게 사용할 수 있게 만들어놓은 모듈이다 jpa를 추상화시킨 repository 인터페이스를 제공해준다
스프링이 실행 될때 jpaRepository 인터페이스를 상속받은 인터페이스가 스캔이 되면, 자동으로 bean으로 등록해준다
repository 수정
package com.sparta.springprepare.repository;
import com.sparta.springprepare.dto.MemoRequestDto;
import com.sparta.springprepare.dto.MemoResponseDto;
import com.sparta.springprepare.entity.Memo;
import jakarta.persistence.EntityManager;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.jdbc.support.KeyHolder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.List;
@Component
public class MemoRepository
{
private final JdbcTemplate jdbcTemplate;
public MemoRepository(JdbcTemplate jdbcTemplate)
{
this.jdbcTemplate = jdbcTemplate;
}
public Memo save(Memo memo)
{
KeyHolder keyHolder = new GeneratedKeyHolder(); // 기본 키를 반환받기 위한 객체
String sql = "INSERT INTO memo (username, contents) VALUES (?, ?)";
jdbcTemplate.update( con -> {
PreparedStatement preparedStatement = con.prepareStatement(sql,
Statement.RETURN_GENERATED_KEYS);
preparedStatement.setString(1, memo.getUsername());
preparedStatement.setString(2, memo.getContents());
return preparedStatement;
},
keyHolder);
// DB Insert 후 받아온 기본키 확인
Long id = keyHolder.getKey().longValue();
memo.setId(id);
return memo;
}
public List<MemoResponseDto> findAll()
{
// DB 조회
String sql = "SELECT * FROM memo";
return jdbcTemplate.query(sql, new RowMapper<MemoResponseDto>() {
@Override
public MemoResponseDto mapRow(ResultSet rs, int rowNum) throws SQLException {
// SQL 의 결과로 받아온 Memo 데이터들을 MemoResponseDto 타입으로 변환해줄 메서드
Long id = rs.getLong("id");
String username = rs.getString("username");
String contents = rs.getString("contents");
return new MemoResponseDto(id, username, contents);
}
});
}
public void update(Long id, MemoRequestDto requestDto)
{
// memo 내용 수정
String sql = "UPDATE memo SET username = ?, contents = ? WHERE id = ?";
jdbcTemplate.update(sql, requestDto.getUsername(), requestDto.getContents(), id);
}
public Memo findById(Long id) {
// DB 조회
String sql = "SELECT * FROM memo WHERE id = ?";
return jdbcTemplate.query(sql, resultSet -> {
if(resultSet.next()) {
Memo memo = new Memo();
memo.setUsername(resultSet.getString("username"));
memo.setContents(resultSet.getString("contents"));
return memo;
} else {
return null;
}
}, id);
}
public void delete(Long id)
{
String sql = "DELETE FROM memo WHERE id = ?";
jdbcTemplate.update(sql,id);
}
@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;
}
}
기존에 jdbc로 만들어진 코드를 지우고.
public interface MemoRepository extends JpaRepository<Memo, Long>
{
}
JpaRepository를 상속받은 인터페이스로 만들어준다
어노테이션을 적용하지않아도 자동으로 빈으로 등록되는데 spring data jpa에 의해 자동으로 bean등록이 되었다.
simplejparepository를 보면 우리가 그동안 날것으로 배운 jpa의 기능이 모두 들어가있다.
그래서 코드를 넣지않아도 웬만한 기능은 수행할 수 있다.
repository를 수정하였으니 service를 수정해보자
package com.sparta.springprepare.service;
import com.sparta.springprepare.dto.MemoRequestDto;
import com.sparta.springprepare.dto.MemoResponseDto;
import com.sparta.springprepare.entity.Memo;
import com.sparta.springprepare.repository.MemoRepository;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class MemoService
{
private final MemoRepository memoRepository;
public MemoService(MemoRepository memoRepository)
{
this.memoRepository = memoRepository;
}
public MemoResponseDto createMemo(MemoRequestDto requestDto)
{
// RequestDto -> Entity
Memo memo = new Memo(requestDto);
// DB 저장
Memo saveMemo = memoRepository.save(memo);
// Entity -> ResponseDto
MemoResponseDto memoResponseDto = new MemoResponseDto(memo);
return memoResponseDto;
}
public List<MemoResponseDto> getMemos()
{
return memoRepository.findAll();
}
public Long updateMemo(Long id, MemoRequestDto requestDto)
{
Memo memo = memoRepository.findById(id);
// 해당 메모가 DB에 존재하는지 확인
if(memo != null) {
memoRepository.update(id, requestDto);
return id;
} else {
throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
}
}
public Long deleteMemo(Long id)
{
// 해당 메모가 DB에 존재하는지 확인
Memo memo = memoRepository.findById(id);
if(memo != null) {
// memo 삭제
memoRepository.delete(id);
return id;
} else {
throw new IllegalArgumentException("선택한 메모는 존재하지 않습니다.");
}
}
}
public List<MemoResponseDto> getMemos()
{
return memoRepository.findAll().stream().map(MemoResponseDto::new).toList();
}
getmemos는 위와같이 변경한다.
private Memo findMemo(Long id) {
return memoRepository.findById(id).orElseThrow(() ->
new IllegalArgumentException("Memo not found")
);
}
find memo 메서드가 많이 쓰이는것을 알 수 있다 그걸 빼면 위와같이 뺄 수 있고.
public Long updateMemo(Long id, MemoRequestDto requestDto) {
Memo memo = findMemo(id);
// 해당 메모가 DB에 존재하는지 확인
memo.update(requestDto);
return id;
}
jpa에서는 update가 없는것을 배웠다 더티체킹을 이용한 수정방법을 적용해야한다. 따라서, Memo entity에서 만든 메서드를 이용해 수정을 진행한다.
@Transactional
public Long updateMemo(Long id, MemoRequestDto requestDto) {
Memo memo = findMemo(id);
// 해당 메모가 DB에 존재하는지 확인
memo.update(requestDto);
return id;
}
update에 transaction이 존재해야 변경감지를 할 수 있다.
Jpa Auditing 적용하기 저장된 시간 수정된 시간을 적용하고싶다. 어떻게 해야할까?
이렇게 하면될까? 코드변경 > 로직변경까지 해야한다..
@MapedSuperclass : Entity클래스가 해당 추상클래스를 상속할경우 멤버변수를 컬럼으로 가질 수 있다 @EntityListeners(AuditingEntityListener.class) : 해당 클래스에 auditing 기능을 제공한다 (자동으로 시간을 등록한다 ) @CreatedDate : 객체가 생성된 시간을 저장한다 해당 컬럼의 updatable = false로 하면 변경이되지않는다 @LastModifiedDate : 조회한 값의 데이터를 변경할때 변경된 시간을 저장한다 @Temporal : date나 calendar과 같은 날짜를 매핑할때 쓴다.
@EnableJpaAuditing
@SpringBootApplication
public class SpringPrepareApplication {
public static void main(String[] args) {
SpringApplication.run(SpringPrepareApplication.class, args);
}
}
그리고 springbootApplication 어노테이션이 적용된 메인에서 EnableJpaAuditing 어노테이션을 적용한다.
public class Memo extends Timestamped{
메모는 timestamped를 상속받는다 메모는 timestamped의 필드도 적용되었다.