개발/JPA

JPA *Repository , @Transactional, @Autowired , @Query, @Modifying, DI, JpaRepository-> findById , getById

멋진놈 2022. 7. 21. 17:19
728x90

BoardRepository 생성

package examples.boot.myshop.repository;

import examples.boot.myshop.entity.Board;
import org.springframework.data.jpa.repository.JpaRepository;

public interface BoardRepository extends JpaRepository<Board, Long> {
    /*
     * 이렇게만 인터페이스를 선언해도
     * 기본적으로 입력, 수정, 삭제, 조회가 가능하다.
     * 이것은 인터페이스이지만 이것을 구현하는 클래스를 만들지 않아도 된다.
     * Spring Data JPA가 자동으로 이것을 구현하는 클래스를 프록시 객체로 만들어준다.
     * */

 

CategoryRepository 생성


import examples.boot.myshop.entity.Category;
import org.springframework.data.jpa.repository.JpaRepository;

public interface CategoryRepository extends JpaRepository<Category, Long> {

    /*
    * 이렇게만 인터페이스를 선언해도
    * 기본적으로 입력, 수정, 삭제, 조회가 가능하다.
    * 이것은 인터페이스이지만 이것을 구현하는 클래스를 만들지 않아도 된다.
    * Spring Data JPA가 자동으로 이것을 구현하는 클래스를 프록시 객체로 만들어준다.
    * */

}

 

확인

 

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional // test에서 @Transactional을 사용하면 자동 롤백된다.
public class MyshopApplicationTests {

   @Autowired //테스트할 클래스를 오토와이어로 주입
   CategoryRepository categoryRespository;

   @Autowired
   BoardRepository boardRepository;

   @Test
   public void contextLoads() {
   }

   @Test
   public void test1(){
      Category category=categoryRespository.getOne(1L);
      System.out.println(category.getId());
      System.out.println(category.getName());
      Category category2 = categoryRespository.getOne(1L);
      if(category==category2){
         System.out.println("category==category2");
      }

@Transactional

 

트랜잭션이란?

  • 2개 이상의 쿼리를 하나의 커넥션으로 묶어 DB에 전송하고,
    이 과정에서 에러가 발생할 경우 자동으로 모든 과정을 원래대로 되돌려 놓는다.
  • 하나 이상의 쿼리를 처리할 때 동일한 Connection 객체를 공유하도록 한다.

 

트랜잭션의 성질

  1. 원자성
    • 한 트랜잭션 내에서 실행한 작업들은 하나로 간주한다. 모두성공, 모두실패
  2. 일관성
    • 일관성 있는 데이터베이스 상태를 유지한다.
  3. 격리성
    • 동시에 실행되는 트랜잭션끼리는 서로 영향이 없어야한다.
  4. 지속성
    • 트랜잭션을 성공적으로 마치면, 결과가 항상 저장되어야한다.

 

Spring에서 트랜잭션 처리 방법

  • @Transactional을 선언하여 사용하는 방법이 일반적이며, 선언적 트랜잭션이라고 부른다.
    (@EnableTransactionManagement가 선언되어있어야한다 )
  • 클래스, 메서드에 해당 어노테이션을 선언시, 해당 클래스는 트랜잭션 기능이 추가된 프록시 객체가 생성된다.
  • PlatformTransactionManager를 사용하여 트랜잭션을 시작하고 정상 여부에 따라 commit 혹은 rollback 한다.

 

이것이랑 연계 TransactionStatus status = transactionWebhard.getTransaction( new DefaultTransactionDefinition() );

찾아보기

 


@Autowired

 

@Autowired //테스트할 클래스를 오토와이어로 주입

필요한 의존 객체의 “타입"에 해당하는 빈을 찾아 주입한다.

  • 생성자
  • setter
  • 필드

 

위의 3가지의 경우에 Autowired를 사용할 수 있다. 그리고 Autowired는 기본값이 true이기 때문에 의존성 주입을 할 대상을 찾지 못한다면 애플리케이션 구동에 실패한다. 그러면 Autowired를 사용할 때의 경우의 수가 존재하는데 각각의 상황에 대해서 정리해보자.

 

@Autowired란, 스프링 DI(Dependency Injection)에서 사용되는 어노테이션입니다.  

스프링에서 빈 인스턴스가 생성된 이후 @Autowired를 설정한 메서드가 자동으로 호출되고, 인스턴스가 자동으로 주입됩니다. 

즉, 해당 변수 및 메서드에 스프링이 관리하는 Bean을 자동으로 매핑해주는 개념입니다. @Autowired 는 변수, Setter메서드, 생성자, 일반 메서드에 적용이 가능하며 <property>, <constructor-arg>태그와 동일한 역할을 합니다. 

 

 

DI(Dependency Injection) 

DI(Dependency Injection)란
@Autowired 개념을 알기 위해 먼저 DI 개념을 알아야 합니다.

DI(의존성 종속, Dependency Injection)란, 클래스간의 의존관계스프링 컨테이너가 자동으로 연결해주는 것을 말합니다. 

* Dependency 란, 객체가 다른 객체와 상호작용하는 것을 말합니다.

클래스 A가 클래스 B,C와 상호작용한다면 객체 A는 객체B,C와 의존관계입니다. 

 

DI가 필요한 이유 : 객체 간 의존성 

아래 Factory 인터페이스를 상속받는 ConsoleFactory, UserFactory가 있습니다.

SW를 사용하는 고객은 Factory 클래스만을 호출해야하며, 그것이 ConsoleFactory인지 UserFactory인지 몰라야합니다. 

고객마다 전용 Factory를 생성할 경우 코드 생산성이 떨어지며, 고객이 몰라도 되는 코드가 노출되기 때문입니다. 

때문에 스프링은 Factory가 ConsoleFactory인지 UserFactory인지를 프레임워크가 자동으로 객체간 의존성을 주입해줍니다.

 

 

출처: https://life-with-coding.tistory.com/433 [코딩젤리:티스토리]

 


@Query

 

@Modifying을 말씀드리기 전에 먼저 @Modifying이 적용되는 @Query에 대해 알고 넘어갑시다.

Spring Data JPA에서는 기본적으로 JpaRepository를 통해서 제공되는 findById와 같은 메서드도 있고, 메서드 네이밍만을 통해서 쿼리를 실행할 수 있도록 기능을 제공해주고 있습니다.

하지만,

이 두가지 방법으로도 만들 수 없는 쿼리가 필요하다면, 쿼리를 직접 작성해야 합니다.

그 때 커스텀 Reopository의 메서드에 붙이는 annotation이 @Query입니다.

기본적으로는 JPQL로 작성할 수 있고, nativeQuery=true 옵션으로 네이티브 쿼리도 사용 가능합니다.

 


@Modifying

 

BoardRepository

@Modifying
@Query("update Board p set p.count = p.count + 1 where p.id = :id")
int updateCount(Long id);
Page<Board> findByTitleContainingOrContentContaining(String title, String content, Pageable pageable);

 

BoardService

@Transactional
public int updateCount(Long id) {
    return boardRepository.updateCount(id);
}

BoardController

@GetMapping("/board/{id}")
public String detail(@PathVariable Long id, Model model) {
    model.addAttribute("board", boardService.detail(id));
    boardService.updateCount(id);
    return "thymeleaf/board/board-detail";
}

findById

BoardRepository -> findById

@Modifying 

@Query

~

 

Page<Board> findByTitleContainingOrContentContaining(String title, String content, Pageable pageable);

 

public void save(RequestReply requestReply) {

    // 해당하는 게시판이 존재하는가 ?
    Board board = boardRepository.findById(requestReply.getBoardId())
        .orElseThrow(EntityNotFoundException::new);

    // 댓글 생성
    Reply reply = Reply.builder()
        .content(requestReply.getContent())
        .board(board)
        .build();

    replyRepository.save(reply);
}

해당 로직 테스트 코드는 다음과 같습니다.

@Test
@DisplayName("댓글등록")
void replySave() {
    //given
    StudyBoard studyBoard = StudyBoard.builder()
        .title("토비의 스프링 스터디원 구해요")
        .studyName("초보를 위한 Spring Study")
        .description("이러한 이유로 모집합니다.")
        .place("서울")
        .recruitmentDeadline(LocalDateTime.now().plusDays(7))
        .recruiter(4)
        .createBy("KJJ")
        .build();
    StudyBoard study = boardRepository.save(studyBoard);


    RequestReply requestReply = new RequestReply(study.getId(), "저요저요!");
    System.out.println("==================================================================");
    //when  save 호출시 쿼리 발생 갯수 확인
    replyService.save(requestReply);
}

 

findById()

앞서 살펴본 예제는 findById()를 통해 DB에 쿼리가 발생하여 객체를 찾게 됩니다.

따라 다음과 같이 총 2번의 쿼리가 발생하게 됩니다.

 

여기서 드는 생각은 왜?

  • Board 객체를 찾아야 하지?
  • Id 값을 알고 있으니 Insert Query 하나만 발생해도 충분한 거 아닌가?

이러한 생각이 들 수 있습니다.

하지만 ORM 입장에서는 해당 ID 값 만으로는 어떤 타입의 객체인지 알 수 없습니다.

따라서 앞서 살펴본 것처럼 한번 찾고 Reply 객체를 생성하여 Board를 설정하는 과정이 발생하게 됩니다.

 

이처럼 두 번 Query 가 발생하는 것이 무조건 나쁜 과정일까요?

해당 질문은 아래에서 좀 더 다루겠습니다.

 

 

1.2. findById

@Override
public Optional<T> findById(ID id) {

    Assert.notNull(id, ID_MUST_NOT_BE_NULL);

    Class<T> domainType = getDomainClass();

    if (metadata == null) {
        return Optional.ofNullable(em.find(domainType, id));
    }

    LockModeType type = metadata.getLockModeType();

    Map<String, Object> hints = new HashMap<>();
    getQueryHints().withFetchGraphs(em).forEach(hints::put);

    return Optional.ofNullable(type == null ? em.find(domainType, id, hints) : em.find(domainType, id, type, hints));
}

우리가 잘 알고 있는 메소드입니다.

실제 DB 에 요청해서 엔티티를 가져옵니다.

정확히 말하면 영속성 컨텍스트의 1차 캐시를 먼저 확인하고 데이터가 없으면 실제 DB 에 데이터가 있는지 확인합니다.

 

출처 : https://k3068.tistory.com/m/103

 

[JPA] findById() , getById() 에 대한 생각

서론 이번 글에서는 findById() , getById() 메서드의 차이점과 제가 생각하는 느낀 점에 대해 말해보고자 합니다. 따라서 개인적인 생각이 담긴 글이니 어느정도 비판적인 시각으로 봐주시면 감사하

k3068.tistory.com

 

https://bcp0109.tistory.com/325

 

JPA 의 getById() vs findById()

1. Overview JPA 를 사용할 때 ID 값으로 엔티티를 가져오는 두 가지 메소드가 존재합니다. 비슷하지만 다른 이 두가지 메소드의 차이점에 대해서 알아봅시다. 1.1. getById @Override public T getById(ID id) {..

bcp0109.tistory.com

 

 


getById

 

getById()

getById() 메서드가 생소하신 분들도 있을 것 같은데요 Spring Data JPA 2.5.3 이후부터 getOne() 메서드가 Deprecated 되고추가된 메서드입니다 기존 getOne() 메서드와 하는 일은 동일합니다.

 

그렇다면 앞선 예제에서 findById() 로 Board를 찾는 대신 getById() 를 통해 Board를 찾는다면 어떻게 될까요?

다음과 같이 하나의 Query 만 발생하게 됩니다.

따라서 앞서 findById() 메서드를 사용했을 때 가진 생각을 해결할 수 있는데요!

 

우선 getById() 가 어떻게 작동하는지부터 알아보죠!

 

1.1. getById

@Override
public T getById(ID id) {

    Assert.notNull(id, ID_MUST_NOT_BE_NULL);
    return em.getReference(getDomainClass(), id);
}

getById() 는 원래 getOne() 이었으나 해당 메소드가 Deprecated 되고 대체되었습니다.

내부적으로 EntityManager.getReference() 메소드를 호출하기 때문에 엔티티를 직접 반환하는게 아니라 프록시만 반환합니다.

프록시만 반환하기 때문에 실제로 사용하기 전까지는 DB 에 접근하지 않으며, 만약 나중에 프록시에서 DB 에 접근하려고 할 때 데이터가 없다면 EntityNotFoundException 이 발생합니다.

 

 

 

  • getById() 는 실제 테이블을 조회하는 대신 프록시 객체만 가져옵니다.
  • 프록시 객체만 있는 경우 ID 값을 제외한 나머지 값을 사용하기 전까지는 실제 DB 에 액세스 하지 않기 때문에 SELECT 쿼리가 날아가지 않습니다.

getById() 는 지연 로딩으로 작동하게 되는데요! ID 값을 제외한 나머지 필드에 접근했을 때 Query가 발생하게 됩니다.

public void save(RequestReply requestReply) {
    Board board = boardRepository.getById(requestReply.getBoardId());

    System.out.println("ID 값 :"+ board.getId()); // 이때는 Query 가 발생하지않음
    System.out.println("=============================================");
    System.out.println("Title 값: "+ board.getTitle()); //이때는 정보가 필요하기때문에 Query 가 발생함

    // 댓글 생성
    Reply reply = Reply.builder()
        .content(requestReply.getContent())
        .board(board)
        .build();

    replyRepository.save(reply);
}

예제를 본다면 더욱 이해하기 쉬울 것 같습니다

결과

이처럼 어떠한 객체의 ID 값이 DB에 반드시 존재하고 ID를 제외한 다른 필드에 접근하지 않을 때 사용하게 되면 좋을 것 같습니다.

 

 

findById(), getById() 의 차이점은 즉시 로딩, 지연 로딩으로 가져오는가? 의 차이점이 있습니다.

 

그래서 언제 어떤걸 써야 하는데?

엔티티의 실제 상태값이 필요하지 않고 식별자가 필요하거나 참조를 통한 연관관계 매핑용으로는 getById를 사용하는게 맞고,


엔티티의 실제 상태값이 필요한 경우에는 데이터베이스에 접근하여 실제 객체를 생성하는 findById를 사용하는게 맞다고 생각한다

 

출처 : https://velog.io/@eden/JPA-%EC%97%90%EC%84%9C-getById-vs-findById

 

JPA 에서 getById vs findById

문제점 평소 JPA를 사용하면서 getById와 findById를 혼용해서 사용 하였다. 명확한 차이점을 이해하지 못하고 혼용해서 사용했기에 이번 기회에 알아보려고 한다. getById vs findById 1.SimpleJpaRepository.getB

velog.io