[프로젝트] User 변경 사항 관리 및 추적하기

조찬영·2023년 10월 10일
0

User 변경 사항 관리 및 추척하려는 이유

지난번 로그인 실패 횟수 트래킹 2편 동시성 이슈 해결하기에서 로그인 5회 이상 실패시 계정 잠금 및 추가적인 이메일 인증을 요구하는 기능을 구현하였습니다.
하지만 그 과정에서 새로운 문제를 직면하게 되었는데요.

이메일 인증 성공시 기존의 UserActivity 값으로 변경해야 했지만,
User의 변경 사항에 대해 저장된 데이터가 없어 기존의 값으로 변경하는데 어려움이 있었습니다.
그래서 기존의 데이터들을 관리하는 테이블이 필요하다고 생각되었습니다.

뿐만아니라 User의 password 또는 username등이 변경될 때 기존의 데이터들을 서버쪽에서 여전히 관리해주는 것이 운영적인 측면에서도 중요하다고 생각했습니다.

그래서 이번 시간에는 User의 변경 사항에 대한 데이터를 관리하고 추적하는 기능을 프로젝트에 도입하려 합니다.



1. 변경 사항을 어떻게 관리할 수 있을까? 🤔

1.1 기존의 데이터를 UserLog에 직접 저장하기

사실 처음 떠올린 방법은 UserLog 테이블을 새로 생성하는 방법이였습니다.
예를 들어 변경 작업이 발생하여 기존/변경 으로 데이터가 분류될 때 기존의 데이터들을 UserLog 테이블에 저장하는 방식입니다.

하지만 저는 이 방법이 조금은 불편하게 느껴졌는데요.
그 이유는 다음과 같습니다.

  • 변경 작업이 일어나는 지점을 찾고 해당 부분에 대한 UserLog에 기존 데이터를 직접 저장해 주어야 한다.
  • 코드 생성의 비용이 변경 작업과 비례하여 늘어난다.
  • 이러한 방식은 코드를 작성하는 입장에서도 꽤 부담으로 느껴질 수 있다.

또한 해당 작업은 지속적으로 다뤄질 수 있는 부분이라고 생각되어
어려움없이 수월하게 이루어져야 한다고 생각했습니다.

1.2 Envers 활용한 변경 내용 자동 추적

다행히 어렵지 않게 이를 해결할 수 있는 방법을 찾았는데요.
바로 Envers 였습니다.
Envershibernate 에서 만든 데이터에 대한 변경 이력(audit)을 자동으로 관리해주는 라이브러리입니다.
이를 사용하면 엔티티의 모든 변경 사항(삽입, 수정, 삭제)에 대한 리비전 정보를 자동으로 추적하고 저장할 수 있었습니다.



2. Envers 를 통한 변경 사항 관리 및 추적 구현하기

2-1. 기본 설정하기


gradle 의존성 주입

 implementation 'org.springframework.data:spring-data-envers:2.7.2'

의존성 주입 이후 Envers 를 통해 변경 사항을 관리할 테이블을 지정합니다.

UserEntity

@Entity
@Audited
@Table(name = "\"user\"")
...
public class UserEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
     ...(생략)
	
    @NotAudited
    @OneToMany(mappedBy = "followingUser", fetch = FetchType.LAZY)
    private List<FollowEntity> followingList = new ArrayList<>();

(관련이 적은 부분들은 생략했습니다.)

  • @Audited 어노테이션을 통해 Envers의 관리를 받는 엔티티를 지정할 수 있습니다.
  • @NotAudited 를 통해서 자동 변경 감지에서 제외시킬 수 있습니다.

@EnableJpaRepositories

@EnableJpaRepositories(repositoryFactoryBeanClass = EnversRevisionRepositoryFactoryBean.class)
public class YourConnectionApplication {

    public static void main(String[] args) {
        SpringApplication.run(YourConnectionApplication.class, args);
    }
}

Hibernate Envers와 같은 추가적인 기능을 사용하려면 해당 기능에 맞는 팩토리 빈 클래스를 지정해야 합니다.
그래서 위와 같이 @EnavleJpaRepositories(...)를 추가해 주었습니다.

만약 애플리케이션이 여러 개의 DB와 동시에 연결해야 한다면 (즉, 다중 데이터 소스 환경인 경우), 그때는 각각의 EntityManagerTransactionManager 설정을 지정해 주어야 합니다.

2-2. Envers를 통한 변경 내역 관리 및 추적하기

위와 같이 설정했다면 다음과 같은 필드가 추가된 것을 확인할 수 있습니다.

UserAud

Field

  • 해당 필드에서 추가된 rev 값은 revision의 약자로서 변경된 명세에 대한 고유값입니다.
  • revtype의 숫자값은 다음과 같습니다.( 0 = 삽입, 1 = 수정, 2 = 삭제)

이제 방금전 설정해두었던 UserEntity에 변경 사항을 만들고 실제로 그 내용들이 관리되고 있는지 확인해 보겠습니다.

UserEntity

이로서 User에 관한 변경 내역을 관리하고 추적할 수 있게 된 것을 확인할 수 있습니다.

