회원탈퇴 로직을 작성하면서
회원을 삭제하려면 UserService에서 많은 Entity들을 삭제해야 하는 문제가 있었다.
- 도메인을 분리하지 않았던 문제가 가장 크다.
- 댓글과 좋아요는 여행일정 게시물 테이블과 사용자를 참조하고 있다.
- 여행일정 게시물과 토큰은 사용자를 참조하고 있었다.
FK 제약조건의 문제점
데이터 무결성을 위해서 사용하면 좋지만 회원탈퇴를 하면서 문제가 있었다.
사용자를 제거하기 위해서는 아래와 같은 순서를 지켜줘야 한다.
1. 여행 일정 게시물을 삭제하기 전에 댓글과 좋아요를 제거해야 한다.
- 목적은 여행일정이 아니라 사용자를 제거하기 위함이다.
- 사용자가 작성한 게시물과 게시물과 연관된 다른 사용자의 좋아요 댓글 + 사용자가 작성한 댓글 좋아요도 전부 삭제해야 한다.
2. 사용자를 삭제하기 전에 여행 일정 게시물과 토큰을 제거한다.
- 여행일정에는 사용자가 몇박며칠 여행을 갈것인지 하루에 어떤 장소를 갈것이고 경비는 얼마인지등의 정보가 필요해서 위와 같은 테이블로 설계했다.
JPA를 사용해서 구현했으며 여행일정을 제거하면 orphanRemoval를 통해서 모든 일정들이 제거되는데 여행 장소들은 하루 일정에 대해서 값타입 컬렉션으로 되어 있기 때문에 같이 삭제가 되었다.
여행 일정
몇박며칠에 대한 정보
- 내가 생각했을 때 DB에서는 이렇게 사용하지 못하지만 ORM을 사용하는 만큼 Schedule은 값타입 컬렉션으로 몇박며칠에 대한 정보들을 가지고 있어야 하고 각 하루에 대한 정보 또한 값 타입 컬렉션으로 여행할 장소들에 정보를 가지고 있는게 맞다고 생각했다.
- @ElementCollection 내부에 @ElementCollection이 올 수 없어서 이걸 표현하고자 @OneToMany를 사용했다.
- @ManyToOne으로 여행일정을 양방향으로 가져갈까 고민이 된다.
- cascade = CascadeType.PERSIST가 적용되어 있고 장소들을 값타입 컬렉션을 사용하기 때문에 여행일정 save() 호출 한번에 엄청나게 많은 쿼리들이 나가게 된다.
- insert만 update가 나가는게 아니라 delete할 때도 내부 컬렉션들을 여행 일정 ID를 null로 update치고 삭제한다.
- ID값을 Auto-Increment로 사용하고 있기 때문에 Batch Insert도 사용하지 못하고 있어서 개선할 방법을 찾아봐야 될거 같다.
하루 일정의 여행 장소들
2번에서는 1번이 정상적으로 진행된다면 사용자를 제거하기 전에 여행일정, 토큰값만 지워주면 된다.
3. 사용자 삭제
User table에서 orphanRemoval을 전부 걸어주는 방법도 있었다.
계층 분리를 잘해준다면 사용해줄 수도 있겠지만 양방향으로 가져올 필요가 없다고 생각했고 이벤트를 적용하기로 생각했다.
Evnet 처리하기
Spring을 이용한다면 간단한 Pub/Sub 기능을 구현 할 수 있다.
- UserService(ApplicationEventPublisher)에서 이벤트를 알려주면 구독(@EventListener)하고 있는 쪽에서 로직을 실행할 수 있게 된다.
이벤트 발행 서비스
password가 올바르다면 이벤트를 발생시킨다.
- 최범균님의 DDD책을 보면서 "기존 암호를 올바르게 입력했는지 확인하는 것은 도메인 로직의 핵심 로직이다."라는 문구가 있었고 궁금한게 있었어서 질문을 했었는데 ㅜㅜ 감사하게도 답변이 왔었다.
Event 객체
Event 객체를 넘겨주면 여행일정, 댓글, 좋아요, 토큰, 사용자를 처리해주는 각각의 @EventListener에서 처리하면 된다.
- 삭제하다가 실패하면 Rollback을 하기 위해서 커밋되기 전에 같은 트랜잭션에서 모든 이벤트를 처리했다.
FK 제약 조건으로 인해서 순서대로 데이터를 삭제해야 하기 때문에 @Order 어노테이션을 이용해서 동기처럼 구현했다.
- 데이터 정합성을 위해서 비동기로 처리할 수 없는 형태였다.
- @Order(숫자가 작으면 우선순위 높음)를 사용해서 순서를 적용해줬으며 댓글 좋아요를 따로 삭제할 수 있지만 여행 일정 Event에서 한번에 처리해줬다.
댓글 좋아요를 삭제하고 여행 일정을 삭제해줬다.
Token은 사용자가 제거되기 전에만 삭제되면 된다.
@Order의 default 값은 제일 낮은 우선순위다. 따라서 마지막에 사용자를 삭제할 수 있었다.
이벤트 로직을 작성하면서 느낀점
동기로 처리해서 무결성을 지킬 수 있었으며 이벤트 덕분에 결합도를 느슨하게 가져올 수 있었다.
하지만 지금도 latency가 어느정도 있는데 데이터가 많아지고 User를 참조하는 테이블이 더 많이 생긴다면 이 때도 이방법을 사용할 수 있을까? 나는 그렇지 못한다고 생각한다.
또한 순서를 맞춰서 삭제해 줬음에도 불구하고 orphanRemoval로 데이터를 제거해 줄 때 FK 제약 조건으로 인한 데드락이 발생했다.
- orphanRemoval로 인해서 자식 테이블에 데이터를 제거해줘야 하는데 FK 제약 조건으로 인한 공유락이 원인이라고 생각했다.
- 즉, 자식 테이블에서 Lock이 걸려있는데 orphanRemoval로 인해서 자식 테이블을 삭제해야 하기 때문에 발생한 문제
실무에서는 지금의 방식처럼 사용자의 데이터를 삭제하지 않는다.
CAP Theorem을 생각해보면 지금의 구조에서 성능이 안나오는 것이 이해가 간다.
BASE란?
- ACID와 대조적으로 가용성과 성능을 중시하는 특성을 가진 분산 시스템의 특성
1. 기본적인 가용성 (Basically Avaliable)- 부분적인 고장은 있을 수 있으나, 나머지는 사용이 가능하다.
* 주 서버가 안되더라도 백업 서버는 동작한다.
- 노드의 상태는 외부에서 전송된 정보를 통해 결정됨.
- 분산 노드 간 업데이트는 데이터가 노드에 도달한 시점에 갱신.
* 최신 상태의 데이터로 덮어써진다.
- 일시적으로 비일관적인 상태가 되어도 최적으로는 일관성이 있는 상태가 되는 성질
* 시스템 부하, 네트워크 속도 등의 외부 요인으로 인해 일관성이 일시적으로 깨질 수 있다.
- 부분적인 고장은 있을 수 있으나, 나머지는 사용이 가능하다.
BASE 원칙은 전통의 트랜잭션 시스템을 위한 ACID 원칙에 반대되는데, 이는 분산 환경에서 나타나는 특징이기 때문이다.
이러한 특징에 대해 CAP이론은 다음 3가지 조건을 모두 만족하는 분산 시스템을 만드는 것이 불가능함을 정의한다.
- 일관성 (Consistency) : 모든 시스템의 데이터는 어떤 순간에 항상 같은 데이터를 갖는다.
- 가용성 (Availability) : 분산 시스템에 대한 모든 요청은 내용 혹은 성공/실패에 상관없이 응답을 반환할 수 있다.
- 내구성 (Partition Tolerance) : 네트워크 장애 등 여러 상황에서도 시스템은 동작할 수 있다.
위의 3가지 성질을 모두 만족할 수 없고, 일반적으로 다음과 같이 선택된다.
- CP (Consistency & Partition Tolearance) :
어떤 상황에서도 안정적으로 시스템은 운영되지만 Consistency 가 보장되지 않는다면 Error를 반환한다. (어떤 경우에도 데이터가 달라져서는 안된다.)
* 이는 매 순간 Read/Write에 따른 정합성이 일치할 필요가 있는 경우 적합한 형태이다.
- AP (Availability & Partition Tolerance) :
어떤 상황에서도 안정적으로 시스템은 운영된다. 또한 데이터와 상관없이 안정적인 응답을 받을 수 있다.
다만 데이터의 정합성에 대한 보장은 불가능하다. (특정 시점에 Write 동기화 여부에 따라 데이터가 달라질 수 있다.)
* 이는 결과적으로는 일관성이 보장된다는 Eventual Consistency를 보장할 수 있는 시스템에 알맞는 형태이다.
- 일관성은 어느정도 포기해야 성능을 가져올 수 있다는 뜻이다.
성능 개선을 한다면?
FK를 주지말고 참조할 ID값만 가지고 있게 만든다. (JPA에서 객체 참조가 아니라 ID값을 사용했을 때 장점을 느낄 수 있었다.)
- 지금 구조에서 동기로 처리하지 않고 비동기로 처리할 수 있게된다.
- 도메인이 완벽하게 분리가 되어 있었다면 사용자는 항상 탈퇴하지 않은 사용자만 조회를 할 수 있었을거고 이 사용자의 댓글, 좋아요 게시물을 조회한다면 회원이 탈퇴될 때 여행일정, 댓글, 좋아요를 삭제하지 않아도 된다고 생각이 들었다. (바로 삭제할 필요가 없다.)
isDeleted라는 필드를 추가해서 회원 탈퇴 API 호출시 true만 설정해주면 된다.
- 조회할 때는 where isDeleted = false로 조건을 추가해 조회한다.
- 즉, 얕은 삭제를 해주고 사용자가 자주 이용하지 않는 새벽시간에 배치로 삭제하는 방법이 있을거 같다.
결론
느슨한 결합도를 갖어야 한다고 생각이 들어서 Event로 처리할 수 있었지만 이벤트를 이렇게 사용하기 보다는 얕은 삭제를 사용하는게 더 올바른거 같다.
- 성능 개선을 하고 싶어서 트랜잭션 전파 옵션도 고민하고 비동기도 고민하면서 재미있는 시간을 보냈던거 같다.
(DDD 이야기가 왜 나오는지 몸으로 느끼는 시간이었다.) - 데드락이 발생하면서 기초적인 데이터베이스 공부를 안한거 같다는 생각이 들었다.
결론적으로 사용자는 바로 삭제할 필요는 없었고 내가 요구사항을 명확하게 파악하지 못한게 아닐까라는 생각이 들었다.
- 다음에 비슷한 상황이 온다면 더 좋은 방법을 선택할 수 있지 않을까 싶다.
'개발 > Spring Boot' 카테고리의 다른 글
spring batch에서 DataSource 분리 (1) | 2024.06.02 |
---|---|
Spring Batch JpaItemWriter에서 List<Entity> 처리하기 (0) | 2024.05.25 |
[Spring] 스레드 풀 (1) | 2024.01.29 |
토큰 재발급 로직을 테스트하면서 발생한 문제 (2) | 2024.01.18 |
Test에서 deleteAll과 deleteAllInBatch() (0) | 2023.07.18 |