본문 바로가기

개발

[스프링부트] 탈퇴 회원 처리 (Postgres DB 분리, @Scheduled)

탈퇴한 회원 그냥 repository.delete() 했는데 개빡치게 몇 년간 보관해야된다

 

음... 탈퇴한 유저 데이터에 접근하는 경우가 뭐지

 

흠. 삭제 회원 데이터 접근할 일 딱히 없을 것 같고.. DB 분리하는게 성능상 이점이 있다

 


DB 분리

module-common → [application.yml]

DB를 2개 이상 사용할 경우, 직접 DataSource를 생성해야한다.

 

yml 파일 설정 중 만난 오류
: jdbcUrl is required with driverClassName.

 

 

HikariCP의 Database URL 설정은 url이 아닌 jdbcUrl을 사용하기 때문에 발생하는 오류로, 대부분의 블로그에서 datasource.url을 datasource.jdbc-url로 변경하라고 추천한다.

하지만 자동설정의 경우엔 datasource.jdbc-url이 아닌 datasource.url이 Datasource의 url이 된다.
만약 datasource.jdbc-url로 값을 설정하면 자동설정에선 인식하지 못한다.

 

SpringBoot에선 HikariCP의 설정이 추가로 있다.
application.properties, application.yml에 spring.datasource.hikari로 값을 세팅하면 된다~!

 

spring:
  application:
    name: module-common
  datasource:
    db1:
      hikari:
        jdbc-url: jdbc:postgresql://localhost:5432/{DB name}
        username: {username}
        password: {password}
        driver-class-name: org.postgresql.Driver
    db2:
      hikari:
        jdbc-url: jdbc:postgresql://localhost:5432/{DB name}
        username: {username}
        password: {password}
        driver-class-name: org.postgresql.Driver

 

 

module-common → [Db1Config]

: DB1Config로 기존의 project DB 설정

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        basePackages = "com.example.domain.member.repository", // db1의 repository 패키지
        entityManagerFactoryRef = "db1EntityManager",
        transactionManagerRef = "db1TransactionManager"
)
@EntityScan(basePackages = "com.example.domain.member.entity") // db1의 entity 패키지
public class Db1Config {

    @Primary
    @Bean(name = "db1DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.db1.hikari")
    public DataSource db1DataSource() {
        return DataSourceBuilder.create().build();
    }

    @Primary
    @Bean(name = "db1EntityManager")
    public LocalContainerEntityManagerFactoryBean db1EntityManager(
            EntityManagerFactoryBuilder builder,
            @Qualifier("db1DataSource") DataSource dataSource) {
        Map<String, Object> properties = new HashMap<>();
        properties.put("hibernate.hbm2ddl.auto", "update");
        properties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");

        return builder
                .dataSource(dataSource)
                .packages("com.example.domain.member.entity") // db1의 엔티티 패키지
                .persistenceUnit("db1")
                .properties(properties)
                .build();
    }

    @Primary
    @Bean(name = "db1TransactionManager")
    public PlatformTransactionManager db1TransactionManager(
            @Qualifier("db1EntityManager") EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }
}

 

 

module-common → [Db2Config]

: DB2Config로 추가 project2 DB 설정

@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(
        basePackages = "com.example.domain.deleted.repository", // db2의 repository 패키지
        entityManagerFactoryRef = "db2EntityManager",
        transactionManagerRef = "db2TransactionManager"
)

@EntityScan(basePackages = "com.example.domain.deleted.entity")
public class Db2Config {

    @Bean(name = "db2DataSource")
    @ConfigurationProperties(prefix = "spring.datasource.db2.hikari")
    public DataSource db2DataSource() {
        return DataSourceBuilder.create().build();
    }

    @Bean(name = "db2EntityManager")
    public LocalContainerEntityManagerFactoryBean db2EntityManager(
            EntityManagerFactoryBuilder builder,
            @Qualifier("db2DataSource") DataSource dataSource) {
        Map<String, Object> properties = new HashMap<>();
        properties.put("hibernate.hbm2ddl.auto", "update");
        properties.put("hibernate.dialect", "org.hibernate.dialect.PostgreSQLDialect");

        return builder
                .dataSource(dataSource)
                .packages("com.example.domain.deleted.entity") // db2의 엔티티 패키지
                .persistenceUnit("db2")
                .properties(properties)
                .build();
    }

    @Bean(name = "db2TransactionManager")
    public PlatformTransactionManager db2TransactionManager(
            @Qualifier("db2EntityManager") EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }

 

 

DB 정상 분리 완료!


PostgreSQL 스케줄러

(파티셔닝으로 해보려고했는데 JPA로 생성한 Entity와 SQL 스크립트로 생성한 Entity간에 충돌이 있어서 스케줄러로 변경)

 

module-common → [DeletedMember]

: project2 DB에 회원 탈퇴시 사용될 엔티티 DeletedMember 생성

@NoArgsConstructor
@Entity
@Table(name = "deleted_member")
public class DeletedMember {
    @Id
    @Column(name = "deleted_member_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true)
    private String email;

    @Column(unique = true)
    private String nickname;

    @Column(unique = true)
    private String phone;

    @Enumerated(EnumType.STRING)
    private Gender gender;

    @Column
    private String password;

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate birthday;

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @Column(name = "created_at")
    private LocalDate createdAt;

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    @Column(name = "cancelled_at")
    private LocalDate cancelledAt;

    @Enumerated(EnumType.STRING)
    private Role role;

    @Enumerated(EnumType.STRING)
    private Provider provider;

    @OneToMany(mappedBy = "deletedMember", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<DeletedProfile> deletedProfiles = new ArrayList<DeletedProfile>();
}

 

 

module-common → [DeletedProfile]

: project2 DB에 회원 탈퇴시 사용될 엔티티 DeletedProfile 생성

@NoArgsConstructor
@Getter
@Setter
@Entity
@Table(name = "deleted_profile")
public class DeletedProfile {

    @Id
    @Column(name = "deleted_profile_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @JsonIgnore
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "deleted_member_id")
    private DeletedMember deletedMember;

    @Column
    private String nickname;

    @Enumerated(EnumType.STRING)
    private Gender gender;

    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate birthday;

    @Column
    private Boolean owner; // 본인인지, 아닌지 True, False

    @Enumerated(EnumType.STRING)
    private Choice pregnancy;

    @Enumerated(EnumType.STRING)
    private Choice smoking;

    @Enumerated(EnumType.STRING)
    private Choice hypertension;

    @Enumerated(EnumType.STRING)
    private Choice diabetes;
}

 

 

module-common → [DeletedMemberRepository]

@Repository
public interface DeletedMemberRepository extends JpaRepository<DeletedMember, Long> {

    List<DeletedMember> findByCancelledAtBefore(LocalDate date);
}

 

 

module-common → [DeletedServiceImpl]

: DeletedMember의 DeletedProfile 리스트에 CascadeType.ALL, OrphanRemoval = true 설정을 걸어 DeletedMember가 삭제되면 DeletedProfile이 함께 삭제되도록 설정하였다. 그래서 DeletedProfile을 삭제하는 로직은 따로 존재하지 않는다.

@Scheduled(fixedDelay = 30000) // 30초
    @Transactional
    public void cleanupDeletedMembers() {
        // 3년 전에 탈퇴한 회원을 삭제하는 로직
        LocalDate threeYearsAgo = LocalDate.now().minusYears(3);

        List<DeletedMember> membersToDelete = deletedMemberRepository
                .findByCancelledAtBefore(threeYearsAgo);

        for (DeletedMember member : membersToDelete) {
            deletedMemberRepository.delete(member);
        }
        System.out.println("Deleted members older than 3 years.");
    }

→ 테스트를 위해 fixedDelay = 30000으로 설정

 

@Scheduled(cron = "0 0 0 * * ?")

→ 이후 자정마다 실행되는 로직으로 변경 

 


테스트

탈퇴일자를 나타내는 cancelled_at '2020-09-11'으로 설정

 

30 초 간격으로 스케줄러 정상 작동

 

제대로 삭제되는거 확인