[TIL] 2024-08-23

성장일기·2024년 8월 25일

회고

목록 보기
36/37

중요 학습 내용 [JPQL, Spring Data JPA]

JPQL

  • JPQL vs 순수 JPA
    • 순수 JPA: 기본적으로 다중행 조회 불가능
      • JPQL 사용 시 다중행 조회 가능
  • JPQL 사용 이유
      1. 다중행 조회를 위해
      1. 특정 트랜잭션 전에 INSERT가 종료되어야 하기에
  • JPQL 사용 시, Dialect를 통해 DBMS에 국한되지 않고 쿼리를 작성하면 NativeQuery에 맞게 작성한다.(간단한 Query에 국한)
  • JPQL 사용 시, default로 JPQL 사용의 전/후로 transaction을 종료시킬 수 있다.
    • EntityManager.setFlushMode(FlushModeType.AUTO)
    • EntityManager.setFlushMode(FlushModeType.COMMIT)
      • 동일한 트랜잭션으로 처리
  • TypedQuery 단일행 단일열 조회
    @Test
    public void TypedQuery를_이용한_단일행_단일열_조회_테스트() {
        String jpql = "SELECT menuName FROM Menu WHERE menuCode = 6";
        TypedQuery<String> query = em.createQuery(jpql, String.class);
        String resultMenuName = query.getSingleResult();

        assertEquals(resultMenuName, "생마늘샐러드");
    }
  • Query 단일행 단일열 조회
    @Test
    public void Query를_이용한_단일행_단일열_조회_테스트() {
        String jpql = "SELECT menuName FROM Menu WHERE menuCode = 6";
        Query query = em.createQuery(jpql, String.class);
        Object resultMenuName = query.getSingleResult();

        assertEquals(resultMenuName, "생마늘샐러드");
    }
  • TypedQuery 다중행 다중열 조회
    @Test
    public void TypedQuery를_이용한_다중행_다중열_조회_테스트() {
        // Jpql 에서는 : entity의 별칭을 적으면 모든 속성(컬럼)을 조회
        String jpql = "SELECT m FROM Menu as m";
        TypedQuery<Menu> query = em.createQuery(jpql, Menu.class);
        List<Menu> foundMenuList = query.getResultList();

        assertTrue(!foundMenuList.isEmpty());
    }
  • distinct 중복제거 여러행 조회
    @Test
    public void distinct를_활용한_중복제거_여러_행_조회_테스트() {
        String jpql = "SELECT DISTINCT m.categoryCode FROM Menu m";
        TypedQuery<Integer> query = em.createQuery(jpql, Integer.class);
        List<Integer> categoryCodeList = query.getResultList();

        assertTrue(!categoryCodeList.isEmpty());
    }
  • in 조회
    @Test
    public void in_연산자를_활용한_조회_테스트() {
        String jpql = "SELECT m FROM Menu m WHERE m.categoryCode IN (6, 10)";
        List<Menu> foundMenuList = em.createQuery(jpql, Menu.class).getResultList();
        assertTrue(!foundMenuList.isEmpty());
        foundMenuList.forEach(System.out::println);
    }
  • like 조회
    @Test
    public void like_연산자를_활용한_조회_테스트() {
        String jpql = "SELECT m FROM Menu m WHERE m.menuName LIKE '%마늘%'";
        List<Menu> foundMenuList = em.createQuery(jpql, Menu.class).getResultList();
        assertTrue(!foundMenuList.isEmpty());
    }

프로젝션

  • SELECT 절에 조회할 대상을 지정 가능
  • 가능한 모든 필드를 받기를 권장(후처리가 번거롭기에)
  1. 엔티티 프로젝션

    • 원하는 객체를 바로 조회 가능
    • 조회된 엔티티는 영속성 컨텍스트가 관리
  2. 임베디드 타입 프로젝션

    • 엔티티와 거의 비슷하게 사용되며 조회의 시작점이 될 수 있음
    • 엔터티 타입이 아닌 값 타입으로 조회한 임베디드 타입은 영속성 컨텍스트에서 관리되지 않음
  3. 스칼라 타입 프로젝션

    • 숫자, 문자, 날짜 같은 기본 데이터 타입
    • 스칼라 타입은 영속성 컨텍스트에서 관리되지 않음
  4. new 명령어를 활용한 프로젝션

    • 다양한 종류의 단순 값들을 DTO로 바로 조회하는 방식으로 new 패키지명.DTO명을 쓰면 해당 DTO로 바로 반환
    • new 명령어를 사용한 클래스의 객체는 엔터티가 아니기에 영속성 컨텍스트에서 관리X

