Spring 심화반 - 4주차

귀찮Lee·2022년 4월 22일
0
post-thumbnail

2022년 4월 16일(토)
[스파르타코딩클럽] Spring 심화반 - 4주차 - 1

◎ JPA 이해

  • ORM : Object-Relational Mapping

    • Object: 객체 지향 언어 (Java, Python)
    • Relational: 관계형 데이터베이스 (H2, MySQL)
    • 백엔드 개발자(Backend Developer): 웹 서버를 개발하는 개발자
    • DBA (Database Administration): 데이터베이스 관리자. 데이터베이스를 설치, 구성, 관리 등의 일을 맡은 사람
      -> 다수의 회사에서 백엔드 개발자가 데이터베이스도 관리함

    cf) ORM 없이도 개발 가능, 백엔드 개발자도 DB 에 대해 잘 알고 있어야 함 (DB 테이블 설계, SQL 성능 확보 등의 사유로)

  • 영속성 컨텍스트

    • JPA
      • 객체 - ORM - DB
      • 객체 - 영속성 컨텍스트 매니져 (entity context manager) - DB
    • 영속성 컨텍스트 매니져
      • 객체와 DB의 소통을 효율적으로 관리
    • PK (Primary Key) : 테이블에서 각 row 마다 가져야 하는 유일무이한 값 (Null 허용되지 않음)
      • 자연키: USERNAME, EMAIL (외부에서 nullable = false, unique = true 인 값을 넣어줌)
      • 인조키: ID / @GeneratedValue(strategy = GenerationType.AUTO) 한 값
      • 보통 테이블 ID를 PK로 설정(인조키)

◎ Entity 조회 디버깅 방법

  • JPA 영속성 컨텍스트 1차 캐시 이해 예시 코드에 사용
  1. 함수 밑에 줄의 Line 번호 있는 곳을 클릭 ⇒ 브레이크 포인트 (디버깅할 위치) 설정

  2. 브레이크 포인트 빨간 동그라미에서 마우스 오른쪽 버튼 클릭 / Thread 클릭, Make Default 클릭

  3. 스프링 기동 시 디버깅 모드 선택

  4. 브레이크 포인트 부분에서 디버깅 가능한지 확인

  5. 데이터베이스 로그 세팅

    • DB 관련 설정 변경 : H2 In-memory DB → File DB / JPA 에 의해 변경된 DB 요청 "SQL 쿼리"가 출력되게 설정
    // resources > application.properties
     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
    • 오른쪽 탭 Database에서 New > DataSource > H2 고르기
    • URL : jdbc:h2:file:./myselectdb;AUTO_SERVER=TRUE
    • 스프링 서버 시작해서 myselectdb.mv.db 파일이 생성되었는지 확인
  6. "Step Over" 를 클릭해서 줄 이동을 하면서 DB 테이블 변경내용,

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

  • Entity 저장 시

    • 영속성 컨텍스트 안 1차 캐시에 (@Id ,Entity) 형식으로 저장
    • SQL에 insert query를 요청함
    • User savedUser = save(user)의 리턴값에 값을 돌려줌
      ※ 이때 user 객체와 savedUser 객체는 안에 값은 같아도, 서로 다른 객체이다.
  • Entity 조회 시

    • 예를 들어 User found = findById("0")을 요청
    • 영속성 컨텍스트 안 1차 캐시에 나용이 있다면,
      1차 캐시에서 내용을 가져옴
    • 영속성 컨텍스트 안 1차 캐시에 나용이 없다면,
      1차 캐시에 내용 저장, 1차 캐시에서 내용을 가져옴
    • User found1 = findById("0"), User found2 = findById("0")를 사용한다면,
      found1과 found2는 같은 객체임
    • test 폴더 내에서 한다면 서로 다른 객체가 됨 (test 폴더 내에서는 1차 캐시를 사용하지 않는 것이 기본값임)
  • '1차 캐시' 사용의 장점

    • DB 조회 횟수를 줄임
    • '1차 캐시'를 사용해 DB row 1개 당 객체 1개가 사용되는 것을 보장
  • Entity 삭제

    • 영속성 컨텍스트 안 1차 캐시에 해당 내용 삭제
    • SQL에 delete query를 요청함
  • Entity 업데이트 방법

    • Entity 객체를 setter를 이용해 수정한다면, 1차 캐시에만 해당 내용이 반영됨
      • findById와 같은 함수를 사용한다면 1차 캐시에 값이 나오므로 실제 클라이언트에 반영된 내용을 보냄, 그러나 DB에는 해당 내용이 반영되지 않음!!
    • 방법1. user를 가져와서 setter를 통해 값을 바꾸고, save(user) 를 함
      • SQL에 update query를 요청함 [같은 함수를 사용해도, JPA가 판단하여 query를 다르게 보냄]
      • User savedUser = save(user)의 리턴값에 값을 돌려줌
    • 방법2. @Transactional을 추가한다면, save()를 추가하지 않아도 반영됨
      • 함수가 실행 중에 반영사항을 모아서, 끝날 때 변경된 사항등을 DB에 적용시킨다고 생각해도 무방하다.

