TA 역할 Spring 경계 smoke #7
9 changed files with 431 additions and 0 deletions
|
|
@ -0,0 +1,3 @@
|
||||||
|
# role-ta-live-1522-001-attempt-3-run-12d03ada6afe
|
||||||
|
|
||||||
|
Forge 이슈 작업 브랜치 `forge/role-ta-live-1522-001-attempt-3-run-12d03ada6afe`.
|
||||||
131
docs/adr/ADR-001-spring-architecture-boundaries.md
Normal file
131
docs/adr/ADR-001-spring-architecture-boundaries.md
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
# ADR-001: Spring Architecture Boundaries
|
||||||
|
|
||||||
|
## Status
|
||||||
|
Accepted
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
### 프로젝트 배경
|
||||||
|
runtime-role-matrix-live-202607141522 프로젝트는 역할 기반 접근 제어(RBAC) 매트릭스를 실시간으로 관리하는 시스템이다. Spring Boot 기반으로 구축되며, 다중 클라이언트 환경에서 일관된 아키텍처 패턴이 필요하다.
|
||||||
|
|
||||||
|
### 문제 정의
|
||||||
|
1. **Controller-Service-Repository 경계 모호**: 각 계층의 책임이 명확하지 않아 코드의 응집도 감소 및 결합도 증가
|
||||||
|
2. **오류 계약 부재**: 예외 처리 전략이 통일되지 않아 일관되지 않은 API 응답 생성
|
||||||
|
3. **트랜잭션 경계 불명확**: 서비스 계층에서 트랜잭션 관리 방식이 표준화되지 않아 데이터 일관성 위험
|
||||||
|
|
||||||
|
### 기술 환경
|
||||||
|
- Spring Boot 3.2.x / Java 17 / Jakarta EE 10
|
||||||
|
- Spring Data JPA / Spring Web (REST API)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### 1. Controller-Service-Repository 경계 정의
|
||||||
|
|
||||||
|
| 계층 | 책임 | 금지 사항 |
|
||||||
|
|------|------|----------|
|
||||||
|
| **Controller** | HTTP 요청/응답, 입력 검증(@Valid), ResponseEntity 반환 | 비즈니스 로직, DB 접근, @Transactional |
|
||||||
|
| **Service** | 비즈니스 로직, @Transactional 관리, 도메인 객체 조작 | HttpServletRequest/Response 접근, 응답 형식 직접 생성 |
|
||||||
|
| **Repository** | DB 접근, 쿼리 실행, Entity 관리 | 비즈니스 로직, Service 호출 |
|
||||||
|
|
||||||
|
```java
|
||||||
|
// Controller 예시
|
||||||
|
@RestController @RequiredArgsConstructor
|
||||||
|
public class RoleMatrixController {
|
||||||
|
private final RoleMatrixService service;
|
||||||
|
@PostMapping @Valid @RequestBody RoleCreateRequest req
|
||||||
|
public ResponseEntity<ApiResponse<RoleResponse>> createRole(req) {
|
||||||
|
return ResponseEntity.status(CREATED).body(ApiResponse.success(service.createRole(req)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Service 예시
|
||||||
|
@Service @RequiredArgsConstructor @Transactional(readOnly = true)
|
||||||
|
public class RoleMatrixService {
|
||||||
|
private final RoleRepository roleRepository;
|
||||||
|
@Transactional public RoleResponse createRole(RoleCreateRequest req) {
|
||||||
|
Role role = Role.create(req.getName(), req.getDescription());
|
||||||
|
return RoleResponse.from(roleRepository.save(role));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Repository 예시
|
||||||
|
@Repository
|
||||||
|
public interface RoleRepository extends JpaRepository<Role, Long> {
|
||||||
|
Optional<Role> findByName(String name);
|
||||||
|
@Query("SELECT r FROM Role r LEFT JOIN FETCH r.permissions WHERE r.id = :id")
|
||||||
|
Optional<Role> findByIdWithPermissions(@Param("id") Long id);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 오류 계약 정의
|
||||||
|
|
||||||
|
**예외 계층**: `BaseException` → `BusinessException` / `SystemException`
|
||||||
|
|
||||||
|
**오류 응답 표준 형식**:
|
||||||
|
```json
|
||||||
|
{"success": false, "error": {"code": "ROLE_NOT_FOUND", "message": "요청한 역할을 찾을 수 없습니다."}, "timestamp": "..."}
|
||||||
|
```
|
||||||
|
|
||||||
|
| 예외 유형 | HTTP 상태 | 용도 |
|
||||||
|
|----------|-----------|------|
|
||||||
|
| BusinessException | 4xx | 클라이언트 오류 (not found, duplicate, denied) |
|
||||||
|
| SystemException | 5xx | 서버 오류 (database, external service) |
|
||||||
|
|
||||||
|
```java
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
@ExceptionHandler(BusinessException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleBusiness(BusinessException ex) {
|
||||||
|
return ResponseEntity.status(ex.getErrorCode().getHttpStatus())
|
||||||
|
.body(ApiResponse.error(ex.getErrorCode()));
|
||||||
|
}
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleGeneric(Exception ex) {
|
||||||
|
return ResponseEntity.status(INTERNAL_SERVER_ERROR)
|
||||||
|
.body(ApiResponse.error(ErrorCode.INTERNAL_SERVER_ERROR));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 트랜잭션 경계 정의
|
||||||
|
|
||||||
|
| 상황 | 설정 | 설명 |
|
||||||
|
|------|------|------|
|
||||||
|
| 기본 읽기 | `@Transactional(readOnly = true)` | 성능 최적화, Dirty Checking 비활성화 |
|
||||||
|
| 쓰기 작업 | `@Transactional` | 변경 감지 활성화 |
|
||||||
|
| 전파 정책 | REQUIRED (기본) | 기존 트랜잭션 참여 또는 신규 생성 |
|
||||||
|
| 격리 수준 | READ_COMMITTED | 기본값 |
|
||||||
|
| 롤백 조건 | unchecked exception | RuntimeException 자동 롤백 |
|
||||||
|
|
||||||
|
**규칙**: 트랜잭션 시작/종료점은 Service Layer public 메서드. 다중 Repository 호출 시同一 트랜잭션에서 실행.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Alternatives
|
||||||
|
|
||||||
|
| 대안 | 장점 | 단점 | 미선택 이유 |
|
||||||
|
|------|------|------|------------|
|
||||||
|
| Transaction Script | 단순, 직관적 | 복잡도 증가 시 유지보수 어려움 | 확장성 부족 |
|
||||||
|
| DDD 패턴 | 복잡 도메인 캡슐화 | 학습 곡선 높음, 과도한 추상화 | 현재 규모에서 과도한 복잡성 |
|
||||||
|
| Checked Exception | 컴파일 타임 강제 | 코드 복잡도 증가, 트랜잭션 충돌 | Spring 예외 처리와 불일치 |
|
||||||
|
| Repository DTO 반환 | 즉시 변환 가능 | 계층 결합, 테스트 어려움 | 책임 분리 위반 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
### 긍정적 결과
|
||||||
|
- **명확한 책임 분리**: 계층별 집중으로 가독성 향상
|
||||||
|
- **일관된 오류 처리**: 표준화된 예외/응답으로 API 일관성 확보
|
||||||
|
- **테스트 용이성**: 계층별 Mock 가능
|
||||||
|
- **트랜잭션 안전성**: 명확한 경계로 데이터 일관성 보장
|
||||||
|
|
||||||
|
### 부정적 결과
|
||||||
|
- **초기 설정 비용**: 예외 계층, DTO 등 부가 코드 증가
|
||||||
|
- **추가 추상화**: 간단 CRUD도 Service 경유 필요
|
||||||
|
|
||||||
|
### 재검토 조건
|
||||||
|
- 도메인 복잡도大幅 증가 시 DDD 패턴 도입 검토
|
||||||
|
- 성능 병목 발생 시 쿼리 최적화
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
package com.klaroworks.runtime.controller;
|
||||||
|
|
||||||
|
import com.klaroworks.runtime.dto.ApiResponse;
|
||||||
|
import com.klaroworks.runtime.dto.RoleCreateRequest;
|
||||||
|
import com.klaroworks.runtime.dto.RoleResponse;
|
||||||
|
import com.klaroworks.runtime.service.RoleMatrixService;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/roles")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class RoleMatrixController {
|
||||||
|
|
||||||
|
private final RoleMatrixService roleMatrixService;
|
||||||
|
|
||||||
|
@PostMapping
|
||||||
|
public ResponseEntity<ApiResponse<RoleResponse>> createRole(
|
||||||
|
@Valid @RequestBody RoleCreateRequest request) {
|
||||||
|
return ResponseEntity.status(HttpStatus.CREATED)
|
||||||
|
.body(ApiResponse.success(roleMatrixService.createRole(request)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}")
|
||||||
|
public ResponseEntity<ApiResponse<RoleResponse>> getRoleById(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(roleMatrixService.getRoleById(id)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping
|
||||||
|
public ResponseEntity<ApiResponse<List<RoleResponse>>> getAllRoles() {
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(roleMatrixService.getAllRoles()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/{id}")
|
||||||
|
public ResponseEntity<ApiResponse<RoleResponse>> updateRole(
|
||||||
|
@PathVariable Long id,
|
||||||
|
@Valid @RequestBody RoleCreateRequest request) {
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(roleMatrixService.updateRole(id, request)));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}")
|
||||||
|
public ResponseEntity<Void> deleteRole(@PathVariable Long id) {
|
||||||
|
roleMatrixService.deleteRole(id);
|
||||||
|
return ResponseEntity.noContent().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/main/java/com/klaroworks/runtime/dto/ApiResponse.java
Normal file
39
src/main/java/com/klaroworks/runtime/dto/ApiResponse.java
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
package com.klaroworks.runtime.dto;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonInclude;
|
||||||
|
import com.klaroworks.runtime.exception.ErrorCode;
|
||||||
|
import lombok.*;
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
@Data @Builder @NoArgsConstructor @AllArgsConstructor
|
||||||
|
@JsonInclude(JsonInclude.Include.NON_NULL)
|
||||||
|
public class ApiResponse<T> {
|
||||||
|
private boolean success;
|
||||||
|
private T data;
|
||||||
|
private ErrorInfo error;
|
||||||
|
private String timestamp;
|
||||||
|
|
||||||
|
@Data @Builder @NoArgsConstructor @AllArgsConstructor
|
||||||
|
public static class ErrorInfo {
|
||||||
|
private String code;
|
||||||
|
private String message;
|
||||||
|
private Object details;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> success(T data) {
|
||||||
|
return ApiResponse.<T>builder().success(true).data(data)
|
||||||
|
.timestamp(Instant.now().toString()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> error(ErrorCode code) {
|
||||||
|
return ApiResponse.<T>builder().success(false)
|
||||||
|
.error(ErrorInfo.builder().code(code.getCode()).message(code.getMessage()).build())
|
||||||
|
.timestamp(Instant.now().toString()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <T> ApiResponse<T> error(ErrorCode code, Object details) {
|
||||||
|
return ApiResponse.<T>builder().success(false)
|
||||||
|
.error(ErrorInfo.builder().code(code.getCode()).message(code.getMessage()).details(details).build())
|
||||||
|
.timestamp(Instant.now().toString()).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
package com.klaroworks.runtime.exception;
|
||||||
|
|
||||||
|
public abstract class BaseException extends RuntimeException {
|
||||||
|
private final ErrorCode errorCode;
|
||||||
|
|
||||||
|
protected BaseException(ErrorCode errorCode) {
|
||||||
|
super(errorCode.getMessage());
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected BaseException(ErrorCode errorCode, String message) {
|
||||||
|
super(message);
|
||||||
|
this.errorCode = errorCode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ErrorCode getErrorCode() { return errorCode; }
|
||||||
|
}
|
||||||
|
|
||||||
|
class BusinessException extends BaseException {
|
||||||
|
public BusinessException(ErrorCode errorCode) { super(errorCode); }
|
||||||
|
public BusinessException(ErrorCode errorCode, String message) { super(errorCode, message); }
|
||||||
|
}
|
||||||
|
|
||||||
|
class RoleNotFoundException extends BusinessException {
|
||||||
|
public RoleNotFoundException(Long roleId) {
|
||||||
|
super(ErrorCode.ROLE_NOT_FOUND, String.format("Role not found with id: %d", roleId));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DuplicateResourceException extends BusinessException {
|
||||||
|
public DuplicateResourceException(String resourceType, String identifier) {
|
||||||
|
super(ErrorCode.DUPLICATE_ROLE_NAME, String.format("%s '%s' already exists", resourceType, identifier));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package com.klaroworks.runtime.exception;
|
||||||
|
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
||||||
|
public enum ErrorCode {
|
||||||
|
INTERNAL_SERVER_ERROR("COMMON_001", "내부 서버 오류가 발생했습니다.", HttpStatus.INTERNAL_SERVER_ERROR),
|
||||||
|
INVALID_REQUEST("COMMON_002", "잘못된 요청입니다.", HttpStatus.BAD_REQUEST),
|
||||||
|
ROLE_NOT_FOUND("ROLE_001", "요청한 역할을 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
|
||||||
|
DUPLICATE_ROLE_NAME("ROLE_002", "이미 존재하는 역할 이름입니다.", HttpStatus.CONFLICT),
|
||||||
|
PERMISSION_NOT_FOUND("PERM_001", "요청한 권한을 찾을 수 없습니다.", HttpStatus.NOT_FOUND),
|
||||||
|
PERMISSION_DENIED("PERM_002", "해당 작업에 대한 권한이 없습니다.", HttpStatus.FORBIDDEN);
|
||||||
|
|
||||||
|
private final String code;
|
||||||
|
private final String message;
|
||||||
|
private final HttpStatus httpStatus;
|
||||||
|
|
||||||
|
ErrorCode(String code, String message, HttpStatus httpStatus) {
|
||||||
|
this.code = code;
|
||||||
|
this.message = message;
|
||||||
|
this.httpStatus = httpStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCode() { return code; }
|
||||||
|
public String getMessage() { return message; }
|
||||||
|
public HttpStatus getHttpStatus() { return httpStatus; }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
package com.klaroworks.runtime.exception;
|
||||||
|
|
||||||
|
import com.klaroworks.runtime.dto.ApiResponse;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(BusinessException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException ex) {
|
||||||
|
log.warn("Business exception: {}", ex.getErrorCode().getCode());
|
||||||
|
return ResponseEntity.status(ex.getErrorCode().getHttpStatus())
|
||||||
|
.body(ApiResponse.error(ex.getErrorCode()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleValidationException(MethodArgumentNotValidException ex) {
|
||||||
|
var errors = ex.getBindingResult().getFieldErrors().stream()
|
||||||
|
.collect(java.util.stream.Collectors.toMap(
|
||||||
|
e -> e.getField(), e -> e.getDefaultMessage()));
|
||||||
|
log.warn("Validation failed: {}", errors);
|
||||||
|
return ResponseEntity.badRequest()
|
||||||
|
.body(ApiResponse.error(ErrorCode.INVALID_REQUEST, errors));
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<ApiResponse<Void>> handleGenericException(Exception ex) {
|
||||||
|
log.error("Unexpected error", ex);
|
||||||
|
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||||
|
.body(ApiResponse.error(ErrorCode.INTERNAL_SERVER_ERROR));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
package com.klaroworks.runtime.service;
|
||||||
|
|
||||||
|
import com.klaroworks.runtime.dto.RoleCreateRequest;
|
||||||
|
import com.klaroworks.runtime.dto.RoleResponse;
|
||||||
|
import com.klaroworks.runtime.entity.Role;
|
||||||
|
import com.klaroworks.runtime.exception.DuplicateResourceException;
|
||||||
|
import com.klaroworks.runtime.exception.RoleNotFoundException;
|
||||||
|
import com.klaroworks.runtime.repository.RoleRepository;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public class RoleMatrixService {
|
||||||
|
|
||||||
|
private final RoleRepository roleRepository;
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public RoleResponse createRole(RoleCreateRequest request) {
|
||||||
|
if (roleRepository.existsByName(request.getName())) {
|
||||||
|
throw new DuplicateResourceException("Role", request.getName());
|
||||||
|
}
|
||||||
|
Role role = Role.create(request.getName(), request.getDescription());
|
||||||
|
return RoleResponse.from(roleRepository.save(role));
|
||||||
|
}
|
||||||
|
|
||||||
|
public RoleResponse getRoleById(Long id) {
|
||||||
|
return roleRepository.findById(id)
|
||||||
|
.map(RoleResponse::from)
|
||||||
|
.orElseThrow(() -> new RoleNotFoundException(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RoleResponse> getAllRoles() {
|
||||||
|
return roleRepository.findAll().stream().map(RoleResponse::from).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public RoleResponse updateRole(Long id, RoleCreateRequest request) {
|
||||||
|
Role role = roleRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new RoleNotFoundException(id));
|
||||||
|
role.update(request.getName(), request.getDescription());
|
||||||
|
return RoleResponse.from(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void deleteRole(Long id) {
|
||||||
|
if (!roleRepository.existsById(id)) throw new RoleNotFoundException(id);
|
||||||
|
roleRepository.deleteById(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,58 @@
|
||||||
|
package com.klaroworks.runtime.service;
|
||||||
|
|
||||||
|
import com.klaroworks.runtime.dto.RoleCreateRequest;
|
||||||
|
import com.klaroworks.runtime.dto.RoleResponse;
|
||||||
|
import com.klaroworks.runtime.entity.Role;
|
||||||
|
import com.klaroworks.runtime.exception.RoleNotFoundException;
|
||||||
|
import com.klaroworks.runtime.repository.RoleRepository;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.InjectMocks;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.*;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class RoleMatrixServiceTest {
|
||||||
|
|
||||||
|
@Mock private RoleRepository roleRepository;
|
||||||
|
@InjectMocks private RoleMatrixService service;
|
||||||
|
private Role testRole;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
testRole = Role.builder().id(1L).name("ADMIN").description("Administrator").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("역할 생성 시 save 호출 및 응답 반환")
|
||||||
|
void createRole_callsSaveAndReturnsResponse() {
|
||||||
|
when(roleRepository.save(any(Role.class))).thenReturn(testRole);
|
||||||
|
RoleResponse response = service.createRole(new RoleCreateRequest("ADMIN", "Administrator"));
|
||||||
|
assertNotNull(response);
|
||||||
|
assertEquals("ADMIN", response.getName());
|
||||||
|
verify(roleRepository, times(1)).save(any(Role.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("존재하지 않는 역할 조회 시 RoleNotFoundException 발생")
|
||||||
|
void getRoleById_whenNotFound_throwsException() {
|
||||||
|
when(roleRepository.findById(999L)).thenReturn(Optional.empty());
|
||||||
|
assertThrows(RoleNotFoundException.class, () -> service.getRoleById(999L));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("모든 역할 조회 시 Role 목록 반환")
|
||||||
|
void getAllRoles_returnsListOfRoles() {
|
||||||
|
when(roleRepository.findAll()).thenReturn(List.of(testRole));
|
||||||
|
List<RoleResponse> roles = service.getAllRoles();
|
||||||
|
assertEquals(1, roles.size());
|
||||||
|
assertEquals("ADMIN", roles.get(0).getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue