[TIL] 230520 <Spring> Spring Data JPA
- -
[SpringBoot 환경에서의 JPA]
- SpringBoot 환경에서는 EntityManagerFactory와 EntityManager를 자동으로 생성해줌
 ㄴ application.properties에 DB 정보를 전달해 주면 이를 토대로 EntityManagerFactory가 생성됨
- @PersistenceConext 애너테이션을 사용하면 자동으로 생성된 EntityManager를 주입받아 사용가능
@PersistenceContext
EntityManager em;
▶ build.gradle : spring-boot-starter-data-jpa 추가
// JPA 설정
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'▶ application.properties : Hibernate 설정
- show_sql, format_sql, use_sql_comments 옵션
- Hibernate가 DB에 요청하는 모든 SQL을 보기좋게 출력
 
- ddl-auto
- create : 기존 테이블 삭제 후 다시 생성 (DROP + CREATE)
- create-drop : create와 같으나 종료시점에 테이블을 DROP
- update : 변경된 부분만 반영
- validate : Entity와 테이블이 정상 매핑되었는지만 확인
- none: 아무것도 하지 않음
 
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=trueSpring의 트랜잭션
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 옵션으로 적용됨
- readOnly = true 옵션
- 트랜잭션에서 데이터를 읽기만 할 때 사용
- 이 속성을 사용하면 읽기 작업에 대한 최적화를 수행가능
- 만약, 해당 트랜잭션에서 데이터를 수정하려고 하면 예외가 발생하기 때문에 주의
 
 
@Transactional

TransactionTest
package com.sparta.memo;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
import com.sparta.memo.entity.Memo;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
@SpringBootTest
public class TransactionTest {
	@PersistenceContext
	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 객체를 저장합니다.
	}
}ㄴ 트랜잭션이 적용되어 DB 작업이 성공
- JPA를 사용하여 DB에 데이터를 저장, 수정, 삭제 하려면 트랜잭션 적용이 반드시 필요
- 조회 작업은 단순하게 데이터를 읽기만 하기 때문에 트랜잭션 적용이 필수는 아님
- 다만 조회의 경우에도 트랜잭션 환경이 필요한 경우가 있을 수 있기 때문에
 조회 작업 기능만 존재하는 메서드일 경우에만 앞서 본 예시처럼 readOnly = true 옵션이 설정된 @Transactional을 적용
 
- 다만 조회의 경우에도 트랜잭션 환경이 필요한 경우가 있을 수 있기 때문에
영속성 컨텍스트와 트랜잭션의 생명주기

- 스프링 컨테이너 환경에서는 영속성 컨텍스트와 트랜잭션의 생명주기가 일치!
 ㄴ 트랜잭션이 유지되는 동안은 영속성 컨텍스트도 계속 유지가 되기 때문에 영속성 컨텍스트의 기능을 사용 가능
- 따라서 트랜잭션이 적용하지 않으면 영속성 컨텍스트가 유지되지 못해 오류가 발생함
 ⚠️ Spring은 어떻게 Service 부터 Repository 까지 Transaction을 유지?!
ㄴService의 트랜잭션이 적용된 메서드에서 Repository의 메서드를 호출할 때 트랜잭션을 제어할 수 있도록 
   '트랜잭션 전파 기능'을 제공!!!
트랜잭션 전파
- @Transactional에서 트랜잭션 전파 옵션을 지정가능

- 트랜잭션 전파의 기본 옵션은 REQUIRED
 ㄴ REQUIRED 옵션은 부모 메서드에 트랜잭션이 존재하면 자식 메서드의 트랜잭션은 부모의 트랜잭션에 합류하게됨

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)
package com.sparta.memo;
import com.sparta.memo.entity.Memo;
import com.sparta.memo.repository.MemoRepository;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.transaction.annotation.Transactional;
@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
	@Disabled
	@DisplayName("메모 생성 실패")
	void test2() {
		Memo memo = new Memo();
		memo.setUsername("Robbie");
		memo.setContents("@Transactional 테스트 중!");
		em.persist(memo);  // 영속성 컨텍스트에 메모 Entity 객체를 저장합니다.
	}
	@Test
	@Transactional
	@Rollback(value = false)
	@DisplayName("트랜잭션 전파 테스트")
	void test3() {
		memoRepository.createMemo(em);
		System.out.println("테스트 test3 메서드 종료");
	}
}
ㄴ 실행 결과 자식 메서드 createMemo가 종료될 때 update가 실행되는 것이 아니라 
     부모 메서드에 트랜잭션이 합류되면서 부모 메서드가 종료된 후 트랜잭션이 커밋될 때 update가 실행된 것을 확인가능
▼ 부모 메서드 test3의 @Transactional, @Rollback(value = false)을 주석 처리하고 다시 한번 update를 시도해보면 합류할 부모 트랜잭션이 없기 때문에 자식 메서드가 종료된 후 트랜잭션이 커밋되면서 update가 실행된 것을 확인가능

[Spring Data JPA]

