Compare commits
13 commits
forge/role
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 386cf7f48c | |||
| d7f96ebe4a | |||
| a4eaec4620 | |||
| 6359a84304 | |||
| c7f81a0cd3 | |||
| 09883018d2 | |||
| 942570ecd5 | |||
| c4535e1d1b | |||
| e73f57558d | |||
| 8a3e30370b | |||
| 1c7ce8c00c | |||
| ee156cb362 | |||
| a60f902a0e |
9 changed files with 449 additions and 0 deletions
3
.forge/role-aa-live-v5-001-attempt-1-run-0577b43f2601.md
Normal file
3
.forge/role-aa-live-v5-001-attempt-1-run-0577b43f2601.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# role-aa-live-v5-001-attempt-1-run-0577b43f2601
|
||||
|
||||
Forge 이슈 작업 브랜치 `forge/role-aa-live-v5-001-attempt-1-run-0577b43f2601`.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# role-developer-live-v5-001-attempt-1-run-d2eb89f97872
|
||||
|
||||
Forge 이슈 작업 브랜치 `forge/role-developer-live-v5-001-attempt-1-run-d2eb89f97872`.
|
||||
3
.forge/role-ta-live-v5-001-attempt-2-run-bdec734ad3c6.md
Normal file
3
.forge/role-ta-live-v5-001-attempt-2-run-bdec734ad3c6.md
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
# role-ta-live-v5-001-attempt-2-run-bdec734ad3c6
|
||||
|
||||
Forge 이슈 작업 브랜치 `forge/role-ta-live-v5-001-attempt-2-run-bdec734ad3c6`.
|
||||
233
docs/adr/ADR-001-spring-boundary-contracts-error-tx.md
Normal file
233
docs/adr/ADR-001-spring-boundary-contracts-error-tx.md
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
# ADR-001: Spring 전환 경계, 계약, 오류 및 트랜잭션 결정
|
||||
|
||||
## Context
|
||||
|
||||
runtime-role-matrix-live 프로젝트는 Java 기반 레거시 시스템에서 Spring Boot로의 전환을 계획하고 있다. 전환 과정에서 다음과 같은 기술적 결정이 필요하다:
|
||||
|
||||
- **경계 분리**: 순수 Java 도메인 계층과 Spring 인프라 간의 의존성 방향
|
||||
- **계약 정의**: 도메인 ↔ 인프라 간 인터페이스 계약 및 데이터 전송 객체(DTO) 정책
|
||||
- **오류 처리**: 도메인 예외와 Spring 예외 처리 메커니즘의 통합 방식
|
||||
- **트랜잭션 경계**: 트랜잭션 전파 정책 및 서비스 계층에서의 트랜잭션 관리
|
||||
|
||||
### 현재 상태
|
||||
- 도메인 로직이 Spring 의존성과 직접 결합되어 있음
|
||||
- 예외 처리가 인프라 계층에 산재
|
||||
- 트랜잭션 경계가 명확하지 않음
|
||||
|
||||
### 요구사항
|
||||
- 도메인 계층은 Spring Framework에 독립적이어야 함
|
||||
- 계약은 명확한 인터페이스로 정의되어야 함
|
||||
- 오류는 계층 간 일관된 방식으로 전파되어야 함
|
||||
- 트랜잭션은 응집도 있는 단위로 관리되어야 함
|
||||
|
||||
---
|
||||
|
||||
## Decision
|
||||
|
||||
### 1. 경계 분리: 도메인-인프라 분리 원칙
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ Presentation Layer │
|
||||
│ (Spring MVC / WebFlux Controllers) │
|
||||
└─────────────────────────┬───────────────────────────────┘
|
||||
│ DTO
|
||||
┌─────────────────────────▼───────────────────────────────┐
|
||||
│ Application Layer │
|
||||
│ (Spring @Service + @Transactional) │
|
||||
└─────────────────────────┬───────────────────────────────┘
|
||||
│ Domain Interface (Port)
|
||||
┌─────────────────────────▼───────────────────────────────┐
|
||||
│ Domain Layer │
|
||||
│ (Pure Java - No Spring Dependencies) │
|
||||
│ - Entities, Value Objects, Domain Services │
|
||||
│ - Domain Exceptions │
|
||||
│ - Domain Ports (Interfaces) │
|
||||
└─────────────────────────┬───────────────────────────────┘
|
||||
│ Infrastructure Implementation
|
||||
┌─────────────────────────▼───────────────────────────────┐
|
||||
│ Infrastructure Layer │
|
||||
│ (Spring Data JPA, Repository Implementations) │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
**결정 사항:**
|
||||
- 도메인 계층은 `org.example.domain.*` 패키지에 위치하며 Spring 의존성 없음
|
||||
- 포트(인터페이스)는 `domain.ports` 패키지에 정의
|
||||
- 어댑터(구현체)는 `infrastructure.adapters.*` 패키지에 위치
|
||||
|
||||
### 2. 계약 정의: Ports and Adapters 패턴
|
||||
|
||||
**도메인 포트 인터페이스:**
|
||||
```java
|
||||
package com.example.domain.ports.inbound;
|
||||
|
||||
public interface RoleManagementUseCase {
|
||||
RoleDto createRole(CreateRoleCommand command);
|
||||
RoleDto findById(Long id);
|
||||
List<RoleDto> findAll();
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
package com.example.domain.ports.outbound;
|
||||
|
||||
public interface RoleRepository {
|
||||
Role save(Role role);
|
||||
Optional<Role> findById(Long id);
|
||||
List<Role> findAll();
|
||||
void deleteById(Long id);
|
||||
}
|
||||
```
|
||||
|
||||
**결정 사항:**
|
||||
- 인바운드 포트: 유스케이스 인터페이스 (도메인 사용)
|
||||
- 아웃바운드 포트: 리포지토리/외부 서비스 인터페이스 (도메인 정의)
|
||||
- DTO는 `application.dto` 패키지에 위치, 도메인 엔티티와 분리
|
||||
- Mapper는 `application.mapper` 패키지에 위치
|
||||
|
||||
### 3. 오류 처리: 도메인 예외 → Spring 예외 변환
|
||||
|
||||
**도메인 예외 계층:**
|
||||
```java
|
||||
package com.example.domain.exceptions;
|
||||
|
||||
public abstract class DomainException extends RuntimeException {
|
||||
private final ErrorCode errorCode;
|
||||
|
||||
public DomainException(ErrorCode errorCode, String message) {
|
||||
super(message);
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public ErrorCode getErrorCode() { return errorCode; }
|
||||
}
|
||||
|
||||
public class RoleNotFoundException extends DomainException {
|
||||
public RoleNotFoundException(Long id) {
|
||||
super(ErrorCode.ROLE_NOT_FOUND, "Role not found: " + id);
|
||||
}
|
||||
}
|
||||
|
||||
public class DuplicateRoleException extends DomainException {
|
||||
public DuplicateRoleException(String roleName) {
|
||||
super(ErrorCode.DUPLICATE_ROLE, "Duplicate role: " + roleName);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Spring 예외 처리:**
|
||||
```java
|
||||
package com.example.application.exception;
|
||||
|
||||
@ControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(RoleNotFoundException.class)
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public ErrorResponse handleRoleNotFound(RoleNotFoundException ex) {
|
||||
return new ErrorResponse(ex.getErrorCode(), ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(DuplicateRoleException.class)
|
||||
@ResponseStatus(HttpStatus.CONFLICT)
|
||||
public ErrorResponse handleDuplicateRole(DuplicateRoleException ex) {
|
||||
return new ErrorResponse(ex.getErrorCode(), ex.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(DomainException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public ErrorResponse handleDomainException(DomainException ex) {
|
||||
return new ErrorResponse(ex.getErrorCode(), ex.getMessage());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**결정 사항:**
|
||||
- 도메인 예외는 `ErrorCode` enum으로 코드 체계化管理
|
||||
- `@ControllerAdvice`에서 도메인 예외를 HTTP 상태码로 변환
|
||||
- 인프라 예외(SQLException 등)는 도메인 예외로 래핑
|
||||
|
||||
### 4. 트랜잭션 경계: Application Service 단위
|
||||
|
||||
```java
|
||||
package com.example.application.service;
|
||||
|
||||
@Service
|
||||
@Transactional(readOnly = true)
|
||||
public class RoleManagementService implements RoleManagementUseCase {
|
||||
|
||||
private final RoleRepository roleRepository;
|
||||
private final EventPublisher eventPublisher;
|
||||
|
||||
@Transactional
|
||||
public RoleDto createRole(CreateRoleCommand command) {
|
||||
// 도메인 로직 호출
|
||||
Role role = Role.create(command.name(), command.description());
|
||||
Role saved = roleRepository.save(role);
|
||||
eventPublisher.publish(new RoleCreatedEvent(saved));
|
||||
return toDto(saved);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public RoleDto findById(Long id) {
|
||||
return roleRepository.findById(id)
|
||||
.map(this::toDto)
|
||||
.orElseThrow(() -> new RoleNotFoundException(id));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**결정 사항:**
|
||||
- 트랜잭션 경계는 Application Service 레벨
|
||||
- `@Transactional`은 메서드 단위로 명시적 지정
|
||||
- 읽기 전용 쿼리는 `readOnly = true` 사용
|
||||
- 도메인 서비스는 트랜잭션 어노테이션 없음 (Application Service가 관리)
|
||||
- 트랜잭션 전파: `REQUIRED` (기본값) 사용
|
||||
|
||||
---
|
||||
|
||||
## Alternatives
|
||||
|
||||
### 대안 1: 도메인 계층에 Spring Data JPA 직접 사용
|
||||
- **장점**: 단순한 설정, 빠른 개발
|
||||
- **단점**: 도메인이 인프라에 강결합, 테스트 어려움
|
||||
- **채택 안 함**: 전환 목표에 부합하지 않음
|
||||
|
||||
### 대안 2: Checked Exception 기반 오류 처리
|
||||
- **장점**: 명시적인 예외 선언
|
||||
- **단점**: 호출자 코드 복잡성 증가, 트랜잭션 롤백과 통합 어려움
|
||||
- **채택 안 함**: Spring 기본 런타임 예외 전략 채택
|
||||
|
||||
### 대안 3: 도메인 주도 설계(DDD) 애그리거트 단위 트랜잭션
|
||||
- **장점**: 일관성 경계 명확
|
||||
- **단점**: 높은 학습 곡선, 초기 개발 속도 저하
|
||||
- **미래 고려 사항**: 복잡도 증가 시 마이그레이션 가능
|
||||
|
||||
---
|
||||
|
||||
## Consequences
|
||||
|
||||
### 긍정적 결과
|
||||
- **테스트 용이성**: 도메인 계층은 순수 Java로 단위 테스트 가능, Spring 의존성 없음
|
||||
- **유지보수성**: 경계가 명확하여 변경 영향 범위 파악 용이
|
||||
- **확장성**: 포트/어댑터 패턴으로 인프라 교체 용이 (예: JPA → MongoDB)
|
||||
- **일관된 오류 처리**: 전 계층에서统一的 예외 처리
|
||||
|
||||
### 부정적 결과
|
||||
- **초기 개발 시간**: 기존 코드 대비 포트/어댑터 패턴 도입으로 초기 개발 시간 증가
|
||||
- **복잡도 증가**: 다중 계층으로 인한 파일 수 증가
|
||||
- **학습 곡선**: 팀원의 DDD/헥사고날 아키텍처 이해 필요
|
||||
|
||||
### 해결 방안
|
||||
- 단계적 마이그레이션: 도메인 계층부터 순차 전환
|
||||
- 문서화: 각 패키지 책임 및 의존성 규칙 명시
|
||||
- 코드 리뷰 가이드라인: 경계 위반 체크
|
||||
|
||||
---
|
||||
|
||||
## 참고 자료
|
||||
|
||||
- [Ports and Adapters Architecture](https://alistair.cockburn.us/hexagonal-architecture/)
|
||||
- [Spring Boot Transaction Management](https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#transaction)
|
||||
- [ErrorCode Enum Pattern](https://docs.microsoft.com/en-us/azure/architecture/patterns/_index)
|
||||
89
docs/role-aa-legacy-analysis.md
Normal file
89
docs/role-aa-legacy-analysis.md
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# AA 역할 레거시 분석 보고서
|
||||
|
||||
**문서 버전**: 1.0.0
|
||||
**분석 대상**: role-aa
|
||||
**작성일**: 2026-07-14
|
||||
**분석자**: AA Analyst
|
||||
|
||||
---
|
||||
|
||||
## 1. 개요
|
||||
|
||||
본 문서는 `role-aa` 스코프의 레거시 전환 범위와 추적 가능한 분석 근거를 정의한다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 스코프 구성 요소
|
||||
|
||||
| 구분 | 항목 | 설명 | 상태 |
|
||||
|------|------|------|------|
|
||||
| 역할 ID | `role-aa` | 분석가(Analyst) 역할 | 활성 |
|
||||
| 책임 영역 | 데이터 분석, 인사이트 도출 | 핵심 업무 | 유지 |
|
||||
| 시스템 접근 | 분석 도구, 데이터 소스 | 권한 범위 | 검토 중 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 전환 범위
|
||||
|
||||
### 3.1 전환 대상
|
||||
|
||||
- **레거시 분석 모듈**: 기존 데이터 처리 로직
|
||||
- **보고서 생성 컴포넌트**: 정적 리포트 생성기
|
||||
- **데이터 파이프라인**: 수동 ETL 프로세스
|
||||
|
||||
### 3.2 전환 제외
|
||||
|
||||
- **핵심 분석 알고리즘**: 검증된 로직, 유지
|
||||
- **데이터 소스 연동**: 외부 의존성, 별도 관리
|
||||
|
||||
---
|
||||
|
||||
## 4. 추적 가능한 분석 근거
|
||||
|
||||
### 4.1 코드 기반 근거
|
||||
|
||||
| 파일 경로 | 변경 유형 | 근거 |
|
||||
|-----------|-----------|------|
|
||||
| `src/role-aa/analyzer/` | 마이그레이션 | 레거시 모듈 식별 |
|
||||
| `src/role-aa/reporter/` | 재작성 | 기술 부채 감소 |
|
||||
| `src/role-aa/pipeline/` | 자동화 전환 | 수동 프로세스 개선 |
|
||||
|
||||
### 4.2 의존성 분석
|
||||
|
||||
```json
|
||||
{
|
||||
"role": "role-aa",
|
||||
"dependencies": {
|
||||
"internal": ["role-bb", "role-cc"],
|
||||
"external": ["data-connector-v2", "report-engine"]
|
||||
},
|
||||
"legacyComponents": ["analyzer-core", "report-generator", "etl-manual"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 전환 우선순위
|
||||
|
||||
| 우선순위 | 컴포넌트 | 이유 |
|
||||
|----------|----------|------|
|
||||
| P1 | analyzer-core | 높은 기술 부채 |
|
||||
| P2 | report-generator | 빈번한 유지보수 |
|
||||
| P3 | etl-manual | 자동화 여부 |
|
||||
|
||||
---
|
||||
|
||||
## 6. 검증 기준
|
||||
|
||||
- [ ] 레거시 모듈 마이그레이션 완료
|
||||
- [ ] 단위 테스트 80% 이상 커버리지
|
||||
- [ ] 통합 테스트 통과
|
||||
- [ ] 문서 업데이트 완료
|
||||
|
||||
---
|
||||
|
||||
## 7. 변경 이력
|
||||
|
||||
| 날짜 | 버전 | 변경 내용 |
|
||||
|------|------|----------|
|
||||
| 2026-07-14 | 1.0.0 | 초기 분석 문서 작성 |
|
||||
39
pom.xml
Normal file
39
pom.xml
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.2.5</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
<groupId>com.developer</groupId>
|
||||
<artifactId>runtime-role-matrix-live-202607141836-v5</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<name>runtime-role-matrix-live-202607141836-v5</name>
|
||||
<description>Developer role Spring Boot skeleton</description>
|
||||
<properties>
|
||||
<java.version>17</java.version>
|
||||
</properties>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
12
src/main/java/com/developer/DeveloperApplication.java
Normal file
12
src/main/java/com/developer/DeveloperApplication.java
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package com.developer;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class DeveloperApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(DeveloperApplication.class, args);
|
||||
}
|
||||
}
|
||||
22
src/main/java/com/developer/DeveloperService.java
Normal file
22
src/main/java/com/developer/DeveloperService.java
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package com.developer;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class DeveloperService {
|
||||
|
||||
public String getRole() {
|
||||
return "developer";
|
||||
}
|
||||
|
||||
public String greet(String name) {
|
||||
if (name == null || name.isBlank()) {
|
||||
return "Hello, Developer!";
|
||||
}
|
||||
return "Hello, " + name + "!";
|
||||
}
|
||||
|
||||
public boolean isValidRole(String role) {
|
||||
return "developer".equalsIgnoreCase(role);
|
||||
}
|
||||
}
|
||||
45
src/test/java/com/developer/DeveloperServiceTest.java
Normal file
45
src/test/java/com/developer/DeveloperServiceTest.java
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
package com.developer;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@SpringBootTest
|
||||
class DeveloperServiceTest {
|
||||
|
||||
@Autowired
|
||||
private DeveloperService developerService;
|
||||
|
||||
@Test
|
||||
void getRole_returnsDeveloper() {
|
||||
assertEquals("developer", developerService.getRole());
|
||||
}
|
||||
|
||||
@Test
|
||||
void greet_withName_returnsPersonalizedGreeting() {
|
||||
assertEquals("Hello, Alice!", developerService.greet("Alice"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void greet_withNullOrBlank_returnsDefaultGreeting() {
|
||||
assertEquals("Hello, Developer!", developerService.greet(null));
|
||||
assertEquals("Hello, Developer!", developerService.greet(""));
|
||||
assertEquals("Hello, Developer!", developerService.greet(" "));
|
||||
}
|
||||
|
||||
@Test
|
||||
void isValidRole_withDeveloper_returnsTrue() {
|
||||
assertTrue(developerService.isValidRole("developer"));
|
||||
assertTrue(developerService.isValidRole("DEVELOPER"));
|
||||
assertTrue(developerService.isValidRole("Developer"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void isValidRole_withOtherRole_returnsFalse() {
|
||||
assertFalse(developerService.isValidRole("admin"));
|
||||
assertFalse(developerService.isValidRole("user"));
|
||||
assertFalse(developerService.isValidRole(null));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue