2023-02-01-TIL
2023-02-01-TIL
Today I Learned
프로젝트 패키지 구조와 객체 네이밍 규칙을 정하면서 계층형과 도메인형 패키지, DTO와 VO의 차이, 엔티티 setter 문제를 정리했다. 이어서 페이지네이션을 검색 엔진, UI, DB 성능 세 관점에서 찾아봤다.
Package Structure
우선, 계층형 구조를 기반으로 시작할 지, 도메인을 중심으로 시작할 지를 정해야 한다.
계층형(package by layer)은 controller, service, repository처럼 기술적 역할로 패키지를 나눈다. 처음 보는 사람도 구조를 바로 이해할 수 있지만, 기능 하나를 고치려면 여러 패키지를 오가야 하고 도메인이 늘어날수록 각 패키지가 비대해진다. 도메인형(package by feature)은 member, order처럼 기능 단위로 나누고 그 안에 계층을 둔다. 관련 코드가 한곳에 모여 응집도가 높고, 패키지 접근 제한자로 도메인 내부를 감출 수 있으며, 나중에 모듈이나 서비스로 떼어 내기도 쉽다. Sahibinden 글이 두 방식을 구체적으로 비교한다.
1
2
3
4
5
6
package by layer package by feature
├── controller ├── member
├── service │ ├── MemberController
└── repository │ ├── MemberService
│ └── MemberRepository
└── order
- https://velog.io/@jsb100800/Spring-boot-directory-package
- https://medium.com/sahibinden-technology/package-by-layer-vs-package-by-feature-7e89cde2ae3a
DTO, DAO, VO, POJO
그냥 컨트롤러에서 사용하는 요청/응답 객체는 Response/Request라고 네이밍하고, 그 외의 계층간에 데이터 전송 시에는 DTO네이밍을 사용한다. VO는 사용하지 않고 enum이나 VO를 정의할 때는 그냥 객체이름만 사용한다. 엔티티도 마찬가지로 객체이름만 사용하여 정의한다.
이 규칙을 정하기 위해 용어를 정리하면 다음과 같다.
- POJO: 특정 프레임워크의 클래스를 상속하거나 인터페이스를 구현하지 않은 평범한 자바 객체.
- DTO(Data Transfer Object): 계층이나 프로세스 사이에 데이터를 옮기기 위한 객체. 로직 없이 데이터만 담는다.
- VO(Value Object): 값 자체로 동등성을 판단하는 불변 객체.
Money,Address처럼 필드 값이 같으면 같은 객체로 본다. 그래서equals()와hashCode()를 값 기준으로 재정의한다. - DAO(Data Access Object): DB 접근 로직을 캡슐화한 객체. Spring Data JPA에서는 Repository가 이 역할을 한다.
Adam Bien의 글처럼 예전 J2EE 문헌에서는 VO가 DTO와 같은 뜻으로 쓰이기도 해서 용어가 혼란스럽다. 그래서 팀 안에서 이름 규칙을 명시적으로 정해 두는 것이 실용적이다.
- https://blog.devgenius.io/dao-dto-po-so-bo-vo-wtf-6673c9dd5437
- https://www.quora.com/What-is-DTO-DAO-and-VO-in-Java
- https://adambien.blog/roller/abien/entry/value_object_vs_data_transfer
- https://www.adam-bien.com/roller/abien/entry/how_evil_are_actually_data
- https://www.baeldung.com/java-pojo-javabeans-dto-vo
- https://stackoverflow.com/questions/1612334/difference-between-dto-vo-pojo-javabeans
- https://maenco.tistory.com/entry/Java-DTO%EC%99%80-VO%EC%9D%98-%EC%B0%A8%EC%9D%B4
- https://youngjinmo.github.io/2021/04/dto-vo-entity/
Entity Setter 사용금지
엔티티에 public setter를 열어 두면 어디서든 상태를 바꿀 수 있어서, 값이 왜 언제 바뀌었는지 추적하기 어렵고 객체가 지켜야 할 규칙을 우회할 수 있다. 그래서 생성은 생성자나 정적 팩터리 메서드, 빌더로 필수 값을 받아 처리하고, 변경은 changeAddress(), cancel()처럼 의도가 드러나는 메서드로만 허용하는 방식이 권장된다. JPA가 요구하는 기본 생성자는 protected로 막아 둔다. JPA의 변경 감지(dirty checking)는 필드 값만 보므로 setter가 없어도 정상 동작한다.
- https://velog.io/@aidenshin/%EB%82%B4%EA%B0%80-%EC%83%9D%EA%B0%81%ED%95%98%EB%8A%94-JPA-%EC%97%94%ED%8B%B0%ED%8B%B0-%EC%9E%91%EC%84%B1-%EC%9B%90%EC%B9%99
- https://multifrontgarden.tistory.com/293
- https://jeongkyun-it.tistory.com/221
- https://www.inflearn.com/questions/16235/%EC%83%9D%EC%84%B1-%EB%A9%94%EC%84%9C%EB%93%9C-setter-%EC%A7%88%EB%AC%B8
Google Pagination
검색 엔진 관점에서 Google 검색 센터 문서는 ?page=n처럼 페이지마다 고유 URL을 주고, 첫 페이지를 모든 페이지의 canonical로 지정하지 말라고 권한다. 예전에는 <link rel="next">, <link rel="prev">로 페이지 관계를 표시했지만 문서에 따르면 Google은 더 이상 이 태그를 사용하지 않는다. 2011년 블로그 글은 그 태그를 처음 소개하던 시기의 자료로 함께 남겨 두었다.
DB 관점의 GitLab 페이지네이션 성능 가이드는 정렬 기준 컬럼이 유일하지 않으면 페이지 사이에서 행이 중복되거나 빠질 수 있으므로 id 같은 타이브레이커 컬럼을 정렬에 추가하고, 인덱스 컬럼 순서를 ORDER BY와 맞추라고 권한다.
- https://stackoverflow.com/questions/19118532/pagination-in-sql-performance-issue
- https://docs.gitlab.com/ee/development/database/pagination_performance_guidelines.html
- https://developers.google.com/search/docs/specialty/ecommerce/pagination-and-incremental-page-loading?hl=ko
- https://developers.google.com/search/blog/2011/09/pagination-with-relnext-and-relprev
Pagination Design Best Practices
UI 측면에서 자주 언급되는 원칙은 현재 페이지를 분명히 표시하고, 이전·다음 버튼을 일관된 위치에 두며, 클릭 영역을 충분히 크게 하는 것이다. 목록 성격에 따라 번호 페이지, “더 보기” 버튼, 무한 스크롤 중 하나를 고른다. 번호 페이지는 특정 위치로 돌아가기 쉽고, 무한 스크롤은 탐색형 피드에 맞지만 푸터에 닿기 어렵고 위치를 기억하기 어렵다.
- https://www.smashingmagazine.com/2007/11/pagination-gallery-examples-and-good-practices/
- https://fireart.studio/blog/pagination-design-examples/
- https://app.uxcel.com/courses/ui-components-best-practices/best-practices-005
- https://medium.com/uxness/best-practices-for-designing-pagination-in-web-1c33140f31b
Pagination Performance
LIMIT ... OFFSET ... 방식은 뒤 페이지로 갈수록 느려진다. DB가 OFFSET만큼의 행을 읽고 버린 뒤에야 결과를 돌려주기 때문이다. 향로 님의 두 글은 이를 개선하는 방법을 다룬다.
- No Offset: 마지막으로 본 행의 키를 기준으로
WHERE id < :lastId ORDER BY id DESC LIMIT n처럼 조회한다. 인덱스로 시작 위치를 바로 찾으므로 페이지 깊이와 상관없이 빠르지만, 임의의 페이지 번호로 바로 이동할 수 없다. 커버링 인덱스: 번호 페이지가 꼭 필요하면 인덱스만으로 조회 가능한 서브쿼리로 먼저 ID 목록을 구하고, 그 ID로 실제 행을 조인해 가져와 불필요한 행 접근을 줄인다.
- https://jojoldu.tistory.com/528
- https://jojoldu.tistory.com/529
- https://lhr0419.medium.com/mysql-pagination%EC%9D%98-%EC%84%B1%EB%8A%A5%EC%9D%84-%ED%96%A5%EC%83%81%EC%8B%9C%ED%82%A4%EC%9E%90-319cb3f09e03
댓글
아직 댓글이 없습니다