Skip to content

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-13T12: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-13T12:00:00",
    "updatedAt": "2025-10-13T12: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-13T12: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

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages