[데브코스] Spring Boot 인증·인가(Auth) (19강) - username 중복 체크 로직을 Controller → Service로 이동한 이유

zuno·2026년 1월 12일

회원가입 기능을 구현하면서
username 중복 체크 로직을 컨트롤러에서 서비스로 이동하는 리팩토링을 진행했다.

겉으로 보면 단순한 위치 이동처럼 보이지만,
실제로는 에러 처리 품질, 책임 분리, 프론트엔드 협업과 직접적으로 연결되는 중요한 변경이다.


🔴 기존 구조 (18강까지)

Controller에서 중복 체크

memberService.findByUsername(reqBody.username())
    .ifPresent(_member -> {
        throw new ServiceException("409-1", "이미 존재하는 아이디입니다.");
    });

Member member = memberService.join(
    reqBody.username(),
    reqBody.password(),
    reqBody.nickname()
);

Service에서는 단순 저장

public Member join(String username, String password, String nickname) {
    Member member = new Member(username, password, nickname);
    return memberRepository.save(member);
}

그리고 DB에는 이미 다음 설정이 존재한다.

@Column(unique = true)
private String username;

❓ 의문점

이미 DB에 unique 제약이 걸려 있는데
굳이 코드에서 username 중복 체크를 또 해야 하나?

이론적으로는 DB 유니크 제약만으로도 중복은 막을 수 있다.


❌ DB 유니크 제약에만 의존했을 때 발생한 문제

중복된 username으로 회원가입 요청을 보내면 다음과 같은 응답이 나온다.

{
  "status": 500,
  "error": "Internal Server Error",
  "message": "could not execute statement [Unique index or primary key violation...]"
}

문제점 정리

1️⃣ HTTP 상태 코드가 500 (서버 오류)
→ 사실은 클라이언트 요청 문제인데 서버가 잘못한 것처럼 보인다.

2️⃣ 메시지가 너무 기술적
→ 프론트엔드 개발자가 처리하기 어렵고
→ 사용자에게 그대로 보여주기엔 최악이다.

3️⃣ 우리 프로젝트의 응답 규칙(RsData)이 깨진다
resultCode, msg, data 형태가 유지되지 않는다.


✅ 그래서 선택한 방향

DB 유니크 제약은 “최후의 안전장치”로 유지하고
비즈니스 에러는 Service 레벨에서 명확하게 처리한다


🔁 19강 리팩토링 내용

변경 사항

  • username 중복 체크 로직을 Controller → Service로 이동

Service에서 직접 중복 검증

public Member join(String username, String password, String nickname) {
    memberRepository.findByUsername(username)
        .ifPresent(_member -> {
            throw new ServiceException("409-1", "이미 존재하는 아이디입니다.");
        });

    Member member = new Member(username, password, nickname);
    return memberRepository.save(member);
}

Controller는 이제 요청 전달 역할만 수행한다.


🎯 이 구조가 더 좋은 이유

1️⃣ 책임 분리

  • Controller

    • 요청/응답 처리
    • HTTP 관점의 역할
  • Service

    • 비즈니스 규칙
    • “회원가입이 가능한가?”
    • “이미 존재하는 아이디인가?”

👉 username 중복 여부는 명백한 비즈니스 규칙이므로 Service가 책임지는 것이 맞다.


2️⃣ 프론트엔드에게 명확한 에러 제공

{
  "resultCode": "409-1",
  "msg": "이미 존재하는 아이디입니다.",
  "data": null
}
  • HTTP 상태 코드: 409 Conflict
  • 프론트엔드에서 분기 처리 가능
  • 사용자에게 그대로 보여줘도 문제 없음

👉 DB 에러보다 훨씬 친절하고 예측 가능하다.


3️⃣ DB 유니크 제약은 여전히 필요하다

  • 동시 요청
  • 레이스 컨디션
  • 실수로 검증 로직이 빠진 경우

👉 이런 상황을 막는 최후의 방어선이 DB 유니크 제약이다.

즉,

  • Service: 사람이 이해할 수 있는 에러 처리
  • DB: 절대 깨지면 안 되는 무결성 보장

🤔 DB 에러만 써도 되지 않나?

가능은 하지만 실무에서는 거의 쓰지 않는다.

이유는 단순하다.

프론트엔드와 사용자에게 너무 불친절하기 때문

그래서:

  • DataIntegrityViolationException 그대로 노출 → 안 좋은 설계
  • ServiceException으로 의미 있는 에러 전달 → 좋은 설계

📌 정리

  • username에 유니크 제약이 있어도 중복 체크는 Service에서 수행
  • 이유:
    • 에러 메시지 품질
    • HTTP 상태 코드 제어
    • 프론트엔드 협업
    • 역할과 책임 분리
  • DB 유니크 제약은 제거하지 않고 마지막 안전장치로 유지

이 리팩토링을 통해
회원가입 API는 더 명확하고, 예측 가능하고, 협업 친화적인 구조가 되었다.

0개의 댓글