팀프로젝트일기/SKKUNION

SKKUNION - 데이터를 한 번 뽑아보자!

Recfli 2023. 10. 8. 16:47

JPA 자동생성 DATA 뽑는 방식

JpaRepository를 extends하면 자동생성 DATA를 뽑을 수 있습니다..! 우선 딱 두 개의 엔티티와 Repository의 코드부터 보면 이해가 쉬우니 해당 부분부터 살펴보시면 될 것 같습니다.

 

 

User 엔티티

@Data
@Builder
@Entity
@AllArgsConstructor // constructor 생성
@NoArgsConstructor // getter, setter 생성
public class User {

    // 혹시나 나중에 수정할지도 몰라서 전혀 관련이 없는 DB 자체 내부 설정값을 PK로 두겠습니다.
    // 다른 Table에서도 id를 사용할 예정이니까. 얘만 이름을 따로 설정할게요.
    @Id @GeneratedValue
    @Column(name = "USER_ID")
    private Long Id;

    @NotNull
    private String userEmail; // DB 내부 user_Email로 변경

    @NotNull
    private String userPassword; // DB 내부 user_password로 변경

    private String userName; // DB 내부 user_name으로 변경

    private int age;

    // Enum 타입이 되 STRING으로 저장함.
    // 성별은 수정될 일이 없지만 다른 경우에 요청 사항이 늘어나면 이렇게 작성할 예정.
    @Enumerated(EnumType.STRING)
    @NotNull
    private GenderType userGender;

    // User 1개 당 여러 개의 UserTag를 가짐. 그렇기 때문에 @OneToMany 어노테이션
    // @ManyToOne으로 설계를 할 수도 있지만 여러 문제점이 있고 Tag를 조회하는 방향이
    // 보통 user에서 조회할 가능성이 많기 때문에 이쪽에서 접근 가능하게 양방향으로 열어두었음.
    // mappedBy는 조회만 가능한 것을 의미함.
    @OneToMany(mappedBy = "user")
    @Builder.Default
    private List<UserTag> userTags = new ArrayList<>();

}

 

UserTag 엔티티

 

@Data
@Builder
@Entity // Entity 테이블 표시
@AllArgsConstructor // constructor 생성
@NoArgsConstructor // getter, setter 생성
public class UserTag {
    @Id @GeneratedValue
    @Column(name = "USER_TAG_ID")
    private Long id;

    // 외래키 표시, 연관관계의 주인이라는 의미, user랑 조인돼서 가져옴.
    @ManyToOne
    @JoinColumn(name = "USER_ID")
    private User user;

    // Enum 타입이 되 STRING으로 저장함.
    // 이 경우에 요청 사항이 늘어나면 이게 매우 유리함.
    @Enumerated(EnumType.STRING)
    @NotNull
    private UserTagType userTag;
}

 

 둘은 User 하나가 여러 개의 특징을 가질 수 있게 해당처럼 테이블을 구성했었습니다. 

 

 여기에서 이제 데이터를 가져와야하죠? 그래서 예전에 만들던거에서 바꾼거라;; 이름이 이상하긴 한데 일단 쓸게요. 나중에 바꿉시다. LoginRepository가 사실 User와 UserTag를 관리하는 Repository라고 생각하시면 돼요.

 

 JpaRepository는 어떤 기능을 하는지 어떻게 가져오는지는 검색을 해보시기 바랍니다. 자동적으로 interface로 만들어주면 굉장히 유용하게 사용할 수 있어요. 간단한 건 자동으로 만들어줍니다. 또한 여기에서는 Tag를 한꺼번에 User를 조회하면서 싹 가져와야하는데 동적쿼리가 필요없죠? 그래서 fetch join을 사용해서 데이터를 조인해서 jpql로 작성을 했습니다.

 

 fetch join을 왜 했는지 모르실 수도 있는데, 해당 부분은 영속성 컨텍스트와 지연로딩에 대해서 공부해보시면 무슨 내용인지 이해하실 수 있어요. 꼭 모르셔도 사용하는데는 문제는 없습니다.

 

 저렇게 데이터를 가져오면 DB 내의 User 테이블에 있는 모든 User와 해당 User의 객체 내부에 있는 UserTags 리스트 내부에 모든 Tags 리스트가 한꺼번에 조회돼서 객체가 만들어져요! 그리고 findByUserEmail 부분은 UserEmail이 pk로 지정되지 않은 부분이라 해당 부분은 다음처럼 따로 인터페이스로 만들면 Spring이 해당 코드에 맞게 해줍니다. 조건 And 붙이고 싶으시거나 Or 조건을 붙이고 싶으시면 그것도 자동으로 만들어주니 Spring JPA 공식문서에서 읽어보세요.

 

@Repository
public interface LoginRepository extends JpaRepository<User, Long> {
    @Query("select u from User u join fetch u.userTags")
    List<UserTag> findAllUserTags();



    // 유저의 이메일 정보로 찾는 함수
    public User findByUserEmail(String UserEmail);
}

 

 그러면 이제 저걸 어떻게 사용하는지는 이제 테스트 코드를 작성하시면 됩니다. UserTagRepository 부분은 솔직히 그냥 해당 Entity로 만든거라 코드가 필요없을 것 같아서 JpaRepository 수정 안하고 그냥 썼어요. DB까지 실제로 테스트를 해보시고 싶으시면 Data 날라가는거 상관없이 어차피 DB에 아직 들어간 게 없으니까 DB에 데이터 부분은 create로 해놓을게요. Test는 Test라는 폴더 아래에 원하시는 이름으로 만드시면 됩니다.

 

 우선 저는 userRepositoryTest라는 이름으로 테스트를 만들었어요. 자세한 내용은 주석 참고바랍니다.

 

@SpringBootTest  // SpringBootApplication에서 등록한 빈에 있는 걸 그대로 사용할 수 있음.
@AutoConfigureTestDatabase(replace = Replace.NONE)
@Transactional // 두 부분은 실제 Data를 DB에서 가져오고 저장도 할 수 있게 하려고 놔둔 것
public class userRepositoryTest {


    @Autowired UserTagRepository userTagRepository; // 테스트용 UserTag 부분
    @Autowired LoginRepository loginRepository; // 테스트용 User 부분 이름은 실수에요;;

    @Autowired
    EntityManager em; // 영속성 컨텍스트 문제 때문에 그걸 해결하려면 필요해요.
    

}

 

아무튼 제일 쉬운 데이터 넣고 가져오기는 아래 테스트 코드를 작성해주세요.

// 멤버 넣고 전체 조회
@Test
void findAllFromUser(){
    // 멤버 객체 생성 부분
    User user = User.builder()
            .userEmail("qwer@qwer")
            .userPassword("qwer")
            .userGender(GenderType.MALE)
            .build();

    User user2 = User.builder()
            .userEmail("qwer1@qwer")
            .userPassword("qwer")
            .userGender(GenderType.MALE)
            .build();

    User user3 = User.builder()
            .userEmail("qwer2@qwer")
            .userPassword("qwer")
            .userGender(GenderType.MALE)
            .build();

    
    // user 3개 데이터 DB에 저장
    loginRepository.save(user);
    loginRepository.save(user2);
    loginRepository.save(user3);

    // 이거 딱 돌려보면 어? insert 쿼리도 안날라가고 select query도 안날라가네?
    // DB에서 가져와야하는데? 이게 정상입니다.
    // 그건 Spring에서 미리 SQL 날릴 걸 가지고 있다가 나중에 진짜로 날려야할 때 쓰려고
    // 영속성 컨텍스트라는 곳에 보관한 거라. 쿼리를 보고 싶으시면 아래 주석을 해제해주세요.
 	// em.clear();
    // em.flush();
   
    List<User> users = loginRepository.findAll();

    // 이건 제가 검증하는 부분으로 hasSize는 리스트 객체의 내부 객체 갯수를 비교해줍니다.
    // 저거 개수가 다르면 실제로 받은 건 null인지 아니면 2개인지 그런 정보도 다 로그로 띄워줘요.
    Assertions.assertThat(users).hasSize(3);
}

 

이제 자동으로 만들어주지 않는 부분 userEmail로 데이터 조회하기 하는 방법입니다. 자세한 내용은 주석을 참고해주시면 감사하겠습니다.

 

// Email로 데이터 가져오는 방법
@Test
void findAllMemberWithEmail(){
    // user 데이터를 넣는 부분
    User user = User.builder()
            .userEmail("qwer@qwer")
            .userPassword("qwer")
            .userGender(GenderType.MALE)
            .build();
    
    // user 데이터를 가져오는 부분
    loginRepository.save(user);
    
    // em.clear();
    // em.flush();

    
    // 마치 select * from user u where user_email = qwer@qwer의 역할을 findByUserEmail이 합니다.
    User findUser = loginRepository.findByUserEmail("qwer@qwer");

    // 저건 글자 같은지 확인해주는 메서드에요. Java에서는 ==이랑 equals 뭐 되게 많아요.
    // 해당 부분은 나중에 각각 무슨 차이인지 공부해보시면 도움이 많이 될 겁니다.
    Assertions.assertThat(findUser.getUserEmail()).isEqualTo("qer@qwer");
}

 

마지막으로는 fetchJoin을 하는 방법인데, fetchjoin은 inner 조인해서 User 엔티티 내부에 있는 UserTag 객체를 모두 싹 다 가져와서 둘 다 있는 객체를 만들어줍니다. 원래는 fetchjoin으로 안하면 User마다 UserTag를 검색해서 채우는 N+1문제도 일어나요. 이건 제가 설정한 엔티티가 지연로딩 방법으로 되어있기 때문인데 이건 굳이 userTag부분이 필요한 게 아니면 안 가져오는 프록시 객체를 spring에서는 생성하기 때문입니다.

 

이 부분은 프록시, 지연로딩에 대해서 공부해보시면 이해하기 좀 더 수월하실 것이라고 생각해요! 아래에는 fetchjoin해서 가져온 거 테스트 하는 방법 남겨놓을게요. 지금 DB에는 저기 내용 밖에 없기 때문에 저렇게 index로 가져온 건데 실제로는 조건을 좀 더 붙여서 해당에 맞게 DB에서 딱 가져오셔야됩니다.

 

@Test // 이건 데이터를 join해서 가져오는 거에요. query는 LoginRepository 확인 부탁드립니다.
void userTagTest(){
    // user 데이터 넣고 저장
    User user = User.builder()
            .userEmail("qwer@qwer1r")
            .userPassword("qwer")
            .userGender(GenderType.MALE)
            .build();

    // userTag 정보 넣고 저장
    UserTag userTag = UserTag.builder()
            .userTag(UserTagType.EXTROVERTED)
            .user(user).build();

    // userTag 정보 넣고 저장
    UserTag userTag2 = UserTag.builder()
            .userTag(UserTagType.INTROVERTED)
            .user(user).build();

    userTagRepository.save(userTag);
    userTagRepository.save(userTag2);
    loginRepository.save(user);

    em.clear();
    em.flush();



    // 이제 fetch join으로 한번에
    List<User> users = loginRepository.findAllUserTags();

    User findUser = users.get(0);

    System.out.println(findUser.getUserTags().get(0).getUserTag());
    System.out.println(findUser.getUserTags().get(1).getUserTag());
}

 

 

동적쿼리 작성

동적으로 쿼리를 한 번 작성해봅시다. 그러니까 들어오는 조건에 따라서 범위 설정도 해주는 그런 거요. 우선 다른 프로젝트에서 들고와서 이 부분도 엔티티 내부는 이렇게 되어있어요.

 