- Spring Data JPA는 JPA를 쉽게 사용할 수 있게 만들어놓은 하나의 모듈
 ㄴ JPA를 추상화시킨 Repository 인터페이스를 제공
- Repository 인터페이스는 Hibernate와 같은 JPA구현체를 사용해서 구현한 클래스를 통해 사용됨
 ㄴ 개발자들은 Repository 인터페이스를 통해 JPA를 간편하게 사용가능!
Spring Data JPA의 SimpleJpaRepository

- Spring Data JPA에서는 JpaRepository 인터페이스를 구현하는 클래스를 자동으로 생성해줌
- Spring 서버가 뜰 때 JpaRepository 인터페이스를 상속받은 인터페이스가 자동으로 스캔이 되면,
 해당 인터페이스의 정보를 토대로 자동으로 SimpleJpaRepository 클래스를 생성해 주고, 이 클래스를 Spring ‘Bean’으로 등록함
 
- Spring 서버가 뜰 때 JpaRepository 인터페이스를 상속받은 인터페이스가 자동으로 스캔이 되면,
- 따라서 인터페이스의 구현 클래스를 직접 작성하지 않아도 JpaRepository 인터페이스를 통해 JPA의 기능을 사용가능
Spring Data JPA 사용방법

- JpaRepository<"@Entity 클래스", "@Id 의 데이터 타입">를 상속받는 interface 로 선언!
- Spring Data JPA에 의해 자동으로 Bean 등록됨
- 제네릭스의 @Entity 클래스 위치에 Memo Entity를 추가했기 때문에 
 해당 MemoRepository는 DB의 memo 테이블과 연결되어 CRUD 작업을 처리하는 인터페이스가 되었음!
 
메모장 프로젝트 Spring Data JPA 적용
MemoRepository
public interface MemoRepository extends JpaRepository<Memo, Long> {
}
MemoService
package com.sparta.memo.service;
import java.util.List;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.sparta.memo.dto.MemoRequestDto;
import com.sparta.memo.dto.MemoResponseDto;
import com.sparta.memo.entity.Memo;
import com.sparta.memo.repository.MemoRepository;
@Service
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() {
		// DB 조회
		return memoRepository.findAll().stream().map(MemoResponseDto::new).toList();
	}
	@Transactional
	public Long updateMemo(Long id, MemoRequestDto requestDto) {
		// 해당 메모가 DB에 존재하는지 확인
		Memo memo = findMemo(id);
		// memo 내용 수정
		memo.update(requestDto);
		return id;
	}
	public Long deleteMemo(Long id) {
		// 해당 메모가 DB에 존재하는지 확인
		Memo memo = findMemo(id);
		// memo 삭제
		memoRepository.delete(memo);
		return id;
	}
	private Memo findMemo(Long id) {
		return memoRepository.findById(id).orElseThrow(() ->
			new IllegalArgumentException("선택한 메모는 존재하지 않습니다.")
		);
	}
}▶ save

- SimpleJpaRepository의 save 메서드를 확인해보면 영속성 컨텍스트에 entity를 저장하는 코드가 작성되어있음
- save 메서드를 사용해 데이터를 저장가능
- 파라미터로는 저장하려는 entity 객체를 넣어주면됨
- 해당 메서드에 @Transactional이 적용되어있음
 
▶ findAll

▶ findById

- SimpleJpaRepository의 findById 메서드를 확인해보면 반환 타입이 Optional임
- 파라미터로는 삭제하고자하는 Entity의 id 값을 넣어주면 됨
 
- Optional<Entity 타입>을 반환 타입으로 받고 추가적으로 null을 체크하거나
 위 코드와 같이 orElseThrow를 사용하여 반환 값이 null일 경우 예외를 던지도록 처리가능!
 >> Optional 자료 https://www.baeldung.com/java-optional
▶ update
@Transactional
public Long updateMemo(Long id, MemoRequestDto requestDto) {
    // 해당 메모가 DB에 존재하는지 확인
    Memo memo = findMemo(id);
    // memo 내용 수정
    memo.update(requestDto);
    return id;
}- SimpleJpaRepository에 update라는 메서드는 존재하지 않음!!!
- 따라서 위 코드처럼 영속성 컨텍스트의 변경감지를 통해 update를 진행
- 변경감지가 적용되기 위해 해당 메서드에 @Transactional을 추가하였음
Memo.java의 update 메서드
	public void update(MemoRequestDto requestDto) {
		this.username = requestDto.getUsername();
		this.contents = requestDto.getContents();
	}
▶ delete

- delete 메서드를 사용해 해당 Entity(데이터)를 테이블에서 삭제
- 파라미터로는 삭제하려는 entity 객체를 넣어주면됨
 
- delete 메서드에 @Transactional이 적용되어있는 것을 확인가능
[JPA Auditing 적용하기]
메모장 프로젝트에 JPA Auditing 적용

