From fcc6fed9a0707e123d09d8dac53e019ae50ec464 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Tue, 14 Jul 2026 06:52:00 +0000 Subject: [PATCH 1/9] forge: open work branch for role-ta-live-1522-001-attempt-3-run-12d03ada6afe --- .forge/role-ta-live-1522-001-attempt-3-run-12d03ada6afe.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .forge/role-ta-live-1522-001-attempt-3-run-12d03ada6afe.md diff --git a/.forge/role-ta-live-1522-001-attempt-3-run-12d03ada6afe.md b/.forge/role-ta-live-1522-001-attempt-3-run-12d03ada6afe.md new file mode 100644 index 0000000..a96892b --- /dev/null +++ b/.forge/role-ta-live-1522-001-attempt-3-run-12d03ada6afe.md @@ -0,0 +1,3 @@ +# role-ta-live-1522-001-attempt-3-run-12d03ada6afe + +Forge 이슈 작업 브랜치 `forge/role-ta-live-1522-001-attempt-3-run-12d03ada6afe`. -- 2.49.1 From be9404f26d9a964cbba528c203c2fef6dcd447db Mon Sep 17 00:00:00 2001 From: forge-bot Date: Tue, 14 Jul 2026 06:58:53 +0000 Subject: [PATCH 2/9] =?UTF-8?q?TA=20=EC=97=AD=ED=95=A0=20Spring=20?= =?UTF-8?q?=EA=B2=BD=EA=B3=84=20smoke=20(role-ta-live-1522-001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ADR-001-spring-architecture-boundaries.md | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 docs/adr/ADR-001-spring-architecture-boundaries.md diff --git a/docs/adr/ADR-001-spring-architecture-boundaries.md b/docs/adr/ADR-001-spring-architecture-boundaries.md new file mode 100644 index 0000000..be26926 --- /dev/null +++ b/docs/adr/ADR-001-spring-architecture-boundaries.md @@ -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> 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 { + Optional findByName(String name); + @Query("SELECT r FROM Role r LEFT JOIN FETCH r.permissions WHERE r.id = :id") + Optional 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> handleBusiness(BusinessException ex) { + return ResponseEntity.status(ex.getErrorCode().getHttpStatus()) + .body(ApiResponse.error(ex.getErrorCode())); + } + @ExceptionHandler(Exception.class) + public ResponseEntity> 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 패턴 도입 검토 +- 성능 병목 발생 시 쿼리 최적화 -- 2.49.1 From 63ed044c81cf1a210e310c52b3970da721559232 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Tue, 14 Jul 2026 06:58:54 +0000 Subject: [PATCH 3/9] =?UTF-8?q?TA=20=EC=97=AD=ED=95=A0=20Spring=20?= =?UTF-8?q?=EA=B2=BD=EA=B3=84=20smoke=20(role-ta-live-1522-001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../runtime/exception/ErrorCode.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/main/java/com/klaroworks/runtime/exception/ErrorCode.java diff --git a/src/main/java/com/klaroworks/runtime/exception/ErrorCode.java b/src/main/java/com/klaroworks/runtime/exception/ErrorCode.java new file mode 100644 index 0000000..850b044 --- /dev/null +++ b/src/main/java/com/klaroworks/runtime/exception/ErrorCode.java @@ -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; } +} -- 2.49.1 From 9a9dd78c1a5bbb9565044ffe364ed70f598ae306 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Tue, 14 Jul 2026 06:58:55 +0000 Subject: [PATCH 4/9] =?UTF-8?q?TA=20=EC=97=AD=ED=95=A0=20Spring=20?= =?UTF-8?q?=EA=B2=BD=EA=B3=84=20smoke=20(role-ta-live-1522-001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../runtime/exception/BaseException.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/main/java/com/klaroworks/runtime/exception/BaseException.java diff --git a/src/main/java/com/klaroworks/runtime/exception/BaseException.java b/src/main/java/com/klaroworks/runtime/exception/BaseException.java new file mode 100644 index 0000000..f7202af --- /dev/null +++ b/src/main/java/com/klaroworks/runtime/exception/BaseException.java @@ -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)); + } +} -- 2.49.1 From a0b93636c99b66edb6364ee1dcf5cf10a7e7dd42 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Tue, 14 Jul 2026 06:58:57 +0000 Subject: [PATCH 5/9] =?UTF-8?q?TA=20=EC=97=AD=ED=95=A0=20Spring=20?= =?UTF-8?q?=EA=B2=BD=EA=B3=84=20smoke=20(role-ta-live-1522-001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/GlobalExceptionHandler.java | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/main/java/com/klaroworks/runtime/exception/GlobalExceptionHandler.java diff --git a/src/main/java/com/klaroworks/runtime/exception/GlobalExceptionHandler.java b/src/main/java/com/klaroworks/runtime/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000..7ac66ac --- /dev/null +++ b/src/main/java/com/klaroworks/runtime/exception/GlobalExceptionHandler.java @@ -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> 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> 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> handleGenericException(Exception ex) { + log.error("Unexpected error", ex); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) + .body(ApiResponse.error(ErrorCode.INTERNAL_SERVER_ERROR)); + } +} -- 2.49.1 From 803a09d6ffcf696986957f54d9fb48563f22bf84 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Tue, 14 Jul 2026 06:58:58 +0000 Subject: [PATCH 6/9] =?UTF-8?q?TA=20=EC=97=AD=ED=95=A0=20Spring=20?= =?UTF-8?q?=EA=B2=BD=EA=B3=84=20smoke=20(role-ta-live-1522-001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../klaroworks/runtime/dto/ApiResponse.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 src/main/java/com/klaroworks/runtime/dto/ApiResponse.java diff --git a/src/main/java/com/klaroworks/runtime/dto/ApiResponse.java b/src/main/java/com/klaroworks/runtime/dto/ApiResponse.java new file mode 100644 index 0000000..e10bb94 --- /dev/null +++ b/src/main/java/com/klaroworks/runtime/dto/ApiResponse.java @@ -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 { + 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 ApiResponse success(T data) { + return ApiResponse.builder().success(true).data(data) + .timestamp(Instant.now().toString()).build(); + } + + public static ApiResponse error(ErrorCode code) { + return ApiResponse.builder().success(false) + .error(ErrorInfo.builder().code(code.getCode()).message(code.getMessage()).build()) + .timestamp(Instant.now().toString()).build(); + } + + public static ApiResponse error(ErrorCode code, Object details) { + return ApiResponse.builder().success(false) + .error(ErrorInfo.builder().code(code.getCode()).message(code.getMessage()).details(details).build()) + .timestamp(Instant.now().toString()).build(); + } +} -- 2.49.1 From bc492baab30808c504403013aa38b6da933721d7 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Tue, 14 Jul 2026 06:58:59 +0000 Subject: [PATCH 7/9] =?UTF-8?q?TA=20=EC=97=AD=ED=95=A0=20Spring=20?= =?UTF-8?q?=EA=B2=BD=EA=B3=84=20smoke=20(role-ta-live-1522-001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../runtime/service/RoleMatrixService.java | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/main/java/com/klaroworks/runtime/service/RoleMatrixService.java diff --git a/src/main/java/com/klaroworks/runtime/service/RoleMatrixService.java b/src/main/java/com/klaroworks/runtime/service/RoleMatrixService.java new file mode 100644 index 0000000..e473322 --- /dev/null +++ b/src/main/java/com/klaroworks/runtime/service/RoleMatrixService.java @@ -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 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); + } +} -- 2.49.1 From 0a7b2d9a4f765e9f2ac86b0d65049d39fbbedfd7 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Tue, 14 Jul 2026 06:59:00 +0000 Subject: [PATCH 8/9] =?UTF-8?q?TA=20=EC=97=AD=ED=95=A0=20Spring=20?= =?UTF-8?q?=EA=B2=BD=EA=B3=84=20smoke=20(role-ta-live-1522-001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/RoleMatrixController.java | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 src/main/java/com/klaroworks/runtime/controller/RoleMatrixController.java diff --git a/src/main/java/com/klaroworks/runtime/controller/RoleMatrixController.java b/src/main/java/com/klaroworks/runtime/controller/RoleMatrixController.java new file mode 100644 index 0000000..c6580c3 --- /dev/null +++ b/src/main/java/com/klaroworks/runtime/controller/RoleMatrixController.java @@ -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> createRole( + @Valid @RequestBody RoleCreateRequest request) { + return ResponseEntity.status(HttpStatus.CREATED) + .body(ApiResponse.success(roleMatrixService.createRole(request))); + } + + @GetMapping("/{id}") + public ResponseEntity> getRoleById(@PathVariable Long id) { + return ResponseEntity.ok(ApiResponse.success(roleMatrixService.getRoleById(id))); + } + + @GetMapping + public ResponseEntity>> getAllRoles() { + return ResponseEntity.ok(ApiResponse.success(roleMatrixService.getAllRoles())); + } + + @PutMapping("/{id}") + public ResponseEntity> updateRole( + @PathVariable Long id, + @Valid @RequestBody RoleCreateRequest request) { + return ResponseEntity.ok(ApiResponse.success(roleMatrixService.updateRole(id, request))); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteRole(@PathVariable Long id) { + roleMatrixService.deleteRole(id); + return ResponseEntity.noContent().build(); + } +} -- 2.49.1 From ff29deb091046a947006823a4f02b1f3d3307c26 Mon Sep 17 00:00:00 2001 From: forge-bot Date: Tue, 14 Jul 2026 06:59:01 +0000 Subject: [PATCH 9/9] =?UTF-8?q?TA=20=EC=97=AD=ED=95=A0=20Spring=20?= =?UTF-8?q?=EA=B2=BD=EA=B3=84=20smoke=20(role-ta-live-1522-001)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/RoleMatrixServiceTest.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 src/test/java/com/klaroworks/runtime/service/RoleMatrixServiceTest.java diff --git a/src/test/java/com/klaroworks/runtime/service/RoleMatrixServiceTest.java b/src/test/java/com/klaroworks/runtime/service/RoleMatrixServiceTest.java new file mode 100644 index 0000000..8d8ce49 --- /dev/null +++ b/src/test/java/com/klaroworks/runtime/service/RoleMatrixServiceTest.java @@ -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 roles = service.getAllRoles(); + assertEquals(1, roles.size()); + assertEquals("ADMIN", roles.get(0).getName()); + } +} -- 2.49.1