인프런/실전! 스프링 데이터 JPA

4) WEB 확장기능 - 도메인 클래스 컨버터, 페이징과 정렬

backend dev 2024. 6. 13.

Web 확장 - 도메인 클래스 컨버터

 

HTTP 파라미터로 넘어온 엔티티의 아이디로 엔티티 객체를 찾아서 바인딩

 

@RestController
@RequiredArgsConstructor
public class MemberController {

    private final MemberRepository memberRepository;

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

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

도메인 클래스 컨버터 사용 후

@GetMapping("/members2/{id}")
public String findMember2(@PathVariable("id") Member member) { // 스프링이 식별자를 가지고 찾아오는 과정을 알아서 해준다.
    return member.getUsername();
}

 

 

- HTTP 요청은 회원 id 를 받지만 도메인 클래스 컨버터가 중간에 동작해서 회원 엔티티 객체를 반환

 

- 도메인 클래스 컨버터도 리파지토리를 사용해서 엔티티를 찾음

 

주의

도메인 클래스 컨버터로 엔티티를 파라미터로 받으면, 이 엔티티는 단순 조회용으로만 사용해야 한다.

(트랜 잭션이 없는 범위에서 엔티티를 조회했으므로, 엔티티를 변경해도 DB에 반영되지 않는다.)

[트랜잭션이 시작해야 영속성컨텍스트가 존재하므로]

 


Web 확장 - 페이징과 정렬

스프링 데이터가 제공하는 페이징과 정렬 기능을 스프링 MVC에서 편리하게 사용할 수 있다.

@GetMapping("/members")
public Page<Member> list(Pageable pageable) { //Pageable -> 페이징에 대한 파라미터 정보, Page -> 페이징에 대한 결과 정보
    return memberRepository.findAll(pageable);
}
@NoRepositoryBean
public interface PagingAndSortingRepository<T, ID> extends Repository<T, ID> {

    /**
     * Returns all entities sorted by the given options.
     *
     * @param sort the {@link Sort} specification to sort the results by, can be {@link Sort#unsorted()}, must not be
     *          {@literal null}.
     * @return all entities sorted by the given options
     */
    Iterable<T> findAll(Sort sort);

    /**
     * Returns a {@link Page} of entities meeting the paging restriction provided in the {@link Pageable} object.
     *
     * @param pageable the pageable to request a paged result, can be {@link Pageable#unpaged()}, must not be
     *          {@literal null}.
     * @return a page of entities
     */
    Page<T> findAll(Pageable pageable);
}

스프링 데이터 jpa 인터페이스에 존재하는 findAll 메소드를 사용하면 된다.

Pageable에는 페이징 조건이 들어가고, api를 요청할때 파라미터로 페이징조건을 넣어주면된다.

Pageable은 객체이므로 앞에 @ModelAttribute가 생략되었다고 보면된다.

그러므로 파라미터가 전달되면 그 파라미터로 Pageable의 구현체인 PageRequest를 생성해주고 Pageable참조변수에

담아준다.

 

스프링이 사용하는 Pageable의 구현체는 PageRequest이다.

페이지 번호, 페이지 사이즈, 정렬방법 등 다양한 페이징 조건을 넣을 수 있다.

 

페이지 사이즈를 명시적으로 입력하지않으면 기본값은 20

 

2번페이지,사이즈3으로 설정시 결과

[페이지값만 넣어도 되고-> 기본사이즈인 20으로 설정됨 ,사이즈값만 넣어도된다.-> 1페이지부터 보여줌]

 

 

요청 파라미터

 

예) /members?page=0&size=3&sort=id,desc&sort=username,desc

page: 현재 페이지, 0부터 시작한다.

size: 한 페이지에 노출할 데이터 건수

sort: 정렬 조건을 정의한다. 예) 정렬 속성,정렬 속성...(ASC | DESC), 정렬 방향을 변경하고 싶으면

sort 파라 미터 추가 ( asc 생략 가능)

 

기본값

글로벌 설정: 스프링 부트

 

application.*

spring.data.web.pageable.default-page-size=20 /# 기본 페이지 사이즈/
spring.data.web.pageable.max-page-size=2000 /# 최대 페이지 사이즈/

 

 

개별 설정

@PageableDefault 어노테이션을 사용

