728x90

Spring Data JPA를 사용하여 유저의 관한 CRUD 연산을 수행해보았다.

 

UserRepository.java

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import usw.suwiki.domain.user.User;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Optional;

@Repository
public interface UserRepository extends JpaRepository<User, Long> {

    Optional<User> findById(Long userIdx);

    Optional<User> findByLoginId(String loginId);

    Optional<User> findByEmail(String email);

    List<User> findByLastLoginBefore(LocalDateTime localDateTime);

    List<User> findByRequestedQuitDate(LocalDateTime localDateTime);

    //loginId, email 입력값 검증
    @Query(value = "SELECT loginId, email FROM User WHERE loginId = :loginId and email = :email")
    String findPwLogicByLoginIdAndEmail(@Param("loginId") String loginId, @Param("email") String email);

    //loginId와 email 에 일치하는 유저의 비밀번호 변경
    @Modifying
    @Query(value = "UPDATE User Set password = :resetPassword WHERE loginId = :loginId and email = :email")
    void resetPassword(@Param("resetPassword") String resetPassword, @Param("loginId") String loginId, @Param("email") String email);

    //User 비밀번호 수정 (마이페이지에서 비밀번호 재 설정)
    @Modifying
    @Query(value = "UPDATE User Set password = :editMyPassword WHERE loginId = :loginId")
    void editPassword(@Param("editMyPassword") String editMyPassword, @Param("loginId") String loginId);

    //User 삭제
    @Modifying
    @Query(value = "DELETE from User WHERE id = :id")
    void deleteUserNotEmailCheck(@Param("id") Long id);

    //격리테이블에 본 테이블 데이터 옮기기
    @Modifying
    @Query(value = "INSERT INTO user SELECT id, login_id, password, email, restricted, role, written_evaluation, written_exam, view_exam_count, point, last_login, requested_quit_date, created_at, updated_at FROM user_isolation WHERE id = :id", nativeQuery = true)
    void insertUserIsolationIntoUser(@Param("id") Long id);

    
    //UserIdx 로 블랙리스트 출소 유저 반영해주기
    @Modifying
    @Query(value = "UPDATE User SET restricted = false WHERE id = :userIdx")
    void unRestricted(@Param("userIdx") Long userIdx);
}

 

@Repository

레포지토리를 명시하며, 컴포넌트 스캔의 대상으로 삽입된다.

 

extends JpaRepository<User, Long>

1. Spring Data JPA 를 사용하기 위해선, 해당 레포지토리는 Interface로 생성되어야 하며,

2. JpaRepository라는 클래스를 상속받아야한다.

3. 이때 <> 타입 값에는, 어떤 도메인에 대한 레포지토리인지를 명시하기 위한 도메인 클래스와,

해당 클래스의 PrimaryKey의 Type을 순서대로 넣어주면 된다.

ex) <User, Long>

 

JpaRepository가 제공하는 기본 CRUD

  <S extends T> S save(S entity);	// CREATE

  Optional<T> findById(ID primaryKey);  // READ

  Iterable<T> findAll(); // READ

  long count(); // READ       

  void delete(T entity); // DELETE

  boolean existsById(ID primaryKey); // READ

  // … more functionality omitted.
  
  // https://docs.spring.io/spring-data/jpa/docs/current/reference/html/

주석을 보면 CRUD 중, U는 따로 제공받지 않는 것을 알 수 있다.

 

UserRepository

//User 비밀번호 수정 (마이페이지에서 비밀번호 재 설정)
@Modifying(clearAutomatically = true)
@Query(value = "UPDATE User Set password = :editMyPassword WHERE loginId = :loginId")
void editPassword(@Param("editMyPassword") String editMyPassword, @Param("loginId") String loginId);

위와 같이 @Modifying + @Query 문을 통해

쿼리를 JPQL형태로 입력하거나, 순수 SQL 질의 문구를 만들어 넣어 Update를 수행한다.

 

여기서 매우 중요한 개념이 @Modifying(clearAutomatically = true) 에 들어있다.


영속성 컨텍스트

영속성 컨텍스트객체와 관계형 데이터베이스 사이에 위치하는 공간이다.


영속성 컨텍스트라는 개념이 등장한 이유를 위한 배경지식을 더 알아보겠다.

