v0.1
📂 목차
- 핵심 기능 도출
- 요구사항 정의(Requirement Specification)
- 기획 협업(생략)
- 기술 설계(Design Specification)
- 개발 Sprint
- Sprint Review & Retrospective
📚 본문
User 관련 기능을 RESTful 하게 구현하고 재사용하기 위해 User Component 혹은 패키지를 구현한다. 필요에 따라 서브 프로세스는 생략한다.
핵심 기능 도출
대부분의 서비스에 대한 User 관리 기능을 총체적으로 관리하는 컴포넌트를 개발하기 위해 핵심 기능을 도출한다.
최소 기능 정의
서비스가 존재할 수 있는 가장 작은 단위를 다음 기능을 포함하는 서비스로 정의
- 회원가입 (Registration)
- 목표: 새로운 사용자가 서비스에 계정을 생성하고 접속 가능
- 핵심 기능:
- 사용자로부터 이메일 주소, 비밀번호, 사용자 이름을 입력
- 이메일 주소의 유일성 및 유효성을 검증
- 비밀번호는 보안을 위해 해싱하여 저장
- 성공적으로 계정이 생성되면 사용자에게 확인 응답
User Flow 설계
UF Registration
graph TD
A[클라이언트] --> B{POST /api/v1-beta/users/register};
B --> C[백엔드: 요청 수신];
C --> D{요청 바디 유효성 검증};
D -- 실패 (400 Bad Request) --> E[응답: 에러 메시지];
D -- 성공 --> F{이메일 중복 확인};
F -- 중복 (409 Conflict) --> E;
F -- 고유함 --> G[비밀번호 해싱];
G --> H[사용자 정보 DB 저장];
H --> I{DB 저장 성공?};
I -- 실패 (500 Internal Server Error) --> E;
I -- 성공 --> J[응답: 201 Created, 생성된 사용자 ID/Email];
J --> A;
E --> A;
요구사항 정의
앞서 다뤘던 MVP 와 User Flow Chart 를 통한 기능에 대한 구체적인 요구사항을 수집한다. 개발자들이 기능을 구현할 때 참고할 수 있는 구체적인 설계 가이드라인이 되기에 상세히 작성할수록 좋다.
핵심 기능에 대한 요구 수집
RQ Registration
새로운 사용자가 서비스에 계정을 생성
- API 엔드포인트:
POST /api/v1/users/register
- 요청 (Request Body): JSON 형식
email
(문자열, 필수): 사용자의 이메일 주소- 제약사항: 유효한 이메일 형식 (예:
user@example.com
). - 제약사항: 시스템 내에서 고유해야 함
- 길이 제한: 최대 255자
- 제약사항: 유효한 이메일 형식 (예:
password
(문자열, 필수): 사용자가 설정할 비밀번호- 제약사항: 최소 8자 이상, 대문자/소문자/숫자/특수문자 중 3가지 이상 포함
- 길이 제한: 최소 8자, 최대 64자
username
(문자열, 필수): 사용자 이름 (닉네임)- 제약사항: 중복 허용 (필요에 따라 고유성 제약 추가 가능)
- 길이 제한: 최소 2자, 최대 30자
- 성공 응답 (
Response - 201 Created
): JSON 형식id
(정수/UUID): 생성된 사용자의 고유 IDemail
(문자열): 등록된 이메일 주소username
(문자열): 등록된 사용자 이름message
(문자열): “회원가입이 성공적으로 완료되었습니다.”
- 오류 응답 (Response): JSON 형식
status
(정수):400 Bad Request
(유효성 검증 실패)409 Conflict
(이메일 중복)500 Internal Server Error
(서버 내부 오류)
code
(문자열): 특정 오류를 식별하는 내부 코드INVALID_EMAIL_FORMAT
PASSWORD_POLICY_VIOLATION
EMAIL_ALREADY_EXISTS
message
(문자열): 사용자에게 표시할 오류 메시지- “이메일 형식이 올바르지 않습니다.”
- “비밀번호는 최소 8자 이상, 64자 이하이며, 영문 대소문자, 숫자, 특수문자 중 3가지 이상을 포함해야 합니다.”
- “이름은 최소 2자 이상이며, 최대 30자 입니다.”
- “이미 등록된 이메일 주소입니다.”
- “서버 오류가 발생했습니다. 사용자 정보를 저장하는 데 실패했습니다. 잠시 후 다시 시도해 주세요.”
- 백엔드 처리 로직:
- 요청 데이터 유효성 검증
- 이메일 중복 확인
- 비밀번호 해싱 (예: bcrypt)
- 사용자 정보를 데이터베이스에 저장
(선택 사항) 회원가입 시점에 기본 역할(Role) 부여 (예: USER)
시나리오/유스케이스 작성
작성하기 전 유스케이스 다이어그램
graph LR
Actor(사용자) --> UC1(회원가입);
SC Registration
공통 전처리
- 요청 바디에서
email
,password
,username
을 추출한다. - 각 필드에 대해 유효성 검증을 수행한다.
email
: 문자열 여부 및 이메일 형식 검증password
: 길이 및 보안 정책(대소문자, 숫자, 특수문자 포함) 검증username
: 최소/최대 길이 확인
성공 흐름
- 이메일이 중복인지 확인한다.
- 비밀번호를 해싱 처리한다.
- 해싱된 비밀번호와 함께 사용자 정보를 DB에 저장한다.
- 저장이 성공하면 다음 정보를 포함한
201 Created
응답을 반환한다.id
: 생성된 사용자 IDemail
: 등록된 이메일 주소username
: 등록된 사용자명message
: “회원가입이 성공적으로 완료되었습니다.”
실패 흐름 입력 무효
- 검증에 실패한 경우
400 Bad Request
응답과 함께 적절한 오류 메시지를 반환한다.- “이메일 형식이 올바르지 않습니다.”
- “비밀번호는 최소 8자 이상, 64자 이하이며, 영문 대소문자, 숫자, 특수문자 중 3가지 이상을 포함해야 합니다.”
- “이름은 최소 2자 이상이며, 최대 30자 입니다.”
실패 흐름 이메일 중복
- 이메일이 중복인지 확인한다.
- 이메일이 중복인 경우
409 Conflict
응답과 함께 적절한 오류 메시지를 반환한다.- “이미 등록된 이메일 주소입니다.”
실패 흐름 DB 서버 통신 실패
- 이메일이 중복인지 확인한다.
- 비밀번호를 해싱 처리한다.
- 해싱된 비밀번호와 함께 사용자 정보를 DB에 저장한다.
- DB에 반영되지 않은 경우
500 Internal Server Error
와 함께 적절한 오류 메시지를 반환한다.- “서버 오류가 발생했습니다. 사용자 정보를 저장하는 데 실패했습니다. 잠시 후 다시 시도해 주세요.”
기술 설계
API 스펙 초안 작성
API Registration
새로운 사용자가 서비스에 가입하기 위해 필요한 정보를 제출하는 API입니다.
- HTTP Method:
POST
- URL:
/api/v1-beta/users/register
요청 바디 (Request Body)
필드명 | 데이터 타입 | 설명 |
---|---|---|
email |
string | 사용자 이메일 주소 |
password |
string | 사용자 비밀번호 |
username |
string | 사용자 이름 |
요청 바디 예시
{
"email": "user@example.com",
"password": "Password123!",
"username": "홍길동"
}
응답 (Response)
상태 | 코드 | 설명 |
---|---|---|
201 |
Created |
회원가입이 성공적으로 완료되었을 때 반환하는 응답 |
400 |
Bad Request |
요청 바디의 유효성 검증에 실패했을 때 반환하는 응답 |
409 |
Conflict |
이메일이 이미 사용 중이어서 회원가입이 불가능할 때 반환하는 응답 |
500 |
Internal Server Error |
서버 내부에서 예기치 않은 오류가 발생했을 때 반환하는 응답 |
응답 바디 예시 헤더는 생략:
{
"id": "uuid-1234-5678-90ab",
"email": "user@example.com",
"username": "홍길동",
"message": "회원가입이 성공적으로 완료되었습니다."
}
{
"error": "비밀번호는 최소 8자 이상, 64자 이하이며, 영문 대소문자, 숫자, 특수문자 중 3가지 이상을 포함해야 합니다."
}
{
"error": "이미 등록된 이메일 주소입니다."
}
{
"error": "서버 오류가 발생했습니다. 잠시 후 다시 시도해 주세요."
}
DB 테이블/엔티티 구조 정의
erDiagram
users {
UUID id PK "사용자 고유 ID"
VARCHAR email "이메일 주소"
VARCHAR password "해싱된 비밀번호"
VARCHAR username "사용자 이름"
}
서비스 계층의 로직 흐름 정리
SD Registration
sequenceDiagram
participant User as 사용자
participant Client as 클라이언트(웹/앱)
participant APIServer as API 서버
participant Service as 서비스 계층
participant Database as 데이터베이스
Title: 사용자 회원가입 시퀀스 다이어그램
%% 성공 흐름
User->>Client: 회원가입 정보 입력
Client->>APIServer: 회원가입 요청<br/>(POST /api/v1-beta/users/register)
APIServer->>Service: 요청 처리 위임
Service->>Service: 1. 유효성 검증<br/>(email, password, username)
Service->>Database: 2. 이메일 중복 확인
Database-->>Service: 중복 없음
Service->>Service: 3. 비밀번호 해싱
Service->>Database: 4. 사용자 정보 저장
Database-->>Service: 저장 성공
Service-->>APIServer: 201 Created 응답 반환
APIServer-->>Client: 201 Created 응답
Client-->>User: "회원가입 성공" 메시지 표시
%% 입력 무효 실패 흐름
alt 입력 무효
User->>Client: 잘못된 정보 입력
Client->>APIServer: 회원가입 요청<br/>(POST /api/v1-beta/users/register)
APIServer->>Service: 요청 처리 위임
Service->>Service: 1. 유효성 검증<br/>(실패)
Service-->>APIServer: 400 Bad Request 반환
APIServer-->>Client: 400 Bad Request 응답
Client-->>User: "잘못된 입력입니다" 메시지 표시
end
%% 이메일 중복 실패 흐름
alt 이메일 중복
User->>Client: 중복 이메일 입력
Client->>APIServer: 회원가입 요청<br/>(POST //api/v1-beta/users/register)
APIServer->>Service: 요청 처리 위임
Service->>Service: 1. 유효성 검증<br/>(성공)
Service->>Database: 2. 이메일 중복 확인
Database-->>Service: 중복된 이메일 존재
Service-->>APIServer: 409 Conflict 반환
APIServer-->>Client: 409 Conflict 응답
Client-->>User: "이미 등록된 이메일입니다" 메시지 표시
end
개발 Sprint
여기서는 구현하기 전 사용할 공통 라이브러리들을 전부 불러오고, 서버의 구동 profile 에 맞춰 configuration 들을 미리 작성해준다.
핵심 기능 우선순위 설정 및 분할
빈 class 파일들을 생성하고 Data Access, Presentation, Business Logic Layer 을 중점으로 작성해준다.
-
엔드포인트(
UserController
) 구현 (/api/v1-beta/users
) -
register(
POST /api/v1-beta/users/register
)- User Entity 구현
- 비즈니스 엔티티 유효성 검증을 위한 커스텀 인터페이스들 정의
@Password
@Email
-jakarta.validation.constraints
참고
- 비즈니스 엔티티 유효성 검증을 위한 빈 밸리데이터 정의
PasswordRuleValidator
클래스 구현
- User JPA 변환 및 DAO 클래스(
UserRepository
) 구현- 이메일 중복 검증을 위한
existsByEmail
함수 정의 - 유저 데이터 저장하는
createUser
함수 정의
- 이메일 중복 검증을 위한
- User 관련 최종 서비스를 제공하는 클래스(
UserService
) 구현register
함수 구현
UserController
의register
함수 구현
classDiagram
direction LR
subgraph Presentation_Layer
class UserController {
+ register(String email, String password, String username) : ResponseEntity<User>
}
end
subgraph Business_Logic_Layer
class IUserService {
<<interface>>
~ register(String email, String password, String username) : Optional<User>
}
end
subgraph Data_Access_Layer
class IUserRepository {
<<interface>>
~ existsByEmail(email) : boolean
~ save(user) : User
}
end
subgraph Domain_and_Validation
class User {
<<Entity>>
- @Email String email
- @Password String password
- String username
}
class IPassword {
<<interface>>
}
class PasswordRuleValidator {
+ @Override isValid(Object value, ConstraintValidatorContext context)
}
class ConstraintValidator {
<<interface>>
+ isValid(value, context)
}
end
UserController --|> IUserService: use
IUserService --|> IUserRepository: use
IUserRepository --|> User: CRUD
User ..|> IPassword
ConstraintValidator <|.. PasswordRuleValidator: implements
ConstraintValidator ..> IPassword: generic type
TDD 기반 소스코드 작성
위의 class diagram을 바탕으로 Test class 들을 작성해주어야 한다.
@DataJpaTest
@ActiveProfiles("test")
class UserRepositoryTest {
@Autowired
UserRepository userRepository;
@BeforeEach
void setup() {
userRepository.save(
new User("1@email.com", "name", "1234!@")
);
}
@AfterEach
void init() {
userRepository.deleteAll();
}
@ParameterizedTest
@CsvSource({"1@gmail.com, name, 1234!@"})
void givenTheData_whenCreatingSameUser_thenThrowsException(String email, String username, String password) {
User user = new User(email, username, password);
assertThrows(
DataIntegrityViolationException.class,
() -> userRepository.save(user)
);
}
@ParameterizedTest
@CsvSource({"user@email.com, user, 12329212@@!hdH"})
void givenCreatedUser_whenExistingUser_thenExpectedResult(String email, String username, String password) {
userRepository.save(new User(email, username, password));
assertTrue(userRepository.existsByEmail(email));
assertTrue(userRepository.existsByEmail("1@email.com"));
}
}
@SpringBootTest
@ActiveProfiles("test")
public class UserServiceTest {
@Autowired
private UserService userService;
private static final BCryptPasswordEncoder PASSWORD_ENCODER = new BCryptPasswordEncoder();
// AI 생성 테스트케이스
// --- 테스트 데이터 제공 메서드 ---
// @MethodSource 가 참조하는 정적 메서드입니다.
// 각 Arguments 는 User 객체와 예상 결과를 담고 있습니다.
private static Stream<Arguments> wrongUserTestCases() {
return Stream.of(
// 이메일 형식이 유효하지 않은 경우
Arguments.of("invalid_email.com", "user", "Valid-Password-123!",
"이메일 형식이 올바르지 않습니다."),
// 사용자 이름이 너무 짧은 경우 (최소 2자)
Arguments.of("valid@email.com", "a", "Valid-Password-123!",
"이름은 최소 2자 이상이며, 최대 30자 입니다."),
// 비밀번호가 너무 짧은 경우 (최소 8자)
Arguments.of("valid@email.com", "user", "short1!",
"비밀번호는 최소 8자 이상, 64자 이하이며, 영문 대소문자, 숫자, 특수문자 중 3가지 이상을 포함해야 합니다."),
// 비밀번호가 복잡성 조건을 만족하지 못하는 경우 (3가지 이상 포함)
Arguments.of("valid@email.com", "user", "lowercaseonly",
"비밀번호는 최소 8자 이상, 64자 이하이며, 영문 대소문자, 숫자, 특수문자 중 3가지 이상을 포함해야 합니다."), // 소문자만
Arguments.of("valid@email.com", "user", "UPPERCASEONLY",
"비밀번호는 최소 8자 이상, 64자 이하이며, 영문 대소문자, 숫자, 특수문자 중 3가지 이상을 포함해야 합니다."), // 대문자만
Arguments.of("valid@email.com", "user", "1234567890",
"비밀번호는 최소 8자 이상, 64자 이하이며, 영문 대소문자, 숫자, 특수문자 중 3가지 이상을 포함해야 합니다."), // 숫자만
Arguments.of("valid@email.com", "user", "only!@#$",
"비밀번호는 최소 8자 이상, 64자 이하이며, 영문 대소문자, 숫자, 특수문자 중 3가지 이상을 포함해야 합니다."), // 특수문자만
Arguments.of("valid@email.com", "user", "Abcdefg1",
"비밀번호는 최소 8자 이상, 64자 이하이며, 영문 대소문자, 숫자, 특수문자 중 3가지 이상을 포함해야 합니다."), // 대문자, 소문자, 숫자 (3가지) -> 통과해야 하지만, 예시를 위해 실패로 가정
Arguments.of("valid@email.com", "user", "Abcdefg!",
"비밀번호는 최소 8자 이상, 64자 이하이며, 영문 대소문자, 숫자, 특수문자 중 3가지 이상을 포함해야 합니다.")// 대문자, 소문자, 특수문자 (3가지) -> 통과해야 하지만, 예시를 위해 실패로 가정
);
}
private static Stream<Arguments> rightUserTestCases() {
return Stream.of(
Arguments.of("valid@email.com", "user", "Abcdefg!",
new User("valid@email.com", "user", PASSWORD_ENCODER.encode("Abcdefg!"))),
Arguments.of("valid@gmail.com", "user", "12341234",
new User("valid@gmail.com", "user", PASSWORD_ENCODER.encode("12341234")))
);
}
@ParameterizedTest
@MethodSource("wrongUserTestCases")
void givenWrongUser_whenRegister_thenExpectedResult(
String email, String username, String password, String expected) {
ValidationException exception = assertThrows(
ValidationException.class,
() -> userService.register(email, username, password)
);
assertTrue("예상한 메시지가 아닙니다.", expected.contains(exception.getMessage()));
}
@Test
void givenSameUser_whenRegister_thenExpectedException() {
userService.register("email@email.com", "username", "Password123!");
assertThrows(
Exception.class,
() -> userService.register("email@email.com", "hi", "Password123!")
);
}
@ParameterizedTest
@MethodSource("rightUserTestCases")
void givenValidUser_whenRegister_thenExpectedResult(String email, String username, String password, User expected) {
Optional<User> user = userService.register(email, username, password);
assertTrue("등록 실패", user.isPresent());
User validUser = user.get();
assertEquals("이메일 불일치", expected.getEmail(), validUser.getEmail());
assertEquals("이름 불일치", expected.getUsername(), validUser.getUsername());
assertTrue("패스워드 불일치", PASSWORD_ENCODER.matches(password, validUser.getPassword()));
}
}
컨트롤러 수준은 더 공부하고 작성