◎ DB, JPA 연관관계 이해

  • DB의 연관관계 필요성

    • 유저 테이블과 음식 테이블이 있는 상황에서 "주문" 데이터를 저장한다고 생각하면,
    • 유저 한명이 여러개의 음식을 시킬 수 있음
    • 음식 한 종류를 여러명이 시킬수 있음
      -> N:N 매칭이 되는 테이블을 만들 필요성이 생김
  • JPA 연관관계 설정방법

    • JPA 의 경우는 Enitity 클래스의 필드 위에 연관관계 어노테이션 (@) 을 설정해 주는 것만으로 연관관계가 형성
    • Entity 예시
    @Enitity
    public class Order {
        @OneToMany
        private List<Food> foods;
    
            @OneToOne
            private Coupon coupon;
    }
    ---------------------------------------------
    @Entity
    public class User {
        @ManyToMany
        List<Restaurant> likeRestaurants;
    }
  • 구현시 Database 상황

    • ManyToOne
    • ManyToMany

◎ Spring Data JPA 이해

  • Spring Data JPA에서는

    • JPA 를 편리하게 사용하기 위해, 스프링에서 JPA 를 Wrapping
    • 연관관계를 만드는데 반복적인 코드를 사용하므로, Spring Data JPA가 대신 작성
    • Repostiory 인터페이스만 작성하면, 필요한 구현은 스프링이 대신해준다.
  • 기본제공 기능 예시

// 1. 상품 저장
Product product = new Product(...);
productRepository.save(product);

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

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

// 4. 상품 삭제
productRepository.delete(product);
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);
}
    // (5) 연관관계가 있을경우, 포함된 객체 안의 값을 읽고 싶을 때는 _ 사용 (페이지화 사용 아래 참고)
    Page<Product> findAllByUserIdAndFolderList_Id(Long userId, Long folderId, Pageable pageable);
    // (6) 찾고자 하는 대상이 여러개 일때(Name : ["신발", "바지"]), 뒤에 In을 사용  
    List<Folder> findAllByNameIn(List<String> folderNames);
  • 구현 예시
  • ManyToMany 예시
// model
@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;
    }
}
// RequestDto
@Getter
public class FolderRequestDto {
    List<String> folderNames;
}
// Controller
@RestController
public class FolderController {
    private final FolderService folderService;

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

    @PostMapping("api/folders")
    public List<Folder> addFolders(
            @RequestBody FolderRequestDto folderRequestDto,
            @AuthenticationPrincipal UserDetailsImpl userDetails
    ) {
        List<String> folderNames = folderRequestDto.getFolderNames();
        User user = userDetails.getUser();

        List<Folder> folders = folderService.addFolders(folderNames, user);
        return folders;
    }
}
// Service
@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);
    }
}
// Repository
public interface FolderRepository extends JpaRepository<Folder, Long> {
    List<Folder> findAllByUser(User user);
}

◎ 페이징 및 정렬

  • 페이징의 필요성
    • 데이터가 매우 많을 때, 한번에 전부 데이터를 주면 굉장히 비효율적이다.
    • Google에는 페이지를 나누어, youtube에는 무한 스크롤 등을 이용해 페이징을 함
  • 페이징 구현
import org.springframework.data.domain.Page;
    // 로그인한 회원이 등록한 관심 상품 조회
    @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);
    }
// Service
    // 회원 ID 로 등록된 상품 조회
    public Page<Product> getProducts(Long userId, 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.findAllByUserId(userId, pageable);
    }
// Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
    Page<Product> findAllByUserId(Long userId, Pageable pageable);
}
profile
배운 것은 기록하자! / 오류 지적은 언제나 환영!

0개의 댓글