회원 탈퇴 구현

Chocomilk
Dong-gle
Published in
7 min readSep 20, 2023

1. 개요

동글에서 회원 탈퇴 구현 기능을 맡았다. 우리의 정책은 다음과 같다.

회원 탈퇴시 30일 동안 정보를 저장 후, 30일이 지나면 해당 회원이 가지고 있던 모든 테이블의 레코드를 삭제한다.

2. 회원 탈퇴

2.1. 회원 탈퇴 애플리케이션 로직 구현

이를 구현하기 위해 애플리케이션에서는 soft delete 개념을 도입하였고, isDeleted라는 컬럼과 언제 탈퇴했는지를 나타내는 deletedAt 컬럼을 추가하였다.

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
...
private boolean isDeleted = false;
private LocalDateTime deletedAt;
...

public void updateDeletedAt() {
this.deletedAt = LocalDateTime.now();
}
}

기본적으로 Member를 생성하면 isDeletedfalse로 두었다. 만약 회원 탈퇴 요청이 있다면, 서비스 레이어에서 updatedDeletedAt()을 호출하여 언제 회원 탈퇴를 하였는지 저장했다. 이 날짜를 활용해 30일이 지난 회원인지 아닌지 확인할 수 있다.

2.2. Trouble Shooting

soft delete를 도입하게 되면, 모든 조회 요청마다 해당 memberisDeleted 속성이 false인지 where조건문에 명시를 해주어야 한다. 이 부분을 빼먹으면 이미 탈퇴한 회원들의 값도 조회가 되기 때문에 유의해야 한다. 또한 값을 삭제할 때에 DELETE 쿼리가 아니라 실질적으로 isDeleted 속성을 바꿔주어야 하므로 UPDATE 쿼리를 날려야 한다. 이 부분도 개발자가 팀 내의 정책을 잘 파악하고 유의해야 할 부분 중 하나일 것이다.

2.3. 해결 방안

@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@SQLDelete(sql = "UPDATE member SET is_deleted = true WHERE id = ?")
@Where(clause = "is_deleted = false")
public class Member extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private boolean isDeleted = false;
private LocalDateTime deletedAt;
}

스프링에서는 위처럼 @SQLDelete 애너테이션을 붙이면, 실질적으로 DELETE 쿼리가 나갈 때 이를 캐치하여 명시된 쿼리를 날려주는 것이다. 즉 writingRepository.delete(writing)을 실행하면 DELETE 쿼리가 아니라 UPDATE 쿼리인 UPDATE member SET is_deleted = true WHERE id = ?가 나간다는 것이다. 이로써 DELETE 쿼리를 날리더라도 안전하게 soft delete를 만족할 수 있게 해준다.

또한 @Where 애너테이션을 붙이면, 모든 조회 쿼리의 where절에 is_deleted = false를 붙여준다. 이로써 개발자가 신경쓰지 않더라도 모든 조회 쿼리에 대해 안전하게 탈퇴하지 않은 회원들만 조회할 수 있도록 해준다.

2.4. 예상되는 문제점

  1. SQL 관련 기술이 엔티티에 침범되었다.
  2. 모든 where 조건문에 is_deleted = false 가 붙었으므로, 이 컬럼에 대한 인덱스를 고려해야 한다.
  3. 스프링에서 제공하는 이 애너테이션 기능을 잘 모른다면, 쿼리의 형태가 어떻게 되는지 파악하기 힘들 수 있다.

하지만.. 어찌됐든 서비스를 운영하는 입장에서 실수를 하는 상황을 만드는 것보다 도메인에 SQL 관련 기술이 침투하더라도 안전하고 편하게 운영할 수 있는 것이 더 중요하다는 생각이 들었다. 또한 인덱스야 걸어주면 되고, 애너테이션 기반의 기능 또한 팀원들에게 잘 알려주고 문서화를 해두면 안전하고 빠르게 기술을 개발할 수 있을 것이라고 판단했다. 따라서 동글은 위와 같은 방식으로 soft delete를 구현하였다.

3. 스케줄링

3.1. Trouble Shooting

이제 하루마다 한 번씩 데이터베이스 테이블을 돌면서, 회원 탈퇴를 한지 30일이 지난 회원들의 데이터를 모두 삭제하는 것을 구현해야 하는 문제를 만났다. 이를 위해서 우리가 사용할 수 있는 방법으로 생각해 본 것은 다음과 같다.

  1. 스프링의 스케줄러
  2. EC2 인스턴스 내에서 crontab을 사용한 sql 스크립트 실행

하루에 한 번씩 나름 대용량의 데이터를 처리할 수 있는 상황이라는 것이 고민이 되었다. 물론 현재는 하루에 회원 탈퇴를 한지 30일이 지난 회원이 그렇게 많지 않고, 새벽 3시에 작업을 진행하기에 서비스의 퍼포먼스에 그렇게 문제가 되지 않을 것 같았다.

하지만 하루에 처리해야할 데이터의 양이 매우 많다면 어찌됐든 성능상의 문제가 발생할 수 있을 것이고, 이때 EC2 인스턴스 내에서 스크립트를 실행한다면 애플리케이션의 퍼포먼스에 영향을 주지 않고 작업을 실행할 수 있을 것이라고 생각했다.

또한 해당 작업을 애플리케이션에 종속되지 않고 독립적으로도 실행할 수 있다는 점에서 두 번째 방법을 채택하게 되었다.

3.2. delete_member.sql

SET @cutOffDate = DATE_SUB(NOW(), INTERVAL 30 DATE);

START TRANSACTION;

DELETE FROM image_block
WHERE id IN(
SELECT id
FROM block
WHERE writing_id IN (
SELECT id
FROM writing
WHERE member_id IN (
SELECT id
FROM member
WHERE is_deleted = true AND deleted_at <= @cutOffDate
)
)
);

DELETE FROM block
WHERE writing_id IN (
SELECT id
FROM writing
WHERE member_id IN (
SELECT id
FROM member
WHERE is_deleted = true AND deleted_at <= @cutOffDate
)
);

DELETE FROM writing
WHERE member_id IN (
SELECT id
FROM member
WHERE is_deleted = true AND deleted_at <= @cutOffDate
);

DELETE FROM category
WHERE member_id IN (
SELECT id
FROM member
WHERE is_deleted = true AND deleted_at <= @cutOffDate
);

DELETE FROM member
WHERE is_deleted = true AND deleted_at <= @cutOffDate;

COMMIT;

주요한 테이블 삭제 쿼리만 작성하였다. 이를 통해 회원 탈퇴를 한지 30일이 지난 회원의 정보를 지울 수 있다.

3.3. sql_batch.bash

#!/bin/sh
sudo mysql --user=root --password={비밀번호} {데이터베이스명} < /home/ubuntu/delete_member.sql;

이를 셸 스크립트를 실행하면 우리의 데이터베이스에 바로 접속해, 위에서 작성한 deleted_member.sql 스크립트를 실행시킬 수 있다. 비밀번호를 스크립트 상에 명시해줘야 하므로 파일이 노출되지 않도록 주의해야 한다.

3.4. crontab

SHELL=/bin/bash
0 0 3 * * sh /home/ubuntu/sql_batch.bash

이제 위에서 작성한 sql_batch.bash 스크립트를 특정 시간마다 호출해줄 수 있는 crontab 설정을 해보겠다. 간단하게 crontab -e 명령어로 설정을 수정할 수 있고, crontab -l 명령어로 설정 정보를 확인할 수 있다.

맨 처음 다섯 개의 명령어는 차례대로 분(0-59), 시간(0-23), 일(1-31), 월(1-12), 요일(0-7) 을 의미한다. 이후에는 sh /home/ubuntu/sql_batch.bash 명령어를 통해 해당 셸 스크립트를 실행하게 하였다.

--

--