1. Spring Data JPAJPA를 더 편리하게 사용하기 위함에 등장하게 된 것이다.

2. Hibernate는 JPA를 다루는 여러 방법 중 한 방법이다.

3. JPA는 기존 JAVA/JDBC 를 편리하게 사용하기 위하여 만들어 진 것이다.



영속성 컨텍스트라는 중간 계층을 둠으로써 얻는 이점은 다음과 같이 5가지 정도가 있다

  • 1차 캐시
  • 조회한 entity 객체의 동일성(identity) 보장
  • 트랜잭션을 지원하는 쓰기 지연
  • 변경 감지(Dirty Checking)
  • 지연 로딩(Lazy Loading)

 

* [1차 캐시]

영속성 컨텍스트 내부에는 캐시가 있는데 이를 1차 캐시라고 한다. 영속 상태의 엔티티를 이곳에 저장한다.

1차 캐시의 키는 식별자 값(데이터베이스의 기본 키, 엔티티 도메인에 @Id 어노테이션으로 명시)이고 값은 엔티티 인스턴스이다.

조회하는 방법은 다음과 같다.

1. 1차 캐시에서 엔티티를 찾는다
2. 있으면 메모리에 있는 1차 캐시에서 엔티티를 조회한다.
3. 없으면 데이터베이스에서 조회한다.
4. 조회한 데이터로 엔티티를 생성해 1차 캐시에 저장한다. (엔티티를 영속상태로 만든다)
5. 조회한 엔티티를 반환한다.

길고 길었다. 위와 같은 개념을 왜 알아보았는가? 함은 지금부터 설명한다.

 

기존 나의 코드

//User 비밀번호 수정 (마이페이지에서 비밀번호 재 설정)
@Modifying
@Query(value = "UPDATE User Set password = :editMyPassword WHERE loginId = :loginId")
void editPassword(@Param("editMyPassword") String editMyPassword, @Param("loginId") String loginId);

기존 작성했던 코드를 보면 처음 보았던 메서드와 살짝 다른 부분이 있다.

바로 @Modifying 에 아무 옵션이 안달린 것인데

이는 영속성 컨텍스트의 1차 캐시와 실제 DB간의 무결성 문제가 발생할 가능성이 있게 된다.

 


 

Situation 1

 

1. 입력값으로 "passwor1d" 를 전송하고 editPassword 메서드를 실행시켰다.

2. DB를 확인하니 성공적으로 변경되었다. (영속성 컨텍스트를 거치지 않고 바로 DB에 질의를 했기 때문)

3.

하지만 영속성 컨텍스트의 1차 캐시에UPDATE 하기 전의 영속성 상태인 Entity를 가지고 있고

캐시 정책에 따라 캐시에 엔티티가 존재한다면 캐시에 담긴 그 값을 반환하게 된다. (DB에 적용)

 

** 캐시에 있는 비밀번호 항목은 "abcd", 메서드가 종료된 후 DB를 직접 조회했을때의 값은 "passwor1d"

*** 캐시에 있는 값(수정 전 값)이 다시 DB에 적용되는 상황이 있을 수 있게된다.

**** 즉, DB와 캐시의 담긴 내용이 달라 무결성이 깨지게된다.

 


이를 해결할 수 있는 방법이 여러가지가 있지만

@Modifying을 통해 UPDATE 쿼리를 질의하는 상황(벌크 연산)에서 간단하게 해결할 수 있는 방법이 있다.

 

//User 비밀번호 수정 (마이페이지에서 비밀번호 재 설정)
@Modifying(clearAutomatically = true)
@Query(value = "UPDATE User Set password = :editMyPassword WHERE loginId = :loginId")
void editPassword(@Param("editMyPassword") String editMyPassword, @Param("loginId") String loginId);

위와 같이 @Modifying에 (clearAutomatically = true) 옵션을 부여해 주는 것이다.

 

이 옵션이 의미하는 것은, 벌크연산을 수행한 후 영속성 컨텍스트를 초기화 한다는 의미이다.

 

이렇게 되면 메서드를 실행하여 담기게된 캐시에 들어있는 엔티티가 사라지게 되고,

 

DB에 적용이 된 후 1차 캐시에 값에 의해 다시 덮어씌어지는 상황을 방지할 수 있다.

728x90
복사했습니다!