Spring Boot JPA/실습

JPA Join과 Config 설정으로 데이터베이스 관계 정리

qoeka 2025. 1. 7. 18:22

 

1. 회원가입

 

 

 

 

 

 

 

@Email // 자동으로 이메일 형식인지 아닌지 체크하는 어노테이션
@NotBlank // null, "", " " 이런 값들을 체크하는 어노테이션
    public String email;
@Size(min = 8, max = 20) // 최소 4글자 이상, 최대 20글자 이하인지 체크하는 어노테이션
    public String password;
@Size(min = 2, max = 10) // 최소 2글자 이상, 최대 10글자 이하인지 체크하는 어노테이션
    public String nickname;

 

코드를 최소화하면서 검증을 간편하게 처리할 수 있는 기능이 바로 벨리데이션이다. 이는 코드의 복잡성을 줄이고, 검증 과정을 간소화하는 역할을 한다.

 

 

 

필요하니까 레포지토리로 가서 상속을 받을 준비를 한다.

겉으로 보이지는 않지만 CRUD 관련 함수들이 모두 포함되어 있다.

 

그리고 다시 서비스로 이동해 로직을 처리하려고 한다.

 

보이지 않지만 이미 존재하는 함수를 활용할 수 있기 때문에, 존재 여부를 묻는 exists 메서드를 사용하면 된다. 하지만 아직 회원가입을 하지 않았으므로, 아이디 기준으로 존재 여부를 확인할 수 없다.

 

 

아이디가 아닌 이메일을 기준으로 존재 여부를 확인하고 싶을 때, SQL로 생각해보면 WHERE 절에 이메일을 조건으로 검색해 결과가 없을 경우 쉽게 확인할 수 있다.

 

회원가입 중이므로 아이디는 아직 없고, 이메일은 유니크하니 이를 기준으로 존재 여부를 확인할 수 있다. 하지만 JPA에서는 findById는 제공되지만 findByEmail은 기본적으로 제공되지 않는다.

 

이럴 때는 existsByEmail(String email)과 같은 메서드를 직접 만들어 사용하면 된다. 존재 여부를 확인하는 것이므로 find보다는 exists를 사용하는 것이 더 깔끔하다. JPA는 이런 식으로 필요에 따라 메서드를 만들어 사용하는 방식이다.

 

 

 

리턴할 값이 없으므로 void를 사용하고, 다른 곳에서도 사용할 수 있도록 public을 붙이면 완성이다.

 

마지막으로 컨트롤러로 돌아가서 마무리해주면 된다.

 

 

하지만 이렇게만 작성하면 안 된다.

 

Config에서 해당 요청을 통과시킬 수 있도록 설정해줘야 한다.

 

 

 

요청을 통과시킬 수 있도록 설정해야 한다. 그렇지 않으면 보안 관련 401 에러가 발생한다.

Config와 Filter는 애플리케이션의 보안을 관리하는 핵심 역할을 한다. 요청이 들어올 때 인증과 권한을 검사하며, 안전하지 않은 요청을 차단하기 위해 반드시 통과되어야 한다.

 

 

 

 

2. 로그인

 

 

 

 

 

이제 다시 컨트롤러가서 마무리한다

 

 

서비스에서 던져준게있으니 트라이캐치로 처리해준다.

왜 이렇게 하는 걸까? 
throw → 문제가 생겼다는 사실을 알림.
try-catch (컨트롤러) → 던져진 예외를 잡고, 
클라이언트가 이해하기 쉽게 메시지를 수정해서 돌려줌.

 

 

 

3.상품 목록 조회

 

포링키가 있는 경우,

두 데이터를 연결할 때 해당 키를 기준으로 위처럼 조인컬럼어노테이션을 사용하되, 클래스를 반환타입으로 설정해주면된다

 

클래스를 반환 타입으로 설정하는 이유는 연결된 엔티티의 모든 데이터를 함께 조회하기 위해서다. 이를 통해 해당 엔티티의 속성뿐만 아니라, 연관된 엔티티의 속성까지 한 번에 가져올 수 있다.

 

포링키가 없는 경우에는 별도의 조인 없이 데이터를 그대로 사용해도 문제가 없다.

 

 
@JoinColumn(name = "product_id")
public Products product;

 

 

 

 

리뷰 테이블의 product_id는 반드시 product 테이블에 존재하는 값이다.

리뷰 테이블에서는 같은 product_id가 여러 번 등장할 수 있다.

예를 들어, 사용자 1번, 2번, 3번이 각각 프로덕트 1번에 대한 리뷰를 작성할 수 있다.

product 테이블에서는 product_id가 하나만 존재하지만, 리뷰 테이블에서는 여러 번 중복될 수 있다.

이런 관계를 Many-to-One(다대일) 관계라고 부른다.

 

이 관계를 JPA에서는 @ManyToOne 어노테이션으로 표현할 수 있다.

@JoinColumn(name = "product_id")는 리뷰 테이블의 product_id 컬럼이 product 테이블의 id와 연결됨을 의미한다.

 

@ManyToOne
@JoinColumn(name = "product_id")
public Products product;

 

 

이제 이건 프로덕트로 할거니깐 프로덕트 컨트롤러를 새로 만들어주고 시작하겠다.

 

 

 

 

findAll()을 사용하면 SQL로 해석했을 때,

SELECT *
FROM products
LIMIT 0, 7

 

과 같은 쿼리를 실행하는 것과 같다.

즉, products 테이블의 모든 데이터를 가져오는데, 특정 개수만 조회하도록 제한을 두는 것이다.

 

하지만 API 명세서를 보니 단순히 상품 정보만 가져오는 것이 아니라,

평균 평점(average rating)과 리뷰 개수(review count)도 함께 제공해야 한다. 따라서 단순히 findAll()로 상품 데이터를 가져오는 것만으로는 부족하다.

 

이제 평균 평점과 리뷰 개수를 추가로 조회할 수 있도록 코드를 작성해야 한다. 이를 위해 반복문을 사용하여 각 상품에 대해 필요한 추가 정보를 가져오는 방식을 사용할 것이다.

즉,

상품 하나를 가져온 후, 해당 상품의 average rating과 review count를 함께 조회하고, 이 과정을 상품 전체에 대해 반복하는 것이다.

 

쉽게말하면, for 반복문을 사용해 products 테이블의 각 상품을 순회하고, 각각의 리뷰 정보를 조회한 뒤,

결과를 결합해 반환하는 방식으로 구현할 수 있다.

 

이처럼 findAll() 메서드는 데이터를 단순 조회할 때 유용하지만, 추가적인 데이터가 필요할 때는 반복문과 추가 쿼리 호출이 필요할 수 있다.

 

 

리뷰 테이블에 사용할 레파지토리가 필요합니다. 따라서, 리뷰 테이블의 생성(Create), 조회(Read), 수정(Update), 삭제(Delete)를 처리할 수 있는 레포지토리를 하나 만들어야 한다

 

또한, 옆의 다른 테이블과 조인하는 작업을 위해 반복문을 통해 가져온 데이터를 결합할 예정입니다. 그러나 해당 레파지토리가 아직 없기 때문에, 데이터를 사용하기 위해 @Autowired를 이용해 레포지토리를 생성하고, 이후 다시 요청을 처리할것이다.

 

 

리뷰 테이블의 ID가 1인 데이터를 불러오는 것이다. 이는 특정 ProductId 값을 기준으로 데이터를 조회하는 방식이다. 그러나 해당 ProductId가 존재하지 않는 경우, 새로운 ProductId를 생성해 주어야 한다.

 

 

 


