[우아한테크코스 4기] 220701 F12 개발일지

Jihoon Oh·2022년 7월 1일
0

우아한테크코스 4기

목록 보기
14/43
post-thumbnail

오늘 진행한 일

오전에는 UI / UX 특강 DAY 2 일정이 있어 페어를 진행하지 못했고 오후에 페어 프로그래밍을 할 수 있었다. 다만 오전의 UI / UX 특강을 통해 프로젝트에 변동 사항이 생겼는데, 기존에는 각종 개발자용 제품에 대한 리뷰와 평점에 중심을 두었다면 이제는 개발자들이 본인의 데스크 셋업을 프로필로 만들고 다른 개발자들의 프로필을 보면서 제품을 고르거나 추천받는 형태로 프로젝트 방향성을 변경했다.

물론 그렇다고 이번 스프린트에서 구현해야 할 항목이 변한 것은 아니다. 프로필 기능이 생긴다 하더라도 제품에 대한 조회, 리뷰 작성 및 조회 기능은 반드시 필요하기 때문에 이번 스프린트는 여전히 해당 기능을 중심으로 개발할 예정이다.

도메인 검증

이전에 리뷰 도메인을 만들어 놨을 때 빠뜨린 것이 있었다. 리뷰를 작성할 때 사용자는 평점을 1~5점을 줄 수 있는데, 리뷰 도메인을 생성할 때 rating 필드를 검증하지 않고 그대로 생성하고 있었다.

어차피 클라이언트 단에서 1~5점 값만 들어올 수 있도록 처리해 주니까 상관 없지 않나? 라는 생각은 금물이다. 비즈니스 규칙으로 평점이 1~5점 사이의 정수라 정하기로 했다면 반드시 백엔드 단에서도 검증을 해주어야 한다.

너무나 당연한 로직인데 까먹고 추가하지 않고 넘어갔기 때문에, 따로 이슈를 파서 추가해주었다.

private void validateRating(final int rating) {
    if (rating < MINIMUM_RATING || rating > MAXIMUM_RATING) {
        throw new InvalidRatingValueException();
    }
}

예외 상황에 대해서 적절한 메시지를 이미 담은 예외 클래스를 만들어서 사용하기로 했으므로 InvalidRatingValueException을 만들어서 반환하기로 했다.

Pageable 활용

어제 개발일지에서도 언급했듯이 프론트엔드의 목록 조회 요청은 모두 무한 페이징과 정렬을 필요로 하는 요청이다. 그래서 레포지토리의 조회 메서드의 반환 형태도 Slice 타입으로 정했다. 어제는 페이징을 간편하게 해주는 Pageable에 대한 이야기는 자세히 하고 넘어가지 않고 페이징의 결과값들에 대한 이야기만 나눴는데, 오늘은 컨트롤러에서 효과적으로 페이징과 정렬 조건을 받아오는 방법에 대해 이야기를 나눴다. 결론을 말하자면, 컨트롤러 단에서 요청을 받을 때 바로 Pageable 타입으로 바인딩해서 해당 PageableJpaRepository까지 계속 넘겨줘서 간단하게 처리할 수 있었다.

Pageable 이해

실제 웹 서비스에서 목록 조회 기능은 대부분 페이징과 정렬을 필요로 하지만 페이징과 정렬을 구현한 다양한 조회 쿼리를 작성하는 것은 여간 복잡하고 귀찮은 일이 아니다. Spring Data Jpa는 페이징 처리를 직접 쿼리를 작성하지 않고도 Pageable 객체를 활용해 간편하게 페이징과 정렬을 사용할 수 있어 비즈니스 로직에 집중할 수 있게 도와준다.

Pageable은 인터페이스로, 페이징에 필요한 페이지 번호, 페이지의 크기, 정렬 조건 등을 제공하는 인터페이스다. 인터페이스이므로 자바 코드 내에서 직접 생성해서 사용할 때는 보통 PageRequest라는 구현체를 쓰게 되는데, 정적 팩터리 메서드를 통해 page, size, sort를 받아 생성한다. 이 때 sort가 무엇이냐 함은 정렬 기준을 담은 Sort 객체를 말한다.

