[스파르타코딩클럽] Spring 심화반 - 4주차

hyeseungS·2022년 2월 21일
0
post-thumbnail

[수업 목표]
1. 스프링에서 DB를 다룰 때 사용되는 JPA(영속성 컨텍스트, JPA 연관관계)를 이해한다.
2. Spring Data JPA(Query Methods, 페이징 및 정렬)에 대해 이해한다.
3. 이를 '나만의 셀렉샵'에 폴더 기능을 추가하여 적용한다.

01. JPA 이해

1) ORM이란?

ORM : Object-Relational Mapping
웹서버에서와 DB에서 사용하는 언어가 다른데 이를 통용하는 역할

  • Object: "객체"지향 언어 (자바, 파이썬)
  • Relational: "관계형" 데이터베이스 (H2, MySQL)

  • 백엔드 개발자: 웹 서버를 개발하는 개발자
  • DBA (Database Administration): 데이터베이스 관리자. DB를 설치, 구성, 관리 등의 일을 맡는 사람

Questions)
1. ORM이 없이 웹 서버 개발은 못 하나?

  • ORM 없이도 충분히 웹 서버 개발 가능
  • ex) AllInOneControlelr에서 Repository 역할 분리
  • ORM 만든 이유?
    • ORM이 없는 환경에서는 백엔드 개발자가 비즈니스 로직 개발보다 SQL 작성에 더 많은 노력을 들여야 하더라..
    • SQL 작성이 단순하고 반복적인데, 실수하기 쉬움
  • 웹 서버 개발 언어(Java, Python, Javascript 등)와 관계형 데이터베이스 언어 (SQL)의 목적 및 사용 방법이 다름
  1. 그럼 이제 백엔드 개발자는 DB에 대해 몰라도 되나?
  • DB 테이블 설계, SQL Query 성능 확보 등을 위해 알아야함!

2) JPA란?

JPA: Java Persistence API
자바 ORM 기술에 대한 표준 명세 (자바의 ORM이다)

  • JPA ex)
    : 자바 언어의 코드가 JPA에 의해 DB 테이블로 생성된다.
    • 테이블 및 컬럼 매핑
      1. Java 클래스
      2. DB 테이블 및 컬럼
		@Entity // DB 테이블 역할을 합니다.
        public class User {
		    // ID가 자동으로 생성 및 증가합니다.
            @GeneratedValue(strategy = GenerationType.AUTO)
    	    @Id
    		private Long id;

    		// nullable: null 허용 여부
    		// unique: 중복 허용 여부 (false 일때 중복 허용)
    		@Column(nullable = false, unique = true)
    		private String username;

    		@Column(nullable = false)
    		private String password;

    		@Column(nullable = false, unique = true)
    		private String email;

    		@Column(nullable = false)
    		@Enumerated(value = EnumType.STRING)
    		private UserRoleEnum role;

    		@Column(unique = true)
    		private Long kakaoId;
        }

Questions)
1. JPA가 없으면?

  • 직접 SQL 문을 작성하여 구현 가능
  • ex) AllInOneController 통한 신규 상품 등록(1주차)
  • 실제로 과거엔 JPA 없이 웹 서버를 개발한 기업이 많았고, 현재도 유효
  1. JPA 사용 트렌드?
  • 과거엔 SQL 매퍼 (MyBatis, JdbcTemplate) 위주로 개발
  • 전 세계적으로 JPA 사용 빈도가 급격히 높아져 현재는 JPA가 대세!
  1. 하이버네이트 (Hibernate)?
  • JPA는 표준 명세(문서)이고, 이를 실제 구현한 프레임워크 중 사실상 표준(그 문서를 가지고 만든 구현체)
  • 스프링 부트에서 기본적으로 "하이버네이트" 사용 중(하이버네이트 외에도 많음, 하이버네이트에 의해 JPA 돌아감)

    사실상 표준 (de facto, 디팩토)
    보통 기업간 치열한 경쟁을 통해 시장에서 결정되는 비 공식적 표준
    출처: 위키백과


02. JPA 영속성 컨텍스트 이해

1) 영속성 컨텍스트란?

  • JPA
    • 객체 - ORM - DB
    • 객체 - 영속성 컨텍스트 매니저 (entity context manager) - DB
  • 영속성 컨텍스트 매니저
    • 객체와 DB의 소통을 효율적으로 관리
  • PK (Primary Key)
    • 테이블에서 각 row마다 가져야하는 유일무이한 값 (Null 허용되지 않음)
    • 샘플
    • 자연키 vs 인조키
      • 자연키 : USERNAME, EMAIL
      • 인조키 : ID
    • 보통 테이블 ID를 PK로 설정 (인조키)

2) 프로젝트 및 H2 Fild DB 세팅

  1. 인텔리제이에서 File > New > Project

  2. 검색창을 클릭 후, 5개 포함하기

  • Lombok, Spring Web, Spring Data JPA, H2 Database, MySQL Driver
  1. DB 관련 설정 변경
  • H2 In-memoery DB(스프링 멈추면 DB 사라짐) -> File DB
  • JPA에 의해 변경된 DB 요청 "SQL 쿼리"가 출력되게 설정
  • resources > application.properties
    • datasource.url이 다름
    • logging~ : 하이버네이트에 대해 SQL을 출력해달라
spring.h2.console.enabled=true
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.url=jdbc:h2:./myselectdb;AUTO_SERVER=TRUE
spring.datasource.username=sa
spring.datasource.password=

spring.jpa.properties.hibernate.format_sql=true
spring.jpa.hibernate.ddl-auto=update
spring.jpa.generate-ddl=true
logging.level.org.hibernate.SQL=debug
logging.level.org.hibernate.type.descriptor.sql=trace

server.port=8090
  1. 회원 테이블 위한 Entity 추가
  • model > User
    • Id를 자연키로 사용
      (name = "id" 하면 username이 테이블에선 id로 저장)
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;

@Setter
@Getter // get 함수를 일괄적으로 만들어줍니다.
@NoArgsConstructor // 기본 생성자를 만들어줍니다.
@Entity // DB 테이블 역할을 합니다.
public class User {
    // nullable: null 허용 여부
    // unique: 중복 허용 여부 (false 일때 중복 허용)
    @Id // PK다
    @Column(name = "id", nullable = false, unique = true)
    private String username;

    @Column(nullable = false, unique = false)
    private String nickname;

    @Column(nullable = false, unique = false)
    private String favoriteFood;

    public User(String username, String nickname, String favoriteFood) {
        this.username = username;
        this.nickname = nickname;
        this.favoriteFood = favoriteFood;
    }
}
  1. 스프링 서버 시작
  • myselectdb.mv.db 파일이 생성되었는지 확인
  1. Intellij - Database 설정 (DataSource - H2)
  • H2 File DB URL
jdbc:h2:file:./myselectdb;AUTO_SERVER=TRUE

  1. myselectdb에서 오른쪽 버튼 > Database Tools > Manage Shown Schemas > All databases > refresh

  2. PUBLIC > tables 확인

3) 영속성 컨텍스트 확인을 위한 테스트 코드 구현

  • 회원 관리를 위한 Controller, Service, Repository 테스트 코드 작성
  • controller > UserController
import com.sparta.jpa.model.User;
import com.sparta.jpa.repository.UserRepository;
import com.sparta.jpa.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {
    private final UserService userService;
    private final UserRepository userRepository;

    @Autowired
    public UserController(UserService userService, UserRepository userRepository) {
        this.userService = userService;
        this.userRepository = userRepository;
    }

    @GetMapping("/user/create")
    public void createUser() {
        User user = userService.createUser();

        // 테스트 회원 데이터 삭제
        userRepository.delete(user);
    }

    @GetMapping("/user/delete")
    public void deleteUser() {
        User user = userService.deleteUser();

        // 테스트 회원 데이터 삭제
        userRepository.delete(user);
    }

    @GetMapping("/user/update/fail")
    public void updateUserFail() {
        User user = userService.updateUserFail();

        // 중요!) DB 에 변경 내용이 적용되었는지 확인!
        // 테스트 회원 데이터 삭제
        userRepository.delete(user);
    }

    @GetMapping("/user/update/1")
    public void updateUser1() {
        User user = userService.updateUser1();

        // 테스트 회원 데이터 삭제 
        userRepository.delete(user);
    }

    @GetMapping("/user/update/2")
    public void updateUse2() {
        User user = userService.updateUser2();

        // 중요!) DB 에 변경 내용이 적용되었는지 확인!
        // 테스트 회원 데이터 삭제
        userRepository.delete(user);
    }
}
  • service > UserService
import com.sparta.jpa.model.User;
import com.sparta.jpa.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.GetMapping;

@Service
public class UserService {
    private final UserRepository userRepository;

    @Autowired
    public UserService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    public User createUser() {
        // 테스트 회원 "user1" 객체 추가
        User beforeSavedUser = new User("user1", "정국", "불족발");
        // 회원 "user1" 객체를 영속화
        User savedUser = userRepository.save(beforeSavedUser); 

        // beforeSavedUser: 영속화되기 전 상태의 자바 일반객체
        // savedUser:영속성 컨텍스트 1차 캐시에 저장된 객체
        assert(beforeSavedUser != savedUser);

        // 회원 "user1" 을 조회
        User foundUser1 = userRepository.findById("user1").orElse(null);
        assert(foundUser1 == savedUser);

        // 회원 "user1" 을 또 조회
        User foundUser2 = userRepository.findById("user1").orElse(null);
        assert(foundUser2 == savedUser);

        // 회원 "user1" 을 또또 조회
        User foundUser3 = userRepository.findById("user1").orElse(null);
        assert(foundUser3 == savedUser);

        return foundUser3;
    }

    public User deleteUser() {
        // 테스트 회원 "user1" 객체 추가
        User firstUser = new User("user1", "지민", "엄마는 외계인");
        // 회원 "user1" 객체를 영속화
        User savedFirstUser = userRepository.save(firstUser);

        // 회원 "user1" 삭제
        userRepository.delete(savedFirstUser);

        // 회원 "user1" 조회
        User deletedUser1 = userRepository.findById("user1").orElse(null);
        assert(deletedUser1 == null);

        // -------------------
        // 테스트 회원 "user1" 객체를 다시 추가
        // 회원 "user1" 객체 추가
        User secondUser = new User("user1", "지민", "엄마는 외계인");

        // 회원 "user1" 객체를 영속화
        User savedSecondUser = userRepository.save(secondUser);
        assert(savedFirstUser != savedSecondUser); // DB입장 완전 다른 것(DB와 1차캐시에 삭제했다가 새로 생성해 저장)
        assert(savedFirstUser.getUsername().equals(savedSecondUser.getUsername()));
        assert(savedFirstUser.getNickname().equals(savedSecondUser.getNickname()));
        assert(savedFirstUser.getFavoriteFood().equals(savedSecondUser.getFavoriteFood()));

        // 회원 "user1" 조회
        User foundUser = userRepository.findById("user1").orElse(null);
        assert(foundUser == savedSecondUser);

        return foundUser;
    }

    public User updateUserFail() {
        // 회원 "user1" 객체 추가
        User user = new User("user1", "뷔", "콜라");
        // 회원 "user1" 객체를 영속화
        User savedUser = userRepository.save(user);

        // 회원의 nickname 변경
        savedUser.setNickname("얼굴천재");
        // 회원의 favoriteFood 변경
        savedUser.setFavoriteFood("버거킹");

        // 회원 "user1" 을 조회
        User foundUser = userRepository.findById("user1").orElse(null);
        // 중요!) foundUser 는 DB 값이 아닌 1차 캐시에서 가져오는 값 (but, DB는 반영이 안됨)
        assert(foundUser == savedUser);
        assert(foundUser.getUsername().equals(savedUser.getUsername()));
        assert(foundUser.getNickname().equals(savedUser.getNickname()));
        assert(foundUser.getFavoriteFood().equals(savedUser.getFavoriteFood()));

        return foundUser;
    }

    public User updateUser1() {
        // 테스트 회원 "user1" 생성
        User user = new User("user1", "RM", "고기");
        // 회원 "user1" 객체를 영속화
        User savedUser1 = userRepository.save(user);

        // 회원의 nickname 변경
        savedUser1.setNickname("남준이");
        // 회원의 favoriteFood 변경
        savedUser1.setFavoriteFood("육회");

        // user1 을 저장
        User savedUser2 = userRepository.save(savedUser1);
        assert(savedUser1 == savedUser2);

        return savedUser2;
    }

    @Transactional
    public User updateUser2() {
        // 테스트 회원 "user1" 생성
        // 회원 "user1" 객체 추가
        User user = new User("user1", "진", "꽃등심");
        // 회원 "user1" 객체를 영속화
        User savedUser = userRepository.save(user);

        // 회원의 nickname 변경
        savedUser.setNickname("월드와이드핸섬 진");
        // 회원의 favoriteFood 변경
        savedUser.setFavoriteFood("까르보나라");

        return savedUser;
    }
}
  • repository > UserRepository
import com.sparta.jpa.model.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, String> {
}

4) Entity 조회 디버깅 방법

DB 변경내용 파악하기 위해 디버깅 (Debugging) 방법
1. UserServicecreateUser() 함수 밑에 줄의 Line 번호 있는 곳을 클릭
-> Break Point (디버깅할 위치) 설정
2. BP 빨간 원에서 마우스 오른쪽 버튼 클릭

  • Thread 클릭
  • Make Default 클릭
  1. 스프링 기동 시 꼭 디버깅 모드 선택!
  2. ARC 통해 "GET /user/create" 호출
  3. BP 에서 디버깅 가능한지 확인
  4. "Step Over"를 클릭하여 줄 이동 하면서 DB 테이블 변경 내용 확인

03. JPA 영속성 컨텍스트 1차 캐시 이해

1) 영속성 컨텍스트 1차 캐시

  • Entity 저장 시
- SQL 언어로 바뀌기 전에 추가적으로 1차 캐시가 만들어짐 
  (Id, Entity를 매핑해서 가지고 있음)
- save에서 만든 User 객체 != 1차 캐시의 Entity 안의 User 객체
- save 하는 순간 1차 캐시에 저장 및 DB에 insert
- return 값은 1차 캐시에 있는 값

  • Entity 조회 시
  1. 1차 캐시에 조회하는 Id가 존재하는 경우
  2. 1차 캐시에 조회하는 Id가 존재하지 않는 경우

'1차 캐시' 사용의 장점

  1. DB 조회 횟수를 줄임
- findById는 1차 캐시에 Id가 존재하면 가져오는 것 (Entity의 값 가져옴)
- 존재하지 않는 경우, SQL 쿼리를 날림 
                -> 찾아와 1차 캐시를 만들어 반환
  1. '1차 캐시'를 사용해 DB row 1개 당 객체 1개가 사용되는 것을 보장 (객체 동일성 보장)
    • 02의 UserService와 비교) '1차 캐시'가 작동하지 않을 때
      • 만약 DB 에서는 'user1' 회원 1명, 자바 객체로는 'user1' 회원 3명(foundUser 1,2,3)이라면??
      • 그리고, 각 객체 회원별로 수정한다면.. 어떤 'user1' 이 맞는걸까??
        -> 지금은 객체도 다르고 변수도 다름! (객체끼리 동일성이 보장이 안됨)
      • test > repository > UserRepositoryTest
    import com.sparta.jpa.model.User;
    import org.junit.jupiter.api.*;
	import org.springframework.beans.factory.annotation.Autowired;
	import org.springframework.boot.test.context.SpringBootTest;

	@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
	@TestInstance(TestInstance.Lifecycle.PER_CLASS)
	@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
	class UserRepositoryTest {
    	@Autowired
    	UserRepository userRepository;

    	@Order(1)
    	@Test
    	public void create() {
        	// 생성한 객체 3개는 모두 다 다른 객체 (1차캐시는 스프링 내부에서만 동작하기 때문)
        	// 회원 "user1" 객체 생성
        	User instance1 = new User("user1", "정국", "불족발");
        	// 회원 "user1" 객체 또 생성
        	User instance2 = new User("user1", "정국", "불족발");
        	assert(instance2 != instance1);
        	// 회원 "user1" 객체 또또 생성
        	User instance3 = new User("user1", "정국", "불족발");
        	assert(instance3 != instance2);
        
        	// 회원 "user1" 객체 추가
        	User user1 = new User("user1", "정국", "불족발");

        	// 회원 "user1" 객체의 ID 값이 없다가..
        	userRepository.save(user1);

        	// 테스트 회원 데이터 삭제
       		userRepository.delete(user1);
    	}

    	@Order(2)
    	@Test
    	public void findUser() {
        	// ------------------------------------
        	// 회원 "user1" 객체 추가
        	User beforeSavedUser = new User("user1", "정국", "불족발");

        	// 회원 "user1" 객체를 영속화
        	User savedUser = userRepository.save(beforeSavedUser); 

        	// 회원 "user1" 을 조회
        	User foundUser1 = userRepository.findById("user1").orElse(null);
        	assert(foundUser1 != savedUser); // 주소가 다름 (원래는 select 하지 않는데 DB로 select, 1차 캐시에 없는 것, 그래서 매번 객체를 새로 만든다)

        	// 회원 "user1" 을 또 조회
        	User foundUser2 = userRepository.findById("user1").orElse(null);
        	assert(foundUser2 != savedUser);

        	// 회원 "user1" 을 또또 조회
        	User foundUser3 = userRepository.findById("user1").orElse(null);
        	assert(foundUser3 != savedUser);

        	// ------------------------------------
        	// 테스트 회원 데이터 삭제
        	userRepository.delete(beforeSavedUser); 
    	}
	}

2) Entity 삭제

  • [UserService] Entity 삭제
    deleteUser()
- 1차 캐시는 DB와 계속 일관성을 맞추기 위해 노력
- delete 후에 findById하면 1차캐시 & DB 없으니까 NULL 반환

3) Entity 업데이트 삭제

  • Entity 객체를 수정해도 DB에는 update가 되지 않음
  • 1차 캐시 Entity 객체에만(자바 세상에서만) 업데이트 반영
  • User DB에는 반영 X
  • [UserService] Entity 업데이트 실패
    updateUserFail()
  • 객체와 DB 값 불일치 확인

4) Entity 업데이트 방법 (1)

  • userRepository.save() 사용
  • [UserService] Entity 업데이트 방법 (1)
    updateUser1()
- JPA가 알아서 DB에 없으면 insert하고, 있으면 update 함
- save하면 DB에 update query 날림

5) Entity 업데이트 방법 (2)

  • @Transactional을 추가

  • 굳이 userRepository.save() 함수를 호출하지 않아도, 함수가 끝나는 시점에 변경된 부분을 알아서 업데이트 해줌 (이를 "Dirty check" 라고 함)

  • 간단히 함수가 종료되는 시점에 각 Entity에 save()가 호출된다라고 이해

    정확한 이해 위해 필요한 추가학습 내용
    : 쓰기 지연 SQL 저장소, flush, commit 등

  • [UserSerivce] Entity 업데이트 방법 (2)
    updateUser2()

- SQL 문들을 모아 두었다가 한번에 실행

04. DB 의 연관관계 이해

1) DB 연관관계

  • JPA가 제공하는 연관관계는 결국 DB의 연관관계를 표현하기 위함
  • 따라서 먼저 DB의 연관관계를 이해해야 함
  • DB의 연관관계는 비즈니스 요구사항에 맞춰 이루어짐

2) 음식 주문앱 DB 설계 예제

