BCrypt strength 분리로 게스트 API 응답시간 55% 개선하기

조용현·2026년 3월 18일

문제 해결

목록 보기
3/15
post-thumbnail

배경

사이드 프로젝트로 커뮤니티 서비스를 개발하고 있습니다. AWS EC2 t2.micro에 Docker로 배포한 뒤 JMeter로 일반 사용자 시나리오 부하 테스트를 돌렸는데 이상한 점을 발견했습니다.

회원과 게스트가 같은 작업을 하는데 응답시간이 14배 차이가 났습니다.

회원 댓글 작성:  평균  88ms
게스트 댓글 작성: 평균 1,248ms  ← 왜?

문제 원인

CPU를 잡아먹는 게스트 요청

Grafana를 보니 피크 시 CPU 사용률이 100%까지 치솟고 있었습니다. 게스트 요청이 CPU를 과도하게 소비하고 있었습니다.

poppingv4_grafana1

파고드니 BCrypt가 문제였습니다.

BCrypt는 의도적으로 느리다

BCrypt는 비밀번호 해싱 알고리즘으로, strength(cost factor) 값에 따라 연산 비용이 기하급수적으로 증가합니다. 해커가 탈취한 해시를 brute force로 공격할 때 시간이 오래 걸리도록 의도적으로 느리게 설계된 겁니다.

strength소요시간
861ms
10 (기본값)103ms

EC2 t2.micro (vCPU 1, RAM 1GB) 환경에서 jshell로 직접 측정. 각 strength당 10회 평균값.

Spring Security의 BCryptPasswordEncoder()기본 strength가 10입니다.

모든 비밀번호 연산에 BCrypt(10)을 사용하고 있었다

SecurityConfig에 빈이 하나뿐이었습니다.

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(); // strength=10, 모든 곳에 사용
}

회원 로그인, 회원 가입, 게스트 게시글, 게스트 댓글 — 모든 비밀번호 연산이 동일한 BCrypt(10)을 사용하고 있었습니다.

// PostService.java
public Long createGuestPost(String slug, GuestPostCreateRequest dto) {
    // 매 게스트 게시글 작성마다 BCrypt(10) 실행 → 103ms
    String encodedPassword = passwordEncoder.encode(dto.guestPassword());
    ...
}

// CommentService.java
public Long createGuestComment(Long postId, GuestCommentCreateRequest dto, Long parentId) {
    // 매 게스트 댓글 작성마다 BCrypt(10) 실행 → 103ms
    String hashedPassword = passwordEncoder.encode(dto.guestPassword());
    ...
}

t2.micro는 vCPU가 1개입니다. BCrypt(10) 요청이 몰릴수록 큐에 쌓여 대기시간이 누적됐고, 부하 테스트에서 Grafana CPU 100%, 게스트 댓글 작성 평균 응답시간 1,248ms가 나온 이유가 바로 이겁니다.


해결 과정

게스트 비밀번호에 strength=10이 필요한가?

회원 비밀번호는 계정 자체를 보호합니다. DB가 털렸을 때 비밀번호가 뚫리면 해당 유저의 모든 개인정보, 다른 서비스의 동일 비밀번호까지 위험해집니다. strength=10 이상이 맞습니다.

게스트 비밀번호는 익명 게시글/댓글의 수정·삭제만 보호합니다. 탈취되더라도 피해 범위가 해당 게시글 하나입니다. strength=8로도 충분합니다.

보안 수준이 다른 대상에 동일한 비용을 쓰고 있었던 겁니다.

PasswordEncoder 빈 분리

// SecurityConfig.java
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder(); // 회원용: strength=10 유지
}

@Bean
public PasswordEncoder guestPasswordEncoder() {
    return new BCryptPasswordEncoder(8); // 게스트용: strength=8
}

Spring 자동 주입 활용

같은 타입(PasswordEncoder)의 빈이 두 개가 됐는데, @Qualifier 없이도 필드명이 빈 이름과 일치하면 Spring이 자동으로 매칭해줍니다.

// PostService.java
@RequiredArgsConstructor
public class PostService {
    private final PasswordEncoder guestPasswordEncoder; // → BCrypt(8) 자동 주입
}

// CommentService.java
@RequiredArgsConstructor
public class CommentService {
    private final PasswordEncoder guestPasswordEncoder; // → BCrypt(8) 자동 주입
}

게스트 작성/수정/삭제 로직에서 guestPasswordEncoder를 쓰도록 바꿨고, 회원 로그인/가입에는 기존 passwordEncoder(BCrypt 10)를 그대로 유지했습니다.

// PostService.java - 변경 후
public Long createGuestPost(String slug, GuestPostCreateRequest dto) {
    // BCrypt(10) → BCrypt(8): 103ms → 61ms
    String encodedPassword = guestPasswordEncoder.encode(dto.guestPassword());
    ...
}

public boolean verifyGuestPassword(Long postId, String rawPassword) {
    return guestPasswordEncoder.matches(rawPassword, post.getGuestPasswordHash());
}

// CommentService.java - 변경 후
public Long createGuestComment(Long postId, GuestCommentCreateRequest dto, Long parentId) {
    String hashedPassword = guestPasswordEncoder.encode(dto.guestPassword());
    ...
}

테스트 시나리오

테스트 환경

  • EC2 t2.micro (1 vCPU, 1GB RAM), Docker 환경
  • MySQL 8, Spring Boot, HikariCP(pool size: 30)
  • JMeter + Prometheus + Grafana 모니터링

시나리오 구성

Thread Group역할스레드 수비율
TG1게시글 상세 조회 (비로그인)5050%
TG2회원 로그인 + 게시글/댓글 작성2020%
TG3게스트 게시글/댓글 작성2020%
TG4좋아요1010%

읽기 50% / 쓰기 50% 비율, 총 100개 스레드로 구성했습니다.


결과

전체 지표

지표변경 전변경 후개선율
전체 평균 응답시간322ms156ms-51.5%
P951,961ms724ms-63.1%
P993,014ms1,452ms-51.8%
Throughput35.1 RPS38.1 RPS+8.5%
에러율0%0%유지

엔드포인트별 주요 개선

엔드포인트변경 전변경 후개선율
POST /boards/{slug}/guest1,355ms600ms-55.7%
POST /boards/{slug}/{postId}/comments/guest1,248ms540ms-56.8%
GET /boards/{slug}/{postId} P991,985ms211ms-89.4%

게스트 작업 응답시간이 절반 이하로 줄었습니다. CPU 부하가 줄어들면서 GET 요청의 꼬리 지연(tail latency)도 함께 개선됐습니다.

poppingv5_grafana1


트레이드오프

strength를 낮추면 해시 강도가 달라지는 건 사실입니다. 다만 게스트 비밀번호가 보호하는 범위(게시글 하나의 수정/삭제 권한)와 회원 비밀번호가 보호하는 범위(계정 전체)의 차이를 생각하면 합리적인 트레이드오프라고 판단했습니다. 서버 성능에 여유가 생기면 strength를 다시 올릴 수 있습니다.

BCrypt strength는 "높을수록 좋다"가 아니라 보호하는 대상의 가치에 맞는 비용을 설정해야 한다는 게 이번 개선의 핵심이었습니다.


참고

profile
백엔드 개발자

0개의 댓글