기존 설계했던 데이터베이스 스키마를 가지고 간단히 다시 설계를 해보는 시간을 가졌다.
이유는 객체지향프로그래밍을 연습하고 있는데 객체의 책임을 나누고 연관관계에 있는 객체들을 테이블로 구성해보기 위함이였다.
설계했던 테이블들은 로그인과 회원가입, 인증에 필요한 데이터들이였고 처음에 다음과 같이 설계했다.
사용자와 인증은 일대일 관계로 같이 생성이 되고 같이 삭제가 되는 로직이였고, 어느 테이블에 외래키가 들어가야하나 고민을 많이 하지 않고 설계했던 것 같다.
그래서 사용자 객체에 관련된 모든 로직들이 있었고, 이를 각각의 로직들을 담당하는 객체들로 분리해보고 싶었다.
다시 짜보면서 일대일테이블 관계에서 어느 테이블에 외래키가 들어가야하는지 많이 찾아보았는데, 명확한 답이 정해져있지는 않지만 종속 유형에 해당하는 테이블에 외래키가 존재하도록 고려해볼만하다고 한다.
종속 유형이라하면 주 테이블이 아닌 대상 테이블로 독립적으로 존재할 수 없는 테이블인데, 위의 사용자와 인증 테이블을 놓고 보면 인증 테이블은 사용자 테이블 없이 존재할 수 없으므로 인증 테이블에 외래키가 존재해야 한다고 생각했다.
그럼 하나씩 분리해보자.
기존 사용자 객체가 가지고 있던 비밀번호 변경, 유효성 검사, 암호화, 일치 여부의 로직을 비밀번호 객체가 수행하도록 한다.
테이블에는 변경된 날짜와 암호화된 값을 기록한다.
분리된 비밀번호 객체는 다음과 같은 메소드들을 가지게 되었다.
static async create(password: string): Promise<Password> {
this.validate(password);
return new Password(await this.encrypt(password));
}
/* 비밀번호 변경 */
public async changePassword(old: string, toChange: string, updatedAt: Date): Promise<boolean> {
if ( !await this.comparePassword(old) )
throw new UnauthorizedException('Something Error Id Or Password')
if (this.equals(old, toChange))
throw new ConflictException('Duplicated Password');
await this.changeSucceeded(toChange, updatedAt);
return true
}
/* 비밀번호 유효성 검사 */
private static validate(plainTextPassword: string) {
const passwordCheckReg = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d]{10,}$/;
if (!passwordCheckReg.test(plainTextPassword))
throw new BadRequestException('Password Validate Failed');
}
/* 비밀번호 암호화 */
private static async encrypt(plainTextPassword: string): Promise<string> {
return bcrypt.hashSync(plainTextPassword, 5);
}
/* 암호화 되지 않은 비밀번호와 비교 */
public async comparePassword(plainTextPassword: string): Promise<boolean> {
return await bcrypt.compare(plainTextPassword, this.value)
}
/* 비밀번호 변경 완료 */
private async changeSucceeded(newPassword: string, updatedAt: Date) {
this.value = (await Password.create(newPassword)).getValue();
this.updatedAt = updatedAt;
}
/* 기존 사용하던 비밀번호랑 같은지 */
private equals(old: string, toChange: string): boolean { return old === toChange; };
로그인을 시도할 때 항상 로그인 횟수를 먼저 검사하고, 차단된 유저인지, 차단된 시간으로부터 일정시간이 지났는지를 검사한다.
따라서 테이블에는 차단된 날짜와 시도 횟수를 기록하였다.
로그인 정보 테이블은 다음과 같은 메소드들을 가지게 되었다.
check(now: Date): void {
/* 로그인시도 횟수가 5번이면 Block Time 체크 */
if (this.loginTry == 5)
this.blockTimeCheck(now);
}
initBlockInfo(): void { this.loginTry = 0; };
/* 로그인에 실패하였을 때 로직 */
handleFailed(now: Date): void {
this.addFailedCount();
this.tryCountCheck(now);
};
/* 로그인 횟수가 5번까지 허용되므로 그 이후로는 증가x */
addFailedCount() {
if( this.loginTry != 5 )
this.loginTry += 1;
}
/* 차단된 상태인지 여부 */
toBeBlocked(): boolean { return this.loginTry == 5; };
/* 5번째 시도해서 틀린 사람은 시도한 시간을 차단 시간으로 설정 */
private tryCountCheck(now: Date):void {
if( this.loginTry == 5 )
this.blockedAt = now;
}
/* 차단된 시간으로부터 일정 시간 이후에만 로그인 가능해짐 */
private blockTimeCheck(now: Date): void {
if( !this.blockedAt ) return;
/* 5분 경과 안했으면 Throw Error */
if ( Math.floor( (now.getTime() - this.blockedAt.getTime()) / 1000 ) < 300 )
throw new ForbiddenException(ErrorMessage.LOGIN_COUNT_EXCEED)
}
로그인 성공, 비밀번호 변경, 회원가입 시 항상 새로운 AccessToken을 발급해주어야 하고, 회원가입을 제외하고는 기존에 발급되었던 RefreshToken의 만료 여부를 판단한다.
따라서 refreshToken과 refreshToken의 갱신을 위한 갱신 날짜를 기록해 두었다.
인증 객체는 다음과 같은 메소드를 가지게 되었다.
/* refreshToken 발급된지 1주일이 지났는지 */
isNeededUpdate(now: Date): boolean {
return Math.floor(
((now.getTime() - this.refreshedAt.getTime()) / AUTHENTICATION_OPTION.EXPIRED_TIME)
) > AUTHENTICATION_OPTION.REFRESH_CYCLE ? true : false;
}
/* AccessToken */
getClientAuthentication(): string { return this.clientAuthentication; };
updateClientAuthentication(clientAuthentication: string) { this.clientAuthentication = clientAuthentication; };
/* refreshToken을 갱신했으면 갱신 날짜 업데이트 */
updateRefreshAuthentication(refreshAuthentication: string, refreshedAt: Date) {
this.refreshAuthentication = refreshAuthentication;
this.refreshedAt = refreshedAt;
};
static createWith ( clientAuthentication: string, refreshAuthentication: string ) {
return new UserAuthentication(clientAuthentication, refreshAuthentication, new Date())
}
user 객체를 통해 참조하고 있는 객체들을 타고 가면서 알 수 있어 단방향 매핑으로 설정할 수 있지만, 외래키를 대상 테이블에 두기 위하여 양방향으로 매핑만 해주었다.
기존에 User객체에 있던 로직들을 분리하는 작업이라 조금은 수월했지만..
초반부터 객체를 구성할 때 각 객체는 어떤 로직을 가지고 있을지, 어떤 객체들을 직접 참조해야 하는지 잘 생각해보자.
Microsoft - One-to-one relationships
TypeORM - One-to-one relations