예를 들어, 우리가 음식 주문앱 DB를 설계한다고 가정하자!
일단 "고객이 1개의 음식을 주문할 수 있다"라는 요구사항을 받았다고 해보자

  1. 일단 각 주체의 테이블 설계가 필요
  • 고객 (User) 테이블
  • 음식 (Food) 테이블
  1. 연관 관계 고민
  • 고객이 음식 주문 시, 주문 정보는 어느 테이블에 들어가야 할까?

  • 고객 테이블? 음식 테이블?

  • Tip) 테이블 설계 시 실제 값을 넣어봄

  • 시도1) "고객 테이블"에 주문 정보를 넣어보자!
    -> 문제점: 회원 중복

  • 시도2) "음식 테이블"에 주문 정보를 넣어보자!
    -> 문제점: 음식 중복

  • '주문'을 위한 테이블 필요 -> Order 테이블 추가
    (일단은 하나의 회원이 하나의 주문만 한다고 가정)

    • 회원 1명은 주문 N개를 할 수 있다.
      • 회원 : 주문 = 1 : N 관계
    • 음식 1개는 주문 N개에 포함될 수 있다.
      • 음식 : 주문 = 1 : N 관계
    • 결론적으로
      • 회원 : 음식 = N : N 관계
  • N : N의 관계를 가지기 위해서는 테이블 하나가 더 필요.


05. JPA 연관관계

1) JPA 연관관계 설정방법

JPA의 경우는 Entity 클래스의 필드 위에 연관관계 어노테이션 (@) 을 설정해주는 것만으로 연관관계가 형성됨.

  • '음식 배달 서버'를 개발한다고 가정

연관관계

관계코드 선언Entity
일대다 (1:N)@OneToManyOrder(1) : Food(N)
다대일 (N:1)@ManyToOneOwner(N) : Restaurant(1)
일대일 (1:1)@OneToOneOrder(1) : Coupon(1)
다대다 (N:N)@ManyToManyUser(N) : Restaurant(N)

2) JPA 코드 구현

중요) 항상 Entity 본인 중심으로 관계를 생각!

  • 주문 (Order) 코드
@Entity
public class Order {
    @OneToMany
    private List<Food> foods;

	@OneToOne
	private Coupon coupon;
}
- Order 입장에서 OneToMany(One은 자기자신을 의미)
  • 음식점주 (Owner)
@Entity
public class Owner {
	@ManyToOne
	Restaurant restaurant;
}
  • 고객 (User)
@Entity
public class User {
	@ManyToMany
	List<Restaurant> likeRestaurants;
}

06. Spring Data JPA 이해

1) Spring Data JPA는?

  • JPA를 편리하게 사용하기 위해, 스프링에서 JPA를 Wrapping
    (JPA도 자바 개발자들이 효율적으로 DB를 관리하기 위해 만들어짐)
  • 스프링 개발자들이 JPA를 사용할 때 필수적으로 생성해야 하나, 예상 가능하고 반복적인 코드들 -> Spring Data JPA 가 대신 작성
  • Repository 인터페이스만 작성하면, 필요한 구현은 스프링이 대신 알아서 해줌.

2) Spring Data JPA 예제

  • 상품 Entity 선언
@Entity
public class Product extends Timestamped {
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Id
    private Long id;
    private Long userId;
    private String title;
    private String image;
    private String link;
    private int lprice;
    private int myprice;
}
  • Spring Data JPA) 상품 Repository 생성
    (원래는 JPA로 저장하기 위해선는 Product 클래스 가지고 save해서 영속성 컨텍스트를 가지고 저장을 해줘야 DB로 저장이 됐었음
public interface ProductRepository extends JpaRepository<Product, Long> {
}
  • Spring Data JPA) 기본 제공해 주는 기능
// 1. 상품 생성
Product product = new Product(...);
productRepository.save(product);

// 2. 상품 전체 조회
List<Product> products = productRepository.findAll();

// 3. 상품 전체 개수 조회
long count = productRepository.count();

// 4. 상품 삭제
productRepository.delete(product);
- save는 JpaRepository > PagingAndSortRepository가 
  extends하는 CrudRepository에 있음
- 여기서 GoTo Implementation으로 구현체 가보면 SimpleJpaRepository에 있음(em: 영속성 컨텍스트 매니저[persist, merge등을 함])
- 원래는 JPA사용하면 이러한 것들 코드로 구현해주는 데 PK 어떤건 지만 알려주면 알아서 해준다
  
  • ID 외의 필드에 대한 추가 기능은 interface만 선언해 주면, 구현은 Spring Data JPA가 대신!
    (Query Methods : Spring Data JPA 이용해서 쿼리 만드는 방법)
public interface ProductRepository extends JpaRepository<Product, Long> {
    // (1) 회원 ID 로 등록된 상품들 조회
    List<Product> findAllByUserId(Long userId);

    // (2) 상품명이 title 인 관심상품 1개 조회
	Product findByTitle(String title);

	// (3) 상품명에 word 가 포함된 모든 상품들 조회
	List<Product> findAllByTitleContaining(String word);

	// (4) 최저가가 fromPrice ~ toPrice 인 모든 상품들을 조회
    List<Product> findAllByLpriceBetween(int fromPrice, int toPrice);
}
- By: By 다음에 오는 걸 가지고 조건이 들어감.
      By 다음이 인자로 넘어와야함. (이는 Product에 선언되어있어야함)
      (select 문 where 조건으로) 
      + And로 조건 추가 가능

07. 페이징 및 정렬 설계

1) 페이징 이해

  • 한 번에 수백 ~ 수억 개의 데이터를 내린다면?
    (네트워크 속도나 처리 능력에 따라 쉽게 받을 수 있진 않음)
  • 페이지네이션 (Pagination), 페이징 (Paging)
    (일부분만 보여줄 수 있도록)
  • Infinite Scroll 설명
    (페이지네이션이긴 함)

2) '나만의 셀렉샵'에 페이징 및 정렬 기능 추가 API 설계

  • UI
  • 상품 조회 API 수정 필요 (GET /api/products)
  • Client -> Server
  1. 페이징
    • page: 조회할 페이지 번호 (1부터 시작)
    • size: 한 페이지에 보여줄 상품 개수 (10개로 고정)
  2. 정렬
    • sortBy (정렬 항목)
      a. id : Product 테이블의 id
      b. title : 상품명
      c. lprice : 최저가
      d. createdAt : 생성일 (보통 id와 동일, 여기선 안함)
    • isAsc (오름차순?
      a. true : 오름차순 (asc)
      b. false : 내림차순 (desc)
  • Server -> Client
    • number : 조회된 페이지 번호 (0부터 시작,
      UI에서는 1부터 시작 / Client 넘겨줄 때는 0부터 시작,
      맞추기 위해 여기서는 클라이언트가 +1 하기로 약속)
    • content : 조회된 상품 정보 (배열)
    • size : 한 페이지에 보여줄 상품 개수 (요청한 것)
    • numberOfElements : 실제 조회된 상품 개수 (실제로 10개가 아닐 수 있음)
    • first : 첫 페이지인지? (boolean,
      첫 페이지면 왼쪽으로 더이상 갈 수 없어 비활성화 시켜야함)
    • last : 마지막 페이지인지? (boolean)
    • totalElement : 전체 상품 개수 (회원이 등록한 모든 상품의 개수)
    • totalPages : 전체 페이지 수
      totalPages = totalElement / size 결과를 소수점 올림
      1 / 10 = 0.1 =>1 페이지
      9 / 10 = 0.9 =>1페이지
      10 / 10 = 1 =>1페이지
      11 / 10 => 1.1 =>2페이지
      	

08. 페이징 및 정렬 구현

1) 페이징 및 정렬 구현

<1> 프론트엔드 개발자의 UI 작업 적용

  • resources > templates > index.html
  • resources > static > basic.js

<2> 스프링 서버 구현

  • controller > ProductController
    • 07.에서 이야기한 Client로부터 받아올 것
      (Client -> Server) 추가하기(RequestParam으로 받아옴)
    • Show Context Actions > Change Signature ... 하면 Service에 자동으로 추가됨
    • 추가로, page는 Client에서 +1된 것으로 넘어와서 -1 해야함
	// 로그인한 회원이 등록한 관심 상품 조회
	@GetMapping("/api/products")
    public Page<Product> getProducts(
            @RequestParam("page") int page,
            @RequestParam("size") int size,
            @RequestParam("sortBy") String sortBy,
            @RequestParam("isAsc") boolean isAsc,
            @AuthenticationPrincipal UserDetailsImpl userDetails
    ) {
        Long userId = userDetails.getUser().getId();
        page = page - 1;
        return productService.getProducts(userId, page, size, sortBy, isAsc);
    }

    // (관리자용) 전체 상품 조회
    @Secured(UserRoleEnum.Authority.ADMIN)
    @GetMapping("/api/admin/products")
    public Page<Product> getAllProducts(
            @RequestParam("page") int page,
            @RequestParam("size") int size,
            @RequestParam("sortBy") String sortBy,
            @RequestParam("isAsc") boolean isAsc
    ) {
				page = page -1;
        return productService.getAllProducts(page, size, sortBy, isAsc);
    }
  • service > ProductService
    • Query와 관련된 문제
    • Spring Data JPA가 페이징이 많이 필요하다는 것을 알고 구현해둠
    • Pageable 객체를 만들어서 넣어줌
 	// 회원 ID 로 등록된 상품 조회
    public Page<Product> getProducts(Long userId, int page, int size, String sortBy, boolean isAsc) {
    	// 아래 방향은 Sort.Direction에 정의되어있음 
        // (그 안에 ASC, DESC 가 있음)
        Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;
        // 스프링 프레임워크가 만든 Sort
        //  direction(오름차순 or 내림차순) & sortBy(무엇으로 sort 할 건지)
        Sort sort = Sort.by(direction, sortBy);
        // 넘겨줄 Pageable 객체
        // new PageRequest 대신에 static 함수인 PageRequest.of로 함
        // Pageable은 인터페이스, PageRequest는 구현체
        Pageable pageable = PageRequest.of(page, size, sort);

        return productRepository.findAllByUserId(userId, pageable);
    }

    // (관리자용) 상품 전체 조회
    public Page<Product> getAllProducts(int page, int size, String sortBy, boolean isAsc) {
        Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;
        Sort sort = Sort.by(direction, sortBy);
        Pageable pageable = PageRequest.of(page, size, sort);

        return productRepository.findAll(pageable); // 이것도 원래 있는 것
    }
  • repository > ProductRespository
    • List 대신 Page(페이지 관련 정보 반환해줌)로 바꾸고 Pageable 객체(페이징 관련 정보들 입력으로 받음, 스프링에서 이미 만든 것)를 넣음
  import com.sparta.springcore.model.Product;
  import org.springframework.data.domain.Page;
  import org.springframework.data.domain.Pageable;
  import org.springframework.data.jpa.repository.JpaRepository;

  public interface ProductRepository extends JpaRepository<Product, Long> {
      Page<Product> findAllByUserId(Long userId, Pageable pageable);
  } 

2) 테스트 데이터 생성

  • testdata > TestDataRunner
```java
import com.sparta.springcore.dto.ItemDto;
import com.sparta.springcore.model.Product;
import com.sparta.springcore.model.User;
import com.sparta.springcore.model.UserRoleEnum;
import com.sparta.springcore.repository.ProductRepository;
import com.sparta.springcore.repository.UserRepository;
import com.sparta.springcore.service.ItemSearchService;
import com.sparta.springcore.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import static com.sparta.springcore.service.ProductService.MIN_MY_PRICE;

// 스프링 빈으로 등록하겠다(스프링 기동 시에 스프링 빈으로 등록하겠다)
// ApplicationRunner 인터페이스 사용하겠다
@Component
public class TestDataRunner implements ApplicationRunner {

	// 원래는 Autowired 안했었음, 스프링이 권장하진 않음
    @Autowired
    UserService userService;

    @Autowired
    ProductRepository productRepository;

    @Autowired
    UserRepository userRepository;

    @Autowired
    PasswordEncoder passwordEncoder;

    @Autowired
    ItemSearchService itemSearchService;

	// run 하면, 스프링 기동 시에 아래 코드들이 실행된다
    @Override
    public void run(ApplicationArguments args) throws Exception {
        // 테스트 User 생성
        User testUser = new User("슈가", passwordEncoder.encode("123"), "sugar@sparta.com", UserRoleEnum.USER);
        testUser = userRepository.save(testUser);

        // 테스트 User 의 관심상품 등록
        // 검색어 당 관심상품 10개 등록
        createTestData(testUser, "신발");
        createTestData(testUser, "과자");
        createTestData(testUser, "키보드");
        createTestData(testUser, "휴지");
        createTestData(testUser, "휴대폰");
        createTestData(testUser, "앨범");
        createTestData(testUser, "헤드폰");
        createTestData(testUser, "이어폰");
        createTestData(testUser, "노트북");
        createTestData(testUser, "무선 이어폰");
        createTestData(testUser, "모니터");
    }

    private void createTestData(User user, String searchWord) throws IOException {
        // 네이버 쇼핑 API 통해 상품 검색
        List<ItemDto> itemDtoList = itemSearchService.getItems(searchWord);

        List<Product> productList = new ArrayList<>();

		// itemDtoList를 productList로 변환
        for (ItemDto itemDto : itemDtoList) {
            Product product = new Product();
            // 관심상품 저장 사용자
            product.setUserId(user.getId());
            // 관심상품 정보
            product.setTitle(itemDto.getTitle());
            product.setLink(itemDto.getLink());
            product.setImage(itemDto.getImage());
            product.setLprice(itemDto.getLprice());

            // 희망 최저가 랜덤값 생성
            // 최저 (100원) ~ 최대 (상품의 현재 최저가 + 10000원)
            int myPrice = getRandomNumber(MIN_MY_PRICE, itemDto.getLprice() + 10000);
            product.setMyprice(myPrice);

            productList.add(product);
        }

        productRepository.saveAll(productList); // 전부 DB에 저장된다
    }

    public int getRandomNumber(int min, int max) {
        return (int) ((Math.random() * (max - min)) + min);
    }
}
실행 후 네트워크 가서 확인해보면, 
- 요청 시에 product?sortBy=id&isAsc=true ... 로 요청됨
- 요청 결과로 온 reponse(preview) 확인해보기
	content : 상품 검색된 것
    나머지는 스프링이 만든 것

3) 테스트 코드 수정

- 원래는 pagenation 관련 테스트 코드 추가해야함
- But, 여기선 그냥 에러만 안나게 sample data로  
       page, size, sortBy, isAsc 선언만 해주기 
  • test > integration > ProductIntegrationTest
@Test
@Order(3)
@DisplayName("회원이 등록한 모든 관심상품 조회")
void test3() {
	// given
    int page = 0;
    int size = 10;
    String sortBy = "id";
    boolean isAsc = false;
    ...
  • test > integration > UserProductIntegrationTest
@Test
@Order(5)
@DisplayName("회원이 등록한 모든 관심상품 조회")
void test5() {
	// given
    int page = 0;
    int size = 10;
    String sortBy = "id";
    boolean isAsc = false;
    ...

09. '나만의 셀렉샵'에 폴더 기능 추가

  1. 배경
  • 회원들 중 페이지네이션 기능만으로 원하는 관심 상품을 쉽게 찾기 어렵다는 불만이 접수 됨
  • 폴더 별로 관심상품을 저장/관리할 수 있는 기능을 추가하기로 함
  1. 요구사항
    a. 폴더 생성

    • 회원별 폴더를 추가 O
    • 폴더를 추가할 때 1개 ~ N개를 한 번에 추가 O

    b. 관심상품에 폴더 설정

    • 하나의 관심상품에 폴더는 N개 설정 O
    • 관심상품이 등록되는 시점에는 어느 폴더에도 저장 X
    • 관심상품 별로 1번에서 생성한 폴더를 선택하여 추가 O

    c. 폴더 별 조회

    • 회원은 폴더별로 관심상품 조회 O
    • 조회 방법
      • '전체' 클릭 : 폴더와 상관 없이 회원이 저장한 전체 관심상품들을 조회 O
      • '폴더명' 클릭 : 폴더별 저장된 관심상품들을 조회 O

10. 폴더 테이블 설계

1) '폴더' 테이블 설계

  • 폴더 테이블에 필요한 정보
    1. 폴더명 : 회원이 등록한 폴더 이름 저장

    2. 회원 ID : 폴더를 등록한 회원의 ID를 저장

      • A 회원이 생성한 폴더는 A회원에게만 보여야함
      • 회원과 폴더의 관계
        -> '회원과 폴더'의 관계 = '회원과 관심상품' 관계

2) 기존 관계 설정 방법의 문제점 ('회원과 폴더')

  • 기존 관계설정 방법을 구현한 Folder 객체
@Entity
public class Folder {
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Id
    private Long id;

	@Column(nullable = false)
	private String name;

    @Column(nullable = false)
    private Long userId;
}
  • 폴더가 가지고 있는 userId가 "회원의 Id 값"이라는 사실은 "개발을 진행한 사람"만 알 수 있음
  • 객체 (Folder Entity 클래스)와 DB 모두에서 회원과 상품이 어떤 관계인지를 알 수 없음
  • 객체) 회원과 폴더의 관계
  • DB) 회원과 폴더의 관계
  • 회원 -> 폴더 조회
  1. user1이 저장한 모든 폴더 조회
// 1. 로그인한 회원 (user1) 의 id 를 조회
Long userId = user1.getId();
// 2. userId 로 저장된 모든 folder 조회
List<Folder> folders = folderRepository.findAllByUserId(userId);
  1. 이를 위해, Folder Repository에 userId를 기준으로 조회하는 함수 생성 필요
public interface FolderRepository extends JpaRepository<Folder, Long> {
    List<Folder> findAllByUserId(Long userId);
}
  • 폴더 -> 회원 조회
  1. folder1의 userId로 회원 조회
// 1. folder1 의 userId 를 조회
Long userId = folder1.getUserId();
// 2. userId 로 저장된 회원 조회
User user = userRepository.findById(userId);

-> 이를 JPA 연관관계로 해결할 수 있음


11. JPA 연관관계를 이용한 폴더 테이블 설계

1) 회원 Entity 관점

  • 회원 1명이 여러 개의 폴더를 가질 수 있음
  • "@OneToMany" 로 설정
public class User {
    @OneToMany
    private List<Folder> folders;
}
  • 회원이 가진 폴더들을 조회
List<Folder> folders = user.getFolders();
  • 기존 방식과 비교!
public class Folder {
    private Long userId;
}
  1. user1이 저장한 모든 폴더 조회
// 1. 로그인한 회원 (user1) 의 id 를 조회
Long userId = user1.getId();
// 2. userId 로 저장된 모든 folder 조회
List<Folder> folders = folderRepository.findAllByUserId(userId);
  1. 이를 위해, Folder Repository에 userId를 기준으로 조회하는 함수 생성 필요

2) 폴더 Entity 관점

  • 폴더 여러 개를 회원 1명이 가질 수 있음
  • "@ManyToOne"
public class Folder{
    @ManyToOne
    private User user;
}
  • 폴더를 소유한 회원을 조회
folder.getUser();

3) 객체의 관계를 맺어주면, DB의 관계 설정 맺어줌

  • 객체) 회원과 폴더의 관계
    폴더를 소유한 회원 id가 아닌 객체를 저장
    - 폴더에 user 객체 가진 것은 Has-a 관계라고함(객체가 객체를 가짐)
  • DB) 회원과 폴더의 관계
  • JPA 연관관계 Column 설정 방법
@ManyToOne
@JoinColumn(name = "USER_ID", nullable = false)
private User user;
  • @JoinColum 내 속성값 설정
    • name : 외래키 명

    • nullable : 외래키 null 허용 여부

      • false (default)
        • 예) 폴더는 회원에 의해서만 만들어짐.
          user 값이 필수
      • true
        • 예) 공용폴더의 경우, 폴더의 user 객체를 null로 설정하기로 함

12. 폴더 생성 기능 구현

1) 요구사항

  1. 회원별 폴더를 추가 O
  2. 폴더를 추가할 때 1개~N개를 한 번에 추가 O
  3. 회원별 저장한 폴더들이 조회되어야 함
  • API 설계

폴더 생성 및 조회

설명API입력출력
회원의 폴더 생성POST /api/foldersfoldersNames (JSON)
: 생성된 폴더명들
folders (JSON)
: 생성된 폴더의 정보들
회원의 폴더 조회GET /index.html
model 추가 -> folders

2) UI 작업 적용

  • resources > static > basic.js
  • reousrces > static > css > style.css
  • resources > template > index.html -> 타임리프

3) 서버 구현

  1. 회원의 폴더 생성
  • model > Folder
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.*;

@Setter
@Getter // get 함수를 일괄적으로 만들어줍니다.
@NoArgsConstructor // 기본 생성자를 만들어줍니다.
@Entity // DB 테이블 역할을 합니다.
public class Folder {

    // ID가 자동으로 생성 및 증가합니다.
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Id
    private Long id;

    @Column(nullable = false)
    private String name;

    @ManyToOne
    @JoinColumn(name = "USER_ID", nullable = false)
    private User user;

	// 클라이언트로부터 폴더 명이랑 사용자 정보 넘어온다
    public Folder(String name, User user) {
        this.name = name;
        this.user = user;
    }
}
  • dto > FolderRequestDto
import lombok.Getter;

import java.util.List;

@Getter
public class FolderRequestDto {
    List<String> folderNames;
}
  • controller > FolderController
import com.sparta.springcore.dto.FolderRequestDto;
import com.sparta.springcore.model.Folder;
import com.sparta.springcore.model.User;
import com.sparta.springcore.security.UserDetailsImpl;
import com.sparta.springcore.service.FolderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

// JSON 형태이므로
@RestController
public class FolderController {
    private final FolderService folderService;

	// FolderService DI 받기
    @Autowired
    public FolderController(FolderService folderService) {
        this.folderService = folderService;
    }

    @PostMapping("api/folders")
    public List<Folder> addFolders(
            @RequestBody FolderRequestDto folderRequestDto,
            @AuthenticationPrincipal UserDetailsImpl userDetails
    ) {
    	// 그냥 DTO 대신에 Entity 객체를 List로 넘겨주자
        List<String> folderNames = folderRequestDto.getFolderNames();
        User user = userDetails.getUser();

        List<Folder> folders = folderService.addFolders(folderNames, user);
        return folders;
    }
}
  • service > FolderService
import com.sparta.springcore.model.Folder;
import com.sparta.springcore.model.User;
import com.sparta.springcore.repository.FolderRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
public class FolderService {

    private final FolderRepository folderRepository;

    @Autowired
    public FolderService(FolderRepository folderRepository) {
        this.folderRepository = folderRepository;
    }
    
    // 로그인한 회원에 폴더들 등록
    public List<Folder> addFolders(List<String> folderNames, User user) {
        List<Folder> folderList = new ArrayList<>();

        for (String folderName : folderNames) {
            Folder folder = new Folder(folderName, user);
            folderList.add(folder);
        }

        return folderRepository.saveAll(folderList);
    }

    // 로그인한 회원이 등록된 모든 폴더 조회
    public List<Folder> getFolders(User user) {
        return folderRepository.findAllByUser(user); // model에 추가 위함
    }
}
  • repository > FolderRepository
import com.sparta.springcore.model.Folder;
import com.sparta.springcore.model.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface FolderRepository extends JpaRepository<Folder, Long> {
    List<Folder> findAllByUser(User user); // model에 추가하기 위함
}
  1. 회원이 저장한 폴더 조회
  • controller > HomeController
import com.sparta.springcore.model.Folder;
import com.sparta.springcore.model.UserRoleEnum;
import com.sparta.springcore.security.UserDetailsImpl;
import com.sparta.springcore.service.FolderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;

@Controller
public class HomeController {

    private final FolderService folderService;

    @Autowired
    public HomeController(FolderService folderService) {
        this.folderService = folderService;
    }


    @GetMapping("/")
    public String home(Model model, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        model.addAttribute("username", userDetails.getUsername());

        if (userDetails.getUser().getRole() == UserRoleEnum.ADMIN) {
            model.addAttribute("admin_role", true);
        }

		// UI에 반영되도록
        List<Folder> folderList = folderService.getFolders(userDetails.getUser());
        model.addAttribute("folders", folderList);

        return "index";
    }
}

13. 관심상품에 폴더 추가 구현

1) 요구사항

  1. 하나의 관심상품에 폴더를 0개 ~ N개 설정 O
  2. 관심상품이 등록되는 시점에는 어느 폴더에도 저장 X
  3. 관심상품 별로 1번에서 생성한 폴더를 선택하여 추가 O
  • 폴더 전체 조회 및 선택
    -> 폴더 추가 시 전체 폴더 조회해야함
    (12에 HomeController에서 만들었었음)
  • 폴더와 상품의 연관관계는?
    • 상품 1개에 여러개의 폴더 저장 가능
    • 폴더 1개에 여러개의 상품 저장 가능
    • 결론적으로
      • 상품 : 폴더 = N : N

2) API 설계 및 구현

폴더 생성 및 조회

설명API입력출력
폴더 전체 조회GET /api/foldersList<Folder>
폴더 추가POST /api/products/{productId}/folder{productId}: 관심상품 ID

[Form 형태]
folderId: 추가할 폴더
폴더가 추가된 관심상품 ID
상품 조회 시
폴더정보 추가
GET /api/productsproduct 정보에 folderList 추가
-> 폴더 추가 : 관심 상품에 추가하는 것, folderId는 Form 형태로 Body에 넣어줌
  • controller > FolderController
// 회원이 등록한 모든 폴더 조회
@GetMapping("api/folders")
public List<Folder> getFolders(
	@AuthenticationPrincipal UserDetailsImpl userDetails
) {
	return folderService.getFolders(userDetails.getUser());
}
  • controller > ProductController
    : 관심상품에 폴더 추가되는 거니까 ProductController에 추가
// 상품에 폴더 추가
@PostMapping("/api/products/{productId}/folder")
public Long addFolder(
	@PathVariable Long productId, // 주소 안에 있는 거 가져옴
	@RequestParam Long folderId, // Form 형태로 받아오는 것
	@AuthenticationPrincipal UserDetailsImpl userDetails
) {
	User user = userDetails.getUser();
	Product product = productService.addFolder(productId, folderId, user);
	return product.getId(); // Client에게 보내줄 것
}
  • service > ProductService
@Transactional // 객체에 저장된 애를 DB에 적용하기 위해 필요
public Product addFolder(Long productId, Long folderId, User user) {
	// 1) 상품을 조회합니다.
	Product product = productRepository.findById(productId)
			.orElseThrow(() -> new NullPointerException("해당 상품 아이디가 존재하지 않습니다."));

	// 2) 관심상품을 조회합니다.
    Folder folder = folderRepository.findById(folderId)
            .orElseThrow(() -> new NullPointerException("해당 폴더 아이디가 존재하지 않습니다."));

    // 3) 조회한 폴더와 관심상품이 모두 로그인한 회원의 소유인지 확인합니다.
    Long loginUserId = user.getId();
    if (!product.getUserId().equals(loginUserId) || !folder.getUser().getId().equals(loginUserId)) {
    	throw new IllegalArgumentException("회원님의 관심상품이 아니거나, 회원님의 폴더가 아닙니다~^^");
     }

    // 4) 상품에 폴더를 추가합니다.
    product.addFolder(folder);

    return product;
}
- getProducts 함수에서 추가로 folderRepository에서 get 할 
  필요 없이 자동으로 Product 가져올 때 folderList 가져옴 
  (Product에서 ManyToMany 해서)
- getProducts에서 ProductController로 Page<Product>로 넘겨줘 
  JSON으로 변환하는 순간에 Folder들이 들어옴
  • model > Product
    : Product마다 folderList를 넣어줌
@ManyToMany // fetch 타입이 있음(fetch= 이렇게 할 수도 있음)
private List<Folder> folderList;

// Product에 폴더를 추가
public void addFolder(Folder folder) {
	this.folderList.add(folder);
}

-> N:N 관계를 해서 DB에 PRODUCT_FOLDER_LIST가 자동으로 생김(중간의 관계를 맺음)


14. 폴더 별 관심상품 조회 구현

1) 요구사항

  1. 회원은 폴더 별로 관심상품 조회 O
  2. 조회 방법
  • '전체' : 폴더와 상관 없이 회원이 저장한 전체 관심상품들을 조회 O
  • '폴더별' : 폴더 별 저장된 관심상품들을 조회 O

2) API 설계 및 구현

  • '전체' 상품 조회는 이미 구현 완료!!!("GET /api/products")

폴더 별 관심상품 조회

설명API입력출력
폴더 별 상품 조회GET /api/folders/{folderId}/products{folderId} : 조회를 원하는 폴더 idPage<Product>

-> Pagenation은 그대로 적용해야함
(Spring data JPA 이용, folderId & size 등등 들어가야함)

  • controller > FolderController
// 회원이 등록한 폴더 내 모든 상품 조회
@GetMapping("api/folders/{folderId}/products")
public Page<Product> getProductsInFolder(
	@PathVariable Long folderId,
    @RequestParam int page,
    @RequestParam int size,
    @RequestParam String sortBy,
    @RequestParam boolean isAsc,
    @AuthenticationPrincipal UserDetailsImpl userDetails
) {
	page = page - 1;
    return folderService.getProductsInFolder(
		folderId,
        page,
        size,
        sortBy,
        isAsc,
        userDetails.getUser()
	);
}
  • service > FolderService
    : FolderService, ProductService 상관 없음
// 회원 ID 가 소유한 폴더에 저장되어 있는 상품들 조회
public Page<Product> getProductsInFolder(
	Long folderId,
    int page,
    int size,
    String sortBy,
    boolean isAsc,
    User user
) {
	Sort.Direction direction = isAsc ? Sort.Direction.ASC : Sort.Direction.DESC;
    Sort sort = Sort.by(direction, sortBy);
    Pageable pageable = PageRequest.of(page, size, sort);
    Long userId = user.getId();
    return productRepository.findAllByUserIdAndFolderList_Id(userId, folderId, pageable);
}
  • repository > ProductRepository
    : Folder 안의 Product 가져오는 방법
import com.sparta.springcore.model.Product;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product, Long> {
    Page<Product> findAllByUserId(Long userId, Pageable pageable);
    // folderList_Id는 결국 folderList에서의 id를 가지고 조회
    // folderId
    Page<Product> findAllByUserIdAndFolderList_Id(Long userId, Long folderId, Pageable pageable);
}

15. 중복 폴더명 생성 이슈

문제점 : 현재 폴더명이 중복해서 생성되고 있음

  • 해결방법: 생성할 폴더명이 입력으로 들어왔을 때, DB에 동일 폴더명이 없는 경우에만 생성

  • 수정할 파일 : FolderService.java, FolderRepository.java

  • 추가로 테스트 코드 에러 해결(ProductServiceTest.java)

  • service > FolderService

// 로그인한 회원에 폴더들 등록
public List<Folder> addFolders(List<String> folderNames, User user) {
	// 1) 입력으로 들어온 폴더 이름을 기준으로, 회원이 이미 생성한 폴더들을 조회합니다.
	List<Folder> existFolderList = folderRepository.findAllByUserAndNameIn(user, folderNames);

	List<Folder> folderList = new ArrayList<>();
    for (String folderName : folderNames) {
    	// 2) 이미 생성한 폴더가 아닌 경우만 폴더 생성
        if (!isExistFolderName(folderName, existFolderList)) {
        	Folder folder = new Folder(folderName, user);
            folderList.add(folder);
        }
    }

    return folderRepository.saveAll(folderList);
}
  • repository > FolderRepository
import com.sparta.springcore.model.Folder;
import com.sparta.springcore.model.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface FolderRepository extends JpaRepository<Folder, Long> {
    List<Folder> findAllByUser(User user);
    List<Folder> findAllByUserAndNameIn(User user, List<String> names);
}

결과


마치며

4주차에서는 앞서 언급만 했었던 Spring Data JPA에 대해 자세하게 배웠다. 데이터베이스 수업에서 배우던 연관관계를 직접 DB를 통해 보면서 실습하게 되어 흥미로웠다. 복습하면서 이해하기 어려운 부분이 많아서 강의를 다시 들으면서 정리해보았다. 드디어 1주 남았다.. 아즈아😬
출처: 스파르타 코딩 클럽


5주차

[스파르타코딩클럽] Spring 심화반 - 5주차


profile
Studying!!

0개의 댓글