스프링/6. 스프링 DB-2

32. 스프링데이터JPA 적용 2

sdafdq 2023. 10. 12. 09:32

우리가 저번에

public interface SpringDataJpaItemRepository extends JpaRepository<Item, Long> {
    List<Item> findByItemNameLike(String itemName);
    List<Item> findByPriceLessThanEqual(Integer price);

    List<Item> findByItemNameLikeAndPriceLessThanEqual(String itemName, Integer price);


    @Query("select i from Item i where i.itemName like :itemName and i.price <= :price")
    List<Item> findItems(@Param("itemName")String itemName, @Param("price") Integer price);
}

실제로 이렇게 인터페이스를 만들어 봤다.

SpringDataJpa는 인터페이스만 만드는 거다.

 

저 JpaRepository가 SpringDataJpa다.

들어가 보면 

package org.springframework.data.jpa.repository;

패키지 경로가 이렇다.

 

여튼 인터페이스라도, 저게 스프링DataJpa가 프록시로 구현체를 만들어 스프링 컨테이너에 빈으로 등록해 준다.

 

 

그럼 여튼 이제, 빈으로 우리가 지은 인터페이스의 이름인 springDataJpaItemRepository 라는 이름으로 등록될건데, 그럼 본격적으로 Service에서 쓰면 되나?

아니다.

 

그렇게 하면 서비스 코드를 다 바꿔야 한다.

 

우리가 원하는 것은 Service의 비즈니스 로직은 그대로 놔두면서 repository 등을 갈아끼울 수 있는 형태로 놔두는 거다.

그럴려면, 우리가 기존에 구현했던 리포지토리 인터페이스가,

public interface ItemRepository {
    Item save(Item item);
    
    void update(Long itemId, ItemUpdateDto updateParam);
    
    Optional<Item> findById(Long id);
    
    List<Item> findAll(ItemSearchCond cond);
}

저 springDataJpaItemRepository를 쓰도록 하면된다.

뭔가 리포지토리가 리포지토리를 쓴다고 생각하니 느낌이 이상하긴 하지만,

원래 리포지토리가 service와 DB의 징검다리 였지만,

저 springDataJpaItemRepository는 리포지토리와 DB의 번역기 역할이라고 하자.

... 뭔가 더 좋은 이름은 없을까.

사실 springDataJpaItemRepository의 역할 자체가 리포지토리라서.. 번역기도 좀 이상하고..

 

그러니까 더 정확히는 번역 + 리포지토리 기능이라서..

아예 분리를 해놨다면 이름 짓기는 편리했겠지만,

또 같이 기능을 붙여놔서 사용하기 매우 편리하긴 하다..

 

여튼간에,

 

@Repository
@Transactional
@RequiredArgsConstructor
public class JpaItemRepositoryV2 implements ItemRepository {

    private final SpringDataJpaItemRepository repository;

    @Override
    public Item save(Item item) {
        return repository.save(item);
    }

    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        Item findedItem = repository.findById(itemId).orElseThrow();
        findedItem.setItemName(updateParam.getItemName());
        findedItem.setPrice(updateParam.getPrice());
        findedItem.setQuantity(updateParam.getQuantity());
    }

    @Override
    public Optional<Item> findById(Long id) {
        return repository.findById(id);
//        return Optional.empty();
    }

    @Override
    public List<Item> findAll(ItemSearchCond cond) {
        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        if(StringUtils.hasText(itemName) && maxPrice != null){
            return repository.findItems("%" + itemName + "%", maxPrice);
        }
        if(StringUtils.hasText(itemName)){
            return repository.findByItemNameLike("%" + itemName + "%");
        }
        if(maxPrice != null){
            return repository.findByPriceLessThanEqual(maxPrice);
        }else{
            return repository.findAll();
        }
    }
}

@RequiredArgsConstructor로 final이 붙은 것은 생성자가 자동으로 정의 된다.

@Repository 등록해놨으므로 컴포넌트 스캔의 대상에 등록된다. 물론 우리는 컴포넌트 스캔의 범위를 컨트롤러 쪽으로 한정 지었다. 서비스나 리포지토리 등은 우리가 직접 넣을 것을 지정하기 위해.

여튼 @Repository는 그 컴포넌트 스캔의 대상 + 예외 번역(다른 기술 종속성 예외 -> 스프링이 통합관리하는 예외)

 

