Spring Boot 3.x ํต์ฌ ์ด๋
ธํ
์ด์
, Gradle & Maven ๋น๋ ์ค์ , RESTful API ์ค๊ณ, ์์กด์ฑ ์ฃผ์
(DI), application.yml ์ค์ , JPA ํธ๋์ญ์
, ์ ์ญ ์์ธ ์ฒ๋ฆฌ ๋ฐ ์ค๋ฌด ์ต์ ํ ๋ ํผ๋ฐ์ค
## 1. ํ๋ก์ ํธ ์์ & ๊ตฌ์กฐ (Project Setup & Architecture)
```text
my-spring-app/
โโโ src/
โ โโโ main/
โ โ โโโ java/com/example/demo/
โ โ โ โโโ DemoApplication.java โ @SpringBootApplication ์ง์
์
โ โ โ โโโ controller/ โ HTTP ์์ฒญ ์์ & ์๋ต ๋ฐํ
โ โ โ โโโ service/ โ ๋น์ฆ๋์ค ๋ก์ง & ํธ๋์ญ์
๊ด๋ฆฌ
โ โ โ โโโ repository/ โ DB ์ ๊ทผ (Spring Data JPA)
โ โ โ โโโ domain/ (๋๋ entity/) โ JPA ์ํฐํฐ ๊ฐ์ฒด
โ โ โ โโโ dto/ โ ๊ณ์ธต ๊ฐ ๋ฐ์ดํฐ ์ ๋ฌ ๊ฐ์ฒด (Request/Response)
โ โ โ โโโ config/ โ ๋ณด์, ์น, ๋น๋๊ธฐ ๋ฑ ๋น ์ค์ ํด๋์ค
โ โ โ โโโ exception/ โ ์ปค์คํ
์์ธ & @RestControllerAdvice
โ โ โโโ resources/
โ โ โโโ application.yml โ ์ ์ญ ํ๊ฒฝ ์ค์ ํ์ผ
โ โ โโโ application-local.yml โ ๋ก์ปฌ ๊ฐ๋ฐ์ฉ ํ๋กํ์ผ
โ โ โโโ static/ & templates/ โ ์ ์ ์์ (HTML/CSS)
โ โโโ test/java/com/example/demo/ โ ๋จ์/ํตํฉ ํ
์คํธ ์ฝ๋
โโโ build.gradle (๋๋ pom.xml) โ ๋น๋ ์คํฌ๋ฆฝํธ ๋ฐ ์์กด์ฑ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
โโโ gradlew / gradlew.bat (๋๋ mvnw) โ OS๋ณ ๋น๋ ๋ํผ ์คํฌ๋ฆฝํธ
```
```java
// DemoApplication.java - ์คํ๋ง ๋ถํธ ์ ํ๋ฆฌ์ผ์ด์
์์ ์ง์
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
// @SpringBootApplication = @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
```
## 2. Gradle ๋น๋ ์ค์ (build.gradle - Groovy & Kotlin)
```groovy
// build.gradle (Groovy DSL) - Spring Boot 3.x ํ์ค ์ค๋ฌด ํ
ํ๋ฆฟ
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.5'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17) // ๋๋ 21
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
// 1. ํต์ฌ ์คํํฐ ์์กด์ฑ
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
// 2. ๊ฐ๋ฐ ํธ์ & ํซ ๋ฆฌ๋ก๋
developmentOnly 'org.springframework.boot:spring-boot-devtools'
// 3. ๋กฌ๋ณต (Lombok)
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testCompileOnly 'org.projectlombok:lombok'
testAnnotationProcessor 'org.projectlombok:lombok'
// 4. ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋๋ผ์ด๋ฒ
runtimeOnly 'com.mysql:mysql-connector-j'
runtimeOnly 'com.h2database:h2' // ๋ก์ปฌ/ํ
์คํธ ์ธ๋ฉ๋ชจ๋ฆฌ์ฉ
// 5. ํ
์คํธ ์์กด์ฑ (JUnit 5, AssertJ, Mockito ๋ด์ฅ)
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
// ์ค๋ฌด ๋ฐฐํฌ ์ต์ ํ: ์คํ ๊ฐ๋ฅํ ๋จ์ผ fat-jar ์์ฑ (plain jar ์์ฑ ๋ฐฉ์ง)
bootJar {
archiveFileName = 'app.jar'
}
jar {
enabled = false // plain jar ๋นํ์ฑํ
}
```
```bash
# ์์ฃผ ์ฌ์ฉํ๋ Gradle ๋น๋ ๋ช
๋ น์ด
./gradlew build # ์ปดํ์ผ, ํ
์คํธ ๋ฐ jar ๋น๋
./gradlew bootRun # ๋ก์ปฌ์์ ์คํ๋ง ๋ถํธ ์ฑ ์ฆ์ ์คํ
./gradlew clean build -x test # ํ
์คํธ๋ฅผ ๊ฑด๋๋ฐ๊ณ ๋น ๋ฅธ ๋น๋
./gradlew dependencies # ํ๋ก์ ํธ ์์กด์ฑ ํธ๋ฆฌ ํ์ธ
```
## 3. Maven ๋น๋ ์ค์ (pom.xml)
```xml
<?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
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<!-- [1] ์คํ๋ง ๋ถํธ ์คํํฐ ๋ถ๋ชจ POM ์์ (๋ฒ์ ๊ด๋ฆฌ ๋ฐ ๊ธฐ๋ณธ ํ๋ฌ๊ทธ์ธ ์์) -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/> <!-- ๋ถ๋ชจ POM์ ์ ์ฅ์์์ ์กฐํ -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Spring Boot Project</description>
<!-- [2] Java ๋ฒ์ ๋ฐ ์ธ์ฝ๋ฉ ์์ฑ ์ ์ -->
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<!-- [3] ์์กด์ฑ ๋ชฉ๋ก (๋ถ๋ชจ POM์ผ๋ก ์ธํด <version> ํ๊ทธ ์๋ต ๊ฐ๋ฅ) -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Lombok & ๊ฐ๋ฐ ํด -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<!-- DB ๋๋ผ์ด๋ฒ -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- ํ
์คํธ ์์กด์ฑ -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- [4] ๋น๋ ๋ฐ ํ๋ฌ๊ทธ์ธ ์ค์ -->
<build>
<plugins>
<!-- ์คํ ๊ฐ๋ฅํ Fat-JAR ํจํค์ง ํ๋ฌ๊ทธ์ธ -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
```
```bash
# ์์ฃผ ์ฌ์ฉํ๋ Maven ๋น๋ ๋ช
๋ น์ด
./mvnw clean package # ์ปดํ์ผ, ํ
์คํธ ๋ฐ jar ํจํค์ง
./mvnw spring-boot:run # ์คํ๋ง ๋ถํธ ์ฑ ๋ก์ปฌ ์คํ
./mvnw clean package -DskipTests # ํ
์คํธ๋ฅผ ์ ์ธํ๊ณ ๋น ๋ฅธ ๋น๋
./mvnw dependency:tree # ์์กด์ฑ ๊ณ์ธต ๊ตฌ์กฐ ํ์ธ
```
## 4. ํต์ฌ Starter ์์กด์ฑ ํต ๋ ํผ๋ฐ์ค (Starters Cheatsheet)
```groovy
// ๋ชฉ์ ๋ณ ํ์ ์คํํฐ ์ํฐํฉํธ ์ ๋ฆฌ (Gradle: implementation '...', Maven: <artifactId>...)
// ๐ ์น & API
'org.springframework.boot:spring-boot-starter-web' // Spring MVC, REST, ๋ด์ฅ ํฐ์บฃ(Tomcat)
'org.springframework.boot:spring-boot-starter-webflux' // ๋
ผ๋ธ๋กํน ๋ฆฌ์กํฐ๋ธ ์น (Netty ๊ธฐ๋ฐ)
// ๐๏ธ ๋ฐ์ดํฐ & ORM
'org.springframework.boot:spring-boot-starter-data-jpa' // Spring Data JPA + Hibernate Core
'org.springframework.boot:spring-boot-starter-data-redis' // Redis ์บ์, ์ธ์
, Lettuce ํด๋ผ์ด์ธํธ
'org.mybatis.spring.boot:mybatis-spring-boot-starter:3.0.3' // MyBatis SQL ๋งคํผ ์ฐ๋
// ๐ ๋ณด์ & ๊ฒ์ฆ
'org.springframework.boot:spring-boot-starter-security' // ์คํ๋ง ์ํ๋ฆฌํฐ ์ธ์ฆ & ์ธ๊ฐ
'org.springframework.boot:spring-boot-starter-validation' // Hibernate Validator (@Valid, @NotNull)
// ๐จ ๋ฉ์์ง & ๋ฐฐ์น
'org.springframework.boot:spring-boot-starter-mail' // JavaMailSender ์ด๋ฉ์ผ ๋ฐ์ก
'org.springframework.boot:spring-boot-starter-batch' // ๋์ฉ๋ ์ฒญํฌ ๋จ์ ๋ฐฐ์น ์์
// ๐ ์ด์ & ๊ด์ธก์ฑ
'org.springframework.boot:spring-boot-starter-actuator' // ์ ํ๋ฆฌ์ผ์ด์
์ํ(Health), ๋ฉํธ๋ฆญ
'io.micrometer:micrometer-registry-prometheus' // ํ๋ก๋ฉํ
์ฐ์ค(Prometheus) ๋ชจ๋ํฐ๋ง ์์ง
```
## 5. ํต์ฌ ์ปดํฌ๋ํธ & ์ด๋
ธํ
์ด์
(Core Annotations & IoC)
```java
// [1] ์คํ
๋ ์คํ์
์ด๋
ธํ
์ด์
(์๋ ์ปดํฌ๋ํธ ์ค์บ ๋์)
@Component // ๊ธฐ๋ณธ ์คํ๋ง ๊ด๋ฆฌ ๋น์ผ๋ก ๋ฑ๋ก
@Service // ๋น์ฆ๋์ค ๋ก์ง ๊ณ์ธต (๋ช
์์ ์ญํ ๊ตฌ๋ถ)
@Repository // ๋ฐ์ดํฐ ์ ๊ทผ ๊ณ์ธต (ํ๋ซํผ ์ข
์ ์์ธ๋ฅผ DataAccessException์ผ๋ก ๋ณํ)
@Controller // Spring MVC ์ปจํธ๋กค๋ฌ (๋ทฐ ํ
ํ๋ฆฟ ๋ฐํ์ฉ)
@RestController // @Controller + @ResponseBody (JSON/REST API ์ ์ฉ)
// [2] ์ค์ ํด๋์ค ๋ฐ ์๋ ๋น ๋ฑ๋ก
@Configuration // ์คํ๋ง ์ค์ ํด๋์ค๋ก ์ ์ธ (์ฑ๊ธํค CGLIB ํ๋ก์ ์ ์ฉ)
public class AppConfig {
@Bean // ๋ฉ์๋๊ฐ ๋ฐํํ๋ ๊ฐ์ฒด๋ฅผ ์คํ๋ง ์ปจํ
์ด๋์ ๋น์ผ๋ก ์๋ ๋ฑ๋ก
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
// [3] ํ๋กํผํฐ ๊ฐ ์ฃผ์
@Component
public class ApiClient {
// ๋จ์ผ ํ๋กํผํฐ ๊ฐ ์ฃผ์
(๊ธฐ๋ณธ๊ฐ ์ค์ ๊ฐ๋ฅ)
@Value("${external.api.timeout:5000}")
private int timeout;
@Value("${external.api.base-url}")
private String baseUrl;
}
// [4] ํ์
์ธ์ดํ ํ๋กํผํฐ ๋ฐ์ธ๋ฉ (@ConfigurationProperties)
@Component
@ConfigurationProperties(prefix = "app.mail")
@Getter @Setter
public class MailProperties {
private String host;
private int port;
private List<String> defaultRecipients;
}
```
## 6. ์์กด์ฑ ์ฃผ์
& ๋น ์๋ช
์ฃผ๊ธฐ (DI & Bean Lifecycle)
```java
// ์ค๋ฌด ๊ถ์ฅ ํจํด: ์์ฑ์ ์ฃผ์
+ Lombok @RequiredArgsConstructor
@Service
@RequiredArgsConstructor // final ํ๋ ๋์ ์์ฑ์ ์๋ ์์ฑ
public class UserService {
// final ํค์๋๋ก ๋ถ๋ณ์ฑ ๋ณด์ฅ ๋ฐ ๋๋ฝ ๋ฐฉ์ง
private final UserRepository userRepository;
private final EmailService emailService;
// ๋น ๋ฑ๋ก ์งํ 1ํ ์ด๊ธฐํ ์์
์คํ (DB ๊ธฐ์ด ๋ฐ์ดํฐ ์ธํ
๋ฑ)
@PostConstruct
public void init() {
System.out.println("UserService ๋น ์ด๊ธฐํ ์๋ฃ");
}
// ์คํ๋ง ์ปจํ
์ด๋ ์ข
๋ฃ ์ง์ ๋ฆฌ์์ค ๋ฐํ/์ ๋ฆฌ ์์
์คํ
@PreDestroy
public void cleanup() {
System.out.println("UserService ๋น ์ข
๋ฃ ๋ฆฌ์์ค ์ ๋ฆฌ");
}
}
// ๋์ผ ํ์
๋น ์ถฉ๋ ์ ํด๊ฒฐ ๋ฐฉ๋ฒ (@Primary, @Qualifier)
@Component
@Primary // ๊ธฐ๋ณธ ์ฐ์ ์์ ๋น์ผ๋ก ์ง์
public class KakaoPayService implements PaymentService { }
@Component("naverPay")
public class NaverPayService implements PaymentService { }
@Service
@RequiredArgsConstructor
public class OrderService {
// ํน์ ๋น์ ๋ช
์์ ์ผ๋ก ์ฃผ์
๋ฐ์ ๋
@Qualifier("naverPay")
private final PaymentService paymentService;
}
```
## 7. RESTful API ์ปจํธ๋กค๋ฌ (Spring MVC REST)
```java
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
// 1. ๋จ๊ฑด ์กฐํ: GET /api/v1/users/{id}
@GetMapping("/{id}")
public ResponseEntity<UserResponse> getUser(@PathVariable Long id) {
UserResponse response = userService.findById(id);
return ResponseEntity.ok(response); // HTTP 200 OK
}
// 2. ๋ชฉ๋ก ํ์ด์ง ๊ฒ์: GET /api/v1/users?role=ADMIN&page=0&size=10
@GetMapping
public ResponseEntity<Page<UserResponse>> getUsers(
@RequestParam(required = false, defaultValue = "USER") String role,
@PageableDefault(size = 10, sort = "id", direction = Sort.Direction.DESC) Pageable pageable) {
return ResponseEntity.ok(userService.findAll(role, pageable));
}
// 3. ์์ฑ: POST /api/v1/users
@PostMapping
public ResponseEntity<UserResponse> createUser(@Valid @RequestBody UserCreateRequest request) {
UserResponse created = userService.create(request);
URI location = URI.create("/api/v1/users/" + created.getId());
return ResponseEntity.created(location).body(created); // HTTP 201 Created
}
// 4. ์์ : PUT /api/v1/users/{id}
@PutMapping("/{id}")
public ResponseEntity<UserResponse> updateUser(
@PathVariable Long id,
@Valid @RequestBody UserUpdateRequest request) {
return ResponseEntity.ok(userService.update(id, request));
}
// 5. ์ญ์ : DELETE /api/v1/users/{id}
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.delete(id);
return ResponseEntity.noContent().build(); // HTTP 204 No Content
}
}
```
## 8. DTO ์ ํจ์ฑ ๊ฒ์ฆ (Bean Validation)
```java
// ์์กด์ฑ: implementation 'org.springframework.boot:spring-boot-starter-validation'
@Getter
@NoArgsConstructor
public class UserCreateRequest {
@NotBlank(message = "์ด๋ฆ์ ํ์ ์
๋ ฅ ํญ๋ชฉ์
๋๋ค.")
@Size(min = 2, max = 30, message = "์ด๋ฆ์ 2~30์ ์ด๋ด์ฌ์ผ ํฉ๋๋ค.")
private String name;
@NotBlank(message = "์ด๋ฉ์ผ์ ํ์ ์
๋ ฅ ํญ๋ชฉ์
๋๋ค.")
@Email(message = "์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ ํ์์ด ์๋๋๋ค.")
private String email;
@NotBlank(message = "๋น๋ฐ๋ฒํธ๋ ํ์ ์
๋ ฅ ํญ๋ชฉ์
๋๋ค.")
@Pattern(
regexp = "^(?=.*[A-Za-z])(?=.*\\d)(?=.*[@$!%*#?&])[A-Za-z\\d@$!%*#?&]{8,}$",
message = "๋น๋ฐ๋ฒํธ๋ 8์ ์ด์, ์๋ฌธ, ์ซ์, ํน์๋ฌธ์๋ฅผ ๊ฐ๊ฐ ์ต์ 1๊ฐ ํฌํจํด์ผ ํฉ๋๋ค."
)
private String password;
@Min(value = 14, message = "14์ธ ์ด์๋ง ๊ฐ์
๊ฐ๋ฅํฉ๋๋ค.")
@Max(value = 120, message = "์ ํจํ ๋์ด๋ฅผ ์
๋ ฅํด์ฃผ์ธ์.")
private int age;
}
```
## 9. ์ ์ญ ์์ธ ์ฒ๋ฆฌ (Global Exception Handling)
```java
// [1] ํต์ผ๋ ์๋ฌ ์๋ต DTO
@Getter
@Builder
public class ErrorResponse {
private final LocalDateTime timestamp = LocalDateTime.now();
private final int status;
private final String error;
private final String message;
private final Map<String, String> validationErrors;
}
// [2] ์ ์ญ ์์ธ ์ฒ๋ฆฌ๊ธฐ (@RestControllerAdvice)
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 1. ์ ํจ์ฑ ๊ฒ์ฆ ์คํจ ์์ธ (@Valid ์คํจ ์ ์๋ ๋ฐ์)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidationException(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(err ->
errors.put(err.getField(), err.getDefaultMessage())
);
ErrorResponse response = ErrorResponse.builder()
.status(HttpStatus.BAD_REQUEST.value())
.error(HttpStatus.BAD_REQUEST.getReasonPhrase())
.message("์
๋ ฅ๊ฐ ๊ฒ์ฆ์ ์คํจํ์ต๋๋ค.")
.validationErrors(errors)
.build();
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(response);
}
// 2. ๋น์ฆ๋์ค ์ปค์คํ
์์ธ
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNotFoundException(ResourceNotFoundException ex) {
ErrorResponse response = ErrorResponse.builder()
.status(HttpStatus.NOT_FOUND.value())
.error(HttpStatus.NOT_FOUND.getReasonPhrase())
.message(ex.getMessage())
.build();
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
}
// 3. ๊ธฐํ ์๋ฒ ๋ด๋ถ ์ค๋ฅ (์ตํ์ ๋ณด๋ฃจ)
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneralException(Exception ex) {
log.error("Unhandled Exception occurred: ", ex);
ErrorResponse response = ErrorResponse.builder()
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.error(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase())
.message("์๋ฒ ๋ด๋ถ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค. ์ ์ ํ ๋ค์ ์๋ํด์ฃผ์ธ์.")
.build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response);
}
}
```
## 10. application.yml ํต์ฌ ์ค์ (Configuration)
```yaml
# ๊ธฐ๋ณธ ์๋ฒ ๋ฐ ํ๋กํ์ผ ์ค์
server:
port: 8080
servlet:
context-path: /
encoding:
charset: UTF-8
force: true
spring:
application:
name: lucky-api-service
profiles:
active: local # ๊ธฐ๋ณธ ํ์ฑํ ํ๋กํ์ผ
# ๋ฐ์ดํฐ์์ค & HikariCP ์ปค๋ฅ์
ํ ์ค์
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/lucky_db?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8
username: root
password: password123
hikari:
maximum-pool-size: 10
minimum-idle: 5
connection-timeout: 30000
idle-timeout: 600000
max-lifetime: 1800000
# JPA & Hibernate ์ค์
jpa:
open-in-view: false # OSIV ๋นํ์ฑํ (์ปค๋ฅ์
๊ณ ๊ฐ ๋ฐฉ์ง ๋ฐ ์ค๋ฌด ํ์ ๊ถ์ฅ)
hibernate:
ddl-auto: validate # local: update / dev, prod: validate or none
show-sql: false # sysout ๋์ logback์ผ๋ก ํ์ธ ๊ถ์ฅ
properties:
hibernate:
format_sql: true
default_batch_fetch_size: 100 # N+1 ํด๊ฒฐ์ ์ํ In-query ๋ฐฐ์น ์ฌ์ด์ฆ
# ๋ก๊น
๋ ๋ฒจ ์ค์
logging:
level:
root: INFO
com.example.demo: DEBUG
org.hibernate.SQL: DEBUG # SQL ์ฟผ๋ฆฌ ๋ก๊น
org.hibernate.orm.jdbc.bind: TRACE # ๋ฐ์ธ๋ฉ ํ๋ผ๋ฏธํฐ ๊ฐ ํ์ (Spring Boot 3.x)
```
## 11. Spring Data JPA & ํธ๋์ญ์
(Data & Transaction)
```java
// Repository ์ธํฐํ์ด์ค ์ ์ (JpaRepository ๊ธฐ๋ณธ ๋ด์ฅ ๋ฉ์๋ ํ์ฉ)
public interface UserRepository extends JpaRepository<User, Long> {
// ์ฟผ๋ฆฌ ๋ฉ์๋ ํจํด (์ด๋ฆ ๊ธฐ๋ฐ ์๋ ์ฟผ๋ฆฌ ์์ฑ)
Optional<User> findByEmail(String email);
boolean existsByEmail(String email);
List<User> findByStatusAndCreatedAtAfter(UserStatus status, LocalDateTime dateTime);
// JPQL ์ง์ ์ ์ (@Query)
@Query("SELECT u FROM User u WHERE u.role = :role AND u.isActive = true")
List<User> findActiveUsersByRole(@Param("role") Role role);
// ๋ฒํฌ ์
๋ฐ์ดํธ/์ญ์ (@Modifying ํ์, ์์์ฑ ์ปจํ
์คํธ ์ด๊ธฐํ ์ค์ )
@Modifying(clearAutomatically = true)
@Query("UPDATE User u SET u.status = :newStatus WHERE u.lastLoginAt < :cutoff")
int bulkUpdateStatus(@Param("newStatus") UserStatus newStatus, @Param("cutoff") LocalDateTime cutoff);
}
// ์๋น์ค ๊ณ์ธต ํธ๋์ญ์
์ฒ๋ฆฌ
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true) // ํด๋์ค ์ ์ฒด ์ฝ๊ธฐ ์ ์ฉ (์ฑ๋ฅ ์ต์ ํ & ์ค๋
์ท ์๋ต)
public class UserService {
private final UserRepository userRepository;
public UserResponse findById(Long id) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค: " + id));
return UserResponse.from(user);
}
@Transactional // ์ฐ๊ธฐ/์์ ์์
์๋ ๋ฐ๋์ readOnly = false (๊ธฐ๋ณธ๊ฐ)
public Long updateName(Long id, String newName) {
User user = userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค."));
user.updateName(newName); // ๋ํฐ ์ฒดํน(Dirty Checking)์ผ๋ก save() ํธ์ถ ์์ด ์๋ UPDATE ์ฟผ๋ฆฌ ๋ฐ์
return user.getId();
}
}
```
## 12. Spring Security 6.x ๊ธฐ๋ณธ ๊ตฌ์ฑ (Security FilterChain)
```java
// Spring Boot 3.x (Spring Security 6.x ์ต์ ์ปดํฌ๋ํธ ์คํ์ผ)
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthFilter;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// 1. CSRF & FormLogin ๋นํ์ฑํ (Stateless REST API ํ๊ฒฝ)
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
// 2. ์ธ์
์ ์์ฑํ์ง ์๊ณ ์์ ๋ฌด์ํ(Stateless)๋ก ์ค์
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 3. URL ์์ฒญ๋ณ ์ ๊ทผ ๊ถํ ์ค์
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/auth/**", "/public/**", "/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
// 4. JWT ํํฐ๋ฅผ UsernamePasswordAuthenticationFilter ์ด์ ์ ๋ฐฐ์น
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder(); // ๋น๋ฐ๋ฒํธ ๋จ๋ฐฉํฅ ์ํธํ
}
}
```
## 13. ๋น๋๊ธฐ ์ฒ๋ฆฌ & ์ค์ผ์ค๋ง (Async & Scheduling)
```java
// [1] ๋น๋๊ธฐ ์ฒ๋ฆฌ ํ์ฑํ ๋ฐ ์คํ (@EnableAsync)
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "mailTaskExecutor")
public Executor mailTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(5);
executor.setMaxPoolSize(10);
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("async-mail-");
executor.initialize();
return executor;
}
}
@Service
public class EmailService {
@Async("mailTaskExecutor") // ์ง์ ๋ ์ค๋ ๋ํ์์ ๋ฐฑ๊ทธ๋ผ์ด๋ ๋น๋๊ธฐ ์คํ
public CompletableFuture<Boolean> sendWelcomeEmail(String toEmail) {
// ๊ธด ์๊ฐ์ด ์์๋๋ ์ธ๋ถ SMTP ๋ฉ์ผ ๋ฐ์ก ๋ก์ง
return CompletableFuture.completedFuture(true);
}
}
// [2] ์ฃผ๊ธฐ์ ์์
์ค์ผ์ค๋ง (@EnableScheduling)
@Configuration
@EnableScheduling
public class SchedulerConfig { }
@Component
public class CacheEvictScheduler {
// ๋งค์ผ ์๋ฒฝ 3์ ์คํ (์ด ๋ถ ์ ์ผ ์ ์์ผ)
@Scheduled(cron = "0 0 3 * * ?")
public void runDailyBatch() {
System.out.println("์ผ์ผ ๋ง๋ฃ ์บ์ ์ ๋ฆฌ ์คํ");
}
// ์ด์ ์์
์์ ๊ธฐ์ค 5์ด๋ง๋ค ์คํ
@Scheduled(fixedRate = 5000)
public void heartbeat() { }
}
```
## 14. ํ
์คํธ ์ฝ๋ ์์ฑ (Testing with MockMvc)
```java
// [1] ์ปจํธ๋กค๋ฌ ์ฌ๋ผ์ด์ค ํ
์คํธ (@WebMvcTest)
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Autowired
private ObjectMapper objectMapper;
@Test
@DisplayName("ํ์ ๋จ๊ฑด ์กฐํ ์ฑ๊ณต ์ 200 OK์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ฐํํ๋ค")
void getUserSuccess() throws Exception {
// given
UserResponse response = new UserResponse(1L, "lucky", "lucky@example.com");
given(userService.findById(1L)).willReturn(response);
// when & then
mockMvc.perform(get("/api/v1/users/1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1L))
.andExpect(jsonPath("$.name").value("lucky"))
.andExpect(jsonPath("$.email").value("lucky@example.com"));
}
}
// [2] ์๋น์ค ๋จ์ ํ
์คํธ (Mockito)
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
@DisplayName("์กด์ฌํ์ง ์๋ ์ ์ ์กฐํ ์ ResourceNotFoundException์ด ๋ฐ์ํ๋ค")
void userNotFoundThrowsException() {
// given
given(userRepository.findById(999L)).willReturn(Optional.empty());
// when & then
org.junit.jupiter.api.Assertions.assertThrows(
ResourceNotFoundException.class,
() -> userService.findById(999L)
);
}
}
```
## 15. ์ด์ ๋ชจ๋ํฐ๋ง & ์ต์ ํ (Actuator & Production)
```yaml
# Actuator ์ค์ (application.yml)
management:
endpoints:
web:
exposure:
# ์ค๋ฌด ๊ถ์ฅ: ํฌ์ค์ฒดํฌ, ์งํ ๋ฐ ํ๋ก๋ฉํ
์ฐ์ค๋ง ๋
ธ์ถ (env, beans ๋ฑ ๋ฏผ๊ฐ ์ ๋ณด ์จ๊น)
include: health, info, metrics, prometheus
base-path: /actuator
endpoint:
health:
show-details: when_authorized # ์์ธ ์ํ ์ ๋ณด๋ ๊ถํ ๋ณด์ ์์๊ฒ๋ง ๋
ธ์ถ
probes:
enabled: true # ์ฟ ๋ฒ๋คํฐ์ค Liveness, Readiness ํ๋ก๋ธ ์ง์
# ๋ฌด์ค๋จ ๋ฐฐํฌ๋ฅผ ์ํ Graceful Shutdown ์ค์
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 20s # ์ข
๋ฃ ์ ์ฒ๋ฆฌ ์ค์ธ ์์ฒญ ๋๊ธฐ ์๊ฐ (์ต๋ 20์ด)
```
์๊ฒฌ ๋ฐ ์ง๋ฌธ
0์์ง ๋ฑ๋ก๋ ์๊ฒฌ์ด ์์ต๋๋ค. ์ฒซ ๋ฒ์งธ ๋๊ธ์ ๋จ๊ฒจ๋ณด์ธ์!
๋๊ธ ์์
๋๊ธ ์ญ์