# 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) |