@Transactional 트랜잭션을 위한 애노테이션이다. 이렇게 하면 또 프록시 기술로 Transaction 기능이 있는 프록시 클래스로 저 repository 클래스를 감싼다. 참고로 프록시 여러 번 감쌀 수 있다. 즉, Transactional 뿐 아니라 추가적으로 다른 기술을 사용하기 위해 프록시로 감싸게 할 수 있다.

 

보면 이제 우리가 구현한 ItemRepository 인터페이스를 상속받아 구현한다.

 

그럼 이제 이 리포지토리를 서비스에 들어갈 ItemRepository로 갈아끼울 수 있다.

또, 상속받아 그냥.. 뭐라고 해야할까 리팩토링? 확장해봤자 리팩토링, 즉 블랙박스 안을 몰라도 우리가 기능을 사용할 때 준 것이 똑같으면 나오는 것도 똑같은.. 대충 그런 느낌의 확장이기 때문에 기존 서비스 코드를 전혀 바꿀 필요가 없다.

 

private final SpringDataJpaItemRepository repository;

자 이제 이거

이거는 SpringDataJpa 상속받아 만든 인터페이스이고, SpringDataJpa 그렇게 만들어 놓으면 프록시로 스프링 빈으로 등록해 주기 때문에, 자동으로 주입받는다.

 

그럼 그냥 쓰면 된다.

 

맨 위에 우리가 해 놓은 거 써도 되고, 아니면 findAll이나 findById 이런 정말 공통적인 부분들은 우리가 굳이 구현? 인터페이스라서 구현이라 하기에는 좀 그렇긴 한데, 여튼 인터페이스에 구현 해 놓은 거 외에도 쓸 수 있다.

 

그냥 SpringDataJpaItemRepository.save(Item) 이런 식으로 쓰면 된다.

 

우리가 저 SpringDataJpaItemRepository 구현할 떄 <Item, Long> 써놔서 각각 DB에서 관리할 데이터, ID로 쓸 것의 타입 다 안다.

 

여튼 그렇게 쓰는거다.

save 할 때 item을 반환해 주는데, 

우리가 서비스에서 저걸 쓸 때 id는 지정해서 사용하지 않는다. id는 db에서 자동으로 등록되게끔 했으므로.

근데도 save에서 반환된 (save하면 save한걸 반환해 주는 듯.) 것엔 제대로 id가 들어가 있다.

 

@Data
@Entity
//@Table(name="item")
public class Item {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name="item_name", length = 10)
    private String itemName;
    private Integer price;
    private Integer quantity;

    public Item() {
    }

    public Item(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }
}

여기 보면 우리가 private Long id;

에다가 id이고, generatedValue 자동으로 계산되는 값(db에 의해서)

라고 해 놨기 때문에, 자동으로 저기에 넣어줄 것이다. @Id 저걸 해 놔야 이것이 PK이다. 라고 인지한다.

@Entity 저걸 써 놔야 JPA가 알아듣는다. (스프링 데이터 JPA 뿐 아니라 JPA도)

 

여튼 저렇게 해 놔서 알아서 잘 id 넣어서 반환해 준다. @Id 붙여놔서 그런 거 같다. @GeneratedValue와 무관하다고 한다.

 

 

여튼 다시,

@Repository
@Transactional
@RequiredArgsConstructor
public class JpaItemRepositoryV2 implements ItemRepository {

    private final SpringDataJpaItemRepository repository;

    @Override
    public Item save(Item item) {
        return repository.save(item);
    }

    @Override
    public void update(Long itemId, ItemUpdateDto updateParam) {
        Item findedItem = repository.findById(itemId).orElseThrow();
        findedItem.setItemName(updateParam.getItemName());
        findedItem.setPrice(updateParam.getPrice());
        findedItem.setQuantity(updateParam.getQuantity());
    }

    @Override
    public Optional<Item> findById(Long id) {
        return repository.findById(id);
//        return Optional.empty();
    }

    @Override
    public List<Item> findAll(ItemSearchCond cond) {
        String itemName = cond.getItemName();
        Integer maxPrice = cond.getMaxPrice();

        if(StringUtils.hasText(itemName) && maxPrice != null){
            return repository.findItems("%" + itemName + "%", maxPrice);
        }
        if(StringUtils.hasText(itemName)){
            return repository.findByItemNameLike("%" + itemName + "%");
        }
        if(maxPrice != null){
            return repository.findByPriceLessThanEqual(maxPrice);
        }else{
            return repository.findAll();
        }
    }
}

