forked from boostcampwm-2024/web17-juchumjuchum
-
Notifications
You must be signed in to change notification settings - Fork 1
Guest 로그인 중복 문제 해결
KWAKMANBO edited this page Jan 22, 2025
·
1 revision
- 로컬 환경에서 Guest로그인을 사용하다 문제가 발생했다. 처음에는 잘되던 게스트로 로그인 버튼이 나중가서는 먹통이 되는 문제였다.
- 게스트로 로그인 버튼을 클릭하면 서버에서는 임의의 유저를 생성해주는데 생성해주는 정보가 중복되서 생기는 문제 같았다.
- 게스트로 로그인 기능이 제대로 작동하지 않는 이유를 분석해보기로했다.
- 게스트로 로그인버튼을 클릭하게되면 frontend 서버는 backend 서버의
/api/auth/tester/login
로 요청을 보내게 된다. - 요청이 들어오면 TestAuthGuard가 실행된다.
- TestAuthGuard는 TesterStrategy를 사용하며 해당 Statategy의 validate메소드를 사용함
- validate메소드에서는 testerAuthService의 attemptAuthentication메소드를 실행한다.
- attemptAuthentication메소드는 registerTester라는 메소드를 실행
- 위와 같은 로직 순서로 실행되는데 이중 registerTester메소드만 확인하면 된다. 나머지는 registerTester라는 메소드를 실행하기위해 거쳐가는 중간 단계일뿐 다른 로직을 실행하는 부분은 없다.
async registerTester() {
return this.register({
nickname: this.generateRandomNickname(),
email: 'tester@nav',
type: OauthType.LOCAL,
oauthId: String((await this.getMaxOauthId(OauthType.LOCAL)) + 1),
});
}
-
nickname: this.generateRandomNickname()
: this.generateRandomNickname() 랜돔으로 닉네임을 생성해주는 함수를 사용해 nickname을 생성하고, 생성한 nickname을 nickname에 저장 -
email: 'tester@nav'
: 모든 게스트들은 email을tester@nav
로고정 - type : Oauth의 타입을 지정하는 부분으로 google인지local인지를 의미한다.
- oauthId : DB에 저장된 Oauth_ID 값중 가장 큰값을 가져와 +1을 해서 게스트 계정을 생성한다.
- register 함수의 내용도 한번 알아보자
async register({ nickname, email, type, oauthId }: RegisterRequest) {
return await this.dataSource.transaction(async (manager) => {
await this.validateUserExists(type, oauthId, manager);
const subName = await this.createSubName(nickname);
return await manager.save(User, {
nickname,
email,
type,
oauthId,
subName,
});
});
}
- DB에 유저를 생성하는 메소드 register의 내용이다.
-
await this.validateUserExists(type, oauthId, manager);
이 함수는 주어진 매개변수를 이용해 User테이블에 해당 값을 가진 user가 있는지 확인하고 없다면 유저를 생성하고 있다면 유저를 생성하지 못하게하는 함수이다. - 문제는 이 validateUserExists에서 발생한다. 이제부터 무슨 문제인지 알아보자
private async validateUserExists(
type: OauthType,
oauthId: string,
manager: EntityManager,
) {
if (await manager.exists(User, { where: { oauthId, type } })) {
throw new BadRequestException('user already exists');
}
}
- 문제 상황이였던 'user already exists'가 여기서 등장하는 것으로보아
oauthId
와type
이 User 테이블 속에서 중복되서 발생하는 문제인것 같다. - DB를 확인해보니 10개 까지는 계정이 잘만들어지는데 그 이후부터는 위처럼 에러가 발생하는 것을 알 수 있었다.
- 그렇다면 OauthID는 어디서부터 오는것일까?찾아보니
registerTester()
의oauthId: String((await this.getMaxOauthId(OauthType.LOCAL)) + 1)
부분에서 오는 것을 알 수 있었고await this.getMaxOauthId(OauthType.LOCAL))
함수도 확인해보기로 했다.
private async getMaxOauthId(oauthType: OauthType) {
const result = await this.dataSource.manager
.createQueryBuilder(User, 'user')
.select('MAX(user.oauthId)', 'max')
.where('user.type = :oauthType', { oauthType })
.getRawOne();
console.log(result.max);
return result ? Number(result.max) : 1;
}
- 위와 같은 쿼리를 이용해 user테이블에서 OauthId가 가장 큰값을 가져오는것을 알 수 있었다.
- 위와같은 에러가 뜨고, 분명히 우리는 OauthID가 10까지 있는것을 확인했는데 이 getMaxOauthId함수는 max값으로 10이아닌 9를 가져와서 생기는 문제임을 알 수 있었다.
- 이제 문제가 일어나는 이유는 알았다. 그럼 문제의 근본적인 원인이 무엇인지 알아보보자
- 문제의 근본적인 원인은 User 테이블의 Oauth_Id의 타입 문제였다.
- users테이블의 DDL을 보며 근본적인 원인을 알아보자
-- auto-generated definition
create table users
(
id int auto_increment
primary key,
nickname varchar(50) not null,
subName varchar(10) default '0001' not null,
email varchar(50) not null,
role varchar(5) default 'USER' not null,
type varchar(10) default 'local' not null,
is_light tinyint default 1 not null,
created_at timestamp(6) default CURRENT_TIMESTAMP(6) not null,
updated_at timestamp(6) default CURRENT_TIMESTAMP(6) not null on update CURRENT_TIMESTAMP(6),
oauth_id varchar(255) not null,
constraint nickname_sub_name
unique (nickname, subName),
constraint type_oauth_id
unique (type, oauth_id)
);
- 위 users테이블의 DDL을 보면 oauth_id가 varchar(255)로 선언되어있는 것을 확인할 수 있는데 위 getMaxOauthId메소드는 숫자를 비교하는것을 염두에 뒀지만, 실제로는 문자열이 비교되서 생기는 문제였다.
- 그렇기 때문에 게스트 유저가 10개가 되고나서부터는 중복 되는 문제가 생긴거였는데, 그 이유는 users테이블 oauth_id에서 숫자로치면 10이 제일 큰값이지만, 문자열로치면 '9' 가 '10'보다 크기 때문에 9를 max값으로 반환하기 때문이였다.
- 정리하면 oauth_id가 문자열이기 떄문에 숫자비교가 아닌 문자열로 비교되서 생기는 문제였다.
- 이제는 문제를 해결해보자
- 문제를 해결하는 방식은 2가지가 있다.
- users 테이블 DDL에서 oauth_id의 타입을 varchar 에서 int 로변경하는 방법이 있다.
- 이방법은 근본적인 원인을 바로 해결할 수 있지만, 타입을 바꾸면 다른 메소드에도 영향이 갈 수 있어 또다른 수정이 생길 수 있다는 단점이 있다.
- DB에서 oauth_id를 불러올떄 타입 변환을 해서 비교하는 방법이다
- 이방법은 불러올때 oauth_id를 직접 타입 변환을 해야하기 떄문에 오버플로우가 발생한다. 하지만 해당 getMaxOauthId메소드 내에서만 변경되기 떄문에 1번의 또다른 수정 발생을 막을 수 있다는 장점이 있다.
- 리팩토링 기간이 얼마 남지 않아 나는 2번 방법을 선택했다.
private async getMaxOauthId(oauthType: OauthType) {
const result = await this.dataSource.manager
.createQueryBuilder(User, 'user')
.select('MAX(CAST(user.oauthId AS SIGNED))', 'max')
.where('user.type = :oauthType', { oauthType })
.getRawOne();
console.log(result.max);
return result ? Number(result.max) : 1;
}
-
CAST(user.oauthId AS SIGNED)
를 통해서 oauthId를 숫자로 변환해서 비교하는 방식으로 변경했다. - oauthId가 숫자로 변경되서 비교되기 때문에 정상적으로 users테이블의 oauth_id에서 제일 큰값을 가져오는 것을 아래 이미지에서 확인할 수 있다.
- 로그인도 정상적으로 되는것을 확인할 수 있다.
- [1주 2일차 합동 개발 일지](marketCap 데이터 null 이슈 해결)
- 인터셉터를 이용한 로거 개발기
- 배포 환경에서 웹 소캣 연결 실패 문제 해결
- Github Actions를 이용한 CI CD 구축
- nGrinder 테스트 시나리오
- nGrinder TPS가 측정되지 않는 문제
- 메트릭 수집에 필요한 툴들 설치하기
- Node Exporter 연결 안되는 문제
- StockService에서 Repository 계층 분리하기
- Server와 Grafana연동하기
- Guest 로그인 중복 문제 해결