작성
·
1.5K
1
질문이 있어 남깁니다.
30. 회원 애플리케이션 기능 추가 강의 중 Activate 메서드를 작성하면서 설명해주신 Spring Data Jpa 사용시 save를 사용해야 한다고 공식문서에 나와있다고 하셨는데 해당 문서에 대한 링크를 알수있을까요?
save를 안티패턴이라고도 설명을하고 불필요한 오버헤드 발생에 대해서는 어떻게 생각하시나요?
강의의 내용 처럼 JpaRepository가 아닌 Repository를 사용 하는경우에는 필수적으로 save를 해야 하나 JpaRepository의 경우는 Jpa 자체에서는 save라는 것이 없기 때문에 새로운 엔티티를 생성할때만 사용을하고 업데이트의 경우는 생략을 해야하는 것일까요?
답변 3
15
안녕하세요. 좋은 질문을 해주셔서 감사합니다.
우선 생각해볼 것은 JPA에는 Spring Data에서 쓰는 save()가 없습니다. JPA 관점에서 save()라는 메소드 자체가 스펙과 권장 사용 패턴과 맞지 않습니다. JPA에 충실하고 싶은 개발자가 보기에 save() 자체가 안티패턴입니다. 안티패턴이라는 게 그냥 자신의 생각이나 취향과 다른 걸 부르는 말이라면요.
JPA 스펙에는 명확하게 persist()와 merge() 두 개 뿐입니다. 이건 SQL 관점과는 또 다릅니다. insert, update가 아니죠. 그렇게 보면 RDB/SQL 입장에서 JPA는 이상하고, 안티패턴이라고 부를 수도 있습니다.
비록 최종적으로 RDB의 SQL로 변환되지만, 이렇게 추상화 단계가 하나씩 쌓이면서 사용하는 패턴이 새로 생기는 것은 각각이 제공하려고 하는 유익이 있기 때문입니다. 이걸 원하면 그 방식을 사용하면 되겠죠.
JPA의 persist()는 비영속 객체를 영속화 시키는 것이고 merge()는 여러가지 기능이 있지만 파라미터로 전달되는 엔티티의 상태가 무엇이든 상관없이 이를 영속 컨텍스트에 집어넣는 것이 목적입니다. 실제로는 준영속 객체일 수도 이는 엔티티를 영속화 시키려는 것이 목적이죠. 각각 insert, update가 일어날 가능성이 있지만 JPA 관점으로 본다면 그렇게 생각하기 보다는 엔티티의 상태 관점으로 보는 것이 바람직합니다.
JPA가 이렇게 두 가지 분명한 표준 메소드를 가지고 있음에도 Spring Data JPA에는 save() 하나 밖에 없습니다. 두 가지 이유가 있습니다. Spring Data JPA는 Spring Data라는 저장 기술의 추상화 위에 만들어진 프레임워크이기 때문에 Spring Data의 리포지토리 추상화를 지켜야 합니다. Spring Data는 등록, 수정이라는 별개의 목적을 위한 작업을 구분하지 않고, 영속화 또는 저장이라는 관점에서 save() 하나만 정의되어 있고, 이건 각 기술의 구현을 이용해서 필요한 기능을 수행하도록 만들어져 있습니다.
Spring Data JPA의 경우 save()에서 대략 4가지 정도의 테스트를 통해서 주어진 엔티티의 상태를 판단하고 new라고 판단되면 JPA의 persist(), 아니면 merge()를 호출하고 리턴합니다. 사실 이게 모든 엔티티 설계와 상태를 완벽하게 커버해주지 못하기 때문에 필요한 경우 JPA에는 없는 Persistable 인터페이스를 구현하고 isNew() 메소드를 통해서 지금 새 엔티티인지를 직접 Spring Data JPA에게 알려줘야 하기도 합니다. 어쨌든 Spring Data JPA는 그 자체로 JPA가 아니며, JPA의 관점으로 모든 사용 방식을 결정할 수 없습니다. 물론 JPA를 내부에서 이용하고 있지만요.
Spring Data JPA의 레퍼런스 문서 https://docs.spring.io/spring-data/jpa/reference/jpa/transactions.html 를 보시면 이런 예가 나옵니다.
@Service
public class UserManagementImpl implements UserManagement {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
public UserManagementImpl(UserRepository userRepository,
RoleRepository roleRepository) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
}
@Transactional
public void addRoleToAllUsers(String roleName) {
Role role = roleRepository.findByName(roleName);
for (User user : userRepository.findAll()) {
user.addRole(role);
userRepository.save(user);
}
}
}
제가 다른 개발자가 이런 식으로 코드를 작성한 걸 봤으면 리뷰에서 지적을 했을 겁니다. User의 addRole()은 아마도 양방향 관계로 되어있고 그 내부에서 role.addUser(user)를 하고 컬렉션에도 추가해주거나, 단방향으로 User가 오너인 many to many로 되어있을 겁니다. 뭐가 됐든 User와 Role 두 가지 다 리포지토리에서 방금 꺼내왔으니 영속 상태일 거고 그 관계가 변경되면 알아서 DB에 반영이 될 겁니다. 둘 다 영속 상태이니 JPA의 merge()가 호출될 필요조차 없겠죠.
그럼에도 Spring Data JPA 세부 기술을 설명하는 독립된 문서에서(이건 Spring Data Commons 문서가 아닙니다) userRepository.save(user)를 사용해서 User에 변경이 있으니 리포지토리에 저장을 하는, JPA를 잘 안다는 분들이 보면 안티패턴이라고 생각할만한 코드가 나오죠.
저도 처음엔 좀 의아해 했는데 이어지는 설명을 보고 이해가 됐습니다.
Note that the call to
save
is not strictly necessary from a JPA point of view, but should still be there in order to stay consistent to the repository abstraction offered by Spring Data.
순수하게 JPA 관점에서 save()가 꼭 필요한 것은 아니지만, Spring Data가 제공하는 리포지토리 추상화에 대한 일관성을 지키기 위해서 save()는 여기 있어야 한다라고 되어있죠.
기술적으로 있어도 된다고 할 수 있습니다. 어짜피 merge()가 호출될 거고, merge()는 이미 영속 컨텍스트라면 그냥 무시합니다. 에러를 내지 않습니다. 무시하는 것도 merge() 스펙에 나오는 기능의 하나입니다.
기술적으로 문제가 되지 않더라도 쓸모 없는 코드이다라고 JPA 잘 모르는 사람이나 이러는 거지라고 생각할 수 있습니다. 성능 얘기는 여기서 솔직히 아무 의미없습니다. 그거 체크해서 무시하는 작업은 너무 가벼운 작업이라 성능에 아무런 의미있는 영향을 주지 않습니다.
그럼에도 save()를 사용하는 것이 JPA를 쓰겠다가 아니라 Spring Data JPA를 쓰겠다고 결정한 개발자에게는 일관성을 지키고, Spring Data의 철학을 따르겠다는 면에서는 중요하다고 생각합니다. 굳이 공식 레퍼런스 문서에 저런 예를 들어서 save()가 있어야 한다고 설명한 이유가 있는 것이죠.
JPA가 아닌 Spring Data JPA를 쓰기 때문에 save()라는 다소 불편할 수도 있는 메소드를 persist, merge 대신 쓰기도 하자나요. JPA는 이래야해라고 주장하는 사람들은 그건 왜 안티패턴이라고 안 부르는지 모르겠습니다. 이미 save()를 쓰기 시작한 순간부터 우리는 JPA 방식과는 다른 접근 방법을 선택한 것입니다.
그럼에도 안 쓸 수도 있겠죠. Spring Data JPA를 쓰는데 왜 JPA 관점에서 필요하지 않은 상황이지만 변경이 일어나는 경우에 save()를 안 쓴다고 에러가 나거나 누가 잡아가지 않을 겁니다. 이걸 써야 한다라고 강조하는 사람도 별로 없을 겁니다. 그럼에도 save()를 꼭 사용하자고 이야기하는 또 한가지 이유가 있습니다.
이것도 역시 Spring Data이기 때문인데요. 아마도 두 번째 주제를 다룰 때 나올 것인데, Spring Data가 주는 놀라운 기능 중에서 도메인 이벤트를 순수하게 도메인 레벨에서 관리하게 하는 기능이 있습니다. 엔티티에서 이벤트를 발행하면, 이게 결국 스프링 인벤트 시스템을 타고 동작하게 되죠. 그래서 도메인 로직과 도메인 이벤트 발행이 분리되는 처참한 코드를 피할 수 있습니다. 매우 응집도가 높은 코드가 되죠.
그런데 이런 Spring Data의 도메인 이벤트 기능을 사용할 때 반드시 필요한 것이 최초 등록 뿐 아니라 수정이 일어나는 경우에도 반드시 리포지토리의 save()를 호출해야 합니다. save()가 실행되는 메카니즘을 이용해서 이벤트가 발행됐는지 체크하고 이를 처리하는 방식이 꼭 필요하기 때문입니다.
그래서 도메인 이벤트와 같은 훅킹 방식이 필요한 경우에는 save()를 매번 호출하라고 하는데, 이것 때문에 save()를 최총 등록 외에는 안 썼던 개발자들은 그걸 집어넣느라고 고생을 합니다. 그러다 빼먹어서 도메인 이벤트 처리를 놓치는 버그가 생기기도 하죠.
그런데 애초부터 Spring Data JPA의 가이드대로 엔티티의 변경이 일어날 때도 자연스럽게 save()를 호출하게 만들었다면, 그런 걱정을 할 필요가 없겠죠.
결국 이 강의 시리즈에서 도메인 이벤트의 사용을 적극적으로 활용할 것이기 때문에 save()의 사용은 Spring Data JPA의 철학과 Spring Data 추상화의 일관성을 지키는 사용기술의 가이드 대로 작성하는 세련된 코드가 되면서 동시에 도메인 이벤트 등 Spring Data가 제공하는 부가 기능 사용도 자연스럽게 이용할 수 있게 해주는 두 가지 효과가 있습니다. 안 쓸 이유가 없습니다.
수정 상황에서 Spring Data JPA의 save() 호출하는 걸 가지고 안티패턴이라고 하는 분이 있다면, 그 분은 JPA도 잘 모르고 Spring Data JPA도 잘 모르기 때문이라고 생각하고 넘어가시면 됩니다.
더 궁금하신 게 있으면 알려주세요.
1
아 그리고 save()는 Spring Data Commons에 있는 CrudRepository에 있는 것이고, Repository 인터페이스를 직접 사용하는 경우에도 save()를 쓰면 관례를 따라 CrudRepository를 쓰는 것과 동일하게 처리됩니다. 그때문에 달라지는 건 없습니다.
0
안녕하세요, 인프런 AI 인턴입니다. 홀리몰리님!
Spring Data JPA에 대한 질문을 해주셨네요. 일반적으로 JpaRepository
의 save
메서드는 엔티티를 저장하거나 업데이트할 때 사용됩니다. 공식 문서에서 save
메서드를 사용하는 것에 대한 자세한 설명은 Spring Data JPA Reference Documentation를 참조하실 수 있습니다.
1. Spring Data Jpa와 save 메서드의 사용
Spring Data JPA는 save
메서드를 통해 새로운 엔티티의 저장뿐 아니라 기존 엔티티의 업데이트도 처리합니다. save
메서드 자체는 saveAndFlush
와 달리 명시적으로 즉시 데이터베이스에 반영하지 않을 수 있으며, 트랜잭션 내에서 변경이 발생했을 때 자동으로 반영되도록 되어 있습니다.
2. save
메서드를 안티패턴이라고 할 때와 이에 대한 오버헤드
엔티티를 매번 save
하는 것은 때때로 불필요한 데이터베이스 방문을 초래할 수 있습니다. Spring Data JPA는 트랜잭션 내에서 변경 감지(Dirty Checking)를 사용하여, 엔티티가 변경된 경우에만 실제 업데이트 쿼리를 발생시키므로 save
메서드를 매번 호출하지 않아도 됩니다. 따라서, 불필요하게 전체 엔티티를 save
하는 대신에 적절히 변경 감지 기능을 활용하는 것이 좋습니다.
3. JpaRepository와 다른 Repository 인터페이스의 차이
JpaRepository
인터페이스는 Spring Data JPA의 일부분으로, 기본적인 CRUD 기능을 제공합니다. 새롭게 생성된 엔티티를 저장할 때는 save
메서드를 사용하고, 기존 엔티티의 변화를 반영할 때는 변경 감지를 신뢰할 수 있습니다. 만약 save
가 항상 불필요한 경우라면, 직접 JPQL을 사용하거나 EntityManager API를 직접 관리하여 JPA의 기능을 더욱 세밀하게 조정할 수 있도록 코드 설계를 하는 것도 고려해볼 수 있습니다.
추가적인 설명은 아래 유사한 질문에서 참고하실 수 있습니다:
해당 문서와 강의 내용을 참고하여 조금 더 깊이 있는 이해에 도움이 되시길 바랍니다. 궁금한 점이 더 있으시면 언제든지 질문해 주세요.
저는 질문자님의 이해를 돕고자 지식공유자님의 기존 답변을 참고하여 유사한 답변 링크를 추천해드리고 있습니다.
현재 베타 기능으로 답변이 만족스럽지 않을 수 있는 점 양해 부탁드립니다. 🙏
추가적으로 궁금한 점이 있으시면, 이어서 질문해 주세요. 곧 지식공유자께서 답변해 주실 것입니다.
정말 너무 좋은답변을 주셔서 감사합니다.
Spring Data와 Spring Boot 프레임워크를 사용하는 상황에서는, 스프링이 추구하는 방향성에 맞춰 개발하는 것이 사이드 이펙트를 줄이는 데 유리하다는 점을 알게 되었습니다.이는 프레임워크의 일관된 구조와 동작 방식을 따름으로써, 예기치 않은 문제를 줄일 수 있다라는 생각이 듭니다.
다시 한 번 정성스러운 답변 감사합니다.