2-3. Custom Revision 생성하기

변경 사항에 대한 Envers 테이블을 커스텀하게 설정할 수도 있습니다.

CustomRevisionEntity

@Entity
@RevisionEntity
@Table(name = "custom_revision")
public class CustomRevisionEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @RevisionNumber
    private Long rev;

    @RevisionTimestamp
    private Date createdAt;

}

기본적으로 update 되는 내역에 대한 데이터를 모두 담고 있기 때문에 컬럼의 수가 방대해질 수 있고 그렇기에 기본적인 식별값을 Long 타입으로 설정해 두었습니다.
(필요에 따라서 필드값을 추가할 수 있습니다.)



3. 변경 내역을 통한 서비스 퀄리티 높이기😃

기본적인 설정들이 모두 끝났고 드디어 제가 구현하고자 했던 기능을 구현해보려고 합니다.

제가 구현하고자 했던 기능은 잠금된 계정이 인증을 통해서 해제되었을때 UserActivity값을 변경 내역을 확인하여 원복시키는 기능이였습니다.

@Getter
@AllArgsConstructor
public enum UserActivity {

    NORMAL("일반 유저"),
    FLAGGED("신고받은 유저"),
    LOCKED("일시정지 "),
    BAN("이용 제한 유저");

    private final String description;
}

하지만 기존에 변경 내역을 알 수 있는 방법이 없었고 다음과 같은 문제를 안고 있었습니다.

계정 인증 코드 일부분

if (userEntity.getUserActivity() == UserActivity.LOCKED) {
            userEntity.changeActivity(UserActivity.NORMAL);
        }

어떠한 맥락과 관계없이 UserActivity.LOCKED이라면 모든 UserActivity 값은 NORMAL 상태가 됩니다.

이제 해당 코드를 개선해 보겠습니다.

3.1 UserRevisionRepository 생성하기

우선적으로 구현해야 할 기능은 다음과 같은 요구 사항을 만족시켜야 합니다.

  1. User 를 조회한다.
  2. Locked 를 제외한 User의 activity 값을 Envers를 통해 조회한다.
  3. 해당 activity 값에서 가장 최신의 값을 조회한다.

해당 요구사항은 조건부정렬이 결합되어있어 저는 쿼리를 통해 해당 문제를 해결하려 했습니다.

하지만 Spring Data JPARevisionRepository 인터페이스는 리비전 엔티티에 대한 커스텀 쿼리 메서드를 지원하지 않습니다.

따라서 이런 경우에는 Hibernate EnversAuditReader를 직접 사용해야합니다.
( QueryDSL 사용하여 쿼리를 작성할 수도 있습니다.)

이렇게 해서 만들어진 Repository는 다음과 같습니다.

UserRevisionRepository

@Repository
@RequiredArgsConstructor
public class UserRevisionRepository {

    private final EntityManager entityManager;

    public UserActivity findPreviousActivityByUserId(Long userId) {
        AuditReader auditReader = AuditReaderFactory.get(entityManager);

        UserEntity userEntity = ClassUtil.castingInstance(
            auditReader.createQuery()
                .forRevisionsOfEntity(
                    UserEntity.class, true, true)
                .add(AuditEntity.id().eq(userId))
                .add(AuditEntity.property("userActivity").ne(UserActivity.LOCKED))
                .addOrder(AuditEntity.revisionNumber().desc())
                .setMaxResults(1)   // 가장 최근의 값을 가져오기 위함
                .getSingleResult(),
            UserEntity.class
        );

        return userEntity.getUserActivity();
    }
}
  • EntityManager를 통해 AuditReader에 직접 접근하였습니다.

  • 쿼리 설정 부분에서는 UserEntity.class에 대한 리비전 정보 중 id가 입력 파라미터인 userId와 일치하면서 userActivity 프로퍼티가 LOCKED 상태가 아닌 것을 찾도록 조건을 설정하고 있습니다.

  • 그 후 결과를 내림차순으로 정렬하고 가장 최근 값을 가져오도록 설정합니다.

  • 마지막으로 해당 결과에서 userActivity 프로퍼티 값을 가져와 반환합니다.


3.2 변경 내역을 통한 코드 개선하기

이제 UserRevisionRepository를 통해 문제가 되었던 코드를 다음과 같이 수정했습니다.


인증 부분 개선

 if (userEntity.getUserActivity() == UserActivity.LOCKED) {
            UserActivity previousActivity = userRevisionRepository.findPreviousActivityByUserId(
                userEntity.getId());
            userEntity.changeActivity(previousActivity);
        }

Envers 를 통해 변경 내역을 저장하고 이를 활용하여 기존의 서비스를 더 퀄리티있게 개선시키는 작업을 해보았습니다.

이로서 문제가 되었던 부분을 해결하였습니다.


끝마치며

더 좋은 방법들이 있다면 같이 언제나 알려주세요 :)
프로젝트에 전체 코드는 [프로젝트 깃 허브 링크]에서 확인하실 수 있습니다.

profile
보안/응용 소프트웨어 개발자

0개의 댓글