- 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>
6.8 KiB
6.8 KiB
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/ # 유틸리티 클래스
컨트롤러 패턴
@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();
}
}
서비스 패턴
@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));
}
}
엔티티 패턴
@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 패턴
// 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());
}
}
예외 처리
@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 보안 설정
# 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개)
테스트 패턴
// 서비스 단위 테스트
@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) |