Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

kangwooc/user-service

Repository files navigation

User Service

사용자 인증/인가 및 관리를 담당하는 Spring Boot 기반 백엔드 서비스입니다. Hexagonal Architecture(육각형 아키텍처)를 적용하여 유지보수성과 확장성을 확보한 멀티 모듈 프로젝트입니다.

목차


기술 스택

Core Framework & Language

  • Kotlin 2.2.10 (JVM 21)
  • Spring Boot 3.5.6
  • Spring Security 6.x
  • Spring Data JPA

Database

  • PostgreSQL 17 (Bitnami image)
    • Master-Slave Replication (Read/Write 분리)
    • Master: localhost:5432
    • Standby: localhost:5433
  • Flyway (Database Migration)

Cache & Session

  • Redis 7.0.11 (Refresh Token 저장소)

Authentication

  • JWT (JSON Web Token)
  • BCrypt (Password Hashing)

Testing

  • JUnit 5
  • Mockito-Kotlin
  • Spring Boot Test
  • MockMvc (Integration Test)

API Documentation

  • Swagger/OpenAPI 3.0

Build & Dependency Management

  • Gradle 8.x (Kotlin DSL)
  • Docker Compose (로컬 개발 환경)

프로젝트 구조

이 프로젝트는 **Hexagonal Architecture (Ports and Adapters)**를 기반으로 멀티 모듈 구조를 채택했습니다.

user-service/
├── bootstrap/ # 애플리케이션 진입점 및 실행 모듈
│ ├── src/main/kotlin/ # 메인 애플리케이션 클래스
│ ├── src/main/resources/ # 전역 설정 파일
│ │ └── db/migration/ # Flyway 마이그레이션 스크립트
│ ├── src/test/kotlin/ # 통합 테스트
│ └── compose.yaml # Docker Compose 설정
│
├── user-api/ # API 계층 (Adapters - Driving)
│ ├── user-api-auth/ # 인증 API (회원가입, 로그인)
│ ├── user-api-member/ # 회원 API (사용자 정보 관리)
│ └── user-api-admin/ # 관리자 API (전체 사용자 조회)
│
├── user-application/ # 애플리케이션 계층 (Use Cases)
│ ├── port/ # Application Port 인터페이스
│ ├── adapter/ # Use Case 구현
│ ├── dto/ # 요청/응답 DTO
│ └── event/ # 도메인 이벤트 리스너
│
├── user-domain/ # 도메인 계층 (Core Business Logic)
│ ├── entity/ # 엔티티 (User, Role, Permission)
│ ├── vo/ # Value Object (Email, Password)
│ ├── repository/ # Repository Port 인터페이스
│ └── exception/ # 도메인 예외
│
├── user-infrastructure/ # 인프라스트럭처 계층 (Adapters - Driven)
│ ├── user-infrastructure-auth/ # JWT 인증 구현체
│ ├── user-infrastructure-persistence/ # JPA Repository 구현체
│ ├── user-infrastructure-redis/ # Redis 구현체
│ └── user-infrastructure-mq/ # 메시지 큐 (placeholder)
│
└── common/ # 공통 모듈
 ├── config/ # 공통 설정
 ├── dto/ # 공통 응답 형식
 ├── exception/ # 전역 예외 처리
 └── auth/ # 인증/인가 유틸리티

의존성 규칙 (Dependency Rule)

Hexagonal Architecture의 핵심 원칙을 준수합니다:

  1. Domain Layer → 외부 의존성 없음 (순수 비즈니스 로직)
  2. Application Layer → Domain Layer 의존
  3. Infrastructure Layer → Domain/Application의 Port 인터페이스 구현
  4. API Layer → Application Layer의 Service Port 호출
  5. Bootstrap → 모든 모듈 조합 및 실행
┌─────────────────────────────────────────────────────┐
│ bootstrap │
│ (Application Runner) │
└─────────────────────────────────────────────────────┘
 │ │
 ▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ user-api-* │───────▶│ user-application │
│ (Controllers) │ │ (Use Cases/Port) │
└──────────────────┘ └──────────────────────┘
 │
 ▼
 ┌──────────────────────┐
 │ user-domain │
 │ (Entities/VOs/Port) │
 └──────────────────────┘
 さんかく
 │ (implements)
 ┌──────────────────────┐
 │ user-infrastructure │
 │ (JPA/JWT/Redis) │
 └──────────────────────┘

실행 방법

1. 사전 요구사항

  • JDK 21 설치
  • Docker 설치 (PostgreSQL, Redis 실행용)
  • Git 설치

2. 프로젝트 클론

git clone <repository-url>
cd user-service

3. Docker Compose로 인프라 실행

Spring Boot의 Docker Compose 지원 기능을 사용하므로, 별도로 Docker Compose를 실행할 필요가 없습니다. 애플리케이션 실행 시 자동으로 PostgreSQL(Master/Slave)와 Redis 컨테이너가 시작됩니다.

만약 수동으로 실행하려면:

cd bootstrap
docker-compose -f compose.yaml up -d

컨테이너 확인:

docker ps

다음 컨테이너들이 실행되어야 합니다:

  • postgres-main (포트 5432) - Master DB
  • postgres-standby (포트 5433) - Slave DB (Read Replica)
  • redis (포트 6379) - Refresh Token 저장소

4. 애플리케이션 빌드

./gradlew clean build

5. 애플리케이션 실행

./gradlew :bootstrap:bootRun

또는 빌드된 JAR 파일 실행:

java -jar bootstrap/build/libs/bootstrap-0.0.1.jar

6. 애플리케이션 확인

  • 애플리케이션: http://localhost:8080
  • Swagger UI: http://localhost:8080/swagger-ui/index.html
  • API 문서: http://localhost:8080/v3/api-docs

7. 테스트 실행

전체 테스트:

./gradlew test

특정 모듈 테스트:

./gradlew :user-domain:test
./gradlew :user-application:test
./gradlew :bootstrap:test

특정 테스트 클래스:

./gradlew :bootstrap:test --tests "com.example.integration.AuthControllerIntegrationTest"

API 문서

Swagger UI를 통해 대화형 API 문서를 제공합니다: http://localhost:8080/swagger-ui/index.html

주요 엔드포인트

1. 인증 API (user-api-auth)

Method Endpoint Description Authentication
POST /signup 회원가입 불필요
POST /signin 로그인 (JWT 발급) 불필요

2. 회원 API (user-api-member)

Method Endpoint Description Authentication
GET /users/{userId} 사용자 정보 조회 필요 (본인/관리자)
PUT /users/{userId} 사용자 정보 수정 필요 (본인/관리자)
DELETE /users/{userId} 사용자 탈퇴 필요 (본인/관리자)

3. 관리자 API (user-api-admin)

Method Endpoint Description Authentication
GET /admin/users 전체 사용자 목록 조회 (페이징) 필요 (ADMIN만)

요청/응답 예시

POST /signup - 회원가입

Request Body:

{
 "email": "user@example.com",
 "password": "Password123!",
 "role": "MEMBER"
}

Response (201 Created):

{
 "status": true,
 "message": "회원가입이 완료되었습니다",
 "data": {
 "id": 1,
 "email": "user@example.com",
 "role": "MEMBER",
 "createdAt": "2025年10月13日T12:00:00"
 }
}
POST /signin - 로그인

Request Body:

{
 "email": "user@example.com",
 "password": "Password123!",
 "role": "MEMBER"
}

Response (200 OK):

{
 "status": true,
 "message": "로그인이 완료되었습니다",
 "data": {
 "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
 "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
 "tokenType": "Bearer",
 "accessTokenExpiresIn": 3600000,
 "refreshTokenExpiresIn": 604800000
 }
}
GET /users/{userId} - 사용자 조회

Headers:

Authorization: Bearer {accessToken}

Response (200 OK):

{
 "status": true,
 "message": "사용자 정보 조회 완료",
 "data": {
 "id": 1,
 "email": "user@example.com",
 "role": "MEMBER",
 "createdAt": "2025年10月13日T12:00:00",
 "updatedAt": "2025年10月13日T12:00:00"
 }
}
GET /admin/users - 전체 사용자 조회 (관리자)

Headers:

Authorization: Bearer {accessToken}

Query Parameters:

  • page: 페이지 번호 (0부터 시작, 기본값: 0)
  • size: 페이지 크기 (1-100, 기본값: 20)
  • sort: 정렬 옵션 (예: createdAt,desc 또는 email,asc)

Example Request:

GET /admin/users?page=0&size=20&sort=createdAt,desc

Response (200 OK):

{
 "status": true,
 "message": "전체 사용자 조회 완료",
 "data": {
 "content": [
 {
 "id": 1,
 "email": "user@example.com",
 "role": "MEMBER",
 "createdAt": "2025年10月13日T12:00:00"
 }
 ],
 "totalElements": 100,
 "totalPages": 5,
 "currentPage": 0,
 "pageSize": 20,
 "hasNext": true,
 "hasPrevious": false
 }
}

설계 결정 이유

1. Hexagonal Architecture 채택

선택 이유:

  • 비즈니스 로직의 독립성: 도메인 계층이 프레임워크(Spring)나 외부 라이브러리(JPA, Redis)에 의존하지 않아 비즈니스 로직 변경이 용이
  • 테스트 용이성: 인프라 구현체를 Mock으로 대체하여 단위 테스트 작성이 간편
  • 기술 스택 변경 유연성: 예를 들어, JPA를 MyBatis로 변경하거나 Redis를 Memcached로 교체해도 도메인 계층 수정 불필요
  • 팀 협업 효율: 도메인 전문가는 user-domain에, 인프라 전문가는 user-infrastructure에 집중 가능

구현 방식:

  • Port 인터페이스: 도메인/애플리케이션 계층에서 정의 (UserRepository, TokenProvider, RefreshTokenStore)
  • Adapter 구현체: 인프라 계층에서 실제 구현 (UserRepositoryImpl, JwtTokenProvider, RefreshTokenRedisRepository)

2. 멀티 모듈 구조

선택 이유:

  • 명확한 계층 분리: 각 모듈의 책임이 명확하여 코드 이해도 향상
  • 의존성 제어: Gradle 설정으로 잘못된 계층 간 참조 방지 (예: domain → infrastructure 참조 불가)
  • 독립적인 배포: 향후 마이크로서비스 전환 시 모듈별로 독립 배포 가능
  • 빌드 최적화: 변경된 모듈만 재빌드하여 빌드 시간 단축

3. PostgreSQL Master-Slave Replication

선택 이유:

  • 읽기 성능 향상: 읽기 쿼리를 Slave DB로 분산하여 Master DB 부하 감소
  • 고가용성: Master 장애 시 Slave를 Master로 승격 가능 (Failover)
  • 실무 반영: 실제 프로덕션 환경에서 자주 사용되는 아키텍처 학습

구현 방식:

  • ReplicationRoutingDataSource: Spring의 AbstractRoutingDataSource를 상속하여 트랜잭션 읽기/쓰기 속성에 따라 DataSource 라우팅
  • @Transactional(readOnly = true): Slave DB로 라우팅
  • @Transactional: Master DB로 라우팅

4. JWT 기반 인증

선택 이유:

  • Stateless: 서버에 세션 저장 불필요, 수평 확장(Scale-out) 용이
  • MSA 친화적: API Gateway와 결합하여 인증 정보 전달 간편
  • 모바일 앱 지원: 쿠키 기반 세션보다 모바일 환경에 적합

구현 방식:

  • Access Token: 짧은 유효기간 (1시간), 모든 API 요청에 포함
  • Refresh Token: 긴 유효기간 (7일), Redis에 저장하여 토큰 갱신 시 사용
  • JwtAuthenticationFilter: 모든 요청에서 JWT 검증 및 SecurityContext 설정

보안 고려사항:

  • Access Token은 stateless하지만, Refresh Token은 Redis에 저장하여 강제 로그아웃 구현 가능
  • 사용자 탈퇴 시 Redis에서 Refresh Token 삭제하여 즉시 인증 무효화

5. Value Object 패턴 (Email, Password)