Spring Data JPA

  • Spring

    • Spring Data(Module)

      • Spring Data Mongo
      • Spring Data ElasticSearch
        ...
      • Spring Data JPA
        • Repository Interface를 활용하여 QueryMethod를 호출하여 Query 작성 가능
    • ...

  • 순수 JPA에서의 persist() -> save(for Spring Data JPA)

상속 관계

Repository Interface <- CrudRepository Interface
CrudRepository Interface <- PagingAndSortingRepository Interface
PagingAndSortingRepository Interface <- JpaRepository Interface

Repository

  • Runtime 시점에 JpaRepository를 상속받은 Interface의 하위 구현체가 생성됨
    • 해당 하위 구현체(객체)는 자동으로 Bean으로 등록됨
  • [Repository.class]
// 마크형 인터페이스
@Indexed
public interface Repository<T, ID> {
// T: Entity Type
// ID: PK Type
}
  • [MenuRepository.java]
public interface MenuRepository extends JpaRepository<Menu, Integer> {
    List<Menu> findByMenuPriceGreaterThan(int menuPrice);
}

Query Method

  • 내부적으로 method 이름을 parsing하여 JPQL 작성
  • Sort Type
  • [MenuService.java]
    public List<MenuDTO> findMenuList() {
        return menuRepository.findAll(Sort.by("menuCode").descending())    // sort type 지정
                .stream()
                .map(menu -> mapper.map(menu, MenuDTO.class))
                .collect(Collectors.toList());
    }
  • [MenuRepository.java]
    public interface MenuRepository extends JpaRepository<Menu, Integer> {
    }

ModelMapper

  • [build.gradle]
    ...
    // https://mvnrepository.com/artifact/org.modelmapper/modelmapper
    implementation 'org.modelmapper:modelmapper:3.1.1'
    ...
  • [..config.AppConfig]
@Configuration
public class AppConfig {

    // 엔티티와 DTO 매핑을 위한 modelmapper 라이브러리 bean 객체 등록(DI 추가)
    @Bean
    public ModelMapper modelMapper() {
        return new ModelMapper();
    }
}
  • [MenuService]
@Service
public class MenuService {
    private final ModelMapper mapper;
    private final MenuRepository menuRepository;

    @Autowired
    public MenuService(ModelMapper mapper, MenuRepository menuRepository) {
        this.menuRepository = menuRepository;
        this.mapper = mapper;
    }

    public MenuDTO findMenuByCode(int menuCode) {
        Menu menu = menuRepository.findById(menuCode).orElseThrow(IllegalAccessError::new);

        return mapper.map(menu, MenuDTO.class);
    }
}
  • mapping이 쉬울 때만 가능
    • 따라서 필요 시, 별도로 매핑 작업 필요
  • 이후 유지보수 시, 커스텀이 어렵기에, 사용하지 않는 것이 좋을 것 같다.
  • 내부적으로 setter를 활용

Pagable(one of Paging)

  • 스크롤 압박 시, 추가 데이터는 클라이언트 요청에 따라 paging 처리

  • [common.Pagination.java]

    • PagingButtonInfo(버튼 생성에 필요한 정보) 생성 및 반환
public class Pagination {
    public static PagingButtonInfo getPagingButtonInfo(Page page) {
        int currentPage = page.getNumber() + 1;     // 인덱스 개념 -> 실제 페이지 번호
        int defaultButtonCount = 10;                // 한 페이지에 보일 버튼 최대 갯수
        int startPage;                          // 한 페이지에 보여질 첫 버튼
        int endPage;                           // 한 페이지에 보여질 마지막 버튼

        startPage = (int) (Math.ceil((double) currentPage/defaultButtonCount) -1) * defaultButtonCount + 1;

        endPage = startPage + defaultButtonCount - 1;

        // endPage에 대한 예외상황 처리
        if (page.getTotalPages() < endPage) {
            endPage = page.getTotalPages();
        }
        // 아예 메뉴가 없거나 10개도 안될 때
        if (page.getTotalElements() < endPage) {
            endPage = startPage;
        }
        return new PagingButtonInfo(currentPage, startPage, endPage);
    }
}
  • [common.PagingButtonInfo.java]
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
@ToString
public class PagingButtonInfo {
    private int currentPage;
    private int startPage;
    private int endPage;
}
  • [MenuController.java]
public class MenuController {
    private final MenuService menuService;
    
    @GetMapping("/list")
    public String findMenuList(@PageableDefault Pageable pageable, Model model) {

        Page<MenuDTO> menuDTOList = menuService.findMenuList(pageable);

        log.debug("조회한 내용 목록: {}", menuDTOList.getContent());
        log.debug("총 페이지 수: {}", menuDTOList.getTotalPages());
        log.debug("총 메뉴 수: {}", menuDTOList.getTotalElements());
        log.debug("해당 페이지에 표시될 요소 수: {}", menuDTOList.getSize());
        log.debug("해당 페이지에 실제 요소 수: {}", menuDTOList.getNumberOfElements());
        log.debug("첫 페이지 여부: {}", menuDTOList.isFirst());
        log.debug("마지막 페이지 여부: {}", menuDTOList.isLast());
        log.debug("정렬 방식: {}", menuDTOList.getSort());
        log.debug("여러 페이지 중 현재 페이지 인덱스: {}", menuDTOList.getNumber());

        // 설명. 화면에서 페이징 버튼을 그려내기 위해 필요한 재료 준비(모듈화(2개))
        PagingButtonInfo paging = Pagination.getPagingButtonInfo(menuDTOList);

        model.addAttribute("menuList", menuDTOList);
        model.addAttribute("paging", paging);

        return "menu/list";
    }
}
  • [MenuService.java]
  • 고려사항
  1. 넘어온 Pageble에 담긴 페이지 번호를 인덱스 개념으로 변경
  2. 한 페이지에 뿌려질 데이터 크기
  3. 정렬 기준
    public Page<MenuDTO> findMenuList(Pageable pageable) {
        pageable = PageRequest.of(pageable.getPageNumber() <= 0 ? 0 : pageable.getPageNumber() - 1,
                pageable.getPageSize(),
                Sort.by("menuCode").descending());
        Page<Menu> menuList = menuRepository.findAll(pageable);

        return menuList.map(menu -> mapper.map(menu, MenuDTO.class));
    }

Log

LOGBACK

...
Logger logger = LoggerFactory.getLogger(getClass());
Logger logger = LoggerFactory.getLogger(MenuController.class);
logger.debug("variable: {}", variable);
  • SpringBoot의 내장기능

  • LEVELS

    • DEBUG
    • ERROR
    • INFO(Default)
    • TRACE
    • WARN
  • println vs logger

    • logger가 성능적으로 우수
    • 외부 리소스 파일로 별도 저장 및 송출 가능
      • 유의미한 데이터는 단순 Debuging이 아닌
        • 어떤 사용자가 접속
        • 하루 방문자 tracking
        • 방문 경로
        • 퇴출 경로
    • 로그 레벨에 따른 확인 가능
      • 개발 시: level DEBUG
      • 배포 시: level INFO
  • Project 전체에서 설정 가능

    • use

    • [application.yml]

      ...
      
      logging:
        level:
          com:
            team:
              package: debug
  • SLF4J (in Lombok)

@Slf4j
public class Class {
    public String string = "hello"
    log.debug("string: {}", string);
} 
profile
엔지니어로의 성장일지

0개의 댓글