@GetMapping("/members")
public Page<Member> list(@PageableDefault(size = 5,sort = "username",direction = Sort.Direction.DESC) Pageable pageable) {
    return memberRepository.findAll(pageable);
}

 

접두사

페이징 정보가 둘 이상이면 접두사로 구분

[ 한 페이지에 페이징해야하는 정보가 2개이상 있다면 ]

@Qualifier 에 접두사명 추가

 "{접두사명}_xxx"
예제: /members?member_page=0&order_page=1

 

예시 코드들)

public String list(
        @Qualifier("member") Pageable memberPageable,
        @Qualifier("order") Pageable orderPageable, ... 

 

@GetMapping("/members")
    public MemberOrderResponse list(
        @Qualifier("member") @PageableDefault(size = 20) Pageable memberPageable,
        @Qualifier("order") @PageableDefault(size = 10) Pageable orderPageable
    ) {
        Page<Member> members = memberRepository.findAll(memberPageable);
        Page<Order> orders = orderRepository.findAll(orderPageable);
        
        return new MemberOrderResponse(members, orders);
    }

 

Page 내용을 DTO로 변환하기

 

엔티티를 API로 노출하면 다양한 문제가 발생한다. 그래서 엔티티를 꼭 DTO로 변환해서 반환해야 한다.

Page는 map() 을 지원해서 내부 데이터를 다른 것으로 변경할 수 있다.

 

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

 

 

Page.map() 코드 최적화

@Data
@AllArgsConstructor
public class MemberDto {
    private Long id;
    private String username;
    private String teamName;

    // 엔티티는 DTO를 바라보면 안된다 ( 의존X) , DTO는 엔티티를 바라봐도 된다. (의존O)
    public MemberDto(Member member) {
        this.id = member.getId();
        this.username = member.getUsername();
    }
}
@GetMapping("/members")
public Page<MemberDto> list(@PageableDefault(size = 5,sort = "username",direction = Sort.Direction.DESC) Pageable pageable) {
    return memberRepository.findAll(pageable).map(MemberDto::new);
}

 

 

Page를 1부터 시작하기

스프링 데이터는 Page를 0부터 시작한다.

만약 1부터 시작하려면?

 

방법 2가지

1. Pageable, Page를 파리미터와 응답 값으로 사용히지 않고, 직접 클래스를 만들어서 처리한다.

그리고 직접 PageRequest(Pageable 구현체)를 생성해서 리포지토리에 넘긴다.

물론 응답값도 Page 대신에 직접 만들어서 제공해야 한다

@GetMapping("/members")
public Page<MemberDto> list(@PageableDefault(size = 5,sort = "username",direction = Sort.Direction.DESC) 페이징정보객체 임의객체) { 
    PageRequest pageRequest = PageRequest.of(임의객체.pageNumber, 임의객체.pageSize);
    Page<MemberDto> map = memberRepository.findAll(pageRequest).map(MemberDto::new);
    MyPage<MemberDto> ~~ 반환객체도 생성해줘야한다.

 

 

2. spring.data.web.pageable.one-indexed-parameters 를 true 로 설정한다.

그런데 이 방법은 web에서 page 파라미터를 -1 처리 할 뿐이다.

따라서 응답값인 Page에 모두 0 페이지 인덱스를 사용하는 한계가 있다

 

이 설정은 클라이언트가 1부터 시작하는 페이지 인덱스를 사용하는 것을 허용하지만,

서버 쪽 로직과 데이터 구조에서는 여전히 0부터 시작하는 인덱스를 사용하게된다.

따라서 클라이언트와 서버 간의 인덱싱 차이를 명시적으로 처리해줘야 하는 경우가 생길 수 있다.

 

one-indexed-parameters Page 1요청 ( http://localhost:8080/members?page=1 )

해당 설정을하고 1페이지를 요청하면

```json
{
 "content": [
 ...
 ],
 "pageable": {
 "offset": 0,
 "pageSize": 10,
 "pageNumber": 0 //0 인덱스
 },
 "number": 0, //0 인덱스
 "empty": false
}

결과상으로는 0번 페이지로 동작하게된다.

 

즉 입력은 1페이지로 입력할수있게하지만

결과는 0페이지결과로 매핑한것

 

 

 

 

가급적 페이지 인덱스를 0부터 시작하는 기본값을 사용하는것이 좋다.

싫다면 페이지관련객체를 직접 생성한다.

 

댓글