회원가입 기능을 구현하면서
username 중복 체크 로직을 컨트롤러에서 서비스로 이동하는 리팩토링을 진행했다.
겉으로 보면 단순한 위치 이동처럼 보이지만,
실제로는 에러 처리 품질, 책임 분리, 프론트엔드 협업과 직접적으로 연결되는 중요한 변경이다.
memberService.findByUsername(reqBody.username())
.ifPresent(_member -> {
throw new ServiceException("409-1", "이미 존재하는 아이디입니다.");
});
Member member = memberService.join(
reqBody.username(),
reqBody.password(),
reqBody.nickname()
);
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 유니크 제약만으로도 중복은 막을 수 있다.
중복된 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 레벨에서 명확하게 처리한다
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는 이제 요청 전달 역할만 수행한다.
Controller
Service
👉 username 중복 여부는 명백한 비즈니스 규칙이므로 Service가 책임지는 것이 맞다.
{
"resultCode": "409-1",
"msg": "이미 존재하는 아이디입니다.",
"data": null
}
👉 DB 에러보다 훨씬 친절하고 예측 가능하다.
👉 이런 상황을 막는 최후의 방어선이 DB 유니크 제약이다.
즉,
가능은 하지만 실무에서는 거의 쓰지 않는다.
이유는 단순하다.
프론트엔드와 사용자에게 너무 불친절하기 때문
그래서:
DataIntegrityViolationException 그대로 노출 → 안 좋은 설계ServiceException으로 의미 있는 에러 전달 → 좋은 설계이 리팩토링을 통해
회원가입 API는 더 명확하고, 예측 가능하고, 협업 친화적인 구조가 되었다.