선택 이유:

  • 도메인 규칙 캡슐화: 이메일 유효성 검증, 비밀번호 정책 검증을 Value Object 내부에 위치
  • 불변성(Immutability): 한 번 생성된 Email, Password는 변경 불가능하여 부수효과 방지
  • 재사용성: 여러 엔티티에서 동일한 검증 로직 공유

6. Role별 비밀번호 정책 (Strategy Pattern)

선택 이유:

  • 역할별 보안 강도 차별화: ADMIN은 더 강력한 비밀번호 요구 (12자 이상, 4종류 문자 모두 포함)
  • 확장성: 새로운 역할(예: SUPER_ADMIN) 추가 시 새로운 Policy 클래스만 추가
  • 단일 책임 원칙: 각 Policy 클래스는 하나의 역할에 대한 검증만 담당

구현 방식:

  • MemberPasswordPolicy: 8자 이상, 영문/숫자/특수문자 중 2가지 이상
  • AdminPasswordPolicy: 12자 이상, 영문(대소)/숫자/특수문자 모두 포함

7. Soft Delete 패턴

선택 이유:

  • 데이터 보존: 법적 요구사항(예: 개인정보보호법)을 위해 탈퇴 로그 유지
  • 복구 가능성: 사용자가 실수로 탈퇴한 경우 데이터 복구 가능
  • 참조 무결성 유지: 외래키 관계가 있는 다른 테이블의 데이터 보존

구현 방식:

  • User.delete(): 이메일과 비밀번호를 익명화하고 deletedAt 타임스탬프 설정
  • 익명화 규칙: 이메일은 deleted_{timestamp}@deleted.com, 비밀번호는 랜덤 해시

GDPR 준수:

  • 탈퇴 시 즉시 개인정보(이메일) 익명화하여 복구 불가능한 형태로 변환
  • deletedAt 인덱스를 활용하여 N일 후 물리 삭제하는 배치 작업 구현 가능

8. 동일 이메일, 다른 Role 가입 허용

선택 이유:

  • 유연한 권한 관리: 한 명의 사용자가 MEMBER와 ADMIN 역할을 동시에 가질 수 있음
  • 실무 요구사항 반영: 쇼핑몰 직원이 일반 회원으로도 쇼핑하고, 관리자로도 시스템 관리하는 경우

구현 방식:

  • 데이터베이스 스키마: (email, role_id) 복합 유니크 제약
  • JPA Repository: findByEmailAndRoleId() 메서드로 조회

주의사항:

  • 로그인 시 반드시 emailrole을 모두 입력받아야 함
  • 같은 이메일이라도 Role별로 다른 비밀번호 설정 가능

문제 해결 과정 및 고민

1. Master-Slave DB 라우팅 이슈

문제: 초기 구현 시 읽기 전용 트랜잭션에서도 Master DB로 연결되는 문제 발생.

원인: @Transactional(readOnly = true)가 선언되어도, Spring의 DataSourceTransactionManager가 트랜잭션 시작 시점에 DataSource를 결정하는데, 이때 TransactionSynchronizationManager.isCurrentTransactionReadOnly()false를 반환.

해결: ReplicationRoutingDataSource에서 determineCurrentLookupKey() 메서드를 오버라이드하여 트랜잭션 속성을 직접 확인:

override fun determineCurrentLookupKey(): Any {
 return if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) {
 DatabaseType.SLAVE
 } else {
 DatabaseType.MASTER
 }
}

검증: 통합 테스트에서 로그를 확인하여 읽기 쿼리가 postgres-standby:5433으로, 쓰기 쿼리가 postgres-main:5432로 가는지 확인.

2. Password Value Object의 BCrypt 해싱 시점

문제: Password Value Object를 생성할 때마다 BCrypt 해싱을 수행하면, 테스트에서 동일한 비밀번호를 여러 번 생성 시 해시값이 달라져 비교가 어려움.

해결 방안:

  1. Plain Text로 생성: Password.of(plainText, policy) - 새로운 비밀번호 입력 시 사용
  2. Hash로 생성: Password.fromHash(hash, changedAt) - DB에서 조회 시 사용
