MySQL에서 이메일 재사용 가능하게 하기- Soft Delete와 Unique를 함께 활용하다.
개발환경
- Javascript 런타임 플랫폼: Node.js
- 언어: TypeScript
- 프레임워크: Express
- DB: MySQL
- ORM: TypeORM
현재 조건 상황
- 회원가입시, 이메일과 패스워드가 필수 입력입력인데, 이메일의 경우 mySQL에서 unique 처리
- 회원가입 과정에서 이메일 중복 여부를 확인하고 통과했을 경우에만 회원가입 가능
- 회원 삭제시, 실제 회원정보를 DB에서 지우지 않고 TypeORM의softDelete 방식으로 deleted_at 컬럼에 삭제일시가 기록되는 방식 (일정기간 이후 삭제할 요량으로 단기간 데이터 보존)
해당 이메일은 이미 mySQL에서 unique로 입력되어있는 이메일이기 때문에 이메일 중복확인에서 통과되지 않는다.
해당 이메일을 재사용하려면?
3가지 방안들
- mySQL - entity - email column 에서 unique 조건을 제거하고, 중복여부를 필터링하는 코드를 추가하는 방법으로 진행한다.
- DB에서의 unique 조건을 유지시키기 위해 삭제 로직 중, email을 변경하여 저장한다.
- 삭제된 email로 회원가입이 들어올 경우, 복구 과정을 추가한다.
1번의 경우
mySQL의 unique 제약 조건을 제거하면 이메일 중복 확인 로직을 코드에서 추가적으로 수정하여 처리해야 한다.
즉, deleted_at
컬럼이 null
인지 아닌지에 대한 확인이 필요하기에 Users
DB에서의 조건이 실행되어야 한다.
이 경우, 데이터베이스에서의 조회 로직에 Indexing이 들어간 Unique 컬럼 조회와 더불어 전체스캔을 필요로 하는 MySQL의 Is Null
절을 추가적으로 사용해야하며 , 이로 인해 성능에 약간의 영향을 미칠 것으로 예상된다.
또한 개발 도중의 문제로 deleted_at
컬럼의 데이터에 문제가 생긴다면 걷잡을 수 없는 혼란을 야기할 수도 있다.
그러나 이 방법은 프로그래밍적으로 더 유연하게 사용할 수 있고, 사용자의 이메일을 변경하지 않아도 되기에 데이터의 원형을 유지하면서도 새 사용자가 이전에 사용되었던 이메일을 재사용할 수 있다는 이점이 있다.
2번의 경우
예를 들어, 사용자가 삭제되면 해당 사용자의 이메일을 "[email protected]"
와 같이 변경하여 mySQL에서의 unique 조건을 그대로 유지할 수 있다.
이 방법은 데이터베이스의 무결성을 유지하면서도 이메일 재사용 문제를 해결할 수 있다.
하지만 이 경우 사용자의 원래 이메일을 변경해야 하므로 데이터의 원형을 유지하는 데 어려움이 있을 수 있다.
3번의 경우
복구과정을 추가할 시, 애초에 이메일을 오타로 입력하여 등록이 되어버리면 이미 이 이메일은 이전 회원의 소유로 귀속되기에 사실상 원래 이메일의 소유자가 회원가입을 하려고 할 때 불가능해진다.
따라서 이 방법은 패스!
고려사항
1번과 2번의 방안을 두고 저울질을 해봤을 때,
- 1번 : unique 조건을 해제하고 코드를 추가하여 개발 유연성을 확보하면 상시적으로 시스템의 성능에 영향이 간다.
- 2번 : unique 조건을 유지시키고, 회원 삭제시 데이터 원형을 변형시키는 방법을 하였을 때에는 추후 복구시 다시 데이터 원형을 살릴 수 있는 코드가 추가되어야 한다.
즉, 추가코드는 필수적이지만 상시적으로 해당 코드를 운용할 것인가? 간헐적인 사용시 해당 코드를 운용할 것인가의 문제로 귀결되면서 2번의 방법을 사용하기로 결정하였다.
그리고 롤백시 데이터 원형을 살릴 수 있으면서 알아보기 쉽도록 deleted.현재시간
이라는 규칙을 가진 string
을 추가하여 변형된 email 주소를 업데이트 하기로 했다.
또한 이 방법은 추후 deleted_at
컬럼의 내용에 대한 백업 역할을 기대해볼 수도 있다.
이렇게 하면 추후에 .deleted
이후의 문자를 모두 날려버리는 간단한 아래 코드만으로 복구가 가능해진다.
const recoveredEmail = email.replace(/\.deleted\.\d+/, '');
이리하여 변경된 프로젝트의 코드는 다음과 같다.
변경 전
// user.service.ts의 userDelete 함수 중,
// ... 이전 코드
// 사용자 정보의 유효성 검사 함수를 불러온다.
await findUserInfoByUserId(userId);
// transaction을 시작한다.
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 사용자의 User entity를 삭제한다.
await queryRunner.manager.softDelete(User, userId);
// ... 이후 코드
변경 후
// user.service.ts의 userDelete 함수 중,
// ... 이전 코드
// 사용자 정보의 유효성 검사 함수를 불러온다.
const userInfo = await findUserInfoByUserId(userId);
// transaction을 시작한다.
const queryRunner = dataSource.createQueryRunner();
await queryRunner.connect();
await queryRunner.startTransaction();
try {
// 사용자의 email을 변경한다. 추후 해당 email의 재사용을 위한 고민중 230607 추가
const email = `${userInfo.email}.deleted.${Date.now()}`;
// 객체 리터럴 단축구문으로 email의 변경내용을 간략하게 표현한다. 230607 추가
await queryRunner.manager.update(User, userId, { email });
// 사용자의 User entity를 삭제한다.
await queryRunner.manager.softDelete(User, userId);
// ... 이후 코드
변경 후, 실행결과 출력