Spring Data Jpa의 JpaRepository를 상속한 레포지토리 인터페이스는 이 Pageable을 매개변수로 받는 메서드를 정의함으로써 간단하게 페이징을 구현할 수 있다.

public interface ReviewRepository extends JpaRepository<Review, Long> {

    Slice<Review> findPageByProductId(Long productId, Pageable pageable);

    Slice<Review> findPageBy(Pageable pageable);
}

(여기서 메서드 네임에 Page를 붙이지 않아도 정상적으로 동작하나 전체 리스트가 아닌 페이징 된 정보를 반환한다는 점에 집중하여 Page라는 단어를 붙인 것임에 주의하자)

Pageable을 매개변수로 받는 메서드는 페이징의 결과물을 반환할 수 있다. 어제 개발일지에서 언급한 Page, Slice, List가 그것이다. 위 코드에서 Pageable을 매개변수로 넣어줌으로써 Spring Data Jpa는 Slice 객체를 반환하는 메서드를 만들 수 있게 되었다.

Controller에서 Pageable 바인딩하기

그렇다면 다음과 같은 클라이언트의 페이징 요청을 받아서 어떻게 Pageable로 가공해 JpaRepository까지 전달해야 할까?

...?page=0&size=10

가장 러프한 방법으로는 페이지 번호, 크기, 정렬 조건을 요청의 파라미터로 받아서(쿼리 스트링의 형태로) 자바 코드 내에서 PageRequest.of 메서드를 통해 자바 코드 내에서 직접 Pageable을 구현하여 전달하는 방법이 있을 것이다. 하지만 훨씬 간단한 방법으로 Pageable을 만들 수 있다.

쿼리스트링으로 페이징 정보를 받아올 컨트롤러 메서드의 매개변수로 Pageable 하나를 추가해 주기만 하면 된다. Pageable을 추가한 F12의 컨트롤러 메서드는 다음과 같은 형태로 작성되었다.

@GetMapping("/reviews")
public ResponseEntity<ReviewPageResponse> showPage(final Pageable pageable) {
    final ReviewPageResponse reviewPageResponse = reviewService.findPage(pageable);
    return ResponseEntity.ok(reviewPageResponse);
}

컨트롤러 메서드의 매개변수에 Pageable 타입이 들어있으면 자동으로 page, size 값을 Pageable로 매핑해준다. 또한 정렬 조건도 함께 넣을 수 있는데, 이는 Pageable 타입에 바인딩 할 때 Sort 객체도 바인딩 해주기 때문이다.

...?page=0&size=10&sort=rating,desc

쿼리스트링에 sort=rating,desc가 추가되었다. sort 조건 역시 Pageable처럼 Sort 객체로 자동으로 바인딩된다. 위 쿼리스트링처럼 rating,desc로 작성해준다면 rating의 내림차순 정렬을 하도록 하는 Sort 객체가 바인딩된다.

그리고 앞서 말했듯이 Pageable을 바인딩할 때 Sort도 함께 바인딩해주기 때문에, showPage 메서드의 인수로 들어온 PageableSort 객체를 포함하고 있다.

처음에는 Sort를 바인딩할 때 저렇게 rating,desc로 콤마로 구분해서 오름차순 / 내림차순 조건을 함께 걸어줄 수 있는 지 몰라 일단 기본값인 오름차순으로 받아온 Sort 객체를 다시 desc 처리를 해 주고 Pageable에 넣어서 새로운 Pageable을 만들어서 서비스로 넘겨줘야 하나 하는 생각을 했었는데, 최대한 간략하게 바인딩 하려고 이런 저런 자료를 찾아보다 보니 Sort 바인딩을 할 때 오름차순 / 내림차순도 함께 바인딩을 할 수 있더라… 참고로 정렬 조건을 여러개 걸고 싶을때는 뒤에 다시 &sort=...를 붙여주면 된다고 한다.

참고) https://jistol.github.io/spring/2017/02/11/jpa-sort/

이렇게 Spring Data Jpa와 Pageable을 활용한 덕분에 너무나 간단하게 페이징과 정렬 정보를 객체로 바인딩하고, 이를 이용해 조회 메서드를 작성할 수 있었다. 오오 Spring Data Jpa

팀 일주일 회고

F12 팀은 매일 데일리 미팅과 데일리 회고를 진행하는데, 그 중에서도 금요일 데일리 회고는 일주일을 돌아보는 일주일 회고를 하기로 결정했다. 그래서 오늘이 첫 일주일 회고였는데, 팀원들의 얼굴에 방학이 끝나고 개학 첫 주를 맞이하느라 생긴 일주일 간의 피로와 그래도 일주일이 무사히 끝났다는 해방감이 같이 보였다.

팀원들이 모두 일주일 소감과 제안 사항을 이야기했는데, 팀원들이 대체로 일정을 타이트하게 잡은 부분에서는 힘들지만 의사결정이 큰 트러블 없이 빠르게 진행된다는 점에서 좋다고 느끼는 듯 했다. 5인 페어를 진행한 부분에 대해서는 직접 페어를 진행한 백엔드 크루들 사이에서는 5인으로 페어를 하는 만큼 이전의 개발 방식보다 속도는 훨씬 느리지만 컨벤션을 잘 맞춰가는데 도움이 되고, 서로 잘 아는 분야가 다르기 때문에 각자 부족한 부분을 채워주면서 배우는 재미를 느끼게 한다는 의견이 있었다. 그리고 5인 페어를 직접 진행하지는 않지만 프론트엔드 크루들이 5인 페어를 보고 보기 좋고, 기초를 다지는데 좋아보인다는 생각도 말해주었다. 확실히 첫 스프린트는 5인 페어로 진행하기를 잘한 것 같다.

제안 사항도 몇가지 나왔는데, 우선 회의가 가끔 의식의 흐름대로 빠지는 경우가 있어서 회의의 중심을 잡아 줄 회의 호스트, 회의 기록에 집중할 서기를 무조건 정하고 가자는 의견이 나왔다. 다만 데일리 미팅 때는 다같이 이런 저런 얘기를 하는 것이 맞는 듯 하여, 데일리 때는 호스트와 서기를 뽑지 않고, 개발 관련된 안건이 있어서 진행하는 회의에 한해 호스트와 서기를 뽑고 진행하기로 절충했다.

오늘 발생한 이슈

@EnableJpaAuditing 어노테이션을 main에서 @Configuration 클래스로 옮겼더니 테스트가 실패하는 이슈(해결)

어제 코린은 @EnableJpaAuditing 어노테이션을 @Configuration이 붙은 설정 빈에 붙여서 쓰는 것이 더 맞지 않냐는 의견을 냈었는데, 어제는 검색 결과 나온 레퍼런스들을 따라서 그냥 main에 붙이는 것으로 결론내렸다. 하지만 오늘 오리가(다른 팀이지만 많은 도움을 주고 있다) 얘는 JPA 설정인데 스프링부트 메인에 붙이는 것이 과연 맞을까? 라는 의견을 제시해 주었고, 우리 팀 백엔드 크루들과 논의를 해 본 결과 JPA의 설정이니 따로 JPA 설정과 관련된 설정 클래스를 만들고 거기서 사용하는 것이 맞겠다는 의견에 합의했다. 그래서 JpaConfig 클래스를 만들고 해당 클래스로 @EnableJpaAuditing을 옮겼는데 웬걸, 생성 시간을 사용하는 테스트들이 모두 실패로 바뀌었다.

다들 당황해서 머리를 싸맸는데, 허탈하게도 문제는 매우 간단했다. main에 붙어 있던 어노테이션을 @Configuration이 붙은 설정 파일로 옮겨주었는데, @DataJpaTest는 JPA 관련된 설정만 읽어오므로 JpaConfig에 붙은 @EnableJpaAuditing을 설정하지 못하는 것이었다. 테스트 클래스에 @Import(JpaConfig.class)를 붙여줌으로써 해결했다.

페이징 요청 처리 시 페이지의 최대 크기를 제한하고, 제한을 넘기면 예외를 반환하기(미해결)