Timestamped
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;
}1. Timestamped
Spring Data JPA에서는 시간에 대해서 자동으로 값을 넣어주는 기능인 JPA Auditing을 제공!
- @MappedSuperclass
- JPA Entity 클래스들이 해당 추상 클래스를 상속할 경우 
 createdAt, modifiedAt 처럼 추상 클래스에 선언한 멤버변수를 '컬럼'으로 인식하게 해줌
 
- JPA Entity 클래스들이 해당 추상 클래스를 상속할 경우 
- @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
 
 
2. ⚠️ @SpringBootApplication 이 있는 class에 @EnableJpaAuditing 추가!
     ㄴ JPA Auditing 기능을 사용하겠다는 정보를 전달해주기 위해 @EnableJpaAuditing 을 추가해야함
MemoApplication

3. 적용하고자하는 Entity 클래스에서 Timestamped를 상속받기

+) 화면에 시간을 보이게 하기 위해 추가적으로 해줘야 할 것
MemoResponseDto

[Query Methods]
Spring Data JPA에서는 메서드 이름으로 SQL을 생성할 수 있는 Query Methods 기능을 제공

- JpaRepository 인터페이스에서 해당 인터페이스와 매핑되어있는 테이블에, 요청하고자하는 SQL을 메서드 이름을 사용해 선언가능
MemoRepository
public interface MemoRepository extends JpaRepository<Memo, Long> {
    List<Memo> findAllByOrderByModifiedAtDesc();
}- SimpleJpaRepository 클래스가 생성될 때 위처럼 직접 선언한 JpaRepository 인터페이스의 모든 메서드를 자동으로 구현해줌
- JpaRepository 인터페이스의 메서드 즉, Query Methods는 개발자가 이미 정의 되어있는 규칙에 맞게 메서드를 선언하면,
 해당 메서드 이름을 분석하여 SimpleJpaRepository에서 구현이 됨!!
- 따라서 우리는 인터페이스에 필요한 SQL에 해당하는 메서드 이름 패턴으로 메서드를 선언 하기만 하면 따로 구현하지 않아도 사용가능!!
 
- JpaRepository 인터페이스의 메서드 즉, Query Methods는 개발자가 이미 정의 되어있는 규칙에 맞게 메서드를 선언하면,
- 1) findAllByOrderByModifiedAtDesc 해당 메서드 이름은 Memo 테이블에서 ModifiedAt 즉, 수정 시간을 기준으로 전체 데이터를 내림차순으로 가져오는 SQL을 실행하는 메서드를 생성 가능
- 2) List<Memo> findAllByUsername(String username);
- 이렇게 Query Method를 선언했을 경우 ByUsername 에 값을 전달해줘야하기 때문에 
 파라미터에 해당 값의 타입과 변수명을 선언해줌
- 즉, Query Methods 는 메서드의 파라미터를 통해 SQL에 필요한 값을 동적으로 받아 처리가능
 
- 이렇게 Query Method를 선언했을 경우 ByUsername 에 값을 전달해줘야하기 때문에 
MemoService (findAll()에서 findAllByUsername()으로 사용 메서드를 변경하였음)
public List<MemoResponseDto> getMemos() {
    // DB 조회
    return memoRepository.findAllByOrderByModifiedAtDesc().stream().map(MemoResponseDto::new).toList();
}
Query Methods 기능을 사용하여 내용(contents)에 특정 키워드가 포함된 메모를 조회하는 API를 구현하기
Controller를 통해 keyword를 Query String 방식으로 전달받아 메모를 조회하는 API를 추가
- http://localhost:8080/api/memos/contents?keyword=
- 메서드명은 getMemosByKeyword
MemoController
	@GetMapping("/memos/contents")
	public List<MemoResponseDto> getMemosByKeyword(@RequestParam("keyword") String keyword) {
		return memoService.getMemosByKeyword(keyword);
	}  //@RequestParam("keyword") 생략 가능!MemoService
	public List<MemoResponseDto> getMemosByKeyword(String keyword) {
		return memoRepository.findAllByContentsContainsOrderByModifiedAtDesc(keyword).stream().map(MemoResponseDto::new).toList();
	}MemoRepository
public interface MemoRepository extends JpaRepository<Memo, Long> {
	List<Memo> findAllByOrderByModifiedAtDesc();
	List<Memo> findAllByContentsContainsOrderByModifiedAtDesc(String keyword);
}'TIL' 카테고리의 다른 글
| [TIL] 230522 <Spring> JPA 한 걸음 더 나아가기 (0) | 2024.05.22 | 
|---|---|
| [TIL] 230521 <Spring> Bean, 인증과 인가, RestTemplate & Open API (0) | 2024.05.21 | 
| [TIL] 230517 <Spring> 3 Layer Architecture, IoC와 DI, JPA CORE (0) | 2024.05.17 | 
| [TIL] 230516 <Spring> 메모장, Database와 SQL (0) | 2024.05.16 | 
| [TIL] 230514 <Spring> Spring MVC (0) | 2024.05.14 | 
소중한 공감 감사합니다