이제 update 부터 보자면,

 

저렇게 조회해서 얻어온 다음에 값만 바꿔도, DB에 반영됨.

나는 뭔가 프록시 처럼 감싸진게 나오나 했더니 그건 아니라고 함. 뭐 플래시, 자동 무슨 검사 이런 거 알아야 한다고 하는 데, 지금은 가르쳐주지 않을 듯.

 

다음 findById

저거는 진짜 SpringDataJPA에 우리가 따로 정의해 놓지 않아도 있는건데, (하긴 이건 워낙 다른 DB도 공통적인 기능임) 저렇게 하면 됨. 그냥 id 넣고 찾으면 됨. 애초에 SpringDataJPA 자체가 저거 반환 타입 자체를 Optional로 해놨음.

 

그리고 findAll

여기는 이제, 조건에 따라 다 호출할 메소드를 나눠놨음.

findItems는 우리가 쿼리애노테이션으로 쿼리 직접 써놓은 곳,

@Query("select i from Item i where i.itemName like :itemName and i.price <= :price")
List<Item> findItems(@Param("itemName")String itemName, @Param("price") Integer price);

이렇게 해 놓으면 @Param("itemName") 에 쿼리문은 :itemName 지정해 놔서 정의할 때 인자 순서는 상관없음.

그런데 당근 호출할 때 인자 순서는 상관 있음.. 정의할 때는 어떤 @Query와의 약속이 있기때문에 그럴 수 있다 쳐도 호출하는 건 그냥 자바에서 메소드 호출하는 거임.

 

여튼 근데, like로 할 때는 "%"양옆으로 붙여놔야 함.

왜 %itemName% 이런 식으로 되게 해 놔야 함.

 

우리가 옛날에 sql에서

item_name like concat( '%', :itemName, '%')";

이렇게 했듯이.

 

아 참고로, 지금 현시점 하이버네이트 최신 버전이 5.6.7인데,

이거 like 쓸 때 오류 있었음.

 

보~ 통 정말 코딩 하다보면 에러의 원인은 개발자가 99%인데,

이거 5.6.7 버전에 이상이 있다고 함. 인터넷에 찾아보면 라이브러리 자체 문제라고 함.

 

그래서 build.gradle에

ext["hibernate.version"] = "5.6.5.Final"

이렇게 버전 지정을 아예 해 놨음.

 

여튼,

이렇게 해 놨긴 했는데,

실제로는 Querysql인가 동적쿼리 하기 쉬운 기술 그거 이용해서 하지, 저렇게 지저분하게는 안함.

 

자 이제 해 놨고,

@Configuration
@RequiredArgsConstructor
public class SpringDataJpaConfig {
    private final SpringDataJpaItemRepository springDataJpaItemRepository;

    @Bean
    public ItemService itemService(){
        return new ItemServiceV1(itemRepository());
    }

    @Bean
    public ItemRepository itemRepository(){
        return new JpaItemRepositoryV2(springDataJpaItemRepository);
    }
}

Config 등록

SpringDataJpaItemRepository 저거 final로 하고 @RequiredArgsConstructor로 자동으로 final 붙은 거 인자로 받도록 하는 생성자 정의하게 해 주고,

 

그거를 이제 우리가 구현한 Repository에 넣어줌.

 

 

@Import(SpringDataJpaConfig.class)
@SpringBootApplication(scanBasePackages = "hello.itemservice.web")
public class ItemServiceApplication {

	public static void main(String[] args) {
		SpringApplication.run(ItemServiceApplication.class, args);
	}

	@Bean
	@Profile("local")
	public TestDataInit testDataInit(ItemRepository itemRepository) {
		return new TestDataInit(itemRepository);
	}
}

이렇게 하면 이제 저 Config(구성, 구성옵션이란 뜻)를 사용하게 되서 스프링에 저 옵션으로 등록 됨.

 

테스트 돌려봤더니 다 잘됨.

안되면 하이버네이트 버전 낮췄는지 생각해 보셈.

'스프링 > 6. 스프링 DB-2' 카테고리의 다른 글

34. Querydsl 등장  (0) 2023.10.13
33. Querydsl  (0) 2023.10.13
31. 스프링DataJpa 적용1  (0) 2023.10.12
30. 스프링DataJPA 주요 기능  (0) 2023.10.12
29. 스프링 데이터 JPA 전체적인 기능  (0) 2023.10.12