Skip to content

[1주차] 회원가입 / 내 정보 조회 / 비밀번호 수정 기능 구현 - 정인철#9

Merged
incheol789 merged 8 commits intoLoopers-dev-lab:incheol789from
incheol789:main
Feb 5, 2026
Merged

[1주차] 회원가입 / 내 정보 조회 / 비밀번호 수정 기능 구현 - 정인철#9
incheol789 merged 8 commits intoLoopers-dev-lab:incheol789from
incheol789:main

Conversation

@incheol789
Copy link
Copy Markdown

@incheol789 incheol789 commented Feb 5, 2026

📌 Summary

  • 배경: 서비스 이용을 위한 회원 관리 기능이 필요함. 회원가입, 로그인 인증, 비밀번호 변경 등 기본적인 회원 관련 API가 없어 서비스 출시가 불가능한 상태.
  • 목표: 회원가입 / 내 정보 조회 / 비밀번호 수정 API 구현 및 TDD 기반 개발 프로세스 적용
  • 결과: 3개의 회원 API 구현 완료. 단위 테스트 22개, 통합 테스트 4개, E2E 테스트 8개 작성.

🧭 Context & Decision

문제 정의

  • 현재 동작/제약: 회원 관련 기능이 없음. Example 도메인만 존재하는 상태.
  • 문제(또는 리스크): 회원 식별 없이는 주문, 장바구니 등 핵심 기능 개발이 불가능함.
  • 성공 기준(완료 정의):
    • 회원가입 시 유효성 검증(아이디/비밀번호/이메일 형식) 통과
    • 헤더 기반 인증으로 내 정보 조회 가능
    • 비밀번호 변경 시 기존 비밀번호와 중복 체크

선택지와 결정

1. 인증 방식

  • 고려한 대안:
    • A: JWT 토큰 기반 인증
    • B: 세션 기반 인증
    • C: 커스텀 헤더 기반 간이 인증 (X-Loopers-LoginId, X-Loopers-LoginPw)
  • 최종 결정: C. 커스텀 헤더 기반 간이 인증
  • 트레이드오프: 보안상 취약하지만, 현재 단계에서는 회원 도메인 구현에 집중. Spring Security 도입은 추후 예정.
  • 추후 개선 여지: Spring Security + JWT 인증으로 전환 예정

2. 비밀번호 암호화 방식

  • 고려한 대안:
    • A: BCrypt (Spring Security 기본 제공)
    • B: SHA-256 단방향 해시
  • 최종 결정: B. SHA-256
  • 트레이드오프: BCrypt가 더 안전하지만 Spring Security 의존성 없이 구현 가능. 추후 Security 도입 시 마이그레이션 필요.

3. 테스트 전략 (TDD 적용 시 고민했던 부분)

  • 고려한 대안:
    • A: E2E 테스트만 작성 (빠르게 검증)
    • B: 단위 테스트 + 통합 테스트 + E2E 테스트 (피라미드 구조)
  • 최종 결정: B. 테스트 피라미드 구조
  • 트레이드오프: 테스트 작성 시간이 오래 걸리지만, 리팩토링 시 안전망 확보. 특히 MemberModel의 유효성 검증 로직은 단위 테스트로 빠르게 피드백 받을 수 있었음.

💭 TDD 적용하면서 느낀 점

  • Red 단계에서 "어디까지 테스트를 먼저 작성해야 하나?" 고민이 많았음. 처음엔 E2E부터 작성했다가 실패 원인 파악이 어려워서, 결국 도메인 모델 단위 테스트부터 작성하는 게 낫다고 판단.
  • MemberServiceUnitTest에서 Dummy/Stub/Mock/Spy/Fake 패턴을 모두 실습해봄. 언제 어떤 테스트 더블을 써야 하는지 아직 감이 부족한데, 이번에 정리해보니 조금 이해됨.

🏗️ Design Overview

변경 범위

  • 영향 받는 모듈/도메인: apps/commerce-api
  • 신규 추가:
    • domain/member/ - MemberModel, MemberService, MemberRepository
    • application/member/ - MemberFacade, MemberInfo
    • interfaces/api/member/ - MemberV1Controller, MemberV1ApiSpec, MemberV1Dto
    • infrastructure/member/ - MemberRepositoryImpl, MemberJpaRepository
    • support/crypto/PasswordEncoder - SHA-256 암호화 유틸
  • 제거/대체: 없음

주요 컴포넌트 책임

  • MemberModel: 회원 엔티티. 생성 시 유효성 검증, 비밀번호 변경 로직 보유
  • MemberService: 회원 비즈니스 로직. 가입/조회/비밀번호 변경 처리
  • MemberFacade: Controller와 Service 사이 조율. DTO 변환 담당
  • MemberV1Controller: REST API 엔드포인트. 요청/응답 처리
  • PasswordEncoder: SHA-256 기반 비밀번호 단방향 암호화

🔁 Flow Diagram

Main Flow - 회원가입

sequenceDiagram
  autonumber
  participant Client
  participant Controller as MemberV1Controller
  participant Facade as MemberFacade
  participant Service as MemberService
  participant Repo as MemberRepository
  participant DB

  Client->>Controller: POST /api/v1/members
  Controller->>Facade: register(command)
  Facade->>Service: register(loginId, pw, name, birth, email)
  Service->>Repo: findByLoginId(loginId)
  Repo->>DB: SELECT
  DB-->>Repo: null (not exists)
  Service->>Service: new MemberModel (validation)
  Service->>Service: applyEncodedPassword (SHA-256)
  Service->>Repo: save(member)
  Repo->>DB: INSERT
  DB-->>Repo: saved entity
  Repo-->>Service: MemberModel
  Service-->>Facade: MemberModel
  Facade-->>Controller: MemberInfo
  Controller-->>Client: 200 OK + memberId
Loading

Main Flow - 내 정보 조회

sequenceDiagram
  autonumber
  participant Client
  participant Controller as MemberV1Controller
  participant Facade as MemberFacade
  participant Service as MemberService
  participant Repo as MemberRepository
  participant DB

  Client->>Controller: GET /api/v1/members/me
  Note over Client,Controller: Headers: X-Loopers-LoginId, X-Loopers-LoginPw
  Controller->>Facade: getMyInfo(loginId, password)
  Facade->>Service: getMyInfo(loginId, password)
  Service->>Repo: findByLoginId(loginId)
  Repo->>DB: SELECT
  DB-->>Repo: MemberModel
  Service->>Service: verify password (SHA-256 비교)
  Service-->>Facade: MemberModel
  Facade-->>Controller: MemberInfo (with maskedName)
  Controller-->>Client: 200 OK + member info
Loading

Exception Flow - 중복 아이디

sequenceDiagram
  autonumber
  participant Client
  participant Controller
  participant Service
  participant Repo

  Client->>Controller: POST /api/v1/members (duplicate loginId)
  Controller->>Service: register(...)
  Service->>Repo: findByLoginId(loginId)
  Repo-->>Service: MemberModel (exists!)
  Service->>Service: throw CoreException(CONFLICT)
  Service-->>Controller: CoreException
  Controller-->>Client: 409 Conflict
Loading

✅ Test Plan

단위 테스트 (MemberModelTest - 22 cases)

  • 정상 생성
  • loginId 검증 (null, empty, 특수문자, 한글)
  • password 검증 (길이, 특수문자 포함, 생년월일 포함 불가)
  • email 형식 검증
  • maskedName 동작 확인 (3자 이상, 1자)
  • changePassword 검증

서비스 단위 테스트 (MemberServiceUnitTest - 16 cases)

  • Dummy: 같은 비밀번호 에러 시 Repository 호출 안 함
  • Stub: 고정 응답으로 가입/조회 테스트
  • Mock: Repository 메서드 호출 검증
  • Spy: 내부 메서드 호출 및 상태 검증
  • Fake: 인메모리 Repository로 실제 동작 테스트

통합 테스트 (MemberServiceIntegrationTest - 4 cases)

  • 가입 후 조회
  • 중복 아이디 CONFLICT
  • 존재하지 않는 회원 NOT_FOUND
  • 비밀번호 변경 후 새 비밀번호로 조회

E2E 테스트 (MemberV1ApiE2ETest - 8 cases)

  • 회원가입 성공 (200)
  • 회원가입 중복 (409)
  • 내 정보 조회 성공 + 이름 마스킹 확인
  • 내 정보 조회 - 잘못된 비밀번호 (400)
  • 내 정보 조회 - 인증 헤더 누락 (400)
  • 비밀번호 변경 성공 (200)
  • 비밀번호 변경 - 기존과 동일 (400)

📝 추가 메모

TDD 진행하면서 어려웠던 점

  1. Red 단계에서 테스트 범위 결정: 처음에 E2E 테스트부터 작성했는데, 실패 시 원인 파악이 어려웠음. 결국 도메인 모델 → 서비스 → E2E 순으로 작성하는 게 효율적이었음.

  2. 테스트 더블 선택: Mockito의 mock, spy, stub 개념이 헷갈렸음. 이번에 정리한 기준:

    • Dummy: 파라미터 채우기용 (실제 사용 X)
    • Stub: 고정 응답 반환 (상태 검증)
    • Mock: 호출 여부 검증 (행위 검증)
    • Spy: 실제 객체 + 일부 동작 감시
    • Fake: 실제 구현의 간소화 버전 (인메모리 DB 등)
  3. Given-When-Then 경계: Given에서 어디까지 준비해야 하는지 애매했음. 결론은 "테스트 대상 메서드 호출 직전까지가 Given".

리뷰어에게 질문

  1. PasswordEncodersupport/crypto에 두었는데, domain/member 안에 두는 게 더 적절할까요?
  2. 테스트 코드에서 @Nested 클래스 사용이 과한 것 같기도 한데, 가독성 면에서 괜찮을까요?
  3. MemberServiceUnitTest에 테스트 더블 패턴별로 예시를 다 넣었는데, 실무에서는 이렇게 다양하게 쓰는 편인가요?

Summary by CodeRabbit

릴리스 노트

  • 새로운 기능
    • 회원가입 기능 추가 (로그인ID, 비밀번호, 이름, 생년월일, 이메일)
    • 내 정보 조회 기능 추가 (이름은 마스킹된 형태로 표시)
    • 비밀번호 변경 기능 추가
    • 강화된 비밀번호 보안 검증 (8-16자, 특수문자 제외, 생년월일 미포함)