@Data
@Builder
@Entity
@AllArgsConstructor // constructor 생성
@NoArgsConstructor // getter, setter 생성
public class Shop {

    @Id @GeneratedValue
    @Column(name = "SHOP_ID")
    private Long Id;

    private String name;

    private int price;

    @Enumerated(EnumType.STRING)
    private ShopType shopType;

    private String url;

    private String description;

}

 

제가 가져오고 싶은 건 가격 범위도 설정하고 싶고 shopType에서 혼밥용인지, 2인-4인인지 그룹인지에 따라서 다르게 조회하는 겁니다. 그건 동적쿼리를 작성할 필요가 없을 수도 있긴한데 일단 전 그렇게 해봤습니다. Repository를 이제 만들어봅시다.

 

아래는 제가 만든 동적으로 jpql을 작성해서 데이터를 뽑아오는 방식인데 엄청 복잡한 건 아니라서 저렇게 나왔네요. 주석을 보시면 이해되실거라고 생각합니다.

 

@Repository
@RequiredArgsConstructor
public class RecommendRepository {

    final EntityManager em;


    public List<Shop> findSelectedShop(String type, int priceMin, int priceMax) {

        ShopType nop = null;
        List<Shop> shops = null;

        // type 설정
        if(Objects.equals(type, "alone"))
            nop = ShopType.SINGLETWOFOUR;
        else if (Objects.equals(type, "morethanfour")) {
            nop = ShopType.TWOFOURMORETHANFOUR;
        }

        if(nop == null){
            // 2인 - 4인의 경우는 전부 다 가져옴.
            TypedQuery<Shop> query2 = em.createQuery("SELECT s FROM Shop s WHERE s.price >= :price_low AND s.price <= :price_high", Shop.class);
            query2.setParameter("price_low", priceMin);
            query2.setParameter("price_high", priceMax);

            shops = query2.getResultList();

            System.out.println(shops.size());
        }
        else{
            // 2인 - 4인이지 않은 경우만 골라줌.
            TypedQuery<Shop> query = em.createQuery("SELECT s FROM Shop s WHERE s.shopType = :shop_type AND s.price >= :price_low AND s.price <= :price_high", Shop.class);

            if(nop.equals(ShopType.SINGLETWOFOUR))
                query.setParameter("shop_type", ShopType.SINGLETWOFOUR);
            else
                query.setParameter("shop_type", ShopType.TWOFOURMORETHANFOUR);

            query.setParameter("price_low", priceMin);
            query.setParameter("price_high", priceMax);

            shops = query.getResultList();

            System.out.println(shops.size());
        }


        return shops;
    }
}

 

해당 부분으로 이제 실제로 Dto를 받았을 때 해당 Shop의 리스트에서 해당 조건에 맞게 데이터를 DB에서 가져오고 딱 한 개만 조회하고 싶어요. 뭐 이 부분은 Paging을 사용해도 되지만 어차피 많이 사람들이 쓰는 서비스도 아니고 가져오는 데이터도 별로 없어서 GC가 그렇게 일을 많이 하지 않으니 제일 쉬운 방법으로 선택했습니다. service 코드도 보시면 어떻게 하는지 이해가 되실거에요.

 

@Service
@RequiredArgsConstructor
public class ShopRecommendService {

    final ShopRepository shopRepository;

    final RecommendRepository recommendRepository;

    public Shop recommend(@NotNull RecommendDto dto){

        String purpose = dto.getPurpose();
        int priceMin = dto.getPriceLow();
        int priceMax = dto.getPriceHigh();

        List<Shop> shops = recommendRepository.findSelectedShop(purpose, priceMin, priceMax);
        Random random = new Random();

        int tmp = shops.size();
        int idx = random.nextInt(tmp);

        Shop shop = shops.get(idx);


        return shop;
    }

}

 

실제로 사용할 수 있는 코드는 제 깃허브에 올려둘게요. SKKUNION-week6에 가면 다운로드 받아서 사용하실 수 있으세요.