코린이 페이징 요청의 size 값이 너무 크면 애플리케이션이 제대로 처리하지 못하고 예외를 반환할 수도 있다는 의견을 냈고, 그에 따라 요청으로 받을 수 있는 페이지의 최대 크기를 제한하기로 결정했다. 처음에는 PageableHandlerMethodArgumentResolver를 생성해서 maxSize를 우리가 원하는 값으로 설정하고 등록해줘서 해결했는데, 문제는 이 경우 maxSize보다 큰 값이 요청으로 오면 그냥 maxSize 값으로 페이징을 해서 돌려준다는 점이었다.

이게 뭐가 문제냐고 할 수 있는데, 티키가 근데 프론트에서 예를 들어 엄청 큰 값으로 요청을 했는데 백에서 자동으로 크기를 줄여서 줘버리면 데이터가 이것밖에 없구나 하고 생각할 수 있지 않을까? 라면서 maxSize보다 더 큰 값이 요청으로 들어오면 예외를 발생시키는게 더 명시적이지 않을까? 라는 의견을 제시했다. 듣고 보니 타당한 얘기였다. 다만 합리적이라 생각하면서도 나는 도저히 Controller보다 앞 단에서 예외를 터뜨리는 방법을 생각하지 못했는데, 티키가 방법을 제시했다. PageableHandlerMethodArgumentResolver를 그냥 쓰지 말고 상속해서 쓰자. 거기에 있는 resolveArgument를 재정의해서 거기서 검증하고 예외를 터뜨리면 될 것 같다. 라는 이야기였다. 아쉽게도 이 의견이 제시되었을 때 일과가 끝나고 일주일 회고를 할 시간이 되어서 이 이슈는 다음 주 작업으로 넘기고 페어 프로그래밍을 마무리했다. 난 아직 방법이 이해가 잘 가지 않는 만큼 주말동안 HandlerMethodArgumentResolver에 대해 더 공부해 보고 좋은 해결 방안을 찾아가보려고 한다.

일주일 소감

반성할 부분이 있었는데, 솔직히 난 자만했던 것 같다. 스프린트 1에 구현할 기능을 최소한으로 작게 잡았으니 금방 끝내버릴 수 있겠지 라고 생각했었는데 생각보다 어렵기도 했고 고려해야 할 이슈 사항이 계속 터져나왔다. 일찍 시작하고 다같이 하기로 한 게 정말 좋은 선택이었다고 생각한다.

내가 우리 팀에 제안했던게 스프린트 1 기간 동안의 백엔드, 프론트엔드 각각 단체로(5인 / 2인) 페어 프로그래밍을 진행하며 서로 맞춰가는 것, 매일 개발 일지를 작성하는 것이었는데, 둘 다 제안하기를 잘한 것 같다. 5인 페어를 3일 정도 진행했는데 내가 사이드 프로젝트를 진행하면서 놓치고 넘어갔던 JPA 활용법이나, 레벨 2에서 잘 공부하지 않았던 스프링 설정에 관련된 내용을 팀원들을 통해 배울 수 있었고, 역시 다른 팀원들도 본인이 잘 모르던 JPA 기능이나 문법을 먼저 사용해본 내게 배워갈 수 있는 시간이어서 좋았다. 그리고 개발 일지는, 아직 일주일밖에 쓰지 않았지만 하루의 개발 내용을 정리하면서 새로 알게 된 내용이나 공부해봐야 할 내용을 정리할 수 있어서 좋은 것 같다.

이번 주 아쉬웠던 점은 체력 안배에 실패했다는 점이다. 목요일 금요일은 그냥 눈만 감으면 잘 수 있었을 정도로 수면 부족과 등하교로 인한 체력 저하에 시달렸다. 다음 주는 심지어 데모 데이가 있는 만큼 조금 더 바쁠 것 같다. 이번 주 체력적으로 너무나 힘들었던 만큼 체력 안배에 조금 더 신경쓰면서 프로젝트를 진행해야 할 것 같다.

내일 목표

주말은 개인 공부와 휴식을 취하자.

profile
Backend Developeer

0개의 댓글