companion object {
 fun of(plainText: String, policy: PasswordPolicy): Password {
 policy.validate(plainText)
 val hash = BCrypt.hashpw(plainText, BCrypt.gensalt())
 return Password(hash, LocalDateTime.now())
 }
 fun fromHash(hash: String, changedAt: LocalDateTime): Password {
 return Password(hash, changedAt)
 }
}

3. JPA Entity의 Kotlin Data Class 사용 불가

문제: Kotlin의 data classequals(), hashCode(), toString()을 자동 생성하지만, JPA Entity에 사용 시 문제 발생:

  • 기본 생성자 필요
  • Lazy Loading 프록시 객체와 equals() 비교 시 무한 재귀 호출

해결:

  • Entity는 일반 class로 선언
  • equals()hashCode()를 ID 기반으로 직접 구현
  • allOpen 플러그인으로 @Entity 클래스를 자동으로 open 처리 (Hibernate 프록시 지원)
@Entity
class User(...) {
 override fun equals(other: Any?): Boolean {
 if (this === other) return true
 if (other !is User) return false
 return id == other.id
 }
 override fun hashCode(): Int = id?.hashCode() ?: 0
}

4. Refresh Token 저장소 선택 (Redis vs DB)

고민:

  • DB 저장: 영속성 보장, 복잡한 쿼리 가능
  • Redis 저장: 빠른 조회, TTL 자동 만료

선택: Redis

이유:

  • Refresh Token은 임시 데이터이므로 영속성 필요 없음
  • TTL 기능으로 만료된 토큰 자동 삭제 (Garbage Collection 불필요)
  • 로그인 요청 시 빠른 조회 성능

트레이드오프:

  • Redis 장애 시 모든 사용자 재로그인 필요 (해결: 추후 Redis Sentinel/Cluster로 고가용성 확보)

5. 권한 체크 로직의 중복 제거

문제: 여러 컨트롤러에서 "본인 또는 관리자만 접근 가능" 로직이 반복됨.

해결: AuthorizationChecker 유틸리티 클래스 생성

@Component
class AuthorizationChecker {
 fun checkResourceAccess(targetUserId: Long) {
 val currentUser = getCurrentUser()
 if (!currentUser.hasRole("ADMIN") && currentUser.id != targetUserId) {
 throw AccessDeniedException("접근 권한이 없습니다")
 }
 }
}

6. 통합 테스트의 데이터 격리

문제: @SpringBootTest를 사용한 통합 테스트에서 테스트 간 데이터 충돌 발생.

해결:

  1. @Transactional 적용: 각 테스트 메서드 실행 후 자동 롤백
  2. 랜덤 이메일 생성: randomEmail() 메서드로 충돌 방지
@SpringBootTest
@Transactional
class AuthControllerIntegrationTest {
 fun randomEmail(): String {
 return "test-${System.currentTimeMillis()}@example.com"
 }
}

7. 성능 최적화: N+1 문제 방지

문제: User 조회 시 연관된 Role 엔티티를 Lazy Loading으로 조회하면 N+1 쿼리 발생.

해결:

  • @ManyToOne(fetch = FetchType.EAGER) 적용
  • 또는 JPQL로 JOIN FETCH 사용

향후 개선 방안:

  • 대량 조회 시 JPQL JOIN FETCH 또는 EntityGraph 사용
  • 캐싱 전략 적용 (Spring Cache + Redis)

비동기 처리

구현 개요

사용자 탈퇴 시 발생하는 부가 작업(이메일 발송, 파일 삭제 등)을 비동기로 처리하여 API 응답 시간을 단축합니다.

구현 기술

  • Spring @Async: 비동기 메서드 실행
  • @TransactionalEventListener: 트랜잭션 커밋 후 이벤트 발행
  • ThreadPoolTaskExecutor: 커스텀 스레드 풀

코드 구현

1. 비동기 설정

@Configuration
@EnableAsync
class AsyncConfig {
 @Bean
 fun asyncExecutor(): Executor {
 return ThreadPoolTaskExecutor().apply {
 corePoolSize = 2
 maxPoolSize = 5
 queueCapacity = 100
 setThreadNamePrefix("async-")
 initialize()
 }
 }
}

2. 이벤트 리스너

@Component
class UserEventListener {
 @Async
 @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
 fun handleUserDeletedEvent(event: UserDeletedEvent) {
 val data = event.source as UserDeletedEventData
 // 1. 탈퇴 확인 이메일 발송
 sendWithdrawalConfirmationEmail(data)
 // 2. 사용자 업로드 파일 삭제
 deleteUserFiles(data)
 // 3. 관련 데이터 정리
 cleanupRelatedData(data)
 }
}

3. 이벤트 발행

@Transactional
override fun deleteUser(userId: Long) {
 val user = userRepository.findById(userId)
 val eventData = UserDeletedEventData(user.id, user.email.getValue())
 // 1. 사용자 정보 익명화
 user.delete()
 // 2. Refresh Token 삭제
 refreshTokenStore.deleteByUserId(userId)
 // 3. DB에서 삭제
 userRepository.delete(user)
 // 4. 비동기 이벤트 발행 (트랜잭션 커밋 후 실행)
 eventPublisher.publishEvent(UserDeletedEvent(eventData))
}

비동기 처리 선택 이유

장점:

  1. 응답 시간 단축: 사용자는 탈퇴 요청 후 즉시 응답 받음 (부가 작업 대기 불필요)
  2. 트랜잭션 분리: 이메일 발송 실패 시에도 사용자 탈퇴는 완료됨 (부분 실패 허용)
  3. 확장성: 향후 외부 API 호출(예: 결제 취소)이 추가되어도 성능 저하 없음

트레이드오프:

  • 비동기 작업 실패 시 재시도 로직 필요 (현재는 로그만 기록)

기술 선택 이유

Spring @Async vs 메시지 큐 (RabbitMQ/Kafka):

기준 Spring @Async 메시지 큐
구현 복잡도 낮음 (애너테이션 한 줄) 높음 (별도 인프라)
메시지 영속성 없음 (재시작 시 손실) 있음 (Disk 저장)
재시도 로직 직접 구현 필요 Built-in 지원
확장성 단일 서버 제한 다중 서버 분산

선택: Spring @Async

이유:

  • 현재 요구사항(이메일 발송, 파일 삭제)은 중요도가 낮아 영속성 불필요
  • 초기 개발 속도 및 인프라 비용 고려
  • 향후 요구사항 변경 시 메시지 큐로 마이그레이션 가능 (인터페이스는 동일)

테스트 전략

테스트 피라미드 구조

 ┌─────────────┐
 │ Integration │ ← 적음 (End-to-End)
 │ Tests │
 └─────────────┘
 ┌───────────────────┐
 │ Service Tests │ ← 중간 (Business Logic)
 └───────────────────┘
 ┌────────────────────────┐
 │ Unit Tests │ ← 많음 (Domain Objects)
 └────────────────────────┘

1. 단위 테스트 (Unit Tests)

목적: 도메인 객체(Entity, Value Object)와 서비스 로직의 정확성 검증

범위:

  • user-domain: Entity, Value Object, 도메인 로직
  • user-application: Service 메서드 (Mock 사용)

특징:

  • 외부 의존성(DB, Redis) 없음
  • Mockito-Kotlin으로 의존성 Mocking
  • 빠른 실행 속도 (<100ms per test)

예시 테스트:

  • EmailTest: 이메일 유효성 검증
  • PasswordTest: 비밀번호 정책 검증 (MEMBER/ADMIN)
  • UserTest: User 엔티티의 비즈니스 로직 검증 (탈퇴 처리 등)
  • UserServiceImplTest: 회원가입/로그인 로직 검증

2. 통합 테스트 (Integration Tests)

목적: API 엔드포인트와 전체 플로우 검증 (Controller → Service → Repository → DB)

범위:

  • bootstrap: API 엔드포인트 테스트
  • 실제 DB 연결 (Docker Compose 자동 실행)
  • Spring Security 인증/인가 검증

특징:

  • @SpringBootTest: 전체 ApplicationContext 로드
  • MockMvc: HTTP 요청/응답 시뮬레이션
  • @Transactional: 각 테스트 후 자동 롤백

주요 테스트 시나리오:

AuthControllerIntegrationTest

  • 회원가입 성공/실패 (MEMBER/ADMIN, 중복 이메일, 비밀번호 정책)
  • 로그인 성공/실패 (JWT 토큰 발급, 잘못된 비밀번호)
  • 동일 이메일로 다른 역할 가입 가능

MemberControllerIntegrationTest

  • 사용자 정보 조회/수정/삭제 (본인/관리자 권한)
  • 권한 없는 접근 거부 (403 Forbidden)
  • Soft Delete 검증

AdminControllerIntegrationTest

  • 전체 사용자 조회 (페이징, 정렬)
  • ADMIN 권한 검증 (MEMBER는 접근 불가)

3. 인프라 테스트

목적: 외부 시스템(Redis, JWT) 연동 검증

예시:

  • RefreshTokenRedisRepositoryTest: Redis CRUD 및 TTL 검증
  • JwtTokenProviderTest: JWT 생성/검증 로직 테스트

4. 아키텍처 테스트 (ArchUnit)

목적: 의존성 규칙 및 레이어 아키텍처 준수 검증

예시:

@Test
fun `도메인 계층은 인프라 계층에 의존하지 않아야 함`() {
 noClasses()
 .that().resideInAPackage("..userdomain..")
 .should().dependOnClassesThat().resideInAPackage("..userinfrastructure..")
 .check(importedClasses)
}

테스트 커버리지 목표

계층 목표 커버리지
Domain (Entity/VO) 90%+
Application (Service) 80%+
Infrastructure 70%+
API (Controller) 80%+

테스트 실행 방법

# 전체 테스트 실행
./gradlew test
# 특정 모듈 테스트
./gradlew :user-domain:test
./gradlew :user-application:test
# 커버리지 리포트 생성
./gradlew test jacocoTestReport

AI 사용 내역

과제 수행 중 AI(Claude Code)를 활용한 부분을 투명하게 공개합니다.

1. 테스트 코드 생성

위치: bootstrap/src/test/kotlin/com/example/integration/AuthControllerIntegrationTest.kt

사용한 프롬프트:

통합 테스트 코드를 작성해줘.

생성된 코드:

  • AuthControllerIntegrationTest.kt: 회원가입/로그인 API 테스트 (라인 3-4에 AI 사용 주석)
  • MemberControllerIntegrationTest.kt: 회원 관리 API 테스트
  • AdminControllerIntegrationTest.kt: 관리자 API 테스트

수정 사항:

  • AI가 생성한 기본 구조를 유지하되, 엣지 케이스(비밀번호 정책 위반, 역할 불일치 등) 테스트 추가
  • @BeforeEach에서 공통 사용자 생성 로직을 createUserAndGetToken() 메서드로 리팩토링
  • HTTP 상태 코드를 400 Bad Request에서 409 Conflict로 수정 (중복 이메일 오류)

2. Swagger 애너테이션 추가

위치: user-api/*/src/main/kotlin/**/*Controller.kt

사용한 프롬프트:

컨트롤러에 Swagger 애너테이션을 추가해줘.

생성된 코드:

@Tag(name = "인증 API", description = "회원가입, 로그인 등 인증 관련 API")
@Operation(summary = "회원가입", description = "새로운 사용자를 등록합니다")

수정 사항: 없음 (그대로 사용)

3. 인증 / 인가 방식에 대한 JWT / 세션 / OAuth2 등 다양한 인증 방식을 추상화

위치: user-infrastructure-auth/src/main/kotlin/com/example/infrastructure/auth/AuthorizationChecker.kt 사용한 프롬프트:

프롬프트: 인증 / 인가 사용했을때 JWT / 세션 / OAuth2 등 다양한 인증 방식을 추상화가 필요할거 같아

생성된 코드:

  • AuthorizationChecker.kt: 현재 사용자 정보 조회 및 권한 체크 유틸리티 클래스
  • AuthorizationException.kt: 권한 예외 클래스
  • AuthenticatedUser.kt: 현재 인증된 사용자 정보 DTO
  • AuthenticationContext.kt: SecurityContext에서 인증 정보 추출 유틸리티

About

유저 서비스

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

Contributors

Languages

AltStyle によって変換されたページ (->オリジナル) /