harness/skills/zio-harness/references/spring-boot.md
ythong c25e55b64b feat: harness + zio-harness Claude Code 플러그인 초기 배포
- harness: 도메인 한 줄 → 에이전트 팀 자동 생성 메타 스킬 (v1.2.0)
- zio-harness: React + Spring Boot + Mobile 풀스택 개발 에이전트 팀 (v1.0.0)
- 에이전트 4종: orchestrator / analyst / bot / agent
- PROJECT_MAP.md 폴더 구조 메모리 시스템
- references/: react, spring-boot, mobile, playwright, database, folder-map

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-17 13:48:56 +09:00

285 lines
6.8 KiB
Markdown

# Spring Boot 개발 가이드
## 스택 가정
- Spring Boot 3.x + Java 17+ / Kotlin
- Spring Data JPA + Hibernate
- Spring Security (JWT)
- Flyway (DB 마이그레이션)
- Gradle 빌드
- 데이터베이스: PostgreSQL (기본), MySQL 지원
---
## 레이어 아키텍처
```
Controller (HTTP 진입점)
↓ DTO (Request)
Service (비즈니스 로직)
↓ Domain/Entity
Repository (데이터 접근)
Database
```
### 폴더 구조
```
src/main/java/{base.package}/
├── controller/ # @RestController — HTTP 요청 처리만
├── service/ # @Service — 비즈니스 로직
├── repository/ # @Repository — JPA 인터페이스
├── domain/ # @Entity — JPA 엔티티
├── dto/
│ ├── request/ # {Action}{Resource}Request
│ └── response/ # {Resource}Response
├── config/ # SecurityConfig, CorsConfig, etc.
├── exception/ # 커스텀 예외 + GlobalExceptionHandler
└── util/ # 유틸리티 클래스
```
---
## 컨트롤러 패턴
```java
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
@GetMapping
public ResponseEntity<List<UserResponse>> getUsers() {
return ResponseEntity.ok(userService.findAll());
}
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.findById(id));
}
@PostMapping
public ResponseEntity<UserResponse> createUser(@Valid @RequestBody CreateUserRequest request) {
return ResponseEntity.status(HttpStatus.CREATED)
.body(userService.create(request));
}
@PutMapping("/{id}")
public ResponseEntity<UserResponse> updateUser(
@PathVariable Long id,
@Valid @RequestBody UpdateUserRequest request) {
return ResponseEntity.ok(userService.update(id, request));
}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build();
}
}
```
---
## 서비스 패턴
```java
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {
private final UserRepository userRepository;
public List<UserResponse> findAll() {
return userRepository.findAll().stream()
.map(UserResponse::from)
.toList();
}
public UserResponse findById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new EntityNotFoundException("User not found: " + id));
return UserResponse.from(user);
}
@Transactional
public UserResponse create(CreateUserRequest request) {
User user = User.builder()
.email(request.getEmail())
.name(request.getName())
.build();
return UserResponse.from(userRepository.save(user));
}
}
```
---
## 엔티티 패턴
```java
@Entity
@Table(name = "users")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Builder
@AllArgsConstructor
public class User extends BaseEntity { // createdAt, updatedAt 포함
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String email;
@Column(nullable = false)
private String name;
}
// BaseEntity.java
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
```
---
## DTO 패턴
```java
// Request
public record CreateUserRequest(
@NotBlank @Email String email,
@NotBlank @Size(min=2, max=50) String name
) {}
// Response — 정적 팩토리 메서드 포함
public record UserResponse(Long id, String email, String name) {
public static UserResponse from(User user) {
return new UserResponse(user.getId(), user.getEmail(), user.getName());
}
}
```
---
## 예외 처리
```java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(EntityNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFound(EntityNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("NOT_FOUND", e.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidation(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(f -> f.getField() + ": " + f.getDefaultMessage())
.collect(Collectors.joining(", "));
return ResponseEntity.badRequest()
.body(new ErrorResponse("VALIDATION_ERROR", message));
}
}
public record ErrorResponse(String code, String message) {}
```
---
## JWT 보안 설정
```yaml
# application.yml
spring:
datasource:
url: ${DB_URL:jdbc:postgresql://localhost:5432/myapp}
username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:password}
jpa:
hibernate:
ddl-auto: validate # Flyway 사용 시 validate
open-in-view: false
jwt:
secret: ${JWT_SECRET}
expiration: 86400000 # 24시간 (ms)
```
---
## DB 마이그레이션 (Flyway)
```
src/main/resources/db/migration/
├── V1__create_users_table.sql
├── V2__add_user_role.sql
└── V3__create_posts_table.sql
```
네이밍: `V{version}__{description}.sql` (V 대문자, 밑줄 2개)
---
## 테스트 패턴
```java
// 서비스 단위 테스트
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@InjectMocks UserService userService;
@Mock UserRepository userRepository;
@Test
void 사용자_생성_성공() {
// given
given(userRepository.save(any())).willReturn(testUser());
// when
UserResponse response = userService.create(new CreateUserRequest("a@b.com", "Alice"));
// then
assertThat(response.email()).isEqualTo("a@b.com");
}
}
// 통합 테스트
@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {
@Autowired MockMvc mockMvc;
@Test
void GET_users_returns_list() throws Exception {
mockMvc.perform(get("/api/v1/users").contentType(APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$").isArray());
}
}
```
---
## 컨벤션
| 항목 | 규칙 |
|------|------|
| 엔티티 | 단수 명사 (User, Post) |
| 컨트롤러 | {Resource}Controller |
| 서비스 | {Resource}Service |
| 리포지토리 | {Resource}Repository |
| Request DTO | {Action}{Resource}Request |
| Response DTO | {Resource}Response |
| API 경로 | `/api/v1/{resource}` (복수, kebab-case) |