[참고 깃허브] 엔티티 클래스 설계와 퍼시스턴스 프레임워크
난관의 여정만능 클래스부작용만능 클래스가 뷰까지 바로 전달되면Entity를 감추기Entity가 뷰, API 응답에 바로 노출될 때의 비용외부 노출용 DTO를 따로 만들기DTO의 이름 고민Immutable 객체의 장점Rich Domain ObjectAGGREGATE로 ENTITY 간의 선긋기AGGREGATE는?AGGREGATE 간의 참조REPOSITORY vs DAO프레임워크 돌아보기프레임워크 활용 전략
1. 엔티티 하나에 너무 많은 것을 담지 마라
2. 엔티티를 외부 레이어에는 감추고, 외부노출용으로는 DTO를 따로 만들어라
3. AGGREGATE 간에 참조는 객체를 참조하는 것이 아닌 id참조로 하라
4. 프레임워크의 특정 기능을 위해서 추구하는 객체 설계를 포기하는 상황이 적을수록 좋음(뭐를 쓰려면 setter를 써야 해서 불변을 포기할래.. 같은)
5. 적용 패턴에 따라 프레임워크를 다르게 사용
- 예) CUD + 단순 R → JPA, 복합적인 R → Spring JDBC
난관의 여정
만능 클래스
- 연관된 모든 테이블의 데이터를 담은 클래스가 주는 어려움!
public class Issue { private Repo repo; private List<Comment> comments; private List<Label> labels; private Milestone milestone; private List<Account> partipants; }
- Account(사용자 계정) 객체가 account.id를 참조하는 모든 테이블과 대응되는 객체를 의존하는 경우
public class Account { private List<Issue> myIssues; private List<Repo> myRepos; private List<Comment> myComment; private List<Label> myLabels; }
User(Account) 클래스를 보면 ORM 사용 성숙도를 알 수 있음
부작용
- 성능 저하
- 항상 연관된 객체를 다 조회한다면 불필요한 쿼리가 많이 날아감
- N+1 쿼리 주의해야 함(하나 조회하는데 수백개 쿼리 날아가는 문제가 있을 수 있음)
- Lazy loading을 쓰지 않고 수동으로 값을 채울 때의 난관
Issue.getComments()
에 값이 채워질지 아닐지는 DAO 내부까지 따라가봐아 알수 있다.- 어떤 때는 있고 어떤 때는 없고...
- 비슷한 메서드가 여러개 생길수도 있다.
findIssueById()
,findIssueByIdWithComments()
만능 클래스가 뷰까지 바로 전달되면
- JSP/Freemarker, SpEL에서 깊은 객체 탐색이 이루어질 가능성이 높음
- 객체 참조 관계를 바꾸는 비용이 큼. 바꾸면 뷰에서도 또 바꿔 주어야 하니
- 그리고, JSP/Freemarker에는 컴파일 타임 검증이 안되니 배포해야 에러를 알 수가 있음
<div>${issue.milestone.creator.email}</div>
Entity를 감추기
Entity가 뷰, API 응답에 바로 노출될 때의 비용
- 캡슐화를 지키기 어려워진다.
- 꼭 필요하지 않는 속성도 외부로 노출되어 향후 수정하기 어려워짐
- JSP, Freemarker에서의 ENTITY 참조
- 컴파일 시점의 검사 범위가 좁다 → ENTITY 클래스를 수정했을때 뷰에서 에러가 나는 경우가 뒤늦게 발견된다.
- JPA를 쓴다면
OpenEntityManagerInViewFilter
를 고려해야한다. - 초보 개발자는 쿼리가 실행되는 시점을 예상하지 못한다.
- JSON 응답
@JsonIgnore
,@JsonView
같은 선언을 통해 Json응답을 결정하므로 그거 보고 JSON의 형태를 예측하는 난이도가 올라감
외부 노출용 DTO를 따로 만들기
- ENTITY → DTO 변환 로직은 컴파일 타임에 체크된다.
- DTO는 비교적 구조를 단순하게 가져갈 수 있다.
- 더 단순한 JSON 응답, JSP에서 쓰기 좋은 구조를 만들기에 유리하다.
- DTO의 변화는 외부 인터페이스로 의식해서 관리하는 범위가 된다.
- 예: Swagger 스펙 활용
- 여러 ENTITY를 조합할수 있는 여지가 생긴다. (Entity 설계가 가벼워진다)
DTO의 이름 고민
- 역할별로 구분된 DTO 정의 예
- 이슈 조회 JSON 응답 : IssueResponse, IssueDto, IssueDetailDto
- 이슈 생성 JSON 요청 : IssueCreationRequest, IssueCreationCommand
- 이슈 조회 조건 : IssueQuery, IssueCriteria
- 이슈 DB 통계 조회 결과 : IssueStatsRow
- 이슈 + 코멘트 복합 조회 결과 : IssueCommentRow
Immutable 객체의 장점
- Cache 하기에 안전하다.
- 다른 레이어에 메서드 파라미터로 보내도 값이 안 바뀌었다는 확신을 할 수 있음
- DTO류가 여러 레이어를 오간다면 Immutable하면 더 좋음
Rich Domain Object
- Domain object가 가진 속성과 연관된 행위
- 해당 객체에 있는 것이 책임이 자연스럽다. (INFORMATION EXEPERT 패턴)
- 데이터 중심 → 책임 중심의 설계로 진화할 수 있다.
- 상태를 바꾸는 메서드가 포함될 수도 있다.
- 상태를 바꿀 때의 정합성 검사를 포함
- 예) Domain Event 추가. Springg Data의 AbstractAggregateRoot에 있는 메서드 참고
- Immutable이 아니게 될 수 있음
- 영속화될 Domain Object라면 상태를 바꾸는건 시스템의 상태를 바꾸는 경우에 한해야 함
- 메서드명도 그 행위를 잘 드러내어야한다. (
setTitle()
→changeTitle()
)
AGGREGATE로 ENTITY 간의 선긋기
AGGREGATE는?
- 하나의 단위로 취급되는 연관된 객체군, 객체망
- ENTITY와 VALUE OBJECT의 묶음
- 엄격한 데이터 일관성, 제약사항이 유지되어야 할 단위
- Transaction, Lock의 필수 범위
- 불변식(Invariants, 데이터가 변경될 때마다 유지돼야 하는 규칙)이 적용되는 단위
- Document DB와 어울림
AGGREGATE 간의 참조
- 다른 AGGREGATE의 Root를 직접 참조하지 않고 ID로만 참조하기
Stackoverflow의 한 답변
It makes life much easier if you just keep a reference of the aggregate's ID rather than the actual aggregate itself.
- 참조될 타입을 알수 있도록 힌트를 주는 클래스를 만들어도 좋음
public class Issue { private Association<Repo> repoId; } public class Association<T> { private final long id; public Association(long id) { this.id = id; } ... }
REPOSITORY vs DAO
- DAO는 퍼시스턴스 레이어를 캡슐화
- DDD의 REPOSITORY는 도메인 레이어에 객체 지향적인 컬렉션 관리 인터페이스를 제공
개인적으로 TRANSACTION SCRIPT 패턴에 따라 도메인 레이어가 구성되고 퍼시스턴스 레이어에 대한 FAÇADE의 역할을 하는 객체가 추가될 때는 거리낌 없이 DAO라고 부른다. 도메인 레이어가 DOMAIN MDOEL 패턴으로 구성되고 도메인 레이어 내에 객체 컬렉션에 대한 인터페이스가 필요한 경우에는 REPOSITORY라고 부른다. 결과적으로 두 객체의 인터페이스의 차이가 보잘 것 없다고 하더라도 DAO가 등장하게된 시대적 배경과 현재까지 변화되어온 과정 동안 개발 커뮤니티에 끼친 영향력을 깨끗이 지워 버리지 않는 한 DAO와 REPOSITORY를 혼용해서 사용하는 것은 더 큰 논쟁의 불씨를 남기는 것이라고 생각한다
- Repository 는 AGGREGATE ROOT를 맡아서 진행할 때. DAO는 그거 상관없이 그냥 데이터 접근할 때
프레임워크 돌아보기
- '선을 넘는 Entity' 로는 어떤 프레임워크를 써도 개발이 괴로움
- 반대로 경계가 잘 처진 Entity를 쓴다면 프레임워크의 마법이 필수적이지 않음
- 프레임워크의 특정 기능을 위해서 추구하는 객체 설계를 포기하는 상황이 적을수록 좋음
- 예: Spring JDBC의
BeanPropertyRowMapper
를 쓰려면 setter가 필수. Cache 되었을때 부작용을 막기 위해 Immutable하게 만들고 싶어도 할 수 없음
프레임워크 활용 전략
- 적용 패턴에 따라 프레임워크를 다르게 사용
- 예) CUD + 단순 R → JPA, 복합적인 R → Spring JDBC
- 복합적인 R
- JPA를 써도 쿼리작성/최적화 의식을 하면서 구현하고 있을 것임
- Native SQL 위주라면 Spring JDBC로도 쓸만함 : 단순한 쿼리 실행기, 확장 가능.
- 하나의 프레임워크로 통일한다면 Spring Data JDBC도 고려해볼만함
- AGGREGATE/ENTITY 개념으로 다루기 어려운 부분은
NamedParameterJdbcTemplate
을 직접 사용하면 됨.