Spring/SpringDataJPA

Web 확장 - 도메인 클래스 컨버터 & 페이징과 정렬

느리지만 꾸준하게 2022. 4. 18. 17:31

path variable 기반으로 코드를 작성해보자.

 

 

// MemberController Class

package study.datajpa.controller;


import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import study.datajpa.entity.Member;
import study.datajpa.repository.MemberRepository;

import javax.annotation.PostConstruct;

@RestController
@RequiredArgsConstructor
public class MemberController {

    private final MemberRepository memberRepository;

    @GetMapping("/members/{id}")
    public String findMember(@PathVariable("id") Long id) {
        Member member = memberRepository.findById(id).get();
        return member.getUsername();
    }

    @PostConstruct
    public void init() {
        memberRepository.save(new Member("userA"));
    }
}

 

// DataJpaApplication

package study.datajpa;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

import java.util.Optional;
import java.util.UUID;

@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {

   public static void main(String[] args) {
      SpringApplication.run(DataJpaApplication.class, args);
   }

   @Bean
   public AuditorAware<String> auditorProvider() {
      return () -> Optional.of(UUID.randomUUID().toString());
   }
}

 

 

 

결과는 아래와 같이 나온다.

insert into member (age, created_date, tema_id, username, member_id) values (?, ?, ?, ?, ?)
insert into member (age, created_date, tema_id, username, member_id) values (0, NULL, NULL, 'userA', 1);

8080번 포트로 들어가서 지정한 경로대로 들어가면 userA가 뜨게 된다.

 

MemberController Class에서 아래와 같이 지정을 해주고 확인을 해주면 userA가 나오게 된다.

@GetMapping("/members2/{id}")
public String findMember(@PathVariable("id") Member member) {
    return member.getUsername();
}

// MemberController Class

package study.datajpa.controller;


import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import study.datajpa.entity.Member;
import study.datajpa.repository.MemberRepository;

import javax.annotation.PostConstruct;

@RestController
@RequiredArgsConstructor
public class MemberController {

    private final MemberRepository memberRepository;

    @GetMapping("/members/{id}")
    public String findMember(@PathVariable("id") Long id) {
        Member member = memberRepository.findById(id).get();
        return member.getUsername();
    }

    @GetMapping("/members2/{id}")
    public String findMember(@PathVariable("id") Member member) {
        return member.getUsername();
    }

    @PostConstruct
    public void init() {
        memberRepository.save(new Member("userA"));
    }
}
  • HTTP 요청은 회원 id를 받지만 도메인 클래스 컨버터가 중간에 동작해서 회원 엔티티 객체를 반환 하고
  • 도메인 클래스 컨버터도 리파지토리를 사용해서 엔티티를 찾는다.

 

  • 주의할 점은 도메인 클래스 컨버터로 엔티티를 파라미터로 받으면, 엔티티는 단순 조회용으로만 사용해야한다.
  • => 트랜잭션이 없는 범위에서 엔티티를 조회했으므로, 엔티티를 변경해도 DB에 반영되지 않는다.

 

 

페이징과 정렬

아래와 같이 코드를 작성하고

 

// MemberController Class

package study.datajpa.controller;


import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import study.datajpa.entity.Member;
import study.datajpa.repository.MemberRepository;

import javax.annotation.PostConstruct;

@RestController
@RequiredArgsConstructor
public class MemberController {

    private final MemberRepository memberRepository;

    @GetMapping("/members/{id}")
    public String findMember(@PathVariable("id") Long id) {
        Member member = memberRepository.findById(id).get();
        return member.getUsername();
    }

    @GetMapping("/members2/{id}")
    public String findMember(@PathVariable("id") Member member) {
        return member.getUsername();
    }


// 페이징과 정렬 
    @GetMapping("/members")
    public Page<Member> list(Pageable pageable) {
        Page<Member> page = memberRepository.findAll(pageable);
        return page;
    }

    @PostConstruct
    public void init() {
        for (int i = 0; i < 100; i++) {
            memberRepository.save(new Member("user" + i, i));
        }
    }
}

 

DataJpaApplication을 돌리면

// DataJpaApplication

package study.datajpa;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

import java.util.Optional;
import java.util.UUID;

@EnableJpaAuditing
@SpringBootApplication
public class DataJpaApplication {

   public static void main(String[] args) {
      SpringApplication.run(DataJpaApplication.class, args);
   }

   @Bean
   public AuditorAware<String> auditorProvider() {
      return () -> Optional.of(UUID.randomUUID().toString());
   }
}

localhost 8080에서 아래와 같이 나오게 되고

 

postman에서도 확인을 할 수 있다.

 

sort도 넣어서 desc 내림차순으로 보여지게 할 수도 있다.

 

 

즉 요청 파라미터는 아래의 특징을 갖는데

  • ) /members?page=0&size=3&sort=id,desc&sort=username,desc
  • page: 현재 페이지, 0부터 시작한다.
  • size: 한 페이지에 노출할 데이터 컨수
  • sort: 정렬 조건을 정의한다. 예) 정렬 속성,(ASC | DESC), 정렬 방향을 변경하고 싶으면 sort 파라미터 추가(asc 생략 가능)

 

