validation -> insert V.S. insert -> catch

최근 다른 팀원이 기능을 구현한 코드를 보면서 기존에 내가 하던 방식과 차이가 있어,
팀원들과 이에 대해 같이 얘기를 나누면서 알아본 것들과 나의 생각을 정리해본다.


상황

다음과 같은 사용자 Entity가 있고
사용자의 화면에 보이는 이름(displayName)과 계좌(account)는 다른 사용자와 중복되지 않는 unique한 값이어야한다
라는 도메인 규칙이 있다고 하자.

그러면 TypeORM을 사용할 경우 아래와 같이 User Entity를 만들 수 있을 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Entity()
export class User extends BaseEntity {
@PrimaryGeneratedColumn()
id: number;

@Column({ unique: true })
displayName: string;

@Column({ unique: true })
account: string;

constructor(displayName: string, account: string) {
this.displayName = displayName;
this.account = account;
}
}

요청을 통해서 새로운 사용자를 저장할 때 앞서 정한 도메인 규칙을 지키기 위해 나의 경우는
아래와 같이 먼저 DB에서 요청한 displayNameaccount와 같은 값을 가진 유저를 읽어온 뒤에 그러한 유저가 있다면 오류 응답 처리를 주고 아닌 경우 DB에 값을 넣는 식으로 코드를 짠다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export class UserSerivce {
constructor(
private readonly userRepository: UserRepository
) {}

async saveUser(displayName: string, account: string): Promise<User> {
const user = await this.userRepository.findByDisplayNameOrAccount(displayName, account);

if (!_.isNil(user)) {
// Conflict(이미 존재함) 오류 응답...
}

try {
return await this.userRepository.save(new User(displayName, account));
} catch {
// Service Unavailable(DB 오류) 오류 응답...
}
}
}

하지만 팀원의 코드는 아래와 같이 미리 validation을 하지 않고 try catch에서 어떤 error인지를 보고 응답을 하는 방식으로 코드를 짰다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export class UserSerivce {
constructor(
private readonly userRepository: UserRepository
) {}

async saveUser(displayName: string, account: string): Promise<User> {
try {
return await this.userRepository.save(new User(displayName, account));
} catch (err) {
// error 상세 정보 확인 후 오류 응답...
if (err.type === UniqueError) {
// Conflict(이미 존재함) 오류 응답...
}

// 기타 오류 처리...
}
}
}

개인적으로 이런 식으로 처리해본 적이 없었고, 이런 코드를 생각조차 해본 적이 없었다.

둘 다 기능 자체는 동일하다.
다만 보다 합리적인 코드를 사용하는 것이 좋은 애플리케이션 개발에 있어 필요하기 때문에
다른 개발자들은 이에 대해 어떤 생각을 하는지 구글링을 해봤다.

찾아본 결과

위와 같은 경우에 대해서 다수의 답변은 DB에서 처리하라였다.
왜냐하면

  1. DB에서 unique 값에 대한 처리를 효율적으로 해주고 있다.
    DB에서 insert 시에 unique 값에 대한 확인을 이미 하기 때문에
    validation 작업은 중복 작업을 하는 것이다.

  2. Race Condition
    다음과 같은 경우를 생각해보자.

같은 값을 가진 2개의 요청이 들어와 2개의 Thread 1Thread 2가 validation & insert를 수행한다 했을 때
다음과 같은 타임라인으로 실행이 된다면 Thread 2는 validation이 통과되지만 insert 시에 unique constraint로 인해 DB Error를 던질 것이다.

그렇기 때문에 insert 이전에 validation 로직이 들어가 있더라도 이러한 Race Condition으로 인해 무용지물이 될 수 있다.

크게 위와 같은 2가지의 이유로 unique value에 대한 체크는 DB에서 한다라는것이 주된 의견이었다.

나의 생각

여러 개발자들의 의견에도 불구하고 나는 먼저 validation을 한 후에 insert가 이뤄져야한다고 생각한다.
왜냐면 그 편이 코드에 plain 하게 도메인 규칙을 잘 드러낸다고 생각하기 때문이다.

물론 validation을 하기 위해 추가 네트워크 통신이 발생한다라는 단점과 Race Condition에 대한 문제를 인지한다 하더라도,
명확한 객체 모델링과 동작을 한 눈에 파악할 수 있는 메서드를 통해 쉽게 도메인 규칙을 파악할 수 있게 하는 것이 중요하다고 생각한다.

또 constraint를 위반한 경우 던지는 error도 DB마다 다를 것이고,
DB를 변경할 경우 repository 인스턴스만 갈아끼우면 되기 때문에 변경에도 열려져 있는 코드가될 것이라 생각된다.


참고:

Share