incheol789 and others added 8 commits February 5, 2026 01:07
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@incheol789 incheol789 self-assigned this Feb 5, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Feb 5, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

  • ✅ Full review completed - (🔄 Check again to review again)

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@apps/commerce-api/src/main/java/com/loopers/domain/member/MemberModel.java`:
- Around line 13-23: Add JPA `@Column` annotations to MemberModel's field
declarations so DB-level constraints are enforced: annotate loginId with
`@Column`(nullable = false, unique = true) and annotate password, name, birthDate,
and email with `@Column`(nullable = false); also add the javax.persistence.Column
(or jakarta.persistence.Column depending on project) import. Update the
MemberModel class fields (loginId, password, name, birthDate, email) accordingly
to match the BaseEntity pattern.

In
`@apps/commerce-api/src/main/java/com/loopers/domain/member/MemberService.java`:
- Around line 25-27: Replace the insecure SHA-256 single-hash PasswordEncoder
with an adaptive, salted algorithm (e.g., BCrypt or Argon2) and update usages in
MemberService/MemberModel: change PasswordEncoder.encode(...) to produce a
properly salted/strength-configured hash and ensure
MemberModel.applyEncodedPassword(...) stores that encoded hash (not the raw or
single-sha256 value); also replace any PasswordEncoder.matches(...)
implementation that uses String.equals() with the algorithm's safe verify method
(or a constant-time comparison) so password checks are resistant to timing
attacks. Ensure configuration exposes work factor/params (e.g., BCrypt rounds)
and update tests/creation flows that call PasswordEncoder.encode and matches
accordingly.
- Around line 18-27: Add a DB-level uniqueness constraint on the loginId field
and handle concurrent-insert failures by mapping integrity exceptions to your
domain exception: add `@Column`(unique = true) on MemberModel.loginId, keep the
existing pre-check in MemberService.register (which calls PasswordEncoder.encode
and memberRepository.save), and wrap the save call (or the transaction boundary)
to catch DataIntegrityViolationException (or the JPA equivalent) and rethrow as
new CoreException(ErrorType.CONFLICT, "이미 존재하는 loginId입니다.") so concurrent
inserts fail cleanly and surface the same domain error.

In
`@apps/commerce-api/src/main/java/com/loopers/support/crypto/PasswordEncoder.java`:
- Around line 9-25: The current PasswordEncoder.encode method uses MessageDigest
SHA-256 (MessageDigest.getInstance("SHA-256")) which produces unsalted, fast
hashes and is vulnerable to rainbow-table and brute-force attacks; replace this
implementation with a password‑hashing algorithm that handles salts and work
factor for you (e.g., BCrypt or Argon2). Concretely, change
PasswordEncoder.encode to call a secure password hasher (for example Spring's
BCryptPasswordEncoder.encode or a libsodium/Argon2 wrapper) so salts are
generated and stored internally and a configurable strength/work-factor is
applied, and update any code that relies on the SHA-256 output format to accept
the new hashed string format. Ensure exceptions are propagated or wrapped
appropriately and add unit tests verifying verification via the chosen library
(e.g., BCryptPasswordEncoder.matches or Argon2 verify) rather than recomputing
raw hashes.
- Around line 27-29: The matches method in PasswordEncoder uses String.equals,
which is vulnerable to timing attacks; change matches(String rawPassword, String
encodedPassword) to compute the encoded value via encode(rawPassword) and then
perform a constant-time comparison (e.g. convert both to byte[] and use
MessageDigest.isEqual or a constant-time byte loop) instead of String.equals,
and handle nulls safely so comparisons never short-circuit.
🧹 Nitpick comments (2)
apps/commerce-api/src/main/java/com/loopers/interfaces/api/member/MemberV1Controller.java (1)

35-49: 응답 매핑을 별도 메서드로 추출하는 것을 고려해 보세요.

MemberInfo에서 MyInfoResponse로의 변환 로직이 인라인으로 작성되어 있습니다. 현재 코드도 작동하지만, 매핑 로직을 MyInfoResponse의 정적 팩토리 메서드로 추출하면 재사용성과 가독성이 향상됩니다.

♻️ 정적 팩토리 메서드 제안

MemberV1Dto.java에 추가:

public record MyInfoResponse(
        String loginId,
        String maskedName,
        LocalDate birthDate,
        String email
) {
    public static MyInfoResponse from(MemberInfo info) {
        return new MyInfoResponse(
            info.loginId(),
            info.maskedName(),
            info.birthDate(),
            info.email()
        );
    }
}

컨트롤러에서 사용:

 MemberInfo info = memberFacade.getMyInfo(loginId, loginPw);
-MemberV1Dto.MyInfoResponse response = new MemberV1Dto.MyInfoResponse(
-        info.loginId(),
-        info.maskedName(),
-        info.birthDate(),
-        info.email()
-);
+MemberV1Dto.MyInfoResponse response = MemberV1Dto.MyInfoResponse.from(info);
 return ApiResponse.success(response);
apps/commerce-api/src/test/java/com/loopers/domain/member/MemberModelTest.java (1)

92-138: password와 email의 null/empty 케이스 테스트를 추가하세요.

MemberModel 생성자는 password와 email에 대해 null 검증을 수행하지만(생성자 40줄, 65줄), loginId, name, birthDate와 달리 명시적인 null 테스트 케이스가 누락되어 있습니다. 테스트 일관성을 위해 다음 케이스들을 추가하는 것을 권장합니다:

  • password가 null일 때 BAD_REQUEST 예외 발생
  • email이 null일 때 BAD_REQUEST 예외 발생

@incheol789 incheol789 merged commit d6abdc8 into Loopers-dev-lab:incheol789 Feb 5, 2026
1 check passed
ukukdin added a commit that referenced this pull request Feb 13, 2026
refactor: UserRegisterService에서 레이어 간 책임 분리 안되어 있는것
@incheol789 incheol789 changed the title [round-1] 회원가입 / 내 정보 조회 / 비밀번호 수정 기능 구현 [1주차] 회원가입 / 내 정보 조회 / 비밀번호 수정 기능 구현 - 정인철 Feb 15, 2026
letter333 added a commit that referenced this pull request Mar 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant