우아한 CRUD - Spring Data JDBC Advanced
2022, Sep 03
강의 주소
[우아한테크세미나] 200507 우아한CRUD by 정상혁&이명현님
github
https://github.com/mhyeon-lee/spring-data-sample-codes
O/R Mapping
- jpa와 차이점
- @Value : jpa는 proxy로 만들어야하는데 @Value로 선언한 것들은 불변 final객체여서 이것들을 proxy로 만들수 없다. 반면 spring data jdbc는 proxy로 만들지 않기 때문에 가능하다.
- @PersistenceConstructor
- jpa의 경우는 기본적으로 프록시로 만들기 때문에, Bean 객체에 @NoArgsConstructor를 필수로 붙이고 reflection을 사용해서 결과를 필드와 바인딩하는 방식인데, spring data jdbc는 그런방식을 선호하지 않는다 (final을 사용하지 않으려고 reflection을 쓰지도 았는다.) 그래서 PersistenceConstructor를 생성자에 붙이고 생성자를 통해서 객체를 구현할 수 있도록 하고 있다. 생성자가 하나면 안붙여도 되고, 생성자가 여러개 인 경우 붙여서 db를 통해 맵핑하는게 먼지 알려줘야한다.
- 연관 관계
- spring data jdbc는 entity 설계시 Aggregate 개념 적용을 하라고 말한다. 안그러면 사용하기 어려워진다.
- AggregateRoot 하나에 Repository 하나로 구성
- OneToOne, OneToMany 지원
- 하위 클래스를 맵핑하게 되는데, 그 엔티티들이 db상으로는 pk가 존재하지만, 객체 클래스에는 @id 를 선언하지 않아도 된다. (jpa와의 차이점)
- ManytoOne, ManyToMany 미지원
- Aggregate의 개념에 위배되므로 향후에도 지원하지 않을 것으로 보임
- 양방향 연관관계 매핑 금지
- Aggregate간의 관계는 Reference Id로 관리한다.
- AggregateReferene로 타입지원 가능
- Embedded
- @Embeded를 jpa와 마찬가지로 지원하는데, 차이가 있다면, @Embedded.Nullable와 @Embedded.Empty가 있다.
- Type Converter 설정
- jdbc driver에서 지원하지 않는 타입들은 custom converter를 작성해야한다.
- jpa처럼 특정 필드에 @Converter를 설정할 수 없기 때문에, 별도의 타입을 선언하고 해당 타입 전용 Converter를 등록해줘야한다.
- DDL 관리
- hibernate와 같은 자동 ddl생성기능은 없다.
- 대신 liquibase나 flyway와 같은 schema관리 솔루션을 활용가능하다.
- Spring Data R2DBC
- 연관관계나 Embedded 미지원
CRUD - Create
- jpa의 @GeneratedValue 기능이 없기 때문에, 그냥 save호출하면 insert가 아니라 update가 실행된다. 그래서 insert를 해야한다고 하면 @id속성 값 존재여부로 update와 분기해야하는데
- auto_increment를 사용하지 않고, 직접 id값을 할당하는 경우는 아래 전략을 취해야한다.
- Persistable인터페이스 구현
- 필드하나를 생성해서 지금 실행하는게 insert인지 update인지를 분기하도록 한다.
- jpa의 postPersist 비슷한 클래스를 생성해서 bean으로 등록한 후, insert실행된 이후에 위에서 선언한 필드의 값을 update가 되도록 값을 변경하는 인터페이스를 구현
- AfterSaveEvent 또는 AfterSaveCallback을 등록해서 insert후 값 변경 가능
- BeforeSaveEvent / BeforeSaveCallback 등록
- save나 callback을 bean으로 등록하고 나서 insert나 update되기전에 callback이 실행되고, callback이 들어가기전에 id를 할당하고 그담에 callback실행시키게 하는 것
- @Version 사용
- 2.0.0.RC2에 추가된 기능
- Optimistic Locking을 사용하기 위해서 쓴다. @Version 값이 0 또는 널이라면, @Id값이 존재해도 save실행시 insert 동작한다.
- Insert 지원 CustomRepository 구현
- spring data의 customRepository 확장해서 쓰는 방법을 활용하는 것
- Persistable인터페이스 구현
- Lifecycle Events vs. Entity Callbacks
- 다른 비즈니스를 처리하기 위한 Trigger는 Event를 사용하고, 영속 대상이 되는 Aggregate 객체의 속성을 조작할 때는 Callback을 사용한다.
- 연관관계 Cascade
- AggregateRoot에서부터 연결되는 엔티디들에 다 적용된다.
- OneToOne/ OneToMany 구분없이 동일한 순서로 insert실행된다.
CRUD - Update
- 순서
- root부터 update하고, 연관관계를 가지는 하위를 다 삭제하고 다시 다 insert한다.
- update root → delete all referenced → insert all referenced
- 문제
- 하위의 pk가 auto_increment라고 하면 id가 변경되니까 문제된다. 그래서 id 없이 하는 것이 좋다.
- 동시 수정 데이터 유실 가능성
- 현재 aggregate상태를 그대로 저장하기 때문에 데이터에 동시에 접근한 후 update하면, 데이터 유실 가능성이 있다.
- Optimistic Locking (2.0.0.RC2)
- @Version을 지정하면 update구문의 where절에서 version matching을 한 후 결과를 체크한다.
- Pessimistic Locking
- update를 수행하기 전에 for update로 조회하면, pessimistic locking을 걸수 있다. 이러면 동시 데이터 update로 인한 유실을 막을 수 있다.
- 비효율
- 연관관계를 모두 삭제하고 다시 넣기 때문에 비효율적인 측면이 있다.
- 복잡한 연관관계 설계를 지양한다.
- 일부 속성 변경할 땐 @Modifying을 사용할 수 있다.
- 데이터 수정/삭제는 한 방향으로 진행되어야 경합 상황에서 dead lock발생을 줄일 수 있다.
- 연관관계를 모두 삭제하고 다시 넣기 때문에 비효율적인 측면이 있다.
CRUD - Delete
- Soft Delete
- hibernate에서는 @SQLDelete를 사용해서 Delete실행시 삭제 판별 Column을 update하는 soft delete를 구현할 수 있다. → 실제로 row를 삭제하는 것이 아니라 상태만 delete되었다고 데이터 update하는 것을 말한다.
CRUD - Read
- findById
- n+1 fetch 문제있다. - entityrowmapper에 대해서 학습 후 다시 보자
- derived query
- findBy~~, countBy~ 로 할 수 있게
- 이것도 n+1문제 있음
- 복합View조회
- 서비스가 고도화될수록 다양한 view제공 요구사항이 발생한다.
- 복잡한 조회조건과 데이터를 도메인 모델(aggregate)에 녹이려고 노력하지 말아야 한다.
- 도메인(비즈니스) 요구사항이 변경되면 View가 변경되는가? - 아니다.
- View가 변경되면 도메인(비즈니스) 요구사항이 변경되는가? - 아니다.
- 도메인(Command) 요구사항과 조회(Query)요구사항은 다뤄야 하는 범위가 다르다
- 서로 다른 논리를 합치면 복잡도는 N+N이 아니라 N*N이상이 된다.
- 시스템이 고도화 될 수록 새로운 비즈니스 요구사항 수용과 확장에 어려움을 겪게 된다
- 그래서 별도의 쿼리 모델을 만들어야 한다 → CQRS
- Presentation Model을 Domain Model에서 분리한다.
- Spring JDBC를 사용한다
- 다양하고 복잡한 조회 요구사항 수용과 최적화를 위한 SQL튜닝이 가능해야 한다
- SQL작성이 조금 간편했음 좋겠다
- Spring Data Jdbc의 객체-컬럼 매핑은 활용하고 싶다.
- Query Model은 불변 객체(Immutable Obejct)로 만들고 싶다
- N+1 Fetch로 1:N 구조의 조회 모델에는 Spring Data Jdbc의 ENtityRowMapper를 활용하기 어렵다
- naver에서 이걸 해결하기 위한 라이브러리 작성 (spring jdbc plus)
- https://github.com/naver/spring-jdbc-plus
- spring data jdbc의 객체-컬럼 매핑 활용
- 매핑을 활용한 select statements작성 지원
- 개선된 sqlParameterSource 제공
- 1:N조회 결과 매핑을 지원하는 AggregateResultSetExtractor제공