이제 application.yml에서 global 설정을 해보자. 

// application.yml

spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/datajpa
    username: sa
    password:
    driver-class-name: org.h2.Driver


  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
#        dialect: org.hibernate.dialect.Oracle10gDialect
    #    show_sql: true
        format_sql: true


# 여기부분
  data:
    web:
      pageable:
        default-page-size: 10
        max-page-size: 2000
logging.level:
  org.hibernate.SQL: debug
#  org.hibernate.type: trace

그러면 postman에서 members 조회시 딱 10개만 나오게 된다.

 

 

그리고 memberControllers에만 특별한 설정을 해주고 싶으면 아래와 같이 해줄 수도 있다.

@GetMapping("/members")
public Page<Member> list(@PageableDefault(size = 5) Pageable pageable) {
    Page<Member> page = memberRepository.findAll(pageable);
    return page;
}

@PostConstruct
public void init() {
    for (int i = 0; i < 100; i++) {
        memberRepository.save(new Member("user" + i, i));
    }
}

반환타입이 Page이므로 totalCountQuery도 나가게 된다.

 

 

그리고 엔티티(Member)를 외부에 그대로 노출시키면 안된다.

API를 갖다 쓰는 사람이 API의 스펙이 바뀌어 버리는 위험성을 가질 수있어서

API 스펙일 DTO로 변환을 해주면 좋다. 

// MemberController Class

@GetMapping("/members")
public Page<MemberDto> list(@PageableDefault(size = 5) Pageable pageable) {
    Page<Member> page = memberRepository.findAll(pageable);
    Page<MemberDto> map = page.map(member -> new MemberDto(member.getId(), member.getUsername(), null));
    return map;
}
@GetMapping("/members")
public Page<MemberDto> list(@PageableDefault(size = 5) Pageable pageable) {
    return memberRepository.findAll(pageable)
            .map(member -> new MemberDto(member.getId(), member.getUsername(), null));
}

 

 

아래와 같이 DTO가 반환되는 것을 볼 수 있다.

 

 

 

 

그리고 DTO는 엔티티를 봐도 괜찮기 때문에 MemberController에서 아래와 같이 설정을 해줄 수 있다.

// MemberDto

package study.datajpa.dto;

import lombok.Data;
import study.datajpa.entity.Member;

@Data
public class MemberDto {

    private Long id;
    private String username;
    private String teamName;

    public MemberDto(Long id, String username, String teamName) {
        this.id = id;
        this.username = username;
        this.teamName = teamName;
    }

    public MemberDto(Member member) {
        this.id = member.getId();
        this.username = member.getUsername();
    }
}

 

MemberController에서는 아래와 같이 설정을 해주고

// MemberController Class

@GetMapping("/members")
public Page<MemberDto> list(@PageableDefault(size = 5) Pageable pageable) {
    return memberRepository.findAll(pageable)
            .map(member -> new MemberDto((member));
}

이렇게 바꿔줄 수가 있다.

@GetMapping("/members")
public Page<MemberDto> list(@PageableDefault(size = 5) Pageable pageable) {
    return memberRepository.findAll(pageable)
            .map(MemberDto::new);
}

 

 

만약에 Page를 1부터 시작하게 할려면 두가지 방법이 있는데 두번째 방법을 적용해서 postman에서 확인을 해보면
page=0 과 1이 똑같이 1~5까지 실행이 되는 것이다.

spring:
  datasource:
    url: jdbc:h2:tcp://localhost/~/datajpa
    username: sa
    password:
    driver-class-name: org.h2.Driver


  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
#        dialect: org.hibernate.dialect.Oracle10gDialect
    #    show_sql: true
        format_sql: true

  data:
    web:
      pageable:
        default-page-size: 10
        max-page-size: 2000
        //         one-indexed-parameters: true 설정을 해주고
        one-indexed-parameters: true

postman에서 확인을 해보면
page=0 과 1이 똑같이 1~5까지 실행이 되는 것이다.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

<출처 김영한: 실전! 스프링 데이터 JPA >

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%EB%8D%B0%EC%9D%B4%ED%84%B0-JPA-%EC%8B%A4%EC%A0%84/dashboard

 

실전! 스프링 데이터 JPA - 인프런 | 강의

스프링 데이터 JPA는 기존의 한계를 넘어 마치 마법처럼 리포지토리에 구현 클래스 없이 인터페이스만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 모두 제공합니다.

www.inflearn.com

 

'Spring > SpringDataJPA' 카테고리의 다른 글

새로운 엔티티를 구별하는 방법  (0) 2022.04.18
스프링 데이터 JPA 구현체 분석  (0) 2022.04.18
사용자 정의 리포지토리 구현  (0) 2022.04.18
JPA Hint & Lock  (0) 2022.04.17
@EntityGraph  (0) 2022.04.17