오늘도 그저께와 마찬가지로 5인 페어를 진행했다. 1차 데모 데이까지의 스프린트가 끝나기 전 까지는 계속해서 5인 페어를 하면서 코딩 컨벤션에 적응하며 팀원들 간에 개발 속도를 맞춰가고, 1차 데모 데이 이후 부터는 기능 별로 나눠서 각자 개발을 진행하고 Pull Requests를 활용한 코드 리뷰를 통해 프로젝트를 진행해 나갈 것이다.
Review 도메인에 대한 Create 기능 구현을 완료했다. CRUD 중 C를 완성하는데는 크게 어려움이 없었는데, 굳이 고려해 볼 만한 사항이 있었다면 테이블 상으로 Review는 Product(현재 상황에서는 Keyboard)와 연관관계를 맺어야 하는 상황인데, JPA를 사용함에도 불구하고 필드로 Product 객체를 가지는 것이 아니라 Product의 id값을 가지도록 하는 사항이었다.
나는 우선은 Product의 id 값을 필드로 가지는 것을 주장했는데,
productId
가 필요하기는 하지만, Review를 조회해 올 때 Product를 함께 조회해 올 일은 없다. 다시 말하자면 Review → Product로 객체 그래프 탐색을 사용할 일이 없다.라는 이유에서였다. 실제로 사이드 프로젝트를 진행하면서 우선 id를 넣어놓고 객체 그래프 탐색을 사용할 필요가 있을 때 엔티티로 교체해 주었는데 훨씬 효율이 좋았다고 생각했다.
처음에는 "JPA를 쓰니까 당연히 객체를 필드로 가져서 연관관계 매핑을 해줘야 하는 것 아니야?"
라던 팀원들도 엔티티 대신 식별자 값만 필드로 가지는 방안에 동의했다.
팀원들이(FE, BE 모두) 함께 api 구조를 논의한 결과, 리뷰를 조회할 때 정렬 기준을 최신순, 평점순 두 가지의 경우를 두기로 결정했다. 처음에 나는 최신순 정렬을 id의 역순(당연히 먼저 생성된 순서대로 id가 부여될테니 id의 역순이면 최신순이라고 생각)으로 정렬하려고 생각했다. 그런데 팀원들 사이에서 이런 의견이 나왔다.
최신순 정렬은 비즈니스 로직인데 비즈니스와 관련 없는 대리 키를 사용해도 될까?
지금은 auto_increment 옵션을 쓰니까 id 역순이 최신순이지만 영원히 그걸 보장할 수 있을까?
먼저 id 역순이 최신순을 영원히 보장하느냐는 측면에서는 물론 영원히
라고 단정지을 수는 없지만 바뀔 일이 거의 없다고 생각해서 문제가 크게 없지 않을까라고 생각했다. (예를 들어 시퀀스를 사용하는 다른 DB로 바꾸더라도 생성 순서대로 id를 부여받을테니까) 하지만 비즈니스와 관련 없는 대리 키로 비즈니스 로직을 다룬다는 점이 괜찮을까 라는 의견에는 충분히 납득할 수 있었다. 또한 생각해봤을 때, 생성 시간을 가지고 있는 것이 일종의 로그 역할을 할 수도 있다는 생각도 들어 생성 시간 값을 주고 해당 값으로 정렬하는 것이 좋겠다는 팀원들의 의견에 동의했다.
당시 Review 테이블 설계에는 생성 시간에 대한 컬럼이 들어있지 않았으므로, Review 도메인에 생성 시간 필드를 추가할 필요가 있었다. 다행히 JPA에는 생성 시간을 편하게 매핑할 수 있는 기능이 있다.
@CreatedDate
@Column(name = "created_at")
private LocalDateTime createdAt;
@CreatedDate
어노테이션을 사용하면 LocalDateTime
, LocalDate
타입에 엔티티의 생성 시점을 매핑할 수 있다. (처음 본 크루들도 있었는데 오오 역시 JPA
하면서 신기해했다.) 나도 자주 써본 어노테이션은 아니었는데, 정확히 지금 상황에 적용할 수 있을 것 같다 새로 createdAt
필드를 만들어주고 어노테이션을 매핑해줬다.
하지만 문제가 발생했는데, 실제 정렬 기능을 만들고 보니 createdAt
을 기준으로 한 정렬이 먹히지 않는 문제가 발생했다. 트러블 슈팅 끝에 이유를 찾아냈다. createdAt
값에 null이 들어가고 있었다.
@EnableJpaAuditing
과 @EntityListeners
두 가지 어노테이션을 사용하지 않아서 생기는 문제였다. 하나는 @EnableJpaAuditing
이고, 하나는 @EntityListeners
였다. @EntityListeners
는 엔티티 클래스에 붙이는 어노테이션으로, 이벤트가 발생할 때 마다 특정 로직을 실행시키도록 지정하는 어노테이션이다. 코드에서는 @EntityListeners(AuditingEntityListener.class)
로 매핑해주었는데, callback으로 지정된 AuditingEntityListener
클래스 내부에 touchForCreate
, touchForUpdate
메서드가 있어서 엔티티의 생성과 수정 시간을 기록할 수 있게 해준다. 때문에 @CreatedDate
나 @UpdatedDate
를 사용하려면 @EntityListeners(AuditingEntityListener.class)
어노테이션을 클래스에 붙여주어야 한다.
하지만 단지 이 어노테이션을 붙이기만 하면 여전히 null 값이 들어간다. 해당 클래스에 audit 기능을 사용할 수 있도록 해줬지만 전체 애플리케이션은 JPA auditing을 사용하고 있지 않기 때문이다. 그래서 해당 기능을 사용할 수 있도록 main 메서드에 @EnableJpaAuditing
메서드를 붙여주었다.
이 과정을 거치고 나니 정상적으로 @CreatedDate
의 값이 매핑되고, 정렬도 정상적으로 진행됨을 확인할 수 있었다.
F12 프로젝트는 모든 목록 조회에 대해 무한 페이징
기능을 적용하기로 결정했다. 때문에 목록 조회 시 일반적인 조회 메서드처럼 List의 형태로 받아오는 것이 아니라 Slice 형태로 데이터를 받아올 필요가 있었다.
페이징의 결과물은 세 가지 타입으로 받을 수 있는데, 각각 Page
, Slice
, List
다. List 타입은 당연히 순수한 자바의 List 객체를 말한다. 페이징을 경험해보지 않은 크루들은 Page와 Slice 타입을 처음 보는 듯 했는데, 둘에는 미묘한 차이가 있다는 점을 설명했다.
totalElements
), 다음 페이지의 존재 여부, 페이징해서 가져온 데이터를 확인할 수 있다.count
쿼리를 한번 더 실행한다.count
쿼리 대신 limit + 1
조회를 통해 마지막 페이지인지 정보를 확인한다.우리 프로젝트는 스크롤링을 통한 무한 페이징 방식으로 페이징을 처리하기 때문에, 굳이 전체 데이터 개수를 알 필요가 없다. 그저 다음 페이지가 존재하는지, 아니면 현재 조회한 페이지가 마지막 페이지인지만 알면 된다. 그래서 페이징이 적용된 조회 메서드에서 Slice
를 반환하기로 결정했다.
Review 도메인에 대한 조회 기능이 아직 완성되지 못했다. 때문에 최우선적으로 Review 조회 기능을 완성하고, 여력이 된다면 Keyboard 도메인의 조회에서 아직 만들지 못한 Keyboard 목록을 받아오는 기능 작업을 시작하려고 한다.
또한 GitHub Actions를 이용해 Pull Request가 open되면 자동으로 빌드를 진행해 보는 스크립트를 짜기로 했는데, 해당 부분에서 아직 이해가 되지 않는 부분이 있어 좀 더 공부해보고 스크립트를 작성하는 것을 목표로 한다.
우와 이거 너무 꿀잼이네여
CreatedAt과 JpaAudit 관련 잘 담아갑니다!!!
commit type convention도 너무 깔끔하고요
F12 짱짱맨~~~😆👍