티스토리 뷰

아래 내용은 스프링 데이터 JPA - 김영한 강의 영상을 보고 실무에서 필요할 때

찾아보려고 정리한 내용입니다

 

1. Spring Data JPA 사용할 때 쿼리 호출하는 네 가지 방법
2. 벌크성 수정 쿼리
3. 엔티티 그래프
4. JPA Hint & Lock
5. 사용자 정의 레파지토리
6. 도메인 클래스 컨버터
7. Pageable
8. JPA 로그 설정, p6spy

 

1. Spring Data JPA 사용할 때 쿼리 호출하는 네 가지 방법

첫 번째 방법으로 JpaRepository를 상속 받고 사용하면 일반적인 CRUD(수정은 변경 감지 사용)와

ById 조건 검색은 인터페이스에서 기본적으로 제공하기 때문에 별도로 선언하지 않고 바로 호출할 수 있습니다

 

두 번째 방법으로 쿼리가 단순한(파라미터 1~2)경우 메서드 이름으로 쿼리 생성

public interface MemberRepository extends JpaRepository<Member, Long> {
    // 관례를 활용한 메서드 이름으로 쿼리 생성
    Member findByUsername(String username);
}

 

 

세 번째 방법으로 엔티티에 @NamedQuery 사용해서 쿼리 작성하는 방법

DAO 관련 로직이 엔티티에 들어가면 복잡도, 유지보수 등 관리가 어려워지므로 사용 안하는 것을 권장합니다

@Entity
@NamedQuery(
        name = "Member.findByUsername",
        query = "select m from Member m where m.username = :username"
)
public class Member {
    @Id @GeneratedValue
    @Column(name = "member_id")
    private Long id;
    private String username;
}

@Repository
public class MemberQueryRepository {
    @Autowired
    private EntityManager em;
    public void namedQuery(String username) {
        Member findMember = em.createNamedQuery("Member.findByUsername", Member.class)
                .setParameter("username", username)
                .getSingleResult();
    }
}

 

네 번째 방법으로 기본적으로 제공하는 JpaRepository 의 기능들은 한계가 있는데 이 경우 별도의 레파지토리를 생성하고

필요한 쿼리들을 작성하고 사용합니다

@Repository
public class MemberQueryRepository {
    @Autowired
    private EntityManager em;
    
    // 쿼리를 직접 포함하는 쿼리가 필요한 경우 아래에 작성하여
    // 별도의 레파지토리를 사용
}

 

페이징 필요한 경우

아래와 같이 리턴 타입으로 Page 를 적용하고 제네릭으로 엔티티 타입을 명시합니다

Page<Member> findByAge(int age, Pageable pageable);


2. 벌크성 수정 쿼리

순수 JPA에서 일반 쿼리에 대해 벌크 수정을 할 경우 update(), executeUpdate() 를 사용합니다

Spring Data JPA 에서 벌크 수정을 하고 싶은 경우 아래와 같이 어노테이션으로 해결할 수 있습니다

@Modifying(clearAutomatically = true)
@Query("update Member m set m.age = m.age + 1 where m.age >= :age")
int bulkAgePlus(@Param("age") int age);


벌크 연산을 수행한 후 동일 트랜잭션에서 영속성 컨텍스트를 비우지 않고 그대로 사용하게 되면

영속성 컨텍스트는 벌크 연산에 대해 내용을 알지 못하므로 데이터에 큰 오류가 생길 수 있습니다

 

@Modifying(clearAutomatically = true) 속성 값을 설정하여 영속성을 비우고 문제를 해결할 수 있습니다

3. 엔티티 그래프
패치 조인을 편리하게 하는 방법입니다

사용 방법은 아래와 같이 단순합니다

@EntityGraph(attributePaths = {"team"})
List<Member> findAll();

 

4. JPA Hint & Lock

JPA를 사용하지 않을 때 일반적으로 Hint 라고 하면 데이터베이스에 적용하는 기능을 말합니다

그러나 JPA에서는 JPA 구현체(하이버네이트)에게 제공하는 힌트입니다

@QueryHints(value = @QueryHint(name = "org.hibernate.readOnly", value = "true"))
Member findReadOnlyByUsername(String username);


내용을 보충하자면 JPA에서 수정에 대한 메커니즘은 원본과 스냅샷을 확인하고 변경사항이 있으면 

변경 감지로 데이터를 수정하는데 Hint 를 적용할 경우 스냅샷을 생성하지 않습니다

