JPA 기초
제목을 JPA 기초라고 하긴 했으나 이 포스팅에서는 정말 기본적인 설정과 어노테이션 정도만 포스팅. 예제 환경 Intelli J SpringBoot 2.6.2 Lombok Gradle 7.3.2 JPA는 의존성 추가를 해야 사용할 수 있다. Gradl
myyoun.tistory.com
이전 포스팅인 상단 기초에서 아주 간단한 설정 및 엔티티 어노테이션, 그리고 앞으로 사용하게 될 메소드가 정의되어 있는 위치를 확인했다.
Repository 인터페이스를 생성하고 거기서 상속받는 JpaRepository는 PagingAndSortingRepository를 상속받으며 이 인터페이스 또한 CrudRepository라는 인터페이스를 상속받는다.
JpaRepository를 상속받지 않고 CrudRepository를 상속받아도 될 정도로 많은 메소드가 정의되어 있다고 했는데
공부한 내용은 JpaRepository를 상속받아서 사용했고 CrudRepository안에 있는 메소드들이 JpaRepository를 통해서도
사용할 수 있기 때문에 JpaRepository만 뜯어본다!
일단 공부한 환경은 아래와 같다.
- Intelli J
- SpringBoot 2.6.2
- Lombok
- Gradle 7.3.2
JpaRepository와 CrudRepository에 주석으로 각 메소드들에 대한 설명들이 적혀 있지만 좀 정리하고자 한다.
이 포스팅에서는 아래 메소드들만 정리.
- save()
- saveAll()
- saveAndFlush()
- saveAllAndFlush()
- getOne()
- findById()
- findAll()
- findAllById()
save()
save()는 의미 그대로 엔티티를 저장하고자 할 때 사용하는 메소드이다.
가장 기초적으로 사용하는 방법은 아래와 같다.
@Data
@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor
@Entity
public class User{
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NonNull
private String name;
@NonNull
private String email;
}
public interface UserRepository extends JpaRepository<User, Long>{
}
@SpringBootTest
class UserRepositoryTest{
@Autowired
private UserRepository userRepository;
@Test
void crudTest(){
User user = new User();
user.setName("coco");
user.setEmail("coco@gmail.com");
userRepository.save(user);
}
이렇게 save 메소드는 User 엔티티를 받아 저장해준다.
강의에서는 H2-in-memory DB를 사용했고 별도로 MySQL에서도 테스트했기 때문에 User Entity의 GeneratedValue는
IDENTITY로 설정.
이 테스트를 실행하게 되면 User 테이블에 name = 'coco', email = 'coco@gmail.com' 이렇게 insert 된다.
그리고 Id의 경우는 GeneratedValue에 의해 1씩 증가하며 저장되게 된다.
그리고 CrudRepository안에 있는 메소드들을 보면 update에 대한 메소드는 보이지 않는다.
Create는 save, Read 는 find, Delete는 delete로 메소드가 정의되어 있는것을 볼 수 있는데 Update 메소드는 찾을 수 없다.
그 Update에 대한 처리를 진행하는 것이 save 메소드의 역할 중 하나이기 때문이다.
save 메소드는 새로운 데이터를 넣는 경우 insert로 동작하게 되지만 기존에 있는 데이터를 넣게 되면 update 쿼리를 실행하게 된다.
@SpringBootTest
class UserRepositoryTest{
@Autowired
private UserRepository userRepository;
@Test
void crudTest(){
User user = new User();
user.setName("coco");
user.setEmail("coco@gmail.com");
//insert
userRepository.save(user);
//update
User user2 = userRepository.findById(1L).orElseThrow(RuntimeException::new);
user2.setEmail("coco1@gmail.com");
userRepository.save(user2);
}
이 테스트를 실행하면 name='coco', email='coco@gmail.com' 이라는 데이터가 저장된다.
그리고 user2 에서 사용한 findById는 Id값으로 데이터를 찾아온다는 것인데 where id = 1 이렇게 생각하면 쉽다.
현재 User Entity에서 Id는 id라는 Long 타입의 필드로 명시되어 있기 때문에 Long 타입으로 1L을 넣어주는것.
orElseThrow의 경우는 Jpa에서 Repository에서 리턴타입을 Optional로 받을 수 있도록 지원하기 때문이고
Optional은 null로 인한 예외 처리를 해줘야 한다.
즉, userRepository.findById(1L)은 리턴 타입이 Optional<T> 이기 때문에 null이었을 때 발생할 예외처리를 해줘야 한다는 것이다.
강의에서 orElseThrow를 계속 사용하셨기에 동일하게 작성했지만 orElse(), orElseGet() 등 여러 Optional 메소드가 존재한다.
이것은 따로 정리가 필요...
일단 Optional에 대한 포스팅은 하단 Reference에서 확인.
그럼 코드를 다시 보자면 user2는 findById로 Id가 1L인 데이터를 가져왔기 때문에 테스트 초반 저장된
name='coco', email='coco@gmail.com' 을 의미하게 된다.
그리고 setEmail로 coco1@gmail.com 으로 데이터를 변경해준 뒤 save를 해주게 되면 update가 수행되게 된다.
이 때 로그를 확인해 쿼리를 확인하게 되면 insert로 user 테이블에 데이터를 저장해준 뒤
select로 조회를 2번 진행한다. 그리고 update 쿼리가 수행된것을 볼 수 있다.
첫번째 save(user)로 인해 insert 쿼리가 실행되고 findById(1L)로 select 쿼리가 한번 실행된 후에
save(user2)가 실행되면서 select 쿼리로 조회한번 한 뒤에 update 쿼리가 실행되는 것이다.
코드만 봤을때는 동일한 save이지만 insert와 update를 알아서 구분해 처리해준 것이다.
어떻게 이렇게 수행되는지 보려면 SimpleJpaRepository에서 확인해야 한다.
Intelli J - window 기준 Shift 두번 눌러서 SimpleJpaRepository 검색하면 들어가서 볼 수 있다.

확인해야 하는 부분은 이 부분.
그리고 SimpleJpaRepository는 JpaRepositoryImplementation을 재정의 하고 있고
JpaRepositoryImplementation은 JpaRepository를 상속받고 있다.
즉, JpaRepository에 정의된 메소드들은 SimpleJpaRepository에서 구현체를 제공하고 있다는 것이다.
save()메소드는 제일 먼저 null 체크를 하게 되는데 entity로 받은 인자가 null이면 오류가 발생하게 된다.
그리고 if문으로 isNew 즉, 새로운 데이터면 em(Entity Manager)에서 insert(persist)를 수행한다.
새로운 데이터가 아닌 경우는 Entity Manager에서 update(merge)를 수행하도록 구현되어 있다.
isNew라는 것은 주어진 Entity가 새로운것인지에 대한 여부이기 때문에 코드 그대로
새로운 데이터라면 persist를, 존재하는 데이터라면 merge를 수행하도록 구현한 것이다.
isNew에 대해 좀 더 보기 위해 AbstractPersistable에 들어가서 보게 되면
isNew에 대한 리턴은 null == getId가 된다.
entity에서 Id로 지정해둔 필드가 null 이라면 새로운 데이터라는 조건이 되는것이고
null이 아니라면 존재하는 데이터이기 때문에 update를 처리하도록 하는것이다.
saveAll()
saveAll() 메소드는 의미 그대로 단일 데이터가 아닌 복수의 데이터를 저장하는것이다.
Lists.newArrayList를 통해 저장할 수 있다.
@SpringBootTest
class UserRepositoryTest{
@Autowired
private UserRepository userRepository;
@Test
void crudTest(){
User user = new User();
user.setName("coco");
user.setEmail("coco@gmail.com");
User user2 = new User();
user.setName("mozzi");
user.setEmail("mozzi@gmail.com");
userRepository.saveAll(Lists.newArrayList(user, user2);
}
saveAndFlush(), saveAllANdFlush()
saveAndFlush는 save와 flush를 수행하는 메소드이다.
save는 위에서 설명했으니 생략하고 flush에 대해 먼저 간단하게 설명.
Transaction commit이 일어날 때 flush가 동작하게 되는데 이때 insert, update, delete SQL들이 DB로 넘어간다.
이때 영속성 컨텍스트(Persistence Context)를 비우는것은 아니고 DB와 동기화 한다고 이해하면 된다.
예를들어 user테이블에 insert한 뒤에 다시 set으로 내용을 수정한다면 수정된 Entity를 쓰기 지연 SQL 저장소에 등록해 두었다가 이 쿼리를 DB에 전송한다. 전송이 발생하는것을 flush가 발생한 시점이라고 볼 수 있고 flush가 일어난 다음에 실제 commit이 일어나게 된다.
flush에 대한 자세한것은 Reference에서 참고.
flush는 이러한 동작을 하게 되지만 saveAndFlush에 있는 flush는 DB에 업데이트를 하는 flush가 아니라
영속성 컨텍스트내의 한 공간에 flush를 해 저장해 두었다가 Transaction이 종료되는 시점에 DB에 업데이트하는 형태다.
결과만 놓고 봤을때는 save() 메소드와 동일한 결과를 보여준다.
하지만 과정에서의 차이가 존재하는건데 하나의 테스트에서 save()나 saveAndFlush()를 한번 한 뒤에 데이터를 수정했을때, 그 과정에서의 차이가 발생한다.
최초 insert의 과정은 무조건 Context내의 한 공간에 업데이트가 되는것은 동일하다.
이 한 공간을 space라고 임의로 정했을 때, saveAndFlush()를 통해 업데이트를 진행하게 되면 매번 그 업데이트 쿼리를 space에 보내고 Transaction이 종료되는 시점에 DB에 업데이트를 하게 된다.
하지만 save는 Context에 마지막으로 존재하는 형태의 데이터를 query로 만들어서 Transaction 종료 시점에 DB에 업데이트 하게 된다.
즉, save는 context에 마지막으로 남아있는 데이터만 넘겨줄 쿼리를 한번 생성해 업데이트를 해주지만
saveAndFlush는 업데이트가 한번 발생할때마다 space에 쿼리를 만들어 보내놓고 처리하는 과정인것.
그렇기 때문에 효율성 측면에서는 save가 flush보다 더 낫다고 한다.
하지만 그렇다고 saveAndFlush가 무조건 더 안좋은것은 아니고 환경 혹은 때에 따라 saveAndFlush가 더 좋은 경우가 있다고 한다.
flush에 대해 아직 깊게 알지 못하기 때문에 이부분은 더 공부가 필요...
그리고 saveAndFlush에 대해 테스트를 많이 진행해서 그 과정을 포스팅 해주신 분들이 많으니
Reference를 참고해 볼것.
saveAllAndFlush는 saveAll()과 save()의 차이처럼 한번에 여러개의 데이터를 처리하는 차이이므로 설명은 생략.
getOne()
getOne() 메소드는 Id(primary key)를 통해 매칭되는 하나의 객체를 가져오는 메소드다.
결과만 놓고 봤을때는 findById() 메소드와 동일한 결과를 보여주는데 getOne은 내부적으로
EntityManager.getReference() 를 통해 엔티티를 가져오게 되어있다.
LazyLoading을 지원하고 호출되는 시점에는 일단 Proxy를 가져오며 실제로 가져온 Entity의 속성에 접근하는
순간 DB에 접근하는 방식을 사용한다.
@SpringBootTest
class UserRepositoryTest{
@Autowired
private UserRepository userRepository;
@Test
@Transactional
void crudTest(){
User user = userRepository.getOne(1L);
System.out.println(user);
}
findById()
findById() 메소드는 getOne처럼 Id를 통해 매칭되는 하나의 객체를 가져오는 메소드이다.
getOne은 Proxy를 먼저 가져온 후에 속성에 접근하는 순간 DB에 접근하는 방식이라고 했는데
findById는 DB를 바로 조회해서 필요한 데이터를 가져온다. 그래서 반환되는 객체 역시 데이터가 매핑되어 있는 실제
Entity 객체이다.
@SpringBootTest
class UserRepositoryTest{
@Autowired
private UserRepository userRepository;
@Test
@Transactional
void crudTest(){
Optional<User> user = userRepository.findById(1L);
System.out.println(user);
User user2 = userRepository.findById(1L).orElse(null);
// orElse는 값이 있다면 값을 반환하고 없다면
// ( ) 안에 명시한 다른 값을 반환한다.
// 단 " " 를 통한 문자열 반환은 할 수 없다.
System.out.println(user2);
}
위와 같은 형태로 사용할 수 있는데 findById는 Optional 타입으로 사용하거나 Entity 타입으로 사용할 수 있다.
findAll()
findAll() 메소드는 해당 테이블의 모든 데이터를 List로 가져오는 메소드이다.
쿼리문으로 보면 Select * from User 이런 느낌.
@SpringBootTest
class UserRepositoryTest{
@Autowired
private UserRepository userRepository;
@Test
@Transactional
void crudTest(){
userRepository.findAll().forEach(System.out::println);
//forEach의 경우 가져온 리스트를 라인별로 출력하기 위해 사용한것.
//name을 기준으로 내림차순 정렬
List<User> users = userRepository.findAll(Sort.by(Sort.Direction.DESC, "name"));
users.forEach(System.out::println);
}
이렇게 테이블의 모든 데이터를 가져올 수 있으며 Sort를 추가해 정렬된 데이터를 가져올 수 있다.
그래서 쿼리문 역시 order by user_.name desc 로 동작하는것을 로그에서 볼 수 있다.
findAllById()
findAllById는 여러개의 Id값을 통해 해당 데이터들을 조회하는 메소드이다.
예를들어 각각 1, 3, 5라는 Id값을 갖고 있는 3개의 데이터를 가져오려고 할때 사용한다.
@SpringBootTest
class UserRepositoryTest{
@Autowired
private UserRepository userRepository;
@Test
@Transactional
void crudTest(){
//Id 값이 1, 3, 5인 데이터를 가져오기
List<User> users = userRepository.findAllById(Lists.newArrayList(1L, 3L, 5L);
users.forEach(System.out::println);
//아래와 같이 Id 값을 처리할 수도 있다.
List<Long> ids = new ArrayList<>();
ids.add(1L);
ids.add(2L);
ids.add(3L);
List<User> users2 = userRepository.findAllById(ids);
users2.forEach(System.out::println);
}
이렇게 실행하게 되면 각각 1, 3, 5의 Id 값을 갖고 있는 3개의 데이터가 출력된다.
그리고 로그에서 쿼리문을 보면 where에서 in절을 통해 가져오는것을 볼 수 있다.
Reference
- 패스트캠퍼스 java/spring 초격차 패키지 Spring Data JPA
- Optional
[Java] Optional 관련..
Spring Data JPA 사용 시 Repository에서 리턴 타입을 Optional로 바로 받을 수 있도록 지원하고 있습니다.Optional을 사용하면 반복적인 null 체크를 줄일 수 있기 때문에 잘 사용하면 매우 편리한 것 같습니
velog.io
- flush
[JPA] 영속성 컨텍스트와 플러시 이해하기
영속성 컨텍스트 JPA를 공부할 때 가장 중요한게 객체와 관계형 데이터베이스를 매핑하는 것(Object Relational Mapping) 과 영속성 컨텍스트를 이해하는 것 이다. 두가지 개념은 꼭 알고 JPA를 활용하자.
ict-nroo.tistory.com
- save() 와 saveAndFlush()의 차이
[JPA] save 와 saveAndFlush의 차이
OS : MacOs Mojave DB : MySQL 5.7 DB Tool : Sequel Pro Framework : Spring Boot 2.0 맨밑에 결론있음 1. 준비 다음과 같은 member Entity를 준비했다. public class Member { @Id @GeneratedValue(strategy =..
ramees.tistory.com
'Spring' 카테고리의 다른 글
JPA JpaRepository 메소드 paging (findAll(pageable)) (0) | 2022.02.03 |
---|---|
JPA JpaRepository 메소드 (count(), existsById~(), delete~()) (0) | 2022.01.31 |
JPA 기초 (0) | 2022.01.26 |
JPA란? (0) | 2022.01.13 |
IOC(Inversion Of Control) (0) | 2021.02.21 |