- harness·zio-harness·proposal-builder·zioinfo → plugins/zioinfo (git mv 히스토리 보존) - 스킬 4·커맨드 3(/zioinfo:pmo·proposal·wiki)·에이전트 15·graphify 훅·knowledge 통합 - 신규: /zioinfo:wiki (graphify LLM wiki — graphify-out/wiki/ 커뮤니티별 아티클) - 신규: ZIO WISE 테마 (themes/zioinfo.json, experimental) - manifest 최신화: $schema·displayName(ZIO INFOTECH Suite)·experimental.themes - marketplace.json 단일 엔트리, 루트 plugin.json 제거 - CLAUDE.md·PROJECT_MAP·docs/plugins.md·README 3종·CHANGELOG·설치가이드 pptx 재구성 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
285 lines
6.8 KiB
Markdown
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) |
|