특정 ID의 데이터 수를 세기 위해 count 메서드를 별도로 구현
하였다. 기존의 count 메서드를 사용할 경우, 모든 데이터를 세어버리기 때문에 특정 ID를 기준으로만 개수를 계산할 수 있도록 해당 함수를 정의하였다.

 

이제 이 데이터를 ProductResponse에 저장해주면 된다. 이를 위해 DTO(Data Transfer Object) 클래스를 새롭게 정의해야 한다.

 

DTO 클래스는 데이터 전송을 위해 사용되며, 필요한 데이터만 담아 전달할 수 있도록 구성되어야 한다. 이를 통해 클라이언트에 전달할 데이터를 효율적으로 관리하고, 불필요한 정보를 제외할 수 있다.

 

 

이제 엔터티(Entity)를 DTO(Data Transfer Object)로 변환하여 저장하면 된다.

 

그리고 리스트에 데이터를 저장해야 하므로, 리스트를 미리 생성해야 한다. 이 리스트 생성은 for 반복문을 작성하기 전에 선언해야 한다.

 

메모리에 비어 있는 리스트 객체를 생성한 후, 데이터를 7개 저장해야 한다면 반복문을 통해 하나씩 저장하여 총 7개의 데이터가 리스트에 추가되도록 한다.

모든 데이터 저장이 완료되면, add 메서드를 사용하여 리스트에 추가하는 방식으로 데이터를 담아야 한다.

 

 

 

이제 전체 를 리스판스할 클래스를  하나 만들것이다

 

 

 

[서비스코드 정리]

@Service
public class ProductService {
    @Autowired
    ProductRepository productRepository;
    @Autowired
    ReviewRepository reviewRepository;

   public ProductListResponse getAllProducts(int page, int size, String category) {
        if(category == null) {
            // PageRequest는 Pageable 인터페이스를 구현한 클래스로,
            // 페이지 번호와 페이지 크기를 받아 페이징 처리를 위한 정보를 제공한다.
            PageRequest pageRequest = PageRequest.of(page-1, size);
          Page<Product> productPage = productRepository.findAll(pageRequest);

            ArrayList<ProductResponse> productResponses = new ArrayList<>();

          for(Product product : productPage) {
            double  averageRating= reviewRepository.findAverageRatingByProductId(product.id);
           int reviewCount =reviewRepository.countByProductId(product.id);

              ProductResponse productResponse = new ProductResponse();
                productResponse.id = product.id;
                productResponse.name = product.name;
                productResponse.price = product.price;
                productResponse.category = product.category;
                productResponse.stockQuantity = product.stockQuantity;
                productResponse.averageRating = averageRating;
                productResponse.reviewCount = reviewCount;

                productResponses.add(productResponse);
          } //반복문끝나고 시작
            ProductListResponse productListResponse = new ProductListResponse();
          productListResponse.content = productResponses; // content에 productResponses를 넣어준다.
          //pageable을 이용해서 페이지 정보를 넣어준다.
            productListResponse.page = page;
            productListResponse.size = size;
            productListResponse.totalElements = productPage.getTotalElements();
            productListResponse.totalPages = productPage.getTotalPages();

            return productListResponse;

        } else {return null;

        }

 

 

 

 

 

복잡한 JPA에서는 쿼리 작성이 어렵거나 한계가 생길 수 있기 때문에, SQL과 함께 사용하는 경우가 많다.

SQL을 직접 사용하면 데이터베이스에 저장된 데이터를 보다 세밀하고 직관적으로 다룰 수 있기 때문이다.

 

특히, 복잡한 조건 검색이나 대량 데이터 처리와 같은 상황에서는 JPA만으로는 한계가 있어, SQL을 활용하는 것이 더 효과적이다.

 

 

 

jap는 프러덕트 아이디를 여기는  :띄지말고 바로 변수이름

 

 

 

그런데 오류가 발생하였다..

 

원인을 찾아보니, JPA에서 SQL을 사용할 때는 SQL 쿼리를 문자열로 직접 작성하는 것이 아니라, 엔티티 클래스를 통해 작성해야 한다. 쿼리를 하나만 작성할 경우 오류가 발생할 수 있으며, 쿼리의 끝에 세미콜론(;)을 붙이지 않아야 한다.

또한, 메소드나 쿼리 작성 시 올바른 형식과 문법을 준수해야 오류를 방지할 수 있다.

 

+++ jap에 SQL을 넣어 줄때는 클래스로 넣어줘야한다 하나면 에러 난다 끝에 세미클론 도 뻬줘야한다.+++

 

 

카테고리는 위에것만 하나 더 작성해주면된다 그리고 엘스를 위에 if 적었듯이 적어주면된다

 

 

 

4. 상품 상세조회

 

 

 

 

 

                         

 

sql 작성하듯함수이름 짓듯  레파지토리만들기

 

 

 

여러 개의 데이터를 다루는 경우, 각각의 데이터를 담기 위해 리스트를 사용하는 것이 적합하다. 이는 데이터의 전체를 한 번에 다룰 때 사용되며, 페이징처럼 데이터를 나누어 처리하는 방식은 아니다.

 

 

데이터베이스의 각 리뷰 데이터를 하나씩 꺼내어, 이를 DTO(데이터 전송 객체)인 ReviewResponse에 담아주는 작업을 수행해야 한다. 이를 위해 for 반복문을 사용하여 데이터를 순회하고,

 

반복문 내에서 새로운 객체를 생성하여 메모리를 확보한 뒤 데이터를 저장하는 방식으로 처리한다. 이 과정은 각각의 리뷰를 독립적으로 관리하기 위해 필요하다.

 

 

다 완료 됬으니 전체 데이터를 DTO로 만든다.

 

 

 

public ProductDetailResponse getproduct(Long productId){
     Optional<Product> product= productRepository.findById(productId);
     if (product.isEmpty()){
         throw new RuntimeException();
     }
     ProductDetailResponse productDetailResponse = new ProductDetailResponse();
     productDetailResponse.id = product.get().id;
     productDetailResponse.name = product.get().name;
     productDetailResponse.price = product.get().price;
     productDetailResponse.category = product.get().category;
     productDetailResponse.stockQuantity = product.get().stockQuantity;
     productDetailResponse.description = product.get().description;
     productDetailResponse.createdAt = product.get().createdAt.toString();

   productDetailResponse.averageRating  = reviewRepository.findAverageRatingByProductId(product.get().id);
   productDetailResponse.reviewCount=reviewRepository.countByProductId(product.get().id);

   //최근리뷰5개가져오기
  List<Review> reviewList =  reviewRepository.findTop5ByProduct_IdOrderByCreatedAtDesc(productId);

  //엔터티를 디티오로
   ArrayList<ReviewResponse> reviewResponsesList = new ArrayList<>();
     for(Review review : reviewList){
         ReviewResponse reviewResponse = new ReviewResponse();
         reviewResponse.id = review.id;
         reviewResponse.nickname = review.user.nickname;
         reviewResponse.rating = review.rating;
         reviewResponse.content = review.content;
         reviewResponse.createdAt = review.createdAt.toString();
         reviewResponsesList.add(reviewResponse);

     }
     ProductDetailReviewResponse response = new ProductDetailReviewResponse();
     response.product = productDetailResponse;
     response.recentReview = reviewResponsesList;

     return productDetailResponse;


 }

 

 

 

 

오른쪽과 동일한 이름을 사용하지 않으면 파일을 찾지 못해 403 오류가 발생한다. 이러한 사소다 생각한 부분도 정확히 일치시켜야 하며, 그렇지 않으면 접근이 차단될 수 있다. 이 때문에 오류를 찾느라 고생했다.

 

 

위에 부분을 고치니 위와 같이 잘 작동하는 것을 확인 할 수 있다.