여기서 약간의 성능적 이점을 가질 수 있습니다


그러나 이 옵션을 모든 수정 메서드에 적용한다고 성능에 대해 많은 이득을 취하지 않기 때문에
정말 서비스가 크거나 트래픽이 상당할 경우 이를 고려해야됩니다

 

근데 서비스가 정말 커질 경우엔 Hint를 고려하기 보다 캐시 관련 프로세스를 추가하는게 이점이 더 큽니다

결국 Hint는 사용을 고려할 만큼에 이점이 없다고 합니다(작성일 기준)

Lock 이란 조회한 데이터에 대해서 수정을 못하도록 막는 설정입니다

@Lock(LockModeType.PESSIMISTIC_WRITE)
List<Member> findLockByUsername(String username);



5. 사용자 정의 레파지토리

우선 아래 간단한 코드를 보여드리기 전에 레파지토리의 이름 설정에 주의하셔야 합니다

관례에 의해 적용되기 때문에 레파지토리 구현체 클래스 이름은 인터페이스 명 + Impl 을 붙여주어야 합니다
ex) MemberRepository(JPA), MemberRepositoryCustom(인터페이스), MemberRepositoryImpl(Custom 구현체 클래스)

public interface MemberRepository 
		extends JpaRepository<Member, Long>, MemberRepositoryCustom {}
        
public interface MemberRepositoryCustom {}

public class MemberRepositoryImpl implements MemberRepositoryCustom {}


복잡한 쿼리는 JPQL 또는 Query DSL 무엇을 사용해도 코드가 많이 길어질 수 있습니다

그래서 사용자 정의 레파지토리를 사용하는 것 보다 1번 마지막에 언급했는데

별도의 Query 용 레파지토리를 생성하여 사용하는것을 권장합니다


6. 도메인 클래스 컨버터

컨트롤러에서 @PathVariable 어노테이션을 사용하여 Long 타입의 엔티티 식별 값을 받는 경우

해당 엔티티 자체를 요청 파라미터로 받을 수 있습니다

@GetMapping("/v1/members/{id}")
public MemberDto domainConverter(@PathVariable("id") Member member) {}


이는 단순히 조회할 때 사용하는 것을 권장합니다

7. Pageable

JPA에서 페이지의 시작 값은 0 이며 이를 1로 변경할 수 있는데(아래 참조)

실제로 값이 0에서 1로 변경 되는게 아니고 사용할 때 +1 호출로 이를 해결하고

Pageable 객체 안에는 값이 0으로 설정 되어 있습니다 이는 혼동을 불러올 수 있으니 사용하지 않은 것을 권장합니다

spring:
  data:
    web:
      pageable:
        one-indexed-parameters: 1

 

그리고 페이지 관련 default 설정을 spring.data.web.pageable 하위 속성에서 설정 가능합니다

8. JPA 로그 설정, p6spy

resources - spy.properties 파일 추가

appender=com.p6spy.engine.spy.appender.Slf4JLogger

 

resources - META-INF - spring - org.springframework.boot.autoconfigure.AutoConfiguration.imports(파일명) 파일 추가

com.github.gavlyukovskiy.boot.jdbc.decorator.DataSourceDecoratorAutoConfiguration

 

의존성 추가

implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.8.1'

 

JPA 로그 포맷터 클래스 생성(없어도 되며 로그를 보기 편하게 포맷해주는 역할 입니다)

@Configuration
public class P6SpySqlFormatter implements MessageFormattingStrategy {

    @PostConstruct
    public void setLogMessageFormat() {
        P6SpyOptions.getActiveInstance().setLogMessageFormat(this.getClass().getName());
    }

    @Override
    public String formatMessage(int connectionId, String now, long elapsed, String category, String prepared, String sql, String url) {
        sql = formatSql(category, sql);
        return String.format("[%s] | %d ms | %s", category, elapsed, formatSql(category, sql));
    }

    private String formatSql(String category, String sql) {
        if (sql != null && !sql.trim().isEmpty() && Category.STATEMENT.getName().equals(category)) {
            String trimmedSQL = sql.trim().toLowerCase(Locale.ROOT);
            if (trimmedSQL.startsWith("create") || trimmedSQL.startsWith("alter") || trimmedSQL.startsWith("comment")) {
                sql = FormatStyle.DDL.getFormatter().format(sql);
            } else {
                sql = FormatStyle.BASIC.getFormatter().format(sql);
            }
            return sql;
        }
        return sql;
    }

}