2025-04-08T19:56:24
This commit is contained in:
326
docs/01_Spring Boot MVC.md
Normal file
326
docs/01_Spring Boot MVC.md
Normal file
@@ -0,0 +1,326 @@
|
||||
아래는 Spring Boot MVC 프로젝트의 아키텍처, 프로젝트 구조, 패키징 전략, 레이어드 아키텍처, 그리고 Spring Initializr 사용법을 실무 중심으로 설명한 글입니다. SEO를 고려해 키워드를 자연스럽게 배치하고, 흥미로운 주제를 앞부분에 배치하며, 읽기 쉽게 작성했습니다. Gradle은 `build.gradle.kts`로 작성했으며, 코드 예제도 포함했습니다.
|
||||
|
||||
---
|
||||
|
||||
# Spring Boot MVC 프로젝트 아키텍처 완벽 정리: 구조, 패키징, 레이어드 아키텍처까지
|
||||
|
||||
Spring Boot는 MVC(Model-View-Controller) 패턴을 기반으로 빠르고 효율적인 웹 애플리케이션 개발을 가능하게 합니다. 실무에서 Spring Boot MVC 프로젝트를 시작하려면 아키텍처 설계가 중요하죠. 이 글에서는 **Spring Boot MVC 아키텍처**, **프로젝트 구조**, **패키징 전략**, 그리고 **레이어드 아키텍처**를 실무 관점에서 다룹니다. 보너스로 **Spring Initializr** 사용법까지 소개하니, 초보자부터 숙련자까지 유용할 거예요. 바로 시작해볼까요?
|
||||
|
||||
## 1. Spring Boot MVC 아키텍처란?
|
||||
Spring Boot MVC는 전통적인 MVC 패턴을 따르며, Spring의 강력한 기능을 더해줍니다.
|
||||
- **Model**: 데이터와 비즈니스 로직을 담당.
|
||||
- **View**: 사용자 인터페이스(HTML, Thymeleaf 등).
|
||||
- **Controller**: 요청을 처리하고 Model과 View를 연결.
|
||||
|
||||
Spring Boot는 설정을 자동화해 개발자가 비즈니스 로직에 집중할 수 있게 도와줍니다. 예를 들어, `@RestController`를 사용하면 JSON 응답을 쉽게 반환할 수 있죠.
|
||||
|
||||
**실무 팁**:
|
||||
- REST API 중심이라면 `@RestController`를, 템플릿 기반 웹이라면 `@Controller`와 Thymeleaf를 활용하세요.
|
||||
|
||||
## **1. Spring Boot MVC 주요 구성 요소**
|
||||
|
||||
Spring Boot MVC는 다음과 같은 핵심 요소로 구성됩니다.
|
||||
|
||||
### **1.1 DispatcherServlet (디스패처 서블릿)**
|
||||
- Spring MVC의 중심 역할을 담당하는 **프론트 컨트롤러**.
|
||||
- 모든 요청을 받아서 적절한 컨트롤러로 전달하고, 결과를 다시 응답으로 반환.
|
||||
- Spring Boot에서는 기본적으로 `DispatcherServlet`이 자동 설정됨.
|
||||
|
||||
### **1.2 컨트롤러 (Controller)**
|
||||
- 사용자의 요청을 받아 처리하는 클래스.
|
||||
- `@Controller` 또는 `@RestController`를 사용하여 정의.
|
||||
- 요청을 매핑하는 `@RequestMapping`, `@GetMapping`, `@PostMapping` 등의 어노테이션과 함께 사용.
|
||||
|
||||
### **1.3 서비스 (Service)**
|
||||
- 비즈니스 로직을 처리하는 계층.
|
||||
- `@Service` 어노테이션을 사용하여 선언.
|
||||
- 컨트롤러에서 받은 요청을 처리하고, 데이터를 가공하여 리포지토리 또는 외부 API와 연결.
|
||||
|
||||
### **1.4 리포지토리 (Repository)**
|
||||
- 데이터베이스와 직접적으로 통신하는 계층.
|
||||
- Spring Data JPA를 활용하여 `@Repository`로 선언하고, 엔터티를 저장/조회하는 기능을 수행.
|
||||
|
||||
### **1.5 모델 (Model)**
|
||||
- 응답 데이터로 전달될 객체.
|
||||
- `Model` 객체를 통해 View에 데이터를 전달.
|
||||
- JPA에서 `@Entity`를 사용하여 데이터베이스 테이블과 매핑.
|
||||
|
||||
### **1.6 뷰 (View)**
|
||||
- 클라이언트에게 데이터를 시각적으로 보여주는 역할.
|
||||
- Spring Boot에서는 기본적으로 **Thymeleaf**를 사용하여 HTML을 렌더링.
|
||||
|
||||
## 2. Spring Boot 프로젝트 구조: 실무에서 추천하는 방식
|
||||
Spring Boot 프로젝트를 처음 생성하면 기본 구조가 제공되지만, 실무에서는 역할별로 잘 정리된 구조가 필요합니다. 예시 구조를 보죠:
|
||||
|
||||
```
|
||||
com.example.myapp
|
||||
├── MyAppApplication.java # 메인 클래스
|
||||
├── controller # 요청 처리
|
||||
│ └── UserController.java
|
||||
├── service # 비즈니스 로직
|
||||
│ └── UserService.java
|
||||
├── repository # 데이터 접근
|
||||
│ └── UserRepository.java
|
||||
├── model # 엔티티
|
||||
│ └── User.java
|
||||
├── config # 설정 클래스
|
||||
│ └── WebConfig.java
|
||||
└── resources
|
||||
├── application.yml # 설정 파일
|
||||
└── templates # 뷰 템플릿 (Thymeleaf 등)
|
||||
```
|
||||
|
||||
**설명**:
|
||||
- `MyAppApplication.java`: 애플리케이션 진입점. `@SpringBootApplication` 어노테이션이 핵심입니다.
|
||||
- `controller`, `service`, `repository`: 레이어드 아키텍처를 반영한 분리.
|
||||
- `resources`: 정적 파일, 설정 파일, 뷰 템플릿을 관리.
|
||||
|
||||
## 3. 패키징 전략: 깔끔하고 유지보수 쉽게
|
||||
패키지를 어떻게 나누느냐는 프로젝트의 확장성과 가독성에 큰 영향을 미칩니다. 두 가지 접근법이 흔히 사용됩니다:
|
||||
|
||||
### 기능별 패키징 (Feature-based)
|
||||
```
|
||||
com.example.myapp
|
||||
├── user
|
||||
│ ├── UserController.java
|
||||
│ ├── UserService.java
|
||||
│ └── UserRepository.java
|
||||
├── product
|
||||
│ ├── ProductController.java
|
||||
│ ├── ProductService.java
|
||||
│ └── ProductRepository.java
|
||||
```
|
||||
|
||||
- **장점**: 모듈별로 독립적이어서 팀 작업에 유리.
|
||||
- **실무 추천**: 마이크로서비스로 전환할 가능성이 있다면 이 방식이 좋습니다.
|
||||
|
||||
### 레이어드 패키징 (Layer-based)
|
||||
위의 예시처럼 `controller`, `service`, `repository`로 나눕니다.
|
||||
- **장점**: 전통적이고 직관적. 소규모 프로젝트에 적합.
|
||||
- **실무 팁**: 패키지가 너무 커지면 하위 패키지로 세분화하세요 (예: `controller.api`, `controller.web`).
|
||||
|
||||
## 4. 레이어드 아키텍처: 계층별 역할과 코드 예제
|
||||
Spring Boot에서 레이어드 아키텍처는 책임을 명확히 분리해 유지보수를 쉽게 만듭니다. 각 레이어의 역할과 예제를 보죠.
|
||||
|
||||
### Controller Layer
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UserController {
|
||||
private final UserService userService;
|
||||
|
||||
@Autowired
|
||||
public UserController(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<User> getUser(@PathVariable Long id) {
|
||||
return ResponseEntity.ok(userService.getUser(id));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Service Layer
|
||||
```java
|
||||
@Service
|
||||
public class UserService {
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@Autowired
|
||||
public UserService(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
public User getUser(Long id) {
|
||||
return userRepository.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Repository Layer
|
||||
```java
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
}
|
||||
```
|
||||
|
||||
### Model Layer
|
||||
```java
|
||||
@Entity
|
||||
@Data
|
||||
public class User {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
private String name;
|
||||
private String email;
|
||||
}
|
||||
```
|
||||
|
||||
**실무 팁**:
|
||||
- **의존성 방향**: Controller → Service → Repository로 단방향 유지.
|
||||
- **예외 처리**: Service 레이어에서 비즈니스 예외를 처리하고, Controller에서 HTTP 상태로 변환하세요.
|
||||
|
||||
## 5. Spring Initializr로 프로젝트 시작하기
|
||||
Spring Initializr는 Spring Boot 프로젝트를 빠르게 생성하는 공식 도구입니다. 사용법은 간단합니다:
|
||||
|
||||
1. **웹사이트 접속**: [start.spring.io](https://start.spring.io)에 들어갑니다.
|
||||
2. **설정 선택**:
|
||||
- 프로젝트: Gradle (Kotlin)
|
||||
- 언어: Java
|
||||
- Spring Boot 버전: 최신 stable (예: 3.2.3)
|
||||
- 의존성: `Spring Web`, `Spring Data JPA`, `Lombok` 추가.
|
||||
3. **생성**: "Generate" 버튼을 눌러 다운로드.
|
||||
4. **압축 해제 후 실행**: IDE에서 열고 `gradlew bootRun`으로 실행.
|
||||
|
||||
### Gradle 설정 (build.gradle.kts)
|
||||
```kotlin
|
||||
plugins {
|
||||
id("org.springframework.boot") version "3.2.3"
|
||||
id("io.spring.dependency-management") version "1.1.4"
|
||||
java
|
||||
}
|
||||
|
||||
group = "com.example"
|
||||
version = "0.0.1-SNAPSHOT"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
implementation("org.projectlombok:lombok")
|
||||
annotationProcessor("org.projectlombok:lombok")
|
||||
runtimeOnly("com.h2database:h2")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
}
|
||||
|
||||
tasks {
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## **2. Spring Boot MVC 요청 흐름**
|
||||
|
||||
Spring Boot MVC에서 **클라이언트의 요청이 들어와서 응답이 반환되기까지의 과정**은 다음과 같이 진행됩니다.
|
||||
|
||||
### **1️⃣ 클라이언트의 요청**
|
||||
클라이언트(웹 브라우저, API 호출 등)가 특정 URL로 요청을 보냄.
|
||||
|
||||
```
|
||||
GET http://localhost:8080/home
|
||||
```
|
||||
|
||||
### **2️⃣ DispatcherServlet이 요청을 받음**
|
||||
Spring Boot에서는 `DispatcherServlet`이 모든 요청을 받아서 처리할 컨트롤러를 찾음.
|
||||
|
||||
### **3️⃣ 컨트롤러에서 요청 처리**
|
||||
컨트롤러는 요청을 처리하고, 필요한 경우 서비스 계층을 호출하여 데이터 가공을 수행.
|
||||
|
||||
```java
|
||||
@Controller
|
||||
public class HomeController {
|
||||
|
||||
@GetMapping("/home")
|
||||
public String home(Model model) {
|
||||
model.addAttribute("message", "Welcome to Spring Boot MVC!");
|
||||
return "home"; // home.html 뷰 반환
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **4️⃣ 서비스 계층 호출 (비즈니스 로직 수행)**
|
||||
서비스는 필요한 데이터 조회 및 가공을 수행.
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class HomeService {
|
||||
|
||||
public String getWelcomeMessage() {
|
||||
return "Welcome to Spring Boot MVC!";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
컨트롤러에서 이 서비스를 호출할 수도 있음.
|
||||
|
||||
```java
|
||||
@Controller
|
||||
public class HomeController {
|
||||
|
||||
private final HomeService homeService;
|
||||
|
||||
public HomeController(HomeService homeService) {
|
||||
this.homeService = homeService;
|
||||
}
|
||||
|
||||
@GetMapping("/home")
|
||||
public String home(Model model) {
|
||||
model.addAttribute("message", homeService.getWelcomeMessage());
|
||||
return "home"; // home.html 뷰 반환
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **5️⃣ 데이터베이스 조회 (필요한 경우)**
|
||||
서비스 계층이 데이터를 필요로 하면 리포지토리를 통해 데이터베이스에서 값을 가져옴.
|
||||
|
||||
```java
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public UserService(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
public List<User> getUsers() {
|
||||
return userRepository.findAll();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **6️⃣ 뷰에 데이터 전달**
|
||||
- 컨트롤러에서 `Model` 객체를 이용하여 데이터를 뷰로 전달.
|
||||
- `home.html`에서 이를 활용.
|
||||
|
||||
```html
|
||||
<!-- home.html -->
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<title>Home</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1 th:text="${message}"></h1>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
### **7️⃣ 뷰를 렌더링하여 응답 반환**
|
||||
- `DispatcherServlet`이 컨트롤러에서 받은 뷰(`home.html`)를 렌더링하고, HTML을 클라이언트에게 응답으로 반환.
|
||||
|
||||
## 마무리
|
||||
Spring Boot MVC 아키텍처는 실무에서 구조화된 설계로 생산성을 높여줍니다. **프로젝트 구조**와 **패키징 전략**을 잘 잡고, **레이어드 아키텍처**로 책임을 분리하면 유지보수도 쉬워지죠. Spring Initializr로 시작해 위 예제를 참고하면 금방 적응할 거예요. 질문이 있다면 언제든 물어보세요!
|
||||
|
||||
---
|
||||
|
||||
### SEO 및 가독성 반영
|
||||
- **키워드**: "Spring Boot MVC 아키텍처", "Spring Boot 프로젝트 구조", "레이어드 아키텍처" 등 자연스럽게 사용.
|
||||
- **흥미로운 주제 전반부**: 아키텍처와 구조를 먼저 다뤄 실무 개발자의 관심을 끌었음.
|
||||
- **가독성**: 소제목, 코드 블록, 간결한 설명으로 구성.
|
||||
|
||||
추가로 보완할 내용이나 다른 요청이 있다면 말씀해주세요!
|
||||
213
docs/02_설정 및 의존성 관리.md
Normal file
213
docs/02_설정 및 의존성 관리.md
Normal file
@@ -0,0 +1,213 @@
|
||||
아래는 Spring Boot의 기본 설정 및 의존성 관리에 대해 실무 중심으로 설명한 글입니다. SEO를 고려해 키워드를 적절히 배치하고, 흥미로운 주제를 앞부분에 배치하며, 읽기 쉽게 구성했습니다. 주요 의존성을 정리하고 설정 방법을 코드 예제와 함께 설명했으며, Gradle은 Kotlin DSL(`build.gradle.kts`)로 작성했습니다.
|
||||
|
||||
---
|
||||
|
||||
## Spring Boot 기본 설정과 의존성 관리: 실무에서 바로 써먹는 가이드
|
||||
|
||||
Spring Boot는 복잡한 설정을 줄이고 개발을 빠르게 시작할 수 있게 해주는 프레임워크입니다. 하지만 실무에서 효율적으로 사용하려면 **기본 설정**과 **의존성 관리**를 제대로 이해해야 하죠. 이 글에서는 **Spring Boot 설정 방법**, **주요 의존성 정리**, 그리고 각 의존성의 설정 예제를 다룹니다. "Spring Boot 의존성 관리"나 "Spring Boot 설정" 같은 키워드로 검색하는 개발자라면 꼭 읽어보세요!
|
||||
|
||||
### 1. Spring Boot 기본 설정: 시작은 이렇게!
|
||||
Spring Boot는 기본 설정을 `application.properties` 또는 `application.yml` 파일로 관리합니다. YAML 형식이 가독성이 좋아 실무에서 더 자주 사용되죠. 기본 설정 파일은 `src/main/resources` 폴더에 위치합니다.
|
||||
|
||||
#### 기본 설정 예제 (application.yml)
|
||||
```yaml
|
||||
server:
|
||||
port: 8080
|
||||
spring:
|
||||
application:
|
||||
name: my-app
|
||||
datasource:
|
||||
url: jdbc:h2:mem:testdb
|
||||
driver-class-name: org.h2.Driver
|
||||
username: sa
|
||||
password:
|
||||
logging:
|
||||
level:
|
||||
root: INFO
|
||||
com.example: DEBUG
|
||||
```
|
||||
|
||||
**주요 설정 항목**:
|
||||
- `server.port`: 애플리케이션 포트 (기본값: 8080).
|
||||
- `spring.application.name`: 애플리케이션 이름 (마이크로서비스에서 유용).
|
||||
- `spring.datasource`: 데이터베이스 연결 설정.
|
||||
- `logging.level`: 로그 레벨 조정.
|
||||
|
||||
**실무 팁**:
|
||||
- 환경별 설정 분리: `application-dev.yml`, `application-prod.yml`로 나눠 관리하세요. 실행 시 `-Dspring.profiles.active=dev`로 선택 가능.
|
||||
- 민감 정보는 환경 변수나 외부 설정 관리 도구(Vault 등)로 처리하세요.
|
||||
|
||||
### 2. 의존성 관리: Spring Boot의 강력한 무기
|
||||
Spring Boot는 `spring-boot-starter`라는 의존성 번들로 필요한 라이브러리를 쉽게 추가할 수 있습니다. Gradle이나 Maven으로 관리하며, 버전 충돌 걱정 없이 사용할 수 있도록 자동으로 호환 버전을 맞춰줍니다.
|
||||
|
||||
#### Gradle 설정 (build.gradle.kts)
|
||||
```kotlin
|
||||
plugins {
|
||||
id("org.springframework.boot") version "3.2.3"
|
||||
id("io.spring.dependency-management") version "1.1.4"
|
||||
java
|
||||
}
|
||||
|
||||
group = "com.example"
|
||||
version = "0.0.1-SNAPSHOT"
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter")
|
||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||
}
|
||||
|
||||
tasks {
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `spring-boot-starter`: 기본 의존성 포함 (Spring Core, Logging 등).
|
||||
- `io.spring.dependency-management`: 의존성 버전 관리 플러그인.
|
||||
|
||||
### 3. 주요 의존성과 설정 방법
|
||||
Spring Boot에서 자주 사용하는 의존성과 설정 방법을 정리했습니다. 실무에서 꼭 필요한 것들만 골라봤어요.
|
||||
|
||||
#### 3.1. Spring Web (REST API 및 MVC)
|
||||
- **의존성**: `spring-boot-starter-web`
|
||||
- **설정**: 기본적으로 내장 Tomcat 서버가 포함되며, 추가 설정 없이 REST API를 만들 수 있습니다.
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||
}
|
||||
```
|
||||
|
||||
**예제**:
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class HelloController {
|
||||
@GetMapping("/hello")
|
||||
public String hello() {
|
||||
return "Hello, Spring Boot!";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2. Spring Data JPA (데이터베이스 연동)
|
||||
- **의존성**: `spring-boot-starter-data-jpa`
|
||||
- **추가 의존성**: 데이터베이스 드라이버 (예: H2, MySQL).
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
runtimeOnly("com.h2database:h2") // H2 인메모리 DB
|
||||
}
|
||||
```
|
||||
|
||||
**설정 (application.yml)**:
|
||||
```yaml
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:h2:mem:testdb
|
||||
driver-class-name: org.h2.Driver
|
||||
username: sa
|
||||
password:
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
show-sql: true
|
||||
```
|
||||
|
||||
**실무 팁**:
|
||||
- `ddl-auto`: 개발 시 `update`, 운영 시 `none`으로 설정.
|
||||
- 성능을 위해 `@Entity`에 `fetch = FetchType.LAZY`를 기본으로 사용하세요.
|
||||
|
||||
#### 3.3. Spring Boot Actuator (모니터링)
|
||||
- **의존성**: `spring-boot-starter-actuator`
|
||||
- **설정**: 애플리케이션 상태를 모니터링할 수 있는 엔드포인트 제공.
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||
}
|
||||
```
|
||||
|
||||
**설정 (application.yml)**:
|
||||
```yaml
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health, info, metrics
|
||||
```
|
||||
|
||||
- `/actuator/health`, `/actuator/metrics` 같은 엔드포인트로 상태 확인 가능.
|
||||
|
||||
#### 3.4. Lombok (코드 간소화)
|
||||
- **의존성**: `lombok`
|
||||
- **설정**: 어노테이션 프로세서를 추가해 IDE에서 동작하도록 설정.
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation("org.projectlombok:lombok")
|
||||
annotationProcessor("org.projectlombok:lombok")
|
||||
}
|
||||
```
|
||||
|
||||
**예제**:
|
||||
```java
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class User {
|
||||
private Long id;
|
||||
private String name;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.5. Spring Security (인증 및 권한)
|
||||
- **의존성**: `spring-boot-starter-security`
|
||||
- **설정**: 기본적으로 모든 요청에 인증 요구.
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter-security")
|
||||
}
|
||||
```
|
||||
|
||||
**설정 예제**:
|
||||
```java
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig extends WebSecurityConfigurerAdapter {
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeRequests()
|
||||
.antMatchers("/public/**").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.formLogin();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**실무 팁**: JWT나 OAuth2를 추가로 연동하면 토큰 기반 인증도 가능합니다.
|
||||
|
||||
### 4. 의존성 관리 팁
|
||||
- **버전 명시 피하기**: `spring-boot-starter`가 제공하는 기본 버전을 사용하세요.
|
||||
- **BOM 활용**: `spring-boot-dependencies` BOM(Bill of Materials)이 호환성을 보장합니다.
|
||||
- **의존성 충돌 확인**: `./gradlew dependencies`로 확인 후 불필요한 의존성 제거.
|
||||
|
||||
### 마무리
|
||||
Spring Boot의 기본 설정과 의존성 관리는 실무에서 생산성을 크게 높여줍니다. 이 글에서 다룬 **기본 설정 파일**, **주요 의존성**, **설정 방법**을 참고해 프로젝트를 시작해보세요. "Spring Boot 설정"으로 검색했다면 이 글이 딱 맞을 거예요. 궁금한 점은 댓글로 남겨주세요!
|
||||
|
||||
---
|
||||
|
||||
### SEO 및 가독성 반영
|
||||
- **키워드**: "Spring Boot 기본 설정", "Spring Boot 의존성 관리", "Spring Boot 설정 방법" 등 자연스럽게 배치.
|
||||
- **흥미로운 주제 전반부**: 기본 설정과 의존성 관리의 중요성을 앞부분에 강조.
|
||||
- **가독성**: 소제목, 코드 블록, 짧은 문장으로 구성.
|
||||
|
||||
추가 요청이나 보완할 내용이 있다면 말씀해주세요!
|
||||
209
docs/03_application.yml.md
Normal file
209
docs/03_application.yml.md
Normal file
@@ -0,0 +1,209 @@
|
||||
# Spring Boot에서 application.yaml
|
||||
|
||||
Spring Boot는 기본적으로 `application.properties`를 제공하지만, YAML(`.yaml`) 형식이 계층 구조를 표현하기에 더 적합합니다. 실무에서 환경별 설정(개발, 운영 등)을 분리하거나 복잡한 설정을 관리할 때 유리하죠.
|
||||
|
||||
- **위치**: `src/main/resources/application.yaml`
|
||||
- **특징**: 키-값 쌍을 계층적으로 작성, 공백으로 들여쓰기.
|
||||
|
||||
**실무 팁**:
|
||||
- `.properties`보다 사람이 읽기 쉬운 YAML을 선호하세요.
|
||||
- 환경별로 `application-dev.yaml`, `application-prod.yaml`처럼 나눠 관리하세요.
|
||||
|
||||
## application.yaml 예시와 주요 설정
|
||||
|
||||
| 옵션 | 설명 |
|
||||
|---|---|
|
||||
| `server.port` | 애플리케이션이 실행될 포트 지정 |
|
||||
| `server.servlet.context-path` | 애플리케이션의 기본 컨텍스트 경로 지정 |
|
||||
| `spring.application.name` | 애플리케이션 이름 설정 |
|
||||
| `spring.profiles.active` | 활성화할 프로파일 지정 |
|
||||
| `spring.datasource.url` | 데이터베이스 연결 URL 설정 |
|
||||
| `spring.datasource.username` | 데이터베이스 접속 사용자명 |
|
||||
| `spring.datasource.password` | 데이터베이스 접속 비밀번호 |
|
||||
| `spring.datasource.driver-class-name` | 사용할 데이터베이스 드라이버 클래스 지정 |
|
||||
| `spring.jpa.database-platform` | JPA가 사용할 데이터베이스 플랫폼 (예: `org.hibernate.dialect.MySQL5Dialect`) |
|
||||
| `spring.jpa.hibernate.ddl-auto` | Hibernate의 자동 DDL 생성 옵션 (예: `update`, `create-drop`) |
|
||||
| `spring.jpa.show-sql` | 실행되는 SQL 로그 출력 여부 (`true` / `false`) |
|
||||
| `spring.jpa.properties.hibernate.format_sql` | SQL을 보기 좋게 포맷하여 출력 여부 (`true` / `false`) |
|
||||
| `logging.level.[패키지명]` | 특정 패키지의 로그 레벨 설정 (예: `DEBUG`, `INFO`) |
|
||||
| `spring.messages.basename` | 다국어 메시지 번들 설정 (예: `messages`) |
|
||||
| `spring.thymeleaf.cache` | Thymeleaf 템플릿 캐싱 활성화 여부 (`true` / `false`) |
|
||||
| `spring.mail.host` | SMTP 메일 서버 호스트 주소 |
|
||||
| `spring.mail.port` | SMTP 메일 서버 포트 번호 |
|
||||
| `spring.mail.username` | SMTP 메일 서버 사용자 이름 |
|
||||
| `spring.mail.password` | SMTP 메일 서버 비밀번호 |
|
||||
| `spring.mail.properties.mail.smtp.auth` | SMTP 인증 여부 설정 (`true` / `false`) |
|
||||
| `spring.redis.host` | Redis 서버 호스트 주소 |
|
||||
| `spring.redis.port` | Redis 서버 포트 번호 |
|
||||
| `spring.redis.password` | Redis 서버 비밀번호 |
|
||||
| `management.endpoints.web.exposure.include` | Actuator 엔드포인트 노출 범위 지정 |
|
||||
| `management.endpoint.health.show-details` | Actuator의 `/health` 엔드포인트 상세 정보 출력 여부 |
|
||||
|
||||
|
||||
```yaml
|
||||
server:
|
||||
port: 8080 # 애플리케이션 실행 포트
|
||||
servlet:
|
||||
context-path: /myapp # 기본 URL을 `/myapp`로 설정 (http://localhost:8080/myapp)
|
||||
spring:
|
||||
application:
|
||||
name: my-app # 애플리케이션 이름
|
||||
datasource: # 데이터베이스 설정
|
||||
url: jdbc:h2:mem:testdb # H2 인메모리 DB 연결 설정
|
||||
driver-class-name: org.h2.Driver
|
||||
username: sa
|
||||
password:
|
||||
jpa: # JPA 설정
|
||||
hibernate:
|
||||
ddl-auto: update # 테이블 자동 생성 (`create`, `update`, `validate`, `none` 등)
|
||||
show-sql: true # Hibernate 쿼리가 로그로 출력
|
||||
thymeleaf: # Thymeleaf 설정
|
||||
cache: false # 개발 시 템플릿 캐시를 비활성화하여 변경 사항 즉시 반영
|
||||
messages: # 다국어(i18n) 설정
|
||||
basename: messages # `messages.properties`, `messages_ko.properties` 등 다국어 파일을 설정
|
||||
mail: # 이메일 설정
|
||||
host: smtp.gmail.com
|
||||
port: 587
|
||||
username: myemail@gmail.com
|
||||
password: mypassword
|
||||
properties:
|
||||
mail.smtp.auth: true
|
||||
mail.smtp.starttls.enable: true
|
||||
redis: # Redis 설정
|
||||
host: localhost # Redis 서버 주소
|
||||
port: 6379 # Redis 서버 포트
|
||||
password: secret # Redis 접속 비밀번호
|
||||
profiles: # 프로파일 설정
|
||||
active: dev # `dev` 프로파일을 활성화
|
||||
logging: # 로깅 설정
|
||||
level:
|
||||
root: INFO # 기본 로그 레벨을 INFO로 설정
|
||||
com.example: DEBUG # `com.example` 패키지의 로그 레벨을 DEBUG로 설정
|
||||
file:
|
||||
name: logs/my-app.log # 로그 파일의 경로와 이름
|
||||
management: # Spring Boot Actuator 설정
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health, info # Actuator에서 `/health`, `/info` 엔드포인트 활성화
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always # `/health`에서 상세 정보 제공
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: prod
|
||||
datasource:
|
||||
url: jdbc:mysql://localhost:3306/mydb # MySQL 연결 URL
|
||||
username: root # DB 사용자명
|
||||
password: secret # DB 비밀번호. 운영 환경에서는 환경 변수(`SPRING_DATASOURCE_PASSWORD`)로 관리
|
||||
driver-class-name: com.mysql.cj.jdbc.Driver #JDBC 드라이버 설정
|
||||
jpa:
|
||||
database-platform: org.hibernate.dialect.MySQL8Dialect # Hibernate가 사용할 데이터베이스 방언
|
||||
hibernate:
|
||||
ddl-auto: update
|
||||
show-sql: true
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true # SQL을 보기 좋게 출력
|
||||
```
|
||||
|
||||
|
||||
## 다중 프로파일 설정
|
||||
스프링 부트에서는 `---` (YAML 문서 구분선)을 사용하여 `application.yaml` 파일 내에서 여러 프로파일을 정의합니다. 각 섹션은 `spring.config.activate.on-profile` 속성을 통해 특정 프로파일에 연결됩니다. 실행 시 활성화된 프로파일에 해당하는 설정만 적용됩니다.
|
||||
|
||||
- **공통 설정**: 파일 상단에 위치하며, 모든 프로파일에서 적용됩니다.
|
||||
- **프로파일별 설정**: `---`로 구분된 섹션에서 `spring.config.activate.on-profile`로 프로파일을 지정합니다.
|
||||
|
||||
### 기본 설정 (`application.yaml`)
|
||||
```yaml
|
||||
spring:
|
||||
profiles:
|
||||
active: dev
|
||||
```
|
||||
위 설정은 **`application-dev.yaml`** 파일을 활성화합니다.
|
||||
`spring.profiles.active=prod`로 변경하면 운영 환경 설정이 적용됨.
|
||||
|
||||
### 개발 환경 (`application-dev.yaml`)
|
||||
```yaml
|
||||
server:
|
||||
port: 8081
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:h2:mem:testdb
|
||||
username: sa
|
||||
password:
|
||||
```
|
||||
### 운영 환경 (`application-prod.yaml`)
|
||||
```yaml
|
||||
server:
|
||||
port: 8080
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:mysql://prod-db:3306/proddb
|
||||
username: admin
|
||||
password: securepassword
|
||||
```
|
||||
|
||||
### 프로파일 활성화 방법
|
||||
특정 프로파일을 활성화하려면 아래 방법 중 하나를 사용합니다:
|
||||
|
||||
#### 실행 옵션으로 지정
|
||||
JVM 실행 시 `-Dspring.profiles.active` 속성을 전달합니다.
|
||||
```bash
|
||||
java -jar my-app.jar -Dspring.profiles.active=dev
|
||||
```
|
||||
|
||||
#### 환경 변수로 지정
|
||||
운영 체제 환경 변수로 설정합니다.
|
||||
```bash
|
||||
export SPRING_PROFILES_ACTIVE=dev
|
||||
java -jar my-app.jar
|
||||
```
|
||||
|
||||
#### `application.yaml`에 기본값 설정
|
||||
파일 내에서 기본 프로파일을 지정할 수 있습니다.
|
||||
```yaml
|
||||
spring:
|
||||
profiles:
|
||||
active: dev # 기본 프로파일
|
||||
```
|
||||
|
||||
#### 코드에서 프로파일 지정
|
||||
`SpringApplication`을 사용할 때 프로파일을 설정할 수 있습니다.
|
||||
```java
|
||||
@SpringBootApplication
|
||||
public class MyApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication app = new SpringApplication(MyApplication.class);
|
||||
app.setAdditionalProfiles("dev");
|
||||
app.run(args);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## `@Value`
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class ConfigController {
|
||||
|
||||
@Value("${spring.application.name}")
|
||||
private String appName;
|
||||
|
||||
@Value("${server.port}")
|
||||
private int port;
|
||||
|
||||
@GetMapping("/config")
|
||||
public Map<String, Object> getConfig() {
|
||||
Map<String, Object> config = new HashMap<>();
|
||||
config.put("applicationName", appName);
|
||||
config.put("serverPort", port);
|
||||
return config;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
442
docs/04_컨트롤러와 요청 처리.md
Normal file
442
docs/04_컨트롤러와 요청 처리.md
Normal file
@@ -0,0 +1,442 @@
|
||||
## Spring Boot 컨트롤러와 요청 처리
|
||||
|
||||
Spring Boot에서 **컨트롤러**는 클라이언트 요청을 처리하는 핵심 역할을 합니다. REST API든 웹 페이지든, 요청을 받아 적절히 응답하는 로직을 작성하는 게 컨트롤러의 임무죠. 이 글에서는 **Spring Boot 컨트롤러 작성법**, **요청 처리 방법**, 그리고 **실제 예제**를 통해 실무에서 바로 써먹을 수 있는 노하우를 다룹니다. "Spring Boot 컨트롤러 예제"나 "Spring Boot 요청 처리"로 검색한 개발자라면 이 글이 딱 맞아요. 바로 시작합시다!
|
||||
|
||||
### 1. 컨트롤러란? Spring Boot에서의 역할
|
||||
Spring Boot에서 컨트롤러는 **HTTP 요청**을 받아 비즈니스 로직을 호출하고, 결과를 클라이언트에 반환합니다. `@Controller` 또는 `@RestController` 어노테이션을 사용하며, REST API 개발이 늘어나면서 `@RestController`가 더 자주 보이죠.
|
||||
|
||||
- `@Controller`: 뷰(HTML 등)를 반환할 때 사용.
|
||||
- `@RestController`: JSON, XML 같은 데이터를 직접 반환 (REST API에 적합).
|
||||
|
||||
**실무 팁**:
|
||||
- API와 웹을 분리한다면, `@RestController`는 `/api` 경로, `@Controller`는 `/web` 경로로 구분하세요.
|
||||
|
||||
### 2. 기본 컨트롤러 작성과 요청 처리
|
||||
간단한 예제부터 시작해봅시다. 사용자를 조회하는 REST API를 만들어볼게요.
|
||||
|
||||
| 기능 | 어노테이션 | 설명 |
|
||||
|------|----------|------|
|
||||
| **기본 컨트롤러** | `@RestController` | REST API 컨트롤러 정의 |
|
||||
| **GET 요청 처리** | `@GetMapping` | HTTP GET 요청 매핑 |
|
||||
| **POST 요청 처리** | `@PostMapping` | HTTP POST 요청 매핑 |
|
||||
| **쿼리 파라미터** | `@RequestParam` | URL 쿼리 매개변수 받기 |
|
||||
| **URL 경로 변수** | `@PathVariable` | URL 경로에서 값 추출 |
|
||||
| **JSON 요청 본문** | `@RequestBody` | 요청 본문을 객체로 매핑 |
|
||||
| **요청 헤더 처리** | `@RequestHeader` | 요청 헤더 값을 읽기 |
|
||||
| **응답 처리** | `ResponseEntity` | 상태 코드와 함께 응답 반환 |
|
||||
| **예외 처리** | `@ExceptionHandler` | 예외 발생 시 응답 처리 |
|
||||
|
||||
#### 기본 예제
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UserController {
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<String> getUser(@PathVariable Long id) {
|
||||
return ResponseEntity.ok("User ID: " + id);
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<String> createUser(@RequestBody UserRequest request) {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body("User created: " + request.getName());
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public class UserRequest {
|
||||
private String name;
|
||||
private String email;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 다양한 요청 처리 방법
|
||||
실무에서는 GET, POST 외에도 다양한 HTTP 메서드와 파라미터를 다룹니다. 예제를 통해 알아보죠.
|
||||
|
||||
#### 쿼리 파라미터 처리
|
||||
```java
|
||||
@GetMapping("/search")
|
||||
public ResponseEntity<List<String>> searchUsers(@RequestParam String name,
|
||||
@RequestParam(defaultValue = "10") int limit) {
|
||||
List<String> users = Arrays.asList(name + "1", name + "2"); // 더미 데이터
|
||||
return ResponseEntity.ok(users.subList(0, Math.min(limit, users.size())));
|
||||
}
|
||||
```
|
||||
|
||||
- `@RequestParam`: 쿼리 파라미터 (`?name=John&limit=5`) 처리.
|
||||
- `defaultValue`: 값이 없으면 기본값 적용.
|
||||
|
||||
**실행**:
|
||||
- GET `/api/users/search?name=John&limit=2` → `["John1", "John2"]`
|
||||
|
||||
#### 경로 변수와 혼합 사용
|
||||
```java
|
||||
@GetMapping("/{id}/details")
|
||||
public ResponseEntity<String> getUserDetails(@PathVariable Long id,
|
||||
@RequestParam String format) {
|
||||
return ResponseEntity.ok("User " + id + " in " + format + " format");
|
||||
}
|
||||
```
|
||||
|
||||
**실행**:
|
||||
- GET `/api/users/1/details?format=json` → "User 1 in json format"
|
||||
|
||||
#### 예외 처리
|
||||
```java
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<String> getUser(@PathVariable Long id) {
|
||||
if (id <= 0) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid ID");
|
||||
}
|
||||
return ResponseEntity.ok("User ID: " + id);
|
||||
}
|
||||
```
|
||||
|
||||
- `ResponseStatusException`: HTTP 상태 코드와 메시지를 반환.
|
||||
|
||||
### 4. 실무에서 유용한 컨트롤러 팁
|
||||
컨트롤러를 더 효율적으로 작성하는 방법을 정리했어요.
|
||||
|
||||
#### 4.1. 서비스 레이어와 분리
|
||||
컨트롤러는 요청/응답만 처리하고, 비즈니스 로직은 서비스로 분리하세요.
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UserController {
|
||||
private final UserService userService;
|
||||
|
||||
@Autowired
|
||||
public UserController(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<User> getUser(@PathVariable Long id) {
|
||||
return ResponseEntity.ok(userService.findUserById(id));
|
||||
}
|
||||
}
|
||||
|
||||
@Service
|
||||
public class UserService {
|
||||
public User findUserById(Long id) {
|
||||
return new User(id, "John Doe", "john@example.com"); // 더미 데이터
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class User {
|
||||
private Long id;
|
||||
private String name;
|
||||
private String email;
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.2. 유효성 검사
|
||||
요청 데이터의 유효성을 검사해 안정성을 높이세요.
|
||||
|
||||
```java
|
||||
@PostMapping
|
||||
public ResponseEntity<String> createUser(@Valid @RequestBody UserRequest request,
|
||||
BindingResult result) {
|
||||
if (result.hasErrors()) {
|
||||
return ResponseEntity.badRequest().body(result.getAllErrors().toString());
|
||||
}
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body("User created: " + request.getName());
|
||||
}
|
||||
|
||||
@Data
|
||||
public class UserRequest {
|
||||
@NotBlank(message = "Name is required")
|
||||
private String name;
|
||||
@Email(message = "Invalid email")
|
||||
private String email;
|
||||
}
|
||||
```
|
||||
|
||||
- `@Valid`: 요청 객체 검증.
|
||||
- `BindingResult`: 오류 처리.
|
||||
|
||||
#### 4.3. 전역 예외 처리
|
||||
`@ControllerAdvice`로 공통 예외를 처리하세요.
|
||||
|
||||
```java
|
||||
@ControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(ResponseStatusException.class)
|
||||
public ResponseEntity<String> handleResponseStatusException(ResponseStatusException ex) {
|
||||
return ResponseEntity.status(ex.getStatus()).body(ex.getReason());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 1. 어노테이션 정리표
|
||||
|
||||
| 어노테이션 | 설명 |
|
||||
|--------------------|-----------------------------------------|
|
||||
| `@Controller` | Spring MVC의 컨트롤러 클래스를 정의 |
|
||||
| `@RestController` | `@Controller` + `@ResponseBody`, JSON 응답을 기본으로 함 |
|
||||
| `@RequestMapping` | URL 요청을 특정 컨트롤러 또는 메서드에 매핑 |
|
||||
| `@GetMapping` | HTTP GET 요청을 특정 메서드에 매핑 |
|
||||
| `@PostMapping` | HTTP POST 요청을 특정 메서드에 매핑 |
|
||||
| `@PutMapping` | HTTP PUT 요청을 특정 메서드에 매핑 |
|
||||
| `@DeleteMapping` | HTTP DELETE 요청을 특정 메서드에 매핑 |
|
||||
| `@PatchMapping` | HTTP PATCH 요청을 특정 메서드에 매핑 |
|
||||
| `@RequestParam` | 요청 파라미터를 메서드의 파라미터로 매핑 |
|
||||
| `@PathVariable` | URL 경로 변수를 메서드의 파라미터로 매핑 |
|
||||
| `@ModelAttribute` | 폼 데이터를 객체로 변환하여 전달 |
|
||||
| `@RequestBody` | 요청 본문(JSON 등)을 객체로 변환하여 전달 |
|
||||
| `@ResponseBody` | 반환 데이터를 JSON 형태로 응답 |
|
||||
| `@ResponseStatus` | HTTP 응답 상태 코드를 지정 |
|
||||
| `@ExceptionHandler` | 특정 예외 발생 시 처리할 메서드를 정의 |
|
||||
| `@InitBinder` | 컨트롤러에서 요청 데이터를 변환하는 바인딩 설정을 정의 |
|
||||
| `@CrossOrigin` | 다른 도메인에서 API 요청을 허용하도록 설정 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 어노테이션 설명 및 예제
|
||||
|
||||
### 1) `@Controller`
|
||||
Spring MVC 컨트롤러 클래스임을 나타냅니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Controller
|
||||
public class MyController {
|
||||
@GetMapping("/hello")
|
||||
public String hello() {
|
||||
return "hello"; // hello.html을 렌더링
|
||||
}
|
||||
}
|
||||
```
|
||||
- `hello.html` 뷰 페이지를 반환합니다.
|
||||
|
||||
---
|
||||
|
||||
### 2) `@RestController`
|
||||
`@Controller`와 `@ResponseBody`를 합친 역할을 합니다. 즉, JSON 응답을 기본으로 합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@RestController
|
||||
public class MyRestController {
|
||||
@GetMapping("/api/hello")
|
||||
public String hello() {
|
||||
return "Hello, World!";
|
||||
}
|
||||
}
|
||||
```
|
||||
- `"Hello, World!"`라는 문자열을 JSON 형식으로 반환합니다.
|
||||
|
||||
---
|
||||
|
||||
### 3) `@RequestMapping`
|
||||
URL과 컨트롤러 메서드를 매핑합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Controller
|
||||
@RequestMapping("/users")
|
||||
public class UserController {
|
||||
@GetMapping("/{id}")
|
||||
public String getUser(@PathVariable Long id) {
|
||||
return "user"; // user.html 렌더링
|
||||
}
|
||||
}
|
||||
```
|
||||
- `/users/{id}` 경로로 들어오는 요청을 `getUser` 메서드가 처리합니다.
|
||||
|
||||
---
|
||||
|
||||
### 4) `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping`, `@PatchMapping`
|
||||
각 HTTP 메서드에 대한 매핑을 제공합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/items")
|
||||
public class ItemController {
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public String getItem(@PathVariable Long id) {
|
||||
return "Item: " + id;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String createItem(@RequestBody String item) {
|
||||
return "Created: " + item;
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public String updateItem(@PathVariable Long id, @RequestBody String item) {
|
||||
return "Updated item " + id + " to " + item;
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public String deleteItem(@PathVariable Long id) {
|
||||
return "Deleted item " + id;
|
||||
}
|
||||
}
|
||||
```
|
||||
- 각각 `GET`, `POST`, `PUT`, `DELETE` 요청을 처리하는 컨트롤러입니다.
|
||||
|
||||
---
|
||||
|
||||
### 5) `@RequestParam`
|
||||
쿼리 파라미터를 매핑할 때 사용합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@RestController
|
||||
public class ParamController {
|
||||
@GetMapping("/search")
|
||||
public String search(@RequestParam String query) {
|
||||
return "Searching for: " + query;
|
||||
}
|
||||
}
|
||||
```
|
||||
- `/search?query=Spring` 요청 시 `"Searching for: Spring"` 반환.
|
||||
|
||||
---
|
||||
|
||||
### 6) `@PathVariable`
|
||||
URL 경로 변수를 매핑할 때 사용합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@RestController
|
||||
public class PathVariableController {
|
||||
@GetMapping("/product/{id}")
|
||||
public String getProduct(@PathVariable Long id) {
|
||||
return "Product ID: " + id;
|
||||
}
|
||||
}
|
||||
```
|
||||
- `/product/100` 요청 시 `"Product ID: 100"` 반환.
|
||||
|
||||
---
|
||||
|
||||
### 7) `@ModelAttribute`
|
||||
폼 데이터를 객체로 바인딩할 때 사용합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Controller
|
||||
public class FormController {
|
||||
@PostMapping("/submit")
|
||||
public String submit(@ModelAttribute User user) {
|
||||
return "result"; // result.html 렌더링
|
||||
}
|
||||
}
|
||||
|
||||
class User {
|
||||
private String name;
|
||||
private int age;
|
||||
// Getter & Setter 생략
|
||||
}
|
||||
```
|
||||
- 폼에서 `name`과 `age` 값을 받아 `User` 객체로 변환.
|
||||
|
||||
---
|
||||
|
||||
### 8) `@RequestBody`
|
||||
JSON 데이터를 객체로 변환할 때 사용합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@RestController
|
||||
public class JsonController {
|
||||
@PostMapping("/json")
|
||||
public String receiveJson(@RequestBody User user) {
|
||||
return "Received: " + user.getName();
|
||||
}
|
||||
}
|
||||
```
|
||||
- `{ "name": "Alice", "age": 25 }` 데이터를 `User` 객체로 변환.
|
||||
|
||||
---
|
||||
|
||||
### 9) `@ResponseBody`
|
||||
메서드의 반환값을 HTTP 응답으로 직접 반환할 때 사용합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Controller
|
||||
public class ResponseController {
|
||||
@ResponseBody
|
||||
@GetMapping("/text")
|
||||
public String textResponse() {
|
||||
return "Hello, ResponseBody!";
|
||||
}
|
||||
}
|
||||
```
|
||||
- `"Hello, ResponseBody!"`가 그대로 반환.
|
||||
|
||||
---
|
||||
|
||||
### 10) `@ResponseStatus`
|
||||
HTTP 응답 상태 코드를 설정할 때 사용합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@RestController
|
||||
public class StatusController {
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
@PostMapping("/create")
|
||||
public String create() {
|
||||
return "Created successfully!";
|
||||
}
|
||||
}
|
||||
```
|
||||
- HTTP 201 Created 응답을 반환.
|
||||
|
||||
---
|
||||
|
||||
### 11) `@ExceptionHandler`
|
||||
예외 발생 시 처리할 메서드를 정의합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@RestController
|
||||
public class ExceptionController {
|
||||
@GetMapping("/error")
|
||||
public String error() {
|
||||
throw new RuntimeException("Something went wrong!");
|
||||
}
|
||||
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public String handleRuntimeException(RuntimeException e) {
|
||||
return "Handled error: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
```
|
||||
- `/error` 요청 시 `"Handled error: Something went wrong!"` 반환.
|
||||
|
||||
---
|
||||
|
||||
### 12) `@CrossOrigin`
|
||||
CORS 문제를 해결할 때 사용합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@RestController
|
||||
@CrossOrigin(origins = "http://example.com")
|
||||
public class CorsController {
|
||||
@GetMapping("/data")
|
||||
public String getData() {
|
||||
return "CORS enabled";
|
||||
}
|
||||
}
|
||||
```
|
||||
- `http://example.com`에서 요청 가능.
|
||||
199
docs/05_템플릿 엔진.md
Normal file
199
docs/05_템플릿 엔진.md
Normal file
@@ -0,0 +1,199 @@
|
||||
# Spring Boot의 뷰 렌더링과 Thymeleaf 템플릿
|
||||
|
||||
Spring Boot에서는 **Thymeleaf**를 사용하여 **HTML 기반의 동적 웹 페이지를 렌더링**할 수 있습니다.
|
||||
|
||||
## 뷰(View) 렌더링이란?
|
||||
|
||||
- 클라이언트가 요청을 보내면, **컨트롤러가 요청을 처리하고 데이터를 모델(Model)에 담아 뷰(View)로 전달**합니다.
|
||||
- 뷰(View)는 **HTML 페이지**로, Thymeleaf 같은 템플릿 엔진을 사용하면 **서버에서 동적으로 HTML을 생성**할 수 있습니다.
|
||||
- Spring Boot에서 대표적인 뷰 템플릿 엔진: **Thymeleaf, JSP, FreeMarker** 등이 있지만, **Thymeleaf가 가장 많이 사용됨**.
|
||||
|
||||
## Thymeleaf 설정
|
||||
|
||||
다음과 같이 Thymeleaf 의존성을 추가합니다.
|
||||
```kotlin
|
||||
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
|
||||
```
|
||||
|
||||
Spring Boot는 기본적으로 **`src/main/resources/templates/`** 경로에서 Thymeleaf 템플릿을 찾습니다.
|
||||
|
||||
```
|
||||
src/
|
||||
├── main/
|
||||
│ ├── java/com.example.demo/
|
||||
│ ├── resources/
|
||||
│ │ ├── templates/ → Thymeleaf HTML 파일 위치
|
||||
│ │ │ ├── index.html
|
||||
│ │ │ ├── user.html
|
||||
│ │ ├── application.properties
|
||||
```
|
||||
|
||||
## 기본 컨트롤러와 템플릿 렌더링
|
||||
|
||||
### 컨트롤러 작성
|
||||
```java
|
||||
@Controller
|
||||
public class HomeController {
|
||||
|
||||
@GetMapping("/")
|
||||
public String home(Model model) {
|
||||
model.addAttribute("message", "Welcome to Thymeleaf!");
|
||||
return "index"; // templates/index.html 파일을 렌더링
|
||||
}
|
||||
}
|
||||
```
|
||||
**설명:**
|
||||
- `@Controller` → HTML을 반환하는 컨트롤러.
|
||||
- `Model` → **뷰로 데이터를 전달하는 객체**.
|
||||
- `model.addAttribute("message", "Welcome to Thymeleaf!")` → `message`라는 데이터를 전달.
|
||||
- `return "index";` → `templates/index.html` 파일을 렌더링.
|
||||
|
||||
|
||||
### Thymeleaf 템플릿 (index.html)
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<title>Thymeleaf Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1 th:text="${message}">Default Message</h1>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
#### 실행 결과 (`http://localhost:8080/` 요청)
|
||||
```html
|
||||
<h1>Welcome to Thymeleaf!</h1>
|
||||
```
|
||||
**설명:**
|
||||
- `th:text="${message}"` → `message` 값이 `"Welcome to Thymeleaf!"`로 변경됨.
|
||||
- **템플릿 엔진이 동적으로 HTML을 생성**하여 클라이언트에 응답.
|
||||
|
||||
|
||||
## 변수 출력 및 표현식
|
||||
|
||||
### 기본 표현식 (`th:text`)
|
||||
```html
|
||||
<p th:text="${username}">Default Name</p>
|
||||
```
|
||||
- **`${변수명}`** → `Model`에서 전달된 변수를 표시.
|
||||
|
||||
### 객체의 필드 출력 (`th:text`)
|
||||
```java
|
||||
class User {
|
||||
private String name;
|
||||
private int age;
|
||||
|
||||
public User(String name, int age) { this.name = name; this.age = age; }
|
||||
public String getName() { return name; }
|
||||
public int getAge() { return age; }
|
||||
}
|
||||
```
|
||||
```java
|
||||
@GetMapping("/user")
|
||||
public String userProfile(Model model) {
|
||||
model.addAttribute("user", new User("Alice", 25));
|
||||
return "user";
|
||||
}
|
||||
```
|
||||
```html
|
||||
<p th:text="${user.name}">Default Name</p>
|
||||
<p th:text="${user.age}">Default Age</p>
|
||||
```
|
||||
실행 결과 (`/user` 요청):
|
||||
```html
|
||||
<p>Alice</p>
|
||||
<p>25</p>
|
||||
```
|
||||
|
||||
## 반복문 (`th:each`)
|
||||
|
||||
### 리스트 반복 (`th:each`)
|
||||
```java
|
||||
@GetMapping("/users")
|
||||
public String users(Model model) {
|
||||
List<User> userList = Arrays.asList(
|
||||
new User("Alice", 25),
|
||||
new User("Bob", 30),
|
||||
new User("Charlie", 22)
|
||||
);
|
||||
model.addAttribute("users", userList);
|
||||
return "users";
|
||||
}
|
||||
```
|
||||
```html
|
||||
<ul>
|
||||
<li th:each="user : ${users}">
|
||||
<span th:text="${user.name}"></span> - <span th:text="${user.age}"></span>
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
실행 결과 (`/users` 요청):
|
||||
```html
|
||||
<ul>
|
||||
<li>Alice - 25</li>
|
||||
<li>Bob - 30</li>
|
||||
<li>Charlie - 22</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
## 조건문 (`th:if`, `th:unless`)
|
||||
|
||||
### 값이 있을 때만 표시 (`th:if`)
|
||||
```html
|
||||
<p th:if="${user.age >= 18}">Adult</p>
|
||||
```
|
||||
|
||||
### 값이 없을 때 표시 (`th:unless`)
|
||||
```html
|
||||
<p th:unless="${user.age >= 18}">Minor</p>
|
||||
```
|
||||
|
||||
|
||||
## 폼 처리 (`@PostMapping`)
|
||||
|
||||
### 컨트롤러에서 폼 데이터 받기
|
||||
```java
|
||||
@Controller
|
||||
@RequestMapping("/form")
|
||||
public class FormController {
|
||||
|
||||
@GetMapping
|
||||
public String showForm(Model model) {
|
||||
model.addAttribute("user", new User("", 0));
|
||||
return "form";
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String submitForm(@ModelAttribute User user, Model model) {
|
||||
model.addAttribute("submittedUser", user);
|
||||
return "result";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Thymeleaf 폼 (form.html)
|
||||
```html
|
||||
<form action="#" th:action="@{/form}" th:object="${user}" method="post">
|
||||
Name: <input type="text" th:field="*{name}" />
|
||||
Age: <input type="number" th:field="*{age}" />
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
결과 페이지 (result.html):
|
||||
```html
|
||||
<p>Name: <span th:text="${submittedUser.name}"></span></p>
|
||||
<p>Age: <span th:text="${submittedUser.age}"></span></p>
|
||||
```
|
||||
|
||||
|
||||
## 정리
|
||||
| 기능 | Thymeleaf 문법 | 설명 |
|
||||
|------|--------------|------|
|
||||
| **텍스트 출력** | `th:text="${변수}"` | 변수 값을 출력 |
|
||||
| **객체 속성 출력** | `th:text="${객체.필드}"` | 객체의 필드 값을 출력 |
|
||||
| **반복문** | `th:each="item : ${리스트}"` | 리스트 반복 출력 |
|
||||
| **조건문** | `th:if`, `th:unless` | 조건부 렌더링 |
|
||||
| **폼 바인딩** | `th:object` + `th:field` | HTML 폼과 객체 바인딩 |
|
||||
| **URL 매핑** | `th:action="@{경로}"` | 폼 요청 경로 설정 |
|
||||
213
docs/06_서비스 계층과 비즈니스 로직.md
Normal file
213
docs/06_서비스 계층과 비즈니스 로직.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# **서비스 계층과 비즈니스 로직**
|
||||
|
||||
Spring Boot 애플리케이션을 설계할 때, 코드의 **재사용성, 유지보수성, 테스트 용이성**을 높이려면 **비즈니스 로직을 서비스 계층(Service Layer)에 분리**하는 것이 중요합니다.
|
||||
이 글에서는 **서비스 계층의 역할, 설계 방식, 그리고 예제 코드**를 통해 비즈니스 로직을 어떻게 다루어야 하는지 설명합니다.
|
||||
|
||||
---
|
||||
|
||||
## **1. 서비스 계층이란?**
|
||||
Spring Boot 애플리케이션은 일반적으로 **MVC (Model-View-Controller) 구조**를 따릅니다.
|
||||
이때 **서비스 계층은 컨트롤러와 데이터 계층(Repository) 사이에서 비즈니스 로직을 처리하는 역할**을 합니다.
|
||||
|
||||
### **📌 계층별 역할**
|
||||
| 계층 | 역할 |
|
||||
|------|------|
|
||||
| **Controller (컨트롤러 계층)** | 사용자 요청을 받아 서비스 계층에 전달 |
|
||||
| **Service (서비스 계층)** | 비즈니스 로직을 처리하고 트랜잭션을 관리 |
|
||||
| **Repository (데이터 계층)** | 데이터베이스와 직접적인 통신을 담당 |
|
||||
|
||||
---
|
||||
|
||||
## **2. 서비스 계층의 필요성**
|
||||
|
||||
### **📌 컨트롤러에서 직접 로직을 처리하는 문제점**
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/users")
|
||||
public class UserController {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository; // 데이터 계층 직접 접근
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<User> createUser(@RequestBody User user) {
|
||||
if (user.getName() == null || user.getAge() < 0) {
|
||||
throw new IllegalArgumentException("Invalid user data");
|
||||
}
|
||||
return ResponseEntity.ok(userRepository.save(user)); // 비즈니스 로직이 컨트롤러에 있음
|
||||
}
|
||||
}
|
||||
```
|
||||
**🚨 문제점:**
|
||||
1. **컨트롤러가 너무 많은 책임을 가짐** → 가독성과 유지보수성이 떨어짐.
|
||||
2. **비즈니스 로직이 중복될 가능성 증가** → 여러 컨트롤러에서 같은 로직을 작성할 가능성이 큼.
|
||||
3. **테스트가 어려움** → 서비스 계층이 없으면 컨트롤러 단위 테스트가 복잡해짐.
|
||||
|
||||
---
|
||||
|
||||
## **3. 서비스 계층을 활용한 개선된 설계**
|
||||
|
||||
### **📌 1) Entity (데이터 모델)**
|
||||
```java
|
||||
@Entity
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
private String name;
|
||||
private int age;
|
||||
|
||||
// 기본 생성자 및 getter, setter
|
||||
public User() {}
|
||||
|
||||
public User(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
public String getName() { return name; }
|
||||
public int getAge() { return age; }
|
||||
}
|
||||
```
|
||||
|
||||
### **📌 2) Repository (데이터 계층)**
|
||||
```java
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
}
|
||||
```
|
||||
**설명:**
|
||||
- `JpaRepository<User, Long>` → `User` 엔티티를 다루는 JPA 리포지토리.
|
||||
- `findById(id)`, `save(entity)`, `deleteById(id)` 등 기본적인 DB 연산 제공.
|
||||
|
||||
---
|
||||
|
||||
### **📌 3) Service (비즈니스 로직 계층)**
|
||||
```java
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Transactional
|
||||
public User createUser(User user) {
|
||||
if (user.getName() == null || user.getAge() < 0) {
|
||||
throw new IllegalArgumentException("Invalid user data");
|
||||
}
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
public User getUserById(Long id) {
|
||||
return userRepository.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||
}
|
||||
}
|
||||
```
|
||||
**🚀 개선된 점:**
|
||||
✔ **컨트롤러에서 비즈니스 로직이 제거됨** → 역할이 명확해짐.
|
||||
✔ **비즈니스 로직이 서비스 계층에 집중됨** → 재사용성 증가.
|
||||
✔ **트랜잭션 관리 가능** → `@Transactional`을 활용하여 데이터 일관성 유지.
|
||||
|
||||
---
|
||||
|
||||
### **📌 4) Controller (컨트롤러 계층)**
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/users")
|
||||
public class UserController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<User> createUser(@RequestBody User user) {
|
||||
return ResponseEntity.ok(userService.createUser(user));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<User> getUser(@PathVariable Long id) {
|
||||
return ResponseEntity.ok(userService.getUserById(id));
|
||||
}
|
||||
}
|
||||
```
|
||||
**🚀 컨트롤러가 깔끔해짐!**
|
||||
✔ 비즈니스 로직을 서비스 계층에 위임.
|
||||
✔ 컨트롤러는 단순히 요청을 받고 응답을 반환하는 역할만 수행.
|
||||
|
||||
---
|
||||
|
||||
## **4. 서비스 계층에서 트랜잭션 관리**
|
||||
|
||||
### **📌 트랜잭션 (`@Transactional`)이란?**
|
||||
- 여러 개의 DB 연산을 하나의 단위로 묶어 **"모두 성공하거나, 하나라도 실패하면 롤백"**하도록 보장.
|
||||
- `@Transactional`을 붙이면, **예외가 발생할 경우 자동으로 롤백**됨.
|
||||
|
||||
### **📌 예제: 트랜잭션을 적용한 서비스**
|
||||
```java
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Transactional
|
||||
public void registerUsers(List<User> users) {
|
||||
for (User user : users) {
|
||||
if (user.getAge() < 0) {
|
||||
throw new RuntimeException("Invalid age");
|
||||
}
|
||||
userRepository.save(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
**🚀 특징:**
|
||||
- 만약 `user.getAge() < 0`인 사용자가 있다면, **이전까지 저장된 사용자도 롤백됨**.
|
||||
- 데이터 무결성을 보장할 수 있음.
|
||||
|
||||
---
|
||||
|
||||
## **5. 서비스 계층을 활용한 테스트**
|
||||
|
||||
### **📌 JUnit을 활용한 서비스 계층 테스트**
|
||||
```java
|
||||
@SpringBootTest
|
||||
@RunWith(SpringRunner.class)
|
||||
public class UserServiceTest {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Test
|
||||
public void testCreateUser() {
|
||||
User user = new User("Alice", 25);
|
||||
User savedUser = userService.createUser(user);
|
||||
|
||||
assertNotNull(savedUser);
|
||||
assertEquals("Alice", savedUser.getName());
|
||||
}
|
||||
}
|
||||
```
|
||||
**🚀 서비스 계층이 분리되어 있어, 테스트가 쉬워짐!**
|
||||
✔ `UserService`만 단독으로 테스트 가능.
|
||||
✔ `UserRepository`가 직접 드러나지 않으므로, Mocking(가짜 데이터)도 쉽게 적용 가능.
|
||||
|
||||
---
|
||||
|
||||
## **6. 정리**
|
||||
|
||||
| 계층 | 설명 |
|
||||
|------|------|
|
||||
| **Controller** | 요청을 받고 응답을 반환 (비즈니스 로직 X) |
|
||||
| **Service** | 비즈니스 로직 처리 및 트랜잭션 관리 |
|
||||
| **Repository** | 데이터베이스와 직접 통신 |
|
||||
|
||||
### **✅ 좋은 서비스 계층 설계 원칙**
|
||||
✔ **컨트롤러는 서비스에 로직을 위임**하고, 직접 처리하지 않음.
|
||||
✔ **비즈니스 로직은 서비스 계층에서 일괄적으로 관리**하여 재사용성을 높임.
|
||||
✔ **트랜잭션 관리 (`@Transactional`)을 서비스 계층에서 수행**하여 데이터 무결성을 보장.
|
||||
✔ **테스트가 용이하도록 서비스 계층을 단독으로 분리**.
|
||||
|
||||
서비스 계층을 잘 활용하면, **더 유지보수하기 좋은 코드**를 만들 수 있습니다.
|
||||
213
docs/07_데이터 처리와 JPA.md
Normal file
213
docs/07_데이터 처리와 JPA.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# **데이터 처리와 JPA**
|
||||
|
||||
웹 애플리케이션에서 **데이터 처리(Data Processing)**는 필수적인 요소이며, 이를 효율적으로 다루기 위해 ORM 프레임워크인 **JPA (Java Persistence API)**가 널리 사용됩니다.
|
||||
이번 글에서는 **JPA의 기본 개념, 주요 기능, 실무 적용 방법**을 살펴보겠습니다.
|
||||
|
||||
---
|
||||
|
||||
## **1. 데이터 처리란?**
|
||||
|
||||
데이터 처리는 애플리케이션이 데이터를 **생성(Create), 읽기(Read), 수정(Update), 삭제(Delete)**하는 과정(**CRUD**)을 의미합니다.
|
||||
Spring Boot에서는 **JPA와 Spring Data JPA**를 활용하여 효율적인 데이터 처리를 구현할 수 있습니다.
|
||||
|
||||
**📌 전통적인 JDBC 방식과 JPA 방식 비교**
|
||||
|
||||
| 방식 | 특징 | 코드 복잡도 | 유지보수성 |
|
||||
|------|------|------------|------------|
|
||||
| **JDBC (기본 SQL 사용)** | SQL 쿼리를 직접 작성 | 높음 | 낮음 |
|
||||
| **JPA (ORM 방식)** | 객체 중심의 데이터 처리 | 낮음 | 높음 |
|
||||
|
||||
---
|
||||
|
||||
## **2. JPA란?**
|
||||
|
||||
**JPA (Java Persistence API)**는 객체지향적인 방식으로 데이터베이스를 다룰 수 있도록 도와주는 **ORM(Object-Relational Mapping) 기술**입니다.
|
||||
JPA를 사용하면 SQL을 직접 작성하지 않고, **엔티티(Entity) 객체를 이용하여 데이터베이스를 조작**할 수 있습니다.
|
||||
|
||||
### **📌 JPA의 핵심 개념**
|
||||
1. **엔티티(Entity):** 데이터베이스 테이블과 매핑되는 클래스
|
||||
2. **리포지토리(Repository):** 데이터 저장 및 조회를 담당하는 계층
|
||||
3. **트랜잭션(Transaction):** 데이터 변경 작업을 하나의 단위로 처리
|
||||
4. **JPQL(Java Persistence Query Language):** 객체를 대상으로 하는 쿼리
|
||||
|
||||
---
|
||||
|
||||
## **3. JPA 실무 적용**
|
||||
|
||||
### **📌 1) Entity (데이터 모델 정의)**
|
||||
JPA에서는 `@Entity` 어노테이션을 사용하여 **클래스를 데이터베이스 테이블과 매핑**합니다.
|
||||
|
||||
```java
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity // User 엔티티는 DB의 users 테이블과 매핑됨
|
||||
@Table(name = "users")
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY) // 자동 증가 ID
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false) // name 컬럼은 null을 허용하지 않음
|
||||
private String name;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int age;
|
||||
|
||||
public User() {}
|
||||
|
||||
public User(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
// Getter & Setter 생략
|
||||
}
|
||||
```
|
||||
✔ **`@Entity`**: 해당 클래스가 데이터베이스 테이블과 연결됨
|
||||
✔ **`@Table(name = "users")`**: 테이블명을 `users`로 지정
|
||||
✔ **`@Id`**: 기본 키(Primary Key) 설정
|
||||
✔ **`@GeneratedValue(strategy = GenerationType.IDENTITY)`**: 자동 증가(AUTO_INCREMENT) 설정
|
||||
✔ **`@Column(nullable = false)`**: `null`을 허용하지 않도록 설정
|
||||
|
||||
---
|
||||
|
||||
### **📌 2) Repository (데이터 접근 계층)**
|
||||
JPA에서는 **Spring Data JPA**의 `JpaRepository` 인터페이스를 사용하면, 기본적인 CRUD 기능을 자동으로 구현할 수 있습니다.
|
||||
|
||||
```java
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
// 기본 CRUD 기능 제공
|
||||
}
|
||||
```
|
||||
✔ **`JpaRepository<User, Long>`**: `User` 엔티티를 관리하며, 기본 키 타입은 `Long`
|
||||
✔ `findById(id)`, `save(entity)`, `deleteById(id)` 등 기본적인 데이터 처리 메서드 제공
|
||||
|
||||
---
|
||||
|
||||
### **📌 3) Service (비즈니스 로직 계층)**
|
||||
데이터 처리 로직을 서비스 계층에서 구현하여 컨트롤러와 분리합니다.
|
||||
|
||||
```java
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Transactional
|
||||
public User createUser(String name, int age) {
|
||||
if (age < 0) {
|
||||
throw new IllegalArgumentException("나이는 0 이상이어야 합니다.");
|
||||
}
|
||||
return userRepository.save(new User(name, age));
|
||||
}
|
||||
|
||||
public List<User> getAllUsers() {
|
||||
return userRepository.findAll();
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ **서비스 계층을 사용하여 비즈니스 로직을 분리**
|
||||
✔ **`@Transactional`을 사용하여 트랜잭션 관리**
|
||||
|
||||
---
|
||||
|
||||
### **📌 4) Controller (요청 처리 계층)**
|
||||
사용자의 요청을 받아 서비스 계층을 호출하고 응답을 반환합니다.
|
||||
|
||||
```java
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/users")
|
||||
public class UserController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<User> createUser(@RequestParam String name, @RequestParam int age) {
|
||||
return ResponseEntity.ok(userService.createUser(name, age));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<User>> getAllUsers() {
|
||||
return ResponseEntity.ok(userService.getAllUsers());
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ **`@RestController`**: JSON 형식으로 데이터를 반환하는 컨트롤러
|
||||
✔ **`@RequestMapping("/users")`**: `/users` 경로의 API를 처리
|
||||
✔ **`@PostMapping`**: 새로운 사용자 생성
|
||||
✔ **`@GetMapping`**: 모든 사용자 조회
|
||||
|
||||
---
|
||||
|
||||
## **4. JPA에서 데이터 조회 (JPQL & Native Query)**
|
||||
|
||||
### **📌 1) JPQL (Java Persistence Query Language)**
|
||||
JPA에서는 SQL 대신 **JPQL**을 사용하여 객체 중심의 쿼리를 작성할 수 있습니다.
|
||||
|
||||
```java
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import java.util.List;
|
||||
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
|
||||
// 이름으로 사용자 찾기
|
||||
@Query("SELECT u FROM User u WHERE u.name = :name")
|
||||
List<User> findByName(@Param("name") String name);
|
||||
}
|
||||
```
|
||||
✔ SQL과 유사하지만, **테이블명이 아니라 엔티티 클래스명을 사용**
|
||||
|
||||
---
|
||||
|
||||
### **📌 2) Native Query (SQL 직접 사용)**
|
||||
기본 SQL 쿼리를 직접 사용할 수도 있습니다.
|
||||
|
||||
```java
|
||||
@Query(value = "SELECT * FROM users WHERE age >= :age", nativeQuery = true)
|
||||
List<User> findUsersByAge(@Param("age") int age);
|
||||
```
|
||||
✔ 복잡한 SQL 쿼리를 그대로 활용 가능
|
||||
|
||||
---
|
||||
|
||||
## **5. JPA의 장점과 단점**
|
||||
|
||||
### **📌 JPA의 장점**
|
||||
✔ **SQL을 직접 작성할 필요 없음** → 생산성 증가
|
||||
✔ **객체지향적인 데이터 처리 가능** → 코드의 가독성과 유지보수성 향상
|
||||
✔ **트랜잭션 관리가 용이함**
|
||||
✔ **캐싱 및 성능 최적화 기능 제공**
|
||||
|
||||
### **📌 JPA의 단점**
|
||||
❌ 초기 학습 비용이 존재
|
||||
❌ 복잡한 SQL 튜닝이 필요한 경우 SQL보다 불리할 수 있음
|
||||
❌ 데이터베이스 변경 시, 마이그레이션이 필요
|
||||
|
||||
---
|
||||
|
||||
## **6. 정리**
|
||||
✔ **JPA는 객체지향 방식으로 데이터를 처리하는 ORM 기술**
|
||||
✔ **Entity, Repository, Service, Controller 구조로 데이터 처리를 설계**
|
||||
✔ **JPQL 및 Native Query를 활용하여 데이터 조회 가능**
|
||||
✔ **트랜잭션을 활용하여 데이터 일관성을 유지**
|
||||
|
||||
JPA를 활용하면 **효율적인 데이터 처리와 유지보수성 높은 애플리케이션을 만들 수 있습니다!**
|
||||
184
docs/08_요청 처리 고급 기능.md
Normal file
184
docs/08_요청 처리 고급 기능.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# **Spring Boot 요청 처리 고급 기능: 필터, 인터셉터, AOP 활용**
|
||||
|
||||
Spring Boot에서 클라이언트의 **요청을 처리하는 과정**은 단순히 컨트롤러에서 요청을 받고 응답을 반환하는 것 이상으로 확장될 수 있습니다.
|
||||
특히 **보안, 로깅, 성능 모니터링, 인증/인가**와 같은 공통 기능을 처리하려면 **필터(Filter), 인터셉터(Interceptor), AOP(Aspect-Oriented Programming)** 등의 개념을 활용할 필요가 있습니다.
|
||||
|
||||
이번 글에서는 **필터, 인터셉터, AOP**를 활용하여 **요청을 가로채고, 원하는 로직을 추가하는 방법**을 실무 예제와 함께 설명하겠습니다.
|
||||
|
||||
---
|
||||
|
||||
# **1. 필터(Filter) - 요청 전/후 공통 처리**
|
||||
|
||||
### **📌 필터란?**
|
||||
필터는 **서블릿 수준에서 동작하며, 요청이 컨트롤러에 도달하기 전/후에 특정 로직을 실행**할 수 있는 기능입니다.
|
||||
Spring Boot에서는 `javax.servlet.Filter` 인터페이스를 구현하여 커스텀 필터를 만들 수 있습니다.
|
||||
|
||||
### **📌 필터의 주요 활용 사례**
|
||||
- **CORS 처리**
|
||||
- **요청 및 응답 로깅**
|
||||
- **JWT 인증 처리**
|
||||
- **IP 차단 등 보안 정책 적용**
|
||||
|
||||
### **📌 필터 구현 예제 - 요청 로깅**
|
||||
아래는 **모든 요청의 URL과 실행 시간을 기록하는 필터**입니다.
|
||||
|
||||
```java
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Component // 자동으로 필터 등록
|
||||
public class LoggingFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
HttpServletRequest req = (HttpServletRequest) request;
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
System.out.println("[LoggingFilter] 요청 URI: " + req.getRequestURI());
|
||||
|
||||
chain.doFilter(request, response); // 요청을 다음 필터 또는 컨트롤러로 전달
|
||||
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
System.out.println("[LoggingFilter] 요청 처리 시간: " + duration + "ms");
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `doFilter()` 메서드는 **요청을 가로채서 원하는 작업을 수행한 후, 체인(chain)으로 넘겨줌**
|
||||
✔ 요청 전후로 실행할 로직을 자유롭게 추가 가능
|
||||
|
||||
---
|
||||
|
||||
# **2. 인터셉터(Interceptor) - 요청 흐름을 제어**
|
||||
|
||||
### **📌 인터셉터란?**
|
||||
인터셉터는 **Spring MVC에서 동작하며, 컨트롤러 실행 전후에 특정 로직을 추가할 수 있는 기능**입니다.
|
||||
필터보다 **더 세부적인 요청 흐름을 제어**할 수 있으며, **특정 컨트롤러 또는 요청 경로에만 적용 가능**합니다.
|
||||
|
||||
### **📌 인터셉터의 주요 활용 사례**
|
||||
- **사용자 인증 및 권한 체크**
|
||||
- **API 요청 제한 (Rate Limiting)**
|
||||
- **로그 및 성능 모니터링**
|
||||
- **특정 요청의 파라미터 검사**
|
||||
|
||||
### **📌 인터셉터 구현 예제 - 인증 체크**
|
||||
아래는 **특정 API에 접근할 때, 인증된 사용자만 허용하는 인터셉터**입니다.
|
||||
|
||||
```java
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class AuthInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||
throws Exception {
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
|
||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.getWriter().write("Unauthorized");
|
||||
return false; // 요청을 차단
|
||||
}
|
||||
|
||||
// JWT 검증 로직 추가 가능 (예: Token 검증)
|
||||
System.out.println("[AuthInterceptor] 인증 성공");
|
||||
return true; // 요청 진행 허용
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `preHandle()` 메서드는 **컨트롤러 실행 전에 요청을 가로채서 검사 가능**
|
||||
✔ 인증이 실패하면 `false`를 반환하여 **요청을 차단할 수 있음**
|
||||
|
||||
### **📌 인터셉터 등록 (WebMvcConfigurer)**
|
||||
인터셉터를 사용하려면 **WebMvcConfigurer**에 등록해야 합니다.
|
||||
|
||||
```java
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
private AuthInterceptor authInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(authInterceptor)
|
||||
.addPathPatterns("/api/**") // 특정 URL에만 적용
|
||||
.excludePathPatterns("/api/public/**"); // 예외 URL 설정
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# **3. AOP (Aspect-Oriented Programming) - 횡단 관심사 처리**
|
||||
|
||||
### **📌 AOP란?**
|
||||
AOP는 **비즈니스 로직과는 별개로 공통 기능(로깅, 트랜잭션, 보안 등)을 적용하는 프로그래밍 기법**입니다.
|
||||
Spring Boot에서는 `@Aspect`와 `@Around`를 활용하여 **메서드 실행 전후에 특정 로직을 추가**할 수 있습니다.
|
||||
|
||||
### **📌 AOP의 주요 활용 사례**
|
||||
- **로깅(Log Tracing)**
|
||||
- **트랜잭션 관리**
|
||||
- **메서드 실행 시간 측정**
|
||||
- **입출력 값 검증**
|
||||
|
||||
### **📌 AOP 구현 예제 - 로깅 및 실행 시간 측정**
|
||||
아래는 **모든 서비스 메서드의 실행 시간을 로깅하는 AOP**입니다.
|
||||
|
||||
```java
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Aspect
|
||||
@Component
|
||||
public class LoggingAspect {
|
||||
|
||||
@Around("execution(* com.example.service.*.*(..))") // 모든 서비스 메서드에 적용
|
||||
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
Object result = joinPoint.proceed(); // 실제 메서드 실행
|
||||
|
||||
long duration = System.currentTimeMillis() - start;
|
||||
System.out.println("[AOP] " + joinPoint.getSignature() + " 실행 시간: " + duration + "ms");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `@Aspect` 선언으로 AOP 활성화
|
||||
✔ `@Around("execution(* com.example.service.*.*(..))")`로 **특정 패키지의 모든 메서드에 적용 가능**
|
||||
✔ `joinPoint.proceed()`를 호출하여 **실제 메서드를 실행하고, 이후 로직을 추가할 수 있음**
|
||||
|
||||
---
|
||||
|
||||
# **4. 필터, 인터셉터, AOP 비교**
|
||||
|
||||
| 기능 | 동작 위치 | 주요 목적 | 적용 대상 | 실행 시점 |
|
||||
|------|---------|---------|---------|---------|
|
||||
| **Filter** | 서블릿 레벨 | 요청 전처리 및 후처리 (보안, 로깅) | 모든 요청 | 컨트롤러 실행 전후 |
|
||||
| **Interceptor** | Spring MVC 레벨 | 인증, 권한 체크 | 특정 요청 (API) | 컨트롤러 실행 전후 |
|
||||
| **AOP** | 메서드 레벨 | 로깅, 트랜잭션, 성능 측정 | 특정 패키지/클래스의 메서드 | 메서드 실행 전후 |
|
||||
|
||||
---
|
||||
|
||||
# **5. 정리**
|
||||
✔ **필터(Filter)**: **모든 요청에 대해 전역적인 로직 적용 (보안, CORS, 로깅)**
|
||||
✔ **인터셉터(Interceptor)**: **컨트롤러 실행 전후에 특정 요청을 가로채서 인증/인가 처리**
|
||||
✔ **AOP(Aspect-Oriented Programming)**: **특정 메서드(비즈니스 로직)의 실행 전후에 로직 추가 (로깅, 성능 측정 등)**
|
||||
|
||||
이러한 고급 기능을 활용하면 **Spring Boot 애플리케이션을 더욱 강력하고 유지보수하기 쉽게 만들 수 있습니다!**
|
||||
226
docs/09_세션 & 쿠키, 파일.md
Normal file
226
docs/09_세션 & 쿠키, 파일.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# **Spring Boot: 세션 & 쿠키, 파일 업로드 & 다운로드**
|
||||
|
||||
Spring Boot 애플리케이션에서 **세션과 쿠키**는 사용자 상태를 관리하는 데 유용하며, **파일 업로드 및 다운로드** 기능은 다양한 애플리케이션에서 필수적인 기능입니다.
|
||||
이번 글에서는 **세션과 쿠키의 개념 및 사용법**을 살펴보고, **파일 업로드 및 다운로드를 처리하는 방법**을 예제와 함께 설명하겠습니다.
|
||||
|
||||
---
|
||||
|
||||
# **1. 세션(Session)과 쿠키(Cookie)**
|
||||
|
||||
### **📌 세션(Session)이란?**
|
||||
- **서버 측에서 사용자 상태를 유지하는 기술**
|
||||
- **각 사용자에게 고유한 세션 ID(Session ID)를 부여하고, 서버에서 해당 사용자의 데이터를 저장**
|
||||
- 로그인 정보, 장바구니 데이터 등 **사용자의 지속적인 상태 유지**가 필요한 경우 사용
|
||||
|
||||
### **📌 쿠키(Cookie)란?**
|
||||
- **클라이언트(브라우저)에 저장되는 작은 데이터 조각**
|
||||
- 서버에서 응답 시 쿠키를 설정하면, 클라이언트는 이후 요청에서 해당 쿠키를 함께 전송
|
||||
- **세션과 달리 서버가 아닌 클라이언트에 저장됨**
|
||||
- 로그인 유지, 사이트 설정 저장 등에 활용
|
||||
|
||||
---
|
||||
|
||||
## **1.1 세션(Session) 사용하기**
|
||||
|
||||
### **✅ 세션 저장 및 조회 예제**
|
||||
Spring Boot에서는 `HttpSession`을 사용하여 **세션 데이터를 저장 및 조회**할 수 있습니다.
|
||||
|
||||
```java
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/session")
|
||||
public class SessionController {
|
||||
|
||||
@PostMapping("/set")
|
||||
public String setSession(HttpSession session) {
|
||||
session.setAttribute("username", "JohnDoe"); // 세션에 저장
|
||||
return "세션에 username=JohnDoe 저장 완료";
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
public String getSession(HttpSession session) {
|
||||
String username = (String) session.getAttribute("username");
|
||||
return username != null ? "세션 값: " + username : "세션 값이 없습니다.";
|
||||
}
|
||||
|
||||
@PostMapping("/remove")
|
||||
public String removeSession(HttpSession session) {
|
||||
session.invalidate(); // 세션 삭제
|
||||
return "세션 삭제 완료";
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `setSession()`: `"username"` 값을 세션에 저장
|
||||
✔ `getSession()`: 저장된 세션 값을 조회
|
||||
✔ `removeSession()`: 세션 데이터를 삭제
|
||||
|
||||
---
|
||||
|
||||
## **1.2 쿠키(Cookie) 사용하기**
|
||||
|
||||
### **✅ 쿠키 설정 및 조회 예제**
|
||||
Spring Boot에서는 `HttpServletResponse`와 `HttpServletRequest`를 사용하여 쿠키를 설정하고 조회할 수 있습니다.
|
||||
|
||||
```java
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/cookie")
|
||||
public class CookieController {
|
||||
|
||||
@PostMapping("/set")
|
||||
public String setCookie(HttpServletResponse response) {
|
||||
Cookie cookie = new Cookie("userId", "12345");
|
||||
cookie.setMaxAge(60 * 60); // 1시간 동안 유지
|
||||
cookie.setPath("/"); // 모든 경로에서 유효
|
||||
response.addCookie(cookie);
|
||||
return "쿠키 설정 완료: userId=12345";
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
public String getCookie(HttpServletRequest request) {
|
||||
Cookie[] cookies = request.getCookies();
|
||||
if (cookies != null) {
|
||||
for (Cookie cookie : cookies) {
|
||||
if ("userId".equals(cookie.getName())) {
|
||||
return "쿠키 값: " + cookie.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
return "쿠키가 없습니다.";
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
public String deleteCookie(HttpServletResponse response) {
|
||||
Cookie cookie = new Cookie("userId", null);
|
||||
cookie.setMaxAge(0); // 즉시 삭제
|
||||
cookie.setPath("/");
|
||||
response.addCookie(cookie);
|
||||
return "쿠키 삭제 완료";
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `setCookie()`: `"userId"` 쿠키를 설정
|
||||
✔ `getCookie()`: 저장된 쿠키 값을 조회
|
||||
✔ `deleteCookie()`: `"userId"` 쿠키를 삭제
|
||||
|
||||
---
|
||||
|
||||
# **2. 파일 업로드 및 다운로드**
|
||||
|
||||
Spring Boot에서는 `MultipartFile`을 사용하여 **파일 업로드 및 다운로드**를 간편하게 처리할 수 있습니다.
|
||||
|
||||
---
|
||||
|
||||
## **2.1 파일 업로드 처리**
|
||||
|
||||
### **✅ 파일 업로드 예제**
|
||||
아래는 **Spring Boot에서 파일을 업로드하여 로컬 저장소에 저장하는 코드**입니다.
|
||||
|
||||
```java
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/file")
|
||||
public class FileUploadController {
|
||||
|
||||
private static final String UPLOAD_DIR = "uploads/";
|
||||
|
||||
@PostMapping("/upload")
|
||||
public String uploadFile(@RequestParam("file") MultipartFile file) {
|
||||
try {
|
||||
File uploadDir = new File(UPLOAD_DIR);
|
||||
if (!uploadDir.exists()) {
|
||||
uploadDir.mkdirs(); // 디렉토리 생성
|
||||
}
|
||||
String filePath = UPLOAD_DIR + file.getOriginalFilename();
|
||||
file.transferTo(new File(filePath)); // 파일 저장
|
||||
return "파일 업로드 성공: " + filePath;
|
||||
} catch (IOException e) {
|
||||
return "파일 업로드 실패: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `MultipartFile`을 받아서 `uploads/` 디렉토리에 저장
|
||||
✔ `file.transferTo(new File(filePath))`로 실제 파일을 저장
|
||||
✔ `@RequestParam("file")`로 HTML 폼에서 파일을 받을 수 있음
|
||||
|
||||
### **✅ HTML 파일 업로드 폼 예제**
|
||||
```html
|
||||
<form action="/file/upload" method="post" enctype="multipart/form-data">
|
||||
<input type="file" name="file" />
|
||||
<button type="submit">파일 업로드</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **2.2 파일 다운로드 처리**
|
||||
|
||||
### **✅ 파일 다운로드 예제**
|
||||
Spring Boot에서는 `ResponseEntity`를 사용하여 **파일을 다운로드할 수 있습니다**.
|
||||
|
||||
```java
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/file")
|
||||
public class FileDownloadController {
|
||||
|
||||
private static final String UPLOAD_DIR = "uploads/";
|
||||
|
||||
@GetMapping("/download/{filename}")
|
||||
public ResponseEntity<Resource> downloadFile(@PathVariable String filename) {
|
||||
try {
|
||||
Path filePath = Paths.get(UPLOAD_DIR).resolve(filename).normalize();
|
||||
Resource resource = new UrlResource(filePath.toUri());
|
||||
|
||||
if (!resource.exists()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
|
||||
.body(resource);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `UrlResource`를 이용하여 파일을 읽어옴
|
||||
✔ `CONTENT_DISPOSITION`을 설정하여 **다운로드 가능한 형태로 응답**
|
||||
✔ `/file/download/{filename}` 경로로 요청하면 해당 파일을 다운로드 가능
|
||||
|
||||
### **✅ 파일 다운로드 요청 예제**
|
||||
```html
|
||||
<a href="/file/download/sample.txt">파일 다운로드</a>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# **3. 정리**
|
||||
|
||||
✔ **세션(Session)**: 서버 측에서 사용자 정보를 관리 (로그인 상태 유지 등)
|
||||
✔ **쿠키(Cookie)**: 클라이언트 측에 저장되며, 서버와의 요청에서 사용 가능
|
||||
✔ **파일 업로드**: `MultipartFile`을 이용하여 서버에 파일 저장
|
||||
✔ **파일 다운로드**: `ResponseEntity<Resource>`를 활용하여 파일 제공
|
||||
|
||||
Spring Boot에서 **세션과 쿠키를 활용하여 사용자 상태를 관리**하고, **파일 업로드 및 다운로드를 구현하는 방법**을 익히면 다양한 웹 애플리케이션에서 활용할 수 있습니다!
|
||||
200
docs/21_model_mapper.md
Normal file
200
docs/21_model_mapper.md
Normal file
@@ -0,0 +1,200 @@
|
||||
# **ModelMapper와 DTO 패턴**
|
||||
|
||||
Spring Boot 애플리케이션에서 **DTO(Data Transfer Object) 패턴**은 엔티티(Entity)와 클라이언트 간의 데이터를 효율적으로 주고받기 위한 중요한 패턴입니다.
|
||||
이 과정에서 **ModelMapper**는 DTO와 엔티티 간의 변환을 자동화하는 도구로 많이 사용됩니다.
|
||||
|
||||
이번 글에서는 **ModelMapper의 개념과 사용법**, 그리고 **DTO 패턴의 개념과 장점**을 설명하겠습니다.
|
||||
|
||||
---
|
||||
|
||||
## **1. DTO(Data Transfer Object) 패턴이란?**
|
||||
|
||||
### **📌 DTO란?**
|
||||
- 클라이언트와 서버 간의 **데이터 교환을 위한 객체**
|
||||
- **Entity와 분리**하여 사용
|
||||
- 보안, 데이터 무결성, 성능 최적화를 위해 활용
|
||||
|
||||
---
|
||||
|
||||
## **1.1 DTO 패턴을 사용하는 이유**
|
||||
|
||||
| 문제 | 해결책 (DTO) |
|
||||
|------|------------|
|
||||
| **엔티티를 직접 반환하면 보안 문제가 발생할 수 있음** | **필요한 데이터만 DTO에 담아 전송** |
|
||||
| **JPA 엔티티는 영속성 컨텍스트에서 관리되므로 직접 수정하면 원치 않는 변경 발생 가능** | **DTO를 통해 영속성과 분리된 객체로 전달** |
|
||||
| **엔티티가 클라이언트에 직접 노출되면 변경 시 API가 영향을 받음** | **DTO를 사용하여 API 구조를 안정적으로 유지** |
|
||||
|
||||
---
|
||||
|
||||
## **1.2 DTO 패턴 적용 예제**
|
||||
|
||||
### **📌 Entity 클래스**
|
||||
```java
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String username;
|
||||
private String email;
|
||||
private String password; // 보안 문제로 DTO에서는 제외
|
||||
}
|
||||
```
|
||||
|
||||
### **📌 DTO 클래스**
|
||||
```java
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
public class UserDTO {
|
||||
private String username;
|
||||
private String email;
|
||||
}
|
||||
```
|
||||
✔ **Entity와 달리 `password` 필드를 포함하지 않음** → 보안 강화
|
||||
✔ **불필요한 필드를 제거하여 API 성능 최적화**
|
||||
|
||||
---
|
||||
|
||||
# **2. ModelMapper란?**
|
||||
|
||||
### **📌 ModelMapper란?**
|
||||
- DTO와 Entity 간 변환을 **자동으로 처리해주는 라이브러리**
|
||||
- 수동으로 `set()` 메서드를 호출하지 않아도 DTO ↔ Entity 변환 가능
|
||||
- Spring Boot 프로젝트에서 사용 가능
|
||||
|
||||
---
|
||||
|
||||
## **2.1 ModelMapper 설정 및 사용법**
|
||||
|
||||
### **✅ ModelMapper 의존성 추가**
|
||||
`pom.xml`에 ModelMapper 의존성을 추가합니다.
|
||||
|
||||
```kotlin
|
||||
implementation("org.modelmapper:modelmapper:3.2.2")
|
||||
```
|
||||
|
||||
### **✅ ModelMapper를 Bean으로 등록**
|
||||
Spring Boot에서 `ModelMapper`를 사용하려면 **Bean으로 등록**해야 합니다.
|
||||
|
||||
```java
|
||||
import org.modelmapper.ModelMapper;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class ModelMapperConfig {
|
||||
|
||||
@Bean
|
||||
public ModelMapper modelMapper() {
|
||||
return new ModelMapper();
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ **ModelMapper를 Bean으로 등록하여 서비스에서 주입받아 사용 가능**
|
||||
|
||||
---
|
||||
|
||||
## **2.2 ModelMapper를 활용한 DTO 변환**
|
||||
|
||||
### **✅ DTO로 변환 (Entity → DTO)**
|
||||
```java
|
||||
import org.modelmapper.ModelMapper;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
private final ModelMapper modelMapper;
|
||||
|
||||
public UserService(ModelMapper modelMapper) {
|
||||
this.modelMapper = modelMapper;
|
||||
}
|
||||
|
||||
public UserDTO convertToDTO(User user) {
|
||||
return modelMapper.map(user, UserDTO.class);
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `modelMapper.map(user, UserDTO.class)` → **자동으로 User → UserDTO 변환**
|
||||
✔ 수동으로 `setUsername()`, `setEmail()`을 호출할 필요 없음
|
||||
|
||||
---
|
||||
|
||||
### **✅ Entity로 변환 (DTO → Entity)**
|
||||
```java
|
||||
public User convertToEntity(UserDTO userDTO) {
|
||||
return modelMapper.map(userDTO, User.class);
|
||||
}
|
||||
```
|
||||
✔ `UserDTO`를 `User` 엔티티로 변환 가능
|
||||
✔ DB 저장 시 활용
|
||||
|
||||
---
|
||||
|
||||
## **2.3 ModelMapper를 활용한 API 컨트롤러 예제**
|
||||
|
||||
```java
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/users")
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
public UserController(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public UserDTO getUser(@PathVariable Long id) {
|
||||
User user = userService.getUserById(id); // 가정: userService에서 User 엔티티 조회
|
||||
return userService.convertToDTO(user);
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ 엔티티 대신 `UserDTO`를 반환하여 API 응답을 안전하게 유지
|
||||
✔ **비밀번호(password) 등의 민감한 정보를 클라이언트에 노출하지 않음**
|
||||
|
||||
---
|
||||
|
||||
# **3. DTO vs Entity: 언제 사용해야 할까?**
|
||||
|
||||
| 구분 | DTO | Entity |
|
||||
|------|-----|--------|
|
||||
| **목적** | 클라이언트와 데이터 교환 | DB와 직접 매핑 |
|
||||
| **사용 위치** | 컨트롤러, 서비스 계층 | 리포지토리 계층 |
|
||||
| **영속성 관리** | X (JPA 관리 대상 아님) | O (JPA 관리 대상) |
|
||||
| **보안** | 민감한 정보 제외 가능 | 모든 필드 포함 |
|
||||
|
||||
✔ **API 응답 및 요청에는 DTO 사용**
|
||||
✔ **DB 작업에는 Entity 사용**
|
||||
|
||||
---
|
||||
|
||||
# **4. 정리**
|
||||
|
||||
### ✅ **DTO(Data Transfer Object) 패턴**
|
||||
- **클라이언트와 서버 간 데이터 전송을 위한 객체**
|
||||
- **엔티티를 직접 노출하지 않도록 보호**
|
||||
- **불필요한 필드를 제외하여 성능 최적화**
|
||||
|
||||
### ✅ **ModelMapper 활용**
|
||||
- DTO ↔ Entity 간 변환을 자동화
|
||||
- `modelMapper.map(source, destinationClass)`을 사용하여 변환
|
||||
|
||||
Spring Boot 프로젝트에서 **DTO 패턴과 ModelMapper를 활용하면 보안성과 유지보수성을 높일 수 있습니다.**
|
||||
특히 REST API 설계 시 엔티티를 직접 반환하지 않고 DTO를 사용하면 **보안 강화 및 API 변경 최소화**가 가능합니다. 🚀
|
||||
336
docs/22_file upload.md
Normal file
336
docs/22_file upload.md
Normal file
@@ -0,0 +1,336 @@
|
||||
물론이죠! 아래는 **스프링 부트(Spring Boot)**에서의 **파일 업로드**, **멀티 파일 업로드**, **파일 다운로드**를 다루는 실무 중심의 설명 글입니다.
|
||||
|
||||
---
|
||||
|
||||
# ✅ 스프링 부트에서 파일 업로드 및 다운로드 구현하기
|
||||
|
||||
현대 웹 애플리케이션에서는 사용자가 이미지를 업로드하거나 문서를 다운로드하는 기능이 필수입니다. 스프링 부트는 이러한 기능을 손쉽게 구현할 수 있도록 다양한 API를 제공합니다.
|
||||
이 글에서는 다음을 다룹니다:
|
||||
|
||||
- 단일 파일 업로드
|
||||
- 다중(멀티) 파일 업로드
|
||||
- 파일 다운로드
|
||||
- 실무에서 주의할 점
|
||||
|
||||
---
|
||||
|
||||
## 📁 1. 의존성 설정 (`build.gradle` 또는 `pom.xml`)
|
||||
스프링 부트에서는 기본적으로 `spring-boot-starter-web`만으로도 파일 업로드/다운로드가 가능합니다.
|
||||
|
||||
**Gradle 예시:**
|
||||
```groovy
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📤 2. 단일 파일 업로드
|
||||
|
||||
### ✅ 컨트롤러 예시
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/files")
|
||||
public class FileUploadController {
|
||||
|
||||
private final Path uploadDir = Paths.get("uploads");
|
||||
|
||||
public FileUploadController() throws IOException {
|
||||
Files.createDirectories(uploadDir);
|
||||
}
|
||||
|
||||
@PostMapping("/upload")
|
||||
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
|
||||
if (file.isEmpty()) return ResponseEntity.badRequest().body("파일이 비어 있습니다");
|
||||
|
||||
Path targetPath = uploadDir.resolve(file.getOriginalFilename());
|
||||
Files.copy(file.getInputStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
return ResponseEntity.ok("업로드 성공: " + file.getOriginalFilename());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 🔍 설명
|
||||
- `@RequestParam("file")`: HTML `<input type="file">`의 name과 일치해야 합니다.
|
||||
- `MultipartFile`: 업로드된 파일을 표현하는 객체입니다.
|
||||
- `Files.copy(...)`: 서버의 특정 디렉토리에 저장.
|
||||
|
||||
---
|
||||
|
||||
## 📤 3. 멀티 파일 업로드
|
||||
|
||||
### ✅ 컨트롤러 예시
|
||||
```java
|
||||
@PostMapping("/upload-multiple")
|
||||
public ResponseEntity<String> uploadMultiple(@RequestParam("files") List<MultipartFile> files) throws IOException {
|
||||
if (files.isEmpty()) return ResponseEntity.badRequest().body("업로드된 파일이 없습니다.");
|
||||
|
||||
for (MultipartFile file : files) {
|
||||
if (!file.isEmpty()) {
|
||||
Path targetPath = uploadDir.resolve(file.getOriginalFilename());
|
||||
Files.copy(file.getInputStream(), targetPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.ok("멀티 파일 업로드 성공");
|
||||
}
|
||||
```
|
||||
|
||||
### 🔍 HTML에서 보내는 방법
|
||||
```html
|
||||
<form method="post" enctype="multipart/form-data" action="/files/upload-multiple">
|
||||
<input type="file" name="files" multiple>
|
||||
<button type="submit">업로드</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📥 4. 파일 다운로드
|
||||
|
||||
### ✅ 컨트롤러 예시
|
||||
```java
|
||||
@GetMapping("/download/{filename}")
|
||||
public ResponseEntity<Resource> downloadFile(@PathVariable String filename) throws IOException {
|
||||
Path filePath = uploadDir.resolve(filename);
|
||||
if (!Files.exists(filePath)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
Resource resource = new UrlResource(filePath.toUri());
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + filename + "\"")
|
||||
.body(resource);
|
||||
}
|
||||
```
|
||||
|
||||
### 🔍 주요 포인트
|
||||
- `UrlResource`: 파일을 다운로드 가능한 형태로 변환
|
||||
- `Content-Disposition`: 파일이 브라우저에 표시되지 않고 다운로드되도록 설정
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 실무에서 주의할 점
|
||||
|
||||
### 1. 파일 이름 중복 처리
|
||||
- `UUID`를 붙이거나 시간 기반으로 처리해야 파일 덮어쓰기 방지 가능
|
||||
```java
|
||||
String safeName = UUID.randomUUID() + "_" + file.getOriginalFilename();
|
||||
```
|
||||
|
||||
### 2. 저장 경로 보안
|
||||
- 사용자 입력을 통한 경로 조작 공격 방지 (`../` 등)
|
||||
- 반드시 경로를 정규화 (`normalize()`)하고 업로드 디렉토리 밖으로 벗어나지 않도록 체크
|
||||
|
||||
```java
|
||||
Path normalized = uploadDir.resolve(file.getOriginalFilename()).normalize();
|
||||
if (!normalized.startsWith(uploadDir)) {
|
||||
throw new SecurityException("잘못된 경로입니다");
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 용량 제한 설정
|
||||
**application.properties**
|
||||
```properties
|
||||
spring.servlet.multipart.max-file-size=10MB
|
||||
spring.servlet.multipart.max-request-size=20MB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ 마무리
|
||||
|
||||
| 기능 | 구현 여부 |
|
||||
|-----------------|-----------|
|
||||
| 단일 파일 업로드 | ✅ |
|
||||
| 멀티 파일 업로드 | ✅ |
|
||||
| 파일 다운로드 | ✅ |
|
||||
|
||||
스프링 부트에서는 `MultipartFile`, `Resource`, `ResponseEntity` 등의 기본 컴포넌트를 통해 파일 업로드/다운로드 기능을 쉽게 구현할 수 있습니다. 다만 실무에서는 보안, 파일 크기 제한, 파일명 중복 등의 다양한 상황을 고려해야 합니다.
|
||||
|
||||
---
|
||||
|
||||
필요하다면 `AWS S3`, `FTP`, 혹은 DB BLOB 저장 방식도 활용 가능하며, 이 글은 **로컬 저장소 기반**의 기본 구현을 중심으로 설명했습니다.
|
||||
더 확장하고 싶은 부분이 있으면 언제든지 말해주세요!
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
좋습니다! 이번엔 **파일을 데이터베이스에 BLOB(Binary Large Object)** 형태로 저장하고, 다시 꺼내서 다운로드하는 방식을 다룬 글을 정리해드릴게요.
|
||||
|
||||
---
|
||||
|
||||
# 🗃️ 스프링 부트에서 파일을 DB(BLOB)로 업로드 및 다운로드하기
|
||||
|
||||
웹 애플리케이션에서 파일을 **데이터베이스(BLOB 컬럼)**에 직접 저장하는 방식은 파일 보안이나 백업, 이식성 측면에서 유리할 수 있습니다. 이 글에서는 다음을 다룹니다:
|
||||
|
||||
- DB에 파일 업로드 (BLOB 저장)
|
||||
- DB에서 파일 다운로드
|
||||
- 실제 구현 예시 (Spring Boot + JPA + H2/MySQL)
|
||||
|
||||
---
|
||||
|
||||
## 📦 1. 테이블 설계 (DDL 예시)
|
||||
|
||||
### ✅ MySQL 예시
|
||||
```sql
|
||||
CREATE TABLE file_data (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
file_name VARCHAR(255),
|
||||
content_type VARCHAR(100),
|
||||
data LONGBLOB
|
||||
);
|
||||
```
|
||||
|
||||
> `data` 컬럼이 실제 파일의 바이트 데이터를 저장하는 BLOB 필드입니다.
|
||||
|
||||
---
|
||||
|
||||
## 🧱 2. 엔터티 클래스 정의
|
||||
|
||||
```java
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity
|
||||
@Table(name = "file_data")
|
||||
public class FileData {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String fileName;
|
||||
|
||||
private String contentType;
|
||||
|
||||
@Lob
|
||||
@Column(columnDefinition = "LONGBLOB")
|
||||
private byte[] data;
|
||||
|
||||
// 생성자, Getter/Setter 생략
|
||||
}
|
||||
```
|
||||
|
||||
- `@Lob` 어노테이션은 `byte[]` 데이터를 BLOB으로 매핑합니다.
|
||||
- `contentType`은 다운로드할 때 MIME 타입을 알려주기 위해 사용합니다.
|
||||
|
||||
---
|
||||
|
||||
## 📂 3. 리포지토리 인터페이스
|
||||
|
||||
```java
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface FileDataRepository extends JpaRepository<FileData, Long> {
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📤 4. 파일 업로드 (DB 저장)
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/files")
|
||||
public class FileDataController {
|
||||
|
||||
private final FileDataRepository fileDataRepository;
|
||||
|
||||
public FileDataController(FileDataRepository repo) {
|
||||
this.fileDataRepository = repo;
|
||||
}
|
||||
|
||||
@PostMapping("/upload")
|
||||
public ResponseEntity<String> uploadFile(@RequestParam("file") MultipartFile file) throws IOException {
|
||||
if (file.isEmpty()) return ResponseEntity.badRequest().body("빈 파일입니다");
|
||||
|
||||
FileData fileData = new FileData();
|
||||
fileData.setFileName(file.getOriginalFilename());
|
||||
fileData.setContentType(file.getContentType());
|
||||
fileData.setData(file.getBytes());
|
||||
|
||||
fileDataRepository.save(fileData);
|
||||
|
||||
return ResponseEntity.ok("파일 업로드 성공: ID = " + fileData.getId());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📥 5. 파일 다운로드 (DB에서 BLOB 조회)
|
||||
|
||||
```java
|
||||
@GetMapping("/download/{id}")
|
||||
public ResponseEntity<byte[]> downloadFile(@PathVariable Long id) {
|
||||
return fileDataRepository.findById(id)
|
||||
.map(file -> ResponseEntity.ok()
|
||||
.contentType(MediaType.parseMediaType(file.getContentType()))
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getFileName() + "\"")
|
||||
.body(file.getData()))
|
||||
.orElse(ResponseEntity.notFound().build());
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 6. Postman 또는 HTML 테스트 예시
|
||||
|
||||
### ✅ HTML 업로드 폼
|
||||
```html
|
||||
<form method="post" enctype="multipart/form-data" action="/files/upload">
|
||||
<input type="file" name="file">
|
||||
<button type="submit">업로드</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
### ✅ 다운로드 링크 예시
|
||||
```html
|
||||
<a href="/files/download/1">파일 다운로드</a>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 실무에서 주의할 점
|
||||
|
||||
### 1. 성능 문제
|
||||
- BLOB 방식은 대용량 파일 처리 시 성능 저하가 발생할 수 있음
|
||||
→ **작은 이미지나 문서 위주에 적합**
|
||||
|
||||
### 2. DB 백업 크기 증가
|
||||
- 파일이 많아지면 DB 백업/복원 시간이 길어질 수 있음
|
||||
|
||||
### 3. 파일 크기 제한 설정
|
||||
```properties
|
||||
spring.servlet.multipart.max-file-size=10MB
|
||||
spring.servlet.multipart.max-request-size=20MB
|
||||
```
|
||||
|
||||
### 4. 파일 다운로드 속도
|
||||
- 서버가 파일을 전부 로드해야 하므로, 큰 파일은 **스트리밍 방식 처리**를 고려할 것
|
||||
|
||||
---
|
||||
|
||||
## ✅ 정리
|
||||
|
||||
| 기능 | 구현 방식 |
|
||||
|------------------|-----------|
|
||||
| 파일 업로드 | `MultipartFile` → BLOB 저장 |
|
||||
| 파일 다운로드 | `byte[]` → HTTP 응답 |
|
||||
| 확장성/보안 | 파일 시스템보다 안전하나 대용량엔 부적합 가능 |
|
||||
|
||||
---
|
||||
|
||||
## 🔚 마무리
|
||||
|
||||
파일을 **데이터베이스 BLOB에 저장하는 방식**은 외부 파일 시스템 없이 관리가 간편하고, 트랜잭션 처리와 보안이 뛰어납니다. 단, 대용량 파일이 많을 경우에는 성능과 저장소 이슈를 고려하여 **S3, 파일 시스템 저장 방식**과 함께 혼합하여 쓰는 것이 좋습니다.
|
||||
|
||||
---
|
||||
|
||||
필요하다면 **썸네일 저장, 이미지 미리보기 API, 스트리밍 다운로드**까지 확장도 가능합니다. 원하시면 다음 단계도 이어서 도와드릴게요!
|
||||
176
docs/23_url mapping.md
Normal file
176
docs/23_url mapping.md
Normal file
@@ -0,0 +1,176 @@
|
||||
## 스프링 부트 URL 맵핑 완벽 가이드: 요청과 코드를 연결하는 핵심 원리
|
||||
|
||||
스프링 부트 애플리케이션의 핵심은 사용자의 웹 요청을 적절한 코드 영역으로 연결하는 **URL 맵핑**입니다. URL 맵핑을 제대로 이해하고 활용하는 것은 웹 애플리케이션 개발의 기초이자 효율적인 API 설계의 핵심입니다. 이 글에서는 스프링 부트의 URL 맵핑에 대한 모든 것을 상세하게 알아보겠습니다.
|
||||
|
||||
### 1. URL 맵핑이란 무엇일까요?
|
||||
|
||||
URL 맵핑은 클라이언트(웹 브라우저, 모바일 앱 등)가 특정 URL로 요청을 보냈을 때, 해당 요청을 처리할 서버 측의 특정 핸들러 메서드(주로 컨트롤러 클래스 내의 메서드)를 연결해주는 과정입니다. 스프링 부트는 다양한 어노테이션과 설정을 통해 이러한 맵핑을 간편하게 구현할 수 있도록 지원합니다.
|
||||
|
||||
### 2. 주요 URL 맵핑 어노테이션
|
||||
|
||||
스프링 MVC (Model-View-Controller) 프레임워크에서 URL 맵핑을 정의하는 데 가장 많이 사용되는 어노테이션은 다음과 같습니다.
|
||||
|
||||
* **`@Controller`**: 해당 클래스가 웹 요청을 처리하는 컨트롤러임을 명시합니다. 주로 HTML 뷰를 반환하는 데 사용됩니다.
|
||||
* **`@RestController`**: `@Controller`와 `@ResponseBody` 어노테이션을 결합한 것으로, 해당 클래스의 모든 메서드가 HTTP 응답 본문에 직접 데이터를 써서 반환함을 의미합니다. 주로 RESTful API 개발에 사용됩니다.
|
||||
* **`@RequestMapping`**: 특정 URL 패턴과 HTTP 메서드에 대한 맵핑을 정의하는 가장 기본적인 어노테이션입니다. 클래스 레벨과 메서드 레벨 모두에서 사용할 수 있습니다.
|
||||
* **`value` 또는 `path`**: 맵핑할 URL 패턴을 지정합니다. 여러 개의 패턴을 배열 형태로 지정할 수도 있습니다.
|
||||
* **`method`**: 맵핑할 HTTP 메서드 (GET, POST, PUT, DELETE 등)를 지정합니다. 여러 개의 메서드를 배열 형태로 지정할 수 있습니다.
|
||||
* **`consumes`**: 요청의 Content-Type 헤더를 기반으로 맵핑을 제한합니다.
|
||||
* **`produces`**: 응답의 Content-Type 헤더를 기반으로 맵핑을 제한합니다.
|
||||
* **`params`**: 요청 파라미터의 존재 여부나 특정 값을 기반으로 맵핑을 제한합니다.
|
||||
* **`headers`**: 요청 헤더의 존재 여부나 특정 값을 기반으로 맵핑을 제한합니다.
|
||||
* **`@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping`, `@PatchMapping`**: `@RequestMapping` 어노테이션에 특정 HTTP 메서드를 명시적으로 지정한 축약형 어노테이션입니다. 코드의 가독성을 높여줍니다.
|
||||
* `@GetMapping(value = "/users")` 은 `@RequestMapping(value = "/users", method = RequestMethod.GET)` 과 동일합니다.
|
||||
|
||||
### 3. URL 패턴 매칭 규칙
|
||||
|
||||
`@RequestMapping` 또는 축약형 어노테이션의 `value` 또는 `path` 속성에 지정하는 URL 패턴은 다음과 같은 규칙을 따릅니다.
|
||||
|
||||
* **정확한 매칭**: `/users` 와 같이 정확한 URL 경로를 지정하면 해당 경로로의 요청만 매핑됩니다.
|
||||
* **와일드카드 (`*`)**: 하나의 경로 세그먼트 내에서 모든 문자와 매칭됩니다. 예를 들어 `/products/*` 는 `/products/1`, `/products/abc` 등과 매칭됩니다.
|
||||
* **이중 와일드카드 (`**`)**: 0개 이상의 경로 세그먼트와 매칭됩니다. 클래스 레벨에서 주로 사용되며, 하위의 모든 경로를 포괄합니다. 예를 들어 `@RequestMapping("/api/**")` 로 설정된 컨트롤러는 `/api/users`, `/api/products/1`, `/api/admin/settings` 등 모든 `/api/` 하위 경로와 매칭됩니다.
|
||||
* **경로 변수 (`{variableName}`)**: URL 경로의 특정 부분을 변수로 추출하여 메서드 파라미터로 전달할 수 있습니다. `@PathVariable` 어노테이션과 함께 사용됩니다. 예를 들어 `/users/{userId}` 로 맵핑된 경우, `/users/123` 요청 시 `123` 이 `userId` 변수에 담겨 메서드로 전달됩니다.
|
||||
|
||||
### 4. URL 맵핑 우선순위
|
||||
|
||||
여러 개의 맵핑 규칙이 하나의 URL과 일치할 수 있습니다. 이 경우 스프링은 다음과 같은 우선순위에 따라 가장 적절한 핸들러를 선택합니다.
|
||||
|
||||
1. **정확한 매칭**: 정확히 일치하는 URL이 가장 높은 우선순위를 가집니다.
|
||||
2. **더 구체적인 패턴**: 와일드카드 (`*`, `**`)를 포함하는 패턴보다 정확한 세그먼트가 더 많은 패턴이 높은 우선순위를 가집니다. 예를 들어 `/products/*` 보다 `/products/details` 가 더 구체적입니다.
|
||||
3. **와일드카드 순서**: `*` 보다 `**` 가 낮은 우선순위를 가집니다.
|
||||
4. **HTTP 메서드**: 요청의 HTTP 메서드와 정확히 일치하는 맵핑이 우선됩니다.
|
||||
5. **`consumes`, `produces`, `params`, `headers` 조건**: 이러한 조건이 더 구체적으로 명시된 맵핑이 우선됩니다.
|
||||
|
||||
### 5. URL 맵핑 예시
|
||||
|
||||
다음은 다양한 URL 맵핑 어노테이션 활용 예시입니다.
|
||||
|
||||
**`@Controller` 사용 예시 (HTML 뷰 반환):**
|
||||
|
||||
```java
|
||||
@Controller
|
||||
public class HomeController {
|
||||
|
||||
@GetMapping("/")
|
||||
public String home(Model model) {
|
||||
model.addAttribute("message", "Welcome to the homepage!");
|
||||
return "home"; // home.html 뷰 반환
|
||||
}
|
||||
|
||||
@GetMapping("/about")
|
||||
public String about() {
|
||||
return "about"; // about.html 뷰 반환
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**`@RestController` 사용 예시 (JSON 데이터 반환):**
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UserApiController {
|
||||
|
||||
@GetMapping
|
||||
public List<User> getAllUsers() {
|
||||
// 모든 사용자 정보 조회 로직
|
||||
return Arrays.asList(new User(1, "John Doe"), new User(2, "Jane Smith"));
|
||||
}
|
||||
|
||||
@GetMapping("/{userId}")
|
||||
public ResponseEntity<User> getUserById(@PathVariable Long userId) {
|
||||
// 특정 사용자 정보 조회 로직
|
||||
User user = findUser(userId);
|
||||
if (user != null) {
|
||||
return ResponseEntity.ok(user);
|
||||
} else {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<User> createUser(@RequestBody User newUser) {
|
||||
// 새로운 사용자 생성 로직
|
||||
User createdUser = saveUser(newUser);
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
|
||||
}
|
||||
|
||||
@PutMapping("/{userId}")
|
||||
public ResponseEntity<User> updateUser(@PathVariable Long userId, @RequestBody User updatedUser) {
|
||||
// 특정 사용자 정보 업데이트 로직
|
||||
User user = updateUser(userId, updatedUser);
|
||||
if (user != null) {
|
||||
return ResponseEntity.ok(user);
|
||||
} else {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
}
|
||||
|
||||
@DeleteMapping("/{userId}")
|
||||
public ResponseEntity<Void> deleteUser(@PathVariable Long userId) {
|
||||
// 특정 사용자 삭제 로직
|
||||
deleteUser(userId);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
private User findUser(Long userId) {
|
||||
// ... 구현 생략
|
||||
return null;
|
||||
}
|
||||
|
||||
private User saveUser(User newUser) {
|
||||
// ... 구현 생략
|
||||
return null;
|
||||
}
|
||||
|
||||
private User updateUser(Long userId, User updatedUser) {
|
||||
// ... 구현 생략
|
||||
return null;
|
||||
}
|
||||
|
||||
private void deleteUser(Long userId) {
|
||||
// ... 구현 생략
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**클래스 레벨 `@RequestMapping` 과 메서드 레벨 맵핑 조합:**
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/products")
|
||||
public class ProductApiController {
|
||||
|
||||
@GetMapping
|
||||
public List<Product> getAllProducts() {
|
||||
// ...
|
||||
return null;
|
||||
}
|
||||
|
||||
@GetMapping("/{productId}")
|
||||
public Product getProductById(@PathVariable Long productId) {
|
||||
// ...
|
||||
return null;
|
||||
}
|
||||
|
||||
@PostMapping("/search")
|
||||
public List<Product> searchProducts(@RequestBody SearchCriteria criteria) {
|
||||
// ...
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
위 예시에서 `/api/products` 로 시작하는 모든 요청은 `ProductApiController` 에서 처리하며, 메서드 레벨의 `@GetMapping`, `@PostMapping` 등을 통해 더 구체적인 URL과 매핑됩니다. 예를 들어 `/api/products` 에 대한 GET 요청은 `getAllProducts()` 메서드가 처리하고, `/api/products/{productId}` 에 대한 GET 요청은 `getProductById()` 메서드가 처리합니다.
|
||||
|
||||
### 6. 스프링 부트 자동 구성과 URL 맵핑
|
||||
|
||||
스프링 부트는 개발자가 직접 많은 설정을 하지 않아도 기본적인 URL 맵핑 기능을 제공합니다. `@SpringBootApplication` 어노테이션이 포함된 메인 클래스를 실행하면 스프링 MVC가 자동으로 구성되고, 컨트롤러 어노테이션이 붙은 클래스들의 메서드에 정의된 URL 맵핑 정보를 스캔하여 등록합니다.
|
||||
|
||||
### 7. 사용자 정의 URL 맵핑 설정
|
||||
|
||||
때로는 어노테이션 방식 외에 프로그래밍 방식으로 URL 맵핑을 설정해야 할 경우가 있습니다. 이럴 때는 `WebFluxConfigurer` 또는 `WebMvcConfigurer` 인터페이스를 구현하여 `addResourceHandlers()`, `addViewControllers()` 등의 메서드를 오버라이드하여 사용자 정의 맵핑 규칙을 추가할 수 있습니다.
|
||||
|
||||
### 결론
|
||||
|
||||
스프링 부트의 URL 맵핑은 웹 애플리케이션의 동작 방식을 결정하는 중요한 요소입니다. 다양한 어노테이션과 패턴 매칭 규칙을 이해하고 적절하게 활용하면 효율적이고 유지보수하기 쉬운 웹 애플리케이션을 구축할 수 있습니다. 이 가이드가 스프링 부트 URL 맵핑에 대한 깊이 있는 이해를 돕고, 실제 개발에 유용하게 활용될 수 있기를 바랍니다.
|
||||
349
docs/24_jackson.md
Normal file
349
docs/24_jackson.md
Normal file
@@ -0,0 +1,349 @@
|
||||
## Jackson 어노테이션 완벽 정리 및 상세 설명
|
||||
|
||||
Jackson은 자바 객체를 JSON으로 직렬화하거나 JSON을 자바 객체로 역직렬화하는 데 가장 널리 사용되는 라이브러리입니다. Jackson은 기본적으로 많은 부분을 자동으로 처리하지만, 어노테이션을 사용하면 직렬화 및 역직렬화 과정을 세밀하게 제어할 수 있습니다.
|
||||
|
||||
이번 글에서는 Jackson에서 주로 사용되는 어노테이션들을 표로 정리하고, 각 어노테이션의 역할과 사용법을 상세히 설명하겠습니다.
|
||||
|
||||
### Jackson 주요 어노테이션 요약 표
|
||||
|
||||
| 어노테이션 | 적용 대상 | 설명 |
|
||||
| :----------------------------- | :---------------- | :--------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **클래스 레벨** | | |
|
||||
| `@JsonIgnoreProperties` | 클래스 | 직렬화 또는 역직렬화 시 특정 필드를 무시합니다. |
|
||||
| `@JsonPropertyOrder` | 클래스 | JSON으로 직렬화될 때 필드의 순서를 지정합니다. |
|
||||
| `@JsonRootName` | 클래스 | JSON 최상위 레벨의 이름을 지정합니다. (특정 설정 필요) |
|
||||
| `@JsonTypeInfo` | 클래스 | 상속 관계의 객체를 직렬화/역직렬화할 때 타입 정보를 포함하도록 설정합니다. |
|
||||
| `@JsonTypeName` | 클래스 | `@JsonTypeInfo` 와 함께 사용되어 특정 하위 타입의 이름을 지정합니다. |
|
||||
| `@JsonUnwrapped` | 필드 | 해당 필드의 내용을 현재 객체의 필드인 것처럼 펼쳐서 직렬화/역직렬화합니다. |
|
||||
| **필드 레벨** | | |
|
||||
| `@JsonProperty` | 필드, 메서드 | JSON 필드 이름과 자바 객체 필드 또는 메서드 이름을 매핑합니다. 역직렬화 시 필수 필드를 지정할 수도 있습니다. |
|
||||
| `@JsonIgnore` | 필드, 메서드 | 해당 필드 또는 메서드를 직렬화 및 역직렬화 과정에서 무시합니다. |
|
||||
| `@JsonIgnoreType` | 클래스, 필드 | 해당 타입의 모든 필드를 직렬화 및 역직렬화 과정에서 무시합니다. |
|
||||
| `@JsonSerialize` | 필드, 메서드 | 해당 필드 또는 메서드의 값을 직렬화할 때 사용할 사용자 정의 Serializer를 지정합니다. |
|
||||
| `@JsonDeserialize` | 필드, 메서드 | 해당 필드 또는 메서드의 값을 역직렬화할 때 사용할 사용자 정의 Deserializer를 지정합니다. |
|
||||
| `@JsonFormat` | 필드, 메서드 | 날짜, 시간 등의 특정 타입의 직렬화/역직렬화 포맷을 지정합니다. |
|
||||
| `@JsonValue` | 메서드 | 해당 메서드의 반환 값을 객체 전체의 JSON 값으로 사용합니다. (주로 Enum에서 사용) |
|
||||
| `@JsonCreator` | 생성자, 정적 팩토리 메서드 | 역직렬화 시 사용할 생성자 또는 정적 팩토리 메서드를 지정합니다. `@JsonProperty` 와 함께 사용하여 매개변수를 매핑합니다. |
|
||||
| `@JsonAnyGetter` | 메서드 | 동적으로 생성되는 필드들을 JSON 객체로 직렬화할 때 사용됩니다. (Map 타입 필드에 적용) |
|
||||
| `@JsonAnySetter` | 메서드 | JSON 객체의 알 수 없는 필드들을 역직렬화하여 자바 객체의 Map 타입 필드에 저장할 때 사용됩니다. |
|
||||
| `@JsonManagedReference` | 필드 | 순환 참조 관계에서 직렬화를 제어하는 데 사용됩니다. (Forward reference) |
|
||||
| `@JsonBackReference` | 필드 | 순환 참조 관계에서 역직렬화를 제어하는 데 사용됩니다. (Back reference, 직렬화 시 무시) |
|
||||
| `@JsonView` | 필드, 메서드 | 특정 View 인터페이스를 활성화했을 때만 해당 필드 또는 메서드를 직렬화에 포함합니다. |
|
||||
| `@JsonAlias` | 필드 | 역직렬화 시 JSON 필드 이름의 별칭을 지정합니다. 여러 개의 별칭을 지정할 수 있습니다. |
|
||||
| `@JsonInclude` | 클래스, 필드 | 특정 조건(null 값, 기본값 등)에 따라 필드를 직렬화에서 제외합니다. |
|
||||
|
||||
### Jackson 어노테이션 상세 설명
|
||||
|
||||
이제 각 어노테이션에 대해 더 자세히 알아보겠습니다. 예시 코드를 통해 각 어노테이션의 동작 방식을 명확히 이해할 수 있습니다.
|
||||
|
||||
**클래스 레벨 어노테이션**
|
||||
|
||||
* **`@JsonIgnoreProperties({"field1", "field2"})`**:
|
||||
* 직렬화 또는 역직렬화 시 지정된 이름의 필드를 무시합니다.
|
||||
* `ignoreUnknown = true` 속성을 사용하면 JSON에 정의되지 않은 필드가 있어도 예외를 발생시키지 않고 무시합니다.
|
||||
|
||||
```java
|
||||
@JsonIgnoreProperties({"internalId", "creationDate"})
|
||||
public class User {
|
||||
public int id;
|
||||
public String name;
|
||||
private String internalId;
|
||||
private Date creationDate;
|
||||
|
||||
// ... (생성자, Getter, Setter)
|
||||
}
|
||||
|
||||
// JSON -> User 역직렬화 시 "internalId", "creationDate" 필드는 무시됨
|
||||
// User -> JSON 직렬화 시 "internalId", "creationDate" 필드는 포함되지 않음
|
||||
```
|
||||
|
||||
* **`@JsonPropertyOrder({"name", "id", "email"})`**:
|
||||
* JSON으로 직렬화될 때 필드의 순서를 명시적으로 지정합니다. 지정되지 않은 필드는 지정된 필드 뒤에 알파벳 순으로 나타납니다.
|
||||
|
||||
```java
|
||||
@JsonPropertyOrder({"name", "id", "email"})
|
||||
public class Person {
|
||||
public int id;
|
||||
public String name;
|
||||
public String email;
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
// Person 객체를 JSON으로 직렬화하면 "name", "id", "email" 순서로 필드가 나타남
|
||||
```
|
||||
|
||||
* **`@JsonRootName("user")`**:
|
||||
* JSON을 최상위 레벨의 이름으로 래핑합니다. Jackson의 `SerializationFeature.WRAP_ROOT_VALUE` 설정을 활성화해야 동작합니다.
|
||||
|
||||
```java
|
||||
@JsonRootName("user")
|
||||
public class Profile {
|
||||
public String username;
|
||||
public String role;
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
// ObjectMapper mapper = new ObjectMapper();
|
||||
// mapper.enable(SerializationFeature.WRAP_ROOT_VALUE);
|
||||
// mapper.writeValueAsString(new Profile("testuser", "admin"));
|
||||
// 결과: {"user":{"username":"testuser","role":"admin"}}
|
||||
```
|
||||
|
||||
* **`@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@type")`**:
|
||||
* 상속 관계에 있는 클래스들을 직렬화/역직렬화할 때 타입 정보를 JSON에 포함하도록 설정합니다.
|
||||
* `use`: 타입 정보를 식별하는 방식을 지정 (예: `NAME`, `CLASS`)
|
||||
* `include`: 타입 정보를 JSON에 포함하는 방식 (예: `PROPERTY`, `WRAPPER_OBJECT`)
|
||||
* `property`: 타입 정보를 저장할 JSON 필드 이름
|
||||
|
||||
* **`@JsonTypeName("circle")`**:
|
||||
* `@JsonTypeInfo` 와 함께 사용되어 특정 하위 타입의 이름을 지정합니다.
|
||||
|
||||
```java
|
||||
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "@type")
|
||||
abstract class Shape {
|
||||
public String color;
|
||||
}
|
||||
|
||||
@JsonTypeName("circle")
|
||||
public class Circle extends Shape {
|
||||
public double radius;
|
||||
}
|
||||
|
||||
@JsonTypeName("rectangle")
|
||||
public class Rectangle extends Shape {
|
||||
public double width;
|
||||
public double height;
|
||||
}
|
||||
|
||||
// Circle 객체를 직렬화하면 JSON에 "@type":"circle" 정보가 포함됨
|
||||
```
|
||||
|
||||
* **`@JsonUnwrapped`**:
|
||||
* 포함된 객체의 필드를 현재 객체의 필드처럼 펼쳐서 직렬화/역직렬화합니다. 이름 충돌에 주의해야 합니다.
|
||||
* `prefix` 속성을 사용하여 펼쳐진 필드 이름에 공통 접두사를 추가할 수 있습니다.
|
||||
|
||||
```java
|
||||
public class Address {
|
||||
public String street;
|
||||
public String city;
|
||||
}
|
||||
|
||||
public class PersonInfo {
|
||||
public String name;
|
||||
@JsonUnwrapped
|
||||
public Address address;
|
||||
}
|
||||
|
||||
// PersonInfo 객체를 직렬화하면 JSON은 {"name":"...", "street":"...", "city":"..."} 형태가 됨
|
||||
```
|
||||
|
||||
**필드 레벨 어노테이션**
|
||||
|
||||
* **`@JsonProperty("fieldNameInJson")`**:
|
||||
* 자바 필드 이름과 JSON 필드 이름을 다르게 매핑합니다.
|
||||
* `required = true` 속성을 사용하여 역직렬화 시 해당 필드가 JSON에 반드시 존재해야 함을 명시할 수 있습니다.
|
||||
|
||||
```java
|
||||
public class Product {
|
||||
@JsonProperty("product_id")
|
||||
public int id;
|
||||
@JsonProperty(value = "product_name", required = true)
|
||||
public String name;
|
||||
|
||||
// ...
|
||||
}
|
||||
|
||||
// JSON {"product_id": 123, "product_name": "Laptop"} 은 Product 객체로 역직렬화됨
|
||||
// 역직렬화 시 "product_name" 필드가 없으면 예외 발생
|
||||
```
|
||||
|
||||
* **`@JsonIgnore`**:
|
||||
* 해당 필드를 직렬화 및 역직렬화 과정에서 완전히 무시합니다.
|
||||
|
||||
```java
|
||||
public class Employee {
|
||||
public int id;
|
||||
public String name;
|
||||
@JsonIgnore
|
||||
private String password; // JSON으로 노출하지 않음
|
||||
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
* **`@JsonIgnoreType`**:
|
||||
* 해당 타입의 모든 필드를 직렬화 및 역직렬화 과정에서 무시합니다.
|
||||
|
||||
```java
|
||||
@JsonIgnoreType
|
||||
public class InternalInfo {
|
||||
public String securityCode;
|
||||
public Date lastAccessed;
|
||||
}
|
||||
|
||||
public class SystemData {
|
||||
public String version;
|
||||
public InternalInfo internal; // InternalInfo 타입의 필드는 직렬화/역직렬화 시 무시됨
|
||||
}
|
||||
```
|
||||
|
||||
* **`@JsonSerialize(using = CustomSerializer.class)`**:
|
||||
* 해당 필드를 직렬화할 때 사용할 사용자 정의 Serializer 클래스를 지정합니다. 복잡한 객체나 특정 포맷으로 직렬화해야 할 때 유용합니다.
|
||||
|
||||
* **`@JsonDeserialize(using = CustomDeserializer.class)`**:
|
||||
* 해당 필드를 역직렬화할 때 사용할 사용자 정의 Deserializer 클래스를 지정합니다. JSON 데이터를 특정 객체로 변환하는 로직을 직접 구현할 수 있습니다.
|
||||
|
||||
* **`@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")`**:
|
||||
* 날짜, 시간 등의 특정 타입의 직렬화/역직렬화 포맷을 지정합니다. `pattern`, `timezone`, `locale` 등의 속성을 사용할 수 있습니다.
|
||||
|
||||
```java
|
||||
public class Event {
|
||||
public String name;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd")
|
||||
public Date eventDate;
|
||||
}
|
||||
|
||||
// Event 객체의 eventDate는 "yyyy-MM-dd" 형식의 문자열로 직렬화됨
|
||||
// 해당 형식의 문자열은 Date 객체로 역직렬화됨
|
||||
```
|
||||
|
||||
* **`@JsonValue`**:
|
||||
* 메서드에 적용하며, 해당 메서드의 반환 값을 객체 전체의 JSON 값으로 사용합니다. 주로 Enum에서 Enum의 특정 값을 JSON으로 표현하고자 할 때 사용됩니다.
|
||||
|
||||
```java
|
||||
public enum Status {
|
||||
OK("정상"),
|
||||
ERROR("오류");
|
||||
|
||||
private final String description;
|
||||
|
||||
Status(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@JsonValue
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
||||
|
||||
// Status.OK 객체를 직렬화하면 "정상" 문자열이 됨
|
||||
```
|
||||
|
||||
* **`@JsonCreator`**:
|
||||
* 클래스의 생성자 또는 정적 팩토리 메서드에 적용하여 역직렬화 시 사용할 방법을 지정합니다.
|
||||
* 생성자 파라미터 또는 정적 팩토리 메서드의 매개변수에 `@JsonProperty` 어노테이션을 함께 사용하여 JSON 필드와 매핑합니다.
|
||||
|
||||
```java
|
||||
public class Point {
|
||||
private final int x;
|
||||
private final int y;
|
||||
|
||||
@JsonCreator
|
||||
public Point(@JsonProperty("x_coordinate") int x, @JsonProperty("y_coordinate") int y) {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
}
|
||||
|
||||
public int getX() { return x; }
|
||||
public int getY() { return y; }
|
||||
}
|
||||
|
||||
// JSON {"x_coordinate": 10, "y_coordinate": 20} 은 Point 객체로 역직렬화됨
|
||||
```
|
||||
|
||||
* **`@JsonAnyGetter`**:
|
||||
* Map 타입의 필드에 적용된 메서드에 사용됩니다. 직렬화 시 Map의 키-값 쌍을 JSON 객체의 필드로 동적으로 추가합니다.
|
||||
|
||||
```java
|
||||
public class Attributes {
|
||||
private Map<String, Object> properties = new HashMap<>();
|
||||
|
||||
public void addProperty(String key, Object value) {
|
||||
this.properties.put(key, value);
|
||||
}
|
||||
|
||||
@JsonAnyGetter
|
||||
public Map<String, Object> getProperties() {
|
||||
return properties;
|
||||
}
|
||||
}
|
||||
|
||||
// Attributes 객체에 addProperty("color", "blue"), addProperty("size", "large")를 호출 후 직렬화하면
|
||||
// JSON은 {"color":"blue", "size":"large"} 형태가 됨
|
||||
```
|
||||
|
||||
* **`@JsonAnySetter`**:
|
||||
* Map 타입의 필드에 적용된 메서드에 사용됩니다. 역직렬화 시 JSON 객체에 정의되지 않은 필드들을 Map에 동적으로 저장합니다. 메서드는 필드 이름과 값을 매개변수로 받아야 합니다.
|
||||
|
||||
```java
|
||||
public class UnknownProperties {
|
||||
private Map<String, Object> unknown = new HashMap<>();
|
||||
|
||||
@JsonAnySetter
|
||||
public void addUnknown(String name, Object value) {
|
||||
this.unknown.put(name, value);
|
||||
}
|
||||
|
||||
public Map<String, Object> getUnknown() {
|
||||
return unknown;
|
||||
}
|
||||
}
|
||||
|
||||
// JSON {"name":"Product A", "price": 25.99, "category": "Electronics"} 를 UnknownProperties 객체로 역직렬화하면
|
||||
// "category": "Electronics" 정보는 unknown Map에 저장됨
|
||||
```
|
||||
|
||||
* **`@JsonManagedReference` & `@JsonBackReference`**:
|
||||
* 객체 간의 순환 참조(예: 부모-자식 관계)가 있을 때 직렬화/역직렬화 과정에서 무한 루프를 방지하고 관계를 올바르게 처리하는 데 사용됩니다.
|
||||
* `@JsonManagedReference`: 순환 참조의 "정방향" (부모 -> 자식) 관계를 나타내는 필드에 적용합니다. 이 필드는 정상적으로 직렬화됩니다.
|
||||
* `@JsonBackReference`: 순환 참조의 "역방향" (자식 -> 부모) 관계를 나타내는 필드에 적용합니다. 이 필드는 직렬화 시 무시됩니다. 역직렬화 시에는 `@JsonManagedReference` 필드를 통해 관계가 복원됩니다.
|
||||
|
||||
```java
|
||||
public class Parent {
|
||||
public String name;
|
||||
@JsonManagedReference
|
||||
public List<Child> children;
|
||||
}
|
||||
|
||||
public class Child {
|
||||
public String childName;
|
||||
@JsonBackReference
|
||||
public Parent parent;
|
||||
}
|
||||
|
||||
// Parent 객체를 직렬화하면 children 리스트가 포함되지만, 각 Child 객체의 parent 필드는 제외됨
|
||||
// 역직렬화 시 children 정보를 바탕으로 각 Child 객체의 parent 필드가 올바르게 설정됨
|
||||
```
|
||||
|
||||
* **`@JsonView(MyView.Public.class)`**:
|
||||
* 특정 View 인터페이스를 정의하고, `@JsonView` 어노테이션을 사용하여 해당 View가 활성화되었을 때만 필드를 직렬화에 포함하도록 제어합니다. 다양한 수준의 정보 공개가 필요한 API 개발에 유용합니다.
|
||||
|
||||
```java
|
||||
public class Item {
|
||||
public interface Public {}
|
||||
public interface Internal extends Public {}
|
||||
|
||||
@JsonView(Public.class)
|
||||
public int id;
|
||||
@JsonView(Public.class)
|
||||
public String itemName;
|
||||
@JsonView(Internal.class)
|
||||
public String internalCode;
|
||||
}
|
||||
|
||||
// ObjectMapper mapper = new ObjectMapper();
|
||||
// mapper.setConfig(mapper.getSerializationConfig().withView(Item.Public.class));
|
||||
// Item 객체를 직렬화하면 id와 itemName만 포함됨
|
||||
// mapper.setConfig(mapper.getSerializationConfig().withView(Item.Internal.class));
|
||||
// Item 객체를 직렬화하면 id, itemName, internalCode 모두 포함됨
|
||||
```
|
||||
|
||||
* **`@JsonAlias({"oldName1", "oldName2"})`**:
|
||||
* 역직렬화 시 JSON 필드 이름의 별칭을 지정합니다. API 버전 관리 등으로 인해 필드 이름이 변경되었을 때 이전 이름으로도 역직렬화를 허용할 수 있습니다.
|
||||
|
||||
```java
|
||||
public class LegacyUser {
|
||||
@
|
||||
366
docs/25_map struct.md
Normal file
366
docs/25_map struct.md
Normal file
@@ -0,0 +1,366 @@
|
||||
# MapStruct란?
|
||||
|
||||
**MapStruct**는 Java Bean 간의 매핑을 쉽게 해주는 코드 생성 기반 매퍼 프레임워크입니다.
|
||||
DTO(Data Transfer Object)와 Entity 간의 변환을 할 때 반복되는 getter/setter 작업을 자동화해주며, **런타임이 아닌 컴파일 타임에 매핑 코드를 생성**하므로 성능도 뛰어납니다.
|
||||
|
||||
## 주요 특징
|
||||
- **컴파일 타임에 코드 생성** (런타임 Reflection 사용 X)
|
||||
- Lombok과 잘 연동됨
|
||||
- 커스터마이징 가능 (부분 매핑, `@Mapping` 등)
|
||||
- Spring과 쉽게 통합
|
||||
|
||||
---
|
||||
|
||||
## Spring Boot에서 MapStruct 사용 예시
|
||||
|
||||
### 1. Gradle 설정
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation ("org.mapstruct:mapstruct:1.6.3")
|
||||
annotationProcessor ("org.mapstruct:mapstruct-processor:1.6.3")
|
||||
}
|
||||
|
||||
tasks.withType(JavaCompile) {
|
||||
options.annotationProcessorPath = configurations.annotationProcessor
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. DTO 및 Entity 클래스
|
||||
|
||||
```java
|
||||
// User.java (Entity)
|
||||
@Getter @Setter
|
||||
public class User {
|
||||
private Long id;
|
||||
private String name;
|
||||
private String email;
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
// UserDto.java
|
||||
@Getter @Setter
|
||||
public class UserDto {
|
||||
private String name;
|
||||
private String email;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. 매퍼 인터페이스 작성
|
||||
|
||||
```java
|
||||
@Mapper(componentModel = "spring") // Spring Bean으로 등록
|
||||
public interface UserMapper {
|
||||
UserDto toDto(User user);
|
||||
User toEntity(UserDto userDto);
|
||||
}
|
||||
```
|
||||
|
||||
> `componentModel = "spring"`을 설정하면 Spring의 `@Component`로 등록되므로, 의존성 주입이 가능합니다.
|
||||
|
||||
---
|
||||
|
||||
### 4. 서비스에서 사용
|
||||
|
||||
```java
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class UserService {
|
||||
private final UserMapper userMapper;
|
||||
|
||||
public UserDto getUserDto(User user) {
|
||||
return userMapper.toDto(user);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 마무리
|
||||
|
||||
MapStruct는 반복적인 매핑 코드를 없애고, 명시적이고 빠른 매핑을 제공하는 훌륭한 도구입니다. 특히 **Spring Boot + Lombok**과 함께 사용하면 생산성과 유지보수성이 크게 향상됩니다.
|
||||
|
||||
필요하다면 `@Mapping` 어노테이션을 사용해 필드명을 다르게 매핑하거나, 일부 필드만 매핑하는 커스터마이징도 가능합니다.
|
||||
|
||||
---
|
||||
|
||||
좋습니다! 이번에는 **MapStruct의 고급 기능**들을 소개해드릴게요. 실무에서 자주 쓰이는 매핑 커스터마이징 기능과 예제 중심으로 설명드릴게요.
|
||||
|
||||
---
|
||||
|
||||
## MapStruct 고급 기능
|
||||
|
||||
### 1. **@Mapping**: 이름이 다른 필드 매핑
|
||||
|
||||
DTO와 Entity의 필드명이 다를 경우 사용합니다.
|
||||
|
||||
```java
|
||||
@Getter @Setter
|
||||
public class User {
|
||||
private Long id;
|
||||
private String fullName;
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
@Getter @Setter
|
||||
public class UserDto {
|
||||
private String name;
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface UserMapper {
|
||||
@Mapping(source = "fullName", target = "name")
|
||||
UserDto toDto(User user);
|
||||
}
|
||||
```
|
||||
|
||||
> `source`: 원본 객체의 필드
|
||||
> `target`: 매핑될 대상 객체의 필드
|
||||
|
||||
---
|
||||
|
||||
### 2. **@Mapping(target = ..., ignore = true)**: 특정 필드 무시
|
||||
|
||||
```java
|
||||
@Mapping(target = "id", ignore = true)
|
||||
User toEntity(UserDto dto);
|
||||
```
|
||||
|
||||
`id`는 DB에서 자동 생성되므로 무시할 때 자주 사용합니다.
|
||||
|
||||
---
|
||||
|
||||
### 3. **@InheritInverseConfiguration**: 역 매핑 정의 재사용
|
||||
|
||||
```java
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface UserMapper {
|
||||
@Mapping(source = "fullName", target = "name")
|
||||
UserDto toDto(User user);
|
||||
|
||||
@InheritInverseConfiguration
|
||||
User toEntity(UserDto dto);
|
||||
}
|
||||
```
|
||||
|
||||
> `toDto`에 정의된 매핑 규칙을 `toEntity`에도 반대로 적용합니다.
|
||||
|
||||
---
|
||||
|
||||
### 4. **@AfterMapping / @BeforeMapping**: 매핑 전후 로직 삽입
|
||||
|
||||
```java
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface UserMapper {
|
||||
|
||||
UserDto toDto(User user);
|
||||
|
||||
@AfterMapping
|
||||
default void enrichDto(@MappingTarget UserDto dto) {
|
||||
dto.setName(dto.getName().toUpperCase()); // 예: 이름을 대문자로 처리
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. **중첩 객체 매핑 (Nested Mapping)**
|
||||
|
||||
```java
|
||||
@Getter @Setter
|
||||
public class User {
|
||||
private Address address;
|
||||
}
|
||||
|
||||
@Getter @Setter
|
||||
public class Address {
|
||||
private String city;
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
@Getter @Setter
|
||||
public class UserDto {
|
||||
private String city;
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
@Mapping(source = "address.city", target = "city")
|
||||
UserDto toDto(User user);
|
||||
```
|
||||
|
||||
> 중첩된 객체의 필드도 `. (dot)` 문법으로 접근 가능합니다.
|
||||
|
||||
---
|
||||
|
||||
### 6. **Collection 매핑**
|
||||
|
||||
```java
|
||||
List<UserDto> toDtoList(List<User> users);
|
||||
Set<User> toEntitySet(Set<UserDto> dtos);
|
||||
```
|
||||
|
||||
> List, Set, Map 같은 컬렉션도 자동 매핑됩니다.
|
||||
|
||||
---
|
||||
|
||||
### 7. **Mapper 간 의존성 (다른 Mapper 사용)**
|
||||
|
||||
```java
|
||||
@Mapper(componentModel = "spring", uses = { AddressMapper.class })
|
||||
public interface UserMapper {
|
||||
UserDto toDto(User user);
|
||||
}
|
||||
```
|
||||
|
||||
> 복잡한 구조에서는 하위 매퍼를 만들어 분리할 수 있어요.
|
||||
|
||||
---
|
||||
|
||||
## 정리
|
||||
|
||||
MapStruct 고급 기능을 사용하면 다음과 같은 작업이 쉬워집니다:
|
||||
|
||||
- 필드명이 다른 경우 매핑
|
||||
- 무시할 필드 지정
|
||||
- 매핑 재사용 (`@InheritInverseConfiguration`)
|
||||
- 후처리 로직 삽입
|
||||
- 복잡한 구조/컬렉션/다형성 처리
|
||||
|
||||
---
|
||||
좋아요! 이번에는 **MapStruct의 고급 기능** 중에서도 실무에서 자주 사용되는 **Enum 매핑**, **커스텀 매핑 메서드**, **Builder 패턴 지원**에 대해 자세히 설명드릴게요.
|
||||
|
||||
---
|
||||
|
||||
## 1. Enum 매핑
|
||||
|
||||
MapStruct는 기본적으로 **같은 이름을 가진 Enum 상수끼리 자동으로 매핑**합니다.
|
||||
|
||||
### 예시: 이름이 같은 Enum 매핑
|
||||
|
||||
```java
|
||||
public enum Role {
|
||||
ADMIN, USER
|
||||
}
|
||||
|
||||
public enum RoleDto {
|
||||
ADMIN, USER
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface UserMapper {
|
||||
RoleDto toDto(Role role);
|
||||
}
|
||||
```
|
||||
|
||||
> 이름이 같다면 자동으로 매핑됩니다.
|
||||
> 이름이 다르면 `@ValueMapping` 또는 `@Mapping`을 사용해야 해요.
|
||||
|
||||
### 예시: 이름이 다른 Enum 매핑
|
||||
|
||||
```java
|
||||
public enum Role {
|
||||
ADMINISTRATOR, NORMAL_USER
|
||||
}
|
||||
|
||||
public enum RoleDto {
|
||||
ADMIN, USER
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
@ValueMappings({
|
||||
@ValueMapping(source = "ADMINISTRATOR", target = "ADMIN"),
|
||||
@ValueMapping(source = "NORMAL_USER", target = "USER")
|
||||
})
|
||||
RoleDto toDto(Role role);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 커스텀 매핑 메서드
|
||||
|
||||
MapStruct는 매핑 중 호출할 **사용자 정의 메서드**도 지원합니다.
|
||||
|
||||
### 예시: 날짜 형식 변환
|
||||
|
||||
```java
|
||||
@Getter @Setter
|
||||
public class User {
|
||||
private LocalDateTime registeredAt;
|
||||
}
|
||||
|
||||
@Getter @Setter
|
||||
public class UserDto {
|
||||
private String registeredDate;
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
@Mapper(componentModel = "spring")
|
||||
public interface UserMapper {
|
||||
@Mapping(source = "registeredAt", target = "registeredDate")
|
||||
UserDto toDto(User user);
|
||||
|
||||
default String map(LocalDateTime dateTime) {
|
||||
return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> MapStruct는 `LocalDateTime -> String` 변환 시 `map(...)` 메서드를 자동으로 호출합니다.
|
||||
|
||||
---
|
||||
|
||||
## 3. Builder 패턴 지원
|
||||
|
||||
Builder 패턴을 사용하는 객체도 MapStruct에서 지원합니다.
|
||||
Lombok의 `@Builder`를 쓰거나 직접 Builder 클래스를 만든 경우에도 동작합니다.
|
||||
|
||||
### 예시: Lombok + Builder
|
||||
|
||||
```java
|
||||
@Getter @Setter
|
||||
@Builder
|
||||
public class UserDto {
|
||||
private String name;
|
||||
private String email;
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
@Mapper(componentModel = "spring", builder = @Builder(disableBuilder = false))
|
||||
public interface UserMapper {
|
||||
UserDto toDto(User user);
|
||||
}
|
||||
```
|
||||
|
||||
> MapStruct는 `@Builder`가 붙은 클래스에 대해 자동으로 `.builder().field(...).build()`를 사용해 객체를 생성합니다.
|
||||
|
||||
### 주의할 점:
|
||||
- `@Builder(disableBuilder = true)`이면 Builder 무시
|
||||
- `@Builder(disableBuilder = false)` 또는 생략하면 Builder 사용
|
||||
- 커스텀 Builder 클래스의 경우 구조가 달라지면 빌더 매핑이 안 될 수도 있으니 주의
|
||||
|
||||
---
|
||||
|
||||
## 마무리 정리
|
||||
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| **Enum 매핑** | Enum 이름 자동 매핑, 다를 경우 `@ValueMapping` 사용 |
|
||||
| **커스텀 메서드** | 복잡한 변환 로직을 메서드로 정의 가능 |
|
||||
| **Builder 패턴** | `@Builder` 객체도 매핑 가능 (자동 빌더 생성) |
|
||||
|
||||
---
|
||||
249
docs/31_jpa entity.md
Normal file
249
docs/31_jpa entity.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# Spring Boot의 Model 및 JPA Entity
|
||||
|
||||
|
||||
## Model 및 JPA 관련 어노테이션 정리
|
||||
|
||||
| 어노테이션 | 설명 |
|
||||
|------------------------|----------------------------------|
|
||||
| `@Entity` | JPA 엔티티 클래스임을 명시 |
|
||||
| `@Table` | 데이터베이스의 테이블과 매핑 |
|
||||
| `@Id` | 기본 키(PK) 지정 |
|
||||
| `@GeneratedValue` | 기본 키 값 자동 생성 전략 설정 |
|
||||
| `@Column` | 데이터베이스 컬럼과 매핑 |
|
||||
| `@Transient` | 특정 필드를 영속성에서 제외 |
|
||||
| `@Embedded` | 내장(Embeddable) 타입을 포함 |
|
||||
| `@Embeddable` | 내장 타입을 정의 |
|
||||
| `@Enumerated` | Enum 타입을 컬럼에 매핑 |
|
||||
| `@Lob` | 대용량 데이터(Long Text, Blob) 저장 |
|
||||
| `@Temporal` | 날짜/시간 타입 매핑 |
|
||||
| `@ManyToOne` | 다대일 관계 설정 |
|
||||
| `@OneToMany` | 일대다 관계 설정 |
|
||||
| `@OneToOne` | 일대일 관계 설정 |
|
||||
| `@ManyToMany` | 다대다 관계 설정 |
|
||||
| `@JoinColumn` | 외래 키(FK) 설정 |
|
||||
| `@JoinTable` | 다대다 관계에서 조인 테이블 설정 |
|
||||
| `@MappedSuperclass` | 공통 속성을 가지는 부모 클래스 지정 |
|
||||
| `@Inheritance` | 상속 매핑 전략 설정 |
|
||||
| `@DiscriminatorColumn`| 상속 엔티티 구분 컬럼 설정 |
|
||||
| `@NamedQuery` | JPQL로 미리 정의된 쿼리 설정 |
|
||||
| `@Query` | 사용자 정의 JPQL 쿼리 작성 |
|
||||
| `@Modifying` | 데이터 수정 JPQL 쿼리 작성 |
|
||||
| `@Transactional` | 트랜잭션 범위 설정 |
|
||||
| `@Repository` | 데이터 액세스 계층을 나타냄 |
|
||||
| `@EnableJpaRepositories` | JPA 리포지토리 활성화 |
|
||||
|
||||
|
||||
### `@Entity`
|
||||
JPA에서 엔티티 클래스를 나타냅니다. 해당 클래스는 데이터베이스 테이블과 매핑됩니다.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class User {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
}
|
||||
```
|
||||
- `User` 클래스가 `users` 테이블과 매핑됩니다.
|
||||
|
||||
|
||||
### `@Table`
|
||||
엔티티와 매핑될 테이블의 이름을 지정합니다.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
@Table(name = "user_table")
|
||||
public class User {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
}
|
||||
```
|
||||
- `User` 엔티티가 `user_table`과 매핑됩니다.
|
||||
|
||||
|
||||
### `@Id`
|
||||
엔티티의 **기본 키(PK)** 를 지정합니다.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Product {
|
||||
@Id
|
||||
private Long productId;
|
||||
}
|
||||
```
|
||||
- `productId`가 **기본 키**가 됩니다.
|
||||
|
||||
|
||||
### `@GeneratedValue`
|
||||
기본 키를 자동 생성하도록 설정합니다.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Order {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
}
|
||||
```
|
||||
- `GenerationType.IDENTITY`는 **AUTO_INCREMENT** 방식으로 기본 키를 생성합니다.
|
||||
|
||||
|
||||
### `@Column`
|
||||
필드를 특정 컬럼과 매핑할 때 사용합니다.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Employee {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "emp_name", length = 100, nullable = false)
|
||||
private String name;
|
||||
}
|
||||
```
|
||||
- `emp_name`이라는 컬럼에 매핑되고, 길이는 100이며, **NULL을 허용하지 않음**.
|
||||
|
||||
|
||||
|
||||
### `@Transient`
|
||||
해당 필드를 **DB에 저장하지 않도록** 설정합니다.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Person {
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@Transient
|
||||
private int age;
|
||||
}
|
||||
```
|
||||
- `age` 필드는 **DB에 저장되지 않음**.
|
||||
|
||||
|
||||
### `@Enumerated`
|
||||
Enum 타입을 컬럼에 저장할 때 사용합니다.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Task {
|
||||
public enum Status { PENDING, COMPLETED }
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
private Status status;
|
||||
}
|
||||
```
|
||||
- `EnumType.STRING`을 사용하여 `"PENDING"`, `"COMPLETED"` 형태로 저장됩니다.
|
||||
|
||||
|
||||
### `@ManyToOne`
|
||||
**다대일(N:1) 관계**를 정의합니다.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Order {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "user_id")
|
||||
private User user;
|
||||
}
|
||||
```
|
||||
- `Order` 엔티티는 **User와 다대일 관계**.
|
||||
|
||||
|
||||
### `@OneToMany`
|
||||
**일대다(1:N) 관계**를 정의합니다.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class User {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@OneToMany(mappedBy = "user")
|
||||
private List<Order> orders;
|
||||
}
|
||||
```
|
||||
- 한 명의 `User`가 여러 개의 `Order`를 가질 수 있음.
|
||||
|
||||
|
||||
### `@OneToOne`
|
||||
**일대일(1:1) 관계**를 정의합니다.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Passport {
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@OneToOne
|
||||
@JoinColumn(name = "user_id")
|
||||
private User user;
|
||||
}
|
||||
```
|
||||
- 한 `User`는 하나의 `Passport`를 가짐.
|
||||
|
||||
|
||||
### `@ManyToMany`
|
||||
**다대다(N:M) 관계**를 정의합니다.
|
||||
|
||||
```java
|
||||
@Entity
|
||||
public class Student {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToMany
|
||||
@JoinTable(
|
||||
name = "student_course",
|
||||
joinColumns = @JoinColumn(name = "student_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "course_id")
|
||||
)
|
||||
private List<Course> courses;
|
||||
}
|
||||
```
|
||||
- `Student`와 `Course`는 다대다 관계이며 **중간 테이블(`student_course`)** 을 가짐.
|
||||
|
||||
|
||||
### `@Query`
|
||||
JPQL을 사용하여 **사용자 정의 쿼리**를 작성합니다.
|
||||
|
||||
```java
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
@Query("SELECT u FROM User u WHERE u.name = :name")
|
||||
List<User> findByName(@Param("name") String name);
|
||||
}
|
||||
```
|
||||
- `name`이 특정 값인 `User`를 조회하는 쿼리.
|
||||
|
||||
|
||||
### `@Transactional`
|
||||
**트랜잭션 범위를 설정**합니다.
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class UserService {
|
||||
@Transactional
|
||||
public void updateUser(Long id, String name) {
|
||||
User user = userRepository.findById(id).orElseThrow();
|
||||
user.setName(name);
|
||||
}
|
||||
}
|
||||
```
|
||||
- 메서드 실행 중 예외 발생 시 **자동 롤백**.
|
||||
104
docs/32_jpa 설정.md
Normal file
104
docs/32_jpa 설정.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# JPA
|
||||
|
||||
## `application.yaml` 설정
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
# 공통 JPA 설정
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: update # 테이블 자동 생성/업데이트 설정
|
||||
show-sql: true # SQL 쿼리 로그 출력
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true # SQL 포맷팅 출력
|
||||
|
||||
# 개발 환경 (H2 데이터베이스)
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: dev
|
||||
datasource:
|
||||
url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
|
||||
driver-class-name: org.h2.Driver
|
||||
username: sa
|
||||
password:
|
||||
h2:
|
||||
console:
|
||||
enabled: true # H2 콘솔 활성화
|
||||
path: /h2-console
|
||||
|
||||
# 프로덕션 환경 (MariaDB 데이터베이스)
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: prod
|
||||
datasource:
|
||||
url: jdbc:mariadb://localhost:3306/mydb
|
||||
driver-class-name: org.mariadb.jdbc.Driver
|
||||
username: root
|
||||
password: mypassword
|
||||
jpa:
|
||||
properties:
|
||||
hibernate:
|
||||
dialect: org.hibernate.dialect.MariaDBDialect # MariaDB 전용 dialect
|
||||
```
|
||||
|
||||
### 실행 방법
|
||||
- `dev` 프로파일: `spring.profiles.active=dev`로 실행 (H2 사용)
|
||||
- `prod` 프로파일: `spring.profiles.active=prod`로 실행 (MariaDB 사용)
|
||||
|
||||
|
||||
### 설정 옵션
|
||||
|
||||
#### 1. 공통 JPA 설정 옵션
|
||||
| **옵션** | **설명** | **예시 값** |
|
||||
|-----------------------------|-------------------------------------------------------------------------------------------|-----------------------|
|
||||
| `spring.jpa.hibernate.ddl-auto` | Hibernate가 데이터베이스 스키마를 생성/업데이트하는 방식 (`create`, `update`, `validate`, `none`) | `update` |
|
||||
| `spring.jpa.show-sql` | 실행되는 SQL 쿼리를 콘솔에 출력할지 여부 | `true` |
|
||||
| `spring.jpa.properties.hibernate.format_sql` | 출력되는 SQL을 읽기 쉽게 포맷팅할지 여부 | `true` |
|
||||
|
||||
#### 2. H2 데이터베이스 설정 옵션
|
||||
| **옵션** | **설명** | **예시 값** |
|
||||
|-----------------------------|-------------------------------------------------------------------------------------------|-----------------------|
|
||||
| `spring.datasource.url` | H2 데이터베이스 연결 URL (메모리 모드: `jdbc:h2:mem:testdb`) | `jdbc:h2:mem:testdb` |
|
||||
| `spring.datasource.driver-class-name` | H2 JDBC 드라이버 클래스 | `org.h2.Driver` |
|
||||
| `spring.datasource.username`| H2 데이터베이스 사용자 이름 (기본값: `sa`) | `sa` |
|
||||
| `spring.datasource.password`| H2 데이터베이스 비밀번호 (기본값: 빈 문자열) | `` |
|
||||
| `spring.h2.console.enabled` | H2 웹 콘솔 활성화 여부 (브라우저에서 데이터 확인 가능) | `true` |
|
||||
| `spring.h2.console.path` | H2 콘솔의 접근 경로 | `/h2-console` |
|
||||
|
||||
- **참고**: `DB_CLOSE_DELAY=-1`은 메모리 DB가 애플리케이션 종료 시 닫히지 않도록 유지하며, `DB_CLOSE_ON_EXIT=FALSE`는 연결 종료 시 DB를 닫지 않도록 설정합니다.
|
||||
|
||||
#### 3. MariaDB 데이터베이스 설정 옵션
|
||||
| **옵션** | **설명** | **예시 값** |
|
||||
|-----------------------------|-------------------------------------------------------------------------------------------|-----------------------|
|
||||
| `spring.datasource.url` | MariaDB 데이터베이스 연결 URL (예: `jdbc:mariadb://호스트:포트/데이터베이스명`) | `jdbc:mariadb://localhost:3306/mydb` |
|
||||
| `spring.datasource.driver-class-name` | MariaDB JDBC 드라이버 클래스 | `org.mariadb.jdbc.Driver` |
|
||||
| `spring.datasource.username`| MariaDB 데이터베이스 사용자 이름 | `root` |
|
||||
| `spring.datasource.password`| MariaDB 데이터베이스 비밀번호 | `mypassword` |
|
||||
| `spring.jpa.properties.hibernate.dialect` | Hibernate가 사용할 MariaDB 전용 dialect (SQL 최적화) | `org.hibernate.dialect.MariaDBDialect` |
|
||||
|
||||
|
||||
### 추가 설명
|
||||
1. **H2 설정**:
|
||||
- H2는 인메모리 데이터베이스로, 테스트나 로컬 개발에 적합합니다.
|
||||
- H2 콘솔(`/h2-console`)을 통해 브라우저에서 데이터베이스를 확인할 수 있습니다.
|
||||
|
||||
2. **MariaDB 설정**:
|
||||
- MariaDB는 MySQL과 호환되는 오픈소스 RDBMS로, 프로덕션 환경에서 안정적으로 사용됩니다.
|
||||
- `dialect` 설정은 Hibernate가 MariaDB에 최적화된 SQL을 생성하도록 돕습니다.
|
||||
|
||||
3. **의존성 추가**:
|
||||
- `pom.xml`에 아래 의존성을 추가해야 합니다:
|
||||
```kotlin
|
||||
runtimeOnly("com.h2database:h2")
|
||||
runtimeOnly("org.mariadb.jdbc:mariadb-java-client")
|
||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||
```
|
||||
|
||||
4. **프로파일 사용**:
|
||||
- `application.yaml`에서 `---`로 프로파일을 구분하며, 실행 시 `-Dspring.profiles.active=dev` 또는 `prod`를 지정합니다.
|
||||
|
||||
224
docs/33_jpa repository.md
Normal file
224
docs/33_jpa repository.md
Normal file
@@ -0,0 +1,224 @@
|
||||
# JPA Repository
|
||||
|
||||
스프링 부트에서 `JpaRepository`는 `Spring Data JPA` 모듈의 핵심 인터페이스로, 데이터베이스와의 상호작용을 추상화하여 CRUD(Create, Read, Update, Delete) 작업과 쿼리 작성을 간소화합니다. `JpaRepository`는 JPA(Java Persistence API)를 기반으로 동작하며, 엔티티(Entity)와 데이터베이스 테이블 간의 매핑을 통해 객체 지향적으로 데이터를 다룰 수 있게 해줍니다. 이를 통해 개발자는 SQL 쿼리를 직접 작성하지 않고도 메서드 이름 규칙(Query Method)이나 `@Query` 어노테이션을 사용해 데이터베이스 작업을 수행할 수 있습니다.
|
||||
|
||||
## JpaRepository의 특징
|
||||
- **상속 구조**: `JpaRepository`는 `CrudRepository`와 `PagingAndSortingRepository`를 확장하며, 기본 CRUD 메서드와 페이징/정렬 기능을 모두 제공합니다.
|
||||
- **자동 구현**: 인터페이스를 정의하기만 하면 스프링이 런타임에 구현체를 생성합니다.
|
||||
- **쿼리 생성**: 메서드 이름 규칙을 통해 자동으로 쿼리를 생성하거나, `@Query`로 커스텀 쿼리 정의 가능.
|
||||
- **트랜잭션 관리**: 기본적으로 읽기 메서드는 읽기 전용 트랜잭션, 쓰기 메서드는 트랜잭션 내에서 실행됩니다.
|
||||
|
||||
## JpaRepository 기본 사용법
|
||||
|
||||
### JpaRepository 인터페이스 생성
|
||||
JpaRepository를 사용하려면, **Repository 인터페이스를 생성하고 JpaRepository를 상속**하면 됩니다.
|
||||
|
||||
```java
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
}
|
||||
```
|
||||
✔ `JpaRepository<User, Long>` → **User 엔티티를 관리하며, 기본 키(ID)의 타입은 Long**
|
||||
✔ `save()`, `findById()`, `deleteById()` 등의 기본 메서드를 자동으로 제공
|
||||
|
||||
|
||||
## JpaRepository의 기본 제공 메서드
|
||||
|
||||
JpaRepository를 상속하면 기본적으로 제공되는 메서드는 다음과 같습니다.
|
||||
|
||||
| 메서드 | 설명 |
|
||||
|--------|------|
|
||||
| `save(T entity)` | 엔티티를 저장 또는 업데이트 |
|
||||
| `findById(ID id)` | ID로 엔티티 조회 (Optional 반환) |
|
||||
| `findAll()` | 모든 엔티티 조회 |
|
||||
| `findAll(Sort sort)` | 정렬 조건을 적용하여 모든 엔티티 조회 |
|
||||
| `deleteById(ID id)` | ID로 엔티티 삭제 |
|
||||
| `delete(T entity)` | 엔티티 삭제 |
|
||||
| `count()` | 엔티티 개수 조회 |
|
||||
| `existsById(ID id)` | 특정 ID의 엔티티 존재 여부 확인 |
|
||||
|
||||
* `save()`는 **ID가 있으면 업데이트, 없으면 삽입**
|
||||
* `findById()`는 **Optional<T> 반환 → `orElseThrow()`와 함께 사용 가능**
|
||||
|
||||
|
||||
## 메서드 이름을 이용한 자동 쿼리 생성
|
||||
|
||||
`Spring Data JPA`는 메서드 이름을 분석해 쿼리를 자동 생성합니다. 아래 표는 주요 키워드와 그 설명을 정리한 것입니다. 메서드 이름은 일반적으로 `find`, `exists`, `count`, `delete` 등의 동사로 시작하며, 조건과 옵션을 조합합니다.
|
||||
|
||||
### 메서드 이름 규칙
|
||||
|
||||
| **키워드** | **설명** |
|
||||
|----------------------|----------------|
|
||||
| `find` | 조건에 맞는 엔티티를 조회 (단일 또는 리스트 반환) `findByName(String name)` |
|
||||
| `findDistinct` | 중복 제거 후 조회 `findDistinctByName(String name)` |
|
||||
| `exists` | 조건에 맞는 엔티티가 존재하는지 확인 (boolean 반환) `existsByName(String name)` |
|
||||
| `count` | 조건에 맞는 엔티티 수를 반환 `countByAgeGreaterThan(int age)` |
|
||||
| `delete` | 조건에 맞는 엔티티 삭제 `deleteByName(String name)` |
|
||||
| `By` | 조건을 시작하는 구분자 `findByName(String name)` |
|
||||
| `And` | 여러 조건을 AND로 결합 `findByNameAndAge(String name, int age)`|
|
||||
| `Or` | 여러 조건을 OR로 결합 `findByNameOrAge(String name, int age)` |
|
||||
| `Is`, `Equals` | 특정 필드가 값과 동일한 경우 `findByNameIs(String name)` |
|
||||
| `Not` | 특정 필드가 값과 다른 경우 `findByNameNot(String name)` |
|
||||
| `LessThan` | 특정 필드가 값보다 작은 경우 `findByAgeLessThan(int age)` |
|
||||
| `LessThanEqual` | 특정 필드가 값보다 작거나 같은 경우 `findByAgeLessThanEqual(int age)` |
|
||||
| `GreaterThan` | 특정 필드가 값보다 큰 경우 `findByAgeGreaterThan(int age)` |
|
||||
| `GreaterThanEqual` | 특정 필드가 값보다 크거나 같은 경우 `findByAgeGreaterThanEqual(int age)` |
|
||||
| `Between` | 특정 필드가 두 값 사이에 있는 경우 `findByAgeBetween(int start, int end)` |
|
||||
| `Like` | 특정 필드가 패턴과 일치하는 경우 (SQL의 LIKE) `findByNameLike(String pattern)` |
|
||||
| `NotLike` | 특정 필드가 패턴과 일치하지 않는 경우 `findByNameNotLike(String pattern)` |
|
||||
| `StartingWith` | 특정 필드가 값으로 시작하는 경우 `findByNameStartingWith(String prefix)` |
|
||||
| `EndingWith` | 특정 필드가 값으로 끝나는 경우 `findByNameEndingWith(String suffix)` |
|
||||
| `Containing` | 특정 필드가 값을 포함하는 경우 `findByNameContaining(String part)` |
|
||||
| `In` | 특정 필드가 값 목록에 포함된 경우 `findByAgeIn(List<Integer> ages)` |
|
||||
| `NotIn` | 특정 필드가 값 목록에 포함되지 않은 경우 `findByAgeNotIn(List<Integer> ages)` |
|
||||
| `True` | 특정 boolean 필드가 `true`인 경우 `findByActiveTrue()` |
|
||||
| `False` | 특정 boolean 필드가 `false`인 경우 `findByActiveFalse()` |
|
||||
| `OrderBy` | 결과를 특정 필드로 정렬 `findByNameOrderByAgeAsc(String name)` |
|
||||
| `Asc` | 오름차순 정렬 `findByNameOrderByAgeAsc(String name)` |
|
||||
| `Desc` | 내림차순 정렬 `findByNameOrderByAgeDesc(String name)` |
|
||||
| `First`, `Top` | 결과 중 처음 N개만 반환 (숫자 생략 시 1개) `findFirstByName(String name)` |
|
||||
| `FirstN`, `TopN` | 결과 중 처음 N개 반환 `findTop10ByName(String name)` |
|
||||
| `Page` | 페이징된 결과 반환 (`Page<T>` 타입) `findByName(String name, Pageable pageable)` |
|
||||
| `Slice` | 페이징된 결과의 일부 반환 (`Slice<T>` 타입, 전체 카운트 제외) `findByName(String name, Pageable pageable)` |
|
||||
|
||||
|
||||
#### 단일 필드로 조회
|
||||
```java
|
||||
List<User> findByUsername(String username);
|
||||
```
|
||||
✔ `SELECT * FROM user WHERE username = ?`
|
||||
|
||||
|
||||
#### 여러 조건으로 조회 (AND 조건)
|
||||
```java
|
||||
List<User> findByUsernameAndEmail(String username, String email);
|
||||
```
|
||||
✔ `SELECT * FROM user WHERE username = ? AND email = ?`
|
||||
|
||||
|
||||
#### 여러 조건으로 조회 (OR 조건)
|
||||
```java
|
||||
List<User> findByUsernameOrEmail(String username, String email);
|
||||
```
|
||||
✔ `SELECT * FROM user WHERE username = ? OR email = ?`
|
||||
|
||||
|
||||
#### 특정 값보다 큰 데이터 조회
|
||||
```java
|
||||
List<User> findByAgeGreaterThan(int age);
|
||||
```
|
||||
✔ `SELECT * FROM user WHERE age > ?`
|
||||
|
||||
|
||||
#### 특정 값 포함 여부 조회
|
||||
```java
|
||||
List<User> findByUsernameLike(String keyword);
|
||||
```
|
||||
✔ `SELECT * FROM user WHERE username LIKE ?`
|
||||
|
||||
|
||||
#### 정렬 추가 (`OrderBy`)
|
||||
```java
|
||||
List<User> findByUsernameOrderByAgeDesc(String username);
|
||||
```
|
||||
✔ `SELECT * FROM user WHERE username = ? ORDER BY age DESC`
|
||||
|
||||
|
||||
## 페이징 및 정렬 처리
|
||||
|
||||
Spring Data JPA는 **페이징(Pageable)과 정렬(Sort)** 기능을 기본으로 제공합니다.
|
||||
|
||||
- **페이징 주요 용어**:
|
||||
- **Page**: 현재 페이지 번호 (0부터 시작).
|
||||
- **Size**: 한 페이지에 포함될 데이터 개수.
|
||||
- **Total Elements**: 전체 데이터 개수.
|
||||
- **Total Pages**: 전체 페이지 수.
|
||||
|
||||
- **장점**:
|
||||
- 데이터베이스 부하 감소.
|
||||
- 사용자 경험 개선 (한 번에 모든 데이터를 로드하지 않음).
|
||||
|
||||
- **정렬 주요 용어**:
|
||||
- **Sort**: 정렬 기준과 방향을 정의.
|
||||
- **Direction**: `ASC` (오름차순), `DESC` (내림차순).
|
||||
|
||||
- **`Pageable`**:
|
||||
- 페이징과 정렬 정보를 담는 인터페이스.
|
||||
- `PageRequest.of(page, size)`: 기본 페이징 설정.
|
||||
- `PageRequest.of(page, size, sort)`: 정렬 포함 페이징 설정.
|
||||
|
||||
- **`Sort`**:
|
||||
- 정렬 조건을 정의.
|
||||
- `Sort.by(Direction, "fieldName")`: 단일 필드 정렬.
|
||||
- `Sort.by(Sort.Order.asc("field1"), Sort.Order.desc("field2"))`: 다중 필드 정렬.
|
||||
|
||||
- **`Page<T>`**:
|
||||
- 페이징된 결과를 반환하며, 전체 페이지 수, 총 요소 수 등 메타데이터 포함.
|
||||
- 주요 메서드: `getContent()`, `getTotalElements()`, `getTotalPages()`.
|
||||
|
||||
- **`Slice<T>`**:
|
||||
- 전체 카운트를 포함하지 않은 페이징 결과 (다음 페이지 존재 여부만 확인 가능).
|
||||
- 주로 무한 스크롤에 사용.
|
||||
|
||||
```java
|
||||
Page<User> findByName(String name, Pageable pageable);
|
||||
```
|
||||
```java
|
||||
Pageable pageable = PageRequest.of(0, 10, Sort.by("age").ascending());
|
||||
Page<User> users = userRepository.findByName("John", pageable);
|
||||
```
|
||||
|
||||
### 페이징을 위한 `Pageable` 인터페이스 사용
|
||||
페이징을 적용하려면 `Pageable` 객체를 매개변수로 전달하면 됩니다.
|
||||
|
||||
```java
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
Page<User> findByAgeGreaterThan(int age, Pageable pageable);
|
||||
}
|
||||
```
|
||||
|
||||
✔ `Page<T>`를 반환하여 총 페이지 개수, 현재 페이지 정보 등을 확인 가능
|
||||
|
||||
|
||||
### 페이징 및 정렬을 컨트롤러에서 사용하기
|
||||
컨트롤러에서 `PageRequest.of(페이지번호, 개수, 정렬기준)`을 사용하여 페이징을 요청할 수 있습니다.
|
||||
|
||||
```java
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/users")
|
||||
public class UserController {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public UserController(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public Page<User> getUsers(@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size) {
|
||||
Pageable pageable = PageRequest.of(page, size, Sort.by("age").descending());
|
||||
return userRepository.findAll(pageable);
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `PageRequest.of(page, size, Sort.by("age").descending())` → **나이 기준 내림차순 정렬**
|
||||
✔ 클라이언트에서 `GET /users?page=0&size=10` 요청 시 첫 번째 페이지의 10개 데이터 반환
|
||||
|
||||
## 커스텀 쿼리
|
||||
메서드 이름으로 표현하기 어려운 쿼리는 `@Query` 사용:
|
||||
```java
|
||||
@Query("SELECT u FROM User u WHERE u.name = ?1 AND u.age > ?2")
|
||||
List<User> findCustomUsers(String name, int age);
|
||||
```
|
||||
|
||||
525
docs/34_jdbc.md
Normal file
525
docs/34_jdbc.md
Normal file
@@ -0,0 +1,525 @@
|
||||
# 스프링 부트의 JDBC
|
||||
|
||||
스프링 부트에서 JDBC(Java Database Connectivity)는 데이터베이스와의 연결을 관리하고 SQL을 실행하기 위한 기본적인 방법입니다. 스프링 부트는 `Spring JDBC` 모듈을 통해 JDBC를 추상화하여, 반복적인 보일러플레이트 코드를 줄이고 생산성을 높입니다. 특히, `JdbcTemplate` 클래스를 중심으로 간편하게 데이터베이스 작업을 수행할 수 있으며, 스프링 부트의 자동 설정 기능을 활용해 데이터소스(DataSource) 설정도 최소화할 수 있습니다.
|
||||
|
||||
JDBC는 자바 애플리케이션이 관계형 데이터베이스(RDBMS)에 접근하도록 설계된 표준 API입니다. 스프링 부트는 이를 기반으로:
|
||||
- **연결 관리**: 데이터베이스 연결 풀을 자동 설정.
|
||||
- **쿼리 실행**: SQL 실행과 결과 매핑을 단순화.
|
||||
- **예외 처리**: JDBC 예외를 스프링의 `DataAccessException`으로 변환.
|
||||
|
||||
## 의존성 추가
|
||||
스프링 부트에서 JDBC를 사용하려면 `spring-boot-starter-jdbc` 의존성을 추가합니다. 데이터베이스 드라이버도 필요합니다 (예: H2, MySQL).
|
||||
|
||||
```kotlin
|
||||
implementation("org.springframework.boot:spring-boot-starter-jdbc<")
|
||||
runtimeOnly("com.h2database:h2")
|
||||
```
|
||||
|
||||
|
||||
## `application.yaml` 설정
|
||||
데이터소스(DataSource)를 설정합니다. 스프링 부트는 기본적으로 HikariCP라는 고성능 연결 풀을 사용합니다.
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
datasource:
|
||||
url: jdbc:h2:mem:testdb # H2 메모리 DB URL
|
||||
driver-class-name: org.h2.Driver
|
||||
username: sa
|
||||
password:
|
||||
h2:
|
||||
console:
|
||||
enabled: true # H2 콘솔 활성화
|
||||
```
|
||||
|
||||
|
||||
## JdbcTemplate 사용
|
||||
`JdbcTemplate`은 스프링 JDBC의 핵심 클래스이며, SQL 실행과 결과를 처리하는 메서드를 제공합니다.
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public UserService(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
// 사용자 추가
|
||||
public void addUser(String name, int age) {
|
||||
String sql = "INSERT INTO users (name, age) VALUES (?, ?)";
|
||||
jdbcTemplate.update(sql, name, age);
|
||||
}
|
||||
|
||||
// 사용자 조회
|
||||
public List<User> getAllUsers() {
|
||||
String sql = "SELECT id, name, age FROM users";
|
||||
return jdbcTemplate.query(sql, (rs, rowNum) -> new User(
|
||||
rs.getLong("id"),
|
||||
rs.getString("name"),
|
||||
rs.getInt("age")
|
||||
));
|
||||
}
|
||||
|
||||
// ID로 사용자 조회
|
||||
public User getUserById(Long id) {
|
||||
String sql = "SELECT id, name, age FROM users WHERE id = ?";
|
||||
return jdbcTemplate.queryForObject(sql, new Object[]{id}, (rs, rowNum) -> new User(
|
||||
rs.getLong("id"),
|
||||
rs.getString("name"),
|
||||
rs.getInt("age")
|
||||
));
|
||||
}
|
||||
|
||||
// 사용자 삭제
|
||||
public void deleteUser(Long id) {
|
||||
String sql = "DELETE FROM users WHERE id = ?";
|
||||
jdbcTemplate.update(sql, id);
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class User {
|
||||
private Long id;
|
||||
private String name;
|
||||
private int age;
|
||||
}
|
||||
```
|
||||
|
||||
- **설명**:
|
||||
- `update()`: INSERT, UPDATE, DELETE와 같은 쓰기 작업.
|
||||
- `query()`: 여러 행을 조회하고 결과를 객체로 매핑.
|
||||
- `queryForObject()`: 단일 행을 조회.
|
||||
|
||||
|
||||
## 주요 JdbcTemplate 메서드
|
||||
|
||||
|
||||
| **메서드** | **설명** | **반환 타입** | **사용 예시** |
|
||||
|------------------------------------------------|-------------------------------------------------------------------------------------------|----------------------|-----------------------------------------------|
|
||||
| `execute(String sql)` | DDL(예: 테이블 생성)이나 단순 SQL 실행, 결과를 반환하지 않음 | `void` | `jdbcTemplate.execute("CREATE TABLE users(id INT)")` |
|
||||
| `update(String sql, Object... args)` | INSERT, UPDATE, DELETE와 같은 쓰기 작업 실행, 영향을 받은 행 수 반환 | `int` | `jdbcTemplate.update("INSERT INTO users VALUES (?, ?)", name, age)` |
|
||||
| `update(String sql, PreparedStatementSetter pss)` | PreparedStatement를 커스터마이징하여 쓰기 작업 실행 | `int` | `jdbcTemplate.update("INSERT INTO users VALUES (?, ?)", ps -> { ps.setString(1, name); ps.setInt(2, age); })` |
|
||||
| `queryForObject(String sql, Class<T> requiredType, Object... args)` | 단일 값(예: `Integer`, `String`) 조회 | `T` | `jdbcTemplate.queryForObject("SELECT count(*) FROM users", Integer.class)` |
|
||||
| `queryForObject(String sql, RowMapper<T> rowMapper, Object... args)` | 단일 행을 객체로 매핑하여 조회 | `T` | `jdbcTemplate.queryForObject("SELECT * FROM users WHERE id = ?", (rs, rowNum) -> new User(rs.getLong("id"), rs.getString("name"), rs.getInt("age")), id)` |
|
||||
| `query(String sql, RowMapper<T> rowMapper, Object... args)` | 여러 행을 객체 리스트로 매핑하여 조회 | `List<T>` | `jdbcTemplate.query("SELECT * FROM users", (rs, rowNum) -> new User(rs.getLong("id"), rs.getString("name"), rs.getInt("age")))` |
|
||||
| `queryForList(String sql, Class<T> elementType, Object... args)` | 여러 행을 단일 값 리스트로 조회 (예: `List<String>`) | `List<T>` | `jdbcTemplate.queryForList("SELECT name FROM users", String.class)` |
|
||||
| `queryForList(String sql, Object... args)` | 여러 행을 `Map` 리스트로 조회 (컬럼명과 값 쌍) | `List<Map<String, Object>>` | `jdbcTemplate.queryForList("SELECT * FROM users")` |
|
||||
| `queryForMap(String sql, Object... args)` | 단일 행을 `Map`으로 조회 (컬럼명과 값 쌍) | `Map<String, Object>` | `jdbcTemplate.queryForMap("SELECT * FROM users WHERE id = ?", id)` |
|
||||
| `query(String sql, ResultSetExtractor<T> rse, Object... args)` | ResultSet을 커스터마이징하여 결과 처리 | `T` | `jdbcTemplate.query("SELECT * FROM users", rs -> { /* 커스텀 처리 */ return result; })` |
|
||||
| `batchUpdate(String sql, List<Object[]> batchArgs)` | 여러 행을 일괄적으로 삽입/업데이트/삭제, 각 행마다 영향을 받은 행 수 배열 반환 | `int[]` | `jdbcTemplate.batchUpdate("INSERT INTO users VALUES (?, ?)", batchArgs)` |
|
||||
| `call(CallableStatementCreator csc, List<SqlParameter> declaredParameters)` | 저장 프로시저 호출 | `Map<String, Object>` | `jdbcTemplate.call(csc, params)` (복잡한 예시는 생략) |
|
||||
|
||||
1. **`execute`**
|
||||
- DDL(테이블 생성/삭제)이나 결과가 필요 없는 작업에 사용.
|
||||
- 예: 데이터베이스 초기화.
|
||||
|
||||
2. **`update`**
|
||||
- 데이터 쓰기 작업에 적합하며, `?` 플레이스홀더를 사용해 안전하게 파라미터 바인딩.
|
||||
- 반환값: 영향을 받은 행 수 (예: 삽입된 행 수).
|
||||
|
||||
3. **`queryForObject`**
|
||||
- 단일 값(스칼라)이나 단일 행 조회에 사용.
|
||||
- 결과가 없으면 `EmptyResultDataAccessException` 발생.
|
||||
|
||||
4. **`query`**
|
||||
- 여러 행 조회 시 `RowMapper`로 각 행을 객체로 변환.
|
||||
- 예: `User` 객체 리스트 반환.
|
||||
|
||||
5. **`queryForList`**
|
||||
- 간단한 리스트 반환에 유용 (컬럼 하나만 조회하거나 `Map`으로 결과 필요 시).
|
||||
- 예: 사용자 이름 리스트.
|
||||
|
||||
6. **`queryForMap`**
|
||||
- 단일 행의 모든 컬럼을 키-값 쌍으로 반환.
|
||||
- 결과가 없으면 예외 발생.
|
||||
|
||||
7. **`batchUpdate`**
|
||||
- 대량 데이터 삽입/업데이트 시 성능 최적화.
|
||||
- 예: `List<Object[]>`로 여러 행을 한 번에 처리.
|
||||
|
||||
8. **`call`**
|
||||
- 저장 프로시저 호출에 사용되며, 복잡한 경우에만 필요.
|
||||
|
||||
|
||||
## 데이터베이스 초기화
|
||||
스프링 부트는 `schema.sql`과 `data.sql` 파일을 통해 데이터베이스를 초기화할 수 있습니다.
|
||||
|
||||
- **`src/main/resources/schema.sql`**:
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255),
|
||||
age INT
|
||||
);
|
||||
```
|
||||
|
||||
- **`src/main/resources/data.sql`**:
|
||||
```sql
|
||||
INSERT INTO users (name, age) VALUES ('John', 25);
|
||||
INSERT INTO users (name, age) VALUES ('Jane', 30);
|
||||
```
|
||||
|
||||
- **설정**: `spring.sql.init.mode=always`로 활성화 (`application.yaml`).
|
||||
|
||||
|
||||
## 트랜잭션 관리
|
||||
JDBC 작업에서 트랜잭션을 적용하려면 `@Transactional` 어노테이션을 사용합니다.
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public UserService(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void addUserWithTransaction(String name, int age) {
|
||||
String sql = "INSERT INTO users (name, age) VALUES (?, ?)";
|
||||
jdbcTemplate.update(sql, name, age);
|
||||
// 예외 발생 시 롤백
|
||||
if (age < 0) {
|
||||
throw new IllegalArgumentException("Age cannot be negative");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **설명**: 트랜잭션 내에서 실행되며, 예외 발생 시 롤백됩니다.
|
||||
|
||||
|
||||
## 커스텀 데이터소스 설정 (옵션)
|
||||
기본 설정 대신 수동으로 `DataSource`를 정의할 수 있습니다.
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
public class DataSourceConfig {
|
||||
|
||||
@Bean
|
||||
public DataSource dataSource() {
|
||||
HikariDataSource dataSource = new HikariDataSource();
|
||||
dataSource.setJdbcUrl("jdbc:h2:mem:testdb");
|
||||
dataSource.setDriverClassName("org.h2.Driver");
|
||||
dataSource.setUsername("sa");
|
||||
dataSource.setPassword("");
|
||||
return dataSource;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
|
||||
return new JdbcTemplate(dataSource);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 사용 예시
|
||||
```java
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public UserService(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
// 단일 사용자 조회
|
||||
public User findUserById(Long id) {
|
||||
String sql = "SELECT id, name, age FROM users WHERE id = ?";
|
||||
return jdbcTemplate.queryForObject(sql, (rs, rowNum) -> new User(
|
||||
rs.getLong("id"),
|
||||
rs.getString("name"),
|
||||
rs.getInt("age")
|
||||
), id);
|
||||
}
|
||||
|
||||
// 모든 사용자 조회
|
||||
public List<User> findAllUsers() {
|
||||
String sql = "SELECT id, name, age FROM users";
|
||||
return jdbcTemplate.query(sql, (rs, rowNum) -> new User(
|
||||
rs.getLong("id"),
|
||||
rs.getString("name"),
|
||||
rs.getInt("age")
|
||||
));
|
||||
}
|
||||
|
||||
// 사용자 추가
|
||||
public void addUser(String name, int age) {
|
||||
String sql = "INSERT INTO users (name, age) VALUES (?, ?)";
|
||||
jdbcTemplate.update(sql, name, age);
|
||||
}
|
||||
|
||||
// 배치 삽입
|
||||
public void batchAddUsers(List<User> users) {
|
||||
String sql = "INSERT INTO users (name, age) VALUES (?, ?)";
|
||||
List<Object[]> batchArgs = users.stream()
|
||||
.map(user -> new Object[]{user.getName(), user.getAge()})
|
||||
.collect(Collectors.toList());
|
||||
jdbcTemplate.batchUpdate(sql, batchArgs);
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public class User {
|
||||
private Long id;
|
||||
private String name;
|
||||
private int age;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 스프링 부트에서 JPA와 JDBC(JdbcTemplate)의 비교
|
||||
|
||||
| **특징** | **JPA** | **JdbcTemplate** |
|
||||
|---------------------|---------------------------------------------|--------------------------------------------|
|
||||
| **접근 방식** | 객체 지향적 (ORM 기반) | SQL 중심 (직접 쿼리 작성) |
|
||||
| **추상화 수준** | 높음 (엔티티와 매핑 관리) | 낮음 (SQL과 JDBC API 추상화) |
|
||||
| **주요 클래스** | `EntityManager`, `JpaRepository` | `JdbcTemplate` |
|
||||
| **의존성** | `spring-boot-starter-data-jpa` | `spring-boot-starter-jdbc` |
|
||||
| **데이터베이스 제어**| 엔티티와 매핑으로 간접 제어 | SQL로 직접 제어 |
|
||||
|
||||
- **JPA**: 객체와 데이터베이스 테이블 간의 매핑을 통해 SQL을 자동 생성하고 관리.
|
||||
- **JdbcTemplate**: 개발자가 직접 SQL을 작성하며, JdbcTemplate이 JDBC 작업을 단순화.
|
||||
|
||||
|
||||
------
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# **Spring JDBC Template의 주요 기능 설명**
|
||||
|
||||
Spring의 `JdbcTemplate`은 SQL을 더욱 간결하고 효율적으로 실행할 수 있도록 돕는 핵심적인 데이터 액세스 기술입니다.
|
||||
이 글에서는 **SQL 파라미터 바인딩(`SqlParameterSource`)**, **행 매핑(`RowMapper`)**, **간단한 JDBC 처리(`SimpleJdbc` 클래스들)**에 대해 설명하겠습니다.
|
||||
|
||||
---
|
||||
|
||||
## **1. SqlParameterSource - SQL 파라미터 바인딩**
|
||||
|
||||
일반적으로 SQL을 실행할 때 **파라미터를 바인딩**해야 합니다.
|
||||
Spring은 이를 쉽게 처리할 수 있도록 **`SqlParameterSource` 인터페이스**를 제공합니다.
|
||||
|
||||
### **✅ 주요 구현체**
|
||||
| 구현체 | 설명 |
|
||||
|--------|------|
|
||||
| `MapSqlParameterSource` | **키-값(Map) 형태**로 파라미터 바인딩 |
|
||||
| `BeanPropertySqlParameterSource` | **Java 객체(Bean)의 필드 값**을 자동으로 바인딩 |
|
||||
| `NamedParameterJdbcTemplate` | **이름 기반**(`:paramName`)으로 SQL 파라미터 바인딩 가능 |
|
||||
|
||||
---
|
||||
|
||||
### **📌 `MapSqlParameterSource` 예제**
|
||||
키-값 형태의 데이터를 SQL에 바인딩하는 방법입니다.
|
||||
|
||||
```java
|
||||
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Repository
|
||||
public class UserRepository {
|
||||
|
||||
private final NamedParameterJdbcTemplate jdbcTemplate;
|
||||
|
||||
public UserRepository(NamedParameterJdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
public void updateUserEmail(Long userId, String newEmail) {
|
||||
String sql = "UPDATE users SET email = :email WHERE id = :id";
|
||||
|
||||
MapSqlParameterSource params = new MapSqlParameterSource()
|
||||
.addValue("email", newEmail)
|
||||
.addValue("id", userId);
|
||||
|
||||
jdbcTemplate.update(sql, params);
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ **`:email`, `:id`** → **이름 기반 파라미터 바인딩**
|
||||
✔ **`MapSqlParameterSource`** → **SQL 실행 시 필요한 파라미터 전달**
|
||||
|
||||
---
|
||||
|
||||
### **📌 `BeanPropertySqlParameterSource` 예제**
|
||||
Java 객체의 필드 값을 자동으로 SQL에 바인딩하는 방법입니다.
|
||||
|
||||
```java
|
||||
import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource;
|
||||
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public class UserRepository {
|
||||
|
||||
private final NamedParameterJdbcTemplate jdbcTemplate;
|
||||
|
||||
public UserRepository(NamedParameterJdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
public void insertUser(User user) {
|
||||
String sql = "INSERT INTO users (name, email) VALUES (:name, :email)";
|
||||
|
||||
BeanPropertySqlParameterSource params = new BeanPropertySqlParameterSource(user);
|
||||
|
||||
jdbcTemplate.update(sql, params);
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ **User 객체의 필드(`name`, `email`)**가 자동으로 SQL의 `:name`, `:email`에 매핑됨
|
||||
|
||||
---
|
||||
|
||||
## **2. RowMapper - SQL 결과를 객체로 변환**
|
||||
|
||||
데이터베이스에서 조회한 결과(ResultSet)를 **Java 객체로 변환**하는 역할을 합니다.
|
||||
Spring의 `RowMapper<T>` 인터페이스를 사용하면 **JDBC 결과를 쉽게 Java 객체로 매핑**할 수 있습니다.
|
||||
|
||||
---
|
||||
|
||||
### **📌 `RowMapper` 예제**
|
||||
```java
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
|
||||
import java.sql.ResultSet;
|
||||
import java.sql.SQLException;
|
||||
|
||||
public class UserRowMapper implements RowMapper<User> {
|
||||
@Override
|
||||
public User mapRow(ResultSet rs, int rowNum) throws SQLException {
|
||||
User user = new User();
|
||||
user.setId(rs.getLong("id"));
|
||||
user.setName(rs.getString("name"));
|
||||
user.setEmail(rs.getString("email"));
|
||||
return user;
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `mapRow()` → **`ResultSet`에서 데이터를 읽어 `User` 객체로 변환**
|
||||
|
||||
---
|
||||
|
||||
### **📌 `RowMapper`를 활용한 Repository 예제**
|
||||
```java
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public class UserRepository {
|
||||
|
||||
private final JdbcTemplate jdbcTemplate;
|
||||
|
||||
public UserRepository(JdbcTemplate jdbcTemplate) {
|
||||
this.jdbcTemplate = jdbcTemplate;
|
||||
}
|
||||
|
||||
public List<User> findAllUsers() {
|
||||
String sql = "SELECT * FROM users";
|
||||
return jdbcTemplate.query(sql, new UserRowMapper());
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `jdbcTemplate.query(sql, new UserRowMapper())` → **쿼리 결과를 `UserRowMapper`를 이용해 변환**
|
||||
✔ **결과:** `List<User>` 형태로 조회 가능
|
||||
|
||||
---
|
||||
|
||||
## **3. SimpleJdbc 클래스 - 간단한 JDBC 처리**
|
||||
|
||||
Spring에서는 **JDBC 작업을 단순화**하기 위해 `SimpleJdbc` 관련 클래스를 제공합니다.
|
||||
이들은 `JdbcTemplate`보다 **더 직관적이고 간단한 코드로 데이터베이스 작업을 처리**할 수 있습니다.
|
||||
|
||||
### **✅ 주요 클래스**
|
||||
| 클래스 | 설명 |
|
||||
|--------|------|
|
||||
| `SimpleJdbcInsert` | **INSERT 작업을 간단하게 처리** |
|
||||
| `SimpleJdbcCall` | **스토어드 프로시저(Stored Procedure) 호출을 간단하게 처리** |
|
||||
|
||||
---
|
||||
|
||||
### **📌 `SimpleJdbcInsert`를 이용한 INSERT 예제**
|
||||
```java
|
||||
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Repository
|
||||
public class UserRepository {
|
||||
|
||||
private final SimpleJdbcInsert simpleJdbcInsert;
|
||||
|
||||
public UserRepository(DataSource dataSource) {
|
||||
this.simpleJdbcInsert = new SimpleJdbcInsert(dataSource)
|
||||
.withTableName("users")
|
||||
.usingGeneratedKeyColumns("id");
|
||||
}
|
||||
|
||||
public Long insertUser(String name, String email) {
|
||||
Map<String, Object> params = new HashMap<>();
|
||||
params.put("name", name);
|
||||
params.put("email", email);
|
||||
|
||||
return simpleJdbcInsert.executeAndReturnKey(params).longValue();
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `withTableName("users")` → **테이블명 지정**
|
||||
✔ `usingGeneratedKeyColumns("id")` → **자동 생성된 키(`id`)를 반환**
|
||||
|
||||
---
|
||||
|
||||
### **📌 `SimpleJdbcCall`을 이용한 스토어드 프로시저 호출**
|
||||
```java
|
||||
import org.springframework.jdbc.core.simple.SimpleJdbcCall;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.util.Map;
|
||||
|
||||
@Repository
|
||||
public class UserRepository {
|
||||
|
||||
private final SimpleJdbcCall simpleJdbcCall;
|
||||
|
||||
public UserRepository(DataSource dataSource) {
|
||||
this.simpleJdbcCall = new SimpleJdbcCall(dataSource)
|
||||
.withProcedureName("get_user_by_id");
|
||||
}
|
||||
|
||||
public Map<String, Object> getUserById(Long userId) {
|
||||
return simpleJdbcCall.execute(Map.of("user_id", userId));
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `withProcedureName("get_user_by_id")` → **스토어드 프로시저 지정**
|
||||
✔ `execute(Map.of("user_id", userId))` → **프로시저 실행 후 결과 반환**
|
||||
|
||||
---
|
||||
|
||||
## **4. 정리**
|
||||
### **✅ 주요 개념 정리**
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| `SqlParameterSource` | SQL에 안전하게 **파라미터 바인딩** |
|
||||
| `RowMapper<T>` | `ResultSet`을 **Java 객체로 변환** |
|
||||
| `SimpleJdbcInsert` | **INSERT 문을 간단하게 처리** |
|
||||
| `SimpleJdbcCall` | **스토어드 프로시저 호출을 간단하게 처리** |
|
||||
|
||||
**JdbcTemplate을 활용하면 SQL을 더욱 직관적이고 안전하게 실행할 수 있습니다! 🚀**
|
||||
337
docs/35_jdbc_client.md
Normal file
337
docs/35_jdbc_client.md
Normal file
@@ -0,0 +1,337 @@
|
||||
# **Spring JDBC Client 소개 및 활용**
|
||||
|
||||
Spring 6부터 도입된 **`JdbcClient`**는 기존 `JdbcTemplate`보다 **더 간결하고 현대적인 API**를 제공합니다.
|
||||
특히, **메서드 체이닝과 함수형 스타일의 코드 작성이 가능**하여, 기존 JDBC 코드보다 훨씬 직관적인 사용이 가능합니다.
|
||||
|
||||
이 글에서는 `JdbcClient`의 주요 기능과 사용법을 **예제와 함께 설명**하겠습니다.
|
||||
|
||||
---
|
||||
|
||||
## **1. JdbcClient란?**
|
||||
|
||||
`JdbcClient`는 **Spring 6 및 Spring Boot 3에서 새롭게 추가된 기능**으로,
|
||||
기존 `JdbcTemplate`이 가진 **장점은 유지하면서도, 코드의 가독성과 유지보수성을 개선한 API**입니다.
|
||||
|
||||
✔ 기존 `JdbcTemplate`은 **보일러플레이트 코드가 많음**
|
||||
✔ `JdbcClient`는 **메서드 체이닝을 지원하여 코드가 간결해짐**
|
||||
✔ **SQL 실행 및 데이터 조회를 직관적으로 처리 가능**
|
||||
|
||||
---
|
||||
|
||||
## **2. JdbcClient 사용법**
|
||||
### **📌 2.1 의존성 추가**
|
||||
Spring Boot 3 이상에서는 **자동으로 포함**되므로 별도 의존성 추가는 필요 없습니다.
|
||||
하지만, Spring Framework 6에서 단독으로 사용할 경우 다음과 같은 의존성이 필요합니다.
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-jdbc</artifactId>
|
||||
<version>6.x.x</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **📌 2.2 `JdbcClient` 빈 설정**
|
||||
`JdbcClient`는 **DataSource를 기반으로 생성**됩니다.
|
||||
|
||||
```java
|
||||
import org.springframework.jdbc.core.simple.JdbcClient;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
@Configuration
|
||||
public class DatabaseConfig {
|
||||
|
||||
@Bean
|
||||
public JdbcClient jdbcClient(DataSource dataSource) {
|
||||
return JdbcClient.create(dataSource);
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ **`JdbcClient.create(dataSource)`** 를 사용하여 인스턴스 생성
|
||||
✔ **Spring Boot 환경에서는 자동 설정되므로 따로 등록할 필요 없음**
|
||||
|
||||
---
|
||||
|
||||
## **3. CRUD 예제**
|
||||
|
||||
### **📌 3.1 데이터 삽입 (INSERT)**
|
||||
```java
|
||||
import org.springframework.jdbc.core.simple.JdbcClient;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public class UserRepository {
|
||||
|
||||
private final JdbcClient jdbcClient;
|
||||
|
||||
public UserRepository(JdbcClient jdbcClient) {
|
||||
this.jdbcClient = jdbcClient;
|
||||
}
|
||||
|
||||
public int insertUser(String name, String email) {
|
||||
return jdbcClient.sql("INSERT INTO users (name, email) VALUES (:name, :email)")
|
||||
.param("name", name)
|
||||
.param("email", email)
|
||||
.update();
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `sql("INSERT INTO users ...")` → **SQL 작성**
|
||||
✔ `param("name", name)` → **이름 기반 파라미터 바인딩**
|
||||
✔ `update()` → **INSERT/UPDATE/DELETE 실행**
|
||||
|
||||
---
|
||||
|
||||
### **📌 3.2 단일 데이터 조회 (SELECT - 단건 검색)**
|
||||
```java
|
||||
import org.springframework.jdbc.core.simple.JdbcClient;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public class UserRepository {
|
||||
|
||||
private final JdbcClient jdbcClient;
|
||||
|
||||
public UserRepository(JdbcClient jdbcClient) {
|
||||
this.jdbcClient = jdbcClient;
|
||||
}
|
||||
|
||||
public Optional<User> findById(Long id) {
|
||||
return jdbcClient.sql("SELECT * FROM users WHERE id = :id")
|
||||
.param("id", id)
|
||||
.query(User.class)
|
||||
.optional();
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `query(User.class)` → **자동으로 User 객체로 매핑**
|
||||
✔ `optional()` → **결과가 없을 경우 Optional.empty() 반환**
|
||||
|
||||
---
|
||||
|
||||
### **📌 3.3 여러 데이터 조회 (SELECT - 리스트 검색)**
|
||||
```java
|
||||
import org.springframework.jdbc.core.simple.JdbcClient;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public class UserRepository {
|
||||
|
||||
private final JdbcClient jdbcClient;
|
||||
|
||||
public UserRepository(JdbcClient jdbcClient) {
|
||||
this.jdbcClient = jdbcClient;
|
||||
}
|
||||
|
||||
public List<User> findAll() {
|
||||
return jdbcClient.sql("SELECT * FROM users")
|
||||
.query(User.class)
|
||||
.list();
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `query(User.class).list()` → **리스트 형태로 결과 반환**
|
||||
|
||||
---
|
||||
|
||||
### **📌 3.4 데이터 업데이트 (UPDATE)**
|
||||
```java
|
||||
import org.springframework.jdbc.core.simple.JdbcClient;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public class UserRepository {
|
||||
|
||||
private final JdbcClient jdbcClient;
|
||||
|
||||
public UserRepository(JdbcClient jdbcClient) {
|
||||
this.jdbcClient = jdbcClient;
|
||||
}
|
||||
|
||||
public int updateEmail(Long id, String email) {
|
||||
return jdbcClient.sql("UPDATE users SET email = :email WHERE id = :id")
|
||||
.param("email", email)
|
||||
.param("id", id)
|
||||
.update();
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `update()`를 사용하여 데이터를 수정
|
||||
|
||||
---
|
||||
|
||||
### **📌 3.5 데이터 삭제 (DELETE)**
|
||||
```java
|
||||
import org.springframework.jdbc.core.simple.JdbcClient;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public class UserRepository {
|
||||
|
||||
private final JdbcClient jdbcClient;
|
||||
|
||||
public UserRepository(JdbcClient jdbcClient) {
|
||||
this.jdbcClient = jdbcClient;
|
||||
}
|
||||
|
||||
public int deleteById(Long id) {
|
||||
return jdbcClient.sql("DELETE FROM users WHERE id = :id")
|
||||
.param("id", id)
|
||||
.update();
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `update()`를 사용하여 DELETE 실행
|
||||
|
||||
---
|
||||
|
||||
## **4. RowMapper 없이 자동 매핑하기**
|
||||
기존 `JdbcTemplate`에서는 **RowMapper를 사용하여 수동으로 매핑**해야 했지만,
|
||||
`JdbcClient`는 **자동으로 Java 객체와 매핑**할 수 있습니다.
|
||||
|
||||
```java
|
||||
import org.springframework.jdbc.core.simple.JdbcClient;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public class UserRepository {
|
||||
|
||||
private final JdbcClient jdbcClient;
|
||||
|
||||
public UserRepository(JdbcClient jdbcClient) {
|
||||
this.jdbcClient = jdbcClient;
|
||||
}
|
||||
|
||||
public List<User> findAll() {
|
||||
return jdbcClient.sql("SELECT * FROM users")
|
||||
.query(User.class)
|
||||
.list();
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ **User 객체의 필드명과 DB 컬럼명이 일치하면 자동 매핑**
|
||||
|
||||
---
|
||||
|
||||
## **5. 정리**
|
||||
| 기능 | JdbcTemplate | JdbcClient |
|
||||
|------|-------------|------------|
|
||||
| SQL 실행 방식 | `jdbcTemplate.query(sql, rowMapper)` | `jdbcClient.sql(sql).query(clazz)` |
|
||||
| 파라미터 바인딩 | `new MapSqlParameterSource()` 사용 | `.param("name", value)` 사용 |
|
||||
| 결과 변환 | `RowMapper` 필요 | **자동 매핑 가능** |
|
||||
| 코드 스타일 | 절차 지향 | 함수형 스타일 |
|
||||
| 가독성 | 비교적 복잡 | **간결하고 직관적** |
|
||||
|
||||
✔ `JdbcClient`는 **Spring Boot 3 이상에서 사용 가능**
|
||||
✔ 기존 `JdbcTemplate` 대비 **더 간결하고 직관적인 코드 작성 가능**
|
||||
✔ **자동 매핑 기능 제공**하여 `RowMapper` 작성 불필요
|
||||
|
||||
Spring Boot 3 이상을 사용한다면, `JdbcTemplate` 대신 **더 강력하고 간결한 `JdbcClient`를 활용하는 것이 추천됩니다!** 🚀
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# **Spring `JdbcClient` 주요 메서드 정리**
|
||||
|
||||
Spring 6 및 Spring Boot 3에서 도입된 `JdbcClient`는 **기존 `JdbcTemplate`보다 간결한 API**를 제공합니다.
|
||||
아래는 `JdbcClient`의 주요 메서드를 **설명과 함께 표로 정리**한 것입니다.
|
||||
|
||||
---
|
||||
|
||||
## **1. JdbcClient 주요 메서드 정리**
|
||||
|
||||
| 메서드 | 설명 | 예제 |
|
||||
|--------|------|------|
|
||||
| `sql(String sql)` | 실행할 SQL을 설정 | `jdbcClient.sql("SELECT * FROM users")` |
|
||||
| `param(String name, Object value)` | **이름 기반** 파라미터 바인딩 | `.param("id", 1L)` |
|
||||
| `paramSource(SqlParameterSource params)` | **Map 또는 Bean**을 파라미터로 전달 | `.paramSource(new MapSqlParameterSource(Map.of("id", 1L)))` |
|
||||
| `query(Class<T> clazz)` | 결과를 **자동 매핑하여 반환** | `.query(User.class).list()` |
|
||||
| `query(RowMapper<T> rowMapper)` | 커스텀 **RowMapper를 사용한 쿼리 실행** | `.query((rs, rowNum) -> new User(rs.getLong("id"), rs.getString("name"))).list()` |
|
||||
| `queryForObject(Class<T> clazz)` | 단일 결과 반환 (`Optional<T>` 지원) | `.queryForObject(User.class).optional()` |
|
||||
| `list()` | 여러 개의 결과를 리스트로 반환 | `.query(User.class).list()` |
|
||||
| `optional()` | 단일 결과를 **Optional**로 반환 | `.queryForObject(User.class).optional()` |
|
||||
| `update()` | INSERT, UPDATE, DELETE 실행 | `.sql("UPDATE users SET name = :name WHERE id = :id").update()` |
|
||||
| `execute(Function<Connection, T> callback)` | JDBC 커넥션을 직접 다룰 때 사용 | `.execute(conn -> conn.prepareStatement("DELETE FROM users").executeUpdate())` |
|
||||
|
||||
---
|
||||
|
||||
## **2. JdbcClient 주요 메서드 예제**
|
||||
### **📌 단일 데이터 조회 (`queryForObject`)**
|
||||
```java
|
||||
Optional<User> user = jdbcClient.sql("SELECT * FROM users WHERE id = :id")
|
||||
.param("id", 1L)
|
||||
.queryForObject(User.class)
|
||||
.optional();
|
||||
```
|
||||
✔ **SQL 실행 후 `User.class`로 자동 매핑**
|
||||
✔ 결과가 없을 경우 `Optional.empty()` 반환
|
||||
|
||||
---
|
||||
|
||||
### **📌 여러 데이터 조회 (`query` + `list`)**
|
||||
```java
|
||||
List<User> users = jdbcClient.sql("SELECT * FROM users")
|
||||
.query(User.class)
|
||||
.list();
|
||||
```
|
||||
✔ `User.class` 타입으로 리스트 변환
|
||||
|
||||
---
|
||||
|
||||
### **📌 데이터 삽입 (`update`)**
|
||||
```java
|
||||
int rows = jdbcClient.sql("INSERT INTO users (name, email) VALUES (:name, :email)")
|
||||
.param("name", "Alice")
|
||||
.param("email", "alice@example.com")
|
||||
.update();
|
||||
```
|
||||
✔ `update()`를 실행하여 INSERT 수행
|
||||
✔ 반영된 행 수(`int`) 반환
|
||||
|
||||
---
|
||||
|
||||
### **📌 데이터 삭제 (`update`)**
|
||||
```java
|
||||
int rowsDeleted = jdbcClient.sql("DELETE FROM users WHERE id = :id")
|
||||
.param("id", 1L)
|
||||
.update();
|
||||
```
|
||||
✔ 삭제된 행 수 반환
|
||||
|
||||
---
|
||||
|
||||
### **📌 수동 매핑 (RowMapper 사용)**
|
||||
```java
|
||||
List<User> users = jdbcClient.sql("SELECT * FROM users")
|
||||
.query((rs, rowNum) -> new User(rs.getLong("id"), rs.getString("name")))
|
||||
.list();
|
||||
```
|
||||
✔ `RowMapper`를 직접 정의하여 결과 매핑 가능
|
||||
|
||||
---
|
||||
|
||||
## **3. 정리**
|
||||
- `JdbcClient`는 기존 `JdbcTemplate`보다 **간결하고 현대적인 API** 제공
|
||||
- **메서드 체이닝 지원**으로 **코드 가독성이 향상됨**
|
||||
- `queryForObject()`, `query()`를 활용하면 **자동 매핑 가능**
|
||||
- `update()`를 사용하여 **INSERT, UPDATE, DELETE 실행**
|
||||
|
||||
Spring Boot 3 이상을 사용한다면 **`JdbcTemplate`보다 `JdbcClient`를 적극 활용하는 것이 추천됩니다!** 🚀
|
||||
170
docs/41_openapi-doc.md
Normal file
170
docs/41_openapi-doc.md
Normal file
@@ -0,0 +1,170 @@
|
||||
# SpringDoc OpenAPI
|
||||
|
||||
`SpringDoc OpenAPI`는 Spring Boot 애플리케이션에서 **OpenAPI 3 문서를 자동으로 생성**해주는 라이브러리입니다.
|
||||
기존 `SpringFox`보다 **간결하고 최신 OpenAPI 표준을 준수**하며, 유지보수가 활발한 것이 장점입니다.
|
||||
|
||||
**주요 특징**
|
||||
* Spring Boot와 완벽한 호환
|
||||
* **Swagger UI 자동 제공**
|
||||
* REST API 문서를 코드에서 바로 생성
|
||||
* OpenAPI 3 표준 지원
|
||||
|
||||
|
||||
## SpringDoc OpenAPI 설정
|
||||
Spring Boot 3 기준으로 **Maven 또는 Gradle**에 다음 의존성을 추가합니다.
|
||||
|
||||
```kotlin
|
||||
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.6")
|
||||
```
|
||||
|
||||
Spring Boot 애플리케이션을 실행한 후, 브라우저에서 다음 URL로 접속합니다.
|
||||
|
||||
```
|
||||
http://localhost:8080/swagger-ui.html
|
||||
```
|
||||
|
||||
```
|
||||
http://localhost:8080/v3/api-docs
|
||||
```
|
||||
|
||||
* `swagger-ui.html`을 열면 API 문서를 자동으로 확인할 수 있습니다.
|
||||
|
||||
|
||||
## SpringDoc OpenAPI 기본 사용법
|
||||
### 컨트롤러에서 API 문서 자동 생성
|
||||
아래와 같이 **Spring Boot 컨트롤러에 OpenAPI 어노테이션을 추가하면**,
|
||||
Swagger 문서가 자동으로 생성됩니다.
|
||||
|
||||
```java
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/users")
|
||||
@Tag(name = "User API", description = "사용자 관리 API")
|
||||
public class UserController {
|
||||
|
||||
@GetMapping
|
||||
@Operation(summary = "사용자 목록 조회", description = "모든 사용자의 목록을 조회합니다.")
|
||||
public List<Map<String, String>> getUsers() {
|
||||
return List.of(Map.of("id", "1", "name", "Alice"));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@Operation(summary = "사용자 정보 조회", description = "특정 사용자의 정보를 조회합니다.")
|
||||
public Map<String, String> getUser(@PathVariable String id) {
|
||||
return Map.of("id", id, "name", "Alice");
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "사용자 추가", description = "새로운 사용자를 추가합니다.")
|
||||
public String addUser(@RequestBody Map<String, String> user) {
|
||||
return "User " + user.get("name") + " added!";
|
||||
}
|
||||
}
|
||||
```
|
||||
**설명**
|
||||
* `@Tag(name = "User API", description = "사용자 관리 API")` → API 그룹을 정의
|
||||
* `@Operation(summary = "제목", description = "설명")` → API 설명 추가
|
||||
* API 문서는 `http://localhost:8080/swagger-ui.html` 에서 확인 가능
|
||||
|
||||
|
||||
### 요청 및 응답 모델 정의 (`@Schema`)
|
||||
Swagger 문서에서 **요청과 응답 데이터를 명확하게 정의**하려면 DTO 클래스에 `@Schema` 어노테이션을 사용합니다.
|
||||
|
||||
```java
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
|
||||
@Schema(description = "사용자 정보")
|
||||
public class UserDto {
|
||||
|
||||
@Schema(description = "사용자 ID", example = "1")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "사용자 이름", example = "Alice")
|
||||
private String name;
|
||||
|
||||
// 생성자, Getter, Setter 생략
|
||||
}
|
||||
```
|
||||
* `@Schema(description = "설명")` → 필드 설명 추가
|
||||
* `example = "1"` → API 문서에서 예제 값 표시
|
||||
|
||||
|
||||
### API 응답 코드 설정 (`@ApiResponse`)
|
||||
각 API 메서드에 **응답 코드를 명시할 수 있습니다.**
|
||||
|
||||
```java
|
||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@Operation(summary = "사용자 정보 조회", description = "특정 사용자의 정보를 조회합니다.")
|
||||
@ApiResponse(responseCode = "200", description = "성공")
|
||||
@ApiResponse(responseCode = "404", description = "사용자를 찾을 수 없음")
|
||||
public UserDto getUser(@PathVariable Long id) {
|
||||
return new UserDto(id, "Alice");
|
||||
}
|
||||
```
|
||||
* `@ApiResponse(responseCode = "200", description = "성공")` → HTTP 응답 코드 설명 추가
|
||||
|
||||
|
||||
## OpenAPI 문서 커스터마이징
|
||||
Spring Boot의 `application.yml` 또는 `application.properties`에서 OpenAPI 문서를 설정할 수 있습니다.
|
||||
|
||||
```yaml
|
||||
springdoc:
|
||||
api-docs:
|
||||
enabled: true # API 문서 활성화
|
||||
swagger-ui:
|
||||
path: /swagger-ui.html # Swagger UI 경로 변경
|
||||
operations-sorter: method # API 정렬 방식 (메서드명 기준)
|
||||
show-actuator: true # Actuator API 문서 포함 여부
|
||||
```
|
||||
|
||||
|
||||
### 커스텀 OpenAPI 설정 (Security 포함)
|
||||
Spring Boot 애플리케이션에서 **JWT 인증을 Swagger UI에 적용**하려면 `OpenAPI` 빈을 직접 설정할 수도 있습니다.
|
||||
|
||||
```java
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class OpenApiConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info().title("사용자 API")
|
||||
.version("1.0")
|
||||
.description("사용자 관리 서비스"))
|
||||
.addSecurityItem(new SecurityRequirement()
|
||||
.addList("BearerAuth"))
|
||||
.schemaRequirement("BearerAuth",
|
||||
new SecurityScheme().name("Authorization")
|
||||
.type(SecurityScheme.Type.HTTP)
|
||||
.scheme("bearer")
|
||||
.bearerFormat("JWT"));
|
||||
}
|
||||
}
|
||||
```
|
||||
* **JWT 인증을 위한 `SecurityScheme` 설정 가능**
|
||||
* API 문서에 보안 설정을 추가할 수 있음
|
||||
|
||||
|
||||
## **5. 정리**
|
||||
| 기능 | 설명 |
|
||||
|------|------|
|
||||
| **Swagger UI** | `http://localhost:8080/swagger-ui.html`에서 확인 가능 |
|
||||
| **자동 문서 생성** | `@Operation`, `@Tag` 등을 사용하여 API 설명 추가 |
|
||||
| **DTO 모델 문서화** | `@Schema`를 사용하여 요청/응답 모델 정의 |
|
||||
| **응답 코드 정의** | `@ApiResponse`로 HTTP 응답 코드 설명 가능 |
|
||||
| **보안 적용** | JWT, OAuth2 등의 인증을 문서에 포함 가능 |
|
||||
326
docs/90_anno_core.md
Normal file
326
docs/90_anno_core.md
Normal file
@@ -0,0 +1,326 @@
|
||||
# Spring Boot Core 어노테이션 정리
|
||||
|
||||
Spring Boot Core에서 사용되는 주요 어노테이션을 표로 정리한 후, 각각의 어노테이션에 대한 설명과 예제를 제공합니다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 어노테이션 정리표
|
||||
|
||||
| 어노테이션 | 설명 |
|
||||
|------------------------|----------------------------------|
|
||||
| `@SpringBootApplication` | Spring Boot 애플리케이션의 시작점 설정 |
|
||||
| `@Configuration` | 스프링 설정 클래스를 정의 |
|
||||
| `@ComponentScan` | 스프링이 컴포넌트를 자동 검색하도록 설정 |
|
||||
| `@Bean` | 수동으로 빈을 등록 |
|
||||
| `@Component` | 일반적인 빈을 정의 |
|
||||
| `@Service` | 비즈니스 로직을 담당하는 빈을 정의 |
|
||||
| `@Repository` | 데이터 접근 계층 빈을 정의 |
|
||||
| `@Autowired` | 자동으로 빈을 주입 |
|
||||
| `@Qualifier` | 특정 빈을 지정하여 주입 |
|
||||
| `@Primary` | 기본적으로 주입될 빈을 지정 |
|
||||
| `@Value` | 프로퍼티 값을 주입 |
|
||||
| `@PropertySource` | 외부 설정 파일을 로드 |
|
||||
| `@Profile` | 특정 프로파일에서만 빈을 로드 |
|
||||
| `@Lazy` | 필요한 경우에만 빈을 초기화 |
|
||||
| `@Scope` | 빈의 스코프를 지정 |
|
||||
| `@DependsOn` | 특정 빈이 다른 빈보다 먼저 로드되도록 설정 |
|
||||
| `@PostConstruct` | 빈이 생성된 후 실행할 메서드 지정 |
|
||||
| `@PreDestroy` | 빈이 제거되기 전 실행할 메서드 지정 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 어노테이션 설명 및 예제
|
||||
|
||||
### 1) `@SpringBootApplication`
|
||||
Spring Boot 애플리케이션의 시작 클래스를 정의하는 어노테이션.
|
||||
- 내부적으로 `@Configuration`, `@EnableAutoConfiguration`, `@ComponentScan`을 포함.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@SpringBootApplication
|
||||
public class MyApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MyApplication.class, args);
|
||||
}
|
||||
}
|
||||
```
|
||||
- 이 클래스가 애플리케이션의 시작점 역할을 합니다.
|
||||
|
||||
---
|
||||
|
||||
### 2) `@Configuration`
|
||||
Java 기반 설정을 정의하는 클래스에 사용.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Configuration
|
||||
public class AppConfig {
|
||||
@Bean
|
||||
public MyService myService() {
|
||||
return new MyService();
|
||||
}
|
||||
}
|
||||
```
|
||||
- `MyService` 객체를 빈으로 등록.
|
||||
|
||||
---
|
||||
|
||||
### 3) `@ComponentScan`
|
||||
패키지를 검색하여 `@Component`, `@Service`, `@Repository` 등을 자동으로 등록.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@ComponentScan(basePackages = "com.example.service")
|
||||
@Configuration
|
||||
public class AppConfig {
|
||||
}
|
||||
```
|
||||
- `"com.example.service"` 패키지를 스캔하여 빈 등록.
|
||||
|
||||
---
|
||||
|
||||
### 4) `@Bean`
|
||||
메서드의 반환 객체를 스프링 빈으로 등록.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Configuration
|
||||
public class BeanConfig {
|
||||
@Bean
|
||||
public MyService myService() {
|
||||
return new MyService();
|
||||
}
|
||||
}
|
||||
```
|
||||
- `myService()`의 반환 객체가 빈으로 등록.
|
||||
|
||||
---
|
||||
|
||||
### 5) `@Component`
|
||||
일반적인 스프링 빈을 정의할 때 사용.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Component
|
||||
public class MyComponent {
|
||||
public void doSomething() {
|
||||
System.out.println("Component working");
|
||||
}
|
||||
}
|
||||
```
|
||||
- 스프링이 자동으로 빈으로 등록.
|
||||
|
||||
---
|
||||
|
||||
### 6) `@Service`
|
||||
비즈니스 로직을 담당하는 서비스 계층의 빈을 정의.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Service
|
||||
public class MyService {
|
||||
public String getMessage() {
|
||||
return "Hello, Service!";
|
||||
}
|
||||
}
|
||||
```
|
||||
- 서비스 계층의 빈으로 등록.
|
||||
|
||||
---
|
||||
|
||||
### 7) `@Repository`
|
||||
데이터 접근 계층의 빈을 정의.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Repository
|
||||
public class MyRepository {
|
||||
public String findData() {
|
||||
return "Data from DB";
|
||||
}
|
||||
}
|
||||
```
|
||||
- DAO(Data Access Object) 역할을 하는 클래스.
|
||||
|
||||
---
|
||||
|
||||
### 8) `@Autowired`
|
||||
빈을 자동으로 주입.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Component
|
||||
public class MyController {
|
||||
private final MyService myService;
|
||||
|
||||
@Autowired
|
||||
public MyController(MyService myService) {
|
||||
this.myService = myService;
|
||||
}
|
||||
}
|
||||
```
|
||||
- `MyService` 빈이 자동 주입됨.
|
||||
|
||||
---
|
||||
|
||||
### 9) `@Qualifier`
|
||||
같은 타입의 여러 빈 중 특정 빈을 선택하여 주입.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Component
|
||||
public class FirstService implements MyService {
|
||||
}
|
||||
|
||||
@Component
|
||||
public class SecondService implements MyService {
|
||||
}
|
||||
|
||||
@Component
|
||||
public class MyController {
|
||||
private final MyService myService;
|
||||
|
||||
@Autowired
|
||||
public MyController(@Qualifier("secondService") MyService myService) {
|
||||
this.myService = myService;
|
||||
}
|
||||
}
|
||||
```
|
||||
- `secondService` 빈이 주입됨.
|
||||
|
||||
---
|
||||
|
||||
### 10) `@Primary`
|
||||
여러 빈이 있을 때 기본 빈을 지정.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Primary
|
||||
@Component
|
||||
public class DefaultService implements MyService {
|
||||
}
|
||||
```
|
||||
- `@Autowired` 시 기본적으로 `DefaultService`가 주입됨.
|
||||
|
||||
---
|
||||
|
||||
### 11) `@Value`
|
||||
설정 파일에서 값을 주입.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Component
|
||||
public class ConfigComponent {
|
||||
@Value("${app.name}")
|
||||
private String appName;
|
||||
}
|
||||
```
|
||||
- `application.properties`에서 `app.name` 값을 읽음.
|
||||
|
||||
---
|
||||
|
||||
### 12) `@PropertySource`
|
||||
설정 파일을 로드.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Configuration
|
||||
@PropertySource("classpath:app.properties")
|
||||
public class AppConfig {
|
||||
}
|
||||
```
|
||||
- `app.properties` 파일을 로드.
|
||||
|
||||
---
|
||||
|
||||
### 13) `@Profile`
|
||||
특정 프로파일에서만 빈을 로드.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Profile("dev")
|
||||
@Component
|
||||
public class DevService {
|
||||
}
|
||||
```
|
||||
- `"dev"` 프로파일에서만 로드.
|
||||
|
||||
---
|
||||
|
||||
### 14) `@Lazy`
|
||||
빈을 필요할 때만 초기화.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Component
|
||||
@Lazy
|
||||
public class LazyComponent {
|
||||
}
|
||||
```
|
||||
- `LazyComponent`는 필요할 때만 생성됨.
|
||||
|
||||
---
|
||||
|
||||
### 15) `@Scope`
|
||||
빈의 스코프를 설정.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Component
|
||||
@Scope("prototype")
|
||||
public class PrototypeComponent {
|
||||
}
|
||||
```
|
||||
- 새로운 객체가 매번 생성됨.
|
||||
|
||||
---
|
||||
|
||||
### 16) `@DependsOn`
|
||||
다른 빈이 먼저 로드되도록 설정.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Component
|
||||
@DependsOn("anotherComponent")
|
||||
public class MyComponent {
|
||||
}
|
||||
```
|
||||
- `anotherComponent`가 먼저 로드됨.
|
||||
|
||||
---
|
||||
|
||||
### 17) `@PostConstruct`
|
||||
빈 생성 후 실행할 메서드 지정.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Component
|
||||
public class InitComponent {
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
System.out.println("Initialized!");
|
||||
}
|
||||
}
|
||||
```
|
||||
- 빈 생성 후 `init()` 실행.
|
||||
|
||||
---
|
||||
|
||||
### 18) `@PreDestroy`
|
||||
빈 제거 전에 실행할 메서드 지정.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Component
|
||||
public class DestroyComponent {
|
||||
@PreDestroy
|
||||
public void destroy() {
|
||||
System.out.println("Destroyed!");
|
||||
}
|
||||
}
|
||||
```
|
||||
- 빈 제거 전 `destroy()` 실행.
|
||||
|
||||
---
|
||||
|
||||
Spring Boot Core에서 사용되는 주요 어노테이션을 정리했습니다. 이를 활용하면 애플리케이션의 설정과 빈 관리를 효과적으로 수행할 수 있습니다.
|
||||
119
docs/91_dependencies.md
Normal file
119
docs/91_dependencies.md
Normal file
@@ -0,0 +1,119 @@
|
||||
스프링 부트(Spring Boot)는 다양한 기능을 제공하기 위해 여러 가지 **의존성 패키지(스타터)**를 사용합니다. 이 스타터들은 특정 기능 구현에 필요한 라이브러리들을 묶어서 제공하는 일종의 **"기능 모듈 번들"**입니다.
|
||||
아래는 주요한 **Spring Boot 의존성 패키지 목록**과 **각각의 기능 설명**입니다.
|
||||
|
||||
---
|
||||
|
||||
## ✅ 핵심(Spring Boot Core)
|
||||
|
||||
| 의존성 패키지 | 설명 |
|
||||
|--------------------------------------|------|
|
||||
| `spring-boot-starter` | 스프링 부트의 **기본 핵심 기능** 제공 (로깅, 자동 설정 등) |
|
||||
| `spring-boot-starter-parent` | Maven 프로젝트의 부모로 사용. 버전 관리, 플러그인, 의존성 정리 지원 |
|
||||
| `spring-boot-starter-test` | JUnit, Mockito, Spring Test 등 포함된 **테스트 전용** 스타터 |
|
||||
|
||||
---
|
||||
|
||||
## 🌐 웹 개발
|
||||
|
||||
| 의존성 패키지 | 설명 |
|
||||
|--------------------------------------|------|
|
||||
| `spring-boot-starter-web` | Spring MVC, Tomcat(기본), REST API 작성용 핵심 스타터 |
|
||||
| `spring-boot-starter-webflux` | **비동기 웹 개발**을 위한 WebFlux 지원 (Reactor 기반) |
|
||||
| `spring-boot-starter-thymeleaf` | Thymeleaf 템플릿 엔진 사용 (서버 사이드 렌더링 HTML) |
|
||||
| `spring-boot-starter-mustache` | Mustache 템플릿 엔진 지원 |
|
||||
| `spring-boot-starter-freemarker` | FreeMarker 템플릿 엔진 지원 |
|
||||
| `spring-boot-starter-undertow` | 톰캣 대신 Undertow 사용 |
|
||||
| `spring-boot-starter-jetty` | 톰캣 대신 Jetty 사용 |
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ 데이터베이스 & JPA
|
||||
|
||||
| 의존성 패키지 | 설명 |
|
||||
|--------------------------------------|------|
|
||||
| `spring-boot-starter-data-jpa` | JPA, Hibernate 기반 ORM 지원 |
|
||||
| `spring-boot-starter-data-jdbc` | **JDBC 직접 접근용**. ORM 없이 SQL 중심 개발 |
|
||||
| `spring-boot-starter-jdbc` | 순수 JDBC 개발용. 트랜잭션, DataSource 등 포함 |
|
||||
| `spring-boot-starter-data-mongodb` | MongoDB 연동 (Spring Data MongoDB) |
|
||||
| `spring-boot-starter-data-redis` | Redis 연동 (Spring Data Redis) |
|
||||
| `spring-boot-starter-data-elasticsearch` | Elasticsearch 연동 |
|
||||
| `spring-boot-starter-batch` | Spring Batch 기반 대용량 배치 처리 |
|
||||
| `spring-boot-starter-quartz` | Quartz 스케줄링 엔진 연동 |
|
||||
|
||||
---
|
||||
|
||||
## 🔐 보안 & 인증
|
||||
|
||||
| 의존성 패키지 | 설명 |
|
||||
|--------------------------------------|------|
|
||||
| `spring-boot-starter-security` | Spring Security 기반의 인증, 인가 시스템 지원 |
|
||||
| `spring-boot-starter-oauth2-client` | OAuth2 인증 클라이언트용 |
|
||||
| `spring-boot-starter-oauth2-resource-server` | 리소스 서버 기능 제공 (JWT 등) |
|
||||
|
||||
---
|
||||
|
||||
## 📨 메시징 & 통신
|
||||
|
||||
| 의존성 패키지 | 설명 |
|
||||
|--------------------------------------|------|
|
||||
| `spring-boot-starter-mail` | SMTP 메일 전송 기능 지원 |
|
||||
| `spring-boot-starter-amqp` | RabbitMQ 연동 (Spring AMQP) |
|
||||
| `spring-boot-starter-artemis` | ActiveMQ Artemis 연동 |
|
||||
| `spring-boot-starter-integration` | Spring Integration 기반 시스템 통합 |
|
||||
| `spring-boot-starter-websocket` | 웹소켓 실시간 통신 기능 제공 |
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 개발 편의성
|
||||
|
||||
| 의존성 패키지 | 설명 |
|
||||
|--------------------------------------|------|
|
||||
| `spring-boot-devtools` | **자동 리로드**, 빠른 개발을 위한 도구 |
|
||||
| `spring-boot-configuration-processor` | `application.properties` 또는 `application.yml` 자동 완성 지원 |
|
||||
| `spring-boot-starter-actuator` | **서버 상태, 메트릭, 헬스체크, 모니터링** 기능 제공 (`/actuator` 엔드포인트) |
|
||||
|
||||
---
|
||||
|
||||
## 🧪 테스트
|
||||
|
||||
| 의존성 패키지 | 설명 |
|
||||
|--------------------------------------|------|
|
||||
| `spring-boot-starter-test` | JUnit5, Mockito, AssertJ, Spring Test 포함 (모든 테스트에 기본 사용) |
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ 추가 도구 (선택적)
|
||||
|
||||
| 의존성 패키지 | 설명 |
|
||||
|--------------------------------------|------|
|
||||
| `spring-boot-starter-validation` | Bean Validation (JSR-380, Hibernate Validator 포함) |
|
||||
| `spring-boot-starter-logging` | SLF4J + Logback 기반 로깅 설정 (기본 내장) |
|
||||
| `spring-boot-starter-json` | Jackson 기반 JSON 직렬화/역직렬화 기능 |
|
||||
| `spring-boot-starter-cache` | Spring Cache abstraction 지원 |
|
||||
| `spring-boot-starter-aop` | AOP 기능 지원 (AspectJ 등 포함) |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 예시: 일반적인 웹 애플리케이션
|
||||
|
||||
```groovy
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web' // 웹 MVC
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'// ORM
|
||||
implementation 'org.springframework.boot:spring-boot-starter-security'// 보안
|
||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' // 템플릿
|
||||
implementation 'com.mysql:mysql-connector-j' // MySQL JDBC 드라이버
|
||||
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧾 마무리
|
||||
|
||||
스프링 부트의 스타터 패키지는 **복잡한 의존성 관리를 간소화**하고, **빠른 애플리케이션 개발**을 가능하게 해줍니다. 필요에 따라 스타터를 조합하여 자신만의 맞춤형 프로젝트를 구성할 수 있으며, 대부분의 기능은 **자동 설정(Auto Configuration)**으로 바로 사용할 수 있다는 것이 큰 장점입니다.
|
||||
|
||||
---
|
||||
|
||||
원하신다면 "어떤 조합이 어떤 상황에 적절한지" 또는 "실무에서 자주 쓰는 조합 베스트 5" 같은 응용도 정리해드릴 수 있어요!
|
||||
94
docs/92_servlet containers.md
Normal file
94
docs/92_servlet containers.md
Normal file
@@ -0,0 +1,94 @@
|
||||
좋아요! 아래는 Java 기반 웹 서버/서블릿 컨테이너인 **Tomcat**, **Jetty**, **Undertow**에 대한 비교 설명입니다. 이 세 가지는 **Spring Boot**나 기타 웹 프레임워크에서 선택 가능한 내장 웹 서버로도 많이 사용되므로, 실무에서도 선택 기준이 되는 중요한 요소들이 많습니다.
|
||||
|
||||
---
|
||||
|
||||
# ✅ Tomcat, Jetty, Undertow 비교
|
||||
|
||||
| 항목 | **Tomcat** | **Jetty** | **Undertow** |
|
||||
|------|------------|-----------|---------------|
|
||||
| **제공자** | Apache Software Foundation | Eclipse Foundation | Red Hat (JBoss) |
|
||||
| **Servlet 지원** | Servlet 컨테이너 (표준) | Servlet 컨테이너 | Servlet 컨테이너 + Non-blocking 서버 |
|
||||
| **기본 포지션** | 안정성과 호환성 중심 | 가볍고 유연함 | 고성능, Non-blocking |
|
||||
| **Spring Boot 기본** | ✅ 기본 내장 서버 | ❌ | ❌ |
|
||||
| **성능** | 안정적, 무난함 | 빠르고 경량 | 매우 빠름 (Non-blocking I/O) |
|
||||
| **비동기 처리** | 일부 지원 (Servlet 3.1 기준) | 잘 지원 | 완전한 Non-blocking 지원 |
|
||||
| **HTTP/2 지원** | 설정 필요 | 설정 필요 | 기본적으로 잘 지원 |
|
||||
| **설정 난이도** | 쉬움 | 쉬움 | 비교적 복잡함 |
|
||||
| **커뮤니티 / 문서** | 가장 풍부함 | 풍부함 | 상대적으로 적음 |
|
||||
| **가용성** | 전통적인 선택 | 경량/모듈식 프로젝트에서 인기 | 고성능 시스템에서 선호 |
|
||||
|
||||
---
|
||||
|
||||
## 🔵 Apache Tomcat
|
||||
|
||||
- Java 웹 서버의 대표 주자
|
||||
- 오랜 시간 동안 많은 프로젝트에 사용됨
|
||||
- **안정성, 문서화, 호환성**이 매우 뛰어남
|
||||
- Spring Boot의 **기본 내장 톰캣**도 이 녀석
|
||||
- 많은 기업 환경에서 **표준 선택지**로 여겨짐
|
||||
|
||||
💡 *추천 상황*: 대규모 시스템, 전통적인 기업 시스템, Spring Boot 기본 설정 그대로 사용하는 경우
|
||||
|
||||
---
|
||||
|
||||
## 🟢 Jetty
|
||||
|
||||
- 가볍고 유연한 구조의 웹 서버
|
||||
- Eclipse 프로젝트로, 모듈화된 구조
|
||||
- **빠른 시작 속도**와 **작은 메모리 풋프린트**가 강점
|
||||
- **오버헤드 적고 가볍게 돌릴 수 있는 서버**가 필요할 때 유리
|
||||
|
||||
💡 *추천 상황*: 임베디드 시스템, 마이크로서비스, 빠른 개발 환경
|
||||
|
||||
---
|
||||
|
||||
## 🔴 Undertow
|
||||
|
||||
- Red Hat(JBoss)에서 개발
|
||||
- **Non-blocking I/O** 구조 기반의 고성능 서버
|
||||
- Servlet 컨테이너로도 동작하고, **프레임워크 없이도 HTTP 처리 가능**
|
||||
- **최소한의 스레드로 최대 성능**을 뽑아내는 것이 특징
|
||||
- HTTP/2, WebSocket 등 최신 기능 잘 지원
|
||||
|
||||
💡 *추천 상황*: 고성능 API 서버, 대규모 트래픽 처리 시스템, 실시간 통신(WebSocket 등)
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Spring Boot에서 선택하는 방법
|
||||
|
||||
Spring Boot에서는 기본 내장 톰캣이 설정되어 있지만, 다음과 같이 원하는 서버로 쉽게 교체할 수 있습니다:
|
||||
|
||||
### Gradle 예시:
|
||||
|
||||
```groovy
|
||||
// Tomcat 제거
|
||||
implementation('org.springframework.boot:spring-boot-starter-web') {
|
||||
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
|
||||
}
|
||||
|
||||
// Jetty 사용
|
||||
implementation 'org.springframework.boot:spring-boot-starter-jetty'
|
||||
|
||||
// 또는 Undertow 사용
|
||||
// implementation 'org.springframework.boot:spring-boot-starter-undertow'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# 🚦 정리하면
|
||||
|
||||
| 상황 | 추천 서버 |
|
||||
|------|-----------|
|
||||
| **안정성과 호환성이 중요** | Tomcat |
|
||||
| **가볍고 빠른 스타트업이 필요** | Jetty |
|
||||
| **고성능 / 비동기 / 실시간 처리 필요** | Undertow |
|
||||
|
||||
---
|
||||
|
||||
필요하시면 다음도 도와드릴 수 있어요:
|
||||
|
||||
- Spring Boot에서 Undertow로 바꾸는 실습 예제
|
||||
- 성능 벤치마크 예시
|
||||
- 서버별로 WebSocket 설정하는 방법
|
||||
|
||||
도움 필요하신가요? 😄
|
||||
101
docs/README.md
Normal file
101
docs/README.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# **실무 중심 Spring Boot MVC 책 목차**
|
||||
|
||||
## **1. Spring Boot MVC 개요**
|
||||
1.1. Spring Boot와 MVC 아키텍처 개념
|
||||
1.2. Spring Boot MVC 프로젝트 구조
|
||||
2.2. 기본 패키징 전략 및 레이어드 아키텍처
|
||||
2.1. Spring Boot 프로젝트 생성 (Spring Initializr 사용)
|
||||
|
||||
## **2. Spring Boot MVC 프로젝트 시작하기**
|
||||
1.3. Spring Boot 설정 및 의존성 관리
|
||||
1.3 주요 의존성 및 설정 (`spring-boot-starter-web` 등)
|
||||
---
|
||||
2.3. 1.4 `application.yml` 설정
|
||||
4.4 로깅 (`SLF4J`, `Logback`)
|
||||
|
||||
## **3. 컨트롤러와 요청 처리**
|
||||
3.1. `@Controller`와 `@RestController`
|
||||
3.2. 요청 매핑:`@RequestMapping`과 HTTP 메서드 매핑 (`@GetMapping`, `@PostMapping` 등)
|
||||
3.3. 요청 데이터 처리: (`@RequestParam`, `@PathVariable`, `@ModelAttribute`)
|
||||
3.4. 응답 데이터 처리: `@ResponseBody`, `ResponseEntity`
|
||||
|
||||
## **4. 뷰(View) 처리와 템플릿 엔진**
|
||||
4.1. Thymeleaf 설정
|
||||
3.2 HTML 템플릿 구성 (`th:text`, `th:each`,`th:fragment` 등)
|
||||
4.2. 모델 데이터 전달 (`Model`, `ModelAndView`)
|
||||
4.3. HTML 폼 처리 및 바인딩 (`@ModelAttribute`)
|
||||
4.4. 국제화(i18n) 및 메시지 번들
|
||||
|
||||
## **7. Spring Boot와 REST API 개발**
|
||||
7.1. RESTful API 설계 원칙
|
||||
7.2. JSON 데이터 직렬화와 `@ResponseBody`
|
||||
7.3. `ResponseEntity`를 활용한 응답 제어
|
||||
7.4. API 버전 관리
|
||||
7.3 `@RestControllerAdvice`를 활용한 전역 예외 처리
|
||||
7.4 API 문서화 (`Swagger`, `Spring REST Docs`)
|
||||
7.5 CORS 설정 및 보안 고려사항
|
||||
|
||||
## **8. 요청 및 응답 데이터 처리**
|
||||
8.1. DTO 패턴과 ModelMapper 적용
|
||||
8.2. 파일 업로드 및 다운로드 처리
|
||||
8.3. JSON 및 XML 데이터 변환
|
||||
6.3 세션 및 쿠키 관리
|
||||
|
||||
## **5. 예외 처리 및 AOP 활용**
|
||||
5.1. `@ExceptionHandler`를 이용한 예외 처리
|
||||
5.2. `@ControllerAdvice`를 이용한 전역 예외 처리
|
||||
5.3. AOP 기반 로깅 및 성능 모니터링
|
||||
|
||||
|
||||
## **6장. 요청 처리 고급 기능**
|
||||
6.1 필터와 인터셉터 (`Filter`, `HandlerInterceptor`)
|
||||
6.2 AOP를 활용한 로깅 및 인증 (`@Aspect`, `@Around`)
|
||||
|
||||
|
||||
|
||||
## **6. Spring Boot와 데이터베이스 연동**
|
||||
6.1. Spring Boot + JPA 설정 및 H2 Database 사용
|
||||
6.2. Spring Data JPA와 Repository 패턴 (`@Entity`, `@Repository`)
|
||||
6.3. CRUD API 구현
|
||||
6.4. 페이징과 정렬 (`Pageable`,`Sort` 활용)
|
||||
6.5. 트랜잭션 관리 (`@Transactional`)
|
||||
5.5 커스텀 쿼리 (`@Query`, `Criteria API`)
|
||||
|
||||
## **4장. 서비스 계층과 비즈니스 로직**
|
||||
4.1 서비스 계층의 역할과 `@Service`
|
||||
4.2 트랜잭션 관리 (`@Transactional`)
|
||||
4.3 DTO와 Entity 분리 전략
|
||||
|
||||
|
||||
## **9. 인증과 보안(Spring Security)**
|
||||
9.1. Spring Security 설정
|
||||
9.2. 로그인 및 인증 처리
|
||||
9.3. JWT 기반 인증 구현
|
||||
9.4. OAuth2와 소셜 로그인 적용
|
||||
|
||||
|
||||
## **10. 비동기 처리 및 이벤트 시스템**
|
||||
10.1. `@Async`를 이용한 비동기 처리
|
||||
10.2. 이벤트 기반 아키텍처 (`ApplicationEventPublisher`)
|
||||
10.3. WebSocket을 활용한 실시간 통신
|
||||
|
||||
## **11. 성능 최적화 및 캐싱**
|
||||
11.1. 캐싱(`@Cacheable`, `@CacheEvict`) 적용
|
||||
11.2. Redis를 이용한 캐싱 최적화
|
||||
11.3. 쿼리 최적화 및 Lazy Loading 전략
|
||||
9.2 비동기 처리 (`@Async`)
|
||||
9.3 스케줄링 (`@Scheduled`)
|
||||
|
||||
|
||||
## **12. 배포 및 운영**
|
||||
12.1. Spring Boot 애플리케이션 빌드 (`jar` 및 `war`)
|
||||
12.2. Docker를 이용한 컨테이너 배포
|
||||
12.3. CI/CD 파이프라인 구축 (GitHub Actions, Jenkins)
|
||||
12.4. 로그 및 모니터링 (Spring Actuator, Prometheus, Grafana)
|
||||
|
||||
## **부록**
|
||||
- Spring Boot 주요 설정 파일 예제
|
||||
- 자주 발생하는 에러 및 해결 방법
|
||||
- 실전 프로젝트 예제 코드
|
||||
|
||||
이 책은 **실무에서 바로 적용할 수 있는 실전 중심의 내용을 담는 것**을 목표로 합니다! 필요하면 세부 내용을 더 보강할 수도 있어요.
|
||||
505
docs/_archive/02_컨트롤러와 요청 처리.md
Normal file
505
docs/_archive/02_컨트롤러와 요청 처리.md
Normal file
@@ -0,0 +1,505 @@
|
||||
# **Spring Boot MVC: 컨트롤러와 요청 처리**
|
||||
|
||||
Spring Boot의 핵심 기능 중 하나는 **MVC(Model-View-Controller) 패턴을 사용한 웹 요청 처리**입니다.
|
||||
이 글에서는 **Spring Boot에서 컨트롤러가 어떻게 요청을 처리하는지**, 그리고 실무에서 **자주 사용하는 컨트롤러 관련 기능**을 예제와 함께 설명합니다.
|
||||
|
||||
---
|
||||
|
||||
## **1. 컨트롤러란?**
|
||||
컨트롤러(Controller)는 **클라이언트의 요청을 받아서 비즈니스 로직을 수행하고, 응답을 반환하는 역할**을 합니다.
|
||||
Spring Boot에서는 `@RestController` 또는 `@Controller` 어노테이션을 사용하여 컨트롤러를 정의합니다.
|
||||
|
||||
---
|
||||
|
||||
## **2. 기본 컨트롤러 작성하기**
|
||||
|
||||
Spring Boot에서는 `@RestController`를 사용하면 JSON 형태로 응답을 반환할 수 있습니다.
|
||||
|
||||
### **📌 기본 컨트롤러 예제**
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class HelloController {
|
||||
|
||||
@GetMapping("/hello")
|
||||
public String sayHello() {
|
||||
return "Hello, Spring Boot!";
|
||||
}
|
||||
}
|
||||
```
|
||||
#### **📌 실행 결과 (`http://localhost:8080/api/hello` 요청)**
|
||||
```json
|
||||
Hello, Spring Boot!
|
||||
```
|
||||
**설명:**
|
||||
- `@RestController` → REST API를 위한 컨트롤러를 정의.
|
||||
- `@RequestMapping("/api")` → 모든 경로가 `/api`로 시작하도록 설정.
|
||||
- `@GetMapping("/hello")` → `/api/hello`로 GET 요청이 들어오면 `"Hello, Spring Boot!"` 반환.
|
||||
|
||||
---
|
||||
|
||||
## **3. 요청 매개변수 처리**
|
||||
|
||||
클라이언트가 **쿼리 파라미터** 또는 **URL 경로 변수**를 전달하면, 이를 컨트롤러에서 처리할 수 있습니다.
|
||||
|
||||
### **📌 `@RequestParam` 사용 (쿼리 파라미터)**
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class GreetingController {
|
||||
|
||||
@GetMapping("/greet")
|
||||
public String greet(@RequestParam String name) {
|
||||
return "Hello, " + name + "!";
|
||||
}
|
||||
}
|
||||
```
|
||||
#### **📌 실행 결과 (`http://localhost:8080/api/greet?name=John` 요청)**
|
||||
```json
|
||||
Hello, John!
|
||||
```
|
||||
**설명:**
|
||||
- `@RequestParam`을 사용하면 **쿼리 파라미터에서 값을 가져올 수 있음**.
|
||||
- 예제에서는 `name` 값을 받아 `"Hello, John!"`을 반환.
|
||||
|
||||
---
|
||||
|
||||
### **📌 `@PathVariable` 사용 (URL 경로 변수)**
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class UserController {
|
||||
|
||||
@GetMapping("/user/{id}")
|
||||
public String getUser(@PathVariable int id) {
|
||||
return "User ID: " + id;
|
||||
}
|
||||
}
|
||||
```
|
||||
#### **📌 실행 결과 (`http://localhost:8080/api/user/5` 요청)**
|
||||
```json
|
||||
User ID: 5
|
||||
```
|
||||
**설명:**
|
||||
- `@PathVariable`을 사용하면 **URL 경로에서 변수를 추출**할 수 있음.
|
||||
- 예제에서는 `/user/5` 요청 시, `id=5`로 인식하여 `"User ID: 5"` 반환.
|
||||
|
||||
---
|
||||
|
||||
## **4. 요청 본문 처리 (POST 요청)**
|
||||
|
||||
클라이언트가 **JSON 데이터를 요청 본문(body)에 담아 전송**하면, 이를 컨트롤러에서 처리할 수 있습니다.
|
||||
|
||||
### **📌 `@RequestBody` 사용**
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class ProductController {
|
||||
|
||||
@PostMapping("/product")
|
||||
public String createProduct(@RequestBody Product product) {
|
||||
return "Product created: " + product.getName();
|
||||
}
|
||||
}
|
||||
|
||||
class Product {
|
||||
private String name;
|
||||
private double price;
|
||||
|
||||
// 기본 생성자 필요 (JSON 역직렬화)
|
||||
public Product() {}
|
||||
|
||||
public String getName() { return name; }
|
||||
public double getPrice() { return price; }
|
||||
}
|
||||
```
|
||||
#### **📌 실행 결과 (`POST /api/product` 요청)**
|
||||
##### **요청 본문(JSON)**
|
||||
```json
|
||||
{
|
||||
"name": "Laptop",
|
||||
"price": 1200.00
|
||||
}
|
||||
```
|
||||
##### **응답**
|
||||
```json
|
||||
Product created: Laptop
|
||||
```
|
||||
**설명:**
|
||||
- `@RequestBody`를 사용하면 **JSON 데이터를 Java 객체로 변환하여 받을 수 있음**.
|
||||
- 클라이언트가 `{ "name": "Laptop", "price": 1200.00 }`을 전송하면, 이를 `Product` 객체로 매핑.
|
||||
- `"Product created: Laptop"`을 반환.
|
||||
|
||||
---
|
||||
|
||||
## **5. 응답 데이터 처리**
|
||||
|
||||
Spring Boot에서는 다양한 방식으로 응답을 반환할 수 있습니다.
|
||||
|
||||
### **📌 `ResponseEntity`를 사용한 응답 처리**
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class ResponseController {
|
||||
|
||||
@GetMapping("/status")
|
||||
public ResponseEntity<String> getStatus() {
|
||||
return ResponseEntity.status(HttpStatus.OK)
|
||||
.body("Everything is fine!");
|
||||
}
|
||||
}
|
||||
```
|
||||
#### **📌 실행 결과 (`GET /api/status` 요청)**
|
||||
```json
|
||||
Everything is fine!
|
||||
```
|
||||
**설명:**
|
||||
- `ResponseEntity`를 사용하면 **HTTP 상태 코드와 응답 데이터를 함께 설정 가능**.
|
||||
- `HttpStatus.OK` → 200 응답 코드 설정.
|
||||
|
||||
---
|
||||
|
||||
## **6. 요청 및 응답 헤더 처리**
|
||||
|
||||
### **📌 `@RequestHeader` 사용 (요청 헤더 읽기)**
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class HeaderController {
|
||||
|
||||
@GetMapping("/header")
|
||||
public String getHeader(@RequestHeader("User-Agent") String userAgent) {
|
||||
return "Your User-Agent: " + userAgent;
|
||||
}
|
||||
}
|
||||
```
|
||||
#### **📌 실행 결과 (`GET /api/header` 요청)**
|
||||
```json
|
||||
Your User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)
|
||||
```
|
||||
**설명:**
|
||||
- `@RequestHeader("User-Agent")`를 사용하여 요청 헤더에서 **User-Agent 값 추출**.
|
||||
|
||||
---
|
||||
|
||||
## **7. 예외 처리 (`@ExceptionHandler`)**
|
||||
|
||||
### **📌 컨트롤러에서 예외 처리하기**
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class ExceptionController {
|
||||
|
||||
@GetMapping("/error")
|
||||
public String throwError() {
|
||||
throw new RuntimeException("Something went wrong!");
|
||||
}
|
||||
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public ResponseEntity<String> handleRuntimeException(RuntimeException e) {
|
||||
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
.body("Error: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
```
|
||||
#### **📌 실행 결과 (`GET /api/error` 요청)**
|
||||
```json
|
||||
Error: Something went wrong!
|
||||
```
|
||||
**설명:**
|
||||
- `@ExceptionHandler(RuntimeException.class)`을 사용하여 **컨트롤러 내에서 예외 처리**.
|
||||
|
||||
---
|
||||
|
||||
## **8. 정리**
|
||||
| 기능 | 어노테이션 | 설명 |
|
||||
|------|----------|------|
|
||||
| **기본 컨트롤러** | `@RestController` | REST API 컨트롤러 정의 |
|
||||
| **GET 요청 처리** | `@GetMapping` | HTTP GET 요청 매핑 |
|
||||
| **POST 요청 처리** | `@PostMapping` | HTTP POST 요청 매핑 |
|
||||
| **쿼리 파라미터** | `@RequestParam` | URL 쿼리 매개변수 받기 |
|
||||
| **URL 경로 변수** | `@PathVariable` | URL 경로에서 값 추출 |
|
||||
| **JSON 요청 본문** | `@RequestBody` | 요청 본문을 객체로 매핑 |
|
||||
| **요청 헤더 처리** | `@RequestHeader` | 요청 헤더 값을 읽기 |
|
||||
| **응답 처리** | `ResponseEntity` | 상태 코드와 함께 응답 반환 |
|
||||
| **예외 처리** | `@ExceptionHandler` | 예외 발생 시 응답 처리 |
|
||||
|
||||
Spring Boot의 컨트롤러는 **간결하면서도 강력한 기능**을 제공합니다.
|
||||
실무에서는 **REST API 개발 시 `@RestController`, 요청 처리 시 `@RequestParam`, `@RequestBody`, 예외 처리 시 `@ExceptionHandler`** 등을 적극 활용하면 좋습니다!
|
||||
|
||||
------
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# **Spring Boot MVC에서 컨트롤러와 요청 처리**
|
||||
|
||||
Spring Boot MVC에서 컨트롤러는 **클라이언트 요청을 받아 적절한 응답을 반환하는 역할**을 합니다. 컨트롤러를 정의하려면 `@Controller` 또는 `@RestController`를 사용하며, 요청을 처리하는 메서드는 `@RequestMapping`, `@GetMapping`, `@PostMapping` 등의 어노테이션을 사용하여 HTTP 요청을 매핑할 수 있습니다.
|
||||
|
||||
---
|
||||
|
||||
## **1. 컨트롤러 기본 개념**
|
||||
|
||||
컨트롤러는 **MVC(Model-View-Controller) 패턴에서 "C(Controller)" 역할**을 하며, 클라이언트의 요청을 받아서 처리한 후 적절한 뷰(HTML) 또는 데이터를 응답으로 반환합니다.
|
||||
|
||||
### **📌 컨트롤러를 정의하는 어노테이션**
|
||||
| 어노테이션 | 설명 |
|
||||
|------------|----------------------------------|
|
||||
| `@Controller` | 뷰(HTML)를 반환하는 컨트롤러 |
|
||||
| `@RestController` | JSON, XML 데이터를 반환하는 컨트롤러 |
|
||||
| `@RequestMapping` | HTTP 요청을 특정 메서드에 매핑 |
|
||||
| `@GetMapping` | GET 요청을 처리 |
|
||||
| `@PostMapping` | POST 요청을 처리 |
|
||||
| `@PutMapping` | PUT 요청을 처리 |
|
||||
| `@DeleteMapping` | DELETE 요청을 처리 |
|
||||
|
||||
---
|
||||
|
||||
## **2. 기본 컨트롤러 예제**
|
||||
|
||||
```java
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class HomeController {
|
||||
|
||||
@GetMapping("/")
|
||||
public String home(Model model) {
|
||||
model.addAttribute("message", "Hello, Spring Boot!");
|
||||
return "home"; // home.html 템플릿을 반환
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **🛠️ 코드 설명**
|
||||
- `@Controller` → **이 클래스를 컨트롤러로 선언**
|
||||
- `@GetMapping("/")` → **루트 경로(`/`)로 GET 요청이 들어오면 `home()` 메서드 실행**
|
||||
- `Model` 객체를 이용해 `"message"` 데이터를 뷰로 전달
|
||||
- `return "home"` → **`home.html` 템플릿을 사용하여 응답 반환**
|
||||
|
||||
---
|
||||
|
||||
## **3. `@RestController`를 사용한 JSON 응답 컨트롤러**
|
||||
|
||||
```java
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class ApiController {
|
||||
|
||||
@GetMapping("/api/hello")
|
||||
public String hello(@RequestParam(defaultValue = "Guest") String name) {
|
||||
return "Hello, " + name + "!";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **🛠️ 코드 설명**
|
||||
- `@RestController` → **뷰가 아니라 JSON 데이터 응답을 위한 컨트롤러**
|
||||
- `@GetMapping("/api/hello")` → **`/api/hello`로 GET 요청이 들어오면 `hello()` 실행**
|
||||
- `@RequestParam("name")` → **쿼리 파라미터(name)를 받아서 사용**
|
||||
- `return "Hello, " + name + "!";` → **단순한 문자열을 응답으로 반환**
|
||||
|
||||
### **✅ 실행 예시**
|
||||
요청:
|
||||
```
|
||||
GET http://localhost:8080/api/hello?name=John
|
||||
```
|
||||
응답:
|
||||
```json
|
||||
Hello, John!
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **4. `@RequestParam`과 `@PathVariable`의 차이**
|
||||
|
||||
### **1) `@RequestParam`: 쿼리 파라미터 받기**
|
||||
```java
|
||||
@GetMapping("/search")
|
||||
public String search(@RequestParam String keyword) {
|
||||
return "검색어: " + keyword;
|
||||
}
|
||||
```
|
||||
- 요청 예시:
|
||||
```
|
||||
GET /search?keyword=spring
|
||||
```
|
||||
- 응답: `"검색어: spring"`
|
||||
|
||||
---
|
||||
|
||||
### **2) `@PathVariable`: URL 경로 변수 받기**
|
||||
```java
|
||||
@GetMapping("/user/{id}")
|
||||
public String getUser(@PathVariable int id) {
|
||||
return "User ID: " + id;
|
||||
}
|
||||
```
|
||||
- 요청 예시:
|
||||
```
|
||||
GET /user/123
|
||||
```
|
||||
- 응답: `"User ID: 123"`
|
||||
|
||||
---
|
||||
|
||||
## **5. `@PostMapping`을 이용한 데이터 전송 처리**
|
||||
|
||||
### **📌 POST 요청을 이용한 폼 데이터 처리**
|
||||
```java
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
public class UserController {
|
||||
|
||||
@PostMapping("/user")
|
||||
public String createUser(@RequestParam String name, @RequestParam int age) {
|
||||
return "User Created: " + name + ", Age: " + age;
|
||||
}
|
||||
}
|
||||
```
|
||||
- 요청 예시 (POST 요청):
|
||||
```
|
||||
POST /user
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
|
||||
name=John&age=25
|
||||
```
|
||||
- 응답:
|
||||
```
|
||||
User Created: John, Age: 25
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **6. `@RequestBody`를 이용한 JSON 데이터 처리**
|
||||
`@RequestBody`를 사용하면 **JSON 데이터를 Java 객체로 변환**할 수 있습니다.
|
||||
|
||||
```java
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
public class UserController {
|
||||
|
||||
static class User {
|
||||
public String name;
|
||||
public int age;
|
||||
}
|
||||
|
||||
@PostMapping("/user")
|
||||
public String createUser(@RequestBody User user) {
|
||||
return "User Created: " + user.name + ", Age: " + user.age;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **✅ 실행 예시**
|
||||
요청 (POST 요청, JSON 데이터 전달):
|
||||
```json
|
||||
POST /user
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "Alice",
|
||||
"age": 30
|
||||
}
|
||||
```
|
||||
|
||||
응답:
|
||||
```json
|
||||
User Created: Alice, Age: 30
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **7. `@ResponseEntity`를 활용한 응답 제어**
|
||||
`ResponseEntity`를 사용하면 **HTTP 상태 코드와 응답을 함께 제어**할 수 있습니다.
|
||||
|
||||
```java
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
public class ResponseController {
|
||||
|
||||
@GetMapping("/status")
|
||||
public ResponseEntity<String> getStatus() {
|
||||
return ResponseEntity.ok("서버 정상 작동 중");
|
||||
}
|
||||
|
||||
@GetMapping("/error")
|
||||
public ResponseEntity<String> getError() {
|
||||
return ResponseEntity.status(400).body("잘못된 요청입니다.");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **✅ 실행 예시**
|
||||
- 정상 요청:
|
||||
```
|
||||
GET /status
|
||||
```
|
||||
응답: `200 OK`, `"서버 정상 작동 중"`
|
||||
|
||||
- 오류 요청:
|
||||
```
|
||||
GET /error
|
||||
```
|
||||
응답: `400 Bad Request`, `"잘못된 요청입니다."`
|
||||
|
||||
---
|
||||
|
||||
## **8. `@ExceptionHandler`를 활용한 예외 처리**
|
||||
```java
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
public class ExceptionController {
|
||||
|
||||
@GetMapping("/divide")
|
||||
public int divide(@RequestParam int a, @RequestParam int b) {
|
||||
return a / b; // b가 0이면 오류 발생
|
||||
}
|
||||
|
||||
@ExceptionHandler(ArithmeticException.class)
|
||||
public ResponseEntity<String> handleArithmeticException() {
|
||||
return ResponseEntity.status(400).body("0으로 나눌 수 없습니다.");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **✅ 실행 예시**
|
||||
- 정상 요청:
|
||||
```
|
||||
GET /divide?a=10&b=2
|
||||
```
|
||||
응답: `5`
|
||||
|
||||
- 오류 요청 (`b=0`):
|
||||
```
|
||||
GET /divide?a=10&b=0
|
||||
```
|
||||
응답: `400 Bad Request`, `"0으로 나눌 수 없습니다."`
|
||||
|
||||
---
|
||||
|
||||
## **🔍 정리**
|
||||
Spring Boot MVC에서 컨트롤러를 사용하면 **HTTP 요청을 처리하고, 클라이언트에게 적절한 응답을 반환**할 수 있습니다.
|
||||
- `@Controller`: HTML 뷰 반환
|
||||
- `@RestController`: JSON 응답 반환
|
||||
- `@RequestParam`, `@PathVariable`: 요청 데이터 받기
|
||||
- `@PostMapping` + `@RequestBody`: JSON 데이터 처리
|
||||
- `ResponseEntity`: 응답 제어
|
||||
- `@ExceptionHandler`: 예외 처리
|
||||
|
||||
**이제 실무에서도 자신 있게 컨트롤러를 활용해 보세요!**
|
||||
222
docs/_archive/03_뷰 렌더링과 Thymeleaf.md
Normal file
222
docs/_archive/03_뷰 렌더링과 Thymeleaf.md
Normal file
@@ -0,0 +1,222 @@
|
||||
# **Spring Boot의 뷰 렌더링과 Thymeleaf 템플릿 사용법**
|
||||
|
||||
Spring Boot에서는 **Thymeleaf**를 사용하여 **HTML 기반의 동적 웹 페이지를 렌더링**할 수 있습니다.
|
||||
이 글에서는 **뷰 렌더링 개념, Thymeleaf 설정, 기본 문법, 폼 처리, 반복문 및 조건문 활용** 등을 예제와 함께 설명합니다.
|
||||
|
||||
---
|
||||
|
||||
## **1. 뷰(View) 렌더링이란?**
|
||||
|
||||
- 클라이언트가 요청을 보내면, **컨트롤러가 요청을 처리하고 데이터를 모델(Model)에 담아 뷰(View)로 전달**합니다.
|
||||
- 뷰(View)는 **HTML 페이지**로, Thymeleaf 같은 템플릿 엔진을 사용하면 **서버에서 동적으로 HTML을 생성**할 수 있습니다.
|
||||
- Spring Boot에서 대표적인 뷰 템플릿 엔진: **Thymeleaf, JSP, FreeMarker** 등이 있지만, **Thymeleaf가 가장 많이 사용됨**.
|
||||
|
||||
---
|
||||
|
||||
## **2. Thymeleaf 설정하기**
|
||||
|
||||
### **📌 1) Thymeleaf 의존성 추가 (Maven)**
|
||||
`pom.xml`에 다음과 같이 Thymeleaf 의존성을 추가합니다.
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
### **📌 2) Thymeleaf 템플릿 디렉토리**
|
||||
Spring Boot는 기본적으로 **`src/main/resources/templates/`** 경로에서 Thymeleaf 템플릿을 찾습니다.
|
||||
|
||||
```
|
||||
src/
|
||||
├── main/
|
||||
│ ├── java/com.example.demo/
|
||||
│ ├── resources/
|
||||
│ │ ├── templates/ → Thymeleaf HTML 파일 위치
|
||||
│ │ │ ├── index.html
|
||||
│ │ │ ├── user.html
|
||||
│ │ ├── application.properties
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **3. 기본 컨트롤러와 템플릿 렌더링**
|
||||
|
||||
### **📌 1) 컨트롤러 작성**
|
||||
```java
|
||||
@Controller
|
||||
public class HomeController {
|
||||
|
||||
@GetMapping("/")
|
||||
public String home(Model model) {
|
||||
model.addAttribute("message", "Welcome to Thymeleaf!");
|
||||
return "index"; // templates/index.html 파일을 렌더링
|
||||
}
|
||||
}
|
||||
```
|
||||
**설명:**
|
||||
- `@Controller` → HTML을 반환하는 컨트롤러.
|
||||
- `Model` → **뷰로 데이터를 전달하는 객체**.
|
||||
- `model.addAttribute("message", "Welcome to Thymeleaf!")` → `message`라는 데이터를 전달.
|
||||
- `return "index";` → `templates/index.html` 파일을 렌더링.
|
||||
|
||||
---
|
||||
|
||||
### **📌 2) Thymeleaf 템플릿 (index.html)**
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<title>Thymeleaf Example</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1 th:text="${message}">Default Message</h1>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
#### **📌 실행 결과 (`http://localhost:8080/` 요청)**
|
||||
```html
|
||||
<h1>Welcome to Thymeleaf!</h1>
|
||||
```
|
||||
**설명:**
|
||||
- `th:text="${message}"` → `message` 값이 `"Welcome to Thymeleaf!"`로 변경됨.
|
||||
- **템플릿 엔진이 동적으로 HTML을 생성**하여 클라이언트에 응답.
|
||||
|
||||
---
|
||||
|
||||
## **4. 변수 출력 및 표현식**
|
||||
|
||||
### **📌 1) 기본 표현식 (`th:text`)**
|
||||
```html
|
||||
<p th:text="${username}">Default Name</p>
|
||||
```
|
||||
- **`${변수명}`** → `Model`에서 전달된 변수를 표시.
|
||||
|
||||
### **📌 2) 객체의 필드 출력 (`th:text`)**
|
||||
```java
|
||||
class User {
|
||||
private String name;
|
||||
private int age;
|
||||
|
||||
public User(String name, int age) { this.name = name; this.age = age; }
|
||||
public String getName() { return name; }
|
||||
public int getAge() { return age; }
|
||||
}
|
||||
```
|
||||
```java
|
||||
@GetMapping("/user")
|
||||
public String userProfile(Model model) {
|
||||
model.addAttribute("user", new User("Alice", 25));
|
||||
return "user";
|
||||
}
|
||||
```
|
||||
```html
|
||||
<p th:text="${user.name}">Default Name</p>
|
||||
<p th:text="${user.age}">Default Age</p>
|
||||
```
|
||||
**📌 실행 결과 (`/user` 요청)**
|
||||
```html
|
||||
<p>Alice</p>
|
||||
<p>25</p>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **5. 반복문 (`th:each`)**
|
||||
|
||||
### **📌 1) 리스트 반복 (`th:each`)**
|
||||
```java
|
||||
@GetMapping("/users")
|
||||
public String users(Model model) {
|
||||
List<User> userList = Arrays.asList(
|
||||
new User("Alice", 25),
|
||||
new User("Bob", 30),
|
||||
new User("Charlie", 22)
|
||||
);
|
||||
model.addAttribute("users", userList);
|
||||
return "users";
|
||||
}
|
||||
```
|
||||
```html
|
||||
<ul>
|
||||
<li th:each="user : ${users}">
|
||||
<span th:text="${user.name}"></span> - <span th:text="${user.age}"></span>
|
||||
</li>
|
||||
</ul>
|
||||
```
|
||||
#### **📌 실행 결과 (`/users` 요청)**
|
||||
```html
|
||||
<ul>
|
||||
<li>Alice - 25</li>
|
||||
<li>Bob - 30</li>
|
||||
<li>Charlie - 22</li>
|
||||
</ul>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **6. 조건문 (`th:if`, `th:unless`)**
|
||||
|
||||
### **📌 1) 값이 있을 때만 표시 (`th:if`)**
|
||||
```html
|
||||
<p th:if="${user.age >= 18}">Adult</p>
|
||||
```
|
||||
|
||||
### **📌 2) 값이 없을 때 표시 (`th:unless`)**
|
||||
```html
|
||||
<p th:unless="${user.age >= 18}">Minor</p>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **7. 폼 처리 (`@PostMapping`)**
|
||||
|
||||
### **📌 1) 컨트롤러에서 폼 데이터 받기**
|
||||
```java
|
||||
@Controller
|
||||
@RequestMapping("/form")
|
||||
public class FormController {
|
||||
|
||||
@GetMapping
|
||||
public String showForm(Model model) {
|
||||
model.addAttribute("user", new User("", 0));
|
||||
return "form";
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String submitForm(@ModelAttribute User user, Model model) {
|
||||
model.addAttribute("submittedUser", user);
|
||||
return "result";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### **📌 2) Thymeleaf 폼 (form.html)**
|
||||
```html
|
||||
<form action="#" th:action="@{/form}" th:object="${user}" method="post">
|
||||
Name: <input type="text" th:field="*{name}" />
|
||||
Age: <input type="number" th:field="*{age}" />
|
||||
<button type="submit">Submit</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
### **📌 3) 결과 페이지 (result.html)**
|
||||
```html
|
||||
<p>Name: <span th:text="${submittedUser.name}"></span></p>
|
||||
<p>Age: <span th:text="${submittedUser.age}"></span></p>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **8. 정리**
|
||||
| 기능 | Thymeleaf 문법 | 설명 |
|
||||
|------|--------------|------|
|
||||
| **텍스트 출력** | `th:text="${변수}"` | 변수 값을 출력 |
|
||||
| **객체 속성 출력** | `th:text="${객체.필드}"` | 객체의 필드 값을 출력 |
|
||||
| **반복문** | `th:each="item : ${리스트}"` | 리스트 반복 출력 |
|
||||
| **조건문** | `th:if`, `th:unless` | 조건부 렌더링 |
|
||||
| **폼 바인딩** | `th:object` + `th:field` | HTML 폼과 객체 바인딩 |
|
||||
| **URL 매핑** | `th:action="@{경로}"` | 폼 요청 경로 설정 |
|
||||
|
||||
Thymeleaf는 **Spring Boot와 강력한 통합을 제공**하여, 동적 웹 페이지를 쉽게 렌더링할 수 있습니다.
|
||||
**특히, HTML 파일을 유지하면서 동적 데이터를 추가할 수 있어 유지보수성이 뛰어납니다.**
|
||||
213
docs/_archive/04_서비스.md
Normal file
213
docs/_archive/04_서비스.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# **서비스 계층과 비즈니스 로직**
|
||||
|
||||
Spring Boot 애플리케이션을 설계할 때, 코드의 **재사용성, 유지보수성, 테스트 용이성**을 높이려면 **비즈니스 로직을 서비스 계층(Service Layer)에 분리**하는 것이 중요합니다.
|
||||
이 글에서는 **서비스 계층의 역할, 설계 방식, 그리고 예제 코드**를 통해 비즈니스 로직을 어떻게 다루어야 하는지 설명합니다.
|
||||
|
||||
---
|
||||
|
||||
## **1. 서비스 계층이란?**
|
||||
Spring Boot 애플리케이션은 일반적으로 **MVC (Model-View-Controller) 구조**를 따릅니다.
|
||||
이때 **서비스 계층은 컨트롤러와 데이터 계층(Repository) 사이에서 비즈니스 로직을 처리하는 역할**을 합니다.
|
||||
|
||||
### **📌 계층별 역할**
|
||||
| 계층 | 역할 |
|
||||
|------|------|
|
||||
| **Controller (컨트롤러 계층)** | 사용자 요청을 받아 서비스 계층에 전달 |
|
||||
| **Service (서비스 계층)** | 비즈니스 로직을 처리하고 트랜잭션을 관리 |
|
||||
| **Repository (데이터 계층)** | 데이터베이스와 직접적인 통신을 담당 |
|
||||
|
||||
---
|
||||
|
||||
## **2. 서비스 계층의 필요성**
|
||||
|
||||
### **📌 컨트롤러에서 직접 로직을 처리하는 문제점**
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/users")
|
||||
public class UserController {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository; // 데이터 계층 직접 접근
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<User> createUser(@RequestBody User user) {
|
||||
if (user.getName() == null || user.getAge() < 0) {
|
||||
throw new IllegalArgumentException("Invalid user data");
|
||||
}
|
||||
return ResponseEntity.ok(userRepository.save(user)); // 비즈니스 로직이 컨트롤러에 있음
|
||||
}
|
||||
}
|
||||
```
|
||||
**🚨 문제점:**
|
||||
1. **컨트롤러가 너무 많은 책임을 가짐** → 가독성과 유지보수성이 떨어짐.
|
||||
2. **비즈니스 로직이 중복될 가능성 증가** → 여러 컨트롤러에서 같은 로직을 작성할 가능성이 큼.
|
||||
3. **테스트가 어려움** → 서비스 계층이 없으면 컨트롤러 단위 테스트가 복잡해짐.
|
||||
|
||||
---
|
||||
|
||||
## **3. 서비스 계층을 활용한 개선된 설계**
|
||||
|
||||
### **📌 1) Entity (데이터 모델)**
|
||||
```java
|
||||
@Entity
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
private String name;
|
||||
private int age;
|
||||
|
||||
// 기본 생성자 및 getter, setter
|
||||
public User() {}
|
||||
|
||||
public User(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
public String getName() { return name; }
|
||||
public int getAge() { return age; }
|
||||
}
|
||||
```
|
||||
|
||||
### **📌 2) Repository (데이터 계층)**
|
||||
```java
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
}
|
||||
```
|
||||
**설명:**
|
||||
- `JpaRepository<User, Long>` → `User` 엔티티를 다루는 JPA 리포지토리.
|
||||
- `findById(id)`, `save(entity)`, `deleteById(id)` 등 기본적인 DB 연산 제공.
|
||||
|
||||
---
|
||||
|
||||
### **📌 3) Service (비즈니스 로직 계층)**
|
||||
```java
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Transactional
|
||||
public User createUser(User user) {
|
||||
if (user.getName() == null || user.getAge() < 0) {
|
||||
throw new IllegalArgumentException("Invalid user data");
|
||||
}
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
public User getUserById(Long id) {
|
||||
return userRepository.findById(id)
|
||||
.orElseThrow(() -> new RuntimeException("User not found"));
|
||||
}
|
||||
}
|
||||
```
|
||||
**🚀 개선된 점:**
|
||||
✔ **컨트롤러에서 비즈니스 로직이 제거됨** → 역할이 명확해짐.
|
||||
✔ **비즈니스 로직이 서비스 계층에 집중됨** → 재사용성 증가.
|
||||
✔ **트랜잭션 관리 가능** → `@Transactional`을 활용하여 데이터 일관성 유지.
|
||||
|
||||
---
|
||||
|
||||
### **📌 4) Controller (컨트롤러 계층)**
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/users")
|
||||
public class UserController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<User> createUser(@RequestBody User user) {
|
||||
return ResponseEntity.ok(userService.createUser(user));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<User> getUser(@PathVariable Long id) {
|
||||
return ResponseEntity.ok(userService.getUserById(id));
|
||||
}
|
||||
}
|
||||
```
|
||||
**🚀 컨트롤러가 깔끔해짐!**
|
||||
✔ 비즈니스 로직을 서비스 계층에 위임.
|
||||
✔ 컨트롤러는 단순히 요청을 받고 응답을 반환하는 역할만 수행.
|
||||
|
||||
---
|
||||
|
||||
## **4. 서비스 계층에서 트랜잭션 관리**
|
||||
|
||||
### **📌 트랜잭션 (`@Transactional`)이란?**
|
||||
- 여러 개의 DB 연산을 하나의 단위로 묶어 **"모두 성공하거나, 하나라도 실패하면 롤백"**하도록 보장.
|
||||
- `@Transactional`을 붙이면, **예외가 발생할 경우 자동으로 롤백**됨.
|
||||
|
||||
### **📌 예제: 트랜잭션을 적용한 서비스**
|
||||
```java
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Transactional
|
||||
public void registerUsers(List<User> users) {
|
||||
for (User user : users) {
|
||||
if (user.getAge() < 0) {
|
||||
throw new RuntimeException("Invalid age");
|
||||
}
|
||||
userRepository.save(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
**🚀 특징:**
|
||||
- 만약 `user.getAge() < 0`인 사용자가 있다면, **이전까지 저장된 사용자도 롤백됨**.
|
||||
- 데이터 무결성을 보장할 수 있음.
|
||||
|
||||
---
|
||||
|
||||
## **5. 서비스 계층을 활용한 테스트**
|
||||
|
||||
### **📌 JUnit을 활용한 서비스 계층 테스트**
|
||||
```java
|
||||
@SpringBootTest
|
||||
@RunWith(SpringRunner.class)
|
||||
public class UserServiceTest {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Test
|
||||
public void testCreateUser() {
|
||||
User user = new User("Alice", 25);
|
||||
User savedUser = userService.createUser(user);
|
||||
|
||||
assertNotNull(savedUser);
|
||||
assertEquals("Alice", savedUser.getName());
|
||||
}
|
||||
}
|
||||
```
|
||||
**🚀 서비스 계층이 분리되어 있어, 테스트가 쉬워짐!**
|
||||
✔ `UserService`만 단독으로 테스트 가능.
|
||||
✔ `UserRepository`가 직접 드러나지 않으므로, Mocking(가짜 데이터)도 쉽게 적용 가능.
|
||||
|
||||
---
|
||||
|
||||
## **6. 정리**
|
||||
|
||||
| 계층 | 설명 |
|
||||
|------|------|
|
||||
| **Controller** | 요청을 받고 응답을 반환 (비즈니스 로직 X) |
|
||||
| **Service** | 비즈니스 로직 처리 및 트랜잭션 관리 |
|
||||
| **Repository** | 데이터베이스와 직접 통신 |
|
||||
|
||||
### **✅ 좋은 서비스 계층 설계 원칙**
|
||||
✔ **컨트롤러는 서비스에 로직을 위임**하고, 직접 처리하지 않음.
|
||||
✔ **비즈니스 로직은 서비스 계층에서 일괄적으로 관리**하여 재사용성을 높임.
|
||||
✔ **트랜잭션 관리 (`@Transactional`)을 서비스 계층에서 수행**하여 데이터 무결성을 보장.
|
||||
✔ **테스트가 용이하도록 서비스 계층을 단독으로 분리**.
|
||||
|
||||
서비스 계층을 잘 활용하면, **더 유지보수하기 좋은 코드**를 만들 수 있습니다.
|
||||
213
docs/_archive/05_데이터 처리와 JPA.md
Normal file
213
docs/_archive/05_데이터 처리와 JPA.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# **데이터 처리와 JPA**
|
||||
|
||||
웹 애플리케이션에서 **데이터 처리(Data Processing)**는 필수적인 요소이며, 이를 효율적으로 다루기 위해 ORM 프레임워크인 **JPA (Java Persistence API)**가 널리 사용됩니다.
|
||||
이번 글에서는 **JPA의 기본 개념, 주요 기능, 실무 적용 방법**을 살펴보겠습니다.
|
||||
|
||||
---
|
||||
|
||||
## **1. 데이터 처리란?**
|
||||
|
||||
데이터 처리는 애플리케이션이 데이터를 **생성(Create), 읽기(Read), 수정(Update), 삭제(Delete)**하는 과정(**CRUD**)을 의미합니다.
|
||||
Spring Boot에서는 **JPA와 Spring Data JPA**를 활용하여 효율적인 데이터 처리를 구현할 수 있습니다.
|
||||
|
||||
**📌 전통적인 JDBC 방식과 JPA 방식 비교**
|
||||
|
||||
| 방식 | 특징 | 코드 복잡도 | 유지보수성 |
|
||||
|------|------|------------|------------|
|
||||
| **JDBC (기본 SQL 사용)** | SQL 쿼리를 직접 작성 | 높음 | 낮음 |
|
||||
| **JPA (ORM 방식)** | 객체 중심의 데이터 처리 | 낮음 | 높음 |
|
||||
|
||||
---
|
||||
|
||||
## **2. JPA란?**
|
||||
|
||||
**JPA (Java Persistence API)**는 객체지향적인 방식으로 데이터베이스를 다룰 수 있도록 도와주는 **ORM(Object-Relational Mapping) 기술**입니다.
|
||||
JPA를 사용하면 SQL을 직접 작성하지 않고, **엔티티(Entity) 객체를 이용하여 데이터베이스를 조작**할 수 있습니다.
|
||||
|
||||
### **📌 JPA의 핵심 개념**
|
||||
1. **엔티티(Entity):** 데이터베이스 테이블과 매핑되는 클래스
|
||||
2. **리포지토리(Repository):** 데이터 저장 및 조회를 담당하는 계층
|
||||
3. **트랜잭션(Transaction):** 데이터 변경 작업을 하나의 단위로 처리
|
||||
4. **JPQL(Java Persistence Query Language):** 객체를 대상으로 하는 쿼리
|
||||
|
||||
---
|
||||
|
||||
## **3. JPA 실무 적용**
|
||||
|
||||
### **📌 1) Entity (데이터 모델 정의)**
|
||||
JPA에서는 `@Entity` 어노테이션을 사용하여 **클래스를 데이터베이스 테이블과 매핑**합니다.
|
||||
|
||||
```java
|
||||
import jakarta.persistence.*;
|
||||
|
||||
@Entity // User 엔티티는 DB의 users 테이블과 매핑됨
|
||||
@Table(name = "users")
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY) // 자동 증가 ID
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false) // name 컬럼은 null을 허용하지 않음
|
||||
private String name;
|
||||
|
||||
@Column(nullable = false)
|
||||
private int age;
|
||||
|
||||
public User() {}
|
||||
|
||||
public User(String name, int age) {
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
// Getter & Setter 생략
|
||||
}
|
||||
```
|
||||
✔ **`@Entity`**: 해당 클래스가 데이터베이스 테이블과 연결됨
|
||||
✔ **`@Table(name = "users")`**: 테이블명을 `users`로 지정
|
||||
✔ **`@Id`**: 기본 키(Primary Key) 설정
|
||||
✔ **`@GeneratedValue(strategy = GenerationType.IDENTITY)`**: 자동 증가(AUTO_INCREMENT) 설정
|
||||
✔ **`@Column(nullable = false)`**: `null`을 허용하지 않도록 설정
|
||||
|
||||
---
|
||||
|
||||
### **📌 2) Repository (데이터 접근 계층)**
|
||||
JPA에서는 **Spring Data JPA**의 `JpaRepository` 인터페이스를 사용하면, 기본적인 CRUD 기능을 자동으로 구현할 수 있습니다.
|
||||
|
||||
```java
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
// 기본 CRUD 기능 제공
|
||||
}
|
||||
```
|
||||
✔ **`JpaRepository<User, Long>`**: `User` 엔티티를 관리하며, 기본 키 타입은 `Long`
|
||||
✔ `findById(id)`, `save(entity)`, `deleteById(id)` 등 기본적인 데이터 처리 메서드 제공
|
||||
|
||||
---
|
||||
|
||||
### **📌 3) Service (비즈니스 로직 계층)**
|
||||
데이터 처리 로직을 서비스 계층에서 구현하여 컨트롤러와 분리합니다.
|
||||
|
||||
```java
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Transactional
|
||||
public User createUser(String name, int age) {
|
||||
if (age < 0) {
|
||||
throw new IllegalArgumentException("나이는 0 이상이어야 합니다.");
|
||||
}
|
||||
return userRepository.save(new User(name, age));
|
||||
}
|
||||
|
||||
public List<User> getAllUsers() {
|
||||
return userRepository.findAll();
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ **서비스 계층을 사용하여 비즈니스 로직을 분리**
|
||||
✔ **`@Transactional`을 사용하여 트랜잭션 관리**
|
||||
|
||||
---
|
||||
|
||||
### **📌 4) Controller (요청 처리 계층)**
|
||||
사용자의 요청을 받아 서비스 계층을 호출하고 응답을 반환합니다.
|
||||
|
||||
```java
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/users")
|
||||
public class UserController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<User> createUser(@RequestParam String name, @RequestParam int age) {
|
||||
return ResponseEntity.ok(userService.createUser(name, age));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public ResponseEntity<List<User>> getAllUsers() {
|
||||
return ResponseEntity.ok(userService.getAllUsers());
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ **`@RestController`**: JSON 형식으로 데이터를 반환하는 컨트롤러
|
||||
✔ **`@RequestMapping("/users")`**: `/users` 경로의 API를 처리
|
||||
✔ **`@PostMapping`**: 새로운 사용자 생성
|
||||
✔ **`@GetMapping`**: 모든 사용자 조회
|
||||
|
||||
---
|
||||
|
||||
## **4. JPA에서 데이터 조회 (JPQL & Native Query)**
|
||||
|
||||
### **📌 1) JPQL (Java Persistence Query Language)**
|
||||
JPA에서는 SQL 대신 **JPQL**을 사용하여 객체 중심의 쿼리를 작성할 수 있습니다.
|
||||
|
||||
```java
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import java.util.List;
|
||||
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
|
||||
// 이름으로 사용자 찾기
|
||||
@Query("SELECT u FROM User u WHERE u.name = :name")
|
||||
List<User> findByName(@Param("name") String name);
|
||||
}
|
||||
```
|
||||
✔ SQL과 유사하지만, **테이블명이 아니라 엔티티 클래스명을 사용**
|
||||
|
||||
---
|
||||
|
||||
### **📌 2) Native Query (SQL 직접 사용)**
|
||||
기본 SQL 쿼리를 직접 사용할 수도 있습니다.
|
||||
|
||||
```java
|
||||
@Query(value = "SELECT * FROM users WHERE age >= :age", nativeQuery = true)
|
||||
List<User> findUsersByAge(@Param("age") int age);
|
||||
```
|
||||
✔ 복잡한 SQL 쿼리를 그대로 활용 가능
|
||||
|
||||
---
|
||||
|
||||
## **5. JPA의 장점과 단점**
|
||||
|
||||
### **📌 JPA의 장점**
|
||||
✔ **SQL을 직접 작성할 필요 없음** → 생산성 증가
|
||||
✔ **객체지향적인 데이터 처리 가능** → 코드의 가독성과 유지보수성 향상
|
||||
✔ **트랜잭션 관리가 용이함**
|
||||
✔ **캐싱 및 성능 최적화 기능 제공**
|
||||
|
||||
### **📌 JPA의 단점**
|
||||
❌ 초기 학습 비용이 존재
|
||||
❌ 복잡한 SQL 튜닝이 필요한 경우 SQL보다 불리할 수 있음
|
||||
❌ 데이터베이스 변경 시, 마이그레이션이 필요
|
||||
|
||||
---
|
||||
|
||||
## **6. 정리**
|
||||
✔ **JPA는 객체지향 방식으로 데이터를 처리하는 ORM 기술**
|
||||
✔ **Entity, Repository, Service, Controller 구조로 데이터 처리를 설계**
|
||||
✔ **JPQL 및 Native Query를 활용하여 데이터 조회 가능**
|
||||
✔ **트랜잭션을 활용하여 데이터 일관성을 유지**
|
||||
|
||||
JPA를 활용하면 **효율적인 데이터 처리와 유지보수성 높은 애플리케이션을 만들 수 있습니다!**
|
||||
184
docs/_archive/06_필터, 인터셉터, AOP.md
Normal file
184
docs/_archive/06_필터, 인터셉터, AOP.md
Normal file
@@ -0,0 +1,184 @@
|
||||
# **Spring Boot 요청 처리 고급 기능: 필터, 인터셉터, AOP 활용**
|
||||
|
||||
Spring Boot에서 클라이언트의 **요청을 처리하는 과정**은 단순히 컨트롤러에서 요청을 받고 응답을 반환하는 것 이상으로 확장될 수 있습니다.
|
||||
특히 **보안, 로깅, 성능 모니터링, 인증/인가**와 같은 공통 기능을 처리하려면 **필터(Filter), 인터셉터(Interceptor), AOP(Aspect-Oriented Programming)** 등의 개념을 활용할 필요가 있습니다.
|
||||
|
||||
이번 글에서는 **필터, 인터셉터, AOP**를 활용하여 **요청을 가로채고, 원하는 로직을 추가하는 방법**을 실무 예제와 함께 설명하겠습니다.
|
||||
|
||||
---
|
||||
|
||||
# **1. 필터(Filter) - 요청 전/후 공통 처리**
|
||||
|
||||
### **📌 필터란?**
|
||||
필터는 **서블릿 수준에서 동작하며, 요청이 컨트롤러에 도달하기 전/후에 특정 로직을 실행**할 수 있는 기능입니다.
|
||||
Spring Boot에서는 `javax.servlet.Filter` 인터페이스를 구현하여 커스텀 필터를 만들 수 있습니다.
|
||||
|
||||
### **📌 필터의 주요 활용 사례**
|
||||
- **CORS 처리**
|
||||
- **요청 및 응답 로깅**
|
||||
- **JWT 인증 처리**
|
||||
- **IP 차단 등 보안 정책 적용**
|
||||
|
||||
### **📌 필터 구현 예제 - 요청 로깅**
|
||||
아래는 **모든 요청의 URL과 실행 시간을 기록하는 필터**입니다.
|
||||
|
||||
```java
|
||||
import jakarta.servlet.*;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Component // 자동으로 필터 등록
|
||||
public class LoggingFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
|
||||
throws IOException, ServletException {
|
||||
HttpServletRequest req = (HttpServletRequest) request;
|
||||
long startTime = System.currentTimeMillis();
|
||||
|
||||
System.out.println("[LoggingFilter] 요청 URI: " + req.getRequestURI());
|
||||
|
||||
chain.doFilter(request, response); // 요청을 다음 필터 또는 컨트롤러로 전달
|
||||
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
System.out.println("[LoggingFilter] 요청 처리 시간: " + duration + "ms");
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `doFilter()` 메서드는 **요청을 가로채서 원하는 작업을 수행한 후, 체인(chain)으로 넘겨줌**
|
||||
✔ 요청 전후로 실행할 로직을 자유롭게 추가 가능
|
||||
|
||||
---
|
||||
|
||||
# **2. 인터셉터(Interceptor) - 요청 흐름을 제어**
|
||||
|
||||
### **📌 인터셉터란?**
|
||||
인터셉터는 **Spring MVC에서 동작하며, 컨트롤러 실행 전후에 특정 로직을 추가할 수 있는 기능**입니다.
|
||||
필터보다 **더 세부적인 요청 흐름을 제어**할 수 있으며, **특정 컨트롤러 또는 요청 경로에만 적용 가능**합니다.
|
||||
|
||||
### **📌 인터셉터의 주요 활용 사례**
|
||||
- **사용자 인증 및 권한 체크**
|
||||
- **API 요청 제한 (Rate Limiting)**
|
||||
- **로그 및 성능 모니터링**
|
||||
- **특정 요청의 파라미터 검사**
|
||||
|
||||
### **📌 인터셉터 구현 예제 - 인증 체크**
|
||||
아래는 **특정 API에 접근할 때, 인증된 사용자만 허용하는 인터셉터**입니다.
|
||||
|
||||
```java
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class AuthInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
|
||||
throws Exception {
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
|
||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.getWriter().write("Unauthorized");
|
||||
return false; // 요청을 차단
|
||||
}
|
||||
|
||||
// JWT 검증 로직 추가 가능 (예: Token 검증)
|
||||
System.out.println("[AuthInterceptor] 인증 성공");
|
||||
return true; // 요청 진행 허용
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `preHandle()` 메서드는 **컨트롤러 실행 전에 요청을 가로채서 검사 가능**
|
||||
✔ 인증이 실패하면 `false`를 반환하여 **요청을 차단할 수 있음**
|
||||
|
||||
### **📌 인터셉터 등록 (WebMvcConfigurer)**
|
||||
인터셉터를 사용하려면 **WebMvcConfigurer**에 등록해야 합니다.
|
||||
|
||||
```java
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
private AuthInterceptor authInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(authInterceptor)
|
||||
.addPathPatterns("/api/**") // 특정 URL에만 적용
|
||||
.excludePathPatterns("/api/public/**"); // 예외 URL 설정
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# **3. AOP (Aspect-Oriented Programming) - 횡단 관심사 처리**
|
||||
|
||||
### **📌 AOP란?**
|
||||
AOP는 **비즈니스 로직과는 별개로 공통 기능(로깅, 트랜잭션, 보안 등)을 적용하는 프로그래밍 기법**입니다.
|
||||
Spring Boot에서는 `@Aspect`와 `@Around`를 활용하여 **메서드 실행 전후에 특정 로직을 추가**할 수 있습니다.
|
||||
|
||||
### **📌 AOP의 주요 활용 사례**
|
||||
- **로깅(Log Tracing)**
|
||||
- **트랜잭션 관리**
|
||||
- **메서드 실행 시간 측정**
|
||||
- **입출력 값 검증**
|
||||
|
||||
### **📌 AOP 구현 예제 - 로깅 및 실행 시간 측정**
|
||||
아래는 **모든 서비스 메서드의 실행 시간을 로깅하는 AOP**입니다.
|
||||
|
||||
```java
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Aspect
|
||||
@Component
|
||||
public class LoggingAspect {
|
||||
|
||||
@Around("execution(* com.example.service.*.*(..))") // 모든 서비스 메서드에 적용
|
||||
public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
long start = System.currentTimeMillis();
|
||||
|
||||
Object result = joinPoint.proceed(); // 실제 메서드 실행
|
||||
|
||||
long duration = System.currentTimeMillis() - start;
|
||||
System.out.println("[AOP] " + joinPoint.getSignature() + " 실행 시간: " + duration + "ms");
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `@Aspect` 선언으로 AOP 활성화
|
||||
✔ `@Around("execution(* com.example.service.*.*(..))")`로 **특정 패키지의 모든 메서드에 적용 가능**
|
||||
✔ `joinPoint.proceed()`를 호출하여 **실제 메서드를 실행하고, 이후 로직을 추가할 수 있음**
|
||||
|
||||
---
|
||||
|
||||
# **4. 필터, 인터셉터, AOP 비교**
|
||||
|
||||
| 기능 | 동작 위치 | 주요 목적 | 적용 대상 | 실행 시점 |
|
||||
|------|---------|---------|---------|---------|
|
||||
| **Filter** | 서블릿 레벨 | 요청 전처리 및 후처리 (보안, 로깅) | 모든 요청 | 컨트롤러 실행 전후 |
|
||||
| **Interceptor** | Spring MVC 레벨 | 인증, 권한 체크 | 특정 요청 (API) | 컨트롤러 실행 전후 |
|
||||
| **AOP** | 메서드 레벨 | 로깅, 트랜잭션, 성능 측정 | 특정 패키지/클래스의 메서드 | 메서드 실행 전후 |
|
||||
|
||||
---
|
||||
|
||||
# **5. 정리**
|
||||
✔ **필터(Filter)**: **모든 요청에 대해 전역적인 로직 적용 (보안, CORS, 로깅)**
|
||||
✔ **인터셉터(Interceptor)**: **컨트롤러 실행 전후에 특정 요청을 가로채서 인증/인가 처리**
|
||||
✔ **AOP(Aspect-Oriented Programming)**: **특정 메서드(비즈니스 로직)의 실행 전후에 로직 추가 (로깅, 성능 측정 등)**
|
||||
|
||||
이러한 고급 기능을 활용하면 **Spring Boot 애플리케이션을 더욱 강력하고 유지보수하기 쉽게 만들 수 있습니다!**
|
||||
226
docs/_archive/07_세션과 쿠키.md
Normal file
226
docs/_archive/07_세션과 쿠키.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# **Spring Boot: 세션 & 쿠키, 파일 업로드 & 다운로드**
|
||||
|
||||
Spring Boot 애플리케이션에서 **세션과 쿠키**는 사용자 상태를 관리하는 데 유용하며, **파일 업로드 및 다운로드** 기능은 다양한 애플리케이션에서 필수적인 기능입니다.
|
||||
이번 글에서는 **세션과 쿠키의 개념 및 사용법**을 살펴보고, **파일 업로드 및 다운로드를 처리하는 방법**을 예제와 함께 설명하겠습니다.
|
||||
|
||||
---
|
||||
|
||||
# **1. 세션(Session)과 쿠키(Cookie)**
|
||||
|
||||
### **📌 세션(Session)이란?**
|
||||
- **서버 측에서 사용자 상태를 유지하는 기술**
|
||||
- **각 사용자에게 고유한 세션 ID(Session ID)를 부여하고, 서버에서 해당 사용자의 데이터를 저장**
|
||||
- 로그인 정보, 장바구니 데이터 등 **사용자의 지속적인 상태 유지**가 필요한 경우 사용
|
||||
|
||||
### **📌 쿠키(Cookie)란?**
|
||||
- **클라이언트(브라우저)에 저장되는 작은 데이터 조각**
|
||||
- 서버에서 응답 시 쿠키를 설정하면, 클라이언트는 이후 요청에서 해당 쿠키를 함께 전송
|
||||
- **세션과 달리 서버가 아닌 클라이언트에 저장됨**
|
||||
- 로그인 유지, 사이트 설정 저장 등에 활용
|
||||
|
||||
---
|
||||
|
||||
## **1.1 세션(Session) 사용하기**
|
||||
|
||||
### **✅ 세션 저장 및 조회 예제**
|
||||
Spring Boot에서는 `HttpSession`을 사용하여 **세션 데이터를 저장 및 조회**할 수 있습니다.
|
||||
|
||||
```java
|
||||
import jakarta.servlet.http.HttpSession;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/session")
|
||||
public class SessionController {
|
||||
|
||||
@PostMapping("/set")
|
||||
public String setSession(HttpSession session) {
|
||||
session.setAttribute("username", "JohnDoe"); // 세션에 저장
|
||||
return "세션에 username=JohnDoe 저장 완료";
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
public String getSession(HttpSession session) {
|
||||
String username = (String) session.getAttribute("username");
|
||||
return username != null ? "세션 값: " + username : "세션 값이 없습니다.";
|
||||
}
|
||||
|
||||
@PostMapping("/remove")
|
||||
public String removeSession(HttpSession session) {
|
||||
session.invalidate(); // 세션 삭제
|
||||
return "세션 삭제 완료";
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `setSession()`: `"username"` 값을 세션에 저장
|
||||
✔ `getSession()`: 저장된 세션 값을 조회
|
||||
✔ `removeSession()`: 세션 데이터를 삭제
|
||||
|
||||
---
|
||||
|
||||
## **1.2 쿠키(Cookie) 사용하기**
|
||||
|
||||
### **✅ 쿠키 설정 및 조회 예제**
|
||||
Spring Boot에서는 `HttpServletResponse`와 `HttpServletRequest`를 사용하여 쿠키를 설정하고 조회할 수 있습니다.
|
||||
|
||||
```java
|
||||
import jakarta.servlet.http.Cookie;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/cookie")
|
||||
public class CookieController {
|
||||
|
||||
@PostMapping("/set")
|
||||
public String setCookie(HttpServletResponse response) {
|
||||
Cookie cookie = new Cookie("userId", "12345");
|
||||
cookie.setMaxAge(60 * 60); // 1시간 동안 유지
|
||||
cookie.setPath("/"); // 모든 경로에서 유효
|
||||
response.addCookie(cookie);
|
||||
return "쿠키 설정 완료: userId=12345";
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
public String getCookie(HttpServletRequest request) {
|
||||
Cookie[] cookies = request.getCookies();
|
||||
if (cookies != null) {
|
||||
for (Cookie cookie : cookies) {
|
||||
if ("userId".equals(cookie.getName())) {
|
||||
return "쿠키 값: " + cookie.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
return "쿠키가 없습니다.";
|
||||
}
|
||||
|
||||
@PostMapping("/delete")
|
||||
public String deleteCookie(HttpServletResponse response) {
|
||||
Cookie cookie = new Cookie("userId", null);
|
||||
cookie.setMaxAge(0); // 즉시 삭제
|
||||
cookie.setPath("/");
|
||||
response.addCookie(cookie);
|
||||
return "쿠키 삭제 완료";
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `setCookie()`: `"userId"` 쿠키를 설정
|
||||
✔ `getCookie()`: 저장된 쿠키 값을 조회
|
||||
✔ `deleteCookie()`: `"userId"` 쿠키를 삭제
|
||||
|
||||
---
|
||||
|
||||
# **2. 파일 업로드 및 다운로드**
|
||||
|
||||
Spring Boot에서는 `MultipartFile`을 사용하여 **파일 업로드 및 다운로드**를 간편하게 처리할 수 있습니다.
|
||||
|
||||
---
|
||||
|
||||
## **2.1 파일 업로드 처리**
|
||||
|
||||
### **✅ 파일 업로드 예제**
|
||||
아래는 **Spring Boot에서 파일을 업로드하여 로컬 저장소에 저장하는 코드**입니다.
|
||||
|
||||
```java
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/file")
|
||||
public class FileUploadController {
|
||||
|
||||
private static final String UPLOAD_DIR = "uploads/";
|
||||
|
||||
@PostMapping("/upload")
|
||||
public String uploadFile(@RequestParam("file") MultipartFile file) {
|
||||
try {
|
||||
File uploadDir = new File(UPLOAD_DIR);
|
||||
if (!uploadDir.exists()) {
|
||||
uploadDir.mkdirs(); // 디렉토리 생성
|
||||
}
|
||||
String filePath = UPLOAD_DIR + file.getOriginalFilename();
|
||||
file.transferTo(new File(filePath)); // 파일 저장
|
||||
return "파일 업로드 성공: " + filePath;
|
||||
} catch (IOException e) {
|
||||
return "파일 업로드 실패: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `MultipartFile`을 받아서 `uploads/` 디렉토리에 저장
|
||||
✔ `file.transferTo(new File(filePath))`로 실제 파일을 저장
|
||||
✔ `@RequestParam("file")`로 HTML 폼에서 파일을 받을 수 있음
|
||||
|
||||
### **✅ HTML 파일 업로드 폼 예제**
|
||||
```html
|
||||
<form action="/file/upload" method="post" enctype="multipart/form-data">
|
||||
<input type="file" name="file" />
|
||||
<button type="submit">파일 업로드</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **2.2 파일 다운로드 처리**
|
||||
|
||||
### **✅ 파일 다운로드 예제**
|
||||
Spring Boot에서는 `ResponseEntity`를 사용하여 **파일을 다운로드할 수 있습니다**.
|
||||
|
||||
```java
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.UrlResource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/file")
|
||||
public class FileDownloadController {
|
||||
|
||||
private static final String UPLOAD_DIR = "uploads/";
|
||||
|
||||
@GetMapping("/download/{filename}")
|
||||
public ResponseEntity<Resource> downloadFile(@PathVariable String filename) {
|
||||
try {
|
||||
Path filePath = Paths.get(UPLOAD_DIR).resolve(filename).normalize();
|
||||
Resource resource = new UrlResource(filePath.toUri());
|
||||
|
||||
if (!resource.exists()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok()
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
|
||||
.body(resource);
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `UrlResource`를 이용하여 파일을 읽어옴
|
||||
✔ `CONTENT_DISPOSITION`을 설정하여 **다운로드 가능한 형태로 응답**
|
||||
✔ `/file/download/{filename}` 경로로 요청하면 해당 파일을 다운로드 가능
|
||||
|
||||
### **✅ 파일 다운로드 요청 예제**
|
||||
```html
|
||||
<a href="/file/download/sample.txt">파일 다운로드</a>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# **3. 정리**
|
||||
|
||||
✔ **세션(Session)**: 서버 측에서 사용자 정보를 관리 (로그인 상태 유지 등)
|
||||
✔ **쿠키(Cookie)**: 클라이언트 측에 저장되며, 서버와의 요청에서 사용 가능
|
||||
✔ **파일 업로드**: `MultipartFile`을 이용하여 서버에 파일 저장
|
||||
✔ **파일 다운로드**: `ResponseEntity<Resource>`를 활용하여 파일 제공
|
||||
|
||||
Spring Boot에서 **세션과 쿠키를 활용하여 사용자 상태를 관리**하고, **파일 업로드 및 다운로드를 구현하는 방법**을 익히면 다양한 웹 애플리케이션에서 활용할 수 있습니다!
|
||||
326
docs/_archive/core.md
Normal file
326
docs/_archive/core.md
Normal file
@@ -0,0 +1,326 @@
|
||||
# Spring Boot Core 어노테이션 정리
|
||||
|
||||
Spring Boot Core에서 사용되는 주요 어노테이션을 표로 정리한 후, 각각의 어노테이션에 대한 설명과 예제를 제공합니다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 어노테이션 정리표
|
||||
|
||||
| 어노테이션 | 설명 |
|
||||
|------------------------|----------------------------------|
|
||||
| `@SpringBootApplication` | Spring Boot 애플리케이션의 시작점 설정 |
|
||||
| `@Configuration` | 스프링 설정 클래스를 정의 |
|
||||
| `@ComponentScan` | 스프링이 컴포넌트를 자동 검색하도록 설정 |
|
||||
| `@Bean` | 수동으로 빈을 등록 |
|
||||
| `@Component` | 일반적인 빈을 정의 |
|
||||
| `@Service` | 비즈니스 로직을 담당하는 빈을 정의 |
|
||||
| `@Repository` | 데이터 접근 계층 빈을 정의 |
|
||||
| `@Autowired` | 자동으로 빈을 주입 |
|
||||
| `@Qualifier` | 특정 빈을 지정하여 주입 |
|
||||
| `@Primary` | 기본적으로 주입될 빈을 지정 |
|
||||
| `@Value` | 프로퍼티 값을 주입 |
|
||||
| `@PropertySource` | 외부 설정 파일을 로드 |
|
||||
| `@Profile` | 특정 프로파일에서만 빈을 로드 |
|
||||
| `@Lazy` | 필요한 경우에만 빈을 초기화 |
|
||||
| `@Scope` | 빈의 스코프를 지정 |
|
||||
| `@DependsOn` | 특정 빈이 다른 빈보다 먼저 로드되도록 설정 |
|
||||
| `@PostConstruct` | 빈이 생성된 후 실행할 메서드 지정 |
|
||||
| `@PreDestroy` | 빈이 제거되기 전 실행할 메서드 지정 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 어노테이션 설명 및 예제
|
||||
|
||||
### 1) `@SpringBootApplication`
|
||||
Spring Boot 애플리케이션의 시작 클래스를 정의하는 어노테이션.
|
||||
- 내부적으로 `@Configuration`, `@EnableAutoConfiguration`, `@ComponentScan`을 포함.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@SpringBootApplication
|
||||
public class MyApplication {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(MyApplication.class, args);
|
||||
}
|
||||
}
|
||||
```
|
||||
- 이 클래스가 애플리케이션의 시작점 역할을 합니다.
|
||||
|
||||
---
|
||||
|
||||
### 2) `@Configuration`
|
||||
Java 기반 설정을 정의하는 클래스에 사용.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Configuration
|
||||
public class AppConfig {
|
||||
@Bean
|
||||
public MyService myService() {
|
||||
return new MyService();
|
||||
}
|
||||
}
|
||||
```
|
||||
- `MyService` 객체를 빈으로 등록.
|
||||
|
||||
---
|
||||
|
||||
### 3) `@ComponentScan`
|
||||
패키지를 검색하여 `@Component`, `@Service`, `@Repository` 등을 자동으로 등록.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@ComponentScan(basePackages = "com.example.service")
|
||||
@Configuration
|
||||
public class AppConfig {
|
||||
}
|
||||
```
|
||||
- `"com.example.service"` 패키지를 스캔하여 빈 등록.
|
||||
|
||||
---
|
||||
|
||||
### 4) `@Bean`
|
||||
메서드의 반환 객체를 스프링 빈으로 등록.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Configuration
|
||||
public class BeanConfig {
|
||||
@Bean
|
||||
public MyService myService() {
|
||||
return new MyService();
|
||||
}
|
||||
}
|
||||
```
|
||||
- `myService()`의 반환 객체가 빈으로 등록.
|
||||
|
||||
---
|
||||
|
||||
### 5) `@Component`
|
||||
일반적인 스프링 빈을 정의할 때 사용.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Component
|
||||
public class MyComponent {
|
||||
public void doSomething() {
|
||||
System.out.println("Component working");
|
||||
}
|
||||
}
|
||||
```
|
||||
- 스프링이 자동으로 빈으로 등록.
|
||||
|
||||
---
|
||||
|
||||
### 6) `@Service`
|
||||
비즈니스 로직을 담당하는 서비스 계층의 빈을 정의.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Service
|
||||
public class MyService {
|
||||
public String getMessage() {
|
||||
return "Hello, Service!";
|
||||
}
|
||||
}
|
||||
```
|
||||
- 서비스 계층의 빈으로 등록.
|
||||
|
||||
---
|
||||
|
||||
### 7) `@Repository`
|
||||
데이터 접근 계층의 빈을 정의.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Repository
|
||||
public class MyRepository {
|
||||
public String findData() {
|
||||
return "Data from DB";
|
||||
}
|
||||
}
|
||||
```
|
||||
- DAO(Data Access Object) 역할을 하는 클래스.
|
||||
|
||||
---
|
||||
|
||||
### 8) `@Autowired`
|
||||
빈을 자동으로 주입.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Component
|
||||
public class MyController {
|
||||
private final MyService myService;
|
||||
|
||||
@Autowired
|
||||
public MyController(MyService myService) {
|
||||
this.myService = myService;
|
||||
}
|
||||
}
|
||||
```
|
||||
- `MyService` 빈이 자동 주입됨.
|
||||
|
||||
---
|
||||
|
||||
### 9) `@Qualifier`
|
||||
같은 타입의 여러 빈 중 특정 빈을 선택하여 주입.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Component
|
||||
public class FirstService implements MyService {
|
||||
}
|
||||
|
||||
@Component
|
||||
public class SecondService implements MyService {
|
||||
}
|
||||
|
||||
@Component
|
||||
public class MyController {
|
||||
private final MyService myService;
|
||||
|
||||
@Autowired
|
||||
public MyController(@Qualifier("secondService") MyService myService) {
|
||||
this.myService = myService;
|
||||
}
|
||||
}
|
||||
```
|
||||
- `secondService` 빈이 주입됨.
|
||||
|
||||
---
|
||||
|
||||
### 10) `@Primary`
|
||||
여러 빈이 있을 때 기본 빈을 지정.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Primary
|
||||
@Component
|
||||
public class DefaultService implements MyService {
|
||||
}
|
||||
```
|
||||
- `@Autowired` 시 기본적으로 `DefaultService`가 주입됨.
|
||||
|
||||
---
|
||||
|
||||
### 11) `@Value`
|
||||
설정 파일에서 값을 주입.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Component
|
||||
public class ConfigComponent {
|
||||
@Value("${app.name}")
|
||||
private String appName;
|
||||
}
|
||||
```
|
||||
- `application.properties`에서 `app.name` 값을 읽음.
|
||||
|
||||
---
|
||||
|
||||
### 12) `@PropertySource`
|
||||
설정 파일을 로드.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Configuration
|
||||
@PropertySource("classpath:app.properties")
|
||||
public class AppConfig {
|
||||
}
|
||||
```
|
||||
- `app.properties` 파일을 로드.
|
||||
|
||||
---
|
||||
|
||||
### 13) `@Profile`
|
||||
특정 프로파일에서만 빈을 로드.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Profile("dev")
|
||||
@Component
|
||||
public class DevService {
|
||||
}
|
||||
```
|
||||
- `"dev"` 프로파일에서만 로드.
|
||||
|
||||
---
|
||||
|
||||
### 14) `@Lazy`
|
||||
빈을 필요할 때만 초기화.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Component
|
||||
@Lazy
|
||||
public class LazyComponent {
|
||||
}
|
||||
```
|
||||
- `LazyComponent`는 필요할 때만 생성됨.
|
||||
|
||||
---
|
||||
|
||||
### 15) `@Scope`
|
||||
빈의 스코프를 설정.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Component
|
||||
@Scope("prototype")
|
||||
public class PrototypeComponent {
|
||||
}
|
||||
```
|
||||
- 새로운 객체가 매번 생성됨.
|
||||
|
||||
---
|
||||
|
||||
### 16) `@DependsOn`
|
||||
다른 빈이 먼저 로드되도록 설정.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Component
|
||||
@DependsOn("anotherComponent")
|
||||
public class MyComponent {
|
||||
}
|
||||
```
|
||||
- `anotherComponent`가 먼저 로드됨.
|
||||
|
||||
---
|
||||
|
||||
### 17) `@PostConstruct`
|
||||
빈 생성 후 실행할 메서드 지정.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Component
|
||||
public class InitComponent {
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
System.out.println("Initialized!");
|
||||
}
|
||||
}
|
||||
```
|
||||
- 빈 생성 후 `init()` 실행.
|
||||
|
||||
---
|
||||
|
||||
### 18) `@PreDestroy`
|
||||
빈 제거 전에 실행할 메서드 지정.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Component
|
||||
public class DestroyComponent {
|
||||
@PreDestroy
|
||||
public void destroy() {
|
||||
System.out.println("Destroyed!");
|
||||
}
|
||||
}
|
||||
```
|
||||
- 빈 제거 전 `destroy()` 실행.
|
||||
|
||||
---
|
||||
|
||||
Spring Boot Core에서 사용되는 주요 어노테이션을 정리했습니다. 이를 활용하면 애플리케이션의 설정과 빈 관리를 효과적으로 수행할 수 있습니다.
|
||||
289
docs/_archive/jpa.md
Normal file
289
docs/_archive/jpa.md
Normal file
@@ -0,0 +1,289 @@
|
||||
# Spring Boot의 Model 및 JPA 관련 어노테이션 정리
|
||||
|
||||
Spring Boot에서 **모델(Model)과 JPA(Java Persistence API)** 를 활용할 때 사용되는 주요 어노테이션을 표로 정리하고, 각각에 대한 설명과 예제 코드를 제공합니다.
|
||||
|
||||
---
|
||||
|
||||
## 1. Model 및 JPA 관련 어노테이션 정리표
|
||||
|
||||
| 어노테이션 | 설명 |
|
||||
|------------------------|----------------------------------|
|
||||
| `@Entity` | JPA 엔티티 클래스임을 명시 |
|
||||
| `@Table` | 데이터베이스의 테이블과 매핑 |
|
||||
| `@Id` | 기본 키(PK) 지정 |
|
||||
| `@GeneratedValue` | 기본 키 값 자동 생성 전략 설정 |
|
||||
| `@Column` | 데이터베이스 컬럼과 매핑 |
|
||||
| `@Transient` | 특정 필드를 영속성에서 제외 |
|
||||
| `@Embedded` | 내장(Embeddable) 타입을 포함 |
|
||||
| `@Embeddable` | 내장 타입을 정의 |
|
||||
| `@Enumerated` | Enum 타입을 컬럼에 매핑 |
|
||||
| `@Lob` | 대용량 데이터(Long Text, Blob) 저장 |
|
||||
| `@Temporal` | 날짜/시간 타입 매핑 |
|
||||
| `@ManyToOne` | 다대일 관계 설정 |
|
||||
| `@OneToMany` | 일대다 관계 설정 |
|
||||
| `@OneToOne` | 일대일 관계 설정 |
|
||||
| `@ManyToMany` | 다대다 관계 설정 |
|
||||
| `@JoinColumn` | 외래 키(FK) 설정 |
|
||||
| `@JoinTable` | 다대다 관계에서 조인 테이블 설정 |
|
||||
| `@MappedSuperclass` | 공통 속성을 가지는 부모 클래스 지정 |
|
||||
| `@Inheritance` | 상속 매핑 전략 설정 |
|
||||
| `@DiscriminatorColumn`| 상속 엔티티 구분 컬럼 설정 |
|
||||
| `@NamedQuery` | JPQL로 미리 정의된 쿼리 설정 |
|
||||
| `@Query` | 사용자 정의 JPQL 쿼리 작성 |
|
||||
| `@Modifying` | 데이터 수정 JPQL 쿼리 작성 |
|
||||
| `@Transactional` | 트랜잭션 범위 설정 |
|
||||
| `@Repository` | 데이터 액세스 계층을 나타냄 |
|
||||
| `@EnableJpaRepositories` | JPA 리포지토리 활성화 |
|
||||
|
||||
---
|
||||
|
||||
## 2. Model 및 JPA 관련 어노테이션 설명 및 예제
|
||||
|
||||
### 1) `@Entity`
|
||||
JPA에서 엔티티 클래스를 나타냅니다. 해당 클래스는 데이터베이스 테이블과 매핑됩니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Entity
|
||||
public class User {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
}
|
||||
```
|
||||
- `User` 클래스가 `users` 테이블과 매핑됩니다.
|
||||
|
||||
---
|
||||
|
||||
### 2) `@Table`
|
||||
엔티티와 매핑될 테이블의 이름을 지정합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Entity
|
||||
@Table(name = "user_table")
|
||||
public class User {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
}
|
||||
```
|
||||
- `User` 엔티티가 `user_table`과 매핑됩니다.
|
||||
|
||||
---
|
||||
|
||||
### 3) `@Id`
|
||||
엔티티의 **기본 키(PK)** 를 지정합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Entity
|
||||
public class Product {
|
||||
@Id
|
||||
private Long productId;
|
||||
}
|
||||
```
|
||||
- `productId`가 **기본 키**가 됩니다.
|
||||
|
||||
---
|
||||
|
||||
### 4) `@GeneratedValue`
|
||||
기본 키를 자동 생성하도록 설정합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Entity
|
||||
public class Order {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
}
|
||||
```
|
||||
- `GenerationType.IDENTITY`는 **AUTO_INCREMENT** 방식으로 기본 키를 생성합니다.
|
||||
|
||||
---
|
||||
|
||||
### 5) `@Column`
|
||||
필드를 특정 컬럼과 매핑할 때 사용합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Entity
|
||||
public class Employee {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "emp_name", length = 100, nullable = false)
|
||||
private String name;
|
||||
}
|
||||
```
|
||||
- `emp_name`이라는 컬럼에 매핑되고, 길이는 100이며, **NULL을 허용하지 않음**.
|
||||
|
||||
---
|
||||
|
||||
### 6) `@Transient`
|
||||
해당 필드를 **DB에 저장하지 않도록** 설정합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Entity
|
||||
public class Person {
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@Transient
|
||||
private int age;
|
||||
}
|
||||
```
|
||||
- `age` 필드는 **DB에 저장되지 않음**.
|
||||
|
||||
---
|
||||
|
||||
### 7) `@Enumerated`
|
||||
Enum 타입을 컬럼에 저장할 때 사용합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Entity
|
||||
public class Task {
|
||||
public enum Status { PENDING, COMPLETED }
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
private Status status;
|
||||
}
|
||||
```
|
||||
- `EnumType.STRING`을 사용하여 `"PENDING"`, `"COMPLETED"` 형태로 저장됩니다.
|
||||
|
||||
---
|
||||
|
||||
### 8) `@ManyToOne`
|
||||
**다대일(N:1) 관계**를 정의합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Entity
|
||||
public class Order {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "user_id")
|
||||
private User user;
|
||||
}
|
||||
```
|
||||
- `Order` 엔티티는 **User와 다대일 관계**.
|
||||
|
||||
---
|
||||
|
||||
### 9) `@OneToMany`
|
||||
**일대다(1:N) 관계**를 정의합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Entity
|
||||
public class User {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@OneToMany(mappedBy = "user")
|
||||
private List<Order> orders;
|
||||
}
|
||||
```
|
||||
- 한 명의 `User`가 여러 개의 `Order`를 가질 수 있음.
|
||||
|
||||
---
|
||||
|
||||
### 10) `@OneToOne`
|
||||
**일대일(1:1) 관계**를 정의합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Entity
|
||||
public class Passport {
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@OneToOne
|
||||
@JoinColumn(name = "user_id")
|
||||
private User user;
|
||||
}
|
||||
```
|
||||
- 한 `User`는 하나의 `Passport`를 가짐.
|
||||
|
||||
---
|
||||
|
||||
### 11) `@ManyToMany`
|
||||
**다대다(N:M) 관계**를 정의합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Entity
|
||||
public class Student {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@ManyToMany
|
||||
@JoinTable(
|
||||
name = "student_course",
|
||||
joinColumns = @JoinColumn(name = "student_id"),
|
||||
inverseJoinColumns = @JoinColumn(name = "course_id")
|
||||
)
|
||||
private List<Course> courses;
|
||||
}
|
||||
```
|
||||
- `Student`와 `Course`는 다대다 관계이며 **중간 테이블(`student_course`)** 을 가짐.
|
||||
|
||||
---
|
||||
|
||||
### 12) `@Query`
|
||||
JPQL을 사용하여 **사용자 정의 쿼리**를 작성합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Repository
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
@Query("SELECT u FROM User u WHERE u.name = :name")
|
||||
List<User> findByName(@Param("name") String name);
|
||||
}
|
||||
```
|
||||
- `name`이 특정 값인 `User`를 조회하는 쿼리.
|
||||
|
||||
---
|
||||
|
||||
### 13) `@Transactional`
|
||||
**트랜잭션 범위를 설정**합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Service
|
||||
public class UserService {
|
||||
@Transactional
|
||||
public void updateUser(Long id, String name) {
|
||||
User user = userRepository.findById(id).orElseThrow();
|
||||
user.setName(name);
|
||||
}
|
||||
}
|
||||
```
|
||||
- 메서드 실행 중 예외 발생 시 **자동 롤백**.
|
||||
|
||||
---
|
||||
|
||||
## 3. 정리
|
||||
Spring Boot에서 **모델과 JPA 관련 어노테이션**을 정리했습니다.
|
||||
- `@Entity`, `@Table`, `@Id`, `@Column` → 기본 엔티티 정의
|
||||
- `@ManyToOne`, `@OneToMany`, `@OneToOne`, `@ManyToMany` → 관계 매핑
|
||||
- `@Query`, `@Transactional` → 데이터 액세스 및 트랜잭션 처리
|
||||
|
||||
이제 **Spring Boot + JPA** 개발을 효과적으로 할 수 있습니다!
|
||||
269
docs/_archive/mvc.md
Normal file
269
docs/_archive/mvc.md
Normal file
@@ -0,0 +1,269 @@
|
||||
# Spring Boot MVC 어노테이션 정리
|
||||
|
||||
Spring Boot MVC에서 사용되는 주요 어노테이션을 표로 정리한 후, 각각의 어노테이션에 대한 설명과 예제를 제공합니다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 어노테이션 정리표
|
||||
|
||||
| 어노테이션 | 설명 |
|
||||
|--------------------|-----------------------------------------|
|
||||
| `@Controller` | Spring MVC의 컨트롤러 클래스를 정의 |
|
||||
| `@RestController` | `@Controller` + `@ResponseBody`, JSON 응답을 기본으로 함 |
|
||||
| `@RequestMapping` | URL 요청을 특정 컨트롤러 또는 메서드에 매핑 |
|
||||
| `@GetMapping` | HTTP GET 요청을 특정 메서드에 매핑 |
|
||||
| `@PostMapping` | HTTP POST 요청을 특정 메서드에 매핑 |
|
||||
| `@PutMapping` | HTTP PUT 요청을 특정 메서드에 매핑 |
|
||||
| `@DeleteMapping` | HTTP DELETE 요청을 특정 메서드에 매핑 |
|
||||
| `@PatchMapping` | HTTP PATCH 요청을 특정 메서드에 매핑 |
|
||||
| `@RequestParam` | 요청 파라미터를 메서드의 파라미터로 매핑 |
|
||||
| `@PathVariable` | URL 경로 변수를 메서드의 파라미터로 매핑 |
|
||||
| `@ModelAttribute` | 폼 데이터를 객체로 변환하여 전달 |
|
||||
| `@RequestBody` | 요청 본문(JSON 등)을 객체로 변환하여 전달 |
|
||||
| `@ResponseBody` | 반환 데이터를 JSON 형태로 응답 |
|
||||
| `@ResponseStatus` | HTTP 응답 상태 코드를 지정 |
|
||||
| `@ExceptionHandler` | 특정 예외 발생 시 처리할 메서드를 정의 |
|
||||
| `@InitBinder` | 컨트롤러에서 요청 데이터를 변환하는 바인딩 설정을 정의 |
|
||||
| `@CrossOrigin` | 다른 도메인에서 API 요청을 허용하도록 설정 |
|
||||
|
||||
---
|
||||
|
||||
## 2. 어노테이션 설명 및 예제
|
||||
|
||||
### 1) `@Controller`
|
||||
Spring MVC 컨트롤러 클래스임을 나타냅니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Controller
|
||||
public class MyController {
|
||||
@GetMapping("/hello")
|
||||
public String hello() {
|
||||
return "hello"; // hello.html을 렌더링
|
||||
}
|
||||
}
|
||||
```
|
||||
- `hello.html` 뷰 페이지를 반환합니다.
|
||||
|
||||
---
|
||||
|
||||
### 2) `@RestController`
|
||||
`@Controller`와 `@ResponseBody`를 합친 역할을 합니다. 즉, JSON 응답을 기본으로 합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@RestController
|
||||
public class MyRestController {
|
||||
@GetMapping("/api/hello")
|
||||
public String hello() {
|
||||
return "Hello, World!";
|
||||
}
|
||||
}
|
||||
```
|
||||
- `"Hello, World!"`라는 문자열을 JSON 형식으로 반환합니다.
|
||||
|
||||
---
|
||||
|
||||
### 3) `@RequestMapping`
|
||||
URL과 컨트롤러 메서드를 매핑합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Controller
|
||||
@RequestMapping("/users")
|
||||
public class UserController {
|
||||
@GetMapping("/{id}")
|
||||
public String getUser(@PathVariable Long id) {
|
||||
return "user"; // user.html 렌더링
|
||||
}
|
||||
}
|
||||
```
|
||||
- `/users/{id}` 경로로 들어오는 요청을 `getUser` 메서드가 처리합니다.
|
||||
|
||||
---
|
||||
|
||||
### 4) `@GetMapping`, `@PostMapping`, `@PutMapping`, `@DeleteMapping`, `@PatchMapping`
|
||||
각 HTTP 메서드에 대한 매핑을 제공합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/items")
|
||||
public class ItemController {
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public String getItem(@PathVariable Long id) {
|
||||
return "Item: " + id;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public String createItem(@RequestBody String item) {
|
||||
return "Created: " + item;
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public String updateItem(@PathVariable Long id, @RequestBody String item) {
|
||||
return "Updated item " + id + " to " + item;
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public String deleteItem(@PathVariable Long id) {
|
||||
return "Deleted item " + id;
|
||||
}
|
||||
}
|
||||
```
|
||||
- 각각 `GET`, `POST`, `PUT`, `DELETE` 요청을 처리하는 컨트롤러입니다.
|
||||
|
||||
---
|
||||
|
||||
### 5) `@RequestParam`
|
||||
쿼리 파라미터를 매핑할 때 사용합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@RestController
|
||||
public class ParamController {
|
||||
@GetMapping("/search")
|
||||
public String search(@RequestParam String query) {
|
||||
return "Searching for: " + query;
|
||||
}
|
||||
}
|
||||
```
|
||||
- `/search?query=Spring` 요청 시 `"Searching for: Spring"` 반환.
|
||||
|
||||
---
|
||||
|
||||
### 6) `@PathVariable`
|
||||
URL 경로 변수를 매핑할 때 사용합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@RestController
|
||||
public class PathVariableController {
|
||||
@GetMapping("/product/{id}")
|
||||
public String getProduct(@PathVariable Long id) {
|
||||
return "Product ID: " + id;
|
||||
}
|
||||
}
|
||||
```
|
||||
- `/product/100` 요청 시 `"Product ID: 100"` 반환.
|
||||
|
||||
---
|
||||
|
||||
### 7) `@ModelAttribute`
|
||||
폼 데이터를 객체로 바인딩할 때 사용합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Controller
|
||||
public class FormController {
|
||||
@PostMapping("/submit")
|
||||
public String submit(@ModelAttribute User user) {
|
||||
return "result"; // result.html 렌더링
|
||||
}
|
||||
}
|
||||
|
||||
class User {
|
||||
private String name;
|
||||
private int age;
|
||||
// Getter & Setter 생략
|
||||
}
|
||||
```
|
||||
- 폼에서 `name`과 `age` 값을 받아 `User` 객체로 변환.
|
||||
|
||||
---
|
||||
|
||||
### 8) `@RequestBody`
|
||||
JSON 데이터를 객체로 변환할 때 사용합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@RestController
|
||||
public class JsonController {
|
||||
@PostMapping("/json")
|
||||
public String receiveJson(@RequestBody User user) {
|
||||
return "Received: " + user.getName();
|
||||
}
|
||||
}
|
||||
```
|
||||
- `{ "name": "Alice", "age": 25 }` 데이터를 `User` 객체로 변환.
|
||||
|
||||
---
|
||||
|
||||
### 9) `@ResponseBody`
|
||||
메서드의 반환값을 HTTP 응답으로 직접 반환할 때 사용합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Controller
|
||||
public class ResponseController {
|
||||
@ResponseBody
|
||||
@GetMapping("/text")
|
||||
public String textResponse() {
|
||||
return "Hello, ResponseBody!";
|
||||
}
|
||||
}
|
||||
```
|
||||
- `"Hello, ResponseBody!"`가 그대로 반환.
|
||||
|
||||
---
|
||||
|
||||
### 10) `@ResponseStatus`
|
||||
HTTP 응답 상태 코드를 설정할 때 사용합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@RestController
|
||||
public class StatusController {
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
@PostMapping("/create")
|
||||
public String create() {
|
||||
return "Created successfully!";
|
||||
}
|
||||
}
|
||||
```
|
||||
- HTTP 201 Created 응답을 반환.
|
||||
|
||||
---
|
||||
|
||||
### 11) `@ExceptionHandler`
|
||||
예외 발생 시 처리할 메서드를 정의합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@RestController
|
||||
public class ExceptionController {
|
||||
@GetMapping("/error")
|
||||
public String error() {
|
||||
throw new RuntimeException("Something went wrong!");
|
||||
}
|
||||
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
public String handleRuntimeException(RuntimeException e) {
|
||||
return "Handled error: " + e.getMessage();
|
||||
}
|
||||
}
|
||||
```
|
||||
- `/error` 요청 시 `"Handled error: Something went wrong!"` 반환.
|
||||
|
||||
---
|
||||
|
||||
### 12) `@CrossOrigin`
|
||||
CORS 문제를 해결할 때 사용합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@RestController
|
||||
@CrossOrigin(origins = "http://example.com")
|
||||
public class CorsController {
|
||||
@GetMapping("/data")
|
||||
public String getData() {
|
||||
return "CORS enabled";
|
||||
}
|
||||
}
|
||||
```
|
||||
- `http://example.com`에서 요청 가능.
|
||||
|
||||
---
|
||||
|
||||
이제 Spring Boot MVC의 주요 어노테이션과 예제들을 이해했을 것입니다. 필요에 따라 적절한 어노테이션을 활용하여 프로젝트를 개발하면 됩니다.
|
||||
280
docs/_archive/템플릿 렌더링.md
Normal file
280
docs/_archive/템플릿 렌더링.md
Normal file
@@ -0,0 +1,280 @@
|
||||
# Spring Boot의 Thymeleaf 템플릿 렌더링 관련 어노테이션 정리
|
||||
|
||||
Spring Boot에서 **Thymeleaf**를 활용하여 템플릿을 렌더링할 때 사용되는 주요 어노테이션을 표로 정리하고, 각각에 대한 설명과 예제 코드를 제공합니다.
|
||||
|
||||
---
|
||||
|
||||
## 1. Thymeleaf 관련 어노테이션 정리표
|
||||
|
||||
| 어노테이션 | 설명 |
|
||||
|------------------------|----------------------------------|
|
||||
| `@Controller` | 컨트롤러 클래스를 정의 |
|
||||
| `@RestController` | RESTful API를 제공하는 컨트롤러를 정의 |
|
||||
| `@RequestMapping` | 요청 URL을 매핑 |
|
||||
| `@GetMapping` | GET 요청을 처리 |
|
||||
| `@PostMapping` | POST 요청을 처리 |
|
||||
| `@ModelAttribute` | 모델 데이터를 초기화하여 뷰로 전달 |
|
||||
| `@RequestParam` | 요청 파라미터를 컨트롤러 메서드로 전달 |
|
||||
| `@PathVariable` | URL 경로 변수 값을 컨트롤러 메서드로 전달 |
|
||||
| `@SessionAttributes` | 특정 속성을 세션에 저장 |
|
||||
| `@SessionAttribute` | 세션에서 특정 속성을 가져옴 |
|
||||
| `@RequestBody` | HTTP 요청 본문을 객체로 변환 |
|
||||
| `@ResponseBody` | 객체를 JSON 등의 형식으로 응답 |
|
||||
| `@ResponseStatus` | HTTP 응답 상태 코드를 설정 |
|
||||
|
||||
---
|
||||
|
||||
## 2. Thymeleaf 관련 어노테이션 설명 및 예제
|
||||
|
||||
### 1) `@Controller`
|
||||
Spring MVC에서 사용되는 컨트롤러 클래스임을 나타냅니다.
|
||||
Thymeleaf 템플릿을 반환하는 역할을 합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Controller
|
||||
public class HomeController {
|
||||
@GetMapping("/")
|
||||
public String home() {
|
||||
return "index"; // src/main/resources/templates/index.html 렌더링
|
||||
}
|
||||
}
|
||||
```
|
||||
- `"index"`는 `templates/index.html` 파일을 렌더링합니다.
|
||||
|
||||
---
|
||||
|
||||
### 2) `@RestController`
|
||||
RESTful API를 제공하는 컨트롤러를 정의할 때 사용합니다.
|
||||
`@Controller`와 다르게 `@ResponseBody`가 포함되어 있어, 데이터를 JSON 형식으로 반환합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@RestController
|
||||
public class ApiController {
|
||||
@GetMapping("/api/message")
|
||||
public String getMessage() {
|
||||
return "Hello, REST!";
|
||||
}
|
||||
}
|
||||
```
|
||||
- `"Hello, REST!"` 문자열이 그대로 응답됩니다.
|
||||
|
||||
---
|
||||
|
||||
### 3) `@RequestMapping`
|
||||
요청 URL을 특정 컨트롤러 메서드에 매핑합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Controller
|
||||
@RequestMapping("/home")
|
||||
public class HomeController {
|
||||
@GetMapping
|
||||
public String home() {
|
||||
return "home";
|
||||
}
|
||||
}
|
||||
```
|
||||
- `/home` URL 요청 시 `templates/home.html`이 렌더링됩니다.
|
||||
|
||||
---
|
||||
|
||||
### 4) `@GetMapping`
|
||||
GET 요청을 처리하는 메서드를 정의합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Controller
|
||||
public class PageController {
|
||||
@GetMapping("/about")
|
||||
public String about() {
|
||||
return "about";
|
||||
}
|
||||
}
|
||||
```
|
||||
- `/about` 요청 시 `templates/about.html`이 렌더링됩니다.
|
||||
|
||||
---
|
||||
|
||||
### 5) `@PostMapping`
|
||||
POST 요청을 처리하는 메서드를 정의합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Controller
|
||||
public class FormController {
|
||||
@PostMapping("/submit")
|
||||
public String submitForm(@RequestParam String name, Model model) {
|
||||
model.addAttribute("name", name);
|
||||
return "result";
|
||||
}
|
||||
}
|
||||
```
|
||||
- `/submit`로 POST 요청을 보내면 `name` 값을 `result.html`에 전달합니다.
|
||||
|
||||
---
|
||||
|
||||
### 6) `@ModelAttribute`
|
||||
모델 데이터를 초기화하여 뷰로 전달하는 데 사용됩니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Controller
|
||||
public class UserController {
|
||||
@ModelAttribute("message")
|
||||
public String welcomeMessage() {
|
||||
return "Welcome to our site!";
|
||||
}
|
||||
|
||||
@GetMapping("/welcome")
|
||||
public String welcomePage() {
|
||||
return "welcome";
|
||||
}
|
||||
}
|
||||
```
|
||||
- `welcome.html`에서 `${message}`를 사용하여 `"Welcome to our site!"`를 출력할 수 있습니다.
|
||||
|
||||
---
|
||||
|
||||
### 7) `@RequestParam`
|
||||
HTTP 요청 파라미터를 컨트롤러 메서드로 전달하는 데 사용됩니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Controller
|
||||
public class GreetingController {
|
||||
@GetMapping("/greet")
|
||||
public String greet(@RequestParam(defaultValue = "Guest") String name, Model model) {
|
||||
model.addAttribute("name", name);
|
||||
return "greeting";
|
||||
}
|
||||
}
|
||||
```
|
||||
- `/greet?name=John` 요청 시 `"John"`이 `greeting.html`에 전달됩니다.
|
||||
|
||||
---
|
||||
|
||||
### 8) `@PathVariable`
|
||||
URL 경로 변수 값을 컨트롤러 메서드로 전달할 때 사용됩니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Controller
|
||||
public class ProfileController {
|
||||
@GetMapping("/profile/{username}")
|
||||
public String profile(@PathVariable String username, Model model) {
|
||||
model.addAttribute("username", username);
|
||||
return "profile";
|
||||
}
|
||||
}
|
||||
```
|
||||
- `/profile/john` 요청 시 `"john"`이 `profile.html`에 전달됩니다.
|
||||
|
||||
---
|
||||
|
||||
### 9) `@SessionAttributes`
|
||||
특정 속성을 세션에 저장하여 여러 요청에서 공유할 수 있도록 합니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Controller
|
||||
@SessionAttributes("user")
|
||||
public class SessionController {
|
||||
@ModelAttribute("user")
|
||||
public User user() {
|
||||
return new User();
|
||||
}
|
||||
|
||||
@GetMapping("/session")
|
||||
public String sessionPage() {
|
||||
return "session";
|
||||
}
|
||||
}
|
||||
```
|
||||
- `user` 객체가 세션에 저장됩니다.
|
||||
|
||||
---
|
||||
|
||||
### 10) `@SessionAttribute`
|
||||
세션에 저장된 속성을 가져올 때 사용됩니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Controller
|
||||
public class DashboardController {
|
||||
@GetMapping("/dashboard")
|
||||
public String dashboard(@SessionAttribute("user") User user, Model model) {
|
||||
model.addAttribute("user", user);
|
||||
return "dashboard";
|
||||
}
|
||||
}
|
||||
```
|
||||
- 세션에서 `user` 객체를 가져와 `dashboard.html`에 전달합니다.
|
||||
|
||||
---
|
||||
|
||||
### 11) `@RequestBody`
|
||||
HTTP 요청 본문을 객체로 변환하여 받을 때 사용됩니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@RestController
|
||||
public class JsonController {
|
||||
@PostMapping("/json")
|
||||
public String handleJson(@RequestBody User user) {
|
||||
return "Received: " + user.getName();
|
||||
}
|
||||
}
|
||||
```
|
||||
- JSON 데이터를 `User` 객체로 변환하여 받습니다.
|
||||
|
||||
---
|
||||
|
||||
### 12) `@ResponseBody`
|
||||
컨트롤러 메서드의 반환값을 HTTP 응답 본문으로 반환할 때 사용됩니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@Controller
|
||||
public class TextController {
|
||||
@GetMapping("/text")
|
||||
@ResponseBody
|
||||
public String plainText() {
|
||||
return "This is plain text";
|
||||
}
|
||||
}
|
||||
```
|
||||
- `"This is plain text"`가 그대로 응답됩니다.
|
||||
|
||||
---
|
||||
|
||||
### 13) `@ResponseStatus`
|
||||
HTTP 응답 상태 코드를 설정할 때 사용됩니다.
|
||||
|
||||
#### 예제:
|
||||
```java
|
||||
@RestController
|
||||
public class StatusController {
|
||||
@GetMapping("/forbidden")
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public String forbidden() {
|
||||
return "Access Denied";
|
||||
}
|
||||
}
|
||||
```
|
||||
- `/forbidden` 요청 시 **403 Forbidden** 응답을 반환합니다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 정리
|
||||
Spring Boot에서 **Thymeleaf 템플릿을 렌더링**할 때 자주 사용하는 어노테이션을 정리했습니다.
|
||||
|
||||
- `@Controller` → Thymeleaf 템플릿을 반환하는 컨트롤러
|
||||
- `@GetMapping`, `@PostMapping` → 요청을 처리하는 엔드포인트 설정
|
||||
- `@RequestParam`, `@PathVariable` → 요청 데이터 전달
|
||||
- `@ModelAttribute` → 뷰에 데이터를 전달
|
||||
- `@SessionAttributes`, `@SessionAttribute` → 세션 데이터 관리
|
||||
- `@ResponseBody`, `@RequestBody` → JSON 또는 텍스트 데이터 처리
|
||||
|
||||
이제 Thymeleaf를 사용할 때 필요한 어노테이션을 쉽게 활용할 수 있습니다!
|
||||
98
docs/actuator/01_actuator.md
Normal file
98
docs/actuator/01_actuator.md
Normal file
@@ -0,0 +1,98 @@
|
||||
좋아, 1부 "개요와 시작"을 간결하게 작성해볼게. 실무자가 빠르게 이해하고 따라 할 수 있도록 구성하고, 예시는 Gradle 기준으로 쓸게.
|
||||
|
||||
---
|
||||
|
||||
## **1부. 개요와 시작**
|
||||
|
||||
---
|
||||
|
||||
### **1장. 스프링 부트 액추에이터란?**
|
||||
|
||||
#### 액추에이터의 개요
|
||||
|
||||
- 스프링 부트 액추에이터(Spring Boot Actuator)는 애플리케이션의 상태, 성능, 메트릭, 로그 설정 등을 HTTP 엔드포인트로 노출하여 운영·모니터링을 쉽게 할 수 있도록 도와주는 기능이다.
|
||||
|
||||
#### 주요 기능
|
||||
|
||||
- Health 체크
|
||||
- 애플리케이션 정보 출력
|
||||
- 메트릭 수집 (CPU, 메모리, 요청 수 등)
|
||||
- HTTP 요청 추적
|
||||
- 로깅 레벨 변경
|
||||
- 사용자 정의 엔드포인트 구현
|
||||
|
||||
---
|
||||
|
||||
### **2장. 프로젝트 설정**
|
||||
|
||||
#### Gradle 설정 예시
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
}
|
||||
```
|
||||
|
||||
#### 애플리케이션 실행
|
||||
|
||||
```bash
|
||||
./gradlew bootRun
|
||||
```
|
||||
|
||||
실행 후 `http://localhost:8080/actuator` 접속
|
||||
|
||||
---
|
||||
|
||||
### **3장. 기본 엔드포인트 살펴보기**
|
||||
|
||||
#### 엔드포인트 목록
|
||||
|
||||
- `/actuator/health` – 애플리케이션 상태 확인
|
||||
- `/actuator/info` – 앱 정보 출력
|
||||
- `/actuator/metrics` – 메트릭 데이터
|
||||
- `/actuator/loggers` – 로깅 레벨 제어
|
||||
- `/actuator/beans` – 빈 목록 출력
|
||||
- `/actuator/env` – 환경 변수 출력
|
||||
|
||||
#### application.yml 설정 예시
|
||||
|
||||
```yaml
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: "*"
|
||||
```
|
||||
|
||||
> 기본적으로 대부분의 엔드포인트는 비공개이며, 위 설정으로 전체 노출 가능.
|
||||
|
||||
---
|
||||
|
||||
### **4장. 엔드포인트 테스트하기**
|
||||
|
||||
#### curl로 확인
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/actuator/health
|
||||
```
|
||||
|
||||
#### 결과 예시
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "UP"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **5장. 요약**
|
||||
|
||||
- 액추에이터는 스프링 부트에서 운영 편의성을 높이는 핵심 도구다.
|
||||
- 별도 서버 설치 없이 엔드포인트를 통해 상태, 성능, 설정 정보를 쉽게 확인할 수 있다.
|
||||
- Gradle로 간단히 의존성을 추가하고, `application.yml`로 노출 설정을 제어할 수 있다.
|
||||
|
||||
---
|
||||
|
||||
원하면 각 장에 더 많은 설명이나 예제를 추가해줄 수 있어. 다음은 2부 주요 엔드포인트 설명 들어가면 좋을 것 같아. 계속 이어서 쓸까?
|
||||
190
docs/actuator/02_endpoints.md
Normal file
190
docs/actuator/02_endpoints.md
Normal file
@@ -0,0 +1,190 @@
|
||||
좋아, 이어서 **2부. 주요 엔드포인트 상세 분석**을 작성해볼게. 실무에서 자주 쓰는 엔드포인트 중심으로 간결하게 구성하고, 예시도 함께 제공할게.
|
||||
|
||||
---
|
||||
|
||||
## **2부. 주요 엔드포인트 상세 분석**
|
||||
|
||||
---
|
||||
|
||||
### **6장. `/actuator/health` — 헬스 체크**
|
||||
|
||||
#### 개요
|
||||
애플리케이션의 현재 상태(UP, DOWN 등)를 알려주는 엔드포인트. 모니터링 시스템, 로드 밸런서의 상태 판단 기준으로 자주 사용됨.
|
||||
|
||||
#### 기본 사용
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/actuator/health
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "UP"
|
||||
}
|
||||
```
|
||||
|
||||
#### 커스터마이징
|
||||
|
||||
```yaml
|
||||
management:
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "UP",
|
||||
"components": {
|
||||
"diskSpace": {
|
||||
"status": "UP",
|
||||
"details": {
|
||||
"total": 500000000000,
|
||||
"free": 480000000000,
|
||||
"threshold": 10485760
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 사용자 정의 헬스 인디케이터
|
||||
|
||||
```java
|
||||
@Component
|
||||
public class MyHealthIndicator implements HealthIndicator {
|
||||
@Override
|
||||
public Health health() {
|
||||
return Health.up().withDetail("custom", "OK").build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **7장. `/actuator/info` — 정보 제공**
|
||||
|
||||
#### 설정
|
||||
|
||||
```yaml
|
||||
info:
|
||||
app:
|
||||
name: actuator-demo
|
||||
version: 1.0.0
|
||||
```
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/actuator/info
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"app": {
|
||||
"name": "actuator-demo",
|
||||
"version": "1.0.0"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Git 정보 포함하기
|
||||
|
||||
`build.gradle`
|
||||
|
||||
```kotlin
|
||||
plugins {
|
||||
id "com.gorylenko.gradle-git-properties" version "2.4.0"
|
||||
}
|
||||
```
|
||||
|
||||
```yaml
|
||||
management:
|
||||
info:
|
||||
git:
|
||||
mode: full
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **8장. `/actuator/metrics` — 메트릭 수집**
|
||||
|
||||
#### 개요
|
||||
CPU, 메모리, 요청 횟수 등 주요 리소스 사용량 정보를 제공함.
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/actuator/metrics
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"names": [
|
||||
"jvm.memory.used",
|
||||
"jvm.gc.memory.promoted",
|
||||
"http.server.requests",
|
||||
...
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 특정 메트릭 조회
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/actuator/metrics/http.server.requests
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **9장. `/actuator/loggers` — 로깅 레벨 제어**
|
||||
|
||||
#### 현재 로깅 레벨 조회
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/actuator/loggers/com.example
|
||||
```
|
||||
|
||||
#### 런타임 레벨 변경
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/actuator/loggers/com.example \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"configuredLevel": "DEBUG"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **10장. `/actuator/httptrace` — HTTP 요청 추적**
|
||||
|
||||
> Spring Boot 2.6 이후 기본 미포함. 의존성 추가 필요.
|
||||
|
||||
```kotlin
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-actuator-autoconfigure'
|
||||
```
|
||||
|
||||
```yaml
|
||||
management:
|
||||
endpoint:
|
||||
httptrace:
|
||||
enabled: true
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: httptrace
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **11장. 기타 엔드포인트 요약**
|
||||
|
||||
| 엔드포인트 | 설명 |
|
||||
|------------------|------|
|
||||
| `/actuator/beans` | 등록된 스프링 빈 목록 |
|
||||
| `/actuator/env` | 환경 변수 및 프로퍼티 확인 |
|
||||
| `/actuator/mappings` | URL-컨트롤러 매핑 확인 |
|
||||
| `/actuator/threaddump` | 스레드 덤프 출력 |
|
||||
| `/actuator/configprops` | `@ConfigurationProperties` 설정 확인 |
|
||||
| `/actuator/scheduledtasks` | 스케줄링된 작업 정보 확인 |
|
||||
|
||||
---
|
||||
|
||||
다음은 3부 **보안과 커스터마이징**으로 이어지면 좋아. 계속 이어서 써줄까?
|
||||
162
docs/actuator/03_config.md
Normal file
162
docs/actuator/03_config.md
Normal file
@@ -0,0 +1,162 @@
|
||||
좋아, 이어서 **3부. 보안과 커스터마이징**을 작성해볼게. 액추에이터를 실무에 적용할 때 꼭 필요한 보안 설정과 사용자 정의 방법을 중심으로 정리할게.
|
||||
|
||||
---
|
||||
|
||||
## **3부. 보안과 커스터마이징**
|
||||
|
||||
---
|
||||
|
||||
### **12장. 액추에이터 엔드포인트 보안**
|
||||
|
||||
#### 기본 설정
|
||||
Spring Security를 사용할 경우, 액추에이터 엔드포인트는 기본적으로 인증이 필요함.
|
||||
|
||||
#### `application.yml`에서 노출 대상 설정
|
||||
|
||||
```yaml
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health, info, metrics
|
||||
```
|
||||
|
||||
#### WebSecurityConfigurer를 통한 제어
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
public class ActuatorSecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/actuator/health", "/actuator/info").permitAll()
|
||||
.requestMatchers("/actuator/**").hasRole("ADMIN")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.httpBasic();
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 인증 필요 시 기본 사용자 정보는 `application.yml`에서 설정
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
security:
|
||||
user:
|
||||
name: admin
|
||||
password: admin123
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **13장. 액추에이터 엔드포인트 커스터마이징**
|
||||
|
||||
#### 엔드포인트 비활성화
|
||||
|
||||
```yaml
|
||||
management:
|
||||
endpoint:
|
||||
metrics:
|
||||
enabled: false
|
||||
```
|
||||
|
||||
#### 엔드포인트 경로 변경
|
||||
|
||||
```yaml
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
base-path: /manage
|
||||
```
|
||||
|
||||
> 결과: `/manage/health`, `/manage/info` 등의 경로로 변경됨
|
||||
|
||||
#### 관리 포트를 분리
|
||||
|
||||
```yaml
|
||||
management:
|
||||
server:
|
||||
port: 8081
|
||||
```
|
||||
|
||||
> `/actuator`는 8081 포트에서만 노출됨. 일반 API는 기존 8080에서 제공됨
|
||||
|
||||
---
|
||||
|
||||
### **14장. 사용자 정의 엔드포인트 만들기**
|
||||
|
||||
#### 기본 구조
|
||||
|
||||
```java
|
||||
@Component
|
||||
@Endpoint(id = "customstatus")
|
||||
public class CustomStatusEndpoint {
|
||||
|
||||
@ReadOperation
|
||||
public Map<String, Object> customStatus() {
|
||||
return Map.of(
|
||||
"service", "My Service",
|
||||
"status", "Running",
|
||||
"timestamp", Instant.now()
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> `GET /actuator/customstatus` 호출 시 결과 반환
|
||||
|
||||
```json
|
||||
{
|
||||
"service": "My Service",
|
||||
"status": "Running",
|
||||
"timestamp": "2025-04-07T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
#### WriteOperation 예시 (POST)
|
||||
|
||||
```java
|
||||
@WriteOperation
|
||||
public String updateSomething(@Selector String id, @Nullable String value) {
|
||||
// 업데이트 처리
|
||||
return "Updated " + id + " to " + value;
|
||||
}
|
||||
```
|
||||
|
||||
> POST `/actuator/customstatus/myconfig?value=test` 등으로 호출 가능
|
||||
|
||||
---
|
||||
|
||||
### **15장. 상태 분류 및 외부 연동 대응**
|
||||
|
||||
#### Health 상태 분류 예시
|
||||
|
||||
```yaml
|
||||
management:
|
||||
health:
|
||||
status:
|
||||
http-mapping:
|
||||
DOWN: 503
|
||||
OUT_OF_SERVICE: 503
|
||||
UP: 200
|
||||
UNKNOWN: 200
|
||||
```
|
||||
|
||||
> Kubernetes, AWS 등 상태 확인을 위한 HTTP 상태 코드 제어 가능
|
||||
|
||||
---
|
||||
|
||||
### **요약**
|
||||
|
||||
- 액추에이터는 민감한 정보를 다루므로 보안 설정이 필수
|
||||
- 엔드포인트는 개별적으로 비활성화하거나 커스터마이징 가능
|
||||
- 사용자 정의 엔드포인트로 비즈니스 모니터링도 가능
|
||||
- 상태 값은 외부 환경(K8s 등)에 맞게 분류할 수 있음
|
||||
|
||||
---
|
||||
|
||||
다음은 **4부. 운영 환경 통합**으로 Prometheus, Grafana, Spring Boot Admin 같은 외부 시스템과의 통합 내용을 다루면 좋아. 계속 이어서 써줄까?
|
||||
157
docs/actuator/04_prometheus.md
Normal file
157
docs/actuator/04_prometheus.md
Normal file
@@ -0,0 +1,157 @@
|
||||
좋아, 이제 **4부. 운영 환경 통합**을 작성해볼게. 이 파트에서는 액추에이터를 실무에서 자주 사용하는 외부 도구들과 연동하는 방법을 다룰게. 특히 Prometheus, Grafana, Spring Boot Admin, 클라우드 환경 중심으로 구성할게.
|
||||
|
||||
---
|
||||
|
||||
## **4부. 운영 환경 통합**
|
||||
|
||||
---
|
||||
|
||||
### **16장. Prometheus & Grafana 연동**
|
||||
|
||||
#### Micrometer + Prometheus 설정
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation 'io.micrometer:micrometer-registry-prometheus'
|
||||
}
|
||||
```
|
||||
|
||||
```yaml
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: prometheus
|
||||
metrics:
|
||||
export:
|
||||
prometheus:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
> 결과: `/actuator/prometheus` 엔드포인트에서 Prometheus 포맷의 메트릭 제공
|
||||
|
||||
#### Prometheus 설정 예시 (`prometheus.yml`)
|
||||
|
||||
```yaml
|
||||
scrape_configs:
|
||||
- job_name: 'spring-app'
|
||||
metrics_path: '/actuator/prometheus'
|
||||
static_configs:
|
||||
- targets: ['localhost:8080']
|
||||
```
|
||||
|
||||
#### Grafana 대시보드 구성
|
||||
|
||||
- 데이터 소스: Prometheus
|
||||
- 대시보드: Micrometer for JVM preset 사용 가능
|
||||
- JVM 메모리, GC, HTTP 요청 등 실시간 그래프 시각화
|
||||
|
||||
---
|
||||
|
||||
### **17장. Spring Boot Admin 연동**
|
||||
|
||||
#### 의존성 추가
|
||||
|
||||
```kotlin
|
||||
implementation 'de.codecentric:spring-boot-admin-starter-client'
|
||||
```
|
||||
|
||||
#### 클라이언트 애플리케이션 설정
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
url: http://localhost:8081 # Spring Boot Admin 서버 주소
|
||||
```
|
||||
|
||||
> 액추에이터 엔드포인트가 자동 등록되어 UI로 상태, 메트릭, 로그 등 확인 가능
|
||||
|
||||
#### 서버 애플리케이션 설정 (별도 프로젝트)
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation 'de.codecentric:spring-boot-admin-starter-server'
|
||||
implementation 'spring-boot-starter-security'
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
@EnableAdminServer
|
||||
@SpringBootApplication
|
||||
public class AdminServerApp {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AdminServerApp.class, args);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **18장. 클라우드 환경에서의 액추에이터 활용**
|
||||
|
||||
#### Kubernetes 헬스체크에 활용
|
||||
|
||||
```yaml
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /actuator/health/liveness
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /actuator/health/readiness
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 10
|
||||
```
|
||||
|
||||
> `liveness`/`readiness` 구성은 아래처럼 사용자 정의 가능
|
||||
|
||||
```java
|
||||
@Component("readiness")
|
||||
public class ReadinessHealthIndicator implements HealthIndicator {
|
||||
@Override
|
||||
public Health health() {
|
||||
// DB 연결 확인, 외부 API 체크 등
|
||||
return Health.up().build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### AWS CloudWatch 또는 Azure Monitor 연동
|
||||
- Micrometer에서 제공하는 별도 레지스트리 사용
|
||||
- AWS 예시: `micrometer-registry-cloudwatch2` 사용
|
||||
- Azure 예시: `micrometer-registry-azure-monitor`
|
||||
|
||||
---
|
||||
|
||||
### **19장. Docker + 액추에이터**
|
||||
|
||||
#### Dockerfile 예시
|
||||
|
||||
```dockerfile
|
||||
FROM openjdk:17
|
||||
COPY build/libs/app.jar app.jar
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||
```
|
||||
|
||||
#### Docker + Prometheus 연동
|
||||
- 동일 네트워크에서 Prometheus가 액추에이터 엔드포인트를 스크래핑하도록 구성
|
||||
- 컨테이너 내에서 보안 설정이나 포트 노출 주의
|
||||
|
||||
---
|
||||
|
||||
### **요약**
|
||||
|
||||
- 액추에이터는 Prometheus, Grafana, Spring Boot Admin 등 외부 도구와 쉽게 통합됨
|
||||
- Kubernetes 등 클라우드 환경에서 liveness/readiness probe로 활용 가능
|
||||
- 실시간 모니터링, 장애 탐지, 자원 사용량 분석에 필수적인 구성 요소임
|
||||
|
||||
---
|
||||
|
||||
이제 마지막 **5부. 실전 활용과 사례 연구**로 이어지면 완성형 실무서 느낌이 날 거야. 계속 써줄까?
|
||||
146
docs/actuator/05_ex.md
Normal file
146
docs/actuator/05_ex.md
Normal file
@@ -0,0 +1,146 @@
|
||||
좋아, 마지막 **5부. 실전 활용과 사례 연구**를 작성해볼게. 이 파트는 실무에서 액추에이터를 어떻게 문제 해결, 무중단 배포, 장애 대응에 활용할 수 있는지를 중심으로 구성할게.
|
||||
|
||||
---
|
||||
|
||||
## **5부. 실전 활용과 사례 연구**
|
||||
|
||||
---
|
||||
|
||||
### **20장. 액추에이터 기반 장애 진단**
|
||||
|
||||
#### 메모리 부족 및 GC 감지
|
||||
|
||||
- `/actuator/metrics/jvm.memory.used`, `/jvm.gc.pause` 활용
|
||||
- 메모리 사용량이 한계치 근접 시 Slack, 이메일 알림 연동 가능
|
||||
|
||||
#### 예시: 알림 트리거 (Spring + Slack Webhook)
|
||||
|
||||
```java
|
||||
if (memoryUsed > threshold) {
|
||||
slackService.notify("Memory usage high: " + memoryUsed);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 슬로우 요청 감지
|
||||
|
||||
- `/actuator/metrics/http.server.requests`
|
||||
- `max`, `mean`, `count`, `percentile` 등 분석 가능
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "http.server.requests",
|
||||
"measurements": [
|
||||
{ "statistic": "count", "value": 1500 },
|
||||
{ "statistic": "max", "value": 5.1 },
|
||||
{ "statistic": "mean", "value": 0.4 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> 평균 응답 시간이 급증하면 서비스 병목 가능성 있음
|
||||
|
||||
---
|
||||
|
||||
### **21장. 헬스 체크 기반 무중단 배포 전략**
|
||||
|
||||
#### Blue-Green / Rolling 배포
|
||||
|
||||
- `readiness` 체크를 통해 트래픽 분산
|
||||
- 서비스 준비가 되지 않은 인스턴스는 요청을 받지 않음
|
||||
|
||||
#### Kubernetes 예시
|
||||
|
||||
```yaml
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /actuator/health/readiness
|
||||
port: 8080
|
||||
```
|
||||
|
||||
> readiness 상태가 `UP`이 될 때까지만 트래픽 전달
|
||||
|
||||
---
|
||||
|
||||
#### Custom readiness indicator
|
||||
|
||||
```java
|
||||
@Component("readiness")
|
||||
public class CustomReadinessIndicator implements HealthIndicator {
|
||||
public Health health() {
|
||||
if (dbIsReady()) {
|
||||
return Health.up().build();
|
||||
}
|
||||
return Health.down().withDetail("error", "DB not connected").build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **22장. 실무 프로젝트 적용 사례**
|
||||
|
||||
#### 사례 1. 마이크로서비스 아키텍처 모니터링
|
||||
|
||||
- 수십 개의 서비스 각각 `/actuator/health`, `/metrics` 제공
|
||||
- Prometheus가 모든 서비스 상태 및 메트릭 수집
|
||||
- Grafana에서 전체 시스템 상태 시각화
|
||||
|
||||
#### 사례 2. 장애 조기 감지 시스템 구축
|
||||
|
||||
- 액추에이터 메트릭 + 슬랙 연동
|
||||
- 알림 예시:
|
||||
- DB 커넥션 풀 소진 경고
|
||||
- 평균 응답 시간 증가
|
||||
- 디스크 공간 부족
|
||||
|
||||
#### 사례 3. 운영 리소스 최적화
|
||||
|
||||
- `/metrics` 기반으로 JVM Heap 사이즈 분석
|
||||
- GC 튜닝 전후 성능 비교
|
||||
- 특정 컨트롤러 호출 수 분석하여 리팩토링 대상 선정
|
||||
|
||||
---
|
||||
|
||||
### **23장. 운영 자동화와 연동 사례**
|
||||
|
||||
#### Jenkins + 액추에이터 연동
|
||||
|
||||
- 배포 후 `/actuator/health`로 상태 확인 후 다음 스텝 진행
|
||||
- 실패 시 롤백 스크립트 실행
|
||||
|
||||
#### 예시
|
||||
|
||||
```groovy
|
||||
stage('Health Check') {
|
||||
steps {
|
||||
script {
|
||||
def status = sh(script: 'curl -s http://app:8080/actuator/health | jq -r .status', returnStdout: true).trim()
|
||||
if (status != 'UP') {
|
||||
error("App is not healthy")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **요약**
|
||||
|
||||
- 액추에이터는 단순한 상태 확인 도구를 넘어, 장애 진단, 성능 분석, 배포 전략의 핵심 역할을 한다
|
||||
- 실시간 모니터링과 연동하면 복잡한 운영 환경에서도 안정성을 확보할 수 있다
|
||||
- 마이크로서비스, 클라우드, CI/CD 파이프라인 등 다양한 환경에서 유용하게 쓰인다
|
||||
|
||||
---
|
||||
|
||||
### **부록**
|
||||
|
||||
- A. 주요 설정 정리표
|
||||
- B. Micrometer 메트릭 이름과 설명 목록
|
||||
- C. 에러 사례별 Trouble Shooting 가이드
|
||||
|
||||
---
|
||||
|
||||
이걸로 **스프링 부트 액추에이터 실전 가이드**의 전체 구성은 완성이야. 필요하다면 각 부에 대해 더 자세한 세부 목차나 코드 중심 실습 내용도 추가할 수 있어. 출간용 원고로 다듬을 계획이야?
|
||||
65
docs/actuator/06_setting.md
Normal file
65
docs/actuator/06_setting.md
Normal file
@@ -0,0 +1,65 @@
|
||||
좋아, 주요 설정들을 `application.yml` 형식으로 실무에 바로 적용할 수 있게 정리해줄게. 설명이 필요한 항목에는 주석도 달아뒀어.
|
||||
|
||||
---
|
||||
|
||||
```yaml
|
||||
# Actuator 엔드포인트 노출 설정
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health, info, metrics, prometheus, loggers, customstatus
|
||||
base-path: /actuator # 기본 경로 변경 가능
|
||||
endpoint:
|
||||
health:
|
||||
show-details: always # 상세 정보 노출 (UP, DOWN 원인 포함)
|
||||
status:
|
||||
http-mapping:
|
||||
UP: 200
|
||||
DOWN: 503
|
||||
OUT_OF_SERVICE: 503
|
||||
UNKNOWN: 200
|
||||
metrics:
|
||||
enabled: true
|
||||
prometheus:
|
||||
enabled: true
|
||||
loggers:
|
||||
enabled: true
|
||||
|
||||
metrics:
|
||||
export:
|
||||
prometheus:
|
||||
enabled: true # Prometheus 연동 시 필수
|
||||
|
||||
server:
|
||||
port: 8080 # Actuator 별도 포트 지정 가능
|
||||
|
||||
# info 엔드포인트에 표시할 정보
|
||||
info:
|
||||
app:
|
||||
name: actuator-demo
|
||||
version: 1.0.0
|
||||
author: your-name
|
||||
|
||||
# 보안 설정 예시 (기본 사용자)
|
||||
spring:
|
||||
security:
|
||||
user:
|
||||
name: admin
|
||||
password: admin123
|
||||
|
||||
# Spring Boot Admin 연동 시
|
||||
spring:
|
||||
boot:
|
||||
admin:
|
||||
client:
|
||||
url: http://localhost:8081 # Admin 서버 주소
|
||||
|
||||
# 커스텀 readiness 체크용 헬스 인디케이터 활용 시
|
||||
# readinessProbe, livenessProbe와 연동 가능
|
||||
# (이 설정은 Kubernetes 매니페스트에서 활용됨)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
필요하다면 `logging`, `management.trace.http`, `custom endpoint` 관련 설정도 추가할 수 있어. 특정 환경(AWS, Docker, K8s 등)에 맞춰 세부 설정도 정리해줄까?
|
||||
35
docs/actuator/07_metric.md
Normal file
35
docs/actuator/07_metric.md
Normal file
@@ -0,0 +1,35 @@
|
||||
좋아, Spring Boot Actuator에서 Micrometer를 통해 기본 제공되는 **주요 메트릭 이름과 설명**을 실무 중심으로 정리해줄게. 아래 표는 JVM, HTTP, DB 연결, 시스템 리소스 관련 메트릭들을 중심으로 구성했어.
|
||||
|
||||
---
|
||||
|
||||
### **Actuator 메트릭 이름 정리표**
|
||||
|
||||
| 메트릭 이름 | 설명 |
|
||||
|--------------------------------------|----------------------------------------------------------------------|
|
||||
| `jvm.memory.used` | JVM 힙/비힙 메모리 사용량 |
|
||||
| `jvm.memory.max` | JVM 최대 힙/비힙 메모리 크기 |
|
||||
| `jvm.gc.pause` | GC 수행 시간 (pause 시간) |
|
||||
| `jvm.threads.live` | 현재 살아있는 쓰레드 수 |
|
||||
| `jvm.threads.daemon` | 데몬 쓰레드 수 |
|
||||
| `jvm.threads.peak` | 피크 쓰레드 수 |
|
||||
| `system.cpu.usage` | 시스템 전체 CPU 사용률 (0.0 ~ 1.0) |
|
||||
| `process.cpu.usage` | 현재 애플리케이션의 CPU 사용률 (0.0 ~ 1.0) |
|
||||
| `system.load.average.1m` | 1분 평균 시스템 부하 |
|
||||
| `disk.free` | 남은 디스크 공간 (바이트) |
|
||||
| `disk.total` | 전체 디스크 용량 (바이트) |
|
||||
| `http.server.requests` | HTTP 요청 수, 응답 시간, 에러 비율 등 |
|
||||
| `tomcat.sessions.active.current` | 현재 활성 세션 수 |
|
||||
| `tomcat.sessions.rejected` | 최대 세션 초과로 거절된 세션 수 |
|
||||
| `tomcat.threads.current` | 현재 사용 중인 톰캣 쓰레드 수 |
|
||||
| `logback.events` | 로그 수준별 로깅 이벤트 수 (info, warn, error 등) |
|
||||
| `datasource.connections.active` | 현재 사용 중인 DB 커넥션 수 |
|
||||
| `datasource.connections.max` | 최대 커넥션 수 |
|
||||
| `datasource.connections.pending` | 대기 중인 커넥션 요청 수 |
|
||||
| `hikaricp.connections.active` | HikariCP에서 사용 중인 커넥션 수 |
|
||||
| `hikaricp.connections.idle` | 유휴 커넥션 수 |
|
||||
| `hikaricp.connections.max` | 최대 커넥션 수 |
|
||||
| `hikaricp.connections.pending` | 커넥션 풀에 요청 대기 중인 수 |
|
||||
|
||||
---
|
||||
|
||||
필요하면 특정 메트릭의 `tag` 구조(예: `uri`, `status`, `exception` 등)나, Prometheus에서 쿼리하는 예시도 함께 정리해줄 수 있어. 원할까?
|
||||
93
docs/actuator/README.md
Normal file
93
docs/actuator/README.md
Normal file
@@ -0,0 +1,93 @@
|
||||
좋아, 스프링 부트 액추에이터(Spring Boot Actuator)를 주제로 한 책이라면 실무적인 모니터링, 상태 점검, 보안, 커스터마이징까지 다루는 구성이 좋을 것 같아. 아래는 그에 맞춰 제안하는 목차야:
|
||||
|
||||
---
|
||||
|
||||
### **스프링 부트 액추에이터 실전 가이드**
|
||||
|
||||
#### **1부. 개요와 시작**
|
||||
|
||||
1. 스프링 부트 액추에이터란?
|
||||
- 모니터링의 필요성
|
||||
- 액추에이터의 기본 개념
|
||||
2. 기본 설정과 의존성 추가
|
||||
- Gradle/Maven 설정
|
||||
- 기본 endpoint 확인
|
||||
3. `/actuator` 엔드포인트 둘러보기
|
||||
- 기본으로 제공되는 엔드포인트 목록
|
||||
- Health, Info, Metrics 등 소개
|
||||
|
||||
---
|
||||
|
||||
#### **2부. 주요 엔드포인트 상세 분석**
|
||||
|
||||
4. Health 엔드포인트
|
||||
- 커스터마이징
|
||||
- 구성 요소 상태 추가
|
||||
5. Info 엔드포인트
|
||||
- Git 정보 연동
|
||||
- 커스텀 정보 추가하기
|
||||
6. Metrics 엔드포인트
|
||||
- Micrometer와 통합
|
||||
- 주요 메트릭 수집 항목 설명
|
||||
7. Loggers 엔드포인트
|
||||
- 런타임 로깅 레벨 변경
|
||||
8. HTTP Trace & Mappings
|
||||
- HTTP 요청 추적
|
||||
- 매핑 정보 노출
|
||||
9. Beans, Conditions, Configprops
|
||||
- 빈 목록과 의존성 분석
|
||||
- 자동 구성 진단
|
||||
|
||||
---
|
||||
|
||||
#### **3부. 보안과 커스터마이징**
|
||||
|
||||
10. 액추에이터 엔드포인트 보안
|
||||
- WebSecurityConfigurer 설정
|
||||
- Role 기반 접근 제어
|
||||
11. 엔드포인트 커스터마이징
|
||||
- 경로, 활성화 여부, 노출 수준 조정
|
||||
- Management 포트 분리
|
||||
12. 사용자 정의 엔드포인트 만들기
|
||||
- @Endpoint, @ReadOperation 등 사용법
|
||||
- RESTful 스타일로 확장
|
||||
|
||||
---
|
||||
|
||||
#### **4부. 운영 환경 통합**
|
||||
|
||||
13. Prometheus, Grafana와의 연동
|
||||
- Micrometer Prometheus 설정
|
||||
- Grafana 대시보드 구성
|
||||
14. Spring Boot Admin과 통합
|
||||
- 관리 UI 제공
|
||||
- 서비스 헬스 체크 및 메트릭 확인
|
||||
15. 클라우드 환경에서의 액추에이터
|
||||
- Kubernetes, AWS 등에서 활용하기
|
||||
- 헬스 프로브로 활용
|
||||
|
||||
---
|
||||
|
||||
#### **5부. 실전 활용과 사례 연구**
|
||||
|
||||
16. 액추에이터 기반 장애 진단
|
||||
- CPU, GC, Memory 추적
|
||||
- 슬로우 요청 분석
|
||||
17. 서비스 헬스 기반 무중단 배포
|
||||
- Readiness / Liveness 체크
|
||||
- 롤링 배포 전략과 연계
|
||||
18. 실무 프로젝트 적용 사례
|
||||
- 마이크로서비스에서의 활용
|
||||
- 로깅/모니터링 통합 전략
|
||||
|
||||
---
|
||||
|
||||
#### **부록**
|
||||
|
||||
- A. 액추에이터 전체 설정 옵션 정리
|
||||
- B. Micrometer 주요 태그 및 메트릭 종류
|
||||
- C. Trouble Shooting 가이드
|
||||
|
||||
---
|
||||
|
||||
이 구성은 기초부터 시작해 실전 배포 환경까지 연결되도록 구성했어. 필요하면 각 장을 좀 더 쪼개거나, Kubernetes, AWS, Docker 등 특정 환경에 초점을 둔 부록도 추가할 수 있어. 원하면 각 장의 세부 목차도 만들어줄게.
|
||||
13
docs/app.sh
Executable file
13
docs/app.sh
Executable file
@@ -0,0 +1,13 @@
|
||||
#! /usr/bin/env bash
|
||||
|
||||
case $1 in
|
||||
gitup)
|
||||
echo "Pushing to git"
|
||||
git add -A
|
||||
git commit -m $(date "+%Y-%m-%dT%H:%M:%S")
|
||||
git push origin
|
||||
;;
|
||||
*)
|
||||
echo "Launching editor"
|
||||
code .
|
||||
esac
|
||||
432
docs/caching.md
Normal file
432
docs/caching.md
Normal file
@@ -0,0 +1,432 @@
|
||||
### 스프링 부트의 캐싱에 대한 설명
|
||||
|
||||
스프링 부트에서 캐싱(Caching)은 애플리케이션 성능을 개선하기 위해 자주 사용되는 데이터나 계산 결과를 메모리에 저장하여 반복적인 처리 비용을 줄이는 메커니즘입니다. 스프링은 캐싱 추상화(Cache Abstraction)를 제공하며, 이를 통해 다양한 캐시 구현체(EhCache, Caffeine, Redis 등)를 쉽게 통합할 수 있습니다. 스프링 부트는 기본적으로 캐싱을 활성화하고 설정하기 위한 어노테이션과 클래스를 제공하며, 최소한의 설정으로 캐싱을 적용할 수 있습니다.
|
||||
|
||||
#### 1. 캐싱의 기본 동작
|
||||
- **캐시 히트(Cache Hit)**: 요청된 데이터가 캐시에 존재하면 캐시에서 반환.
|
||||
- **캐시 미스(Cache Miss)**: 캐시에 없으면 원본 데이터 소스(DB 등)에서 조회 후 캐시에 저장.
|
||||
- **캐시 관리**: 캐시 데이터의 추가, 갱신, 삭제를 어노테이션으로 제어.
|
||||
|
||||
#### 2. 캐싱 활성화
|
||||
스프링 부트에서 캐싱을 사용하려면 `@EnableCaching` 어노테이션을 설정 클래스에 추가합니다.
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class CacheConfig {
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 캐시 구현체
|
||||
스프링 부트는 기본적으로 `ConcurrentMapCacheManager`를 사용하지만, 의존성을 추가해 다른 캐시 구현체를 사용할 수 있습니다:
|
||||
- **EhCache**: `spring-boot-starter-cache` + `ehcache.xml`.
|
||||
- **Caffeine**: `com.github.ben-manes.caffeine:caffeine`.
|
||||
- **Redis**: `spring-boot-starter-data-redis`.
|
||||
|
||||
#### 4. 설정 예시 (`application.yaml`)
|
||||
```yaml
|
||||
spring:
|
||||
cache:
|
||||
type: caffeine # 캐시 구현체 지정
|
||||
caffeine:
|
||||
spec: maximumSize=500,expireAfterWrite=600s # Caffeine 설정
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 캐싱 관련 어노테이션 표
|
||||
|
||||
| **어노테이션** | **설명** | **사용 예시** |
|
||||
|-----------------------|-------------------------------------------------------------------------------------------|------------------------------------------------|
|
||||
| `@EnableCaching` | 애플리케이션에서 캐싱 기능을 활성화 | `@EnableCaching` (설정 클래스에 추가) |
|
||||
| `@Cacheable` | 메서드 결과를 캐시에 저장하고, 동일한 요청 시 캐시에서 반환 | `@Cacheable("users")` |
|
||||
| `@CachePut` | 메서드 실행 결과를 캐시에 갱신 (캐시를 항상 업데이트) | `@CachePut(value = "users", key = "#userId")` |
|
||||
| `@CacheEvict` | 캐시에서 특정 데이터를 제거 | `@CacheEvict(value = "users", key = "#userId")`|
|
||||
| `@Caching` | 여러 캐싱 어노테이션(`@Cacheable`, `@CachePut`, `@CacheEvict`)을 조합 | `@Caching(evict = {@CacheEvict("users")})` |
|
||||
| `@CacheConfig` | 클래스 수준에서 캐시 설정(캐시 이름 등)을 공통으로 정의 | `@CacheConfig(cacheNames = {"users"})` |
|
||||
|
||||
- **참고**:
|
||||
- `@Cacheable`: 캐시가 없으면 메서드 실행 후 캐시에 저장, 있으면 캐시 반환.
|
||||
- `@CachePut`: 항상 메서드를 실행하고 결과를 캐시에 반영 (업데이트용).
|
||||
- `@CacheEvict`: 캐시를 지워 데이터 최신성을 유지.
|
||||
|
||||
---
|
||||
|
||||
### 캐싱 관련 주요 클래스 표
|
||||
|
||||
| **클래스** | **설명** | **주요 사용처** |
|
||||
|--------------------------|-------------------------------------------------------------------------------------------|-----------------------------------------------|
|
||||
| `CacheManager` | 캐시를 관리하는 인터페이스, 캐시 생성 및 조회 | 캐시 구현체 설정 (예: `CaffeineCacheManager`) |
|
||||
| `Cache` | 개별 캐시 인스턴스를 나타내며, 데이터 저장/조회/삭제 제공 | `CacheManager.getCache()`로 획득 |
|
||||
| `ConcurrentMapCacheManager` | 기본 캐시 매니저, `ConcurrentHashMap`을 기반으로 메모리 캐싱 | 기본 캐싱 설정 |
|
||||
| `CaffeineCacheManager` | Caffeine 캐시를 사용하는 캐시 매니저 | 고성능 메모리 캐싱 |
|
||||
| `RedisCacheManager` | Redis를 캐시 저장소로 사용하는 캐시 매니저 | 분산 캐싱 |
|
||||
| `EhCacheCacheManager` | EhCache를 사용하는 캐시 매니저 | 복잡한 캐시 설정 |
|
||||
| `SimpleCacheManager` | 사용자 정의 캐시를 직접 설정할 수 있는 간단한 캐시 매니저 | 테스트용 |
|
||||
| `CacheResolver` | 캐시 이름을 동적으로 결정하는 인터페이스 | 커스텀 캐시 선택 |
|
||||
| `KeyGenerator` | 캐시 키를 생성하는 인터페이스 | 커스텀 키 생성 로직 |
|
||||
|
||||
---
|
||||
|
||||
### 캐싱 사용 예시
|
||||
|
||||
#### 1. 기본 캐싱 적용
|
||||
```java
|
||||
@Service
|
||||
@CacheConfig(cacheNames = {"users"})
|
||||
public class UserService {
|
||||
|
||||
@Cacheable(key = "#userId") // userId를 키로 캐싱
|
||||
public User getUser(Long userId) {
|
||||
// DB 조회 (캐시 미스 시 실행)
|
||||
return userRepository.findById(userId).orElse(null);
|
||||
}
|
||||
|
||||
@CachePut(key = "#user.id") // 캐시 갱신
|
||||
public User updateUser(User user) {
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
@CacheEvict(key = "#userId") // 캐시 삭제
|
||||
public void deleteUser(Long userId) {
|
||||
userRepository.deleteById(userId);
|
||||
}
|
||||
}
|
||||
```
|
||||
- **동작**:
|
||||
- `getUser(1)` 호출 시 캐시에 없으면 DB 조회 후 캐시에 저장.
|
||||
- 동일한 `userId`로 다시 호출 시 캐시에서 바로 반환.
|
||||
- `updateUser()` 호출 시 캐시 갱신.
|
||||
- `deleteUser()` 호출 시 캐시 제거.
|
||||
|
||||
#### 2. Caffeine 캐시 설정
|
||||
```java
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class CacheConfig {
|
||||
|
||||
@Bean
|
||||
public CacheManager cacheManager() {
|
||||
CaffeineCacheManager cacheManager = new CaffeineCacheManager("users");
|
||||
cacheManager.setCaffeine(Caffeine.newBuilder()
|
||||
.expireAfterWrite(10, TimeUnit.MINUTES) // 10분 후 만료
|
||||
.maximumSize(1000)); // 최대 1000개 항목
|
||||
return cacheManager;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Redis 캐시 설정
|
||||
```yaml
|
||||
spring:
|
||||
cache:
|
||||
type: redis
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
```
|
||||
```java
|
||||
@Configuration
|
||||
@EnableCaching
|
||||
public class RedisCacheConfig {
|
||||
|
||||
@Bean
|
||||
public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) {
|
||||
return RedisCacheManager.create(connectionFactory);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 추가 설명 및 팁
|
||||
- **캐시 키(Key)**: `@Cacheable(key = "#파라미터명")`으로 메서드 파라미터를 키로 사용하거나, 커스텀 `KeyGenerator`로 복잡한 키 생성 가능.
|
||||
- **조건적 캐싱**: `@Cacheable(condition = "#result != null")`로 결과가 `null`이 아닌 경우에만 캐싱.
|
||||
- **동기화**: `@Cacheable(sync = true)`로 동시 요청 시 메서드 실행을 동기화.
|
||||
- **캐시 구현체 선택**: 메모리 사용량이 적으면 `Caffeine`, 분산 환경이면 `Redis` 추천.
|
||||
|
||||
---
|
||||
|
||||
### 결론
|
||||
스프링 부트의 캐싱은 `@EnableCaching`으로 활성화하고, `@Cacheable`, `@CachePut`, `@CacheEvict` 등의 어노테이션으로 간단히 적용할 수 있습니다. `CacheManager`와 같은 클래스를 활용해 캐시 저장소를 커스터마이징하며, Caffeine, Redis 등 다양한 구현체로 성능과 확장성을 높일 수 있습니다. 이를 통해 데이터베이스 부하를 줄이고 응답 속도를 개선할 수 있는 강력한 도구입니다. 추가 질문이 있다면 언제든 물어보세요!
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
스프링 부트에서 캐싱과 관련된 `application.yaml` 설정 예시를 제공하고, 주요 옵션을 표로 정리하겠습니다. 스프링 부트는 캐싱 추상화를 통해 다양한 캐시 구현체(예: Caffeine, Redis, EhCache 등)를 지원하며, `application.yaml` 파일에서 캐시 종류와 세부 설정을 정의할 수 있습니다.
|
||||
|
||||
---
|
||||
|
||||
### `application.yaml` 예시
|
||||
|
||||
아래는 Caffeine과 Redis 캐시를 각각 개발 환경과 프로덕션 환경에서 사용하는 설정 예시입니다.
|
||||
|
||||
```yaml
|
||||
# 공통 캐시 설정
|
||||
spring:
|
||||
cache:
|
||||
cache-names: users, products # 사용할 캐시 이름 목록
|
||||
|
||||
# 개발 환경 (Caffeine 캐시)
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: dev
|
||||
cache:
|
||||
type: caffeine # Caffeine 캐시 사용
|
||||
caffeine:
|
||||
spec: maximumSize=500,expireAfterWrite=600s # 최대 500개, 10분 후 만료
|
||||
|
||||
# 프로덕션 환경 (Redis 캐시)
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: prod
|
||||
cache:
|
||||
type: redis # Redis 캐시 사용
|
||||
redis:
|
||||
time-to-live: 3600000 # 캐시 항목의 기본 TTL (1시간, 밀리초 단위)
|
||||
cache-null-values: false # null 값 캐싱 여부
|
||||
use-key-prefix: true # 캐시 키에 접두사 사용 여부
|
||||
key-prefix: myapp: # 커스텀 키 접두사
|
||||
redis:
|
||||
host: localhost # Redis 서버 호스트
|
||||
port: 6379 # Redis 서버 포트
|
||||
password: mypassword # Redis 인증 비밀번호 (선택)
|
||||
```
|
||||
|
||||
#### 실행 방법
|
||||
- 개발 환경: `java -jar myapp.jar -Dspring.profiles.active=dev`
|
||||
- 프로덕션 환경: `java -jar myapp.jar -Dspring.profiles.active=prod`
|
||||
|
||||
---
|
||||
|
||||
### 캐시 설정 옵션 표
|
||||
|
||||
#### 1. 공통 캐시 설정
|
||||
| **옵션** | **설명** | **예시 값** |
|
||||
|---------------------------|-------------------------------------------------------------------------------------------|-----------------------|
|
||||
| `spring.cache.type` | 사용할 캐시 구현체 지정 (`caffeine`, `redis`, `ehcache`, `concurrentMap`, `none` 등) | `caffeine` |
|
||||
| `spring.cache.cache-names`| 애플리케이션에서 사용할 캐시 이름 목록 (미리 정의 시 성능 최적화 가능) | `users, products` |
|
||||
|
||||
#### 2. Caffeine 캐시 설정
|
||||
| **옵션** | **설명** | **예시 값** |
|
||||
|---------------------------|-------------------------------------------------------------------------------------------|-----------------------|
|
||||
| `spring.cache.caffeine.spec` | Caffeine 캐시의 세부 설정 (최대 크기, 만료 시간 등, Caffeine Spec 형식) | `maximumSize=500,expireAfterWrite=600s` |
|
||||
|
||||
- **Caffeine Spec 세부 옵션**:
|
||||
- `maximumSize`: 캐시 항목 최대 개수.
|
||||
- `expireAfterWrite`: 쓰기 후 만료 시간 (초 단위, `s` 접미사).
|
||||
- `expireAfterAccess`: 마지막 접근 후 만료 시간.
|
||||
- 예: `maximumSize=1000,expireAfterWrite=300s` (최대 1000개, 5분 후 만료).
|
||||
|
||||
#### 3. Redis 캐시 설정
|
||||
| **옵션** | **설명** | **예시 값** |
|
||||
|---------------------------|-------------------------------------------------------------------------------------------|-----------------------|
|
||||
| `spring.cache.redis.time-to-live` | 캐시 항목의 기본 TTL(Time To Live, 밀리초 단위) | `3600000` (1시간) |
|
||||
| `spring.cache.redis.cache-null-values` | `null` 값을 캐싱할지 여부 (기본값: `true`) | `false` |
|
||||
| `spring.cache.redis.use-key-prefix` | 캐시 키에 접두사를 사용할지 여부 (기본값: `true`) | `true` |
|
||||
| `spring.cache.redis.key-prefix` | 캐시 키에 적용할 커스텀 접두사 (기본값은 캐시 이름) | `myapp:` |
|
||||
|
||||
#### 4. Redis 연결 설정 (캐시와 별개로 Redis 서버 연결에 필요)
|
||||
| **옵션** | **설명** | **예시 값** |
|
||||
|---------------------------|-------------------------------------------------------------------------------------------|-----------------------|
|
||||
| `spring.redis.host` | Redis 서버 호스트 주소 | `localhost` |
|
||||
| `spring.redis.port` | Redis 서버 포트 번호 | `6379` |
|
||||
| `spring.redis.password` | Redis 서버 접속 비밀번호 (선택 사항) | `mypassword` |
|
||||
| `spring.redis.database` | 사용할 Redis 데이터베이스 인덱스 (기본값: `0`) | `1` |
|
||||
|
||||
---
|
||||
|
||||
### 옵션 설명 및 사용 팁
|
||||
|
||||
1. **`spring.cache.type`**
|
||||
- **설명**: 캐시 구현체를 지정합니다. 기본값은 `concurrentMap`이며, 별도 의존성 없이 메모리 기반 캐싱을 제공합니다.
|
||||
- **팁**: 개발 환경에서는 `caffeine`, 분산 환경에서는 `redis` 추천.
|
||||
|
||||
2. **`spring.cache.caffeine.spec`**
|
||||
- **설명**: Caffeine 캐시의 동작을 제어합니다. 단일 문자열로 여러 속성을 조합하며, 콤마(`,`)로 구분합니다.
|
||||
- **예시**: `maximumSize=500,expireAfterWrite=600s`는 최대 500개 항목을 10분 동안 유지.
|
||||
|
||||
3. **`spring.cache.redis.time-to-live`**
|
||||
- **설명**: Redis 캐시 항목의 기본 만료 시간을 설정합니다. 개별 캐시에 `@Cacheable`로 별도 TTL을 지정할 수도 있습니다.
|
||||
- **팁**: 밀리초 단위로 설정하므로, 초 단위로 계산 후 변환 (예: 1시간 = `3600000`).
|
||||
|
||||
4. **`spring.cache.redis.key-prefix`**
|
||||
- **설명**: Redis 키 충돌을 방지하기 위해 접두사를 추가합니다. 예를 들어, `myapp:users:1`처럼 저장됨.
|
||||
- **팁**: 여러 애플리케이션이 동일한 Redis를 사용할 경우 필수.
|
||||
|
||||
---
|
||||
|
||||
### 의존성 추가
|
||||
- **Caffeine**:
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||
<artifactId>caffeine</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
- **Redis**:
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 추가 참고
|
||||
- **캐시 이름**: `spring.cache.cache-names`로 미리 정의하면 캐시 초기화 시 성능이 개선됩니다.
|
||||
- **프로파일 활용**: 위 예시처럼 `dev`와 `prod` 프로파일로 캐시 구현체를 다르게 설정 가능.
|
||||
- **커스텀 설정**: 더 복잡한 요구사항은 `CacheManager` 빈을 직접 정의해 제어할 수 있습니다:
|
||||
```java
|
||||
@Bean
|
||||
public CacheManager cacheManager() {
|
||||
CaffeineCacheManager cacheManager = new CaffeineCacheManager("users");
|
||||
cacheManager.setCaffeine(Caffeine.newBuilder().maximumSize(1000));
|
||||
return cacheManager;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 결론
|
||||
`application.yaml`을 사용하면 캐시 구현체와 세부 설정을 간단히 정의할 수 있으며, 프로파일을 통해 환경별로 유연하게 적용 가능합니다. Caffeine은 메모리 기반 고성능 캐싱에 적합하고, Redis는 분산 환경에서 유용합니다. 위 표와 예시를 참고해 애플리케이션 요구사항에 맞는 캐싱 전략을 구성해보세요! 추가 질문이 있으면 언제든 말씀해주세요.
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
스프링 부트에서 사용 가능한 주요 캐시 구현체들을 비교해보겠습니다. 스프링의 캐싱 추상화는 다양한 캐시 제공자를 지원하며, 각 구현체는 성능, 사용 사례, 설정 복잡도 등에서 차이가 있습니다. 여기서는 대표적인 캐시 구현체인 **ConcurrentMap**, **Caffeine**, **EhCache**, **Redis**를 중심으로 비교하고, 표와 설명을 통해 차이점을 정리하겠습니다.
|
||||
|
||||
---
|
||||
|
||||
### 캐시 구현체 비교 표
|
||||
|
||||
| **구현체** | **ConcurrentMap** | **Caffeine** | **EhCache** | **Redis** |
|
||||
|-------------------|------------------------------------|----------------------------------|----------------------------------|----------------------------------|
|
||||
| **설명** | 기본 메모리 기반 캐시 (`ConcurrentHashMap` 사용) | 고성능 메모리 캐시, Guava 캐시 대체 | 오프-힙 메모리 지원, 복잡한 캐싱 전략 | 분산 환경 지원, 영구 저장 가능 |
|
||||
| **의존성** | 추가 의존성 필요 없음 (스프링 부트 기본 제공) | `com.github.ben-manes.caffeine` | `org.ehcache:ehcache` | `spring-boot-starter-data-redis` |
|
||||
| **성능** | 중간 (단순 메모리 기반) | 매우 높음 (최적화된 알고리즘) | 높음 (오프-힙 사용 시 더 효율적) | 높음 (네트워크 지연 고려 필요) |
|
||||
| **메모리 사용** | JVM 힙 메모리 사용 | JVM 힙 메모리 사용 | 힙 + 오프-힙 메모리 사용 가능 | 독립적인 Redis 서버 메모리 사용 |
|
||||
| **분산 지원** | 없음 (단일 JVM 내에서만 동작) | 없음 (단일 JVM 내에서만 동작) | 제한적 (클러스터링 지원) | 있음 (분산 캐싱에 최적화) |
|
||||
| **영속성** | 없음 (애플리케이션 종료 시 소멸) | 없음 (애플리케이션 종료 시 소멸) | 있음 (디스크 저장 옵션 제공) | 있음 (Redis 설정에 따라 영구 저장) |
|
||||
| **설정 복잡도** | 매우 낮음 (기본 설정으로 충분) | 낮음 (`application.yaml`로 간단 설정) | 중간 (`ehcache.xml` 필요) | 높음 (Redis 서버 설정 필요) |
|
||||
| **만료 정책** | 없음 (수동 관리 필요) | 유연함 (시간 기반, 크기 기반) | 유연함 (복잡한 정책 지원) | 유연함 (TTL 설정 가능) |
|
||||
| **사용 사례** | 간단한 테스트, 소규모 애플리케이션 | 고성능 요구되는 단일 서버 애플리케이션 | 대규모 메모리 관리, 영속성 필요 시 | 분산 시스템, 대규모 트래픽 처리 |
|
||||
| **스프링 설정** | `spring.cache.type: concurrentMap` | `spring.cache.type: caffeine` | `spring.cache.type: ehcache` | `spring.cache.type: redis` |
|
||||
|
||||
---
|
||||
|
||||
### 구현체별 상세 비교
|
||||
|
||||
#### 1. ConcurrentMap
|
||||
- **특징**: 스프링 부트의 기본 캐시 매니저(`ConcurrentMapCacheManager`)로, `ConcurrentHashMap`을 사용해 메모리에 데이터를 저장합니다.
|
||||
- **장점**:
|
||||
- 별도 의존성 없이 즉시 사용 가능.
|
||||
- 설정이 간단하고 가볍습니다.
|
||||
- **단점**:
|
||||
- 만료 정책(TTL)이나 크기 제한이 없어 수동으로 관리해야 함.
|
||||
- 분산 환경 지원 불가, JVM 종료 시 데이터 소실.
|
||||
- **사용 예시**:
|
||||
```yaml
|
||||
spring:
|
||||
cache:
|
||||
type: concurrentMap
|
||||
```
|
||||
- 소규모 애플리케이션이나 개발 초기 테스트에 적합.
|
||||
|
||||
#### 2. Caffeine
|
||||
- **특징**: Guava 캐시의 대체재로, 최신 알고리즘(Window TinyLFU)을 사용해 높은 성능을 제공하는 메모리 기반 캐시입니다.
|
||||
- **장점**:
|
||||
- 빠른 읽기/쓰기 성능.
|
||||
- 최대 크기, 만료 시간 등 유연한 설정 가능.
|
||||
- **단점**:
|
||||
- 분산 환경 지원 안 함.
|
||||
- 메모리만 사용하므로 애플리케이션 재시작 시 데이터 소실.
|
||||
- **사용 예시**:
|
||||
```yaml
|
||||
spring:
|
||||
cache:
|
||||
type: caffeine
|
||||
caffeine:
|
||||
spec: maximumSize=500,expireAfterWrite=600s
|
||||
```
|
||||
- 단일 서버에서 높은 캐시 히트율이 필요한 경우 추천.
|
||||
|
||||
#### 3. EhCache
|
||||
- **특징**: 오픈소스 캐시 라이브러리로, 메모리와 디스크를 활용한 캐싱을 지원하며 복잡한 캐시 정책을 설정할 수 있습니다.
|
||||
- **장점**:
|
||||
- 오프-힙 메모리와 디스크 저장으로 대규모 데이터 처리 가능.
|
||||
- TTL, 캐시 계층화 등 고급 기능 제공.
|
||||
- **단점**:
|
||||
- 설정 파일(`ehcache.xml`)이 필요해 초기 설정이 복잡.
|
||||
- 분산 환경에서는 추가 클러스터링 설정 필요.
|
||||
- **사용 예시**:
|
||||
```yaml
|
||||
spring:
|
||||
cache:
|
||||
type: ehcache
|
||||
```
|
||||
```xml
|
||||
<!-- ehcache.xml -->
|
||||
<cache name="users" maxEntriesLocalHeap="1000" timeToLiveSeconds="3600"/>
|
||||
```
|
||||
- 메모리와 영속성을 동시에 고려해야 할 때 적합.
|
||||
|
||||
#### 4. Redis
|
||||
- **특징**: 인메모리 데이터 저장소로, 분산 환경에서 캐싱과 데이터 공유를 지원합니다.
|
||||
- **장점**:
|
||||
- 분산 시스템에서 여러 서버 간 캐시 공유 가능.
|
||||
- TTL 설정 및 영구 저장 옵션 제공.
|
||||
- **단점**:
|
||||
- 별도의 Redis 서버 운영 필요 (네트워크 지연 발생 가능).
|
||||
- 설정 및 유지보수가 상대적으로 복잡.
|
||||
- **사용 예시**:
|
||||
```yaml
|
||||
spring:
|
||||
cache:
|
||||
type: redis
|
||||
redis:
|
||||
time-to-live: 3600000
|
||||
redis:
|
||||
host: localhost
|
||||
port: 6379
|
||||
```
|
||||
- 대규모 트래픽과 분산 애플리케이션에 적합.
|
||||
|
||||
---
|
||||
|
||||
### 비교 요약 및 선택 가이드
|
||||
1. **ConcurrentMap**
|
||||
- **선택 시기**: 간단한 테스트나 프로토타입 개발 단계.
|
||||
- **제한**: 기능이 단순해 실제 운영 환경에서는 부족.
|
||||
|
||||
2. **Caffeine**
|
||||
- **선택 시기**: 단일 서버에서 고성능 메모리 캐싱이 필요할 때.
|
||||
- **제한**: 분산 환경이나 영속성이 필요하면 부적합.
|
||||
|
||||
3. **EhCache**
|
||||
- **선택 시기**: 대규모 데이터와 영속성이 필요한 단일 서버 애플리케이션.
|
||||
- **제한**: 설정이 복잡하고, 분산 지원이 약함.
|
||||
|
||||
4. **Redis**
|
||||
- **선택 시기**: 여러 서버가 캐시를 공유해야 하는 분산 시스템, 대규모 트래픽 처리.
|
||||
- **제한**: 추가 인프라 관리 비용 발생.
|
||||
|
||||
---
|
||||
|
||||
### 결론
|
||||
캐시 구현체 선택은 애플리케이션의 규모, 성능 요구사항, 분산 여부에 따라 달라집니다. 소규모라면 `ConcurrentMap`이나 `Caffeine`으로 시작하고, 대규모 또는 분산 환경에서는 `Redis`를, 복잡한 캐싱 전략과 영속성이 필요하면 `EhCache`를 고려하세요. 각 구현체의 특성을 이해하고 환경에 맞게 설정하면 최적의 캐싱 전략을 구현할 수 있습니다. 추가 질문이 있다면 말씀해주세요!
|
||||
231
docs/elasticsearch.md
Normal file
231
docs/elasticsearch.md
Normal file
@@ -0,0 +1,231 @@
|
||||
### 스프링 부트에서 엘라스틱서치(Elasticsearch)를 사용하는 방법
|
||||
|
||||
엘라스틱서치(Elasticsearch)는 분산 검색 엔진으로, 대규모 데이터의 검색, 분석, 저장을 효율적으로 처리할 수 있는 도구입니다. 스프링 부트에서 엘라스틱서치를 사용하면 `Spring Data Elasticsearch`를 통해 쉽게 통합할 수 있으며, REST API나 Java 클라이언트를 활용해 데이터를 조작할 수 있습니다. 아래에서는 설정부터 사용 방법까지 단계별로 설명하겠습니다.
|
||||
|
||||
---
|
||||
|
||||
#### 1. 의존성 추가
|
||||
스프링 부트 프로젝트에서 엘라스틱서치를 사용하려면 `pom.xml`에 `spring-boot-starter-data-elasticsearch` 의존성을 추가합니다.
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
- **참고**: 스프링 부트 버전에 따라 엘라스틱서치 클라이언트 버전이 달라질 수 있으니, 사용하는 엘라스틱서치 서버 버전과 호환되도록 확인하세요 (예: Elasticsearch 8.x 사용 시 최신 클라이언트 필요).
|
||||
|
||||
---
|
||||
|
||||
#### 2. `application.yaml` 설정
|
||||
엘라스틱서치 서버와의 연결을 설정합니다.
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
elasticsearch:
|
||||
uris: http://localhost:9200 # 엘라스틱서치 서버 URL
|
||||
username: elastic # 기본 사용자 이름 (선택)
|
||||
password: yourpassword # 비밀번호 (선택, 기본 인증 사용 시)
|
||||
```
|
||||
|
||||
- **옵션**:
|
||||
- `uris`: 단일 또는 다중 노드 URL (예: `http://node1:9200,http://node2:9200`).
|
||||
- `username`/`password`: 엘라스틱서치 보안 설정(X-Pack) 활성화 시 필요.
|
||||
|
||||
---
|
||||
|
||||
#### 3. 엔티티 클래스 정의
|
||||
엘라스틱서치에 저장할 데이터를 표현하는 엔티티 클래스를 정의합니다. `@Document` 어노테이션을 사용해 인덱스를 지정합니다.
|
||||
|
||||
```java
|
||||
import org.springframework.data.annotation.Id;
|
||||
import org.springframework.data.elasticsearch.annotations.Document;
|
||||
import org.springframework.data.elasticsearch.annotations.Field;
|
||||
import org.springframework.data.elasticsearch.annotations.FieldType;
|
||||
|
||||
@Document(indexName = "users") // 인덱스 이름 지정
|
||||
public class User {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
|
||||
@Field(type = FieldType.Text)
|
||||
private String name;
|
||||
|
||||
@Field(type = FieldType.Integer)
|
||||
private int age;
|
||||
|
||||
// Getter, Setter, Constructor
|
||||
public User() {}
|
||||
|
||||
public User(String id, String name, int age) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.age = age;
|
||||
}
|
||||
|
||||
// ... getters and setters ...
|
||||
}
|
||||
```
|
||||
|
||||
- **`@Document`**: 인덱스 이름과 설정(샤드 수, 복제본 수 등)을 정의.
|
||||
- **`@Id`**: 문서의 고유 식별자.
|
||||
- **`@Field`**: 필드 타입과 매핑 설정.
|
||||
|
||||
---
|
||||
|
||||
#### 4. 리포지토리 인터페이스 생성
|
||||
`Spring Data Elasticsearch`는 JPA와 유사하게 리포지토리 인터페이스를 제공합니다.
|
||||
|
||||
```java
|
||||
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
|
||||
|
||||
public interface UserRepository extends ElasticsearchRepository<User, String> {
|
||||
List<User> findByName(String name); // 이름으로 검색
|
||||
List<User> findByAgeGreaterThan(int age); // 나이 기준 검색
|
||||
}
|
||||
```
|
||||
|
||||
- **기능**: 기본 CRUD 메서드(`save`, `findById`, `delete` 등)와 쿼리 메서드 자동 생성.
|
||||
|
||||
---
|
||||
|
||||
#### 5. 서비스 및 컨트롤러 구현
|
||||
리포지토리를 사용해 데이터를 조작하는 서비스와 컨트롤러를 작성합니다.
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public UserService(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
public User saveUser(User user) {
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
public Optional<User> findUserById(String id) {
|
||||
return userRepository.findById(id);
|
||||
}
|
||||
|
||||
public List<User> findUsersByName(String name) {
|
||||
return userRepository.findByName(name);
|
||||
}
|
||||
|
||||
public void deleteUser(String id) {
|
||||
userRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
public UserController(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public User createUser(@RequestBody User user) {
|
||||
return userService.saveUser(user);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<User> getUser(@PathVariable String id) {
|
||||
return userService.findUserById(id)
|
||||
.map(ResponseEntity::ok)
|
||||
.orElseGet(() -> ResponseEntity.notFound().build());
|
||||
}
|
||||
|
||||
@GetMapping("/search")
|
||||
public List<User> searchUsersByName(@RequestParam String name) {
|
||||
return userService.findUsersByName(name);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteUser(@PathVariable String id) {
|
||||
userService.deleteUser(id);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 6. 커스텀 쿼리 사용 (옵션)
|
||||
복잡한 검색 쿼리가 필요할 경우 `ElasticsearchRestTemplate`을 사용합니다.
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
public class ElasticsearchConfig {
|
||||
|
||||
@Bean
|
||||
public ElasticsearchRestTemplate elasticsearchTemplate(RestHighLevelClient client) {
|
||||
return new ElasticsearchRestTemplate(client);
|
||||
}
|
||||
}
|
||||
|
||||
@Service
|
||||
public class CustomUserService {
|
||||
|
||||
private final ElasticsearchRestTemplate template;
|
||||
|
||||
public CustomUserService(ElasticsearchRestTemplate template) {
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
public List<User> searchByName(String name) {
|
||||
NativeSearchQuery searchQuery = new NativeSearchQueryBuilder()
|
||||
.withQuery(QueryBuilders.matchQuery("name", name))
|
||||
.build();
|
||||
SearchHits<User> searchHits = template.search(searchQuery, User.class);
|
||||
return searchHits.get().map(SearchHit::getContent).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **`ElasticsearchRestTemplate`**: 저수준 API로 복잡한 쿼리 실행.
|
||||
- **`QueryBuilders`**: 쿼리 빌더를 사용해 검색 조건 정의.
|
||||
|
||||
---
|
||||
|
||||
#### 7. 테스트
|
||||
엘라스틱서치가 실행 중인지 확인하고, 애플리케이션을 실행합니다. 예를 들어:
|
||||
- **데이터 추가**: `POST /api/users`에 `{ "id": "1", "name": "John", "age": 30 }` 요청.
|
||||
- **검색**: `GET /api/users/search?name=John`으로 이름 검색.
|
||||
|
||||
---
|
||||
|
||||
### 추가 팁
|
||||
1. **의존성 버전 확인**:
|
||||
- 엘라스틱서치 서버 버전과 클라이언트 버전이 일치해야 합니다. 스프링 부트 3.x는 기본적으로 Elasticsearch 8.x를 지원합니다.
|
||||
- 필요 시 `spring.elasticsearch.restclient.version` 속성으로 버전 조정.
|
||||
|
||||
2. **인덱스 관리**:
|
||||
- 인덱스는 자동 생성되지만, 매핑을 미리 정의하려면 `@Mapping` 어노테이션이나 엘라스틱서치 API 사용.
|
||||
|
||||
3. **운영 환경 설정**:
|
||||
```yaml
|
||||
spring:
|
||||
elasticsearch:
|
||||
uris: http://prod-es:9200
|
||||
username: elastic
|
||||
password: securepassword
|
||||
```
|
||||
|
||||
4. **로컬 테스트**:
|
||||
- Docker로 엘라스틱서치를 실행:
|
||||
```bash
|
||||
docker run -d -p 9200:9200 -e "discovery.type=single-node" elasticsearch:8.12.0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 결론
|
||||
스프링 부트에서 엘라스틱서치를 사용하려면 `spring-boot-starter-data-elasticsearch` 의존성을 추가하고, `application.yaml`으로 연결 설정을 정의한 후, 엔티티와 리포지토리를 작성하면 됩니다. 간단한 CRUD는 `ElasticsearchRepository`로 처리하고, 복잡한 쿼리는 `ElasticsearchRestTemplate`을 활용할 수 있습니다. 이를 통해 검색 성능을 극대화하고 대규모 데이터를 효율적으로 관리할 수 있습니다. 추가 질문이 있다면 언제든 물어보세요!
|
||||
163
docs/exception.md
Normal file
163
docs/exception.md
Normal file
@@ -0,0 +1,163 @@
|
||||
### 스프링 부트에서 예외 처리
|
||||
|
||||
스프링 부트에서 예외 처리는 애플리케이션의 안정성과 사용자 경험을 개선하는 데 중요한 역할을 합니다. REST API나 MVC 기반 웹 애플리케이션에서 발생하는 예외를 효과적으로 관리하면, 클라이언트에게 일관된 응답을 제공하고 개발자가 문제를 빠르게 파악할 수 있습니다. 스프링 부트는 다양한 예외 처리 방법을 제공하며, 이를 컨트롤러 단위 또는 전역적으로 설정할 수 있습니다. 아래에서는 주요 개념과 구현 방법을 설명합니다.
|
||||
|
||||
---
|
||||
|
||||
#### 1. 기본 예외 처리
|
||||
스프링 부트는 기본적으로 `BasicErrorController`를 제공하여 예외가 발생했을 때 표준화된 오류 응답을 반환합니다. 예를 들어, 잘못된 URL로 요청하거나 서버에서 예외가 발생하면 JSON 형식(REST API)이나 HTML 페이지(MVC)로 오류 정보를 반환합니다.
|
||||
|
||||
- **기본 응답 예시 (JSON)**:
|
||||
```json
|
||||
{
|
||||
"timestamp": "2025-03-15T12:00:00Z",
|
||||
"status": 404,
|
||||
"error": "Not Found",
|
||||
"message": "No message available",
|
||||
"path": "/wrong/path"
|
||||
}
|
||||
```
|
||||
- **설정**: `application.yaml`에서 `server.error` 속성으로 기본 동작을 커스터마이징할 수 있습니다.
|
||||
```yaml
|
||||
server:
|
||||
error:
|
||||
include-stacktrace: on_param # 스택 트레이스 포함 여부 (never, on_param, always)
|
||||
include-message: always # 오류 메시지 포함
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 2. 컨트롤러 내 예외 처리 (@ExceptionHandler)
|
||||
컨트롤러 내에서 특정 예외를 처리하려면 `@ExceptionHandler` 어노테이션을 사용합니다. 이는 특정 컨트롤러에 국한된 예외 처리를 정의할 때 유용합니다.
|
||||
|
||||
- **예시**:
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class MyController {
|
||||
|
||||
@GetMapping("/user/{id}")
|
||||
public String getUser(@PathVariable Long id) {
|
||||
if (id <= 0) {
|
||||
throw new IllegalArgumentException("ID는 0보다 커야 합니다.");
|
||||
}
|
||||
return "User " + id;
|
||||
}
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public ResponseEntity<String> handleIllegalArgument(IllegalArgumentException ex) {
|
||||
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ex.getMessage());
|
||||
}
|
||||
}
|
||||
```
|
||||
- **동작**: `/api/user/0` 요청 시 `400 Bad Request`와 메시지가 반환됩니다.
|
||||
|
||||
---
|
||||
|
||||
#### 3. 전역 예외 처리 (@ControllerAdvice)
|
||||
애플리케이션 전체에 걸친 예외 처리를 위해 `@ControllerAdvice`와 `@ExceptionHandler`를 조합하여 사용합니다. 이를 통해 모든 컨트롤러에서 발생하는 예외를 일관되게 처리할 수 있습니다.
|
||||
|
||||
- **예시**:
|
||||
```java
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
public ResponseEntity<ErrorResponse> handleIllegalArgument(IllegalArgumentException ex) {
|
||||
ErrorResponse response = new ErrorResponse(HttpStatus.BAD_REQUEST, ex.getMessage());
|
||||
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ErrorResponse> handleGeneralException(Exception ex) {
|
||||
ErrorResponse response = new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류 발생");
|
||||
return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
// 커스텀 오류 응답 객체
|
||||
public record ErrorResponse(HttpStatus status, String message) {}
|
||||
```
|
||||
- **결과**:
|
||||
- `IllegalArgumentException`: `400 Bad Request`와 메시지 반환.
|
||||
- 기타 예외: `500 Internal Server Error`와 기본 메시지 반환.
|
||||
|
||||
---
|
||||
|
||||
#### 4. 커스텀 예외 정의
|
||||
비즈니스 로직에 맞는 예외를 정의하고 이를 처리하는 것도 일반적입니다.
|
||||
|
||||
- **커스텀 예외**:
|
||||
```java
|
||||
public class UserNotFoundException extends RuntimeException {
|
||||
public UserNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
```
|
||||
- **컨트롤러에서 사용**:
|
||||
```java
|
||||
@GetMapping("/user/{id}")
|
||||
public String getUser(@PathVariable Long id) {
|
||||
if (id == null || id <= 0) {
|
||||
throw new UserNotFoundException("사용자를 찾을 수 없습니다: " + id);
|
||||
}
|
||||
return "User " + id;
|
||||
}
|
||||
```
|
||||
- **전역 처리**:
|
||||
```java
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(UserNotFoundException.class)
|
||||
public ResponseEntity<ErrorResponse> handleUserNotFound(UserNotFoundException ex) {
|
||||
return new ResponseEntity<>(new ErrorResponse(HttpStatus.NOT_FOUND, ex.getMessage()), HttpStatus.NOT_FOUND);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 5. 응답 형식 커스터마이징
|
||||
REST API에서 클라이언트가 기대하는 오류 응답 형식을 맞추기 위해 `ResponseEntity`나 커스텀 객체를 사용합니다. 예를 들어:
|
||||
|
||||
- **응답 형식**:
|
||||
```json
|
||||
{
|
||||
"status": 404,
|
||||
"error": "Not Found",
|
||||
"message": "사용자를 찾을 수 없습니다: 0"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 6. 주요 어노테이션 및 클래스
|
||||
- `@ExceptionHandler`: 특정 예외를 처리하는 메서드 정의.
|
||||
- `@ControllerAdvice`: 전역 예외 처리 클래스 정의.
|
||||
- `@RestControllerAdvice`: `@ControllerAdvice` + `@ResponseBody` 조합.
|
||||
- `ResponseEntity`: HTTP 상태 코드, 헤더, 본문을 포함한 응답 생성.
|
||||
- `HttpStatus`: HTTP 상태 코드 열거형 (예: `HttpStatus.NOT_FOUND`).
|
||||
|
||||
---
|
||||
|
||||
#### 7. 예외 처리 모범 사례
|
||||
- **일관성 유지**: 모든 오류 응답 형식을 표준화하여 클라이언트가 쉽게 파싱하도록 합니다.
|
||||
- **세부 정보 제한**: 프로덕션 환경에서는 스택 트레이스나 민감한 정보를 노출시키지 않습니다.
|
||||
- **로그 기록**: 예외 발생 시 로깅(`SLF4J` 등)을 통해 디버깅 정보를 남깁니다.
|
||||
```java
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<ErrorResponse> handleException(Exception ex) {
|
||||
log.error("Unhandled exception occurred", ex);
|
||||
return new ResponseEntity<>(new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR, "서버 오류"), HttpStatus.INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 결론
|
||||
스프링 부트의 예외 처리는 `@ExceptionHandler`로 국부적인 처리를, `@ControllerAdvice`로 전역 처리를 구현하며, 커스텀 예외와 응답 형식을 통해 유연성을 확보할 수 있습니다. 이를 통해 개발자는 환경별 요구사항에 맞춰 안정적이고 유지보수 가능한 애플리케이션을 설계할 수 있습니다. 추가적으로, 스프링 부트의 기본 오류 처리 기능을 활용하면 최소한의 설정으로도 기본적인 예외 처리가 가능하다는 점도 큰 장점입니다.
|
||||
392
docs/full text search.md
Normal file
392
docs/full text search.md
Normal file
@@ -0,0 +1,392 @@
|
||||
# **MariaDB 풀 텍스트 검색 (Full-Text Search) 완벽 가이드**
|
||||
|
||||
**풀 텍스트 검색(Full-Text Search, FTS)**은 일반적인 `LIKE` 연산자보다 강력한 검색 기능을 제공하는 MariaDB의 주요 기능 중 하나입니다.
|
||||
대량의 텍스트 데이터를 빠르게 검색할 수 있도록 **인덱스를 활용한 고속 검색, 랭킹(관련성 점수) 기반 정렬, 논리 연산자 지원** 등의 기능을 제공합니다.
|
||||
|
||||
이 글에서는 **MariaDB의 풀 텍스트 검색 개념, 설정 방법, 사용법, 고급 기능**까지 단계별로 설명하겠습니다.
|
||||
|
||||
---
|
||||
|
||||
## **1. 풀 텍스트 검색(Full-Text Search)란?**
|
||||
|
||||
### **📌 풀 텍스트 검색의 특징**
|
||||
- **일반적인 `LIKE` 검색보다 훨씬 빠름** (전문 검색 인덱스를 사용)
|
||||
- **단어 단위 검색**이 가능 (`LIKE '%word%'`는 문자열 전체에서 검색하지만, FTS는 단어를 인식)
|
||||
- **복잡한 검색 연산 지원** (`MATCH ... AGAINST`를 이용한 자연어 검색, 불리언(Boolean) 검색)
|
||||
- **인덱스를 활용하여 검색 성능 최적화** (`FULLTEXT` 인덱스 사용)
|
||||
- **정렬 기준을 제공** (관련성 점수를 반환하여 가장 연관성 높은 결과부터 출력 가능)
|
||||
|
||||
---
|
||||
|
||||
## **2. 풀 텍스트 검색을 위한 기본 설정**
|
||||
|
||||
### **✅ 1. 테스트용 테이블 생성**
|
||||
MariaDB에서 풀 텍스트 검색을 사용하려면, **TEXT 또는 VARCHAR 컬럼에 `FULLTEXT` 인덱스를 추가해야 합니다.**
|
||||
|
||||
```sql
|
||||
CREATE TABLE articles (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
title VARCHAR(255),
|
||||
content TEXT,
|
||||
FULLTEXT (title, content) -- 풀 텍스트 인덱스 추가
|
||||
) ENGINE=InnoDB;
|
||||
```
|
||||
✔ `FULLTEXT (title, content)` → **title과 content 컬럼을 대상으로 풀 텍스트 검색 지원**
|
||||
✔ `ENGINE=InnoDB` → **MariaDB 10.0.5 이상부터 InnoDB에서도 지원 (기존에는 MyISAM 전용)**
|
||||
|
||||
---
|
||||
|
||||
### **✅ 2. 테스트 데이터 삽입**
|
||||
|
||||
```sql
|
||||
INSERT INTO articles (title, content) VALUES
|
||||
('MariaDB 풀 텍스트 검색', 'MariaDB의 FULLTEXT 검색은 강력한 검색 기능을 제공합니다.'),
|
||||
('MySQL과 MariaDB 비교', 'MariaDB는 MySQL의 오픈소스 버전이며, 여러 기능을 개선했습니다.'),
|
||||
('MariaDB 성능 튜닝', 'MariaDB의 성능을 향상시키는 방법에 대해 설명합니다.');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## **3. 기본적인 풀 텍스트 검색 사용법**
|
||||
|
||||
### **✅ 1. `MATCH ... AGAINST`를 이용한 검색**
|
||||
MariaDB에서 풀 텍스트 검색을 수행하려면 **`MATCH(컬럼명) AGAINST ('검색어')`** 구문을 사용합니다.
|
||||
|
||||
```sql
|
||||
SELECT id, title, MATCH(title, content) AGAINST ('MariaDB') AS relevance
|
||||
FROM articles
|
||||
WHERE MATCH(title, content) AGAINST ('MariaDB');
|
||||
```
|
||||
✔ `MATCH(title, content) AGAINST ('MariaDB')` → **"MariaDB"를 포함하는 레코드 검색**
|
||||
✔ `relevance` (관련성 점수) 반환 → **높은 점수를 가진 데이터가 더 관련성이 높음**
|
||||
|
||||
**📌 실행 결과 예시:**
|
||||
|
||||
| id | title | relevance |
|
||||
|----|----------------|------------|
|
||||
| 1 | MariaDB 풀 텍스트 검색 | 1.2 |
|
||||
| 3 | MariaDB 성능 튜닝 | 0.8 |
|
||||
|
||||
**✅ 장점:**
|
||||
- 일반적인 `LIKE '%검색어%'`보다 훨씬 빠른 검색 속도
|
||||
- 관련성 점수를 기준으로 정렬 가능
|
||||
|
||||
---
|
||||
|
||||
### **✅ 2. 불리언(Boolean) 모드 검색**
|
||||
불리언 모드를 사용하면 **논리 연산자(+,-,*,~ 등)를 이용해 복잡한 검색 조건을 적용할 수 있습니다.**
|
||||
|
||||
```sql
|
||||
SELECT id, title
|
||||
FROM articles
|
||||
WHERE MATCH(title, content) AGAINST ('+MariaDB -MySQL' IN BOOLEAN MODE);
|
||||
```
|
||||
✔ `+MariaDB` → **"MariaDB"가 반드시 포함된 문서만 검색**
|
||||
✔ `-MySQL` → **"MySQL"이 포함된 문서는 제외**
|
||||
|
||||
**📌 주요 연산자 정리**
|
||||
|
||||
| 연산자 | 설명 |
|
||||
|--------|------|
|
||||
| `+` | 반드시 포함 (예: `+MariaDB`) |
|
||||
| `-` | 포함되면 제외 (예: `-MySQL`) |
|
||||
| `*` | 와일드카드 (예: `Mari*` → Maria, MariaDB 모두 검색) |
|
||||
| `~` | 관련성을 낮춤 (예: `~튜닝` → "튜닝" 단어가 있는 경우 가중치를 낮춤) |
|
||||
| `""` | 정확한 문구 검색 (예: `"MariaDB 풀 텍스트"`) |
|
||||
|
||||
---
|
||||
|
||||
## **4. 고급 검색 기능**
|
||||
|
||||
### **✅ 1. 자연어(Natural Language) vs 불리언(Boolean) 검색 비교**
|
||||
MariaDB에서는 **자연어 검색(Natural Language Mode)**과 **불리언 검색(Boolean Mode)** 두 가지 모드를 지원합니다.
|
||||
|
||||
#### **📌 자연어 검색 (기본값)**
|
||||
```sql
|
||||
SELECT * FROM articles
|
||||
WHERE MATCH(title, content) AGAINST ('MariaDB');
|
||||
```
|
||||
✔ **관련성 점수 기반 검색**
|
||||
✔ 자동으로 불필요한 단어 제거
|
||||
|
||||
---
|
||||
|
||||
#### **📌 불리언 검색**
|
||||
```sql
|
||||
SELECT * FROM articles
|
||||
WHERE MATCH(title, content) AGAINST ('+MariaDB -튜닝' IN BOOLEAN MODE);
|
||||
```
|
||||
✔ **연산자를 사용하여 논리적으로 검색 조건 적용 가능**
|
||||
✔ **관련성 점수를 사용하지 않음**
|
||||
|
||||
---
|
||||
|
||||
### **✅ 2. 부분 일치 검색 (NGRAM 인덱스 활용)**
|
||||
MariaDB에서 기본적으로 **한 단어(토큰) 단위로 검색**이 이루어지므로, **부분 검색(예: "DB" 검색)이 불가능**합니다.
|
||||
이를 해결하기 위해 **ngram 인덱스**를 활용할 수 있습니다.
|
||||
|
||||
#### **📌 ngram 인덱스 사용법**
|
||||
```sql
|
||||
ALTER TABLE articles ADD FULLTEXT(title, content) WITH PARSER ngram;
|
||||
```
|
||||
✔ **기본적으로 2글자 단위로 인덱싱 (bigram 방식)**
|
||||
✔ **"DB"와 같은 부분 단어 검색 가능**
|
||||
|
||||
---
|
||||
|
||||
### **✅ 3. 정렬 기준 적용 (`ORDER BY`)**
|
||||
검색 결과를 **관련성(relevance) 점수를 기준으로 정렬**할 수 있습니다.
|
||||
|
||||
```sql
|
||||
SELECT id, title, MATCH(title, content) AGAINST ('MariaDB') AS relevance
|
||||
FROM articles
|
||||
WHERE MATCH(title, content) AGAINST ('MariaDB')
|
||||
ORDER BY relevance DESC;
|
||||
```
|
||||
✔ **관련성 점수가 높은 순으로 정렬**
|
||||
|
||||
---
|
||||
|
||||
## **5. 정리**
|
||||
|
||||
### ✅ **MariaDB 풀 텍스트 검색 핵심 요약**
|
||||
- `FULLTEXT` 인덱스를 사용하여 **빠른 텍스트 검색 가능**
|
||||
- `MATCH() AGAINST()` 구문을 활용하여 검색 수행
|
||||
- **불리언 검색(Boolean Mode)**을 사용하면 논리 연산을 적용 가능
|
||||
- `NGRAM` 인덱스를 적용하면 **부분 검색 지원**
|
||||
|
||||
### ✅ **풀 텍스트 검색을 언제 사용할까?**
|
||||
- **LIKE 검색이 느려질 때**
|
||||
- **대량의 텍스트 데이터를 검색할 때**
|
||||
- **검색어의 관련성(점수) 기준으로 정렬이 필요할 때**
|
||||
|
||||
MariaDB의 풀 텍스트 검색 기능을 활용하면 **강력한 검색 성능과 빠른 응답 속도를 확보할 수 있습니다!** 🚀
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
# **JPA에서 풀 텍스트 검색(Full-Text Search) 활용하기**
|
||||
|
||||
JPA(Hibernate)를 사용할 때 **풀 텍스트 검색(Full-Text Search)**을 적용하는 방법은 크게 두 가지입니다.
|
||||
|
||||
1. **데이터베이스 풀 텍스트 검색 기능 활용** (예: MariaDB의 `FULLTEXT` 인덱스)
|
||||
2. **Hibernate Search (Elasticsearch, Apache Lucene 등과 연동)**
|
||||
|
||||
이 글에서는 **MariaDB의 `FULLTEXT` 인덱스를 활용하여 JPA에서 풀 텍스트 검색을 수행하는 방법**을 설명하겠습니다.
|
||||
|
||||
---
|
||||
|
||||
## **1. MariaDB의 `FULLTEXT` 검색을 JPA에서 사용하기**
|
||||
|
||||
### **✅ 1. 엔티티(Entity) 설정**
|
||||
|
||||
풀 텍스트 검색을 적용하려면 **TEXT 또는 VARCHAR 컬럼에 `FULLTEXT` 인덱스를 추가**해야 합니다.
|
||||
|
||||
#### **📌 엔티티 클래스 예제**
|
||||
```java
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@Table(name = "articles", indexes = {
|
||||
@Index(name = "idx_fulltext_title_content", columnList = "title,content", unique = false)
|
||||
})
|
||||
public class Article {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(nullable = false)
|
||||
private String title;
|
||||
|
||||
@Column(columnDefinition = "TEXT", nullable = false)
|
||||
private String content;
|
||||
}
|
||||
```
|
||||
✔ `@Index(name = "idx_fulltext_title_content", columnList = "title,content")`
|
||||
→ **JPA로 FULLTEXT 인덱스를 직접 설정할 수 없지만, 인덱스 지정 가능**
|
||||
✔ `@Column(columnDefinition = "TEXT")`
|
||||
→ **TEXT 타입 필드(content)를 선언**
|
||||
|
||||
---
|
||||
|
||||
### **✅ 2. `FULLTEXT` 인덱스 수동 생성**
|
||||
JPA에서는 `FULLTEXT` 인덱스를 자동으로 생성하지 않으므로, **SQL로 직접 인덱스를 추가**해야 합니다.
|
||||
|
||||
```sql
|
||||
CREATE TABLE articles (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
FULLTEXT (title, content) -- FULLTEXT 인덱스 추가
|
||||
) ENGINE=InnoDB;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **✅ 3. JPA에서 풀 텍스트 검색 쿼리 작성**
|
||||
|
||||
JPA의 `@Query`를 사용하여 **`MATCH() AGAINST()`** 구문을 활용할 수 있습니다.
|
||||
|
||||
#### **📌 Repository 인터페이스**
|
||||
```java
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface ArticleRepository extends JpaRepository<Article, Long> {
|
||||
|
||||
@Query(value = """
|
||||
SELECT * FROM articles
|
||||
WHERE MATCH(title, content) AGAINST(:keyword)
|
||||
""", nativeQuery = true)
|
||||
List<Article> searchByFullText(@Param("keyword") String keyword);
|
||||
}
|
||||
```
|
||||
✔ `MATCH(title, content) AGAINST(:keyword)` → **title과 content에서 `keyword` 검색**
|
||||
✔ `nativeQuery = true` → **FULLTEXT 검색은 네이티브 쿼리로 실행해야 함**
|
||||
|
||||
---
|
||||
|
||||
### **✅ 4. 서비스(Service) 계층 작성**
|
||||
|
||||
#### **📌 검색 기능을 수행하는 서비스 클래스**
|
||||
```java
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
@Transactional
|
||||
public class ArticleService {
|
||||
|
||||
private final ArticleRepository articleRepository;
|
||||
|
||||
public ArticleService(ArticleRepository articleRepository) {
|
||||
this.articleRepository = articleRepository;
|
||||
}
|
||||
|
||||
public List<Article> searchArticles(String keyword) {
|
||||
return articleRepository.searchByFullText(keyword);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### **✅ 5. 컨트롤러(Controller) 구현**
|
||||
|
||||
#### **📌 검색 API 컨트롤러**
|
||||
```java
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/articles")
|
||||
public class ArticleController {
|
||||
|
||||
private final ArticleService articleService;
|
||||
|
||||
public ArticleController(ArticleService articleService) {
|
||||
this.articleService = articleService;
|
||||
}
|
||||
|
||||
@GetMapping("/search")
|
||||
public List<Article> searchArticles(@RequestParam String keyword) {
|
||||
return articleService.searchArticles(keyword);
|
||||
}
|
||||
}
|
||||
```
|
||||
✔ `/articles/search?keyword=MariaDB` → **MariaDB 관련 문서를 검색하는 API**
|
||||
|
||||
---
|
||||
|
||||
## **2. 불리언(Boolean) 검색 적용**
|
||||
|
||||
MariaDB의 **불리언(Boolean) 검색 모드**를 적용하면 검색 연산자(`+`, `-`, `*`, `" "`)를 사용할 수 있습니다.
|
||||
|
||||
### **✅ `BOOLEAN MODE` 적용 예제**
|
||||
```java
|
||||
@Query(value = """
|
||||
SELECT * FROM articles
|
||||
WHERE MATCH(title, content) AGAINST(:keyword IN BOOLEAN MODE)
|
||||
""", nativeQuery = true)
|
||||
List<Article> searchByFullTextBoolean(@Param("keyword") String keyword);
|
||||
```
|
||||
✔ `IN BOOLEAN MODE` → **논리 연산자를 포함한 검색 수행**
|
||||
|
||||
#### **📌 검색 연산자 예제**
|
||||
| 검색어 | 설명 |
|
||||
|--------|------|
|
||||
| `+MariaDB` | 반드시 "MariaDB" 포함 |
|
||||
| `-MySQL` | "MySQL"이 포함된 결과 제외 |
|
||||
| `Mari*` | "Maria", "MariaDB" 등 검색 가능 |
|
||||
| `"MariaDB 검색"` | 정확한 문구 검색 |
|
||||
|
||||
---
|
||||
|
||||
## **3. 정렬(관련성 점수 기반)**
|
||||
|
||||
풀 텍스트 검색을 수행할 때 **관련성 점수를 기준으로 정렬**할 수 있습니다.
|
||||
|
||||
### **✅ 정렬을 적용한 Repository**
|
||||
```java
|
||||
@Query(value = """
|
||||
SELECT *, MATCH(title, content) AGAINST(:keyword) AS relevance
|
||||
FROM articles
|
||||
WHERE MATCH(title, content) AGAINST(:keyword)
|
||||
ORDER BY relevance DESC
|
||||
""", nativeQuery = true)
|
||||
List<Article> searchByRelevance(@Param("keyword") String keyword);
|
||||
```
|
||||
✔ `MATCH(title, content) AGAINST(:keyword) AS relevance` → **관련성 점수 계산**
|
||||
✔ `ORDER BY relevance DESC` → **가장 연관성 높은 결과부터 정렬**
|
||||
|
||||
---
|
||||
|
||||
## **4. 페이징 처리 적용**
|
||||
|
||||
Spring Data JPA의 `Pageable`을 활용하여 검색 결과에 페이징을 적용할 수 있습니다.
|
||||
|
||||
### **✅ 페이징 지원 Repository**
|
||||
```java
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
@Query(value = """
|
||||
SELECT * FROM articles
|
||||
WHERE MATCH(title, content) AGAINST(:keyword)
|
||||
""", nativeQuery = true)
|
||||
Page<Article> searchByFullTextPaged(@Param("keyword") String keyword, Pageable pageable);
|
||||
```
|
||||
✔ `Page<Article>` 반환 → **Spring Data JPA의 페이징 기능을 활용 가능**
|
||||
|
||||
---
|
||||
|
||||
## **5. 정리**
|
||||
|
||||
### **✅ JPA에서 MariaDB 풀 텍스트 검색 활용 방법**
|
||||
- `FULLTEXT` 인덱스를 적용하여 빠른 검색 수행
|
||||
- `MATCH() AGAINST()`를 활용한 검색 쿼리 작성 (`BOOLEAN MODE` 지원)
|
||||
- 관련성 점수 기반 정렬 가능 (`ORDER BY relevance DESC`)
|
||||
- `Pageable`을 활용하여 **페이징 처리 가능**
|
||||
|
||||
### **📌 풀 텍스트 검색이 필요한 경우**
|
||||
✔ 데이터가 많아 `LIKE '%keyword%'`로 검색할 경우 성능이 저하될 때
|
||||
✔ 연관성 높은 검색 결과를 우선적으로 정렬해야 할 때
|
||||
✔ `BOOLEAN MODE`를 활용해 논리 검색(+, -, *)이 필요할 때
|
||||
|
||||
**MariaDB 풀 텍스트 검색을 활용하면 JPA에서도 강력한 검색 기능을 구현할 수 있습니다! 🚀**
|
||||
105
docs/i18n.md
Normal file
105
docs/i18n.md
Normal file
@@ -0,0 +1,105 @@
|
||||
스프링부트에서 국제화(Internationalization, i18n)는 애플리케이션이 여러 언어와 지역 설정을 지원할 수 있도록 하는 기능입니다. 이를 통해 사용자의 언어 환경에 맞춰 메시지, 날짜, 숫자 형식 등을 동적으로 제공할 수 있습니다. 스프링부트는 이를 쉽게 구현할 수 있도록 `MessageSource`와 같은 기능을 제공합니다. 아래에서 주요 개념과 설정 방법을 설명하겠습니다.
|
||||
|
||||
### 1. **국제화의 기본 개념**
|
||||
국제화는 주로 메시지 파일을 통해 구현됩니다. 메시지 파일은 언어별로 번역된 텍스트를 키-값 쌍으로 저장하며, 사용자의 로케일(Locale)에 따라 적절한 메시지를 불러옵니다. 예를 들어:
|
||||
- `messages.properties`: 기본 메시지 (영어 등)
|
||||
- `messages_ko.properties`: 한국어 메시지
|
||||
- `messages_en.properties`: 영어 메시지
|
||||
|
||||
### 2. **스프링부트에서 국제화 설정**
|
||||
스프링부트에서 국제화를 설정하는 기본 단계는 다음과 같습니다.
|
||||
|
||||
#### (1) **의존성 확인**
|
||||
스프링부트 프로젝트라면 별도의 의존성 추가 없이 `spring-boot-starter`에 포함된 기능을 사용할 수 있습니다.
|
||||
|
||||
#### (2) **메시지 파일 생성**
|
||||
프로젝트의 `src/main/resources` 디렉토리에 메시지 파일을 추가합니다. 예시:
|
||||
- `messages.properties`:
|
||||
```
|
||||
greeting=Hello, {0}!
|
||||
```
|
||||
- `messages_ko.properties`:
|
||||
```
|
||||
greeting=안녕하세요, {0}님!
|
||||
```
|
||||
- `messages_en.properties`:
|
||||
```
|
||||
greeting=Hello, {0}!
|
||||
```
|
||||
|
||||
파일 이름은 `messages_[언어코드].properties` 형식을 따르며, 언어 코드는 ISO 639-1 표준(예: `ko`, `en`, `fr`)을 사용합니다.
|
||||
|
||||
#### (3) **MessageSource 설정**
|
||||
스프링부트는 기본적으로 `MessageSource` 빈을 자동으로 설정합니다. 필요 시 `application.properties`에서 커스터마이징할 수 있습니다:
|
||||
```properties
|
||||
spring.messages.basename=messages
|
||||
spring.messages.encoding=UTF-8
|
||||
```
|
||||
- `spring.messages.basename`: 메시지 파일의 기본 이름 (디렉토리 경로 없이).
|
||||
- `spring.messages.encoding`: 파일 인코딩 설정.
|
||||
|
||||
#### (4) **로케일 설정**
|
||||
스프링부트는 사용자의 요청 헤더(`Accept-Language`)를 기반으로 로케일을 자동 감지합니다. 필요 시 `LocaleResolver`를 커스터마이징할 수 있습니다. 예를 들어, 세션 기반 로케일을 사용하려면:
|
||||
```java
|
||||
@Bean
|
||||
public LocaleResolver localeResolver() {
|
||||
SessionLocaleResolver slr = new SessionLocaleResolver();
|
||||
slr.setDefaultLocale(Locale.KOREAN); // 기본 로케일 설정
|
||||
return slr;
|
||||
}
|
||||
```
|
||||
|
||||
#### (5) **컨트롤러에서 사용**
|
||||
컨트롤러에서 `MessageSource`를 주입받아 메시지를 가져올 수 있습니다:
|
||||
```java
|
||||
@Autowired
|
||||
private MessageSource messageSource;
|
||||
|
||||
@GetMapping("/greet")
|
||||
public String greet(Locale locale) {
|
||||
String greeting = messageSource.getMessage("greeting", new Object[]{"사용자"}, locale);
|
||||
return greeting;
|
||||
}
|
||||
```
|
||||
위 코드는 로케일에 따라 적절한 메시지를 반환합니다.
|
||||
|
||||
#### (6) **Thymeleaf와 같은 템플릿 엔진에서 사용**
|
||||
Thymeleaf를 사용할 경우, 템플릿에서 직접 메시지를 참조할 수 있습니다:
|
||||
```html
|
||||
<p th:text="#{greeting('사용자')}"></p>
|
||||
```
|
||||
`#{key}`는 메시지 파일에서 해당 키를 찾아 로케일에 맞는 값을 렌더링합니다.
|
||||
|
||||
### 3. **추가 기능**
|
||||
- **파라미터 전달**: 메시지에 동적 값을 삽입하려면 `{0}`, `{1}` 같은 플레이스홀더를 사용합니다.
|
||||
- **로케일 변경**: `LocaleChangeInterceptor`를 추가해 URL 파라미터(예: `?lang=ko`)로 로케일을 동적으로 변경할 수 있습니다:
|
||||
```java
|
||||
@Bean
|
||||
public LocaleChangeInterceptor localeChangeInterceptor() {
|
||||
LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
|
||||
lci.setParamName("lang");
|
||||
return lci;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WebMvcConfigurer webMvcConfigurer() {
|
||||
return new WebMvcConfigurer() {
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(localeChangeInterceptor());
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 4. **실행 예시**
|
||||
- 요청: `GET /greet` (`Accept-Language: ko`)
|
||||
- 응답: "안녕하세요, 사용자님!"
|
||||
- 요청: `GET /greet` (`Accept-Language: en`)
|
||||
- 응답: "Hello, 사용자!"
|
||||
|
||||
### 5. **주의점**
|
||||
- 메시지 파일이 없거나 키가 누락된 경우, 기본 메시지 파일(`messages.properties`)을 참조하거나 예외가 발생할 수 있습니다.
|
||||
- UTF-8 인코딩을 명시적으로 설정하지 않으면 한글 등 비ASCII 문자가 깨질 수 있으니 주의하세요.
|
||||
|
||||
스프링부트의 국제화는 이렇게 간단하면서도 강력하게 다국어 지원을 가능하게 합니다. 추가로 궁금한 점이 있으면 말씀해주세요!
|
||||
97
docs/jpa/01_엔티티와 테이블 매핑.md
Normal file
97
docs/jpa/01_엔티티와 테이블 매핑.md
Normal file
@@ -0,0 +1,97 @@
|
||||
아래는 "엔티티와 테이블 매핑"에 대해 예시와 함께 설명하는 글입니다. 롬복(Lombok)을 활용해 코드를 간결하게 작성했습니다.
|
||||
|
||||
---
|
||||
|
||||
### 엔티티와 테이블 매핑
|
||||
|
||||
JPA(Java Persistence API)에서 엔티티(Entity)는 데이터베이스의 테이블과 매핑되는 객체입니다. 즉, 객체지향 프로그래밍의 클래스를 관계형 데이터베이스의 테이블 구조에 연결하는 역할을 합니다. 이를 통해 개발자는 SQL 쿼리를 직접 작성하지 않고도 객체를 통해 데이터를 조작할 수 있습니다. 엔티티와 테이블 매핑은 `@Entity`, `@Table`, `@Id`, `@Column` 같은 어노테이션을 사용해 정의됩니다.
|
||||
|
||||
#### 기본 매핑 규칙
|
||||
- `@Entity`: 클래스가 엔티티임을 나타냅니다. JPA가 이 클래스를 관리 대상으로 인식합니다.
|
||||
- `@Table`: 엔티티가 매핑될 데이터베이스 테이블을 지정합니다. 생략하면 클래스 이름이 테이블 이름으로 사용됩니다.
|
||||
- `@Id`: 엔티티의 기본 키(Primary Key)를 지정합니다.
|
||||
- `@Column`: 필드가 매핑될 테이블의 컬럼을 지정합니다. 생략하면 필드 이름이 컬럼 이름으로 사용됩니다.
|
||||
|
||||
#### 예시: 회원(Member) 엔티티
|
||||
아래는 회원 정보를 관리하는 `Member` 엔티티를 롬복을 사용해 작성한 예시입니다.
|
||||
|
||||
```java
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "members") // 테이블 이름 지정
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor // 기본 생성자 (JPA 요구 사항)
|
||||
public class Member {
|
||||
|
||||
@Id // 기본 키
|
||||
@Column(name = "member_id") // 컬럼 이름 지정
|
||||
private Long id;
|
||||
|
||||
@Column(name = "username", nullable = false) // null 불가 컬럼
|
||||
private String name;
|
||||
|
||||
@Column(name = "email", unique = true) // 유니크 제약 조건
|
||||
private String email;
|
||||
|
||||
// 추가 생성자 (편의를 위해)
|
||||
public Member(Long id, String name, String email) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 코드 설명
|
||||
1. **`@Entity`와 `@Table`**
|
||||
- `@Entity`는 이 클래스가 JPA 엔티티임을 나타냅니다.
|
||||
- `@Table(name = "members")`는 이 엔티티가 `members`라는 테이블에 매핑됨을 지정합니다. 생략하면 클래스 이름(`Member`)이 기본 테이블 이름으로 사용됩니다.
|
||||
|
||||
2. **`@Id`와 `@Column`**
|
||||
- `id` 필드는 `@Id`로 기본 키임을 선언합니다.
|
||||
- `@Column(name = "member_id")`는 `id` 필드가 `members` 테이블의 `member_id` 컬럼에 매핑됨을 나타냅니다.
|
||||
- `name` 필드는 `@Column(name = "username", nullable = false)`로, `username` 컬럼에 매핑되며 `NULL` 값을 허용하지 않습니다.
|
||||
- `email` 필드는 `@Column(name = "email", unique = true)`로, `email` 컬럼에 매핑되며 중복 값을 허용하지 않는 유니크 제약 조건을 가집니다.
|
||||
|
||||
3. **롬복 활용**
|
||||
- `@Getter`와 `@Setter`로 getter와 setter 메서드를 자동 생성합니다.
|
||||
- `@NoArgsConstructor`로 JPA가 요구하는 기본 생성자를 생성합니다. JPA는 엔티티 객체를 생성한 후 리플렉션을 통해 필드 값을 채우므로 기본 생성자가 필수입니다.
|
||||
|
||||
#### 매핑된 테이블 구조
|
||||
위 엔티티에 해당하는 데이터베이스 테이블은 다음과 같습니다:
|
||||
|
||||
```sql
|
||||
CREATE TABLE members (
|
||||
member_id BIGINT PRIMARY KEY,
|
||||
username VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) UNIQUE
|
||||
);
|
||||
```
|
||||
|
||||
#### 동작 방식
|
||||
- JPA는 `Member` 객체를 저장(`persist`)하면 `members` 테이블에 데이터를 삽입합니다.
|
||||
- 예를 들어, `new Member(1L, "홍길동", "hong@example.com")` 객체를 저장하면 테이블에 다음과 같은 레코드가 추가됩니다:
|
||||
```
|
||||
member_id: 1, username: "홍길동", email: "hong@example.com"
|
||||
```
|
||||
|
||||
#### 추가 팁
|
||||
- 필드와 컬럼 이름이 동일하다면 `@Column`은 생략 가능합니다.
|
||||
- `@GeneratedValue`를 `@Id`와 함께 사용하면 기본 키를 자동 생성할 수 있습니다(예: `AUTO_INCREMENT`).
|
||||
|
||||
---
|
||||
|
||||
이처럼 엔티티와 테이블 매핑은 JPA의 핵심 기능으로, 객체와 데이터베이스 간의 자연스러운 연결을 가능하게 합니다. 다음 장에서는 연관관계 매핑(예: `@OneToMany`, `@ManyToOne`)을 다루며 더 복잡한 구조를 살펴보겠습니다.
|
||||
|
||||
---
|
||||
|
||||
이 글이 책의 스타일에 맞는지, 더 추가하거나 수정할 내용이 있는지 알려주시면 반영하겠습니다!
|
||||
140
docs/jpa/02_기본키 맵핑.md
Normal file
140
docs/jpa/02_기본키 맵핑.md
Normal file
@@ -0,0 +1,140 @@
|
||||
아래는 "기본 키 매핑"에 대해 예시와 함께 설명하는 글입니다. 롬복을 사용해 코드를 간결하게 유지했습니다.
|
||||
|
||||
---
|
||||
|
||||
### 기본 키 매핑
|
||||
|
||||
JPA에서 기본 키(Primary Key)는 엔티티를 식별하는 고유한 값으로, 데이터베이스 테이블의 각 레코드를 구분하는 데 사용됩니다. JPA는 `@Id` 어노테이션을 통해 기본 키를 지정하며, 기본 키 값을 자동으로 생성하는 다양한 전략을 제공합니다. 이를 "기본 키 매핑"이라고 부르며, `@GeneratedValue`와 함께 사용해 효율적으로 관리할 수 있습니다.
|
||||
|
||||
#### 기본 키 매핑의 주요 어노테이션
|
||||
- **`@Id`**: 필드가 엔티티의 기본 키임을 나타냅니다. 모든 엔티티는 반드시 하나의 `@Id`를 가져야 합니다.
|
||||
- **`@GeneratedValue`**: 기본 키 값을 자동 생성하도록 설정합니다. `strategy` 속성을 통해 생성 전략을 지정할 수 있습니다.
|
||||
|
||||
#### 기본 키 생성 전략
|
||||
JPA는 네 가지 주요 기본 키 생성 전략을 제공합니다:
|
||||
1. **`GenerationType.AUTO`**
|
||||
- JPA 구현체(예: Hibernate)가 데이터베이스에 맞는 최적의 전략을 자동 선택합니다. 기본값이며, 환경에 따라 다르게 동작할 수 있습니다.
|
||||
2. **`GenerationType.IDENTITY`**
|
||||
- 데이터베이스의 `AUTO_INCREMENT` 기능을 사용합니다. MySQL, PostgreSQL 등에서 흔히 사용됩니다.
|
||||
3. **`GenerationType.SEQUENCE`**
|
||||
- 데이터베이스 시퀀스를 사용합니다. Oracle, PostgreSQL 등에서 지원됩니다.
|
||||
4. **`GenerationType.TABLE`**
|
||||
- 별도의 테이블을 만들어 기본 키 값을 관리합니다. 데이터베이스 종류에 상관없이 동작하지만 성능이 느릴 수 있습니다.
|
||||
|
||||
#### 예시: 기본 키 매핑
|
||||
아래는 `Product` 엔티티를 통해 기본 키 매핑을 보여주는 예시입니다. 롬복을 사용해 코드를 간략화했습니다.
|
||||
|
||||
```java
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Column;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class Product {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY) // 자동 증가 전략
|
||||
@Column(name = "product_id")
|
||||
private Long id;
|
||||
|
||||
@Column(name = "product_name")
|
||||
private String name;
|
||||
|
||||
@Column(name = "price")
|
||||
private int price;
|
||||
|
||||
// 편의를 위한 생성자
|
||||
public Product(String name, int price) {
|
||||
this.name = name;
|
||||
this.price = price;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 코드 설명
|
||||
1. **`@Id`와 `@GeneratedValue`**
|
||||
- `id` 필드는 `@Id`로 기본 키임을 선언합니다.
|
||||
- `@GeneratedValue(strategy = GenerationType.IDENTITY)`는 MySQL의 `AUTO_INCREMENT`처럼 데이터베이스가 기본 키 값을 자동으로 증가시키도록 설정합니다.
|
||||
|
||||
2. **동작 방식**
|
||||
- `Product` 객체를 생성하고 저장(`persist`)하면, `id` 값은 개발자가 직접 지정하지 않아도 데이터베이스가 자동으로 1, 2, 3… 순으로 생성합니다.
|
||||
- 예를 들어, `new Product("노트북", 1500000)`을 저장하면 데이터베이스에 다음과 같은 레코드가 삽입됩니다:
|
||||
```
|
||||
product_id: 1, product_name: "노트북", price: 1500000
|
||||
```
|
||||
|
||||
#### 매핑된 테이블 구조
|
||||
위 엔티티에 해당하는 테이블은 다음과 같습니다:
|
||||
|
||||
```sql
|
||||
CREATE TABLE product (
|
||||
product_id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
product_name VARCHAR(255),
|
||||
price INT
|
||||
);
|
||||
```
|
||||
|
||||
#### 시퀀스 사용 예시
|
||||
만약 Oracle 데이터베이스를 사용한다면 `GenerationType.SEQUENCE`를 사용할 수 있습니다. 아래는 그 예시입니다.
|
||||
|
||||
```java
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.SequenceGenerator;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class Order {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "order_seq")
|
||||
@SequenceGenerator(name = "order_seq", sequenceName = "ORDER_SEQ", allocationSize = 1)
|
||||
@Column(name = "order_id")
|
||||
private Long id;
|
||||
|
||||
@Column(name = "order_amount")
|
||||
private int amount;
|
||||
|
||||
public Order(int amount) {
|
||||
this.amount = amount;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **`@SequenceGenerator`**: `ORDER_SEQ`라는 데이터베이스 시퀀스를 정의합니다. `allocationSize`는 시퀀스 증가 단위를 지정합니다.
|
||||
- 테이블과 별도로 `ORDER_SEQ` 시퀀스가 생성되며, `id` 값은 이 시퀀스에서 가져옵니다.
|
||||
|
||||
#### 선택 기준
|
||||
- **`IDENTITY`**: 간단하고 성능이 좋아 MySQL 환경에서 추천됩니다.
|
||||
- **`SEQUENCE`**: 대량 삽입 시 성능이 우수하며, Oracle 같은 시퀀스 지원 DB에서 적합합니다.
|
||||
- **`AUTO`**: 데이터베이스에 의존하지 않고 유연하게 사용하려 할 때 유용합니다.
|
||||
- **`TABLE`**: 데이터베이스 독립성을 보장해야 할 때 사용되지만, 실무에서는 드물게 선택됩니다.
|
||||
|
||||
#### 주의사항
|
||||
- `@GeneratedValue`를 사용하지 않으면 개발자가 직접 기본 키 값을 설정해야 합니다.
|
||||
- 기본 키는 `Long`, `Integer` 같은 래퍼 타입을 사용하는 것이 좋습니다. 기본형(`long`, `int`)은 0으로 초기화되므로 의도치 않은 동작을 방지할 수 있습니다.
|
||||
|
||||
---
|
||||
|
||||
기본 키 매핑은 JPA에서 데이터의 고유성을 보장하는 핵심 요소입니다. 적절한 생성 전략을 선택하면 개발 편의성과 성능을 모두 확보할 수 있습니다. 다음 장에서는 관계 매핑을 통해 엔티티 간 연결을 다뤄보겠습니다.
|
||||
|
||||
---
|
||||
|
||||
이 글이 책의 흐름에 적합한지, 추가 설명이나 수정이 필요하면 말씀해 주세요!
|
||||
236
docs/jpa/03_관계 매핑.md
Normal file
236
docs/jpa/03_관계 매핑.md
Normal file
@@ -0,0 +1,236 @@
|
||||
아래는 "관계 매핑"에 대해 예시와 함께 설명하는 글입니다. 롬복을 사용해 코드를 간결하게 작성했습니다.
|
||||
|
||||
---
|
||||
|
||||
### 관계 매핑
|
||||
|
||||
JPA에서 관계 매핑(Relationship Mapping)은 엔티티 간의 연관성을 정의하는 방법으로, 객체지향 프로그래밍과 관계형 데이터베이스의 다리를 잇는 핵심 기능입니다. 데이터베이스에서는 외래 키(Foreign Key)를 통해 테이블 간 관계를 표현하지만, JPA에서는 어노테이션(`@OneToMany`, `@ManyToOne` 등)을 사용해 객체 간 관계를 자연스럽게 매핑합니다. 이를 통해 개발자는 SQL 조인을 직접 작성하지 않고도 연관된 데이터를 쉽게 조회하고 관리할 수 있습니다.
|
||||
|
||||
#### 관계 매핑의 주요 유형
|
||||
JPA는 네 가지 주요 관계 매핑을 지원합니다:
|
||||
1. **1:1 (One-to-One)**: 한 엔티티가 다른 엔티티와 단일 관계를 가짐.
|
||||
2. **1:N (One-to-Many)**: 한 엔티티가 여러 엔티티와 관계를 가짐.
|
||||
3. **N:1 (Many-to-One)**: 여러 엔티티가 한 엔티티와 관계를 가짐.
|
||||
4. **N:M (Many-to-Many)**: 여러 엔티티가 서로 여러 엔티티와 관계를 가짐.
|
||||
|
||||
#### 주요 어노테이션
|
||||
- **`@OneToMany`**: 1:N 관계를 정의.
|
||||
- **`@ManyToOne`**: N:1 관계를 정의.
|
||||
- **`@OneToOne`**: 1:1 관계를 정의.
|
||||
- **`@ManyToMany`**: N:M 관계를 정의.
|
||||
- **`@JoinColumn`**: 외래 키 컬럼을 지정.
|
||||
|
||||
#### 예시 1: 1:N과 N:1 관계 (팀과 회원)
|
||||
아래는 `Team`과 `Member` 엔티티 간의 1:N 및 N:1 관계를 보여주는 예시입니다.
|
||||
|
||||
```java
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.OneToMany;
|
||||
import jakarta.persistence.Column;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class Team {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "team_id")
|
||||
private Long id;
|
||||
|
||||
@Column(name = "team_name")
|
||||
private String name;
|
||||
|
||||
@OneToMany(mappedBy = "team") // 1:N 관계, Member의 team 필드에 의해 매핑
|
||||
private List<Member> members = new ArrayList<>();
|
||||
|
||||
public Team(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.Column;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class Member {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "member_id")
|
||||
private Long id;
|
||||
|
||||
@Column(name = "username")
|
||||
private String name;
|
||||
|
||||
@ManyToOne // N:1 관계
|
||||
@JoinColumn(name = "team_id") // 외래 키 컬럼 지정
|
||||
private Team team;
|
||||
|
||||
public Member(String name, Team team) {
|
||||
this.name = name;
|
||||
this.team = team;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 코드 설명
|
||||
1. **`Team` (1:N)**
|
||||
- `@OneToMany(mappedBy = "team")`: `Team`은 여러 `Member`를 가질 수 있으며, 관계의 주인은 `Member`의 `team` 필드입니다. `mappedBy`는 양방향 매핑에서 사용되며, 외래 키를 직접 관리하지 않음을 나타냅니다.
|
||||
|
||||
2. **`Member` (N:1)**
|
||||
- `@ManyToOne`: `Member`는 하나의 `Team`에 속합니다.
|
||||
- `@JoinColumn(name = "team_id")`: `Member` 테이블에 `team_id`라는 외래 키 컬럼이 생성됩니다.
|
||||
|
||||
#### 매핑된 테이블 구조
|
||||
- `Team` 테이블:
|
||||
```sql
|
||||
CREATE TABLE team (
|
||||
team_id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
team_name VARCHAR(255)
|
||||
);
|
||||
```
|
||||
- `Member` 테이블:
|
||||
```sql
|
||||
CREATE TABLE member (
|
||||
member_id BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
username VARCHAR(255),
|
||||
team_id BIGINT,
|
||||
FOREIGN KEY (team_id) REFERENCES team(team_id)
|
||||
);
|
||||
```
|
||||
|
||||
#### 동작 방식
|
||||
- `Team team = new Team("개발팀");`
|
||||
- `Member member = new Member("홍길동", team);`
|
||||
- `team.getMembers().add(member);`
|
||||
- 저장 시 `Member` 테이블에 `team_id`가 `Team`의 `team_id`를 참조하는 레코드가 삽입됩니다.
|
||||
|
||||
#### 예시 2: N:M 관계 (학생과 강의)
|
||||
N:M 관계는 중간 테이블을 통해 구현됩니다.
|
||||
|
||||
```java
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.ManyToMany;
|
||||
import jakarta.persistence.Column;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class Student {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "student_id")
|
||||
private Long id;
|
||||
|
||||
@Column(name = "student_name")
|
||||
private String name;
|
||||
|
||||
@ManyToMany
|
||||
private List<Course> courses = new ArrayList<>();
|
||||
|
||||
public Student(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.ManyToMany;
|
||||
import jakarta.persistence.Column;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Entity
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
public class Course {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "course_id")
|
||||
private Long id;
|
||||
|
||||
@Column(name = "course_name")
|
||||
private String name;
|
||||
|
||||
@ManyToMany(mappedBy = "courses")
|
||||
private List<Student> students = new ArrayList<>();
|
||||
|
||||
public Course(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 코드 설명
|
||||
- `@ManyToMany`: `Student`와 `Course`는 다대다 관계입니다.
|
||||
- JPA는 자동으로 중간 테이블(예: `student_course`)을 생성해 외래 키를 관리합니다.
|
||||
|
||||
#### 매핑된 테이블 구조
|
||||
- `student_course` (중간 테이블):
|
||||
```sql
|
||||
CREATE TABLE student_course (
|
||||
student_id BIGINT,
|
||||
course_id BIGINT,
|
||||
FOREIGN KEY (student_id) REFERENCES student(student_id),
|
||||
FOREIGN KEY (course_id) REFERENCES course(course_id)
|
||||
);
|
||||
```
|
||||
|
||||
#### 주의사항
|
||||
- **양방향 매핑**: `mappedBy`를 사용해 관계의 주인을 명확히 지정해야 중복 매핑을 방지합니다.
|
||||
- **지연 로딩(Lazy Loading)**: `@OneToMany`와 `@ManyToMany`는 기본적으로 지연 로딩이 적용되며, 필요 시 `fetch = FetchType.EAGER`로 즉시 로딩을 설정할 수 있습니다.
|
||||
- **N:M의 한계**: 실무에서는 N:M 대신 중간 엔티티를 만들어 1:N, N:1로 분리하는 경우가 많습니다.
|
||||
|
||||
---
|
||||
|
||||
관계 매핑은 JPA의 강력한 기능으로, 객체 간 관계를 데이터베이스에 자연스럽게 반영합니다. 다음 장에서는 성능 최적화를 위한 페치 전략과 N+1 문제를 다뤄보겠습니다.
|
||||
|
||||
---
|
||||
|
||||
책의 흐름에 맞는지, 추가 예시나 설명이 필요하면 말씀해 주세요!
|
||||
139
docs/jpa/04_JPA 생명주기와 영속성 컨텍스트.md
Normal file
139
docs/jpa/04_JPA 생명주기와 영속성 컨텍스트.md
Normal file
@@ -0,0 +1,139 @@
|
||||
아래는 "JPA 생명주기와 영속성 컨텍스트"에 대해 설명하는 글입니다. 예시를 포함하되, 롬복은 사용하지 않고 명확한 이해를 돕기 위해 기본적인 코드 스타일을 유지했습니다.
|
||||
|
||||
---
|
||||
|
||||
### JPA 생명주기와 영속성 컨텍스트
|
||||
|
||||
JPA에서 엔티티는 특정한 생명주기를 거치며 관리됩니다. 이 생명주기는 영속성 컨텍스트(Persistence Context)라는 JPA의 핵심 개념과 밀접하게 연관되어 있습니다. 영속성 컨텍스트는 엔티티의 상태를 추적하고 관리하며, 데이터베이스와의 동기화를 담당하는 환경입니다. 이를 통해 JPA는 객체와 데이터베이스 간의 매핑을 효율적으로 처리합니다.
|
||||
|
||||
#### 영속성 컨텍스트란?
|
||||
영속성 컨텍스트는 엔티티 매니저(EntityManager)가 엔티티를 관리하는 논리적인 공간입니다. 엔티티가 영속성 컨텍스트에 포함되면 JPA가 해당 엔티티의 상태를 추적하고, 트랜잭션이 커밋될 때 데이터베이스에 반영합니다. 주요 특징은 다음과 같습니다:
|
||||
- **1차 캐시**: 조회한 엔티티를 메모리에 저장해 동일 트랜잭션 내에서 재사용 가능.
|
||||
- **변경 감지(Dirty Checking)**: 엔티티의 변경 사항을 자동으로 감지해 SQL을 생성.
|
||||
- **쓰기 지연(Write Behind)**: 트랜잭션 커밋 시점에 변경 사항을 한꺼번에 데이터베이스에 반영.
|
||||
|
||||
#### 엔티티 생명주기
|
||||
엔티티는 네 가지 상태를 거칩니다:
|
||||
1. **비영속(Transient)**
|
||||
- 엔티티 객체가 생성되었지만, 영속성 컨텍스트나 데이터베이스와 연결되지 않은 상태.
|
||||
- JPA가 전혀 관리하지 않음.
|
||||
2. **영속(Managed)**
|
||||
- 영속성 컨텍스트에 의해 관리되는 상태.
|
||||
- `persist()`나 `find()` 메서드로 엔티티가 영속성 컨텍스트에 포함됨.
|
||||
3. **준영속(Detached)**
|
||||
- 영속성 컨텍스트에서 분리된 상태.
|
||||
- `detach()`나 `close()`로 인해 더 이상 관리되지 않음.
|
||||
4. **삭제(Removed)**
|
||||
- 영속성 컨텍스트와 데이터베이스에서 삭제되도록 예약된 상태.
|
||||
- `remove()` 호출 후 트랜잭션 커밋 시 삭제됨.
|
||||
|
||||
#### 생명주기 예시
|
||||
아래는 `Book` 엔티티를 통해 생명주기를 설명하는 코드입니다.
|
||||
|
||||
```java
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
|
||||
@Entity
|
||||
public class Book {
|
||||
@Id
|
||||
private Long id;
|
||||
private String title;
|
||||
|
||||
public Book() {} // JPA 기본 생성자 요구
|
||||
|
||||
public Book(Long id, String title) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public Long getId() { return id; }
|
||||
public void setId(Long id) { this.id = id; }
|
||||
public String getTitle() { return title; }
|
||||
public void setTitle(String title) { this.title = title; }
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.persistence.EntityManagerFactory;
|
||||
import jakarta.persistence.Persistence;
|
||||
|
||||
public class JpaLifecycleExample {
|
||||
public static void main(String[] args) {
|
||||
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpaExample");
|
||||
EntityManager em = emf.createEntityManager();
|
||||
em.getTransaction().begin();
|
||||
|
||||
// 1. 비영속 상태
|
||||
Book book = new Book(1L, "JPA 기초");
|
||||
System.out.println("상태: 비영속");
|
||||
|
||||
// 2. 영속 상태
|
||||
em.persist(book);
|
||||
System.out.println("상태: 영속");
|
||||
|
||||
// 영속성 컨텍스트에서 동일 객체 재사용 (1차 캐시)
|
||||
Book foundBook = em.find(Book.class, 1L);
|
||||
System.out.println("동일 객체: " + (book == foundBook)); // true
|
||||
|
||||
// 변경 감지
|
||||
book.setTitle("JPA 심화"); // 별도 update 호출 없이 변경 감지됨
|
||||
|
||||
// 3. 삭제 상태
|
||||
em.remove(book);
|
||||
System.out.println("상태: 삭제 예약");
|
||||
|
||||
// 트랜잭션 커밋 - 데이터베이스 반영
|
||||
em.getTransaction().commit();
|
||||
System.out.println("커밋 완료");
|
||||
|
||||
// 4. 준영속 상태
|
||||
em.detach(book); // 이미 삭제되었으므로 효과 없음
|
||||
em.close();
|
||||
|
||||
// 새로운 트랜잭션에서 준영속 상태 확인
|
||||
EntityManager em2 = emf.createEntityManager();
|
||||
em2.getTransaction().begin();
|
||||
book.setTitle("JPA 고급"); // 영속성 컨텍스트가 없으므로 반영 안 됨
|
||||
em2.getTransaction().commit();
|
||||
em2.close();
|
||||
|
||||
emf.close();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 코드 설명
|
||||
1. **비영속 상태**
|
||||
- `Book book = new Book(1L, "JPA 기초");`: 객체는 생성되었지만 JPA와 무관합니다.
|
||||
|
||||
2. **영속 상태**
|
||||
- `em.persist(book)`: 영속성 컨텍스트에 포함되며, 1차 캐시에 저장됩니다.
|
||||
- `em.find(Book.class, 1L)`: 데이터베이스 조회 없이 캐시에서 반환됩니다.
|
||||
|
||||
3. **변경 감지**
|
||||
- `book.setTitle("JPA 심화")`: 영속 상태의 엔티티는 변경이 감지되어 커밋 시 자동으로 `UPDATE` 쿼리가 실행됩니다.
|
||||
|
||||
4. **삭제 상태**
|
||||
- `em.remove(book)`: 엔티티가 삭제 예약되며, 커밋 시 `DELETE` 쿼리가 실행됩니다.
|
||||
|
||||
5. **준영속 상태**
|
||||
- `em.detach(book)` 또는 `em.close()`: 영속성 컨텍스트에서 분리되어 더 이상 관리되지 않습니다.
|
||||
|
||||
#### 영속성 컨텍스트의 이점
|
||||
- **성능 향상**: 1차 캐시로 동일 트랜잭션 내 반복 조회를 줄임.
|
||||
- **자동 동기화**: 변경 감지로 개발자가 직접 SQL을 작성할 필요 없음.
|
||||
- **트랜잭션 보장**: 쓰기 지연으로 트랜잭션 내 변경을 일괄 처리.
|
||||
|
||||
#### 주의사항
|
||||
- 영속성 컨텍스트는 트랜잭션 범위에 따라 생명주기가 달라집니다. 트랜잭션이 끝나면 컨텍스트도 종료됩니다(기본적으로).
|
||||
- 준영속 상태의 엔티티는 변경 감지가 적용되지 않으므로, 필요 시 `merge()`로 다시 영속 상태로 전환해야 합니다.
|
||||
|
||||
---
|
||||
|
||||
JPA의 생명주기와 영속성 컨텍스트는 엔티티 관리의 핵심입니다. 이를 이해하면 데이터베이스 작업을 더 효율적으로 처리할 수 있습니다. 다음 장에서는 트랜잭션 관리와 실무 활용을 다뤄보겠습니다.
|
||||
|
||||
---
|
||||
|
||||
책의 흐름에 맞는지, 추가 예시나 설명이 필요하면 말씀해 주세요!
|
||||
121
docs/jpa/05_엔티티 설계 시 고려사항.md
Normal file
121
docs/jpa/05_엔티티 설계 시 고려사항.md
Normal file
@@ -0,0 +1,121 @@
|
||||
아래는 "엔티티 설계 시 고려사항"에 대해 설명하는 글입니다. 실무에서의 경험과 JPA의 특성을 반영해 실용적인 내용을 담았습니다.
|
||||
|
||||
---
|
||||
|
||||
### 엔티티 설계 시 고려사항
|
||||
|
||||
JPA를 사용해 엔티티를 설계할 때는 단순히 테이블과 객체를 매핑하는 것을 넘어, 객체지향 원칙, 성능, 유지보수성, 그리고 데이터베이스의 특성을 모두 고려해야 합니다. 잘못된 설계는 코드 복잡성을 높이거나 성능 문제를 일으킬 수 있으므로, 초기 설계 단계에서 신중한 접근이 필요합니다. 아래는 엔티티 설계 시 주요 고려사항입니다.
|
||||
|
||||
#### 1. 객체지향 원칙 준수
|
||||
엔티티는 데이터베이스 테이블뿐만 아니라 객체지향의 개념을 반영해야 합니다.
|
||||
- **캡슐화**: 필드를 `private`으로 설정하고, getter/setter를 통해 접근을 제어합니다. 불필요한 setter는 제거해 데이터 무결성을 보호합니다.
|
||||
- **도메인 주도 설계(DDD)**: 엔티티가 단순한 데이터 홀더가 아니라 비즈니스 로직을 포함하도록 설계합니다. 예를 들어, `Order` 엔티티에 주문 상태 변경 메서드를 추가합니다.
|
||||
- **불변성**: 가능하면 생성 시점에 값을 설정하고 변경을 최소화합니다(예: `@Setter` 대신 생성자 사용).
|
||||
|
||||
#### 2. 식별자(기본 키) 전략
|
||||
기본 키는 엔티티의 고유성을 보장하며, 설계 시 다음을 고려합니다:
|
||||
- **자연 키 vs 대리 키**: 주민번호 같은 자연 키는 고유하지만 변경 가능성이 있으므로, 의미 없는 대리 키(예: `Long` 타입 ID)를 사용하는 것이 일반적입니다.
|
||||
- **생성 전략**: `@GeneratedValue`로 `IDENTITY`나 `SEQUENCE`를 선택합니다. 대량 삽입이 많다면 `SEQUENCE`가 유리할 수 있습니다.
|
||||
- **타입 선택**: `Long` 같은 래퍼 타입을 사용해 `null` 가능성을 명확히 하고, 기본형(`long`)의 0 초기화 문제를 피합니다.
|
||||
|
||||
#### 3. 연관관계 설계
|
||||
엔티티 간 관계는 JPA의 강점 중 하나지만, 잘못 설계하면 성능 저하나 복잡성이 증가합니다.
|
||||
- **단방향 vs 양방향**: 가능하면 단방향 매핑을 우선 사용합니다. 양방향은 필요 시에만 추가하고, `mappedBy`로 관계의 주인을 명확히 지정합니다.
|
||||
- **지연 로딩(Lazy Loading)**: `@OneToMany`와 `@ManyToMany`는 기본적으로 지연 로딩을 사용해 불필요한 데이터 조회를 방지합니다. 필요 시 `fetch = FetchType.EAGER`를 고려하되, N+1 문제를 주의합니다.
|
||||
- **N:M 관계**: 실무에서는 중간 엔티티를 만들어 1:N, N:1로 분리하는 것이 유지보수와 확장성 측면에서 유리합니다.
|
||||
|
||||
#### 4. 성능 최적화
|
||||
엔티티 설계가 성능에 미치는 영향은 크므로 다음을 고려합니다:
|
||||
- **필드 수 최소화**: 불필요한 컬럼은 추가하지 않습니다. 예를 들어, 임시 계산 값은 엔티티에 저장하지 않고 DTO로 처리합니다.
|
||||
- **인덱스 활용**: 자주 조회되는 컬럼(예: 외래 키, 검색 조건)에 `@Index`를 추가하거나 데이터베이스에 직접 설정합니다.
|
||||
- **배치 크기 설정**: `@BatchSize`를 사용해 1:N 관계 조회 시 N+1 문제를 완화합니다.
|
||||
|
||||
#### 5. 데이터 무결성과 제약 조건
|
||||
데이터베이스와 엔티티 간 일관성을 유지하려면 제약 조건을 반영해야 합니다:
|
||||
- **`@Column(nullable = false)`**: 필수 입력 필드를 명시합니다.
|
||||
- **`@Column(unique = true)`**: 고유 제약 조건을 설정합니다(예: 이메일).
|
||||
- **Cascade 옵션**: `@OneToMany(cascade = CascadeType.ALL)`로 연관 엔티티를 자동 관리할 수 있지만, 과도한 사용은 예기치 않은 삭제를 유발할 수 있으니 주의합니다.
|
||||
|
||||
#### 6. 값 타입 활용
|
||||
단순 데이터는 값 타입(Embedded Type)으로 설계해 재사용성과 가독성을 높입니다.
|
||||
- 예: 주소(`Address`)를 별도 클래스로 분리.
|
||||
```java
|
||||
@Embeddable
|
||||
public class Address {
|
||||
private String city;
|
||||
private String street;
|
||||
// getter, constructor
|
||||
}
|
||||
|
||||
@Entity
|
||||
public class Member {
|
||||
@Id
|
||||
private Long id;
|
||||
@Embedded
|
||||
private Address address;
|
||||
}
|
||||
```
|
||||
- 값 타입은 불변 객체로 설계해 부작용을 방지합니다.
|
||||
|
||||
#### 7. 실무적 현실성
|
||||
실무에서는 이상적인 설계와 현실적 제약 사이에서 균형을 맞춰야 합니다:
|
||||
- **기존 테이블 매핑**: 레거시 데이터베이스와의 호환성을 위해 테이블 구조를 그대로 반영할 수 있습니다. `@Table`과 `@Column`으로 조정합니다.
|
||||
- **버전 관리**: `@Version`을 사용해 낙관적 락(Optimistic Locking)을 적용하면 동시성 문제를 방지할 수 있습니다.
|
||||
- **테스트 용이성**: 엔티티가 복잡해지면 단위 테스트 작성이 어려워지므로, 단순하고 명확하게 유지합니다.
|
||||
|
||||
#### 8. 확장성과 유지보수성
|
||||
미래의 요구사항 변화를 예측해 유연한 설계를 추구합니다:
|
||||
- **상속 매핑**: `@Inheritance`로 `SINGLE_TABLE`이나 `JOINED` 전략을 사용해 엔티티 계층을 설계합니다. 예: `Item`과 하위 클래스 `Book`, `Electronics`.
|
||||
- **공통 속성 추출**: `@MappedSuperclass`로 공통 필드(예: 생성일, 수정일)를 상속받아 재사용합니다.
|
||||
|
||||
#### 예시: 실무적 엔티티 설계
|
||||
```java
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.Embedded;
|
||||
|
||||
@Entity
|
||||
public class Order {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "order_id")
|
||||
private Long id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "member_id", nullable = false)
|
||||
private Member member;
|
||||
|
||||
@Embedded
|
||||
private Address deliveryAddress;
|
||||
|
||||
@Column(name = "order_date", nullable = false)
|
||||
private LocalDateTime orderDate;
|
||||
|
||||
// 비즈니스 로직
|
||||
public void cancel() {
|
||||
if (deliveryAddress != null) {
|
||||
throw new IllegalStateException("배송 중에는 취소 불가");
|
||||
}
|
||||
// 상태 변경 로직
|
||||
}
|
||||
|
||||
// 생성자, getter
|
||||
}
|
||||
```
|
||||
|
||||
#### 설계 포인트
|
||||
- `member`는 지연 로딩으로 성능 최적화.
|
||||
- `deliveryAddress`는 값 타입으로 재사용성 확보.
|
||||
- `cancel()` 메서드로 도메인 로직 포함.
|
||||
|
||||
---
|
||||
|
||||
엔티티 설계는 JPA 활용의 첫걸음이자 성공적인 프로젝트의 기반입니다. 위 고려사항을 바탕으로 객체와 데이터베이스 간 균형을 맞춘다면, 유지보수성과 성능을 모두 충족하는 설계를 완성할 수 있습니다. 다음 장에서는 연관관계 매핑 심화와 실무 패턴을 다뤄보겠습니다.
|
||||
|
||||
---
|
||||
|
||||
책의 흐름에 맞는지, 추가 내용이나 수정이 필요하면 말씀해 주세요!
|
||||
253
docs/jpa/06_상속 관계 매핑.md
Normal file
253
docs/jpa/06_상속 관계 매핑.md
Normal file
@@ -0,0 +1,253 @@
|
||||
아래는 "상속 관계 매핑"에 대해 설명하는 글입니다. JPA의 상속 매핑 전략을 예시와 함께 다루며, 실무에서의 활용성을 고려해 작성했습니다.
|
||||
|
||||
---
|
||||
|
||||
### 상속 관계 매핑
|
||||
|
||||
JPA에서 상속 관계 매핑(Inheritance Mapping)은 객체지향 프로그래밍의 상속 개념을 데이터베이스 테이블에 반영하는 방법입니다. 객체지향 설계에서 부모 클래스와 자식 클래스로 계층 구조를 구성하듯, JPA는 이를 테이블 구조에 매핑해 데이터의 계층적 특성을 관리할 수 있게 합니다. 이를 위해 JPA는 세 가지 주요 전략을 제공하며, 각 전략은 장단점이 있어 상황에 맞게 선택해야 합니다.
|
||||
|
||||
#### 상속 매핑 전략
|
||||
1. **단일 테이블 전략 (SINGLE_TABLE)**
|
||||
- 모든 클래스(부모, 자식)의 데이터를 하나의 테이블에 저장합니다.
|
||||
- 식별자(Discriminator Column)를 사용해 각 레코드를 구분합니다.
|
||||
2. **조인 전략 (JOINED)**
|
||||
- 부모 클래스와 자식 클래스 데이터를 별도의 테이블로 분리하고, 필요 시 조인으로 연결합니다.
|
||||
3. **테이블 per 클래스 전략 (TABLE_PER_CLASS)**
|
||||
- 각 클래스가 독립적인 테이블을 가지며, 부모 테이블 없이 자식 테이블만 존재합니다.
|
||||
|
||||
#### 주요 어노테이션
|
||||
- **`@Inheritance`**: 상속 전략을 지정합니다. (`strategy` 속성으로 `SINGLE_TABLE`, `JOINED`, `TABLE_PER_CLASS` 설정)
|
||||
- **`@DiscriminatorColumn`**: 단일 테이블 전략에서 구분 컬럼을 정의합니다.
|
||||
- **`@DiscriminatorValue`**: 각 자식 클래스의 구분 값을 지정합니다.
|
||||
|
||||
#### 1. 단일 테이블 전략 (SINGLE_TABLE)
|
||||
모든 필드가 하나의 테이블에 저장되며, 간단하고 조회 성능이 우수합니다.
|
||||
|
||||
```java
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Inheritance;
|
||||
import jakarta.persistence.InheritanceType;
|
||||
import jakarta.persistence.DiscriminatorColumn;
|
||||
import jakarta.persistence.DiscriminatorValue;
|
||||
|
||||
@Entity
|
||||
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
|
||||
@DiscriminatorColumn(name = "item_type")
|
||||
public abstract class Item {
|
||||
@Id
|
||||
private Long id;
|
||||
private String name;
|
||||
private int price;
|
||||
|
||||
// 생성자, getter, setter
|
||||
public Item() {}
|
||||
public Item(Long id, String name, int price) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.price = price;
|
||||
}
|
||||
public Long getId() { return id; }
|
||||
public String getName() { return name; }
|
||||
public int getPrice() { return price; }
|
||||
}
|
||||
|
||||
@Entity
|
||||
@DiscriminatorValue("BOOK")
|
||||
public class Book extends Item {
|
||||
private String author;
|
||||
|
||||
public Book() {}
|
||||
public Book(Long id, String name, int price, String author) {
|
||||
super(id, name, price);
|
||||
this.author = author;
|
||||
}
|
||||
public String getAuthor() { return author; }
|
||||
}
|
||||
|
||||
@Entity
|
||||
@DiscriminatorValue("ELECTRONICS")
|
||||
public class Electronics extends Item {
|
||||
private String brand;
|
||||
|
||||
public Electronics() {}
|
||||
public Electronics(Long id, String name, int price, String brand) {
|
||||
super(id, name, price);
|
||||
this.brand = brand;
|
||||
}
|
||||
public String getBrand() { return brand; }
|
||||
}
|
||||
```
|
||||
|
||||
- **테이블 구조**:
|
||||
```sql
|
||||
CREATE TABLE item (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name VARCHAR(255),
|
||||
price INT,
|
||||
item_type VARCHAR(31), -- 구분 컬럼
|
||||
author VARCHAR(255), -- Book 전용
|
||||
brand VARCHAR(255) -- Electronics 전용
|
||||
);
|
||||
```
|
||||
- **장점**: 조인 없이 단일 테이블에서 조회 가능, 성능 우수.
|
||||
- **단점**: 자식 클래스 필드가 모두 포함되므로 `NULL` 값이 많아질 수 있음, 테이블 크기 증가.
|
||||
|
||||
#### 2. 조인 전략 (JOINED)
|
||||
부모와 자식 클래스가 별도 테이블로 분리되며, 필요 시 조인합니다.
|
||||
|
||||
```java
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Inheritance;
|
||||
import jakarta.persistence.InheritanceType;
|
||||
|
||||
@Entity
|
||||
@Inheritance(strategy = InheritanceType.JOINED)
|
||||
public abstract class Item {
|
||||
@Id
|
||||
private Long id;
|
||||
private String name;
|
||||
private int price;
|
||||
|
||||
// 생성자, getter, setter (위와 동일)
|
||||
}
|
||||
|
||||
@Entity
|
||||
public class Book extends Item {
|
||||
private String author;
|
||||
|
||||
// 생성자, getter (위와 동일)
|
||||
}
|
||||
|
||||
@Entity
|
||||
public class Electronics extends Item {
|
||||
private String brand;
|
||||
|
||||
// 생성자, getter (위와 동일)
|
||||
}
|
||||
```
|
||||
|
||||
- **테이블 구조**:
|
||||
```sql
|
||||
CREATE TABLE item (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name VARCHAR(255),
|
||||
price INT
|
||||
);
|
||||
CREATE TABLE book (
|
||||
id BIGINT PRIMARY KEY,
|
||||
author VARCHAR(255),
|
||||
FOREIGN KEY (id) REFERENCES item(id)
|
||||
);
|
||||
CREATE TABLE electronics (
|
||||
id BIGINT PRIMARY KEY,
|
||||
brand VARCHAR(255),
|
||||
FOREIGN KEY (id) REFERENCES item(id)
|
||||
);
|
||||
```
|
||||
- **장점**: 테이블이 정규화되어 공간 효율적, 데이터 무결성 유지 쉬움.
|
||||
- **단점**: 조회 시 조인이 필요해 성능 저하 가능성 있음.
|
||||
|
||||
#### 3. 테이블 per 클래스 전략 (TABLE_PER_CLASS)
|
||||
각 클래스가 독립적인 테이블을 가집니다.
|
||||
|
||||
```java
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Inheritance;
|
||||
import jakarta.persistence.InheritanceType;
|
||||
|
||||
@Entity
|
||||
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
|
||||
public abstract class Item {
|
||||
@Id
|
||||
private Long id;
|
||||
private String name;
|
||||
private int price;
|
||||
|
||||
// 생성자, getter, setter (위와 동일)
|
||||
}
|
||||
|
||||
@Entity
|
||||
public class Book extends Item {
|
||||
private String author;
|
||||
|
||||
// 생성자, getter (위와 동일)
|
||||
}
|
||||
|
||||
@Entity
|
||||
public class Electronics extends Item {
|
||||
private String brand;
|
||||
|
||||
// 생성자, getter (위와 동일)
|
||||
}
|
||||
```
|
||||
|
||||
- **테이블 구조**:
|
||||
```sql
|
||||
CREATE TABLE book (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name VARCHAR(255),
|
||||
price INT,
|
||||
author VARCHAR(255)
|
||||
);
|
||||
CREATE TABLE electronics (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name VARCHAR(255),
|
||||
price INT,
|
||||
brand VARCHAR(255)
|
||||
);
|
||||
```
|
||||
- **장점**: 각 테이블이 독립적이어서 구조 단순.
|
||||
- **단점**: 부모 클래스(`Item`)로 조회 시 UNION 쿼리가 발생해 성능 저하, 다형성 활용 어려움.
|
||||
|
||||
#### 선택 기준
|
||||
- **`SINGLE_TABLE`**: 데이터가 많지 않고, 조회 성능이 중요한 경우 적합. 예: 소규모 상품 카탈로그.
|
||||
- **`JOINED`**: 데이터 정규화와 무결성이 중요하거나, 자식 클래스의 필드가 많을 때 유리. 예: 복잡한 계층 구조.
|
||||
- **`TABLE_PER_CLASS`**: 드물게 사용되며, 레거시 시스템 호환성이나 특정 요구사항에서 고려. 실무에서는 권장되지 않음.
|
||||
|
||||
#### 주의사항
|
||||
- **기본 생성자**: JPA는 리플렉션을 사용하므로 모든 클래스에 기본 생성자가 필요합니다.
|
||||
- **다형성**: `Item` 타입으로 조회 시 자식 객체가 반환되지만, `TABLE_PER_CLASS`에서는 제한적입니다.
|
||||
- **Discriminator**: `SINGLE_TABLE`에서는 필수, `JOINED`에서는 선택 사항입니다.
|
||||
|
||||
#### 실무 예시
|
||||
```java
|
||||
@Entity
|
||||
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
|
||||
@DiscriminatorColumn(name = "vehicle_type")
|
||||
public abstract class Vehicle {
|
||||
@Id
|
||||
private Long id;
|
||||
private String manufacturer;
|
||||
|
||||
// 생성자, getter
|
||||
}
|
||||
|
||||
@Entity
|
||||
@DiscriminatorValue("CAR")
|
||||
public class Car extends Vehicle {
|
||||
private int seats;
|
||||
|
||||
// 생성자, getter
|
||||
}
|
||||
|
||||
@Entity
|
||||
@DiscriminatorValue("TRUCK")
|
||||
public class Truck extends Vehicle {
|
||||
private double loadCapacity;
|
||||
|
||||
// 생성자, getter
|
||||
}
|
||||
```
|
||||
|
||||
- 차량(`Vehicle`) 계층을 단일 테이블로 관리하며, `vehicle_type`으로 구분합니다.
|
||||
|
||||
---
|
||||
|
||||
상속 관계 매핑은 객체지향 설계를 데이터베이스에 반영하는 강력한 도구입니다. 프로젝트 요구사항과 성능 목표에 따라 적절한 전략을 선택하면 유연성과 효율성을 모두 확보할 수 있습니다. 다음 장에서는 값 타입과 임베디드 타입을 다뤄보겠습니다.
|
||||
|
||||
---
|
||||
|
||||
책의 흐름에 맞는지, 추가 예시나 수정이 필요하면 말씀해 주세요!
|
||||
223
docs/jpa/07_값 타입과 임베디드 타입의 활용.md
Normal file
223
docs/jpa/07_값 타입과 임베디드 타입의 활용.md
Normal file
@@ -0,0 +1,223 @@
|
||||
아래는 "값 타입과 임베디드 타입의 활용"에 대해 설명하는 글입니다. JPA의 값 타입(Value Type)과 임베디드 타입(Embedded Type)을 실무 관점에서 다루며, 예시를 포함해 구체적으로 작성했습니다.
|
||||
|
||||
---
|
||||
|
||||
### 값 타입과 임베디드 타입의 활용
|
||||
|
||||
JPA에서 값 타입(Value Type)은 엔티티와 달리 독립적인 생명주기를 가지지 않고, 특정 엔티티에 속해 그 엔티티와 함께 생성되고 소멸되는 데이터입니다. 값 타입은 단순 데이터(예: 기본 타입)와 복합 데이터(임베디드 타입)로 나뉘며, 특히 임베디드 타입은 객체지향 설계의 재사용성과 가독성을 높이는 데 유용합니다. 이를 잘 활용하면 코드 중복을 줄이고 도메인 모델을 더 풍부하게 표현할 수 있습니다.
|
||||
|
||||
#### 값 타입의 종류
|
||||
1. **기본 값 타입 (Basic Value Type)**
|
||||
- 자바의 기본형(`int`, `boolean`)과 래퍼 클래스(`Integer`, `String`) 등.
|
||||
- 엔티티 필드에 직접 매핑되며, 데이터베이스 컬럼에 저장됩니다.
|
||||
2. **임베디드 타입 (Embedded Type)**
|
||||
- 사용자 정의 클래스를 값 타입으로 사용하며, `@Embeddable`과 `@Embedded`로 정의합니다.
|
||||
- 여러 필드를 묶어 논리적 단위를 형성합니다.
|
||||
3. **컬렉션 값 타입 (Collection Value Type)**
|
||||
- `List`, `Set` 등으로 값 타입을 여러 개 관리합니다(여기서는 생략하고 별도 장에서 다룰 수 있음).
|
||||
|
||||
#### 임베디드 타입의 정의와 활용
|
||||
임베디드 타입은 `@Embeddable`로 정의된 클래스를 엔티티 내에서 `@Embedded`로 사용합니다. 주로 주소, 기간, 좌표 같은 복합 데이터를 표현할 때 유용합니다.
|
||||
|
||||
#### 예시 1: 주소(Address) 임베디드 타입
|
||||
```java
|
||||
import jakarta.persistence.Embeddable;
|
||||
|
||||
@Embeddable
|
||||
public class Address {
|
||||
private String city;
|
||||
private String street;
|
||||
private String zipcode;
|
||||
|
||||
// JPA를 위한 기본 생성자
|
||||
public Address() {}
|
||||
|
||||
public Address(String city, String street, String zipcode) {
|
||||
this.city = city;
|
||||
this.street = street;
|
||||
this.zipcode = zipcode;
|
||||
}
|
||||
|
||||
// getter만 제공해 불변성 보장
|
||||
public String getCity() { return city; }
|
||||
public String getStreet() { return street; }
|
||||
public String getZipcode() { return zipcode; }
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Embedded;
|
||||
|
||||
@Entity
|
||||
public class Member {
|
||||
@Id
|
||||
private Long id;
|
||||
private String name;
|
||||
|
||||
@Embedded
|
||||
private Address address;
|
||||
|
||||
public Member() {}
|
||||
public Member(Long id, String name, Address address) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.address = address;
|
||||
}
|
||||
|
||||
// getter, setter
|
||||
public Long getId() { return id; }
|
||||
public String getName() { return name; }
|
||||
public Address getAddress() { return address; }
|
||||
public void setAddress(Address address) { this.address = address; }
|
||||
}
|
||||
```
|
||||
|
||||
- **테이블 구조**:
|
||||
```sql
|
||||
CREATE TABLE member (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name VARCHAR(255),
|
||||
city VARCHAR(255),
|
||||
street VARCHAR(255),
|
||||
zipcode VARCHAR(255)
|
||||
);
|
||||
```
|
||||
- **설명**: `Address`는 독립적인 엔티티가 아니라 `Member`에 속한 값 타입입니다. 데이터베이스에서는 `Member` 테이블에 포함되며, 별도 테이블이 생성되지 않습니다.
|
||||
|
||||
#### 예시 2: 기간(Period) 임베디드 타입
|
||||
```java
|
||||
import jakarta.persistence.Embeddable;
|
||||
import java.time.LocalDate;
|
||||
|
||||
@Embeddable
|
||||
public class Period {
|
||||
private LocalDate startDate;
|
||||
private LocalDate endDate;
|
||||
|
||||
public Period() {}
|
||||
public Period(LocalDate startDate, LocalDate endDate) {
|
||||
this.startDate = startDate;
|
||||
this.endDate = endDate;
|
||||
}
|
||||
|
||||
public LocalDate getStartDate() { return startDate; }
|
||||
public LocalDate getEndDate() { return endDate; }
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Embedded;
|
||||
|
||||
@Entity
|
||||
public class Project {
|
||||
@Id
|
||||
private Long id;
|
||||
private String title;
|
||||
|
||||
@Embedded
|
||||
private Period duration;
|
||||
|
||||
public Project() {}
|
||||
public Project(Long id, String title, Period duration) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.duration = duration;
|
||||
}
|
||||
|
||||
// getter, setter
|
||||
}
|
||||
```
|
||||
|
||||
- **테이블 구조**:
|
||||
```sql
|
||||
CREATE TABLE project (
|
||||
id BIGINT PRIMARY KEY,
|
||||
title VARCHAR(255),
|
||||
start_date DATE,
|
||||
end_date DATE
|
||||
);
|
||||
```
|
||||
|
||||
#### 임베디드 타입의 장점
|
||||
1. **재사용성**: `Address`나 `Period` 같은 타입을 여러 엔티티에서 재사용 가능.
|
||||
2. **가독성**: 관련 필드를 논리적으로 묶어 도메인 의미를 명확히 표현.
|
||||
3. **불변성**: setter를 제거하고 생성자로만 값을 설정하면 부작용 방지.
|
||||
4. **코드 간소화**: 공통 로직(예: 주소 유효성 검사)을 임베디드 타입에 추가 가능.
|
||||
|
||||
#### 활용 팁
|
||||
- **컬럼명 커스터마이징**: 동일한 임베디드 타입을 여러 번 사용할 경우 충돌을 피하기 위해 `@AttributeOverrides`를 사용합니다.
|
||||
```java
|
||||
@Entity
|
||||
public class Member {
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@Embedded
|
||||
private Address homeAddress;
|
||||
|
||||
@Embedded
|
||||
@AttributeOverrides({
|
||||
@AttributeOverride(name = "city", column = @Column(name = "work_city")),
|
||||
@AttributeOverride(name = "street", column = @Column(name = "work_street")),
|
||||
@AttributeOverride(name = "zipcode", column = @Column(name = "work_zipcode"))
|
||||
})
|
||||
private Address workAddress;
|
||||
|
||||
// 생성자, getter
|
||||
}
|
||||
```
|
||||
- `homeAddress`와 `workAddress`가 동일 테이블에서 다른 컬럼명으로 매핑됩니다.
|
||||
|
||||
- **null 처리**: 임베디드 타입이 `null`이면 모든 필드가 `null`로 저장됩니다. 이를 활용해 선택적 데이터를 표현할 수 있습니다.
|
||||
- **비즈니스 로직 추가**: 예를 들어, `Period`에 `isActive()` 메서드를 추가해 현재 날짜가 기간 내인지 확인 가능.
|
||||
|
||||
#### 주의사항
|
||||
- **생명주기 의존성**: 임베디드 타입은 엔티티에 종속적이므로 독립적으로 저장하거나 조회할 수 없습니다.
|
||||
- **불변성 권장**: 값 타입은 변경 시 기존 객체를 교체하는 방식으로 관리해야 부작용을 줄입니다(예: `setAddress(new Address(...))`).
|
||||
- **성능 고려**: 임베디드 타입이 지나치게 복잡하면 테이블 구조가 비대해질 수 있으니 적절히 분리합니다.
|
||||
|
||||
#### 실무 활용 예시
|
||||
주문 엔티티에서 배송 정보와 결제 정보를 임베디드 타입으로 분리:
|
||||
```java
|
||||
@Embeddable
|
||||
public class DeliveryInfo {
|
||||
private String receiver;
|
||||
private String address;
|
||||
|
||||
// 생성자, getter
|
||||
}
|
||||
|
||||
@Embeddable
|
||||
public class PaymentInfo {
|
||||
private String method;
|
||||
private int amount;
|
||||
|
||||
// 생성자, getter
|
||||
}
|
||||
|
||||
@Entity
|
||||
public class Order {
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@Embedded
|
||||
private DeliveryInfo delivery;
|
||||
|
||||
@Embedded
|
||||
private PaymentInfo payment;
|
||||
|
||||
// 생성자, getter
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
값 타입과 임베디드 타입은 JPA에서 도메인 모델을 풍부하게 만들고, 객체지향 설계를 데이터베이스에 효과적으로 반영하는 도구입니다. 이를 활용하면 코드의 응집도를 높이고 유지보수성을 강화할 수 있습니다. 다음 장에서는 성능 최적화와 관련된 주제를 다뤄보겠습니다.
|
||||
|
||||
---
|
||||
|
||||
책의 흐름에 맞는지, 추가 예시나 수정이 필요하면 말씀해 주세요!
|
||||
167
docs/jpa/11_JPA 성능 최적화.md
Normal file
167
docs/jpa/11_JPA 성능 최적화.md
Normal file
@@ -0,0 +1,167 @@
|
||||
아래는 "JPA 성능 최적화"에 대해 설명하는 글입니다. 실무에서 자주 발생하는 문제와 해결 방법을 중심으로, 예시와 함께 구체적으로 작성했습니다.
|
||||
|
||||
---
|
||||
|
||||
### JPA 성능 최적화
|
||||
|
||||
JPA는 객체와 관계형 데이터베이스를 매핑하며 개발 생산성을 높여주지만, 잘못 사용하면 성능 저하를 초래할 수 있습니다. 성능 최적화는 불필요한 데이터베이스 호출을 줄이고, 쿼리 실행을 효율화하며, 메모리 사용을 최적화하는 데 초점을 맞춥니다. 아래는 JPA 성능 최적화의 주요 기법과 실무 적용 방법을 다룹니다.
|
||||
|
||||
#### 1. N+1 문제와 해결 방법
|
||||
**N+1 문제**는 연관 관계를 조회할 때 발생하는 대표적인 성능 문제입니다. 예를 들어, `Team`과 `Member`의 1:N 관계에서 모든 팀을 조회한 뒤 각 팀의 회원을 개별적으로 조회하면, 초기 쿼리 1번(N개의 `Team` 조회) + N번(`Member` 조회)이 발생합니다.
|
||||
|
||||
- **해결 방법 1: 페치 조인(Fetch Join)**
|
||||
연관된 엔티티를 한 번의 쿼리로 함께 조회합니다.
|
||||
```java
|
||||
@Entity
|
||||
public class Team {
|
||||
@Id
|
||||
private Long id;
|
||||
private String name;
|
||||
|
||||
@OneToMany(mappedBy = "team")
|
||||
private List<Member> members = new ArrayList<>();
|
||||
// getter
|
||||
}
|
||||
|
||||
@Entity
|
||||
public class Member {
|
||||
@Id
|
||||
private Long id;
|
||||
private String name;
|
||||
|
||||
@ManyToOne
|
||||
@JoinColumn(name = "team_id")
|
||||
private Team team;
|
||||
// getter
|
||||
}
|
||||
```
|
||||
```java
|
||||
// JPQL로 페치 조인
|
||||
String jpql = "SELECT t FROM Team t JOIN FETCH t.members";
|
||||
List<Team> teams = entityManager.createQuery(jpql, Team.class).getResultList();
|
||||
```
|
||||
- 결과: 단일 쿼리로 `Team`과 `Member`를 함께 조회(`SELECT t.*, m.* FROM team t INNER JOIN member m ON t.id = m.team_id`).
|
||||
|
||||
- **해결 방법 2: 배치 크기 설정 (@BatchSize)**
|
||||
지연 로딩(Lazy Loading) 시 한 번에 여러 엔티티를 조회합니다.
|
||||
```java
|
||||
@OneToMany(mappedBy = "team")
|
||||
@BatchSize(size = 100)
|
||||
private List<Member> members;
|
||||
```
|
||||
- `size=100`으로 설정하면 100개의 `Team`에 대한 `Member`를 한 번의 IN 쿼리로 조회합니다.
|
||||
|
||||
#### 2. 지연 로딩(Lazy Loading) 활용
|
||||
기본적으로 연관 관계는 지연 로딩으로 설정해 불필요한 데이터 조회를 방지합니다.
|
||||
- `@ManyToOne`, `@OneToOne`: 기본값은 즉시 로딩(EAGER)이므로 `fetch = FetchType.LAZY`로 변경.
|
||||
- `@OneToMany`, `@ManyToMany`: 기본값이 지연 로딩(LAZY)이므로 유지.
|
||||
```java
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "team_id")
|
||||
private Team team;
|
||||
```
|
||||
- **주의**: 지연 로딩 사용 시 프록시 객체가 반환되며, 트랜잭션 밖에서 접근하면 `LazyInitializationException`이 발생할 수 있음.
|
||||
|
||||
#### 3. 페치 조인과 일반 조인의 적절한 사용
|
||||
- **페치 조인**: 연관 데이터를 즉시 로드할 때 사용. 데이터 중복이 발생할 수 있으니 페이징(`setFirstResult`, `setMaxResults`)과 함께 사용 금지.
|
||||
- **일반 조인**: 필터링이나 조건에만 사용하며, 엔티티 로드는 지연 로딩에 의존.
|
||||
|
||||
#### 4. 읽기 전용 쿼리 설정
|
||||
수정할 필요 없는 조회 쿼리는 영속성 컨텍스트의 관리 부담을 줄이기 위해 읽기 전용으로 설정합니다.
|
||||
- **JPQL**:
|
||||
```java
|
||||
entityManager.createQuery("SELECT m FROM Member m", Member.class)
|
||||
.setHint("org.hibernate.readOnly", true)
|
||||
.getResultList();
|
||||
```
|
||||
- **효과**: 변경 감지(Dirty Checking)와 스냅샷 생성이 생략되어 메모리와 성능 개선.
|
||||
|
||||
#### 5. 배치 처리와 대량 데이터 관리
|
||||
대량 데이터를 삽입/수정/삭제할 때는 JPA의 기본 동작(영속성 컨텍스트 사용)이 비효율적일 수 있습니다.
|
||||
- **방법**: JDBC 배치나 벌크 연산 사용.
|
||||
```java
|
||||
// 벌크 업데이트
|
||||
String jpql = "UPDATE Member m SET m.age = m.age + 1 WHERE m.team.id = :teamId";
|
||||
entityManager.createQuery(jpql)
|
||||
.setParameter("teamId", teamId)
|
||||
.executeUpdate();
|
||||
entityManager.clear(); // 영속성 컨텍스트 초기화 필수
|
||||
```
|
||||
- **주의**: 벌크 연산은 영속성 컨텍스트를 무시하므로, 이후 작업 전에 `clear()`나 `refresh()`로 동기화 필요.
|
||||
|
||||
#### 6. 캐시 활용
|
||||
JPA는 1차 캐시와 2차 캐시를 제공하며, 이를 활용해 성능을 높일 수 있습니다.
|
||||
- **1차 캐시**: 트랜잭션 내에서 동일 엔티티를 재조회할 때 데이터베이스 접근을 줄임(기본 제공).
|
||||
- **2차 캐시**: 애플리케이션 범위에서 캐시를 공유. Hibernate 사용 시 `@Cacheable`과 캐시 제공자(Ehcache, Redis 등)를 설정.
|
||||
```java
|
||||
@Entity
|
||||
@Cacheable
|
||||
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
|
||||
public class Product {
|
||||
@Id
|
||||
private Long id;
|
||||
private String name;
|
||||
// getter
|
||||
}
|
||||
```
|
||||
- **적용 시점**: 자주 조회되고 변경이 드문 데이터(예: 카테고리 목록).
|
||||
|
||||
#### 7. 쿼리 최적화
|
||||
- **필요한 필드만 조회**: 엔티티 전체 대신 DTO로 필요한 데이터만 조회.
|
||||
```java
|
||||
public class MemberDto {
|
||||
private Long id;
|
||||
private String name;
|
||||
public MemberDto(Long id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
// getter
|
||||
}
|
||||
|
||||
List<MemberDto> result = entityManager.createQuery(
|
||||
"SELECT new com.example.MemberDto(m.id, m.name) FROM Member m", MemberDto.class)
|
||||
.getResultList();
|
||||
```
|
||||
- **인덱스 활용**: 자주 검색되는 컬럼에 인덱스 추가(데이터베이스 레벨 또는 `@Index`).
|
||||
|
||||
#### 8. 트랜잭션 범위 최소화
|
||||
영속성 컨텍스트는 트랜잭션 범위에서 동작하므로, 불필요하게 긴 트랜잭션은 메모리 사용과 성능 저하를 유발합니다.
|
||||
- **방법**: 비즈니스 로직을 간결히 하고, 읽기 전용 작업은 트랜잭션 없이 처리(`@Transactional(readOnly = true)`).
|
||||
|
||||
#### 실무 예시
|
||||
```java
|
||||
@Repository
|
||||
public class MemberRepository {
|
||||
@PersistenceContext
|
||||
private EntityManager em;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<Member> findMembersWithTeam() {
|
||||
return em.createQuery("SELECT m FROM Member m JOIN FETCH m.team", Member.class)
|
||||
.setHint("org.hibernate.readOnly", true)
|
||||
.getResultList();
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void bulkUpdateAge(Long teamId) {
|
||||
em.createQuery("UPDATE Member m SET m.age = m.age + 1 WHERE m.team.id = :teamId")
|
||||
.setParameter("teamId", teamId)
|
||||
.executeUpdate();
|
||||
em.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 주의사항
|
||||
- **N+1 디버깅**: 쿼리 로그를 활성화해(`spring.jpa.show-sql=true`) 발생 여부 확인.
|
||||
- **캐시 동기화**: 2차 캐시 사용 시 데이터 정합성을 유지하기 위해 적절한 전략 선택.
|
||||
- **테스트**: 성능 개선 후 부작용(예: 데이터 불일치)을 검증.
|
||||
|
||||
---
|
||||
|
||||
JPA 성능 최적화는 애플리케이션 규모와 요구사항에 따라 달라지며, 문제 원인을 분석하고 적절한 기법을 적용하는 것이 중요합니다. 다음 장에서는 트랜잭션 관리와 실무 활용을 다뤄보겠습니다.
|
||||
|
||||
---
|
||||
|
||||
책의 흐름에 맞는지, 추가 내용이나 수정이 필요하면 말씀해 주세요!
|
||||
107
docs/jpa/12_jpa_auditing.md
Normal file
107
docs/jpa/12_jpa_auditing.md
Normal file
@@ -0,0 +1,107 @@
|
||||
JPA Auditing에 대해 설명하자면, 이는 Java Persistence API(JPA)를 사용해 데이터베이스 엔티티의 생성 및 수정과 같은 이벤트를 자동으로 추적하고 기록하는 기능입니다. 주로 애플리케이션에서 데이터의 변경 이력을 관리하거나, 누가 언제 데이터를 생성하거나 수정했는지와 같은 메타데이터를 유지할 때 유용합니다. Spring Data JPA에서는 이 기능을 간편하게 구현할 수 있도록 지원합니다.
|
||||
|
||||
### JPA Auditing의 핵심 개념
|
||||
JPA Auditing은 엔티티의 생명주기 이벤트(예: 생성, 수정)를 감지해 특정 필드에 값을 자동으로 채워주는 메커니즘을 제공합니다. 이를 통해 개발자가 수동으로 매번 값을 설정하지 않아도 되므로 코드가 간결해지고 실수를 줄일 수 있습니다. 대표적으로 사용되는 어노테이션은 다음과 같습니다:
|
||||
|
||||
- `@CreatedDate`: 엔티티가 처음 생성된 날짜와 시간을 기록합니다.
|
||||
- `@LastModifiedDate`: 엔티티가 마지막으로 수정된 날짜와 시간을 기록합니다。
|
||||
- `@CreatedBy`: 엔티티를 생성한 사용자를 기록합니다.
|
||||
- `@LastModifiedBy`: 엔티티를 마지막으로 수정한 사용자를 기록합니다.
|
||||
|
||||
이 어노테이션들은 Spring Data JPA에서 제공되며, Auditing 기능을 활성화해야 동작합니다.
|
||||
|
||||
### 설정 방법
|
||||
JPA Auditing을 사용하려면 몇 가지 설정이 필요합니다. Spring Boot 환경을 기준으로 설명하겠습니다.
|
||||
|
||||
1. **Auditing 활성화**
|
||||
Spring Boot 애플리케이션에서 `@EnableJpaAuditing` 어노테이션을 설정 클래스(예: `@Configuration`이 붙은 클래스)나 메인 애플리케이션 클래스에 추가합니다.
|
||||
```java
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
|
||||
@Configuration
|
||||
@EnableJpaAuditing
|
||||
public class JpaConfig {
|
||||
}
|
||||
```
|
||||
|
||||
2. **Auditable 엔티티 정의**
|
||||
엔티티 클래스에 Auditing 관련 필드와 어노테이션을 추가합니다. 보통 공통 속성을 추상 클래스로 만들어 재사용합니다.
|
||||
```java
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@MappedSuperclass
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public abstract class BaseEntity {
|
||||
|
||||
@CreatedDate
|
||||
private LocalDateTime createdDate;
|
||||
|
||||
@LastModifiedDate
|
||||
private LocalDateTime lastModifiedDate;
|
||||
|
||||
// Getter, Setter
|
||||
}
|
||||
```
|
||||
- `@MappedSuperclass`: 이 클래스를 상속받는 엔티티에 필드가 포함되도록 합니다.
|
||||
- `@EntityListeners(AuditingEntityListener.class)`: Auditing 기능을 수행하는 리스너를 지정합니다.
|
||||
|
||||
3. **실제 엔티티에서 상속**
|
||||
```java
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
|
||||
@Entity
|
||||
public class User extends BaseEntity {
|
||||
|
||||
@Id
|
||||
private Long id;
|
||||
private String name;
|
||||
|
||||
// Getter, Setter
|
||||
}
|
||||
```
|
||||
|
||||
4. **생성자/수정자 정보 추가 (선택)**
|
||||
`@CreatedBy`, `@LastModifiedBy`를 사용하려면 현재 사용자 정보를 제공하는 `AuditorAware` 인터페이스를 구현해야 합니다.
|
||||
```java
|
||||
import org.springframework.data.domain.AuditorAware;
|
||||
import java.util.Optional;
|
||||
|
||||
public class AuditorAwareImpl implements AuditorAware<String> {
|
||||
@Override
|
||||
public Optional<String> getCurrentAuditor() {
|
||||
// 현재 로그인한 사용자 정보를 반환 (예: Spring Security 사용 시)
|
||||
return Optional.of("admin");
|
||||
}
|
||||
}
|
||||
```
|
||||
그리고 이 빈을 등록합니다:
|
||||
```java
|
||||
@Bean
|
||||
public AuditorAware<String> auditorProvider() {
|
||||
return new AuditorAwareImpl();
|
||||
}
|
||||
```
|
||||
|
||||
### 동작 원리
|
||||
- 엔티티가 저장(`persist`)되거나 수정(`merge`)될 때, `AuditingEntityListener`가 동작합니다.
|
||||
- `@CreatedDate`와 `@CreatedBy`는 엔티티가 처음 저장될 때만 값이 설정되고, 이후에는 변경되지 않습니다.
|
||||
- `@LastModifiedDate`와 `@LastModifiedBy`는 엔티티가 수정될 때마다 갱신됩니다.
|
||||
|
||||
### 장점
|
||||
- **코드 간소화**: 반복적인 날짜/사용자 설정 코드를 제거합니다.
|
||||
- **일관성 유지**: 모든 엔티티에 동일한 방식으로 이력을 관리할 수 있습니다.
|
||||
- **유지보수 용이**: 공통 로직을 한 곳에서 관리하므로 변경이 쉬워집니다.
|
||||
|
||||
### 주의점
|
||||
- **시간대 설정**: `@CreatedDate`, `@LastModifiedDate`에 사용할 시간 타입(예: `LocalDateTime`)과 애플리케이션의 시간대 설정을 일치시켜야 합니다.
|
||||
- **AuditorAware 구현**: 사용자 정보를 동적으로 가져오려면 보안 컨텍스트(예: Spring Security)와 연동하는 것이 일반적입니다.
|
||||
|
||||
JPA Auditing은 특히 대규모 프로젝트에서 데이터 변경 추적을 체계적으로 관리할 때 빛을 발합니다. Spring Data JPA의 강력한 기능 중 하나로, 적절히 활용하면 개발 생산성을 크게 높일 수 있습니다!
|
||||
178
docs/jpa/13_paging.md
Normal file
178
docs/jpa/13_paging.md
Normal file
@@ -0,0 +1,178 @@
|
||||
JPA에서 페이지 처리는 대량의 데이터를 효율적으로 조회하고 사용자에게 필요한 만큼만 보여주기 위해 사용되는 기능입니다. 특히 웹 애플리케이션에서 목록 조회 시, 모든 데이터를 한 번에 가져오는 대신 페이징을 통해 데이터를 분할해서 처리하는 것이 일반적입니다. Spring Data JPA는 이를 간편하게 구현할 수 있도록 `PagingAndSortingRepository`와 `Pageable` 인터페이스를 제공합니다. 아래에서 JPA의 페이지 처리에 대해 자세히 설명하겠습니다.
|
||||
|
||||
---
|
||||
|
||||
### 페이지 처리란?
|
||||
페이지 처리는 데이터베이스에서 조회한 결과를 작은 단위(페이지)로 나누어 반환하는 기법입니다. 예를 들어, 게시판에 1,000개의 글이 있다면 한 페이지에 10개씩 보여주고, 사용자가 원하는 페이지를 선택해 해당 데이터만 조회합니다. 이를 통해 성능 최적화와 사용자 경험 개선을 동시에 달성할 수 있습니다.
|
||||
|
||||
---
|
||||
|
||||
### Spring Data JPA에서의 페이지 처리
|
||||
Spring Data JPA는 페이지 처리를 위해 다음과 같은 주요 구성 요소를 제공합니다:
|
||||
|
||||
1. **Pageable 인터페이스**
|
||||
- 페이지 번호, 페이지 크기, 정렬 조건 등을 정의합니다.
|
||||
- `PageRequest` 클래스를 통해 구현체를 생성합니다.
|
||||
- 예: `PageRequest.of(0, 10)`는 첫 번째 페이지(0부터 시작)에 10개의 데이터를 요청합니다.
|
||||
|
||||
2. **Page<T> 반환 타입**
|
||||
- 조회된 데이터와 함께 페이징 관련 메타데이터(총 페이지 수, 총 데이터 수 등)를 제공합니다.
|
||||
- 주요 메서드:
|
||||
- `getContent()`: 현재 페이지의 데이터 목록
|
||||
- `getTotalElements()`: 전체 데이터 개수
|
||||
- `getTotalPages()`: 전체 페이지 수
|
||||
- `getNumber()`: 현재 페이지 번호
|
||||
|
||||
3. **PagingAndSortingRepository**
|
||||
- Spring Data JPA에서 제공하는 기본 레포지토리 인터페이스로, 페이징과 정렬 기능을 지원합니다.
|
||||
- `findAll(Pageable pageable)` 메서드를 통해 페이징된 데이터를 조회할 수 있습니다.
|
||||
|
||||
---
|
||||
|
||||
### 페이지 처리 구현 방법
|
||||
Spring Boot와 Spring Data JPA를 기준으로 페이지 처리 구현 과정을 단계별로 설명하겠습니다.
|
||||
|
||||
#### 1. 엔티티와 레포지토리 정의
|
||||
```java
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
|
||||
@Entity
|
||||
public class Post {
|
||||
@Id
|
||||
private Long id;
|
||||
private String title;
|
||||
private String content;
|
||||
|
||||
// Getter, Setter
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
|
||||
public interface PostRepository extends JpaRepository<Post, Long> {
|
||||
Page<Post> findAll(Pageable pageable); // 기본 제공 메서드와 동일
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 서비스 계층에서 페이지 처리
|
||||
```java
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class PostService {
|
||||
|
||||
private final PostRepository postRepository;
|
||||
|
||||
public PostService(PostRepository postRepository) {
|
||||
this.postRepository = postRepository;
|
||||
}
|
||||
|
||||
public Page<Post> getPosts(int page, int size) {
|
||||
Pageable pageable = PageRequest.of(page, size); // 페이지 번호(0부터), 페이지 크기
|
||||
return postRepository.findAll(pageable);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. 컨트롤러에서 결과 반환
|
||||
```java
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.data.domain.Page;
|
||||
|
||||
@RestController
|
||||
public class PostController {
|
||||
|
||||
private final PostService postService;
|
||||
|
||||
public PostController(PostService postService) {
|
||||
this.postService = postService;
|
||||
}
|
||||
|
||||
@GetMapping("/posts")
|
||||
public Page<Post> getPosts(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size) {
|
||||
return postService.getPosts(page, size);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 4. 실행 결과
|
||||
- 요청: `GET /posts?page=0&size=10`
|
||||
- 응답: JSON 형태로 현재 페이지의 데이터와 페이징 정보가 반환됩니다.
|
||||
```json
|
||||
{
|
||||
"content": [
|
||||
{"id": 1, "title": "Post 1", "content": "Content 1"},
|
||||
{"id": 2, "title": "Post 2", "content": "Content 2"},
|
||||
...
|
||||
],
|
||||
"pageable": {
|
||||
"pageNumber": 0,
|
||||
"pageSize": 10,
|
||||
...
|
||||
},
|
||||
"totalElements": 100,
|
||||
"totalPages": 10,
|
||||
"number": 0
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 정렬 추가하기
|
||||
`Pageable`은 정렬(Sorting)도 지원합니다. 예를 들어, 제목 기준으로 오름차순 정렬을 추가하려면:
|
||||
```java
|
||||
Pageable pageable = PageRequest.of(0, 10, Sort.by("title").ascending());
|
||||
Page<Post> posts = postRepository.findAll(pageable);
|
||||
```
|
||||
|
||||
컨트롤러에서 요청 파라미터로 정렬 조건을 받을 수도 있습니다:
|
||||
```java
|
||||
@GetMapping("/posts")
|
||||
public Page<Post> getPosts(
|
||||
@RequestParam(defaultValue = "0") int page,
|
||||
@RequestParam(defaultValue = "10") int size,
|
||||
@RequestParam(defaultValue = "title") String sortBy) {
|
||||
Pageable pageable = PageRequest.of(page, size, Sort.by(sortBy).ascending());
|
||||
return postService.getPosts(pageable);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 커스텀 쿼리와 페이지 처리
|
||||
특정 조건으로 데이터를 조회하면서 페이징을 적용하려면 `@Query` 어노테이션이나 메서드 이름을 사용합니다.
|
||||
```java
|
||||
public interface PostRepository extends JpaRepository<Post, Long> {
|
||||
Page<Post> findByTitleContaining(String keyword, Pageable pageable);
|
||||
|
||||
@Query("SELECT p FROM Post p WHERE p.content LIKE %:keyword%")
|
||||
Page<Post> searchByContent(@Param("keyword") String keyword, Pageable pageable);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 장점
|
||||
- **성능 최적화**: 필요한 데이터만 조회하므로 메모리와 DB 부하를 줄입니다.
|
||||
- **사용자 경험 개선**: 대량 데이터를 한 번에 로드하지 않고, 페이지 단위로 제공합니다.
|
||||
- **유연성**: 정렬, 필터링 등과 결합해 다양한 요구사항을 처리할 수 있습니다.
|
||||
|
||||
### 주의점
|
||||
- **페이지 번호**: Spring Data JPA는 페이지 번호가 0부터 시작합니다(예: 0은 첫 페이지).
|
||||
- **쿼리 최적화**: 페이징 쿼리는 `OFFSET`과 `LIMIT`을 사용하므로, 데이터가 많아질수록 성능 저하가 발생할 수 있습니다. 이 경우 인덱스 사용이나 커서 기반 페이징을 고려해야 합니다.
|
||||
- **총 개수 계산**: `Page` 객체는 `totalElements`를 계산하기 위해 별도의 `COUNT` 쿼리를 실행합니다. 성능에 민감한 경우 `Slice`를 사용하는 것도 대안입니다(`totalElements`와 `totalPages`를 제공하지 않음).
|
||||
|
||||
---
|
||||
|
||||
### 결론
|
||||
JPA의 페이지 처리는 Spring Data JPA를 통해 매우 간단하면서도 강력하게 구현할 수 있습니다. `Pageable`과 `Page`를 활용하면 데이터 조회를 효율적으로 관리하고, 웹 애플리케이션에서 필수적인 페이징 기능을 손쉽게 제공할 수 있습니다. 대규모 데이터를 다룰 때 필수적인 이 기능을 잘 활용하면 성능과 사용자 경험 모두를 만족시킬 수 있습니다!
|
||||
72
docs/jpa/README.md
Normal file
72
docs/jpa/README.md
Normal file
@@ -0,0 +1,72 @@
|
||||
스프링부트의 JPA에 관한 책을 위한 목차를 제안해 드리겠습니다. 이 목차는 초보자부터 중급 개발자까지 이해할 수 있도록 기본 개념부터 실무 활용까지 다루며, 체계적인 학습 흐름을 고려했습니다.
|
||||
|
||||
---
|
||||
|
||||
### 목차: 스프링부트 JPA 완벽 가이드
|
||||
|
||||
#### 1장. JPA와 스프링부트 소개
|
||||
- 1.1 ORM과 JPA란 무엇인가?
|
||||
- 1.2 스프링부트와 JPA의 만남
|
||||
- 1.3 이 책의 목표와 구성
|
||||
|
||||
#### 2장. 개발 환경 설정
|
||||
- 2.1 스프링부트 프로젝트 생성
|
||||
- 2.2 JPA 의존성 추가
|
||||
- 2.3 데이터베이스 설정 (H2, MySQL 등)
|
||||
- 2.4 기본 애플리케이션 설정과 테스트
|
||||
|
||||
#### 3장. JPA 기초
|
||||
- 3.1 엔티티와 테이블 매핑
|
||||
- 3.2 기본 키 매핑 전략
|
||||
- 3.3 관계 매핑의 이해 (1:1, 1:N, N:M)
|
||||
- 3.4 JPA의 생명주기와 영속성 컨텍스트
|
||||
|
||||
#### 4장. 스프링 데이터 JPA
|
||||
- 4.1 스프링 데이터 JPA란?
|
||||
- 4.2 Repository 인터페이스 활용
|
||||
- 4.3 쿼리 메서드 작성법
|
||||
- 4.4 @Query 어노테이션과 커스텀 쿼리
|
||||
|
||||
#### 5장. 엔티티 설계와 매핑
|
||||
- 5.1 엔티티 설계 시 고려사항
|
||||
- 5.2 연관관계 매핑 심화
|
||||
- 5.3 상속 관계 매핑 (SINGLE_TABLE, JOINED 등)
|
||||
- 5.4 값 타입과 임베디드 타입 활용
|
||||
|
||||
#### 6장. JPA 성능 최적화
|
||||
- 6.1 N+1 문제와 해결 방법
|
||||
- 6.2 페치 조인(Fetch Join)과 지연 로딩(Lazy Loading)
|
||||
- 6.3 배치 처리와 대량 데이터 관리
|
||||
- 6.4 캐시 활용 (1차 캐시, 2차 캐시)
|
||||
|
||||
#### 7장. 트랜잭션 관리
|
||||
- 7.1 트랜잭션의 기본 개념
|
||||
- 7.2 스프링부트에서 트랜잭션 설정
|
||||
- 7.3 @Transactional 어노테이션 활용
|
||||
- 7.4 트랜잭션 롤백과 예외 처리
|
||||
|
||||
#### 8장. 실무에서의 JPA
|
||||
- 8.1 스프링부트와 JPA로 REST API 구축
|
||||
- 8.2 DTO와 엔티티 분리 전략
|
||||
- 8.3 데이터베이스 마이그레이션 (Flyway, Liquibase)
|
||||
- 8.4 테스트 코드 작성 (단위 테스트, 통합 테스트)
|
||||
|
||||
#### 9장. 고급 주제
|
||||
- 9.1 JPA와 QueryDSL 통합
|
||||
- 9.2 멀티 데이터소스 설정
|
||||
- 9.3 스프링부트와 JPA의 이벤트 처리
|
||||
- 9.4 JPA Auditing으로 엔티티 관리
|
||||
|
||||
#### 10장. 문제 해결과 모범 사례
|
||||
- 10.1 흔한 JPA 실수와 디버깅
|
||||
- 10.2 실무에서 유용한 설계 패턴
|
||||
- 10.3 성능 모니터링과 튜닝 팁
|
||||
|
||||
#### 부록
|
||||
- 부록 A. JPA 관련 주요 어노테이션 정리
|
||||
- 부록 B. 샘플 프로젝트 코드
|
||||
- 부록 C. 추가 학습 자료 및 참고 문헌
|
||||
|
||||
---
|
||||
|
||||
이 목차는 JPA의 기초부터 실무 적용까지 자연스럽게 이어지도록 설계되었습니다. 필요에 따라 특정 장을 더 세분화하거나, 독자 타겟(초급/중급)에 맞춰 내용을 조정할 수 있습니다. 추가로 강조하고 싶은 주제나 방향성이 있다면 말씀해 주세요!
|
||||
149
docs/logging.md
Normal file
149
docs/logging.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# 로깅(logging)
|
||||
|
||||
Spring Boot는 기본적으로 **SLF4J**(Simple Logging Facade for Java)와 **Logback**을 로깅 프레임워크로 사용합니다. 별도 설정 없이도 동작하지만, 실무에서는 로그 레벨, 출력 형식, 파일 저장 등을 커스터마이징해야 하죠.
|
||||
|
||||
Spring Boot Starter에 SLF4J와 Logback이 포함되어 있습니다.
|
||||
|
||||
```kotlin
|
||||
// build.gradle.kts
|
||||
dependencies {
|
||||
implementation("org.springframework.boot:spring-boot-starter")
|
||||
}
|
||||
```
|
||||
|
||||
## `application.yaml`
|
||||
|
||||
```yaml
|
||||
# 공통 로깅 설정
|
||||
logging:
|
||||
level:
|
||||
root: INFO # 기본 로깅 레벨
|
||||
com.example.myapp: DEBUG # 특정 패키지의 로깅 레벨
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" # 콘솔 출력 형식
|
||||
file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" # 파일 출력 형식
|
||||
|
||||
# 개발 환경
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: dev
|
||||
logging:
|
||||
level:
|
||||
root: DEBUG # 개발 환경에서 더 상세한 로그
|
||||
file:
|
||||
name: logs/myapp-dev.log # 로그 파일 경로 및 이름
|
||||
|
||||
# 프로덕션 환경
|
||||
---
|
||||
spring:
|
||||
config:
|
||||
activate:
|
||||
on-profile: prod
|
||||
logging:
|
||||
level:
|
||||
root: WARN # 프로덕션에서는 경고 이상만 출력
|
||||
file:
|
||||
name: /var/log/myapp.log # 프로덕션 로그 파일 경로
|
||||
logback:
|
||||
rollingpolicy:
|
||||
max-file-size: 10MB # 파일 크기 제한
|
||||
max-history: 30 # 최대 보관 기간 (일)
|
||||
file-name-pattern: myapp-%d{yyyy-MM-dd}.%i.log # 롤링 파일 이름 패턴
|
||||
```
|
||||
|
||||
### 로깅 설정 옵션
|
||||
|
||||
| **옵션** | **설명** | **예시 값** |
|
||||
|----------------------------------|-------------------------------------------------------------------------------------------|-------------------------------------|
|
||||
| `logging.level.root` | 루트 로거의 기본 로깅 레벨 설정 (`TRACE`, `DEBUG`, `INFO`, `WARN`, `ERROR`, `OFF`) | `INFO` |
|
||||
| `logging.level.{패키지명}` | 특정 패키지나 클래스에 대한 로깅 레벨 설정 | `com.example.myapp: DEBUG` |
|
||||
| `logging.pattern.console` | 콘솔에 출력되는 로그 형식 정의 | `%d{yyyy-MM-dd HH:mm:ss} %-5level %msg%n` |
|
||||
| `logging.pattern.file` | 파일에 기록되는 로그 형식 정의 | `%d{yyyy-MM-dd HH:mm:ss} %-5level %msg%n` |
|
||||
| `logging.file.name` | 로그 파일의 경로와 이름 설정 (절대 경로 또는 상대 경로) | `logs/myapp.log` |
|
||||
| `logging.file.path` | 로그 파일이 저장될 디렉토리 설정 (파일 이름은 기본값 사용) | `/var/log` |
|
||||
| `logging.logback.rollingpolicy.max-file-size` | 로그 파일의 최대 크기 (크기 초과 시 롤링) | `10MB` |
|
||||
| `logging.logback.rollingpolicy.max-history` | 롤링된 로그 파일의 최대 보관 기간 (일 단위) | `30` |
|
||||
| `logging.logback.rollingpolicy.file-name-pattern` | 롤링된 로그 파일의 이름 패턴 정의 | `myapp-%d{yyyy-MM-dd}.%i.log` |
|
||||
| `logging.exception-conversion-word` | 예외 스택 트레이스를 변환하는 패턴 설정 | `%ex` |
|
||||
| `logging.charset.console` | 콘솔 로그의 문자 인코딩 설정 | `UTF-8` |
|
||||
| `logging.charset.file` | 파일 로그의 문자 인코딩 설정 | `UTF-8` |
|
||||
|
||||
|
||||
1. **`logging.level`**
|
||||
- **설명**: 로깅 레벨은 로그의 상세도를 결정합니다. `DEBUG`는 개발 중 디버깅에 유용하고, `WARN`이나 `ERROR`는 프로덕션에서 주로 사용됩니다.
|
||||
- **예시**: `com.example.myapp: DEBUG`로 설정하면 해당 패키지의 로그만 상세히 출력됩니다.
|
||||
|
||||
2. **`logging.pattern`**
|
||||
- **설명**: 로그 메시지의 형식을 정의합니다. `%d` (날짜), `%-5level` (레벨), `%msg` (메시지), `%n` (줄바꿈) 등 패턴을 조합할 수 있습니다.
|
||||
- **팁**: 콘솔과 파일 형식을 다르게 설정해 환경별로 최적화 가능.
|
||||
|
||||
3. **`logging.file`**
|
||||
- **설명**: `name`은 파일 경로를 직접 지정하며, `path`는 디렉토리만 지정할 때 사용됩니다. 두 옵션을 함께 사용할 경우 `name`이 우선합니다.
|
||||
- **예시**: `logs/myapp-dev.log`는 프로젝트 루트 기준 상대 경로로 파일 생성.
|
||||
|
||||
4. **`logging.logback.rollingpolicy`**
|
||||
- **설명**: 로그 파일이 커질 경우 롤링(새 파일 생성)을 설정합니다. `max-file-size`로 크기 제한, `max-history`로 보관 기간을 지정합니다.
|
||||
- **예시**: `file-name-pattern`에서 `%i`는 파일 인덱스를 의미하며, `myapp-2025-03-16.0.log` 같은 이름으로 생성됩니다.
|
||||
|
||||
|
||||
|
||||
## 커스텀 Logback 설정
|
||||
복잡한 설정은 `src/main/resources/logback-spring.xml`로 관리합니다.
|
||||
|
||||
```xml
|
||||
<configuration>
|
||||
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||
<file>logs/my-app.log</file>
|
||||
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
|
||||
<fileNamePattern>logs/my-app-%d{yyyy-MM-dd}.log</fileNamePattern>
|
||||
<maxHistory>30</maxHistory>
|
||||
</rollingPolicy>
|
||||
<encoder>
|
||||
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<root level="INFO">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
<appender-ref ref="FILE" />
|
||||
</root>
|
||||
|
||||
<logger name="com.example" level="DEBUG" additivity="false">
|
||||
<appender-ref ref="CONSOLE" />
|
||||
<appender-ref ref="FILE" />
|
||||
</logger>
|
||||
</configuration>
|
||||
```
|
||||
|
||||
- **ConsoleAppender**: 콘솔에 로그 출력.
|
||||
- **RollingFileAppender**: 날짜별로 로그 파일 저장 (30일 보관).
|
||||
- **logger**: 특정 패키지에 다른 레벨 적용.
|
||||
|
||||
## 코드에서 SLF4J 사용
|
||||
Lombok의 `@Slf4j`로 쉽게 로깅을 추가할 수 있습니다.
|
||||
|
||||
```java
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class ExampleController {
|
||||
@GetMapping("/test")
|
||||
public String test() {
|
||||
log.debug("디버그 로그 테스트");
|
||||
log.info("정보 로그 테스트");
|
||||
log.error("에러 로그 테스트");
|
||||
return "Log Test";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 설정 우선순위
|
||||
환경 변수 > `application.yaml` > 기본값 순으로 적용.
|
||||
65
docs/mvc/README.md
Normal file
65
docs/mvc/README.md
Normal file
@@ -0,0 +1,65 @@
|
||||
스프링부트 웹 MVC에 대한 연속적인 글을 위한 목차를 아래에 제안해 드립니다. 이 목차는 초보자부터 중급 개발자까지 이해할 수 있도록 기초부터 실전까지 다루며, 논리적인 흐름을 유지했습니다.
|
||||
|
||||
### 스프링부트 웹 MVC 시리즈 목차
|
||||
|
||||
1. **스프링부트와 웹 MVC 소개**
|
||||
- 스프링부트란?
|
||||
- MVC 패턴의 기본 개념 (Model, View, Controller)
|
||||
- 스프링부트에서 웹 MVC가 중요한 이유
|
||||
|
||||
2. **개발 환경 설정**
|
||||
- JDK와 IDE 설치 (예: IntelliJ IDEA)
|
||||
- 스프링부트 프로젝트 생성 (Spring Initializr 사용)
|
||||
- 기본 디렉토리 구조 이해
|
||||
|
||||
3. **첫 번째 컨트롤러 만들기**
|
||||
- `@Controller`와 `@RequestMapping` 이해
|
||||
- 간단한 "Hello, World" 웹 페이지 구현
|
||||
- HTTP 요청과 응답 기본 개념
|
||||
|
||||
4. **모델과 뷰 연결**
|
||||
- `@ModelAttribute`와 데이터 전달
|
||||
- Thymeleaf 템플릿 엔진 소개 및 설정
|
||||
- 정적 리소스 (CSS, JS) 추가하기
|
||||
|
||||
5. **HTTP 요청 처리 심화**
|
||||
- GET, POST 등 HTTP 메서드별 처리 (@GetMapping, @PostMapping)
|
||||
- 경로 변수 (@PathVariable)와 쿼리 파라미터 (@RequestParam)
|
||||
- 요청 데이터 유효성 검사 (Validation)
|
||||
|
||||
6. **데이터베이스 연동**
|
||||
- Spring Data JPA 설정
|
||||
- 엔티티와 리포지토리 생성
|
||||
- 컨트롤러에서 CRUD 기능 구현
|
||||
|
||||
7. **RESTful API 설계**
|
||||
- REST API란?
|
||||
- `@RestController`와 JSON 응답
|
||||
- Postman으로 API 테스트하기
|
||||
|
||||
8. **폼 처리와 사용자 입력**
|
||||
- HTML 폼과 Thymeleaf 연동
|
||||
- 사용자 입력 데이터 처리와 오류 메시지 표시
|
||||
- 세션 관리와 로그인 기능 구현
|
||||
|
||||
9. **스프링 시큐리티 기본**
|
||||
- 스프링 시큐리티 설정
|
||||
- 기본 인증과 권한 부여
|
||||
- 로그인/로그아웃 페이지 커스터마이징
|
||||
|
||||
10. **프로젝트 배포 준비**
|
||||
- 빌드와 패키징 (Maven/Gradle)
|
||||
- 외부 설정 파일 사용 (application.properties/yaml)
|
||||
- JAR 파일 실행 및 서버 배포
|
||||
|
||||
11. **실전 팁과 최적화**
|
||||
- 예외 처리 (@ExceptionHandler)
|
||||
- 로깅 설정과 디버깅
|
||||
- 성능 개선을 위한 캐싱 적용
|
||||
|
||||
12. **시리즈 마무리**
|
||||
- 배운 내용 요약
|
||||
- 추가 학습 리소스 추천
|
||||
- 독자 피드백과 Q&A
|
||||
|
||||
이 목차는 스프링부트 웹 MVC를 처음 접하는 독자부터 실무에 적용하려는 개발자까지 유용하게 활용할 수 있도록 설계되었습니다. 필요에 따라 세부 주제를 추가하거나 조정할 수 있습니다. 어떤 방향으로 더 발전시키고 싶은지 알려주시면 구체적으로 도와드리겠습니다!
|
||||
165
docs/pageable.md
Normal file
165
docs/pageable.md
Normal file
@@ -0,0 +1,165 @@
|
||||
Spring Boot의 컨트롤러에서 페이지 요청 파라미터를 받는 방법은 주로 `@RequestParam` 어노테이션을 사용하거나, `Pageable` 객체를 직접 파라미터로 받는 방식으로 구현할 수 있습니다. 두 가지 방법을 아래에서 자세히 설명하겠습니다.
|
||||
|
||||
---
|
||||
|
||||
### 1. `@RequestParam`을 사용해 개별 파라미터로 받기
|
||||
이 방식은 페이지 번호(`page`), 페이지 크기(`size`), 정렬 조건(`sort`) 등을 개별적으로 `@RequestParam`으로 받아서 처리하는 방법입니다. 클라이언트가 URL 쿼리 파라미터로 값을 전달하면 이를 컨트롤러에서 수동으로 `PageRequest` 객체로 변환합니다.
|
||||
|
||||
#### 예제 코드
|
||||
```java
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class PostController {
|
||||
|
||||
private final PostService postService;
|
||||
|
||||
public PostController(PostService postService) {
|
||||
this.postService = postService;
|
||||
}
|
||||
|
||||
@GetMapping("/posts")
|
||||
public Page<Post> getPosts(
|
||||
@RequestParam(defaultValue = "0") int page, // 페이지 번호 (0부터 시작)
|
||||
@RequestParam(defaultValue = "10") int size, // 페이지 크기
|
||||
@RequestParam(defaultValue = "id") String sortBy, // 정렬 기준 필드
|
||||
@RequestParam(defaultValue = "asc") String sortDir) // 정렬 방향 (asc/desc)
|
||||
{
|
||||
// 정렬 방향 설정
|
||||
Sort.Direction direction = sortDir.equalsIgnoreCase("desc") ? Sort.Direction.DESC : Sort.Direction.ASC;
|
||||
Sort sort = Sort.by(direction, sortBy);
|
||||
|
||||
// Pageable 객체 생성
|
||||
Pageable pageable = PageRequest.of(page, size, sort);
|
||||
|
||||
// 서비스 호출
|
||||
return postService.getPosts(pageable);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 요청 예시
|
||||
- `GET /posts?page=1&size=5&sortBy=title&sortDir=desc`
|
||||
- 2번째 페이지(인덱스 1), 5개 데이터, `title` 필드를 기준으로 내림차순 정렬
|
||||
- `GET /posts`
|
||||
- 기본값 적용: 첫 페이지(0), 10개 데이터, `id` 기준 오름차순 정렬
|
||||
|
||||
#### 특징
|
||||
- **장점**: 파라미터를 개별적으로 제어할 수 있어 유연성이 높습니다. 기본값 설정이나 추가적인 검증 로직을 쉽게 추가할 수 있습니다.
|
||||
- **단점**: 코드가 다소 길어질 수 있고, `Pageable` 객체를 직접 생성해야 합니다.
|
||||
|
||||
---
|
||||
|
||||
### 2. `Pageable` 객체를 직접 파라미터로 받기
|
||||
Spring은 `Pageable` 타입을 컨트롤러 메서드의 파라미터로 직접 받을 수 있도록 지원합니다. Spring이 쿼리 파라미터(`page`, `size`, `sort`)를 자동으로 `Pageable` 객체로 변환해줍니다. 이 방식은 코드가 간결하고 Spring Data JPA와의 통합성이 뛰어납니다.
|
||||
|
||||
#### 예제 코드
|
||||
```java
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class PostController {
|
||||
|
||||
private final PostService postService;
|
||||
|
||||
public PostController(PostService postService) {
|
||||
this.postService = postService;
|
||||
}
|
||||
|
||||
@GetMapping("/posts")
|
||||
public Page<Post> getPosts(Pageable pageable) {
|
||||
return postService.getPosts(pageable);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 요청 예시
|
||||
- `GET /posts?page=0&size=10`
|
||||
- 첫 페이지(0), 10개 데이터
|
||||
- `GET /posts?page=1&size=5&sort=title,desc`
|
||||
- 2번째 페이지(1), 5개 데이터, `title` 기준 내림차순 정렬
|
||||
- `GET /posts`
|
||||
- 기본값은 애플리케이션 설정에 따라 다를 수 있음(기본적으로 `page=0`, `size=20`)
|
||||
|
||||
#### 기본값 설정
|
||||
`Pageable`의 기본값을 커스터마이징하려면 `@PageableDefault` 어노테이션을 사용합니다:
|
||||
```java
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.web.PageableDefault;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class PostController {
|
||||
|
||||
@GetMapping("/posts")
|
||||
public Page<Post> getPosts(
|
||||
@PageableDefault(page = 0, size = 10, sort = "id", direction = Sort.Direction.ASC) Pageable pageable) {
|
||||
return postService.getPosts(pageable);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 요청 예시 (기본값 적용)
|
||||
- `GET /posts`
|
||||
- 첫 페이지(0), 10개 데이터, `id` 기준 오름차순 정렬
|
||||
|
||||
#### `sort` 파라미터 형식
|
||||
- `sort=fieldName,direction` 형식으로 전달합니다.
|
||||
- 예: `sort=title,desc` (title 기준 내림차순)
|
||||
- 여러 필드 정렬: `sort=title,desc&sort=id,asc`
|
||||
|
||||
---
|
||||
|
||||
### 두 방식의 비교
|
||||
| 방식 | 장점 | 단점 |
|
||||
|-----------------------|-----------------------------------|-------------------------------|
|
||||
| `@RequestParam` | 세밀한 제어 가능, 기본값 명시적 설정 가능 | 코드가 길어질 수 있음 |
|
||||
| `Pageable` 직접 사용 | 간결한 코드, Spring과의 높은 통합성 | 기본값 설정이 별도로 필요할 수 있음 |
|
||||
|
||||
---
|
||||
|
||||
### 추가 팁
|
||||
1. **전역 기본값 설정**
|
||||
`application.properties`나 `application.yml`에서 `Pageable`의 기본값을 설정할 수 있습니다:
|
||||
```properties
|
||||
spring.data.web.pageable.default-page-size=10
|
||||
spring.data.web.pageable.max-page-size=100
|
||||
```
|
||||
|
||||
2. **검증 추가**
|
||||
페이지 번호나 크기가 음수로 들어오지 않도록 컨트롤러에서 검증 로직을 추가할 수 있습니다:
|
||||
```java
|
||||
if (page < 0 || size <= 0) {
|
||||
throw new IllegalArgumentException("페이지 번호는 0 이상, 크기는 1 이상이어야 합니다.");
|
||||
}
|
||||
```
|
||||
|
||||
3. **응답 커스터마이징**
|
||||
`Page<Post>`를 그대로 반환하면 JSON에 모든 메타데이터가 포함됩니다. 필요한 정보만 반환하려면 DTO로 변환해 응답을 구성할 수 있습니다:
|
||||
```java
|
||||
public class PostResponse {
|
||||
private List<Post> content;
|
||||
private int totalPages;
|
||||
private long totalElements;
|
||||
|
||||
public PostResponse(Page<Post> page) {
|
||||
this.content = page.getContent();
|
||||
this.totalPages = page.getTotalPages();
|
||||
this.totalElements = page.getTotalElements();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 결론
|
||||
컨트롤러에서 페이지 요청 파라미터를 받는 방법은 `@RequestParam`으로 개별 파라미터를 처리하거나, `Pageable`을 직접 사용하는 두 가지가 주로 사용됩니다. 간단한 경우에는 `Pageable`을 직접 사용하는 방식이 편리하고, 세밀한 제어가 필요할 때는 `@RequestParam`을 활용하는 것이 좋습니다. 프로젝트의 요구사항에 맞게 선택하면 됩니다!
|
||||
183
docs/path.md
Normal file
183
docs/path.md
Normal file
@@ -0,0 +1,183 @@
|
||||
스프링 부트에서 컨트롤러의 경로를 지정하는 방법과 전체 요청 경로를 얻는 방법을 설명하겠습니다. 스프링 부트는 주로 `@RequestMapping`과 그 파생 어노테이션을 사용해 경로를 정의하며, 요청 경로를 얻기 위해 `HttpServletRequest` 등의 객체를 활용할 수 있습니다.
|
||||
|
||||
---
|
||||
|
||||
### 1. 컨트롤러에서 경로 지정 방법
|
||||
|
||||
스프링 부트의 컨트롤러에서 경로는 클래스 수준과 메서드 수준에서 정의할 수 있으며, `@RequestMapping` 또는 HTTP 메서드별 전용 어노테이션(`@GetMapping`, `@PostMapping` 등)을 사용합니다.
|
||||
|
||||
#### (1) 클래스 수준 경로 지정
|
||||
컨트롤러 클래스에 `@RequestMapping`을 사용하면 해당 클래스의 모든 메서드에 공통 경로가 적용됩니다.
|
||||
|
||||
- **예시**:
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UserController {
|
||||
@GetMapping
|
||||
public String getUsers() {
|
||||
return "All users";
|
||||
}
|
||||
|
||||
@GetMapping("/active")
|
||||
public String getActiveUsers() {
|
||||
return "Active users";
|
||||
}
|
||||
}
|
||||
```
|
||||
- `/api/users`: 모든 사용자 조회.
|
||||
- `/api/users/active`: 활성 사용자 조회.
|
||||
|
||||
#### (2) 메서드 수준 경로 지정
|
||||
메서드에 경로를 추가로 지정하면 클래스 경로와 결합됩니다.
|
||||
|
||||
- **예시**:
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class ProductController {
|
||||
@GetMapping("/products")
|
||||
public String getProducts() {
|
||||
return "Product list";
|
||||
}
|
||||
|
||||
@PostMapping("/products")
|
||||
public String createProduct() {
|
||||
return "Product created";
|
||||
}
|
||||
}
|
||||
```
|
||||
- `/api/products` (GET): 제품 목록 조회.
|
||||
- `/api/products` (POST): 제품 생성.
|
||||
|
||||
#### (3) 경로 변수 사용 (@PathVariable)
|
||||
URL에 동적인 값을 포함하려면 `{변수명}`을 정의하고 `@PathVariable`로 값을 가져옵니다.
|
||||
|
||||
- **예시**:
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UserController {
|
||||
@GetMapping("/{id}")
|
||||
public String getUserById(@PathVariable Long id) {
|
||||
return "User ID: " + id;
|
||||
}
|
||||
}
|
||||
```
|
||||
- `/api/users/123`: ID가 123인 사용자 조회.
|
||||
|
||||
#### (4) 쿼리 파라미터 사용 (@RequestParam)
|
||||
쿼리 문자열(`?key=value`)을 처리하려면 `@RequestParam`을 사용합니다.
|
||||
|
||||
- **예시**:
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/search")
|
||||
public class SearchController {
|
||||
@GetMapping
|
||||
public String search(@RequestParam String query) {
|
||||
return "Search result for: " + query;
|
||||
}
|
||||
}
|
||||
```
|
||||
- `/api/search?query=book`: "book"에 대한 검색 결과.
|
||||
|
||||
#### (5) 복잡한 경로 패턴
|
||||
정규식이나 와일드카드(`*`, `**`)를 사용해 유연한 경로를 정의할 수 있습니다.
|
||||
|
||||
- **예시**:
|
||||
```java
|
||||
@RestController
|
||||
public class FileController {
|
||||
@GetMapping("/files/{filename:.+}")
|
||||
public String getFile(@PathVariable String filename) {
|
||||
return "File: " + filename;
|
||||
}
|
||||
}
|
||||
```
|
||||
- `/files/report.pdf`: 파일 이름에 확장자 포함 가능.
|
||||
|
||||
#### (6) HTTP 메서드별 어노테이션
|
||||
`@RequestMapping` 대신 특정 HTTP 메서드에 맞는 어노테이션을 사용할 수 있습니다:
|
||||
- `@GetMapping`: GET 요청.
|
||||
- `@PostMapping`: POST 요청.
|
||||
- `@PutMapping`: PUT 요청.
|
||||
- `@DeleteMapping`: DELETE 요청.
|
||||
- `@PatchMapping`: PATCH 요청.
|
||||
|
||||
---
|
||||
|
||||
### 2. 전체 요청 경로 얻는 방법
|
||||
|
||||
컨트롤러에서 현재 요청의 전체 경로를 얻으려면 `HttpServletRequest` 객체를 주입받아 사용하거나, 스프링의 유틸리티 메서드를 활용할 수 있습니다.
|
||||
|
||||
#### (1) HttpServletRequest 사용
|
||||
`HttpServletRequest`를 메서드 파라미터로 추가하면 요청 정보를 얻을 수 있습니다.
|
||||
|
||||
- **예시**:
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class PathController {
|
||||
@GetMapping("/path")
|
||||
public String getFullPath(HttpServletRequest request) {
|
||||
String fullPath = request.getRequestURI(); // 경로만 (/api/path)
|
||||
String query = request.getQueryString(); // 쿼리 문자열 (예: name=john)
|
||||
String fullUrl = request.getRequestURL().toString(); // 전체 URL (http://localhost:8080/api/path)
|
||||
return "Full Path: " + fullPath + ", Query: " + (query != null ? query : "none") + ", Full URL: " + fullUrl;
|
||||
}
|
||||
}
|
||||
```
|
||||
- 요청: `http://localhost:8080/api/path?name=john`
|
||||
- 응답: `"Full Path: /api/path, Query: name=john, Full URL: http://localhost:8080/api/path"`
|
||||
|
||||
- **주요 메서드**:
|
||||
- `getRequestURI()`: 컨텍스트 루트 이후의 경로 반환.
|
||||
- `getQueryString()`: 쿼리 문자열 반환.
|
||||
- `getRequestURL()`: 전체 URL 반환 (쿼리 제외).
|
||||
|
||||
#### (2) RequestContextHolder 사용
|
||||
`HttpServletRequest`를 직접 주입받지 않고, 정적 메서드로 요청 정보를 얻을 수 있습니다.
|
||||
|
||||
- **예시**:
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
public class PathController {
|
||||
@GetMapping("/path")
|
||||
public String getFullPath() {
|
||||
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
return "Full Path: " + request.getRequestURI();
|
||||
}
|
||||
}
|
||||
```
|
||||
- 장점: 메서드 파라미터 없이도 사용 가능 (예: 서비스 계층에서).
|
||||
|
||||
#### (3) 전체 경로와 함께 컨텍스트 경로 고려
|
||||
애플리케이션이 서블릿 컨테이너에서 실행될 경우, 컨텍스트 경로(예: `/myapp`)가 추가될 수 있습니다. 이를 포함하려면:
|
||||
- `request.getContextPath()`: 컨텍스트 경로 반환 (기본값: 빈 문자열).
|
||||
- 전체 경로: `request.getContextPath() + request.getRequestURI()`.
|
||||
|
||||
---
|
||||
|
||||
### 3. 추가 팁
|
||||
- **경로 충돌 방지**: 동일한 경로에 대해 HTTP 메서드로 구분하거나, 경로를 명확히 분리하세요.
|
||||
- **중첩 경로**: 클래스와 메서드 경로가 결합되므로, 설계 시 중복을 피하도록 주의하세요.
|
||||
- **로깅**: 전체 경로를 로깅하여 디버깅에 활용할 수 있습니다.
|
||||
```java
|
||||
@Slf4j
|
||||
@RestController
|
||||
public class LogController {
|
||||
@GetMapping("/test")
|
||||
public String logPath(HttpServletRequest request) {
|
||||
log.info("Request Path: {}", request.getRequestURI());
|
||||
return "Logged";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 결론
|
||||
스프링 부트에서 경로는 `@RequestMapping`과 그 파생 어노테이션으로 유연하게 지정할 수 있으며, 경로 변수와 쿼리 파라미터를 통해 동적 요청을 처리할 수 있습니다. 전체 요청 경로는 `HttpServletRequest`나 `RequestContextHolder`를 사용해 쉽게 얻을 수 있어, 로깅이나 디버깅에 유용합니다. 이를 통해 RESTful API나 MVC 구조를 깔끔하게 설계할 수 있습니다. 추가 질문이 있다면 말씀해주세요!
|
||||
192
docs/resttemplate.md
Normal file
192
docs/resttemplate.md
Normal file
@@ -0,0 +1,192 @@
|
||||
### 스프링 부트의 RestTemplate에 대한 설명
|
||||
|
||||
스프링 부트에서 `RestTemplate`은 외부 HTTP 서비스와 통신하기 위한 동기식(Synchronous) REST 클라이언트입니다. 스프링 프레임워크에서 제공하는 이 클래스는 HTTP 요청(GET, POST, PUT, DELETE 등)을 쉽게 보내고 응답을 처리할 수 있도록 설계되었습니다. RESTful API 호출이 필요한 경우, `RestTemplate`은 간단한 설정과 직관적인 API로 개발자가 빠르게 통합할 수 있게 해줍니다. 아래에서 설정, 사용 방법, 주요 기능을 설명합니다.
|
||||
|
||||
---
|
||||
|
||||
#### 1. RestTemplate의 기본 개념
|
||||
- **동기식**: 요청을 보내고 응답을 기다린 후 결과를 반환합니다 (비동기 처리는 `WebClient` 추천).
|
||||
- **HTTP 메서드 지원**: GET, POST, PUT, DELETE 등 모든 표준 HTTP 메서드 지원.
|
||||
- **유연성**: 요청 헤더, 바디, 응답 타입 등을 커스터마이징 가능.
|
||||
- **스프링 통합**: 스프링의 의존성 주입과 설정을 활용해 쉽게 사용.
|
||||
|
||||
#### 2. 의존성
|
||||
`RestTemplate`은 `spring-boot-starter-web`에 포함되어 별도 의존성 추가 없이 사용 가능합니다.
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 3. 기본 설정
|
||||
`RestTemplate`은 스프링 빈으로 등록해 사용할 수 있습니다. 기본 설정은 스프링 부트가 자동으로 제공하지만, 필요 시 커스터마이징 가능합니다.
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
public class RestTemplateConfig {
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate(RestTemplateBuilder builder) {
|
||||
return builder
|
||||
.setConnectTimeout(Duration.ofSeconds(5)) // 연결 타임아웃 5초
|
||||
.setReadTimeout(Duration.ofSeconds(10)) // 읽기 타임아웃 10초
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **RestTemplateBuilder**: 타임아웃, 인터셉터, 메시지 컨버터 등을 설정.
|
||||
|
||||
---
|
||||
|
||||
#### 4. RestTemplate 사용 예시
|
||||
|
||||
##### (1) GET 요청
|
||||
```java
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
private final RestTemplate restTemplate;
|
||||
|
||||
public UserService(RestTemplate restTemplate) {
|
||||
this.restTemplate = restTemplate;
|
||||
}
|
||||
|
||||
public User getUser(Long id) {
|
||||
String url = "https://api.example.com/users/{id}";
|
||||
return restTemplate.getForObject(url, User.class, id);
|
||||
}
|
||||
|
||||
public ResponseEntity<User> getUserWithResponse(Long id) {
|
||||
String url = "https://api.example.com/users/{id}";
|
||||
return restTemplate.getForEntity(url, User.class, id);
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public class User {
|
||||
private Long id;
|
||||
private String name;
|
||||
private int age;
|
||||
}
|
||||
```
|
||||
|
||||
- **`getForObject`**: 응답 본문을 직접 객체로 변환.
|
||||
- **`getForEntity`**: HTTP 상태 코드, 헤더, 본문을 포함한 `ResponseEntity` 반환.
|
||||
|
||||
##### (2) POST 요청
|
||||
```java
|
||||
public User createUser(User user) {
|
||||
String url = "https://api.example.com/users";
|
||||
return restTemplate.postForObject(url, user, User.class);
|
||||
}
|
||||
```
|
||||
|
||||
- **`postForObject`**: 요청 바디에 객체를 보내고 응답 객체 반환.
|
||||
- **참고**: JSON 직렬화는 `Jackson` 라이브러리에 의존 (기본 포함).
|
||||
|
||||
##### (3) PUT 요청
|
||||
```java
|
||||
public void updateUser(Long id, User user) {
|
||||
String url = "https://api.example.com/users/{id}";
|
||||
restTemplate.put(url, user, id);
|
||||
}
|
||||
```
|
||||
|
||||
- **`put`**: 응답 본문이 필요 없는 업데이트 작업.
|
||||
|
||||
##### (4) DELETE 요청
|
||||
```java
|
||||
public void deleteUser(Long id) {
|
||||
String url = "https://api.example.com/users/{id}";
|
||||
restTemplate.delete(url, id);
|
||||
}
|
||||
```
|
||||
|
||||
- **`delete`**: 리소스 삭제 요청.
|
||||
|
||||
##### (5) 커스텀 요청 (exchange)
|
||||
```java
|
||||
public User customRequest(Long id) {
|
||||
String url = "https://api.example.com/users/{id}";
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.set("Authorization", "Bearer token123");
|
||||
HttpEntity<String> entity = new HttpEntity<>(headers);
|
||||
|
||||
ResponseEntity<User> response = restTemplate.exchange(
|
||||
url, HttpMethod.GET, entity, User.class, id
|
||||
);
|
||||
return response.getBody();
|
||||
}
|
||||
```
|
||||
|
||||
- **`exchange`**: HTTP 메서드, 헤더, 바디를 자유롭게 설정 가능.
|
||||
|
||||
---
|
||||
|
||||
#### 5. 주요 메서드
|
||||
- **`getForObject(String url, Class<T> responseType, Object... uriVariables)`**: GET 요청 후 객체 반환.
|
||||
- **`getForEntity(String url, Class<T> responseType, Object... uriVariables)`**: GET 요청 후 `ResponseEntity` 반환.
|
||||
- **`postForObject(String url, Object request, Class<T> responseType)`**: POST 요청 후 객체 반환.
|
||||
- **`put(String url, Object request, Object... uriVariables)`**: PUT 요청 실행.
|
||||
- **`delete(String url, Object... uriVariables)`**: DELETE 요청 실행.
|
||||
- **`exchange(String url, HttpMethod method, HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables)`**: 모든 HTTP 메서드에 대한 유연한 요청.
|
||||
|
||||
---
|
||||
|
||||
#### 6. 에러 처리
|
||||
`RestTemplate`은 HTTP 오류(4xx, 5xx)를 `RestClientException`으로抛출합니다. 이를 처리하려면 try-catch 사용:
|
||||
|
||||
```java
|
||||
try {
|
||||
User user = restTemplate.getForObject("https://api.example.com/users/999", User.class);
|
||||
return user;
|
||||
} catch (HttpClientErrorException e) {
|
||||
// 4xx 오류 처리 (예: 404 Not Found)
|
||||
log.error("Client error: {}", e.getStatusCode());
|
||||
return null;
|
||||
} catch (HttpServerErrorException e) {
|
||||
// 5xx 오류 처리 (예: 500 Internal Server Error)
|
||||
log.error("Server error: {}", e.getStatusCode());
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
- **커스텀 에러 핸들러**:
|
||||
```java
|
||||
restTemplate.setErrorHandler(new DefaultResponseErrorHandler() {
|
||||
@Override
|
||||
public void handleError(ClientHttpResponse response) throws IOException {
|
||||
// 커스텀 처리
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 7. 장점과 한계
|
||||
- **장점**:
|
||||
- 사용이 간단하고 직관적.
|
||||
- 스프링 생태계와 잘 통합.
|
||||
- 다양한 HTTP 요청 지원.
|
||||
- **한계**:
|
||||
- 동기식이라 비동기 작업에는 부적합 (`WebClient` 권장).
|
||||
- 대규모 병렬 요청 처리 시 성능 저하 가능.
|
||||
- 스프링 5.0 이후 `WebClient`로 대체되는 추세 (유지보수 모드).
|
||||
|
||||
---
|
||||
|
||||
#### 8. WebClient와의 비교
|
||||
- **`RestTemplate`**: 동기식, 간단한 REST 호출에 적합.
|
||||
- **`WebClient`**: 비동기/리액티브, 대규모 트래픽 및 스트리밍에 적합.
|
||||
- **권장**: 새로운 프로젝트라면 `WebClient` 고려, 기존 코드 유지보수 시 `RestTemplate` 사용.
|
||||
|
||||
---
|
||||
|
||||
#### 9. 결론
|
||||
`RestTemplate`은 스프링 부트에서 외부 REST API 호출을 쉽게 처리할 수 있는 도구로, 동기식 요청이 필요한 환경에서 유용합니다. 기본 메서드로 간단한 GET/POST 요청을 처리하거나, `exchange`로 복잡한 요청을 커스터마이징할 수 있습니다. 비록 `WebClient`가 최신 트렌드로 자리잡았지만, `RestTemplate`은 여전히 간단한 시나리오에서 강력한 선택지입니다. 위 예시를 참고하면 RESTful 통신을 빠르게 구현할 수 있습니다. 추가 질문이 있다면 언제든 물어보세요!
|
||||
|
||||
474
docs/security.md
Normal file
474
docs/security.md
Normal file
@@ -0,0 +1,474 @@
|
||||
### 스프링 시큐리티에 대한 설명
|
||||
|
||||
스프링 시큐리티(Spring Security)는 스프링 기반 애플리케이션에서 보안 기능을 제공하는 강력하고 유연한 프레임워크입니다. 인증(Authentication)과 인가(Authorization)를 비롯한 다양한 보안 요구사항을 처리하며, 웹 애플리케이션, REST API, 마이크로서비스 등에서 널리 사용됩니다. 스프링 부트와 통합 시 자동 설정 기능을 통해 최소한의 코드로 보안을 구현할 수 있다는 점이 큰 장점입니다. 아래에서 주요 개념, 동작 방식, 설정 방법 등을 설명합니다.
|
||||
|
||||
---
|
||||
|
||||
#### 1. 스프링 시큐리티의 주요 기능
|
||||
- **인증(Authentication)**: 사용자가 누구인지 확인합니다 (예: 로그인).
|
||||
- **인가(Authorization)**: 인증된 사용자가 어떤 작업을 수행할 수 있는지 권한을 확인합니다 (예: 관리자만 접근 가능).
|
||||
- **보안 필터 체인**: HTTP 요청을 가로채 보안 로직을 적용합니다.
|
||||
- **세션 관리**: 로그인 상태 유지 및 세션 공격 방지.
|
||||
- **CSRF 방어**: 크로스 사이트 요청 위조 공격 방지.
|
||||
- **비밀번호 암호화**: 안전한 비밀번호 저장을 위한 해시 알고리즘 제공.
|
||||
|
||||
---
|
||||
|
||||
#### 2. 스프링 시큐리티의 동작 원리
|
||||
스프링 시큐리티는 **필터 기반 아키텍처**를 사용합니다. HTTP 요청이 들어오면 `FilterChain`을 통해 보안 필터들이 순차적으로 실행되며, 각 필터는 특정 보안 작업(인증, 인가 등)을 처리합니다.
|
||||
|
||||
- **핵심 컴포넌트**:
|
||||
- `SecurityFilterChain`: 요청을 처리하는 필터 체인을 정의.
|
||||
- `AuthenticationManager`: 인증 요청을 처리.
|
||||
- `UserDetailsService`: 사용자 정보를 로드 (예: DB에서 조회).
|
||||
- `PasswordEncoder`: 비밀번호 암호화 및 검증.
|
||||
|
||||
- **동작 흐름**:
|
||||
1. 사용자가 `/login`으로 로그인 요청.
|
||||
2. `UsernamePasswordAuthenticationFilter`가 요청을 가로채 인증 시도.
|
||||
3. `AuthenticationManager`가 `UserDetailsService`로 사용자 정보를 조회.
|
||||
4. 비밀번호 일치 여부 확인 후 인증 객체(`Authentication`) 생성.
|
||||
5. `SecurityContextHolder`에 인증 정보 저장.
|
||||
6. 이후 요청에서 인가 확인 후 접근 허용/차단.
|
||||
|
||||
---
|
||||
|
||||
#### 3. 스프링 부트에서 기본 설정
|
||||
스프링 부트에 `spring-boot-starter-security` 의존성을 추가하면 기본 보안 설정이 활성화됩니다.
|
||||
|
||||
- **의존성 추가** (`pom.xml`):
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
- **기본 동작**:
|
||||
- 모든 요청에 대해 인증 요구.
|
||||
- 기본 사용자 생성 (ID: `user`, 비밀번호: 실행 시 로그에 출력).
|
||||
- `/login` 폼 제공.
|
||||
|
||||
- **application.yaml로 기본 사용자 설정**:
|
||||
```yaml
|
||||
spring:
|
||||
security:
|
||||
user:
|
||||
name: admin
|
||||
password: password123
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 4. 커스터마이징 예시
|
||||
스프링 시큐리티를 커스터마이징하려면 `SecurityFilterChain`을 정의하는 설정 클래스를 작성합니다.
|
||||
|
||||
- **예시**: 기본 인증 및 인가 설정
|
||||
```java
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/public/**").permitAll() // 공개 경로
|
||||
.requestMatchers("/admin/**").hasRole("ADMIN") // 관리자만 접근
|
||||
.anyRequest().authenticated() // 나머지는 인증 필요
|
||||
)
|
||||
.formLogin(form -> form
|
||||
.loginPage("/login") // 커스텀 로그인 페이지
|
||||
.permitAll()
|
||||
)
|
||||
.logout(logout -> logout
|
||||
.logoutUrl("/logout")
|
||||
.logoutSuccessUrl("/login?logout")
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService() {
|
||||
UserDetails user = User.withUsername("user")
|
||||
.password(passwordEncoder().encode("pass"))
|
||||
.roles("USER")
|
||||
.build();
|
||||
UserDetails admin = User.withUsername("admin")
|
||||
.password(passwordEncoder().encode("admin"))
|
||||
.roles("ADMIN")
|
||||
.build();
|
||||
return new InMemoryUserDetailsManager(user, admin);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
}
|
||||
```
|
||||
- **설명**:
|
||||
- `/public/**`: 인증 없이 접근 가능.
|
||||
- `/admin/**`: "ADMIN" 역할 필요.
|
||||
- 그 외: 인증된 사용자만 접근.
|
||||
- 메모리 내 사용자 정의 (실제로는 DB 연동 추천).
|
||||
|
||||
---
|
||||
|
||||
#### 5. 데이터베이스 연동
|
||||
실제 애플리케이션에서는 `UserDetailsService`를 구현해 데이터베이스에서 사용자 정보를 가져옵니다.
|
||||
|
||||
- **예시**:
|
||||
```java
|
||||
@Service
|
||||
public class CustomUserDetailsService implements UserDetailsService {
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public CustomUserDetailsService(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
UserEntity user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
|
||||
return User.withUsername(user.getUsername())
|
||||
.password(user.getPassword())
|
||||
.roles(user.getRole())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
- `SecurityConfig`에 주입:
|
||||
```java
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService(CustomUserDetailsService customUserDetailsService) {
|
||||
return customUserDetailsService;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 6. REST API와 JWT
|
||||
REST API에서는 폼 로그인 대신 JWT(JSON Web Token)를 사용해 stateless 인증을 구현할 수 있습니다.
|
||||
|
||||
- **구현 개요**:
|
||||
1. `/login`에서 사용자 인증 후 JWT 발급.
|
||||
2. 클라이언트가 `Authorization` 헤더에 JWT 포함.
|
||||
3. `JwtAuthenticationFilter`로 요청마다 토큰 검증.
|
||||
|
||||
- **필터 예시**:
|
||||
```java
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
String token = request.getHeader("Authorization");
|
||||
// JWT 검증 로직 추가
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 7. 주요 어노테이션
|
||||
- `@EnableWebSecurity`: 시큐리티 설정 활성화.
|
||||
- `@PreAuthorize`, `@PostAuthorize`: 메서드 수준에서 인가 확인.
|
||||
- 예: `@PreAuthorize("hasRole('ADMIN')")`
|
||||
- `@Secured`: 역할 기반 접근 제어.
|
||||
|
||||
---
|
||||
|
||||
#### 8. 모범 사례
|
||||
- **비밀번호 암호화**: `BCryptPasswordEncoder` 사용.
|
||||
- **CSRF 비활성화**: REST API에서는 `http.csrf().disable()` 설정.
|
||||
- **HTTPS**: 프로덕션에서 보안 통신 강제.
|
||||
- **에러 처리**: 인증/인가 실패 시 커스텀 응답 제공.
|
||||
|
||||
---
|
||||
|
||||
### 결론
|
||||
스프링 시큐리티는 강력한 보안 기능을 제공하며, 스프링 부트와의 통합으로 설정이 간편합니다. 기본 설정으로 빠르게 시작할 수 있고, 커스터마이징을 통해 복잡한 요구사항도 충족할 수 있습니다. 웹 애플리케이션부터 REST API, OAuth2 통합까지 다양한 시나리오를 지원하므로, 보안이 중요한 애플리케이션에서 필수적인 도구입니다. 추가 질문이 있다면 언제든 물어보세요!
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
스프링 시큐리티와 관련된 주요 어노테이션과 클래스들을 표로 정리하겠습니다. 스프링 시큐리티는 인증(Authentication)과 인가(Authorization)를 관리하기 위한 다양한 어노테이션과 클래스를 제공하며, 이를 통해 보안 설정을 코드 수준에서 유연하게 적용할 수 있습니다.
|
||||
|
||||
---
|
||||
|
||||
### 1. 스프링 시큐리티 관련 어노테이션
|
||||
|
||||
| **어노테이션** | **설명** | **사용 예시** |
|
||||
|---------------------------|-------------------------------------------------------------------------------------------|------------------------------------------------|
|
||||
| `@EnableWebSecurity` | 스프링 시큐리티를 활성화하고, 웹 보안 설정을 커스터마이징할 수 있게 함 | `@EnableWebSecurity public class SecurityConfig` |
|
||||
| `@EnableGlobalMethodSecurity` | 메서드 수준 보안을 활성화 (예: `@PreAuthorize`, `@Secured` 등 사용 가능) | `@EnableGlobalMethodSecurity(prePostEnabled = true)` |
|
||||
| `@PreAuthorize` | 메서드 실행 전에 권한을 확인 (SpEL 지원) | `@PreAuthorize("hasRole('ADMIN')")` |
|
||||
| `@PostAuthorize` | 메서드 실행 후 반환값을 기반으로 권한 확인 | `@PostAuthorize("returnObject.owner == authentication.name")` |
|
||||
| `@Secured` | 특정 역할(Role)이 있는 사용자만 메서드 실행 허용 | `@Secured("ROLE_ADMIN")` |
|
||||
| `@RolesAllowed` | JSR-250 표준 어노테이션으로 `@Secured`와 유사한 역할 기반 인가 | `@RolesAllowed("ROLE_USER")` |
|
||||
| `@AuthenticationPrincipal`| 현재 인증된 사용자의 `Principal` 객체를 메서드 파라미터로 주입 | `@GetMapping("/me") public String getUser(@AuthenticationPrincipal UserDetails user)` |
|
||||
| `@EnableMethodSecurity` | 메서드 보안을 개별적으로 활성화 (Spring 6.1 이상에서 `@EnableGlobalMethodSecurity` 대체) | `@EnableMethodSecurity` |
|
||||
|
||||
- **참고**:
|
||||
- `@PreAuthorize`와 `@PostAuthorize`는 Spring Expression Language(SpEL)를 지원해 복잡한 조건을 정의할 수 있습니다.
|
||||
- `@Secured`와 `@RolesAllowed`는 단순 역할 체크에 적합하지만, SpEL은 지원하지 않습니다.
|
||||
|
||||
---
|
||||
|
||||
### 2. 스프링 시큐리티 관련 주요 클래스
|
||||
|
||||
| **클래스** | **설명** | **주요 사용처** |
|
||||
|------------------------------|-------------------------------------------------------------------------------------------|-----------------------------------------------|
|
||||
| `SecurityFilterChain` | HTTP 요청을 처리하는 보안 필터 체인을 정의 | `HttpSecurity` 설정에서 반환 |
|
||||
| `HttpSecurity` | HTTP 보안 설정을 구성 (인증, 인가, CSRF, 로그인 등) | `SecurityFilterChain` 구성 |
|
||||
| `Authentication` | 인증된 사용자의 정보를 나타내는 인터페이스 | `SecurityContext`에서 가져옴 |
|
||||
| `AuthenticationManager` | 인증 요청을 처리하고 `Authentication` 객체를 반환 | 사용자 인증 로직 |
|
||||
| `UserDetails` | 사용자 정보를 나타내는 인터페이스 (ID, 비밀번호, 권한 등) | `UserDetailsService`에서 반환 |
|
||||
| `UserDetailsService` | 사용자 정보를 로드하는 인터페이스 (DB 연동 시 구현 필요) | 커스텀 사용자 조회 로직 |
|
||||
| `PasswordEncoder` | 비밀번호를 암호화하고 검증하는 인터페이스 | `BCryptPasswordEncoder` 구현체 사용 |
|
||||
| `SecurityContext` | 현재 인증 상태를 저장하는 컨텍스트 | `SecurityContextHolder`에서 관리 |
|
||||
| `SecurityContextHolder` | 현재 스레드의 보안 컨텍스트를 관리 (인증 정보 접근) | `Authentication` 객체 가져오기 |
|
||||
| `UsernamePasswordAuthenticationToken` | 사용자 이름과 비밀번호 기반 인증 토큰 | 로그인 인증 시 생성 |
|
||||
| `AccessDeniedException` | 권한 부족으로 접근이 거부될 때 발생하는 예외 | 인가 실패 처리 |
|
||||
| `AuthenticationEntryPoint` | 인증 실패 시 응답을 처리하는 인터페이스 (예: 401 반환) | 커스텀 로그인 실패 처리 |
|
||||
| `OncePerRequestFilter` | 요청당 한 번만 실행되는 필터를 구현하기 위한 추상 클래스 | JWT 인증 필터 구현 |
|
||||
| `InMemoryUserDetailsManager` | 메모리 내에서 사용자 정보를 관리하는 `UserDetailsService` 구현체 | 테스트용 사용자 설정 |
|
||||
| `WebSecurityCustomizer` | 웹 보안 설정을 커스터마이징 (특정 경로 무시 등) | 정적 리소스 보안 제외 |
|
||||
|
||||
---
|
||||
|
||||
### 추가 설명
|
||||
|
||||
#### 어노테이션 사용 팁
|
||||
- **`@EnableWebSecurity`**: 필수 설정으로, `SecurityFilterChain` 빈을 정의할 때 사용됩니다.
|
||||
- **`@PreAuthorize` vs `@Secured`**: `@PreAuthorize`는 조건이 복잡할 때, `@Secured`는 단순 역할 체크에 적합합니다.
|
||||
- **`@AuthenticationPrincipal`**: 컨트롤러에서 현재 로그인한 사용자를 쉽게 가져올 수 있어 편리합니다.
|
||||
|
||||
#### 클래스 활용 예시
|
||||
- **HttpSecurity 설정**:
|
||||
```java
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/admin/**").hasRole("ADMIN")
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin();
|
||||
return http.build();
|
||||
}
|
||||
```
|
||||
|
||||
- **UserDetailsService 구현**:
|
||||
```java
|
||||
@Service
|
||||
public class CustomUserDetailsService implements UserDetailsService {
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) {
|
||||
// DB에서 사용자 조회 로직
|
||||
return new User(username, "{bcrypt}hashedPassword", Arrays.asList(new SimpleGrantedAuthority("ROLE_USER")));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **PasswordEncoder 설정**:
|
||||
```java
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 결론
|
||||
스프링 시큐리티의 어노테이션은 설정과 메서드 수준 보안을 간편하게 적용할 수 있게 해주며, 관련 클래스들은 인증/인가 로직을 세밀하게 제어할 수 있도록 돕습니다. 이를 조합하면 웹 애플리케이션부터 REST API까지 다양한 보안 시나리오를 구현할 수 있습니다. 추가적인 질문이나 구체적인 예제가 필요하면 말씀해주세요!
|
||||
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
|
||||
### HttpSecurity 클래스의 메서드 정리와 예시 설명
|
||||
|
||||
스프링 시큐리티에서 `HttpSecurity` 클래스는 HTTP 요청에 대한 보안 설정을 구성하는 핵심 클래스입니다. `SecurityFilterChain`을 생성할 때 사용되며, 인증(Authentication), 인가(Authorization), 로그인, 로그아웃, CSRF 보호 등 다양한 보안 기능을 메서드 체이닝 방식으로 설정할 수 있습니다. 아래에서 주요 메서드를 표로 정리하고, 예시와 함께 설명하겠습니다.
|
||||
|
||||
---
|
||||
|
||||
### 1. HttpSecurity 주요 메서드 표
|
||||
|
||||
| **메서드** | **설명** |
|
||||
|-------------------------------|-------------------------------------------------------------------------------------------|
|
||||
| `authorizeHttpRequests()` | HTTP 요청에 대한 인가 규칙을 정의 (경로별 접근 제어 설정) |
|
||||
| `formLogin()` | 폼 기반 로그인 설정 (로그인 페이지, 성공/실패 처리 등) |
|
||||
| `httpBasic()` | HTTP 기본 인증 설정 (클라이언트가 사용자 이름과 비밀번호를 헤더로 전송) |
|
||||
| `logout()` | 로그아웃 설정 (로그아웃 URL, 성공 후 리다이렉션 등) |
|
||||
| `csrf()` | CSRF(Cross-Site Request Forgery) 보호 설정 (활성화/비활성화) |
|
||||
| `cors()` | CORS(Cross-Origin Resource Sharing) 설정 |
|
||||
| `sessionManagement()` | 세션 관리 설정 (세션 생성 정책, 최대 세션 수 등) |
|
||||
| `exceptionHandling()` | 인증/인가 예외 처리 설정 (접근 거부, 인증 실패 시 동작) |
|
||||
| `headers()` | HTTP 헤더 보안 설정 (X-Frame-Options, XSS 보호 등) |
|
||||
| `requestMatchers()` | 특정 요청 경로에만 보안 설정 적용 |
|
||||
| `anonymous()` | 익명 사용자에 대한 설정 (익명 사용자의 권한 정의) |
|
||||
| `oauth2Login()` | OAuth2 기반 로그인 설정 (소셜 로그인 등) |
|
||||
| `addFilter()` | 커스텀 필터를 필터 체인에 추가 |
|
||||
| `addFilterBefore()` | 기존 필터 앞에 커스텀 필터 추가 |
|
||||
| `addFilterAfter()` | 기존 필터 뒤에 커스텀 필터 추가 |
|
||||
| `authenticationProvider()` | 커스텀 인증 제공자(AuthenticationProvider)를 추가 |
|
||||
| `accessDeniedHandler()` | 인가 실패(403) 시 호출되는 핸들러 설정 |
|
||||
| `and()` | 메서드 체이닝에서 이전 설정을 종료하고 새로운 설정 섹션으로 이동 |
|
||||
|
||||
---
|
||||
|
||||
### 2. HttpSecurity 설정 예시와 설명
|
||||
|
||||
#### 기본 설정 예시
|
||||
다음은 일반적인 웹 애플리케이션의 보안 설정 예시입니다.
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/public/**").permitAll() // 공개 경로
|
||||
.requestMatchers("/admin/**").hasRole("ADMIN") // 관리자만 접근
|
||||
.anyRequest().authenticated() // 나머지 인증 필요
|
||||
)
|
||||
.formLogin(form -> form
|
||||
.loginPage("/login") // 커스텀 로그인 페이지
|
||||
.defaultSuccessUrl("/home") // 로그인 성공 시 이동
|
||||
.permitAll() // 로그인 페이지는 누구나 접근 가능
|
||||
)
|
||||
.logout(logout -> logout
|
||||
.logoutUrl("/logout") // 로그아웃 요청 URL
|
||||
.logoutSuccessUrl("/login?logout") // 로그아웃 후 이동
|
||||
)
|
||||
.exceptionHandling(ex -> ex
|
||||
.accessDeniedPage("/access-denied") // 인가 실패 시 이동 페이지
|
||||
)
|
||||
.csrf(csrf -> csrf
|
||||
.disable() // CSRF 비활성화 (테스트용)
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService() {
|
||||
UserDetails user = User.withUsername("user")
|
||||
.password("{noop}password") // {noop}은 암호화 없이 사용
|
||||
.roles("USER")
|
||||
.build();
|
||||
return new InMemoryUserDetailsManager(user);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 메서드별 설명
|
||||
1. **`authorizeHttpRequests()`**
|
||||
- **설명**: 요청 경로별로 접근 권한을 설정합니다. `requestMatchers()`로 경로를 지정하고, `permitAll()`, `hasRole()`, `authenticated()` 등으로 권한을 정의합니다.
|
||||
- **예시**: `/public/**`은 누구나 접근 가능, `/admin/**`은 "ROLE_ADMIN" 역할 필요.
|
||||
|
||||
2. **`formLogin()`**
|
||||
- **설명**: 폼 기반 로그인을 활성화하고, 로그인 페이지와 성공/실패 동작을 커스터마이징합니다.
|
||||
- **예시**: `/login` 페이지로 이동하며, 성공 시 `/home`으로 리다이렉션.
|
||||
|
||||
3. **`logout()`**
|
||||
- **설명**: 로그아웃 요청을 처리하고, 성공 후 이동할 경로를 설정합니다.
|
||||
- **예시**: `/logout` 요청으로 로그아웃 후 `/login?logout`으로 이동.
|
||||
|
||||
4. **`exceptionHandling()`**
|
||||
- **설명**: 인증/인가 실패 시 동작을 정의합니다. `accessDeniedPage()`로 403 오류 페이지를 지정할 수 있습니다.
|
||||
- **예시**: 권한 없는 사용자가 접근 시 `/access-denied`로 이동.
|
||||
|
||||
5. **`csrf()`**
|
||||
- **설명**: CSRF 보호를 설정합니다. REST API에서는 보통 비활성화(`disable()`)하지만, 웹 폼에서는 활성화 권장.
|
||||
- **예시**: `csrf().disable()`로 비활성화 (REST API 환경에서 유용).
|
||||
|
||||
---
|
||||
|
||||
#### REST API용 설정 예시 (CSRF 비활성화 및 HTTP Basic)
|
||||
REST API에서는 폼 로그인 대신 HTTP Basic 인증을 사용할 수 있습니다.
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class RestSecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/api/public/**").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.httpBasic(httpBasic -> httpBasic
|
||||
.realmName("My API") // Basic 인증 영역 이름
|
||||
)
|
||||
.csrf(csrf -> csrf.disable()) // REST API는 CSRF 비활성화
|
||||
.sessionManagement(session -> session
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 세션 사용 안 함
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **`httpBasic()`**: 클라이언트가 `Authorization: Basic base64(username:password)` 헤더로 인증.
|
||||
- **`sessionManagement()`**: 세션 생성 정책을 `STATELESS`로 설정해 RESTful 특성 유지.
|
||||
|
||||
---
|
||||
|
||||
#### 커스텀 필터 추가 예시
|
||||
JWT 기반 인증을 위해 커스텀 필터를 추가할 수 있습니다.
|
||||
|
||||
```java
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
String token = request.getHeader("Authorization");
|
||||
// JWT 검증 로직
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class JwtSecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
|
||||
.csrf(csrf -> csrf.disable());
|
||||
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **`addFilterBefore()`**: `UsernamePasswordAuthenticationFilter` 앞에 JWT 필터 추가.
|
||||
|
||||
---
|
||||
|
||||
### 3. 결론
|
||||
`HttpSecurity`의 메서드는 스프링 시큐리티에서 HTTP 요청 보안을 세밀하게 제어할 수 있게 해줍니다. `authorizeHttpRequests()`로 경로별 권한을 설정하고, `formLogin()`, `httpBasic()`, `logout()` 등으로 인증 방식을 정의하며, `csrf()`, `sessionManagement()`으로 추가 보안 옵션을 조정할 수 있습니다. 예시를 통해 웹 애플리케이션과 REST API 모두에 적용 가능한 설정을 확인할 수 있으며, 필요에 따라 커스텀 필터를 추가해 확장성도 확보할 수 있습니다. 추가 질문이 있다면 언제든 물어보세요!
|
||||
38
docs/security/01_인증과 인가.md
Normal file
38
docs/security/01_인증과 인가.md
Normal file
@@ -0,0 +1,38 @@
|
||||
아래는 "스프링 시큐리티" 책의 3장 일부에 해당하는 "인증과 인가 소개"와 "기본 인증 메커니즘"에 대한 내용입니다. 자연스럽고 명확한 설명을 목표로 작성했습니다.
|
||||
|
||||
---
|
||||
|
||||
### 3장. 스프링 시큐리티 기본 동작 이해
|
||||
|
||||
#### 3.1 인증(Authentication)과 인가(Authorization)
|
||||
|
||||
애플리케이션 보안을 다룰 때 가장 중요한 두 개념은 **인증(Authentication)**과 **인가(Authorization)**입니다. 이 두 가지는 보안 시스템의 핵심이며, 서로 밀접하게 연관되어 있지만 역할은 다릅니다.
|
||||
|
||||
**인증(Authentication)**은 "당신이 누구인지" 확인하는 과정입니다. 사용자가 시스템에 접근하려 할 때, 그들이 주장하는 신원을 검증합니다. 예를 들어, 사용자가 로그인 폼에 아이디와 비밀번호를 입력하면 시스템은 이를 데이터베이스나 다른 인증 제공자와 비교해 사용자가 맞는지 확인합니다. 인증이 성공하면 사용자는 시스템에 "로그인"한 상태가 됩니다.
|
||||
|
||||
**인가(Authorization)**는 "당신이 무엇을 할 수 있는지" 결정하는 과정입니다. 인증이 사용자의 신원을 확인했다면, 인가는 그 사용자가 접근할 수 있는 리소스나 수행할 수 있는 작업을 정의합니다. 예를 들어, 일반 사용자는 자신의 프로필만 수정할 수 있고, 관리자는 모든 사용자의 데이터를 삭제할 수 있는 권한을 가질 수 있습니다. 인가는 보통 역할(Role)이나 권한(Authority)을 기반으로 설정됩니다.
|
||||
|
||||
스프링 시큐리티는 이 두 과정을 효과적으로 관리하며, 복잡한 보안 요구사항을 간단한 설정으로 처리할 수 있게 도와줍니다. 인증과 인가가 제대로 작동해야 사용자는 안전하게 시스템을 사용할 수 있고, 개발자는 보안 위협으로부터 애플리케이션을 보호할 수 있습니다.
|
||||
|
||||
#### 3.2 기본 인증 메커니즘
|
||||
|
||||
스프링 시큐리티는 다양한 인증 방식을 지원하지만, 기본적으로 제공하는 인증 메커니즘은 **폼 기반 로그인(Form-based Authentication)**과 **HTTP 기본 인증(HTTP Basic Authentication)**입니다. 스프링부트 프로젝트에 스프링 시큐리티를 추가하면 별도의 설정 없이도 이 기본 인증이 즉시 동작합니다. 이를 이해하려면 먼저 기본 동작을 살펴보겠습니다.
|
||||
|
||||
##### 폼 기반 로그인 (Form-based Authentication)
|
||||
스프링 시큐리티를 추가하면, 애플리케이션의 모든 엔드포인트는 기본적으로 보호됩니다. 보호된 리소스에 접근하려는 사용자는 스프링 시큐리티가 자동으로 제공하는 로그인 페이지로 이동합니다. 이 페이지는 간단한 HTML 폼으로, 사용자가 아이디와 비밀번호를 입력하면 이를 제출해 인증을 시도합니다.
|
||||
|
||||
기본 설정에서는 메모리에 저장된 사용자(예: `user`라는 이름과 무작위 비밀번호)를 사용하며, 이 정보는 애플리케이션 시작 시 콘솔에 출력됩니다. 인증이 성공하면 사용자는 원래 요청한 페이지로 이동하고, 실패하면 오류 메시지가 표시됩니다. 이 과정은 스프링 시큐리티의 **UsernamePasswordAuthenticationFilter**가 처리하며, 이후 자세히 다룰 예정입니다.
|
||||
|
||||
##### HTTP 기본 인증 (HTTP Basic Authentication)
|
||||
또 다른 기본 인증 방식은 HTTP 기본 인증입니다. 이 방식은 브라우저가 제공하는 팝업 창을 통해 사용자 이름과 비밀번호를 입력받습니다. 사용자가 보호된 리소스에 접근하면, 서버는 `401 Unauthorized` 상태 코드와 함께 `WWW-Authenticate` 헤더를 반환합니다. 브라우저는 이를 감지해 인증 창을 띄우고, 입력된 자격 증명은 Base64로 인코딩되어 요청 헤더(`Authorization`)에 포함됩니다.
|
||||
|
||||
HTTP 기본 인증은 설정이 간단하고 REST API나 서버 간 통신에서 유용하지만, 사용자 경험이 제한적이고 비밀번호가 암호화되지 않은 상태로 전송될 수 있어 HTTPS와 함께 사용하는 것이 권장됩니다.
|
||||
|
||||
##### 기본 인증의 동작 원리
|
||||
두 방식 모두 스프링 시큐리티의 **Security Filter Chain**을 통해 처리됩니다. 이 필터 체인은 요청을 가로채 사용자가 인증되었는지, 권한이 있는지 확인한 뒤 적절히 처리합니다. 기본 설정은 빠르게 테스트하거나 학습할 때 유용하지만, 실무에서는 데이터베이스 연동, OAuth2, JWT 등 더 강력한 인증 방식을 사용하게 될 것입니다.
|
||||
|
||||
이 장에서는 기본 인증 메커니즘을 기반으로 스프링 시큐리티의 동작을 이해하고, 이후 장에서 이를 커스터마이징하거나 확장하는 방법을 배워보겠습니다.
|
||||
|
||||
---
|
||||
|
||||
위 내용은 독자가 인증과 인가의 개념을 명확히 이해하고, 스프링 시큐리티의 기본 인증 방식을 쉽게 파악할 수 있도록 작성되었습니다. 추가로 다루고 싶은 세부 사항이나 예제가 있다면 말씀해 주세요!
|
||||
58
docs/security/02_Security Filter Chain의 구조.md
Normal file
58
docs/security/02_Security Filter Chain의 구조.md
Normal file
@@ -0,0 +1,58 @@
|
||||
아래는 "스프링 시큐리티" 책의 3장에 포함될 "Security Filter Chain의 구조"와 "기본 로그인 페이지와 동작 원리"에 대한 내용입니다. 기술적인 깊이를 유지하면서도 초보자가 이해하기 쉽도록 설명했습니다.
|
||||
|
||||
---
|
||||
|
||||
### 3장. 스프링 시큐리티 기본 동작 이해
|
||||
|
||||
#### 3.3 Security Filter Chain의 구조
|
||||
|
||||
스프링 시큐리티의 핵심은 **Security Filter Chain**입니다. 이는 HTTP 요청을 처리하기 전에 보안 로직을 적용하는 일련의 필터들로 구성된 체인입니다. 스프링 시큐리티가 동작하는 방식은 이 필터 체인을 통해 요청을 가로채고, 인증 및 인가를 수행하며, 필요에 따라 요청을 차단하거나 허용하는 구조로 설계되었습니다.
|
||||
|
||||
##### Security Filter Chain이란?
|
||||
Security Filter Chain은 서블릿 필터(Servlet Filter)의 확장된 형태로, 스프링의 `FilterChainProxy`라는 특별한 필터가 이를 관리합니다. 이 체인은 여러 개의 보안 필터로 이루어져 있으며, 각 필터는 특정 보안 작업을 담당합니다. 예를 들어, 사용자의 로그인 요청을 처리하거나, CSRF 공격을 방어하거나, 세션을 관리하는 등의 역할을 합니다.
|
||||
|
||||
##### 주요 필터의 역할
|
||||
스프링 시큐리티는 기본적으로 여러 필터를 제공하며, 이들은 순서대로 실행됩니다. 몇 가지 중요한 필터를 소개하면 다음과 같습니다:
|
||||
- **SecurityContextPersistenceFilter**: 요청 사이에 `SecurityContext`(인증 정보)를 유지합니다. 세션을 통해 사용자의 인증 상태를 저장하고 불러오는 역할을 합니다.
|
||||
- **UsernamePasswordAuthenticationFilter**: 폼 기반 로그인을 처리합니다. 사용자가 제출한 아이디와 비밀번호를 받아 인증을 시도합니다.
|
||||
- **BasicAuthenticationFilter**: HTTP 기본 인증을 처리합니다. 요청 헤더에서 자격 증명을 추출해 인증을 수행합니다.
|
||||
- **FilterSecurityInterceptor**: 인가(Authorization)를 담당합니다. 요청된 리소스에 접근할 권한이 있는지 확인하고, 없으면 예외를 발생시킵니다.
|
||||
|
||||
##### 동작 흐름
|
||||
1. 클라이언트가 HTTP 요청을 보내면, `FilterChainProxy`가 이를 가로챕니다.
|
||||
2. 요청은 Security Filter Chain의 필터들을 순차적으로 통과합니다.
|
||||
3. 각 필터는 자신의 역할에 따라 요청을 처리하거나 다음 필터로 넘깁니다.
|
||||
4. 모든 필터를 통과하면 요청은 컨트롤러로 전달되고, 그렇지 않으면 보안 예외(예: 403 Forbidden)가 발생합니다.
|
||||
|
||||
필터의 순서와 동작은 설정에 따라 커스터마이징할 수 있으며, 이는 스프링 시큐리티의 유연성을 보여줍니다. 기본 설정에서는 이러한 필터들이 자동으로 구성되어 간단한 보안 요구사항을 충족하지만, 실무에서는 필요에 따라 필터를 추가하거나 순서를 조정할 수 있습니다.
|
||||
|
||||
#### 3.4 기본 로그인 페이지와 동작 원리
|
||||
|
||||
스프링 시큐리티를 스프링부트 프로젝트에 추가하면, 별도의 설정 없이도 모든 엔드포인트가 보호되고, 기본 로그인 페이지가 활성화됩니다. 이 기본 로그인 페이지는 스프링 시큐리티의 간편함을 보여주는 좋은 예이며, 그 동작 원리를 이해하면 커스터마이징의 기초를 다질 수 있습니다.
|
||||
|
||||
##### 기본 로그인 페이지의 모습
|
||||
프로젝트에 스프링 시큐리티 의존성을 추가하고 애플리케이션을 실행한 뒤, 브라우저에서 어떤 URL(예: `http://localhost:8080`)에 접속하면 자동으로 `/login` 경로로 리다이렉트됩니다. 이 경로에는 스프링 시큐리티가 생성한 기본 로그인 폼이 나타납니다. 이 폼은 다음과 같은 요소를 포함합니다:
|
||||
- 사용자 이름 입력 필드 (기본값: `username`)
|
||||
- 비밀번호 입력 필드 (기본값: `password`)
|
||||
- "로그인" 버튼
|
||||
|
||||
기본 설정에서는 메모리에 저장된 사용자(`user`)와 애플리케이션 시작 시 생성된 무작위 비밀번호가 사용됩니다. 이 비밀번호는 콘솔 로그에 출력되며, 이를 입력하면 인증이 완료됩니다.
|
||||
|
||||
##### 동작 원리
|
||||
기본 로그인 페이지의 동작은 Security Filter Chain과 밀접하게 연관되어 있습니다. 아래는 그 과정을 단계별로 설명한 것입니다:
|
||||
1. **요청 차단**: 사용자가 보호된 리소스(예: `/home`)에 접근하려 하면, `FilterSecurityInterceptor`가 이를 감지하고 인증 여부를 확인합니다. 인증되지 않았다면 `/login`으로 리다이렉트됩니다.
|
||||
2. **로그인 페이지 표시**: `/login` 경로는 스프링 시큐리티가 내부적으로 제공하는 기본 컨트롤러에 의해 렌더링됩니다. 이 페이지는 간단한 HTML로 구성되어 있습니다.
|
||||
3. **인증 요청 처리**: 사용자가 폼에 아이디와 비밀번호를 입력하고 "로그인" 버튼을 누르면, POST 요청(`/login`)이 발생합니다. 이 요청은 `UsernamePasswordAuthenticationFilter`가 가로채 처리합니다.
|
||||
- 입력된 자격 증명은 `AuthenticationManager`로 전달되어 인증이 시도됩니다.
|
||||
- 기본 설정에서는 `InMemoryUserDetailsManager`가 사용자 정보를 확인합니다.
|
||||
4. **성공 또는 실패 처리**:
|
||||
- 인증 성공 시, `SecurityContext`에 인증 정보가 저장되고, 사용자는 원래 요청한 페이지(예: `/home`)로 리다이렉트됩니다.
|
||||
- 실패 시, `/login?error`로 리다이렉트되며 오류 메시지가 표시됩니다.
|
||||
5. **세션 관리**: 인증이 완료되면 세션이 생성되고, `SecurityContextPersistenceFilter`가 이를 유지합니다.
|
||||
|
||||
##### 한계와 확장
|
||||
기본 로그인 페이지는 빠르게 테스트하거나 프로토타입을 만들 때 유용하지만, 실무에서는 디자인과 기능을 커스터마이징해야 합니다. 예를 들어, 데이터베이스에서 사용자 정보를 가져오거나, 소셜 로그인을 추가하는 방식으로 확장할 수 있습니다. 이후 장에서 이러한 커스터마이징 방법을 자세히 다룰 것입니다.
|
||||
|
||||
---
|
||||
|
||||
위 내용은 Security Filter Chain의 구조와 기본 로그인 페이지의 동작을 체계적으로 설명하며, 스프링 시큐리티의 내부 동작을 이해하는 데 초점을 맞췄습니다. 추가적인 예제 코드나 다이어그램이 필요하다면 말씀해 주세요!
|
||||
227
docs/security/03_UserDetailsService 구현.md
Normal file
227
docs/security/03_UserDetailsService 구현.md
Normal file
@@ -0,0 +1,227 @@
|
||||
아래는 "스프링 시큐리티" 책의 4장에 포함될 "인메모리 인증 및 데이터베이스 연동을 통한 인증", "커스텀 UserDetailsService 구현", 그리고 "비밀번호 암호화와 보안"에 대한 내용입니다. 실무에서의 활용을 고려해 실습 가능하고 명확한 설명을 제공했습니다.
|
||||
|
||||
---
|
||||
|
||||
### 4장. 사용자 인증 구현
|
||||
|
||||
#### 4.1 In-Memory 인증 설정
|
||||
|
||||
**In-Memory 인증**은 스프링 시큐리티에서 사용자 정보를 애플리케이션 메모리에 저장하고 관리하는 가장 간단한 인증 방식입니다. 별도의 데이터베이스나 외부 시스템 없이 빠르게 설정할 수 있어 학습이나 프로토타입 개발에 적합합니다.
|
||||
|
||||
스프링부트에 스프링 시큐리티를 추가하면 기본적으로 In-Memory 인증이 활성화됩니다. 이 경우 `user`라는 이름의 사용자와 무작위 비밀번호가 생성되며, 콘솔에 출력됩니다. 하지만 실무에서는 사용자를 직접 정의하고 싶을 때가 많습니다. 이를 위해 설정 클래스를 작성할 수 있습니다.
|
||||
|
||||
##### 설정 예제
|
||||
```java
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService() {
|
||||
var user = User.withUsername("john")
|
||||
.password("{noop}password123") // {noop}은 암호화 없음을 의미
|
||||
.roles("USER")
|
||||
.build();
|
||||
var admin = User.withUsername("admin")
|
||||
.password("{noop}admin123")
|
||||
.roles("ADMIN")
|
||||
.build();
|
||||
return new InMemoryUserDetailsManager(user, admin);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.anyRequest().authenticated())
|
||||
.formLogin();
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
위 코드에서 `InMemoryUserDetailsManager`를 사용해 두 명의 사용자(`john`과 `admin`)를 메모리에 등록했습니다. 이들은 각각 `USER`와 `ADMIN` 역할을 가지며, 로그인 시 이를 기반으로 인증됩니다. `{noop}`은 비밀번호 암호화를 사용하지 않음을 의미하며, 이후 암호화 섹션에서 개선 방법을 다룹니다.
|
||||
|
||||
In-Memory 인증은 간단하지만 사용자 수가 많거나 동적으로 변경될 경우 한계가 있습니다. 이를 해결하기 위해 데이터베이스 연동으로 넘어갑니다.
|
||||
|
||||
#### 4.2 데이터베이스 연동을 통한 사용자 인증
|
||||
|
||||
실제 애플리케이션에서는 사용자 정보를 데이터베이스에 저장하고, 이를 기반으로 인증을 처리하는 경우가 많습니다. 스프링 시큐리티는 이를 위해 `UserDetailsService` 인터페이스를 제공하며, 데이터베이스와 연동해 사용자 정보를 조회할 수 있습니다.
|
||||
|
||||
##### 데이터베이스 설정
|
||||
먼저, 사용자 정보를 저장할 테이블을 설계해야 합니다. 예를 들어:
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(100) NOT NULL,
|
||||
enabled BOOLEAN NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE authorities (
|
||||
username VARCHAR(50) NOT NULL,
|
||||
authority VARCHAR(50) NOT NULL,
|
||||
FOREIGN KEY (username) REFERENCES users(username)
|
||||
);
|
||||
```
|
||||
위 테이블은 사용자 정보(`users`)와 권한 정보(`authorities`)를 분리해 저장합니다.
|
||||
|
||||
##### 기본 연동 설정
|
||||
스프링 시큐리티는 `JdbcUserDetailsManager`를 통해 데이터베이스와 바로 연동할 수 있습니다.
|
||||
```java
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
|
||||
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.JdbcUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public DataSource dataSource() {
|
||||
return new EmbeddedDatabaseBuilder()
|
||||
.setType(EmbeddedDatabaseType.H2)
|
||||
.addScript("classpath:schema.sql")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService(DataSource dataSource) {
|
||||
return new JdbcUserDetailsManager(dataSource);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.anyRequest().authenticated())
|
||||
.formLogin();
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
`JdbcUserDetailsManager`는 기본적으로 `users`와 `authorities` 테이블을 조회하며, 사용자 이름, 비밀번호, 활성화 여부, 권한 등을 가져옵니다. `schema.sql` 파일에 위의 테이블 생성 쿼리를 추가하면 H2 데이터베이스가 자동으로 설정됩니다.
|
||||
|
||||
#### 4.3 커스텀 UserDetailsService 구현
|
||||
|
||||
기본 제공되는 `JdbcUserDetailsManager`는 편리하지만, 테이블 구조나 비즈니스 로직이 복잡할 경우 커스터마이징이 필요합니다. 이때 `UserDetailsService` 인터페이스를 직접 구현해 커스텀 로직을 추가할 수 있습니다.
|
||||
|
||||
##### 구현 예제
|
||||
```java
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class CustomUserDetailsService implements UserDetailsService {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public CustomUserDetailsService(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
UserEntity user = userRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
|
||||
|
||||
return org.springframework.security.core.userdetails.User
|
||||
.withUsername(user.getUsername())
|
||||
.password(user.getPassword())
|
||||
.authorities(user.getRoles().stream().map(Role::getName).toArray(String[]::new))
|
||||
.disabled(!user.isEnabled())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
위 코드에서:
|
||||
- `UserRepository`는 JPA나 MyBatis 같은 ORM으로 데이터베이스에서 사용자 정보를 조회합니다.
|
||||
- `loadUserByUsername` 메서드는 사용자 이름을 기반으로 사용자 정보를 가져와 `UserDetails` 객체로 변환합니다.
|
||||
- `UserEntity`는 커스텀 엔티티 클래스이며, 역할(`roles`)과 활성화 상태(`enabled`)를 포함합니다.
|
||||
|
||||
##### 설정에 통합
|
||||
```java
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService(UserRepository userRepository) {
|
||||
return new CustomUserDetailsService(userRepository);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
|
||||
.formLogin();
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
이렇게 하면 인증 시 `CustomUserDetailsService`가 호출되어 데이터베이스에서 사용자 정보를 가져옵니다.
|
||||
|
||||
#### 4.4 비밀번호 암호화와 보안
|
||||
|
||||
비밀번호를 평문으로 저장하는 것은 보안상 매우 위험합니다. 스프링 시큐리티는 이를 방지하기 위해 비밀번호 암호화를 권장하며, `PasswordEncoder` 인터페이스를 제공합니다.
|
||||
|
||||
##### PasswordEncoder 소개
|
||||
스프링 시큐리티에서 기본으로 사용하는 암호화 방식은 **BCrypt**입니다. BCrypt는 단방향 해시 함수로, 동일한 입력에 대해 항상 다른 출력(솔트 포함)을 생성해 보안을 강화합니다. 주요 특징은 다음과 같습니다:
|
||||
- **솔팅(Salting)**: 동일한 비밀번호라도 다른 해시값을 생성.
|
||||
- **작업 비용(Work Factor)**: 계산 비용을 조정해 무차별 대입 공격(Brute-force)을 어렵게 함.
|
||||
|
||||
##### 설정 예제
|
||||
```java
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
|
||||
var user = User.withUsername("john")
|
||||
.password(passwordEncoder.encode("password123"))
|
||||
.roles("USER")
|
||||
.build();
|
||||
return new InMemoryUserDetailsManager(user);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
|
||||
.formLogin();
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
- `passwordEncoder.encode()`로 비밀번호를 암호화합니다.
|
||||
- 로그인 시 입력된 비밀번호와 저장된 해시값을 비교하려면 `passwordEncoder.matches()`를 내부적으로 사용합니다.
|
||||
|
||||
##### 데이터베이스 연동 시 적용
|
||||
데이터베이스에 저장할 때는 암호화된 비밀번호를 저장하고, 인증 시 `PasswordEncoder`를 `CustomUserDetailsService`와 함께 사용합니다. 스프링 시큐리티는 이를 자동으로 처리하므로, `UserDetails` 객체에 암호화된 비밀번호만 제공하면 됩니다.
|
||||
|
||||
##### 추가 보안 팁
|
||||
- **다양한 암호화 방식**: `DelegatingPasswordEncoder`를 사용하면 BCrypt 외에 다른 알고리즘(PBKDF2, SCrypt 등)을 지원할 수 있습니다.
|
||||
- **비밀번호 정책**: 강력한 비밀번호를 요구하거나 주기적 변경을 유도하세요.
|
||||
- **암호화 키 관리**: 외부 설정 파일이나 환경 변수를 통해 암호화 키를 관리하세요.
|
||||
|
||||
---
|
||||
|
||||
위 내용은 인증 방식의 기본부터 실무 적용까지 다루며, 비밀번호 암호화로 보안을 강화하는 방법을 설명했습니다. 코드 예제나 특정 부분에 대한 추가 설명이 필요하면 말씀해 주세요!
|
||||
111
docs/security/04_역할과 권한.md
Normal file
111
docs/security/04_역할과 권한.md
Normal file
@@ -0,0 +1,111 @@
|
||||
아래는 "스프링 시큐리티" 책의 5장에 포함될 "역할(Role)과 권한(Authority)의 차이"와 "URL 기반 접근 제어"에 대한 내용입니다. 개념을 명확히 설명하고 실습 가능한 예제를 포함해 실무에서의 활용성을 높였습니다.
|
||||
|
||||
---
|
||||
|
||||
### 5장. 인가와 권한 관리
|
||||
|
||||
#### 5.1 역할(Role)과 권한(Authority)의 차이
|
||||
|
||||
스프링 시큐리티에서 **역할(Role)**과 **권한(Authority)**은 사용자가 시스템에서 무엇을 할 수 있는지를 정의하는 핵심 개념입니다. 두 용어는 종종 혼용되지만, 미묘한 차이가 있으며 이를 이해하면 인가를 더 세밀하게 관리할 수 있습니다.
|
||||
|
||||
##### 역할(Role)이란?
|
||||
역할은 사용자가 속한 그룹이나 직책을 나타내는 상위 수준의 개념입니다. 예를 들어, "관리자(ADMIN)", "일반 사용자(USER)", "게스트(GUEST)" 같은 이름으로 정의됩니다. 역할은 보통 사용자의 주요 책임이나 접근 범위를 나타내며, 스프링 시큐리티에서는 `ROLE_` 접두사를 붙여 표현합니다(예: `ROLE_ADMIN`).
|
||||
|
||||
역할은 단순하고 직관적이어서 소규모 애플리케이션이나 기본적인 권한 관리에 적합합니다. 예를 들어, 관리자 역할은 모든 기능을 사용할 수 있고, 일반 사용자는 제한된 기능만 접근할 수 있도록 설정할 수 있습니다.
|
||||
|
||||
##### 권한(Authority)이란?
|
||||
권한은 더 세분화된 접근 제어 단위로, 특정 작업이나 리소스에 대한 권한을 나타냅니다. 예를 들어, "게시글 작성(WRITE_POST)", "댓글 삭제(DELETE_COMMENT)", "사용자 관리(MANAGE_USERS)" 같은 구체적인 권한을 정의할 수 있습니다. 권한은 `ROLE_` 접두사 없이도 사용 가능하며, 역할보다 유연하게 설계할 수 있습니다.
|
||||
|
||||
권한은 역할에 포함될 수 있으며, 복잡한 시스템에서 세밀한 접근 제어를 구현할 때 유용합니다. 예를 들어, `ROLE_USER`는 `READ_POST`와 `WRITE_POST` 권한을 가질 수 있고, `ROLE_ADMIN`은 추가로 `MANAGE_USERS` 권한을 가질 수 있습니다.
|
||||
|
||||
##### 차이점과 활용
|
||||
- **수준**: 역할은 상위 수준(추상적), 권한은 하위 수준(구체적).
|
||||
- **표기**: 역할은 `ROLE_` 접두사를 기본으로 사용, 권한은 자유롭게 정의.
|
||||
- **복잡성**: 역할은 단순한 분류에 적합, 권한은 세부적인 제어에 유리.
|
||||
|
||||
스프링 시큐리티에서는 `GrantedAuthority` 인터페이스를 통해 두 개념을 통합적으로 관리하며, 설정에 따라 역할과 권한을 혼합해 사용할 수 있습니다. 예를 들어, `ROLE_ADMIN` 역할을 가진 사용자가 `MANAGE_USERS` 권한을 추가로 가질 수 있습니다.
|
||||
|
||||
#### 5.2 URL 기반 접근 제어
|
||||
|
||||
URL 기반 접근 제어는 스프링 시큐리티에서 가장 흔히 사용되는 인가 방식으로, 요청 URL 패턴에 따라 사용자의 접근을 허용하거나 차단합니다. 이를 통해 애플리케이션의 각 엔드포인트를 역할이나 권한에 따라 보호할 수 있습니다.
|
||||
|
||||
##### 기본 설정
|
||||
스프링 시큐리티는 `HttpSecurity`를 사용해 URL 기반 접근 제어를 설정합니다. 기본적으로 모든 요청을 인증된 사용자만 접근 가능하도록 설정할 수 있습니다.
|
||||
|
||||
```java
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService() {
|
||||
var user = User.withUsername("user")
|
||||
.password("{noop}password")
|
||||
.roles("USER")
|
||||
.build();
|
||||
var admin = User.withUsername("admin")
|
||||
.password("{noop}adminpass")
|
||||
.roles("ADMIN")
|
||||
.build();
|
||||
return new InMemoryUserDetailsManager(user, admin);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/admin/**").hasRole("ADMIN") // /admin 하위는 ADMIN만 접근
|
||||
.requestMatchers("/user/**").hasRole("USER") // /user 하위는 USER만 접근
|
||||
.requestMatchers("/public/**").permitAll() // /public은 누구나 접근 가능
|
||||
.anyRequest().authenticated() // 나머지 요청은 인증 필요
|
||||
)
|
||||
.formLogin(); // 기본 로그인 폼 활성화
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### 주요 메서드 설명
|
||||
- **`requestMatchers()`**: 특정 URL 패턴을 지정합니다. 와일드카드(`**`, `*`)를 사용해 여러 경로를 매핑할 수 있습니다.
|
||||
- **`hasRole()`**: 주어진 역할이 있는 사용자만 접근을 허용합니다. `ROLE_` 접두사는 자동으로 추가됩니다(예: `hasRole("ADMIN")` → `ROLE_ADMIN`).
|
||||
- **`permitAll()`**: 인증 없이 누구나 접근 가능하도록 설정합니다.
|
||||
- **`anyRequest()`**: 위에서 정의되지 않은 모든 요청에 대해 적용할 규칙을 지정합니다.
|
||||
- **`authenticated()`**: 인증된 사용자만 접근 가능하도록 설정합니다.
|
||||
|
||||
##### 동작 원리
|
||||
1. 사용자가 `/admin/dashboard`에 접근하면, `FilterSecurityInterceptor`가 요청을 가로챕니다.
|
||||
2. `SecurityConfig`에 정의된 규칙을 확인해 사용자가 `ROLE_ADMIN` 역할을 가졌는지 검사합니다.
|
||||
3. 역할이 일치하면 요청이 통과되고, 그렇지 않으면 `403 Forbidden` 오류가 발생하거나 로그인 페이지로 리다이렉트됩니다.
|
||||
|
||||
##### 권한 기반 제어
|
||||
역할 대신 권한으로 제어하려면 `hasAuthority()`를 사용합니다:
|
||||
```java
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/posts/write").hasAuthority("WRITE_POST")
|
||||
.requestMatchers("/posts/delete").hasAuthority("DELETE_POST")
|
||||
.anyRequest().authenticated()
|
||||
);
|
||||
```
|
||||
이 경우, `UserDetailsService`에서 사용자에게 `WRITE_POST`나 `DELETE_POST` 같은 권한을 부여해야 합니다.
|
||||
|
||||
##### 추가 설정
|
||||
- **순서 중요성**: 규칙은 위에서 아래로 평가되므로, 더 구체적인 규칙을 먼저 작성해야 합니다. 예를 들어, `/public/**`를 `anyRequest()`보다 뒤에 두면 적용되지 않습니다.
|
||||
- **로그인 페이지 접근 허용**: 기본 로그인 페이지(`/login`)는 `permitAll()`로 열어둬야 인증되지 않은 사용자도 접근할 수 있습니다.
|
||||
- **커스터마이징**: `.access()` 메서드를 사용하면 더 복잡한 조건(예: IP 주소나 시간 기반)을 추가할 수 있습니다.
|
||||
|
||||
##### 실습 예제
|
||||
1. `/public/welcome`은 누구나, `/user/profile`은 `USER`, `/admin/manage`는 `ADMIN`만 접근 가능하도록 설정하세요.
|
||||
2. 브라우저에서 각 URL에 접근해 결과를 확인하세요.
|
||||
3. `hasAuthority()`로 `MANAGE_USERS` 권한을 추가하고 테스트해보세요.
|
||||
|
||||
---
|
||||
|
||||
위 내용은 역할과 권한의 차이를 명확히 하고, URL 기반 접근 제어의 설정과 동작을 실습 가능하도록 설명했습니다. 추가적인 코드 예제나 세부 사항이 필요하면 말씀해 주세요!
|
||||
136
docs/security/05_메서드 수준 보안.md
Normal file
136
docs/security/05_메서드 수준 보안.md
Normal file
@@ -0,0 +1,136 @@
|
||||
아래는 "스프링 시큐리티" 책의 5장에 포함될 "메서드 수준 보안 설정"과 "@PreAuthorize, @Secured 어노테이션 활용"에 대한 내용입니다. 이 설명은 개념을 명확히 하고 실무에서 사용할 수 있는 예제를 포함해 작성되었습니다.
|
||||
|
||||
---
|
||||
|
||||
### 5장. 인가와 권한 관리
|
||||
|
||||
#### 5.3 메서드 수준 보안 설정
|
||||
|
||||
URL 기반 접근 제어는 웹 요청 단위로 보안을 적용하는 데 유용하지만, 더 세밀한 제어가 필요한 경우가 있습니다. 예를 들어, 특정 비즈니스 로직이나 서비스 메서드에 접근을 제한하고 싶을 때 **메서드 수준 보안 설정**을 사용합니다. 스프링 시큐리티는 이를 위해 메서드 호출 시점에서 권한을 검사하는 기능을 제공하며, 주로 어노테이션 기반으로 구현됩니다.
|
||||
|
||||
##### 메서드 수준 보안의 필요성
|
||||
- **세밀한 제어**: URL 패턴만으로는 컨트롤러 내부 메서드의 개별 로직을 구분하기 어렵습니다.
|
||||
- **비즈니스 로직 보호**: 데이터베이스 작업이나 민감한 연산을 호출하는 메서드를 보호할 수 있습니다.
|
||||
- **재사용성**: 여러 엔드포인트에서 호출되는 서비스 메서드에 일관된 보안을 적용할 수 있습니다.
|
||||
|
||||
스프링 시큐리티는 메서드 수준 보안을 활성화하려면 `@EnableMethodSecurity`를 설정 클래스에 추가해야 합니다. 이후 `@PreAuthorize`, `@Secured` 같은 어노테이션을 사용해 권한을 검사합니다.
|
||||
|
||||
##### 기본 설정
|
||||
```java
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
|
||||
@Configuration
|
||||
@EnableMethodSecurity
|
||||
public class SecurityConfig {
|
||||
// 다른 설정 (UserDetailsService, SecurityFilterChain 등) 생략
|
||||
}
|
||||
```
|
||||
`@EnableMethodSecurity`는 메서드 보안을 활성화하며, 기본적으로 `@PreAuthorize`, `@PostAuthorize`, `@Secured` 어노테이션을 지원합니다.
|
||||
|
||||
#### 5.4 @PreAuthorize와 @Secured 활용
|
||||
|
||||
스프링 시큐리티는 메서드 수준 보안을 위해 두 가지 대표적인 어노테이션을 제공합니다: **`@Secured`**와 **`@PreAuthorize`**. 두 어노테이션은 비슷한 목적을 가지지만, 사용법과 유연성에서 차이가 있습니다.
|
||||
|
||||
##### @Secured 어노테이션
|
||||
`@Secured`는 메서드에 접근할 수 있는 역할(Role)을 지정하는 간단한 어노테이션입니다. 주로 역할 기반 접근 제어에 사용되며, 설정이 직관적입니다.
|
||||
|
||||
###### 사용 예제
|
||||
```java
|
||||
import org.springframework.security.access.annotation.Secured;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class AdminService {
|
||||
|
||||
@Secured("ROLE_ADMIN")
|
||||
public String manageUsers() {
|
||||
return "User management page for admins only";
|
||||
}
|
||||
|
||||
@Secured({"ROLE_USER", "ROLE_ADMIN"})
|
||||
public String viewProfile() {
|
||||
return "Profile visible to users and admins";
|
||||
}
|
||||
}
|
||||
```
|
||||
- `@Secured("ROLE_ADMIN")`: `ROLE_ADMIN` 역할이 있는 사용자만 `manageUsers()` 메서드를 호출할 수 있습니다.
|
||||
- `@Secured({"ROLE_USER", "ROLE_ADMIN"})`: `ROLE_USER` 또는 `ROLE_ADMIN` 중 하나라도 가진 사용자가 `viewProfile()`를 호출할 수 있습니다.
|
||||
|
||||
###### 동작 원리
|
||||
1. 메서드 호출 시 스프링 시큐리티의 AOP(Aspect-Oriented Programming) 프록시가 개입합니다.
|
||||
2. 현재 인증된 사용자의 `GrantedAuthority` 목록을 확인해 지정된 역할이 있는지 검사합니다.
|
||||
3. 역할이 없으면 `AccessDeniedException`이 발생하고, 호출이 차단됩니다.
|
||||
|
||||
###### 한계
|
||||
- 역할만 지원하며, 권한(Authority)이나 복잡한 조건은 처리 불가.
|
||||
- SpEL(Spring Expression Language)을 지원하지 않아 유연성이 제한적.
|
||||
|
||||
##### @PreAuthorize 어노테이션
|
||||
`@PreAuthorize`는 `@Secured`보다 더 강력하고 유연한 어노테이션으로, SpEL을 사용해 복잡한 조건을 정의할 수 있습니다. 역할, 권한, 메서드 매개변수, 인증 객체 등을 기반으로 접근을 제어할 수 있습니다.
|
||||
|
||||
###### 사용 예제
|
||||
```java
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class PostService {
|
||||
|
||||
@PreAuthorize("hasRole('ADMIN')")
|
||||
public String deleteAllPosts() {
|
||||
return "All posts deleted by admin";
|
||||
}
|
||||
|
||||
@PreAuthorize("hasAuthority('WRITE_POST')")
|
||||
public String createPost(String content) {
|
||||
return "Post created: " + content;
|
||||
}
|
||||
|
||||
@PreAuthorize("#username == authentication.name")
|
||||
public String viewOwnProfile(String username) {
|
||||
return "Profile for " + username;
|
||||
}
|
||||
|
||||
@PreAuthorize("hasRole('USER') and #postId > 0")
|
||||
public String editPost(int postId, String content) {
|
||||
return "Post " + postId + " updated: " + content;
|
||||
}
|
||||
}
|
||||
```
|
||||
- **`hasRole('ADMIN')`**: `ROLE_ADMIN` 역할 검사. (`@Secured`와 유사하지만 SpEL 사용 가능)
|
||||
- **`hasAuthority('WRITE_POST')`**: `WRITE_POST` 권한 검사.
|
||||
- **`#username == authentication.name`**: 메서드 매개변수(`username`)가 현재 인증된 사용자의 이름과 같은지 확인.
|
||||
- **`hasRole('USER') and #postId > 0`**: `ROLE_USER` 역할이 있고, `postId`가 양수일 때만 허용.
|
||||
|
||||
###### 동작 원리
|
||||
1. `@PreAuthorize`는 메서드 실행 전에 SpEL 표현식을 평가합니다.
|
||||
2. 표현식이 `true`면 메서드가 실행되고, `false`면 `AccessDeniedException`이 발생합니다.
|
||||
3. `authentication` 객체(현재 사용자 정보)와 메서드 매개변수를 활용해 동적 조건을 검사합니다.
|
||||
|
||||
###### 장점
|
||||
- **유연성**: 역할, 권한, 매개변수 기반의 복잡한 로직 가능.
|
||||
- **조건문**: `and`, `or`, `not` 같은 연산자 사용 가능.
|
||||
- **커스터마이징**: SpEL을 통해 비즈니스 로직에 맞춘 조건 설정 가능.
|
||||
|
||||
##### @Secured vs @PreAuthorize 비교
|
||||
| 특징 | `@Secured` | `@PreAuthorize` |
|
||||
|-------------------|---------------------------|---------------------------|
|
||||
| 지원 범위 | 역할(Role)만 | 역할, 권한, SpEL 조건 |
|
||||
| 유연성 | 낮음 | 높음 |
|
||||
| 사용 난이도 | 간단 | 약간 복잡 |
|
||||
| 예제 | `@Secured("ROLE_ADMIN")` | `@PreAuthorize("hasRole('ADMIN') and #id > 0")` |
|
||||
|
||||
##### 실습 예제
|
||||
1. 컨트롤러에 `@Secured("ROLE_USER")`를 추가해 특정 엔드포인트 보호.
|
||||
2. 서비스 메서드에 `@PreAuthorize("#id == authentication.principal.username")`를 적용해 사용자 본인의 데이터만 수정 가능하도록 설정.
|
||||
3. `ROLE_ADMIN`과 `MANAGE_USERS` 권한을 가진 사용자로 로그인해 두 어노테이션의 동작 확인.
|
||||
|
||||
##### 주의사항
|
||||
- **성능**: 메서드 수준 보안은 AOP 프록시를 통해 동작하므로, 과도한 사용은 성능에 영향을 줄 수 있습니다.
|
||||
- **예외 처리**: `AccessDeniedException`을 적절히 핸들링해 사용자 친화적인 오류 메시지를 제공하세요.
|
||||
- **설정 활성화**: `@EnableMethodSecurity`가 없으면 어노테이션이 동작하지 않으니 반드시 추가하세요.
|
||||
|
||||
---
|
||||
|
||||
위 내용은 메서드 수준 보안의 필요성과 `@Secured`, `@PreAuthorize` 어노테이션의 활용 방법을 체계적으로 설명했습니다. 추가 예제나 특정 상황에 대한 설명이 필요하면 말씀해 주세요!
|
||||
209
docs/security/06_커스텀 로그인 페이지.md
Normal file
209
docs/security/06_커스텀 로그인 페이지.md
Normal file
@@ -0,0 +1,209 @@
|
||||
아래는 "스프링 시큐리티" 책의 6장에 포함될 "커스텀 로그인 페이지 제작", "로그아웃 기능 구현", 그리고 "세션 관리와 타임아웃 설정"에 대한 내용입니다. 실습 가능한 예제와 함께 실무에서 유용한 설정을 설명했습니다.
|
||||
|
||||
---
|
||||
|
||||
### 6장. 로그인/로그아웃 커스터마이징
|
||||
|
||||
#### 6.1 커스텀 로그인 페이지 제작
|
||||
|
||||
스프링 시큐리티의 기본 로그인 페이지는 간단한 테스트에는 유용하지만, 실무에서는 디자인과 기능을 사용자 맞춤으로 변경해야 합니다. **커스텀 로그인 페이지**를 제작하면 애플리케이션의 UI/UX를 개선하고, 추가적인 인증 로직을 통합할 수 있습니다.
|
||||
|
||||
##### 커스텀 로그인 페이지 설정
|
||||
1. **HTML 페이지 제작**: 먼저, 로그인 폼을 포함한 커스텀 페이지를 만듭니다. 예를 들어, `src/main/resources/templates/login.html`:
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Login</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>로그인</h2>
|
||||
<form method="post" action="/login">
|
||||
<div>
|
||||
<label>아이디</label>
|
||||
<input type="text" name="username" required>
|
||||
</div>
|
||||
<div>
|
||||
<label>비밀번호</label>
|
||||
<input type="password" name="password" required>
|
||||
</div>
|
||||
<button type="submit">로그인</button>
|
||||
</form>
|
||||
<p th:if="${param.error}" style="color:red">아이디 또는 비밀번호가 잘못되었습니다.</p>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
- `action="/login"`: 스프링 시큐리티의 기본 인증 엔드포인트.
|
||||
- `name="username"`, `name="password"`: 기본 필드 이름(변경 가능).
|
||||
- `${param.error}`: Thymeleaf를 사용해 로그인 실패 시 오류 메시지 표시.
|
||||
|
||||
2. **SecurityConfig 설정**:
|
||||
```java
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/login").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin(form -> form
|
||||
.loginPage("/login") // 커스텀 로그인 페이지 경로
|
||||
.defaultSuccessUrl("/home") // 로그인 성공 시 리다이렉트
|
||||
.permitAll() // 로그인 페이지 접근 허용
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
- `loginPage("/login")`: 기본 로그인 대신 커스텀 페이지를 사용.
|
||||
- `defaultSuccessUrl("/home")`: 인증 성공 후 이동할 경로.
|
||||
- `permitAll()`: 인증되지 않은 사용자도 로그인 페이지에 접근 가능.
|
||||
|
||||
3. **컨트롤러 추가**:
|
||||
```java
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class LoginController {
|
||||
|
||||
@GetMapping("/login")
|
||||
public String login() {
|
||||
return "login"; // login.html 반환
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### 동작 원리
|
||||
- 사용자가 보호된 리소스에 접근하면 `/login`으로 리다이렉트됩니다.
|
||||
- 커스텀 폼에서 입력된 `username`과 `password`는 POST 요청으로 `/login`에 전달됩니다.
|
||||
- `UsernamePasswordAuthenticationFilter`가 이를 처리해 인증을 수행합니다.
|
||||
|
||||
##### 추가 커스터마이징
|
||||
- **실패 처리**: `.failureUrl("/login?error")`를 추가해 실패 시 오류 메시지를 전달.
|
||||
- **필드 이름 변경**: `.usernameParameter("id")`, `.passwordParameter("pass")`로 기본 이름 변경 가능.
|
||||
|
||||
#### 6.3 로그아웃 기능 구현
|
||||
|
||||
로그아웃은 사용자의 세션을 종료하고 인증 상태를 해제하는 기능입니다. 스프링 시큐리티는 기본 로그아웃 설정을 제공하지만, 커스터마이징으로 사용자 경험을 개선할 수 있습니다.
|
||||
|
||||
##### 기본 로그아웃 설정
|
||||
```java
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
|
||||
.formLogin(form -> form.loginPage("/login").permitAll())
|
||||
.logout(logout -> logout
|
||||
.logoutUrl("/logout") // 로그아웃 요청 URL
|
||||
.logoutSuccessUrl("/login?logout") // 로그아웃 성공 시 리다이렉트
|
||||
.permitAll() // 로그아웃 접근 허용
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
- `logoutUrl("/logout")`: 기본 로그아웃 엔드포인트(POST 요청 필요).
|
||||
- `logoutSuccessUrl("/login?logout")`: 로그아웃 후 이동할 경로.
|
||||
|
||||
##### 로그아웃 버튼 추가
|
||||
`login.html`에 로그아웃 버튼을 추가하려면:
|
||||
```html
|
||||
<form method="post" action="/logout">
|
||||
<button type="submit">로그아웃</button>
|
||||
</form>
|
||||
```
|
||||
- POST 요청이어야 하며, CSRF 토큰이 필요합니다(기본적으로 활성화).
|
||||
|
||||
##### CSRF 비활성화 시 주의
|
||||
CSRF 보호를 비활성화하면 GET 요청으로도 로그아웃 가능:
|
||||
```java
|
||||
http
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.logout(logout -> logout.logoutUrl("/logout"));
|
||||
```
|
||||
이 경우 `<a href="/logout">로그아웃</a>`로 간단히 구현 가능하지만, 보안성이 낮아지므로 주의하세요.
|
||||
|
||||
##### 커스터마이징
|
||||
- **핸들러 추가**: `.addLogoutHandler()`로 커스텀 로그아웃 로직(예: 로그 기록) 추가.
|
||||
- **성공 핸들러**: `.logoutSuccessHandler()`로 리다이렉트 대신 JSON 응답 반환 가능.
|
||||
|
||||
#### 6.4 세션 관리와 타임아웃 설정
|
||||
|
||||
세션 관리는 사용자의 인증 상태를 유지하고, 세션 타임아웃을 통해 보안을 강화하는 데 중요합니다. 스프링 시큐리티는 세션 설정을 세밀하게 조정할 수 있는 옵션을 제공합니다.
|
||||
|
||||
##### 기본 세션 관리
|
||||
스프링 시큐리티는 인증 성공 시 `SecurityContext`를 세션에 저장합니다. `SecurityContextPersistenceFilter`가 이를 관리하며, 기본적으로 서블릿 컨테이너의 세션 설정을 따릅니다.
|
||||
|
||||
##### 세션 설정 예제
|
||||
```java
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
|
||||
.formLogin(form -> form.loginPage("/login").permitAll())
|
||||
.logout(logout -> logout.logoutUrl("/logout").logoutSuccessUrl("/login?logout"))
|
||||
.sessionManagement(session -> session
|
||||
.maximumSessions(1) // 최대 세션 수 제한 (1명만 로그인 가능)
|
||||
.maxSessionsPreventsLogin(true) // 중복 로그인 차단
|
||||
.expiredUrl("/login?expired") // 세션 만료 시 리다이렉트
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
- `maximumSessions(1)`: 한 사용자가 동시에 여러 세션을 가질 수 없도록 제한.
|
||||
- `maxSessionsPreventsLogin(true)`: 새 로그인 시 기존 세션을 무효화 대신 차단.
|
||||
- `expiredUrl("/login?expired")`: 세션 만료 후 이동 경로.
|
||||
|
||||
##### 타임아웃 설정
|
||||
세션 타임아웃은 서블릿 컨테이너 수준에서 설정하거나, 애플리케이션에서 조정 가능:
|
||||
1. **application.properties**:
|
||||
```properties
|
||||
server.servlet.session.timeout=1800 # 30분 (초 단위)
|
||||
```
|
||||
2. **코드로 설정**:
|
||||
```java
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.session.web.http.SessionRepositoryFilter;
|
||||
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
|
||||
|
||||
@Configuration
|
||||
public class SessionConfig {
|
||||
|
||||
@Bean
|
||||
public SessionRepositoryFilter<?> sessionRepositoryFilter(RedisIndexedSessionRepository sessionRepository) {
|
||||
sessionRepository.setDefaultMaxInactiveInterval(1800); // 30분
|
||||
return new SessionRepositoryFilter<>(sessionRepository);
|
||||
}
|
||||
}
|
||||
```
|
||||
Redis와 같은 외부 저장소를 사용하면 세션 지속성을 높일 수 있습니다.
|
||||
|
||||
##### 세션 관리 추가 옵션
|
||||
- **세션 고정 보호**: `.sessionFixation().migrateSession()`으로 세션 고정 공격 방지.
|
||||
- **무효화**: `.invalidSessionUrl("/login?invalid")`로 무효 세션 처리.
|
||||
- **동시 세션 모니터링**: `HttpSessionEventPublisher`를 추가해 세션 생성/소멸 이벤트 감지.
|
||||
|
||||
##### 실습 예제
|
||||
1. 커스텀 로그인 페이지를 만들고, 로그인 후 `/home`으로 이동.
|
||||
2. 로그아웃 버튼을 추가해 `/login?logout`으로 리다이렉트 확인.
|
||||
3. 세션 타임아웃을 1분으로 설정하고, 만료 후 동작 테스트.
|
||||
|
||||
---
|
||||
|
||||
위 내용은 커스텀 로그인/로그아웃 구현과 세션 관리 방법을 실습 가능하도록 설명했습니다. 추가적인 예제나 설정이 필요하면 말씀해 주세요!
|
||||
260
docs/security/07_소셜 로그인.md
Normal file
260
docs/security/07_소셜 로그인.md
Normal file
@@ -0,0 +1,260 @@
|
||||
아래는 "스프링 시큐리티" 책의 7장에 포함될 "OAuth2 기본 개념", "구글 등 소셜 로그인 구현", 그리고 "커스텀 OAuth2 클라이언트 설정"에 대한 내용입니다. 개념을 명확히 하고 실습 가능한 예제를 포함해 실무 적용성을 높였습니다.
|
||||
|
||||
---
|
||||
|
||||
### 7장. OAuth2와 소셜 로그인
|
||||
|
||||
#### 7.1 OAuth2 기본 개념
|
||||
|
||||
**OAuth2**는 인증(Authentication)과 권한 부여(Authorization)를 위한 표준 프로토콜로, 사용자가 자신의 자격 증명을 직접 공유하지 않고도 제3자 애플리케이션이 리소스에 접근할 수 있도록 합니다. 소셜 로그인(구글, 페이스북 등)이나 API 인증에 널리 사용됩니다.
|
||||
|
||||
##### OAuth2의 주요 구성 요소
|
||||
- **Resource Owner**: 리소스를 소유한 사용자(예: 구글 계정 소유자).
|
||||
- **Client**: 리소스에 접근하려는 애플리케이션(우리의 스프링 앱).
|
||||
- **Authorization Server**: 사용자를 인증하고 토큰을 발급하는 서버(예: 구글 인증 서버).
|
||||
- **Resource Server**: 보호된 리소스를 제공하는 서버(예: 구글 API).
|
||||
- **Access Token**: 클라이언트가 리소스에 접근할 때 사용하는 키.
|
||||
|
||||
##### 인증 흐름 (Authorization Code Grant)
|
||||
가장 흔히 사용되는 흐름으로, 소셜 로그인에 적합합니다:
|
||||
1. 사용자가 클라이언트에서 "구글로 로그인" 버튼을 클릭.
|
||||
2. 클라이언트가 사용자를 Authorization Server로 리다이렉트.
|
||||
3. 사용자가 로그인 후 권한을 승인하면 Authorization Code가 클라이언트로 반환.
|
||||
4. 클라이언트가 코드를 Access Token으로 교환.
|
||||
5. Access Token으로 Resource Server에서 사용자 정보를 가져옴.
|
||||
|
||||
스프링 시큐리티는 OAuth2를 쉽게 통합할 수 있도록 `spring-security-oauth2-client` 모듈을 제공하며, 최소한의 설정으로 소셜 로그인을 구현할 수 있습니다.
|
||||
|
||||
#### 7.3 구글, 깃허브 등 소셜 로그인 구현
|
||||
|
||||
스프링 시큐리티를 사용하면 구글, 깃허브 같은 소셜 로그인을 빠르게 구현할 수 있습니다. 여기서는 구글 로그인을 예로 설명합니다.
|
||||
|
||||
##### 1. 의존성 추가
|
||||
`pom.xml`에 다음 의존성을 추가:
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-oauth2-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
##### 2. 구글 OAuth2 클라이언트 등록
|
||||
1. [Google Cloud Console](https://console.cloud.google.com)에서 프로젝트 생성.
|
||||
2. "OAuth 2.0 클라이언트 ID" 생성:
|
||||
- 애플리케이션 유형: 웹 애플리케이션.
|
||||
- 리다이렉트 URI: `http://localhost:8080/login/oauth2/code/google`.
|
||||
3. 클라이언트 ID와 클라이언트 비밀번호(Secret)를 발급받음.
|
||||
|
||||
##### 3. 설정 파일 작성
|
||||
`application.yml`에 구글 OAuth2 설정 추가:
|
||||
```yaml
|
||||
spring:
|
||||
security:
|
||||
oauth2:
|
||||
client:
|
||||
registration:
|
||||
google:
|
||||
client-id: your-google-client-id
|
||||
client-secret: your-google-client-secret
|
||||
scope: profile, email
|
||||
provider:
|
||||
google:
|
||||
authorization-uri: https://accounts.google.com/o/oauth2/v2/auth
|
||||
token-uri: https://oauth2.googleapis.com/token
|
||||
user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo
|
||||
user-name-attribute: email
|
||||
```
|
||||
|
||||
##### 4. SecurityConfig 설정
|
||||
```java
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/", "/login").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.oauth2Login(oauth2 -> oauth2
|
||||
.loginPage("/login") // 커스텀 로그인 페이지
|
||||
.defaultSuccessUrl("/home") // 로그인 성공 시 이동
|
||||
)
|
||||
.logout(logout -> logout.logoutSuccessUrl("/"));
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### 5. 로그인 페이지 제작
|
||||
`src/main/resources/templates/login.html`:
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Login</title>
|
||||
</head>
|
||||
<body>
|
||||
<h2>로그인</h2>
|
||||
<a href="/oauth2/authorization/google">구글로 로그인</a>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
- `/oauth2/authorization/google`은 스프링 시큐리티가 자동 생성한 구글 로그인 경로.
|
||||
|
||||
##### 6. 사용자 정보 확인
|
||||
로그인 성공 후 `Principal` 객체로 사용자 정보를 가져올 수 있습니다:
|
||||
```java
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class HomeController {
|
||||
|
||||
@GetMapping("/home")
|
||||
public String home(@AuthenticationPrincipal OAuth2User oAuth2User, Model model) {
|
||||
model.addAttribute("name", oAuth2User.getAttribute("name"));
|
||||
model.addAttribute("email", oAuth2User.getAttribute("email"));
|
||||
return "home";
|
||||
}
|
||||
}
|
||||
```
|
||||
`home.html`:
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<h1>환영합니다, <span th:text="${name}"></span>!</h1>
|
||||
<p>이메일: <span th:text="${email}"></span></p>
|
||||
<a href="/logout">로그아웃</a>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
##### 깃허브 로그인 추가
|
||||
`application.yml`에 깃허브 설정 추가:
|
||||
```yaml
|
||||
spring:
|
||||
security:
|
||||
oauth2:
|
||||
client:
|
||||
registration:
|
||||
github:
|
||||
client-id: your-github-client-id
|
||||
client-secret: your-github-client-secret
|
||||
scope: read:user
|
||||
```
|
||||
로그인 링크: `<a href="/oauth2/authorization/github">깃허브로 로그인</a>`.
|
||||
|
||||
##### 동작 원리
|
||||
1. 사용자가 구글 로그인 링크를 클릭하면 구글 인증 서버로 리다이렉트.
|
||||
2. 구글에서 인증 후 리다이렉트 URI로 코드를 반환.
|
||||
3. 스프링 시큐리티가 코드를 토큰으로 교환하고, 사용자 정보를 가져와 `OAuth2User`로 저장.
|
||||
4. 인증 성공 시 `/home`으로 이동.
|
||||
|
||||
#### 7.4 커스텀 OAuth2 클라이언트 설정
|
||||
|
||||
스프링 시큐리티의 기본 설정으로 지원되지 않는 제공자(예: 네이버, 카카오)나 고급 요구사항을 처리하려면 커스텀 OAuth2 클라이언트를 설정해야 합니다.
|
||||
|
||||
##### 네이버 로그인 예제
|
||||
1. **네이버 개발자 센터**에서 클라이언트 ID와 Secret 발급.
|
||||
2. `application.yml`에 추가:
|
||||
```yaml
|
||||
spring:
|
||||
security:
|
||||
oauth2:
|
||||
client:
|
||||
registration:
|
||||
naver:
|
||||
client-id: your-naver-client-id
|
||||
client-secret: your-naver-client-secret
|
||||
redirect-uri: "{baseUrl}/login/oauth2/code/naver"
|
||||
authorization-grant-type: authorization_code
|
||||
scope: name, email
|
||||
provider:
|
||||
naver:
|
||||
authorization-uri: https://nid.naver.com/oauth2.0/authorize
|
||||
token-uri: https://nid.naver.com/oauth2.0/token
|
||||
user-info-uri: https://openapi.naver.com/v1/nid/me
|
||||
user-name-attribute: response # 네이버는 사용자 정보가 'response' 객체에 포함됨
|
||||
```
|
||||
|
||||
3. **커스텀 User Service**:
|
||||
네이버의 사용자 정보 형식이 구글과 다르므로 `OAuth2UserService`를 커스터마이징:
|
||||
```java
|
||||
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
|
||||
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
|
||||
import org.springframework.security.oauth2.core.user.OAuth2User;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
|
||||
|
||||
@Override
|
||||
public OAuth2User loadUser(OAuth2UserRequest userRequest) {
|
||||
OAuth2User oAuth2User = super.loadUser(userRequest);
|
||||
String registrationId = userRequest.getClientRegistration().getRegistrationId();
|
||||
|
||||
if ("naver".equals(registrationId)) {
|
||||
Map<String, Object> response = (Map<String, Object>) oAuth2User.getAttributes().get("response");
|
||||
return new org.springframework.security.oauth2.core.user.DefaultOAuth2User(
|
||||
oAuth2User.getAuthorities(),
|
||||
response, // 네이버의 사용자 정보
|
||||
"id" // 고유 식별자
|
||||
);
|
||||
}
|
||||
return oAuth2User;
|
||||
}
|
||||
}
|
||||
```
|
||||
4. **SecurityConfig에 등록**:
|
||||
```java
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomOAuth2UserService customOAuth2UserService) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
|
||||
.oauth2Login(oauth2 -> oauth2
|
||||
.loginPage("/login")
|
||||
.userInfoEndpoint(userInfo -> userInfo.userService(customOAuth2UserService))
|
||||
.defaultSuccessUrl("/home")
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### 커스터마이징 포인트
|
||||
- **토큰 처리**: `.tokenEndpoint()`로 커스텀 토큰 요청 설정.
|
||||
- **사용자 매핑**: 데이터베이스에 사용자 정보를 저장하거나, 추가 속성을 매핑.
|
||||
- **에러 처리**: `.failureHandler()`로 인증 실패 시 커스텀 로직 추가.
|
||||
|
||||
##### 실습 예제
|
||||
1. 네이버 로그인 버튼 추가: `<a href="/oauth2/authorization/naver">네이버로 로그인</a>`.
|
||||
2. 로그인 후 반환된 사용자 정보(이름, 이메일 등)를 화면에 출력.
|
||||
3. 데이터베이스에 신규 사용자를 등록하는 로직 추가.
|
||||
|
||||
---
|
||||
|
||||
위 내용은 OAuth2의 기본 개념과 구글 소셜 로그인 구현, 커스텀 OAuth2 클라이언트 설정 방법을 설명했습니다. 추가적인 설정이나 예제가 필요하면 말씀해 주세요!
|
||||
254
docs/security/08_JWT.md
Normal file
254
docs/security/08_JWT.md
Normal file
@@ -0,0 +1,254 @@
|
||||
아래는 "스프링 시큐리티" 책의 8장에 포함될 "JWT(Json Web Token) 개요", "스프링 시큐리티와 JWT 연동", 그리고 "Stateless 인증 설계"에 대한 내용입니다. 개념을 명확히 하고, 스프링부트에서의 실습 가능한 구현 방법을 포함했습니다.
|
||||
|
||||
---
|
||||
|
||||
### 8장. JWT와 토큰 기반 인증
|
||||
|
||||
#### 8.1 JWT(Json Web Token) 개요
|
||||
|
||||
**JWT(Json Web Token)**는 인증 및 권한 부여를 위해 사용되는 표준 토큰 형식으로, 클라이언트와 서버 간에 안전하게 정보를 전달합니다. 세션 기반 인증과 달리 서버가 상태를 유지하지 않는(stateless) 특성을 가지며, REST API와 같은 분산 환경에서 널리 사용됩니다.
|
||||
|
||||
##### JWT 구조
|
||||
JWT는 세 부분으로 구성되며, 점(`.`)으로 구분됩니다:
|
||||
1. **Header**: 토큰의 유형(보통 "JWT")과 서명 알고리즘(예: HMAC SHA256)을 정의.
|
||||
- 예: `{"alg": "HS256", "typ": "JWT"}`
|
||||
2. **Payload**: 사용자 정보(클레임, Claims)를 포함. 표준 클레임(예: `sub`, `exp`)과 커스텀 클레임으로 구성.
|
||||
- 예: `{"sub": "user123", "roles": ["USER"], "exp": 1698765432}`
|
||||
3. **Signature**: Header와 Payload를 비밀 키로 서명한 값으로, 무결성을 보장.
|
||||
- `HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)`
|
||||
|
||||
결과적으로 JWT는 `header.payload.signature` 형태로 인코딩됩니다(예: `eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIn0.signature`).
|
||||
|
||||
##### 장점과 단점
|
||||
- **장점**: 서버가 세션을 관리할 필요 없음, 확장성 좋음, 모바일 및 분산 시스템에 적합.
|
||||
- **단점**: 토큰 크기가 클 수 있음, 취소(Revocation)가 복잡, Payload는 암호화되지 않음(기밀 데이터 주의).
|
||||
|
||||
#### 8.2 스프링 시큐리티와 JWT 연동
|
||||
|
||||
스프링부트에서 JWT를 사용하려면 토큰 생성, 검증, 인증 필터를 구현해야 합니다. 여기서는 기본적인 JWT 인증 흐름을 설정합니다.
|
||||
|
||||
##### 1. 의존성 추가
|
||||
`pom.xml`에 JWT 라이브러리 추가:
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt</artifactId>
|
||||
<version>0.9.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
##### 2. JWT 유틸리티 클래스
|
||||
토큰 생성과 검증을 위한 유틸리티:
|
||||
```java
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.util.Date;
|
||||
|
||||
@Component
|
||||
public class JwtUtil {
|
||||
|
||||
private final String SECRET_KEY = "your-secret-key"; // 비밀 키 (환경 변수로 관리 권장)
|
||||
private final long EXPIRATION_TIME = 1000 * 60 * 60; // 1시간
|
||||
|
||||
public String generateToken(String username) {
|
||||
return Jwts.builder()
|
||||
.setSubject(username)
|
||||
.setIssuedAt(new Date())
|
||||
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
|
||||
.signWith(SignatureAlgorithm.HS256, SECRET_KEY.getBytes())
|
||||
.compact();
|
||||
}
|
||||
|
||||
public String extractUsername(String token) {
|
||||
return Jwts.parser()
|
||||
.setSigningKey(SECRET_KEY.getBytes())
|
||||
.parseClaimsJws(token)
|
||||
.getBody()
|
||||
.getSubject();
|
||||
}
|
||||
|
||||
public boolean validateToken(String token) {
|
||||
try {
|
||||
Jwts.parser().setSigningKey(SECRET_KEY.getBytes()).parseClaimsJws(token);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### 3. JWT 인증 필터
|
||||
HTTP 요청에서 JWT를 추출해 인증:
|
||||
```java
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import javax.servlet.FilterChain;
|
||||
import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
|
||||
@Component
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
public JwtAuthenticationFilter(JwtUtil jwtUtil) {
|
||||
this.jwtUtil = jwtUtil;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
String header = request.getHeader("Authorization");
|
||||
if (header != null && header.startsWith("Bearer ")) {
|
||||
String token = header.substring(7);
|
||||
if (jwtUtil.validateToken(token)) {
|
||||
String username = jwtUtil.extractUsername(token);
|
||||
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(username, null, null);
|
||||
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(auth);
|
||||
}
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### 4. SecurityConfig 설정
|
||||
```java
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
|
||||
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
|
||||
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/login").permitAll()
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### 5. 로그인 엔드포인트
|
||||
토큰 발급을 위한 컨트롤러:
|
||||
```java
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
public class AuthController {
|
||||
|
||||
private final JwtUtil jwtUtil;
|
||||
|
||||
public AuthController(JwtUtil jwtUtil) {
|
||||
this.jwtUtil = jwtUtil;
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public String login(@RequestBody LoginRequest request) {
|
||||
// 실제 인증 로직 (예: DB에서 사용자 확인)은 생략
|
||||
// 여기서는 단순히 username으로 토큰 생성
|
||||
return jwtUtil.generateToken(request.getUsername());
|
||||
}
|
||||
}
|
||||
|
||||
class LoginRequest {
|
||||
private String username;
|
||||
private String password;
|
||||
|
||||
// Getters, Setters
|
||||
public String getUsername() { return username; }
|
||||
public String getPassword() { return password; }
|
||||
public void setUsername(String username) { this.username = username; }
|
||||
public void setPassword(String password) { this.password = password; }
|
||||
}
|
||||
```
|
||||
|
||||
##### 동작 흐름
|
||||
1. 클라이언트가 `/login`에 POST 요청으로 사용자 이름과 비밀번호 전송.
|
||||
2. 서버가 JWT를 생성해 반환.
|
||||
3. 클라이언트가 후속 요청에 `Authorization: Bearer <token>` 헤더 포함.
|
||||
4. `JwtAuthenticationFilter`가 토큰을 검증하고 인증 설정.
|
||||
|
||||
#### 8.4 Stateless 인증 설계
|
||||
|
||||
**Stateless 인증**은 서버가 클라이언트의 상태(세션)를 유지하지 않고, 각 요청마다 독립적으로 인증을 처리하는 방식입니다. JWT는 이를 구현하는 데 이상적입니다.
|
||||
|
||||
##### Stateless 설계 특징
|
||||
- **세션 사용 안 함**: 서버는 세션 저장소를 유지하지 않음.
|
||||
- **토큰 기반**: 모든 인증 정보가 JWT에 포함되어 클라이언트가 관리.
|
||||
- **확장성**: 서버 부하가 줄어들고, 여러 서버 간 인증 공유가 쉬움.
|
||||
|
||||
##### Stateless 인증 구현
|
||||
1. **세션 비활성화**: `SecurityConfig`에서 `SessionCreationPolicy.STATELESS` 설정.
|
||||
```java
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
```
|
||||
2. **CSRF 비활성화**: 세션을 사용하지 않으므로 CSRF 토큰도 필요 없음.
|
||||
```java
|
||||
.csrf(csrf -> csrf.disable())
|
||||
```
|
||||
3. **토큰 검증**: 매 요청마다 `JwtAuthenticationFilter`가 토큰을 확인해 인증.
|
||||
|
||||
##### 고려 사항
|
||||
- **토큰 만료**: `EXPIRATION_TIME`을 적절히 설정(예: 1시간). 리프레시 토큰을 추가로 구현 가능.
|
||||
- **토큰 취소**: Stateless 환경에서는 토큰 무효화가 어려움. 블랙리스트를 유지하거나 짧은 만료 시간을 설정.
|
||||
- **보안**: 비밀 키를 안전하게 관리하고, HTTPS를 사용해 토큰 유출 방지.
|
||||
|
||||
##### 리프레시 토큰 추가 (선택)
|
||||
만료된 액세스 토큰을 갱신하려면:
|
||||
1. 로그인 시 액세스 토큰과 리프레시 토큰을 함께 발급.
|
||||
2. 리프레시 토큰은 긴 만료 시간(예: 7일) 설정.
|
||||
3. `/refresh` 엔드포인트에서 리프레시 토큰으로 새 액세스 토큰 발급.
|
||||
|
||||
```java
|
||||
@PostMapping("/refresh")
|
||||
public String refreshToken(@RequestBody String refreshToken) {
|
||||
if (jwtUtil.validateToken(refreshToken)) {
|
||||
String username = jwtUtil.extractUsername(refreshToken);
|
||||
return jwtUtil.generateToken(username); // 새 액세스 토큰 반환
|
||||
}
|
||||
throw new RuntimeException("Invalid refresh token");
|
||||
}
|
||||
```
|
||||
|
||||
##### 실습 예제
|
||||
1. `/login`으로 토큰 발급 후, 보호된 엔드포인트(`/home`)에 접근.
|
||||
2. 토큰 없이 요청 시 403 확인.
|
||||
3. 리프레시 토큰 로직을 추가해 토큰 갱신 테스트.
|
||||
|
||||
---
|
||||
|
||||
위 내용은 JWT의 개념, 스프링부트에서의 구현, 그리고 Stateless 인증 설계를 실습 가능하도록 설명했습니다. 추가적인 예제나 세부 사항이 필요하면 말씀해 주세요!
|
||||
203
docs/security/09_보안 강화.md
Normal file
203
docs/security/09_보안 강화.md
Normal file
@@ -0,0 +1,203 @@
|
||||
아래는 "스프링 시큐리티" 책의 9장에 포함될 "CSRF(Cross-Site Request Forgery) 방어", "XSS(Cross-Site Scripting)와 콘텐츠 보안 정책", 그리고 "보안 헤더 추가"에 대한 내용입니다. 각 보안 위협의 개념과 스프링부트에서의 대응 방법을 실습 가능하도록 설명했습니다.
|
||||
|
||||
---
|
||||
|
||||
### 9장. 보안 강화 기법
|
||||
|
||||
#### 9.1 CSRF(Cross-Site Request Forgery) 방어
|
||||
|
||||
**CSRF(Cross-Site Request Forgery)**는 사용자가 의도하지 않은 요청을 악의적인 웹사이트를 통해 서버로 전송하게 만드는 공격입니다. 예를 들어, 사용자가 로그인한 상태에서 악성 사이트의 링크를 클릭하면, 사용자의 인증 쿠키를 활용해 은행 계좌 이체 같은 요청이 실행될 수 있습니다.
|
||||
|
||||
##### CSRF 동작 원리
|
||||
- 공격자는 피해자가 로그인한 상태를 가정.
|
||||
- 피해자가 공격자의 사이트에서 숨겨진 폼(예: `<form action="http://bank.com/transfer" method="post">`)을 실행.
|
||||
- 브라우저가 자동으로 인증 쿠키를 포함해 요청 전송.
|
||||
|
||||
##### 스프링 시큐리티의 CSRF 보호
|
||||
스프링 시큐리티는 기본적으로 CSRF 보호를 활성화하며, POST, PUT, DELETE 같은 상태 변경 요청에 CSRF 토큰을 요구합니다.
|
||||
|
||||
###### 기본 설정
|
||||
```java
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.formLogin();
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
- CSRF 토큰은 `<input type="hidden" name="_csrf" value="token">` 형태로 폼에 자동 추가(Thymeleaf 사용 시).
|
||||
- 토큰은 세션마다 고유하며, 요청 시 서버가 이를 검증.
|
||||
|
||||
###### Thymeleaf에서 사용
|
||||
```html
|
||||
<form method="post" th:action="@{/update}">
|
||||
<input type="text" name="data">
|
||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||
<button type="submit">제출</button>
|
||||
</form>
|
||||
```
|
||||
|
||||
###### CSRF 비활성화
|
||||
Stateless 인증(JWT 등)을 사용할 때는 CSRF 보호가 필요 없을 수 있습니다:
|
||||
```java
|
||||
http
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
|
||||
```
|
||||
|
||||
##### 방어 팁
|
||||
- **GET 요청 제한**: 상태 변경은 POST로만 처리.
|
||||
- **토큰 확인**: 모든 변경 요청에 CSRF 토큰 포함.
|
||||
- **SameSite 쿠키**: 세션 쿠키에 `SameSite=Strict` 설정 추가(아래 보안 헤더 참조).
|
||||
|
||||
#### 9.2 XSS(Cross-Site Scripting)와 콘텐츠 보안 정책
|
||||
|
||||
**XSS(Cross-Site Scripting)**는 공격자가 웹 페이지에 악성 스크립트를 삽입해 사용자의 브라우저에서 실행시키는 공격입니다. 이를 통해 세션 쿠키를 탈취하거나 페이지를 조작할 수 있습니다.
|
||||
|
||||
##### XSS 유형
|
||||
- **Reflected XSS**: 악성 스크립트가 URL 파라미터 등으로 전달되어 즉시 실행.
|
||||
- **Stored XSS**: 악성 스크립트가 데이터베이스에 저장되어 모든 사용자에게 노출.
|
||||
- **DOM-based XSS**: 클라이언트 측 스크립트가 조작됨.
|
||||
|
||||
##### 스프링에서의 XSS 방어
|
||||
1. **입력 검증**: 사용자 입력을 철저히 검증하고 sanitization 적용.
|
||||
- `HtmlUtils.htmlEscape()`로 HTML 이스케이프:
|
||||
```java
|
||||
import org.springframework.web.util.HtmlUtils;
|
||||
String safeInput = HtmlUtils.htmlEscape(userInput);
|
||||
```
|
||||
2. **템플릿 엔진**: Thymeleaf는 기본적으로 출력값을 이스케이프해 XSS 방어.
|
||||
```html
|
||||
<p th:text="${userInput}"></p> <!-- 자동 이스케이프 -->
|
||||
<p th:utext="${userInput}"></p> <!-- 이스케이프 비활성화, 주의 필요 -->
|
||||
```
|
||||
|
||||
##### 콘텐츠 보안 정책 (CSP)
|
||||
**CSP(Content Security Policy)**는 브라우저가 허용된 소스에서만 리소스를 로드하도록 제한해 XSS를 방어합니다. HTTP 헤더로 설정합니다.
|
||||
|
||||
###### CSP 설정
|
||||
```java
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.header.writers.StaticHeadersWriter;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.headers(headers -> headers
|
||||
.addHeaderWriter(new StaticHeadersWriter("Content-Security-Policy",
|
||||
"default-src 'self'; script-src 'self' https://trusted.cdn.com;"))
|
||||
)
|
||||
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
|
||||
.formLogin();
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
- `default-src 'self'`: 기본적으로 같은 출처에서만 리소스 로드.
|
||||
- `script-src 'self' https://trusted.cdn.com`: 스크립트는 자체 및 신뢰된 CDN에서만 허용.
|
||||
|
||||
##### 방어 팁
|
||||
- **출력 이스케이프**: 모든 동적 콘텐츠를 이스케이프 처리.
|
||||
- **CSP 강화**: 외부 리소스 최소화 및 엄격한 정책 적용.
|
||||
- **입력 제한**: 허용된 문자만 허용(예: 정규식 사용).
|
||||
|
||||
#### 9.3 보안 헤더 추가
|
||||
|
||||
보안 헤더는 브라우저의 기본 보안 기능을 강화해 다양한 공격을 방지합니다. 스프링 시큐리티는 이를 쉽게 추가할 수 있는 설정을 제공합니다.
|
||||
|
||||
##### 주요 보안 헤더
|
||||
1. **X-Content-Type-Options**:
|
||||
- `nosniff`: MIME 타입 스니핑 방지.
|
||||
- 설정:
|
||||
```java
|
||||
http.headers(headers -> headers.contentTypeOptions());
|
||||
```
|
||||
|
||||
2. **X-Frame-Options**:
|
||||
- `DENY` 또는 `SAMEORIGIN`: 클릭재킹(Clickjacking) 방지.
|
||||
- 설정:
|
||||
```java
|
||||
http.headers(headers -> headers.frameOptions(frame -> frame.sameOrigin()));
|
||||
```
|
||||
|
||||
3. **X-XSS-Protection**:
|
||||
- `1; mode=block`: 브라우저의 XSS 필터 활성화(구형 브라우저 지원).
|
||||
- 설정:
|
||||
```java
|
||||
http.headers(headers -> headers.xssProtection());
|
||||
```
|
||||
|
||||
4. **Strict-Transport-Security (HSTS)**:
|
||||
- HTTPS 강제 적용 및 중간자 공격 방지.
|
||||
- 설정:
|
||||
```java
|
||||
http.headers(headers -> headers.httpStrictTransportSecurity(hsts -> hsts.maxAgeInSeconds(31536000)));
|
||||
```
|
||||
|
||||
5. **Content-Security-Policy**: 위 CSP 섹션 참조.
|
||||
|
||||
6. **Referrer-Policy**:
|
||||
- 참조 정보 제한(예: `no-referrer-when-downgrade`).
|
||||
- 설정:
|
||||
```java
|
||||
http.headers(headers -> headers.referrerPolicy(referrer -> referrer.policy(ReferrerPolicy.NO_REFERRER_WHEN_DOWNGRADE)));
|
||||
```
|
||||
|
||||
##### 종합 설정 예제
|
||||
```java
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.headers(headers -> headers
|
||||
.contentTypeOptions()
|
||||
.frameOptions(frame -> frame.deny())
|
||||
.xssProtection()
|
||||
.httpStrictTransportSecurity(hsts -> hsts.maxAgeInSeconds(31536000).includeSubDomains(true))
|
||||
.addHeaderWriter(new StaticHeadersWriter("Referrer-Policy", "strict-origin-when-cross-origin"))
|
||||
.addHeaderWriter(new StaticHeadersWriter("Content-Security-Policy", "default-src 'self'"))
|
||||
)
|
||||
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
|
||||
.formLogin();
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### SameSite 쿠키 설정
|
||||
CSRF 방어를 강화하려면 세션 쿠키에 `SameSite` 속성 추가:
|
||||
```properties
|
||||
# application.properties
|
||||
server.servlet.session.cookie.same-site=strict
|
||||
```
|
||||
- `Strict`: 타사 사이트에서 쿠키 전송 차단.
|
||||
- `Lax`: GET 요청은 허용, POST 등은 차단.
|
||||
|
||||
##### 실습 예제
|
||||
1. CSRF 토큰 없이 POST 요청 시 403 확인.
|
||||
2. XSS 공격 시도(예: `<script>alert('xss')</script>`) 후 이스케이프 동작 확인.
|
||||
3. 브라우저 개발자 도구에서 보안 헤더 적용 여부 점검.
|
||||
|
||||
---
|
||||
|
||||
위 내용은 CSRF, XSS, 보안 헤더의 개념과 스프링 시큐리티에서의 방어 방법을 실습 가능하도록 설명했습니다. 추가적인 예제나 세부 사항이 필요하면 말씀해 주세요!
|
||||
141
docs/security/11_csrf.md
Normal file
141
docs/security/11_csrf.md
Normal file
@@ -0,0 +1,141 @@
|
||||
스프링 부트에서 CSRF 설정을 URL 기반으로 세밀하게 제어하는 방법은 주로 `HttpSecurity` 설정을 통해 이루어집니다. 특정 URL 패턴에 대해서만 CSRF 보호를 활성화하거나 비활성화할 수 있습니다.
|
||||
|
||||
**핵심 아이디어:**
|
||||
|
||||
`HttpSecurity`의 `csrf()` 메서드를 통해 CSRF 설정을 시작하고, `ignoringAntMatchers()`, `requireCsrfProtectionMatcher()` 등의 메서드를 사용하여 특정 URL 패턴을 설정합니다.
|
||||
|
||||
**설정 방법:**
|
||||
|
||||
`WebSecurityConfigurerAdapter`를 상속받는 설정 클래스 또는 `@Bean`으로 `SecurityFilterChain`을 정의하는 방식에서 `HttpSecurity`를 설정합니다.
|
||||
|
||||
**1. 특정 URL 패턴에 대해 CSRF 보호 비활성화:**
|
||||
|
||||
```java
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(csrf -> csrf
|
||||
.ignoringRequestMatchers(
|
||||
new AntPathRequestMatcher("/api/public/**"), // /api/public/으로 시작하는 모든 요청에 대해 CSRF 비활성화
|
||||
new AntPathRequestMatcher("/h2-console/**") // H2 콘솔 접근 시 CSRF 비활성화 (개발 환경)
|
||||
// 추가적인 URL 패턴 설정 가능
|
||||
)
|
||||
)
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.anyRequest().permitAll() // 예시: 모든 요청 허용
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* `.csrf(csrf -> ...)`: CSRF 설정을 시작합니다.
|
||||
* `.ignoringRequestMatchers(...)`: 주어진 `RequestMatcher`와 일치하는 요청에 대해서는 CSRF 보호를 비활성화합니다.
|
||||
* `new AntPathRequestMatcher("/api/public/**")`: Ant 스타일의 URL 패턴을 사용하여 `/api/public/`으로 시작하는 모든 경로를 매칭합니다.
|
||||
* `new AntPathRequestMatcher("/h2-console/**")`: H2 콘솔 경로는 일반적으로 개발 환경에서 사용되므로 CSRF 보호를 비활성화하는 경우가 많습니다.
|
||||
* 여러 개의 `ignoringRequestMatchers()`를 사용하여 다양한 URL 패턴을 설정할 수 있습니다.
|
||||
|
||||
**2. 특정 HTTP 메서드에 대해서만 CSRF 보호 비활성화:**
|
||||
|
||||
특정 URL 패턴에 대해 특정 HTTP 메서드(예: GET 요청)에 대해서만 CSRF 보호를 비활성화하고, 데이터를 변경하는 요청(POST, PUT, DELETE)에는 CSRF 보호를 유지할 수 있습니다.
|
||||
|
||||
```java
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
List<RequestMatcher> withoutCsrf = Arrays.asList(
|
||||
new AntPathRequestMatcher("/api/public/**", "GET"), // /api/public/으로 시작하는 GET 요청에 대해 CSRF 비활성화
|
||||
new AntPathRequestMatcher("/some/readonly/path", "GET")
|
||||
// 추가적인 GET 요청 패턴 설정 가능
|
||||
);
|
||||
|
||||
http
|
||||
.csrf(csrf -> csrf
|
||||
.ignoringRequestMatchers(withoutCsrf.toArray(new RequestMatcher[0]))
|
||||
)
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.anyRequest().permitAll() // 예시: 모든 요청 허용
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* `new AntPathRequestMatcher("/api/public/**", "GET")`: `/api/public/`으로 시작하고 HTTP 메서드가 `GET`인 요청만 매칭합니다.
|
||||
|
||||
**3. 특정 URL 패턴에 대해서만 CSRF 보호 활성화 (기본 설정 외 추가):**
|
||||
|
||||
기본적으로 CSRF 보호는 활성화되어 있지만, 특정 URL 패턴에 대해서만 명시적으로 활성화하거나, `ignoringRequestMatchers()`로 제외했던 패턴 중 일부에 대해 다시 활성화할 수 있습니다. (일반적인 사용 사례는 아닐 수 있습니다.)
|
||||
|
||||
```java
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
import org.springframework.security.web.util.matcher.RequestMatcher;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
List<RequestMatcher> requireCsrf = Arrays.asList(
|
||||
new AntPathRequestMatcher("/admin/**") // /admin/으로 시작하는 요청에 대해 CSRF 활성화 (명시적)
|
||||
// 추가적인 활성화 패턴 설정 가능
|
||||
);
|
||||
|
||||
http
|
||||
.csrf(csrf -> csrf
|
||||
// .ignoringRequestMatchers(...) // 특정 패턴 비활성화 설정이 있을 경우
|
||||
.requireCsrfProtectionMatcher(request -> {
|
||||
// 기본적으로 POST, PUT, DELETE 요청에 대해 CSRF 보호를 적용하고,
|
||||
// requireCsrf 리스트에 있는 패턴에 대해서도 적용
|
||||
boolean defaultMatch = !request.getMethod().equalsIgnoreCase("GET");
|
||||
return defaultMatch || requireCsrf.stream().anyMatch(matcher -> matcher.matches(request));
|
||||
})
|
||||
)
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.anyRequest().permitAll() // 예시: 모든 요청 허용
|
||||
);
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
* `.requireCsrfProtectionMatcher(RequestMatcher)`: CSRF 보호를 적용할 요청을 결정하는 `RequestMatcher`를 설정합니다.
|
||||
* 위 예시에서는 기본적으로 GET 요청이 아닌 경우 CSRF 보호를 적용하고, 추가적으로 `/admin/**` 패턴과 일치하는 모든 요청에 대해 CSRF 보호를 적용합니다.
|
||||
|
||||
**주의 사항:**
|
||||
|
||||
* CSRF 보호를 특정 URL에 대해 비활성화하는 것은 보안 위험을 증가시킬 수 있습니다. 신중하게 결정해야 하며, 정말로 CSRF 보호가 불필요한 경우에만 적용해야 합니다.
|
||||
* RESTful API를 개발하는 경우, Stateless 특성을 유지하기 위해 CSRF 대신 다른 보안 메커니즘(예: JWT 기반 인증)을 사용하는 것을 고려할 수 있습니다. 이 경우 CSRF를 비활성화할 수 있습니다.
|
||||
* H2 콘솔과 같은 개발 도구에 대한 CSRF 비활성화는 개발 환경에서 편의를 위해 사용될 수 있지만, 프로덕션 환경에서는 보안을 위해 접근 제어를 설정하는 것이 좋습니다.
|
||||
|
||||
**결론:**
|
||||
|
||||
스프링 부트에서 URL 기반으로 CSRF 설정을 제어하려면 `HttpSecurity` 설정을 활용하여 `ignoringRequestMatchers()` 또는 `requireCsrfProtectionMatcher()` 메서드를 사용하여 원하는 URL 패턴에 따라 CSRF 보호를 활성화하거나 비활성화할 수 있습니다. 보안상의 영향을 충분히 고려하여 설정을 적용해야 합니다.
|
||||
86
docs/security/README.md
Normal file
86
docs/security/README.md
Normal file
@@ -0,0 +1,86 @@
|
||||
스프링부트의 Security에 관한 책의 목차를 작성해 드리겠습니다. 이 목차는 초보자부터 중급 개발자까지 이해할 수 있도록 기본 개념부터 실무 활용까지 다루며, 실습 예제와 함께 체계적으로 구성했습니다. 아래는 제안하는 목차입니다:
|
||||
|
||||
---
|
||||
|
||||
### 목차
|
||||
|
||||
#### 1장. 스프링 시큐리티 소개
|
||||
- 1.1 스프링 시큐리티란?
|
||||
- 1.2 스프링부트와 시큐리티의 통합
|
||||
- 1.3 보안의 중요성과 기본 개념
|
||||
- 1.4 이 책의 목표와 구성
|
||||
|
||||
#### 2장. 개발 환경 설정
|
||||
- 2.1 스프링부트 프로젝트 생성
|
||||
- 2.2 스프링 시큐리티 의존성 추가
|
||||
- 2.3 기본 설정 확인 및 테스트
|
||||
- 2.4 개발 도구와 환경 준비
|
||||
|
||||
#### 3장. 스프링 시큐리티 기본 동작 이해
|
||||
- 3.1 인증(Authentication)과 인가(Authorization)
|
||||
- 3.2 기본 인증 메커니즘
|
||||
- 3.3 Security Filter Chain의 구조
|
||||
- 3.4 기본 로그인 페이지와 동작 원리
|
||||
|
||||
#### 4장. 사용자 인증 구현
|
||||
- 4.1 In-Memory 인증 설정
|
||||
- 4.2 데이터베이스 연동을 통한 사용자 인증
|
||||
- 4.3 커스텀 UserDetailsService 구현
|
||||
- 4.4 비밀번호 암호화와 보안
|
||||
|
||||
#### 5장. 인가와 권한 관리
|
||||
- 5.1 역할(Role)과 권한(Authority)의 차이
|
||||
- 5.2 URL 기반 접근 제어
|
||||
- 5.3 메서드 수준 보안 설정
|
||||
- 5.4 @PreAuthorize와 @Secured 활용
|
||||
|
||||
#### 6장. 로그인/로그아웃 커스터마이징
|
||||
- 6.1 커스텀 로그인 페이지 제작
|
||||
- 6.2 로그인 성공/실패 처리
|
||||
- 6.3 로그아웃 기능 구현
|
||||
- 6.4 세션 관리와 타임아웃 설정
|
||||
|
||||
#### 7장. OAuth2와 소셜 로그인
|
||||
- 7.1 OAuth2 기본 개념
|
||||
- 7.2 스프링 시큐리티와 OAuth2 통합
|
||||
- 7.3 구글, 깃허브 등 소셜 로그인 구현
|
||||
- 7.4 커스텀 OAuth2 클라이언트 설정
|
||||
|
||||
#### 8장. JWT와 토큰 기반 인증
|
||||
- 8.1 JWT(Json Web Token) 개요
|
||||
- 8.2 스프링 시큐리티와 JWT 연동
|
||||
- 8.3 토큰 발급 및 검증 구현
|
||||
- 8.4 stateless 인증 설계
|
||||
|
||||
#### 9장. 보안 강화 기법
|
||||
- 9.1 CSRF(Cross-Site Request Forgery) 방어
|
||||
- 9.2 XSS(Cross-Site Scripting)와 콘텐츠 보안 정책
|
||||
- 9.3 HTTPS 설정과 SSL/TLS 적용
|
||||
- 9.4 보안 헤더 추가
|
||||
|
||||
#### 10장. 실무에서의 스프링 시큐리티
|
||||
- 10.1 REST API 보안 설계
|
||||
- 10.2 프론트엔드와 백엔드 통합
|
||||
- 10.3 에러 처리와 예외 관리
|
||||
- 10.4 배포 시 보안 점검 항목
|
||||
|
||||
#### 11장. 테스트와 디버깅
|
||||
- 11.1 스프링 시큐리티 테스트 환경 구축
|
||||
- 11.2 MockMvc를 활용한 보안 테스트
|
||||
- 11.3 일반적인 문제 해결과 디버깅 팁
|
||||
|
||||
#### 12장. 프로젝트 실습
|
||||
- 12.1 간단한 웹 애플리케이션 보안 구현
|
||||
- 12.2 JWT 기반 REST API 구축
|
||||
- 12.3 소셜 로그인 통합 프로젝트
|
||||
- 12.4 종합 예제: 회원 관리 시스템
|
||||
|
||||
#### 부록
|
||||
- A. 스프링 시큐리티 설정 참고 자료
|
||||
- B. 자주 사용하는 보안 관련 라이브러리
|
||||
- C. 용어 정리
|
||||
- D. 추가 학습 리소스
|
||||
|
||||
---
|
||||
|
||||
이 목차는 스프링 시큐리티의 기초부터 실무 적용까지 폭넓게 다루며, 독자가 단계적으로 학습하고 실습할 수 있도록 설계되었습니다. 필요에 따라 특정 주제(예: 클라우드 환경에서의 보안, 마이크로서비스 보안)를 추가하거나 세부 항목을 조정할 수 있습니다. 의견이 있으면 말씀해 주세요!
|
||||
74
docs/test/01_스프링 부트 테스트.md
Normal file
74
docs/test/01_스프링 부트 테스트.md
Normal file
@@ -0,0 +1,74 @@
|
||||
아래는 **1장. 스프링 부트 테스트 개요**에 대한 글입니다. 이 장은 책의 도입부로서 독자가 스프링 부트 테스트의 중요성과 기본 개념을 이해할 수 있도록 자연스럽고 명확하게 작성했습니다.
|
||||
|
||||
---
|
||||
|
||||
## 1장. 스프링 부트 테스트 개요
|
||||
|
||||
스프링 부트는 현대적인 자바 애플리케이션 개발을 위한 강력한 프레임워크로, 빠른 설정과 생산성을 높이는 다양한 기능을 제공합니다. 하지만 애플리케이션이 복잡해질수록 코드의 안정성과 품질을 보장하는 일이 점점 더 중요해집니다. 여기서 테스트가 핵심적인 역할을 합니다. 이 장에서는 스프링 부트에서의 테스트가 왜 중요한지, 어떤 도구와 환경을 제공하는지, 그리고 이를 시작하기 위한 기초를 다룹니다.
|
||||
|
||||
### 1.1 테스트의 중요성과 스프링 부트에서의 역할
|
||||
|
||||
소프트웨어 개발에서 테스트는 단순히 버그를 찾는 도구가 아닙니다. 테스트는 코드가 의도한 대로 동작하는지 확인하고, 리팩토링이나 기능 추가 시 기존 로직이 깨지지 않도록 보호하며, 개발자에게 자신감을 심어주는 안전망입니다. 스프링 부트는 애플리케이션 개발 속도를 높이는 데 초점이 맞춰져 있지만, 이는 테스트 없이는 지속 가능하지 않습니다. 예를 들어, 컨트롤러가 REST API를 제대로 처리하는지, 서비스 계층이 비즈니스 로직을 정확히 수행하는지, 데이터베이스 쿼리가 예상대로 동작하는지를 확인하려면 체계적인 테스트가 필수적입니다.
|
||||
|
||||
스프링 부트는 이러한 테스트를 쉽게 작성할 수 있도록 설계되었습니다. 내장된 테스트 지원과 자동 설정 덕분에 개발자는 최소한의 노력으로 단위 테스트부터 통합 테스트까지 구현할 수 있습니다. 테스트가 잘 작성된 프로젝트는 유지보수성과 확장성이 높아지며, 팀 간 협업에서도 신뢰를 더해줍니다.
|
||||
|
||||
### 1.2 스프링 부트 테스트 프레임워크 소개
|
||||
|
||||
스프링 부트는 테스트를 위해 풍부한 도구와 라이브러리를 제공합니다. 기본적으로 `spring-boot-starter-test` 의존성을 추가하면 JUnit, AssertJ, Mockito, Spring Test 등 강력한 테스트 도구가 함께 포함됩니다. 이들은 각각의 역할을 수행하며 테스트 작성을 단순화합니다.
|
||||
|
||||
- **JUnit**: 자바 개발의 표준 테스트 프레임워크로, 테스트 케이스를 정의하고 실행하는 데 사용됩니다.
|
||||
- **Spring Test**: 스프링 컨텍스트를 로드하고 애플리케이션 환경을 시뮬레이션해 통합 테스트를 지원합니다.
|
||||
- **Mockito**: 의존성을 모킹(mock)하여 단위 테스트에서 외부 요소를 배제하고 핵심 로직만 검증할 수 있게 합니다.
|
||||
- **AssertJ**: 가독성 높은 검증 코드를 작성할 수 있는 유연한 라이브러리입니다.
|
||||
|
||||
이 외에도 스프링 부트는 REST API 테스트를 위한 `TestRestTemplate`이나 `WebTestClient`, 인메모리 데이터베이스(H2) 지원 등 실무에서 바로 활용 가능한 기능을 제공합니다. 이 책에서는 이러한 도구들을 어떻게 사용하는지 하나씩 살펴볼 것입니다.
|
||||
|
||||
### 1.3 테스트 환경 설정 기초
|
||||
|
||||
스프링 부트에서 테스트를 시작하려면 먼저 환경을 설정해야 합니다. 가장 간단한 방법은 Maven이나 Gradle 프로젝트에 `spring-boot-starter-test` 의존성을 추가하는 것입니다. 예를 들어, Maven의 경우 `pom.xml`에 다음과 같이 추가하면 됩니다:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
의존성이 추가되면 기본적인 테스트 클래스를 작성할 수 있습니다. 스프링 부트는 `@SpringBootTest`와 같은 애너테이션을 통해 테스트 환경을 손쉽게 구성할 수 있게 해줍니다. 예를 들어, 간단한 테스트 코드는 다음과 같습니다:
|
||||
|
||||
```java
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class MyApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
// 스프링 컨텍스트가 제대로 로드되는지 확인
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
이 코드는 애플리케이션 컨텍스트가 문제없이 로드되는지 확인하는 기본 테스트입니다. 이처럼 스프링 부트는 최소한의 설정으로 테스트를 시작할 수 있는 기반을 제공합니다.
|
||||
|
||||
### 1.4 JUnit과 스프링 부트의 통합
|
||||
|
||||
JUnit은 스프링 부트 테스트의 핵심 엔진입니다. 최신 버전인 JUnit 5는 모듈화된 구조와 강력한 기능을 제공하며, 스프링 부트와 자연스럽게 통합됩니다. JUnit 5는 크게 세 가지 모듈로 나뉩니다:
|
||||
|
||||
- **JUnit Platform**: 테스트 실행을 위한 기반 플랫폼.
|
||||
- **JUnit Jupiter**: 테스트 작성과 새로운 기능을 제공하는 엔진.
|
||||
- **JUnit Vintage**: JUnit 4와의 호환성을 지원.
|
||||
|
||||
스프링 부트는 기본적으로 JUnit Jupiter를 사용하며, `@Test`, `@BeforeEach`, `@AfterEach`와 같은 애너테이션으로 테스트 생명주기를 관리합니다. 또한, 스프링 부트의 `@SpringBootTest`와 결합하면 애플리케이션 컨텍스트를 활용한 통합 테스트도 쉽게 작성할 수 있습니다.
|
||||
|
||||
---
|
||||
|
||||
### 마무리
|
||||
|
||||
이 장에서는 스프링 부트 테스트의 중요성과 기본적인 도구, 설정 방법을 살펴보았습니다. 테스트는 단순한 선택이 아니라, 안정적이고 유지보수 가능한 애플리케이션을 위한 필수 요소입니다. 다음 장부터는 단위 테스트 작성법을 시작으로 구체적인 예제와 함께 테스트 기술을 깊이 파고들어 보겠습니다. 스프링 부트의 테스트 기능을 익히며, 여러분의 코드에 자신감을 더해보세요!
|
||||
|
||||
---
|
||||
|
||||
이 글은 독자에게 친근하게 다가가면서도 기술적인 내용을 명확히 전달하는 데 초점을 맞췄습니다. 추가로 보완하거나 수정하고 싶은 부분이 있다면 말씀해 주세요!
|
||||
158
docs/test/02_단위 테스트.md
Normal file
158
docs/test/02_단위 테스트.md
Normal file
@@ -0,0 +1,158 @@
|
||||
아래는 **2장. 단위 테스트 작성**에 대한 글입니다. 이 장은 스프링 부트에서 단위 테스트의 개념을 설명하고, 실습 가능한 예제와 함께 구체적인 작성 방법을 다룹니다. 초보자와 중급 개발자 모두 이해할 수 있도록 단계적으로 접근했습니다.
|
||||
|
||||
---
|
||||
|
||||
## 2장. 단위 테스트 작성
|
||||
|
||||
단위 테스트는 애플리케이션의 개별 구성 요소가 독립적으로 올바르게 동작하는지 확인하는 과정입니다. 스프링 부트에서는 단위 테스트를 통해 컨트롤러, 서비스, 리포지토리와 같은 각 계층의 로직을 검증할 수 있습니다. 이 장에서는 단위 테스트의 정의와 중요성, 스프링 부트에서의 설정 방법, 그리고 실습 예제를 다룹니다.
|
||||
|
||||
### 2.1 단위 테스트란 무엇인가
|
||||
|
||||
단위 테스트(Unit Test)는 코드의 가장 작은 단위—보통 메서드나 클래스—를 독립적으로 테스트하는 것을 의미합니다. 외부 의존성(데이터베이스, 외부 API 등)에 의존하지 않고, 해당 단위가 기대한 대로 동작하는지 확인하는 데 초점이 맞춰져 있습니다. 단위 테스트의 주요 장점은 다음과 같습니다:
|
||||
|
||||
- **빠른 피드백**: 실행 속도가 빠르며, 문제를 조기에 발견할 수 있습니다.
|
||||
- **리팩토링 안전성**: 코드 변경 시 기존 기능이 깨지지 않음을 보장합니다.
|
||||
- **문서화**: 테스트 코드는 코드의 사용법과 의도를 보여주는 살아있는 문서 역할을 합니다.
|
||||
|
||||
스프링 부트에서는 단위 테스트를 작성할 때 의존성 주입이나 스프링 컨텍스트를 최소화하고, 필요 시 모킹(mock)을 활용해 독립성을 유지합니다.
|
||||
|
||||
### 2.2 스프링 부트에서 단위 테스트 설정
|
||||
|
||||
스프링 부트에서 단위 테스트를 시작하려면 기본적으로 `spring-boot-starter-test` 의존성이 필요합니다(1장에서 설정 방법 참고). 단위 테스트는 스프링 컨텍스트를 로드하지 않으므로 `@SpringBootTest` 대신 JUnit의 기본 애너테이션만 사용하거나, 필요한 경우 특정 빈을 모킹합니다.
|
||||
|
||||
예를 들어, 간단한 서비스 클래스를 테스트한다고 가정해 봅시다. 먼저 테스트 클래스를 다음과 같이 설정합니다:
|
||||
|
||||
```java
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class MyServiceTest {
|
||||
|
||||
@Test
|
||||
void simpleTest() {
|
||||
MyService service = new MyService();
|
||||
String result = service.sayHello("World");
|
||||
assertThat(result).isEqualTo("Hello, World!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
위 코드는 스프링 없이 순수 자바 객체를 테스트하는 예제입니다. 하지만 스프링 부트 애플리케이션에서는 의존성이 있는 경우가 많으므로, 이를 모킹하는 방법을 배워야 합니다.
|
||||
|
||||
### 2.3 Mockito를 활용한 모킹
|
||||
|
||||
실제 애플리케이션에서는 서비스가 리포지토리나 외부 API에 의존하는 경우가 많습니다. 이런 의존성을 제거하고 단위 테스트를 독립적으로 실행하려면 **Mockito**를 사용합니다. Mockito는 가짜 객체(mock)를 만들어 의존성을 대체하며, 특정 동작을 시뮬레이션할 수 있게 해줍니다.
|
||||
|
||||
예를 들어, `UserService`가 `UserRepository`에 의존한다고 가정해 봅시다:
|
||||
|
||||
```java
|
||||
@Service
|
||||
public class UserService {
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public UserService(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
public String getUserName(Long id) {
|
||||
User user = userRepository.findById(id);
|
||||
return user != null ? user.getName() : "Unknown";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
이를 테스트하려면 Mockito를 사용해 `UserRepository`를 모킹합니다:
|
||||
|
||||
```java
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@ExtendWith(MockitoExtension.class) // Mockito를 JUnit 5와 통합
|
||||
class UserServiceTest {
|
||||
|
||||
@Mock
|
||||
private UserRepository userRepository;
|
||||
|
||||
@InjectMocks
|
||||
private UserService userService;
|
||||
|
||||
@Test
|
||||
void getUserName_withExistingUser_returnsName() {
|
||||
// 모킹 설정: ID가 1인 경우 "Alice"라는 사용자를 반환
|
||||
when(userRepository.findById(1L)).thenReturn(new User(1L, "Alice"));
|
||||
|
||||
String result = userService.getUserName(1L);
|
||||
assertThat(result).isEqualTo("Alice");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getUserName_withNonExistingUser_returnsUnknown() {
|
||||
// 모킹 설정: ID가 2인 경우 null 반환
|
||||
when(userRepository.findById(2L)).thenReturn(null);
|
||||
|
||||
String result = userService.getUserName(2L);
|
||||
assertThat(result).isEqualTo("Unknown");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`@Mock`으로 가짜 객체를 만들고, `@InjectMocks`로 이를 주입받는 `UserService`를 테스트합니다. `when().thenReturn()`을 사용해 모킹된 객체의 동작을 정의합니다.
|
||||
|
||||
### 2.4 컨트롤러, 서비스, 리포지토리 단위 테스트 예제
|
||||
|
||||
이제 각 계층별로 단위 테스트를 작성하는 방법을 살펴보겠습니다.
|
||||
|
||||
#### 컨트롤러 단위 테스트
|
||||
컨트롤러는 HTTP 요청을 처리하므로, `@WebMvcTest`를 사용해 MVC 계층만 로드하고 의존성을 모킹합니다:
|
||||
|
||||
```java
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import static org.mockito.Mockito.when;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
|
||||
@WebMvcTest(UserController.class)
|
||||
class UserControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@MockBean
|
||||
private UserService userService;
|
||||
|
||||
@Test
|
||||
void getUserName_returnsName() throws Exception {
|
||||
when(userService.getUserName(1L)).thenReturn("Alice");
|
||||
|
||||
mockMvc.perform(get("/users/1/name"))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(content().string("Alice"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 서비스 단위 테스트
|
||||
위의 `UserServiceTest` 예제를 참고하세요. 서비스는 비즈니스 로직을 테스트하며, 의존성을 모킹해 독립적으로 검증합니다.
|
||||
|
||||
#### 리포지토리 단위 테스트
|
||||
리포지토리는 데이터베이스와 직접 상호작용하므로, 단위 테스트에서는 구현체 대신 인터페이스를 모킹하거나, 간단한 메모리 기반 구현을 사용할 수 있습니다. 통합 테스트에서 실제 DB를 다루는 경우가 많으므로, 여기서는 간단히 생략하고 3장에서 다룹니다.
|
||||
|
||||
---
|
||||
|
||||
### 마무리
|
||||
|
||||
단위 테스트는 스프링 부트 애플리케이션의 각 부분을 독립적으로 검증하며, 빠르고 신뢰할 수 있는 피드백을 제공합니다. Mockito와 같은 도구를 활용하면 복잡한 의존성도 쉽게 관리할 수 있습니다. 다음 장에서는 단위 테스트를 넘어, 애플리케이션 전체를 통합적으로 테스트하는 방법을 배워보겠습니다. 단위 테스트를 마스터하면 더 큰 테스트 세계로의 첫걸음을 내딛게 될 것입니다!
|
||||
|
||||
---
|
||||
|
||||
이 글은 실습 중심으로 작성되었으며, 코드 예제를 통해 실질적인 이해를 돕고자 했습니다. 추가 설명이나 수정이 필요하면 말씀해 주세요!
|
||||
193
docs/test/03_통합 테스트.md
Normal file
193
docs/test/03_통합 테스트.md
Normal file
@@ -0,0 +1,193 @@
|
||||
아래는 **3장. 통합 테스트**에 대한 글입니다. 이 장은 스프링 부트에서 통합 테스트의 개념을 설명하고, 실무에서 자주 사용하는 설정과 예제를 다룹니다. 단위 테스트와의 차이점을 강조하며 실습 중심으로 작성했습니다.
|
||||
|
||||
---
|
||||
|
||||
## 3장. 통합 테스트
|
||||
|
||||
단위 테스트가 개별 구성 요소를 독립적으로 검증한다면, 통합 테스트는 애플리케이션의 여러 부분이 함께 잘 작동하는지 확인하는 과정입니다. 스프링 부트에서는 컨트롤러, 서비스, 리포지토리가 데이터베이스나 외부 시스템과 상호작용하며 기대한 결과를 내는지 테스트할 수 있습니다. 이 장에서는 통합 테스트의 필요성, 설정 방법, 그리고 실습 예제를 다룹니다.
|
||||
|
||||
### 3.1 통합 테스트의 개념과 필요성
|
||||
|
||||
통합 테스트(Integration Test)는 애플리케이션의 여러 모듈 또는 계층이 상호작용할 때 발생할 수 있는 문제를 발견하는 데 목적이 있습니다. 단위 테스트가 모킹으로 의존성을 제거한다면, 통합 테스트는 실제 의존성을 포함해 전체 흐름을 검증합니다. 예를 들어, REST API 요청이 컨트롤러를 거쳐 서비스와 리포지토리를 호출하고, 데이터베이스에서 데이터를 가져오는 과정을 테스트한다고 생각해 봅시다. 이 과정에서 발생할 수 있는 설정 오류, 데이터 불일치, 예외 처리를 확인할 수 있습니다.
|
||||
|
||||
통합 테스트가 필요한 이유는 다음과 같습니다:
|
||||
- **현실적인 동작 확인**: 실제 환경과 유사한 조건에서 동작을 검증합니다.
|
||||
- **의존성 간 문제 탐지**: 모킹으로는 발견하기 어려운 상호작용 문제를 찾습니다.
|
||||
- **배포 전 신뢰성 확보**: 단위 테스트만으로는 놓칠 수 있는 통합된 시스템의 안정성을 보장합니다.
|
||||
|
||||
스프링 부트는 통합 테스트를 쉽게 작성할 수 있도록 강력한 도구를 제공하며, 이를 활용하는 방법을 배워보겠습니다.
|
||||
|
||||
### 3.2 @SpringBootTest 애너테이션 이해
|
||||
|
||||
스프링 부트에서 통합 테스트를 작성할 때 가장 중요한 애너테이션은 `@SpringBootTest`입니다. 이 애너테이션은 스프링 애플리케이션 컨텍스트를 로드하며, 실제 운영 환경과 유사한 설정으로 테스트를 실행합니다. 기본 사용법은 다음과 같습니다:
|
||||
|
||||
```java
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class MyApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
// 애플리케이션 컨텍스트가 로드되는지 확인
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`@SpringBootTest`는 다양한 옵션을 제공합니다:
|
||||
- `webEnvironment`: 웹 애플리케이션 환경을 설정합니다.
|
||||
- `MOCK`: 모킹된 웹 환경(기본값).
|
||||
- `RANDOM_PORT`: 실제 서버를 띄우고 랜덤 포트 사용.
|
||||
- `DEFINED_PORT`: 지정된 포트 사용.
|
||||
- `classes`: 특정 클래스만 로드해 테스트 속도를 높일 수 있습니다.
|
||||
|
||||
예를 들어, 웹 환경에서 테스트하려면 다음과 같이 설정합니다:
|
||||
|
||||
```java
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
class MyWebIntegrationTests {
|
||||
// 테스트 코드
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 테스트용 데이터베이스 설정 (H2, Testcontainers)
|
||||
|
||||
통합 테스트에서는 데이터베이스와의 상호작용을 검증해야 하므로, 테스트용 데이터베이스 설정이 중요합니다. 스프링 부트는 이를 위해 두 가지 인기 있는 방법을 지원합니다: **H2 인메모리 데이터베이스**와 **Testcontainers**.
|
||||
|
||||
#### H2 인메모리 데이터베이스
|
||||
H2는 가볍고 빠른 인메모리 데이터베이스로, 설정이 간단합니다. `pom.xml`에 의존성을 추가합니다:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
그리고 `application-test.properties`에 설정을 추가합니다:
|
||||
|
||||
```properties
|
||||
spring.datasource.url=jdbc:h2:mem:testdb
|
||||
spring.datasource.driverClassName=org.h2.Driver
|
||||
spring.datasource.username=sa
|
||||
spring.datasource.password=
|
||||
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
|
||||
```
|
||||
|
||||
#### Testcontainers
|
||||
Testcontainers는 Docker를 사용해 실제 데이터베이스(예: MySQL, PostgreSQL)를 테스트 환경에서 실행합니다. 설정이 조금 복잡하지만, 운영 환경과 더 가까운 테스트를 제공합니다. 의존성을 추가합니다:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>testcontainers</artifactId>
|
||||
<version>1.19.7</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.testcontainers</groupId>
|
||||
<artifactId>mysql</artifactId>
|
||||
<version>1.19.7</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
테스트 클래스에서 사용 예제:
|
||||
|
||||
```java
|
||||
import org.testcontainers.containers.MySQLContainer;
|
||||
|
||||
@SpringBootTest
|
||||
@Testcontainers
|
||||
class MySqlIntegrationTest {
|
||||
|
||||
@Container
|
||||
private static final MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
|
||||
.withDatabaseName("test")
|
||||
.withUsername("test")
|
||||
.withPassword("test");
|
||||
|
||||
@DynamicPropertySource
|
||||
static void mysqlProperties(DynamicPropertyRegistry registry) {
|
||||
registry.add("spring.datasource.url", mysql::getJdbcUrl);
|
||||
registry.add("spring.datasource.username", mysql::getUsername);
|
||||
registry.add("spring.datasource.password", mysql::getPassword);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testWithRealDatabase() {
|
||||
// 테스트 코드
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 REST API 통합 테스트 실습
|
||||
|
||||
이제 실제 REST API를 호출하는 통합 테스트를 작성해 봅시다. `UserController`가 있다고 가정하고, 이를 테스트합니다:
|
||||
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/users")
|
||||
public class UserController {
|
||||
private final UserService userService;
|
||||
|
||||
public UserController(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/name")
|
||||
public String getUserName(@PathVariable Long id) {
|
||||
return userService.getUserName(id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
통합 테스트 코드:
|
||||
|
||||
```java
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.web.client.TestRestTemplate;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
class UserControllerIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private TestRestTemplate restTemplate;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Test
|
||||
void getUserName_returnsName() {
|
||||
// 테스트 데이터 준비
|
||||
User user = new User(1L, "Alice");
|
||||
userRepository.save(user);
|
||||
|
||||
// API 호출
|
||||
ResponseEntity<String> response = restTemplate.getForEntity("/users/1/name", String.class);
|
||||
|
||||
// 결과 검증
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getBody()).isEqualTo("Alice");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`TestRestTemplate`은 실제 HTTP 요청을 보내고 응답을 검증하는 데 유용합니다. `@Transactional`을 추가하면 테스트 후 데이터가 롤백되어 깔끔하게 유지됩니다.
|
||||
|
||||
---
|
||||
|
||||
### 마무리
|
||||
|
||||
통합 테스트는 스프링 부트 애플리케이션의 전체 흐름을 검증하며, 단위 테스트로는 확인할 수 없는 현실적인 문제를 발견할 수 있습니다. `@SpringBootTest`, H2, Testcontainers를 활용하면 다양한 환경에서 테스트를 실행할 수 있습니다. 다음 장에서는 테스트를 더 효율적으로 작성하고 관리하는 유틸리티와 도구를 살펴보겠습니다. 통합 테스트를 통해 여러분의 애플리케이션에 더 큰 신뢰를 더해보세요!
|
||||
|
||||
---
|
||||
|
||||
이 글은 실습 예제와 함께 통합 테스트의 핵심을 다루었으며, 실무 적용 가능성을 높였습니다. 추가 요청이나 수정 사항이 있다면 말씀해 주세요!
|
||||
201
docs/test/04_테스트 유틸리티와 도구.md
Normal file
201
docs/test/04_테스트 유틸리티와 도구.md
Normal file
@@ -0,0 +1,201 @@
|
||||
아래는 **4장. 테스트 유틸리티와 도구**에 대한 글입니다. 이 장은 스프링 부트 테스트를 더 효율적이고 가독성 있게 작성할 수 있도록 돕는 유틸리티와 도구를 소개하며, 실습 예제를 포함해 실무에서의 활용성을 강조했습니다.
|
||||
|
||||
---
|
||||
|
||||
## 4장. 테스트 유틸리티와 도구
|
||||
|
||||
테스트 코드는 단순히 동작을 검증하는 것을 넘어, 가독성 있고 유지보수하기 쉬워야 합니다. 스프링 부트에서는 이를 지원하는 다양한 유틸리티와 도구가 제공됩니다. 이 장에서는 AssertJ와 Hamcrest로 검증을 강화하고, 스프링 REST Docs로 API 문서를 생성하며, 테스트 코드 리팩토링 팁을 다룹니다.
|
||||
|
||||
### 4.1 AssertJ로 깔끔한 검증 작성
|
||||
|
||||
AssertJ는 JUnit의 기본 `assert` 메서드보다 더 유연하고 가독성 높은 검증을 제공하는 라이브러리입니다. `spring-boot-starter-test`에 기본 포함되어 있어 별도 설정 없이 바로 사용할 수 있습니다. AssertJ의 장점은 메서드 체이닝으로 자연스러운 문장을 만들고, 풍부한 검증 옵션을 제공한다는 점입니다.
|
||||
|
||||
예를 들어, 기본 JUnit 검증과 AssertJ를 비교해 봅시다:
|
||||
|
||||
```java
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class AssertJExampleTest {
|
||||
|
||||
@Test
|
||||
void basicJUnitAssertion() {
|
||||
String result = "Hello, World!";
|
||||
assertEquals("Hello, World!", result); // JUnit 기본 방식
|
||||
}
|
||||
|
||||
@Test
|
||||
void assertJAssertion() {
|
||||
String result = "Hello, World!";
|
||||
assertThat(result)
|
||||
.isNotNull()
|
||||
.startsWith("Hello")
|
||||
.contains("World")
|
||||
.hasLength(13); // AssertJ 방식
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
AssertJ는 리스트, 맵, 객체 등 복잡한 데이터 구조도 쉽게 검증할 수 있습니다:
|
||||
|
||||
```java
|
||||
@Test
|
||||
void assertJListTest() {
|
||||
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
|
||||
assertThat(names)
|
||||
.hasSize(3)
|
||||
.containsExactly("Alice", "Bob", "Charlie")
|
||||
.doesNotContain("David");
|
||||
}
|
||||
```
|
||||
|
||||
AssertJ를 사용하면 실패 메시지도 더 자세히 제공되어 디버깅이 쉬워집니다.
|
||||
|
||||
### 4.2 Hamcrest를 활용한 유연한 매칭
|
||||
|
||||
Hamcrest는 테스트에서 조건을 더 유연하고 읽기 쉽게 표현할 수 있는 매처(matcher)를 제공합니다. AssertJ와 비슷한 역할을 하지만, 문법적으로 다르며 특정 상황에서 더 직관적일 수 있습니다. `spring-boot-starter-test`에도 포함되어 있습니다.
|
||||
|
||||
Hamcrest의 기본 사용법은 다음과 같습니다:
|
||||
|
||||
```java
|
||||
import org.junit.jupiter.api.Test;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
class HamcrestExampleTest {
|
||||
|
||||
@Test
|
||||
void hamcrestBasicTest() {
|
||||
String result = "Hello, World!";
|
||||
assertThat(result, is("Hello, World!")); // 기본 매칭
|
||||
assertThat(result, startsWith("Hello"));
|
||||
assertThat(result, containsString("World"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void hamcrestCollectionTest() {
|
||||
List<Integer> numbers = Arrays.asList(1, 2, 3, 4);
|
||||
assertThat(numbers, hasSize(4));
|
||||
assertThat(numbers, hasItem(greaterThan(2)));
|
||||
assertThat(numbers, everyItem(lessThan(5)));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Hamcrest는 특히 컬렉션이나 복잡한 조건을 테스트할 때 유용하며, AssertJ와 함께 사용해도 무방합니다. 상황에 따라 두 도구를 혼합해 가독성을 높일 수 있습니다.
|
||||
|
||||
### 4.3 스프링 REST Docs로 API 문서화
|
||||
|
||||
스프링 REST Docs는 테스트를 기반으로 REST API 문서를 자동 생성하는 도구입니다. 기존 Swagger와 달리 테스트가 실패하면 문서도 갱신되지 않으므로, 항상 정확한 문서를 유지할 수 있습니다. 설정은 약간 복잡하지만, 품질 높은 문서를 위해 투자할 가치가 있습니다.
|
||||
|
||||
의존성을 추가합니다:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.restdocs</groupId>
|
||||
<artifactId>spring-restdocs-mockmvc</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
테스트 예제:
|
||||
|
||||
```java
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath;
|
||||
import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@WebMvcTest(UserController.class)
|
||||
@AutoConfigureRestDocs(outputDir = "target/snippets")
|
||||
class UserControllerRestDocsTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
void getUserName_documentation() throws Exception {
|
||||
mockMvc.perform(get("/users/1/name"))
|
||||
.andExpect(status().isOk())
|
||||
.andDo(document("user-get-name",
|
||||
responseFields(
|
||||
fieldWithPath("name").description("The name of the user")
|
||||
)));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
위 코드는 `target/snippets` 폴더에 문서 조각을 생성하며, 이를 Asciidoctor로 통합해 HTML 문서로 변환할 수 있습니다. 예를 들어, `pom.xml`에 빌드 플러그인을 추가해 문서를 생성합니다:
|
||||
|
||||
```xml
|
||||
<plugin>
|
||||
<groupId>org.asciidoctor</groupId>
|
||||
<artifactId>asciidoctor-maven-plugin</artifactId>
|
||||
<version>2.2.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>generate-docs</id>
|
||||
<phase>prepare-package</phase>
|
||||
<goals>
|
||||
<goal>process-asciidoc</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<sourceDirectory>src/docs/asciidoc</sourceDirectory>
|
||||
<attributes>
|
||||
<snippets>${project.build.directory}/snippets</snippets>
|
||||
</attributes>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
```
|
||||
|
||||
### 4.4 테스트 코드 리팩토링 팁
|
||||
|
||||
좋은 테스트 코드는 명확하고 중복이 적어야 합니다. 다음은 리팩토링 팁입니다:
|
||||
|
||||
- **공통 설정 분리**: `@BeforeEach`나 별도의 유틸리티 클래스를 활용해 반복 코드를 줄입니다.
|
||||
```java
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
userRepository.save(new User(1L, "Alice"));
|
||||
}
|
||||
```
|
||||
- **테스트 메서드 이름 명확히**: `getUserName_withExistingUser_returnsName`처럼 의도를 드러내게 작성합니다.
|
||||
- **작은 테스트 유지**: 한 테스트가 너무 많은 것을 검증하지 않도록 단일 책임 원칙을 따릅니다.
|
||||
- **커스텀 Matcher/Assertion 사용**: AssertJ나 Hamcrest로 자주 사용하는 검증 로직을 재사용 가능하게 만듭니다.
|
||||
|
||||
예를 들어, 커스텀 AssertJ assertion:
|
||||
```java
|
||||
public class UserAssertions {
|
||||
public static AbstractStringAssert<?> assertThatUserName(String actual) {
|
||||
return assertThat(actual).isNotNull().isNotEmpty();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
사용:
|
||||
```java
|
||||
@Test
|
||||
void testUserName() {
|
||||
String name = userService.getUserName(1L);
|
||||
UserAssertions.assertThatUserName(name).isEqualTo("Alice");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 마무리
|
||||
|
||||
AssertJ와 Hamcrest는 테스트 검증을 풍부하게 하고, 스프링 REST Docs는 API 문서화를 혁신하며, 리팩토링은 테스트 코드의 품질을 높입니다. 이러한 도구와 기법을 활용하면 테스트가 단순한 검증을 넘어 팀과 프로젝트에 실질적인 가치를 더할 수 있습니다. 다음 장에서는 테스트 데이터 관리 전략을 다루며, 통합 테스트를 더 깊이 탐구해 보겠습니다!
|
||||
|
||||
---
|
||||
|
||||
이 글은 도구별 특징과 실습 예제를 중심으로 작성되었으며, 실무에서의 활용성을 높였습니다. 추가 요청이 있다면 말씀해 주세요!
|
||||
207
docs/test/05_테스트와 데이터 관리.md
Normal file
207
docs/test/05_테스트와 데이터 관리.md
Normal file
@@ -0,0 +1,207 @@
|
||||
아래는 **5장. 테스트와 데이터 관리**에 대한 글입니다. 이 장은 스프링 부트에서 테스트 데이터를 효과적으로 생성하고 관리하는 방법을 다루며, 실무에서 흔히 사용하는 전략과 도구를 소개합니다. 실습 예제를 포함해 실질적인 이해를 돕고자 했습니다.
|
||||
|
||||
---
|
||||
|
||||
## 5장. 테스트와 데이터 관리
|
||||
|
||||
통합 테스트에서 데이터베이스와의 상호작용을 검증하려면 테스트 데이터의 생성과 관리가 필수적입니다. 데이터가 없거나 일관성이 부족하면 테스트 결과가 신뢰를 잃을 수 있습니다. 이 장에서는 테스트 데이터를 생성하는 전략, 트랜잭션 관리, 데이터 마이그레이션 도구 활용, 그리고 실제 데이터베이스와의 연동 테스트 방법을 살펴봅니다.
|
||||
|
||||
### 5.1 테스트 데이터 생성 전략
|
||||
|
||||
테스트 데이터는 테스트의 성공 여부를 좌우합니다. 효과적인 데이터 생성 전략은 다음과 같습니다:
|
||||
|
||||
- **수동 생성**: 테스트 코드 내에서 직접 데이터를 삽입합니다. 간단한 경우에 적합합니다.
|
||||
```java
|
||||
@Test
|
||||
void testUserRetrieval() {
|
||||
User user = new User(1L, "Alice");
|
||||
userRepository.save(user);
|
||||
// 검증 로직
|
||||
}
|
||||
```
|
||||
- **픽스처(Fixture)**: 공통 데이터를 별도의 클래스나 파일로 관리합니다.
|
||||
```java
|
||||
class TestFixtures {
|
||||
static User createDefaultUser() {
|
||||
return new User(1L, "Alice");
|
||||
}
|
||||
}
|
||||
```
|
||||
- **랜덤 데이터 생성**: 라이브러리(예: JavaFaker)를 사용해 다양한 데이터를 생성합니다.
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>com.github.javafaker</groupId>
|
||||
<artifactId>javafaker</artifactId>
|
||||
<version>1.0.2</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
```java
|
||||
@Test
|
||||
void testWithRandomData() {
|
||||
Faker faker = new Faker();
|
||||
User user = new User(null, faker.name().fullName());
|
||||
userRepository.save(user);
|
||||
// 검증 로직
|
||||
}
|
||||
```
|
||||
|
||||
각 전략은 테스트의 목적과 복잡도에 따라 선택됩니다. 단순한 테스트에는 수동 생성이, 복잡한 시나리오에는 픽스처나 랜덤 데이터가 유용합니다.
|
||||
|
||||
### 5.2 @Transactional과 데이터 롤백
|
||||
|
||||
스프링 부트에서 통합 테스트를 실행할 때 데이터베이스 상태를 초기화하는 것은 중요합니다. `@Transactional` 애너테이션은 테스트 메서드 실행 후 데이터베이스 트랜잭션을 롤백해 원래 상태로 되돌립니다. 이를 사용하면 테스트 간 간섭을 방지할 수 있습니다.
|
||||
|
||||
예제:
|
||||
|
||||
```java
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@SpringBootTest
|
||||
@Transactional
|
||||
class UserRepositoryTest {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Test
|
||||
void saveUser_rollsBackAfterTest() {
|
||||
User user = new User(1L, "Alice");
|
||||
userRepository.save(user);
|
||||
|
||||
assertThat(userRepository.findById(1L)).isPresent();
|
||||
// 테스트 종료 후 롤백됨
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
테스트가 끝나면 데이터가 자동으로 삭제되므로, 다음 테스트에 영향을 주지 않습니다. 단, 실제 데이터베이스에 반영하고 싶다면 `@Rollback(false)`를 추가할 수 있습니다.
|
||||
|
||||
### 5.3 Flyway 또는 Liquibase를 활용한 테스트 데이터 마이그레이션
|
||||
|
||||
운영 환경에서 데이터베이스 스키마와 초기 데이터를 관리하기 위해 Flyway나 Liquibase를 사용하는 경우, 테스트에서도 이를 활용할 수 있습니다. 이를 통해 테스트 환경을 운영과 동일하게 유지할 수 있습니다.
|
||||
|
||||
#### Flyway 설정
|
||||
의존성 추가:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.flywaydb</groupId>
|
||||
<artifactId>flyway-core</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
`src/test/resources/db/migration`에 SQL 파일 작성 (예: `V1__init_users.sql`):
|
||||
|
||||
```sql
|
||||
CREATE TABLE users (
|
||||
id BIGINT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL
|
||||
);
|
||||
|
||||
INSERT INTO users (id, name) VALUES (1, 'Alice');
|
||||
INSERT INTO users (id, name) VALUES (2, 'Bob');
|
||||
```
|
||||
|
||||
`application-test.properties`에 설정:
|
||||
|
||||
```properties
|
||||
spring.flyway.locations=classpath:db/migration
|
||||
```
|
||||
|
||||
테스트에서 Flyway가 자동으로 스키마와 데이터를 적용합니다.
|
||||
|
||||
#### Liquibase 설정
|
||||
의존성 추가:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.liquibase</groupId>
|
||||
<artifactId>liquibase-core</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
`src/test/resources/db/changelog/db.changelog-master.yaml`:
|
||||
|
||||
```yaml
|
||||
databaseChangeLog:
|
||||
- changeSet:
|
||||
id: 1
|
||||
author: test
|
||||
changes:
|
||||
- createTable:
|
||||
tableName: users
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
type: bigint
|
||||
constraints:
|
||||
primaryKey: true
|
||||
- column:
|
||||
name: name
|
||||
type: varchar(255)
|
||||
constraints:
|
||||
nullable: false
|
||||
- insert:
|
||||
tableName: users
|
||||
columns:
|
||||
- column:
|
||||
name: id
|
||||
value: 1
|
||||
- column:
|
||||
name: name
|
||||
value: Alice
|
||||
```
|
||||
|
||||
Flyway와 Liquibase는 테스트 데이터의 일관성을 보장하며, 복잡한 스키마 변경도 관리할 수 있습니다.
|
||||
|
||||
### 5.4 실제 데이터베이스와의 연동 테스트
|
||||
|
||||
H2나 Testcontainers 대신 실제 데이터베이스(예: MySQL, PostgreSQL)를 사용한 테스트가 필요할 때도 있습니다. 이는 운영 환경과 최대한 가까운 조건에서 테스트를 실행하고자 할 때 유용합니다.
|
||||
|
||||
설정 예제 (`application-test.properties`):
|
||||
|
||||
```properties
|
||||
spring.datasource.url=jdbc:mysql://localhost:3306/testdb
|
||||
spring.datasource.username=testuser
|
||||
spring.datasource.password=testpass
|
||||
spring.jpa.hibernate.ddl-auto=update
|
||||
```
|
||||
|
||||
테스트 코드:
|
||||
|
||||
```java
|
||||
@SpringBootTest
|
||||
class RealDatabaseIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Test
|
||||
@Transactional
|
||||
void testWithRealDatabase() {
|
||||
User user = new User(1L, "Alice");
|
||||
userRepository.save(user);
|
||||
|
||||
User found = userRepository.findById(1L).orElse(null);
|
||||
assertThat(found).isNotNull();
|
||||
assertThat(found.getName()).isEqualTo("Alice");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
실제 데이터베이스 테스트는 속도가 느릴 수 있으므로, CI/CD 파이프라인에서는 별도의 단계로 분리하거나 제한적으로 사용하는 것이 좋습니다.
|
||||
|
||||
---
|
||||
|
||||
### 마무리
|
||||
|
||||
테스트 데이터 관리는 통합 테스트의 신뢰성과 효율성을 높이는 핵심입니다. 수동 생성, 픽스처, 랜덤 데이터 생성으로 유연성을 확보하고, `@Transactional`로 데이터 상태를 관리하며, Flyway나 Liquibase로 일관성을 유지할 수 있습니다. 실제 데이터베이스 연동은 운영 환경과의 차이를 줄이는 데 유용합니다. 다음 장에서는 성능과 부하 테스트를 다루며, 애플리케이션의 한계를 시험해 보겠습니다!
|
||||
|
||||
---
|
||||
|
||||
이 글은 데이터 관리의 다양한 접근법을 다루며, 실습 예제를 통해 실무 적용 가능성을 높였습니다. 추가 요청이나 수정 사항이 있다면 말씀해 주세요!
|
||||
135
docs/test/06_성능 및 부하 테스트.md
Normal file
135
docs/test/06_성능 및 부하 테스트.md
Normal file
@@ -0,0 +1,135 @@
|
||||
아래는 **6장. 성능 및 부하 테스트**에 대한 글입니다. 이 장은 스프링 부트 애플리케이션의 성능과 부하를 테스트하는 방법을 다루며, 실무에서 자주 사용하는 도구와 분석 방법을 소개합니다. 실습 예제를 포함해 실질적인 이해를 돕고자 했습니다.
|
||||
|
||||
---
|
||||
|
||||
## 6장. 성능 및 부하 테스트
|
||||
|
||||
애플리케이션의 기능이 올바르게 동작하는지를 확인하는 단위 및 통합 테스트를 넘어, 실제 사용자 트래픽을 감당할 수 있는지 확인하는 성능 및 부하 테스트는 필수적입니다. 스프링 부트 애플리케이션의 성능 병목 지점을 찾고, 확장성을 검증하는 방법을 이 장에서 다룹니다. JMeter와 Gatling을 활용한 실습과 결과 분석까지 포함했습니다.
|
||||
|
||||
### 6.1 스프링 부트 애플리케이션 성능 테스트 개요
|
||||
|
||||
성능 테스트는 애플리케이션이 특정 조건에서 얼마나 잘 동작하는지 측정합니다. 부하 테스트는 그 하위 범주로, 높은 트래픽이나 동시 사용자 요청을 시뮬레이션해 시스템의 한계를 확인합니다. 주요 목표는 다음과 같습니다:
|
||||
- **응답 시간**: 요청 처리 속도가 적절한지.
|
||||
- **처리량(Throughput)**: 초당 처리할 수 있는 요청 수.
|
||||
- **안정성**: 부하 증가 시 시스템이崩溃하지 않는지.
|
||||
|
||||
스프링 부트는 내장 톰캣 서버와 자동 설정으로 빠르게 실행되지만, 데이터베이스 연결, 캐싱, 비동기 처리 등에서 병목이 발생할 수 있습니다. 이를 테스트로 사전에 파악해야 합니다.
|
||||
|
||||
### 6.2 JMeter를 활용한 부하 테스트
|
||||
|
||||
Apache JMeter는 오픈소스 부하 테스트 도구로, HTTP 요청을 시뮬레이션하고 결과를 분석하는 데 유용합니다. 설치 후 간단한 테스트를 설정해 봅시다.
|
||||
|
||||
#### JMeter 설정
|
||||
1. [JMeter 공식 사이트](https://jmeter.apache.org/)에서 다운로드 후 실행.
|
||||
2. 테스트 계획(Test Plan) 생성:
|
||||
- **스레드 그룹(Thread Group)**: 동시 사용자 수(예: 100명), 반복 횟수 설정.
|
||||
- **HTTP 요청(HTTP Request)**: 대상 URL(예: `http://localhost:8080/users/1/name`) 입력.
|
||||
- **리스너(Listener)**: 결과 트리(View Results Tree), 요약 리포트(Summary Report) 추가.
|
||||
|
||||
#### 예제 테스트
|
||||
`UserController`의 `/users/1/name` 엔드포인트를 테스트한다고 가정:
|
||||
- 스레드 수: 100
|
||||
- 반복 횟수: 10
|
||||
- 요청: GET `/users/1/name`
|
||||
|
||||
JMeter에서 테스트 실행 후 결과:
|
||||
- **평균 응답 시간**: 50ms
|
||||
- **처리량**: 200 req/s
|
||||
- **에러율**: 0%
|
||||
|
||||
#### 분석
|
||||
응답 시간이 길거나 에러가 발생하면 데이터베이스 쿼리 최적화, 캐싱 추가 등 개선이 필요합니다. JMeter는 CSV로 결과를 내보내 상세 분석도 가능합니다.
|
||||
|
||||
### 6.3 Gatling으로 시나리오 기반 테스트
|
||||
|
||||
Gatling은 스칼라 기반의 고성능 부하 테스트 도구로, 시나리오를 코드로 작성해 복잡한 사용자 행동을 시뮬레이션할 수 있습니다. JMeter보다 가볍고, HTML 리포트가 직관적입니다.
|
||||
|
||||
#### Gatling 설정
|
||||
의존성 추가 (Maven 프로젝트로 설정):
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>io.gatling.highcharts</groupId>
|
||||
<artifactId>gatling-charts-highcharts</artifactId>
|
||||
<version>3.9.5</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
#### 테스트 시나리오 작성
|
||||
`src/test/scala`에 시나리오 작성:
|
||||
|
||||
```scala
|
||||
import io.gatling.core.Predef._
|
||||
import io.gatling.http.Predef._
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class UserApiSimulation extends Simulation {
|
||||
val httpProtocol = http
|
||||
.baseUrl("http://localhost:8080")
|
||||
.acceptHeader("application/json")
|
||||
|
||||
val scn = scenario("User API Load Test")
|
||||
.exec(http("Get User Name")
|
||||
.get("/users/1/name"))
|
||||
.pause(1) // 1초 대기
|
||||
|
||||
setUp(
|
||||
scn.inject(
|
||||
rampUsers(100).during(10.seconds) // 10초 동안 100명 증가
|
||||
)
|
||||
).protocols(httpProtocol)
|
||||
}
|
||||
```
|
||||
|
||||
#### 실행 및 결과
|
||||
`mvn gatling:test`로 실행하면 HTML 리포트가 `target/gatling`에 생성됩니다. 리포트는 응답 시간 분포, 요청별 통계 등을 시각화합니다.
|
||||
|
||||
### 6.4 성능 테스트 결과 분석
|
||||
|
||||
성능 테스트 결과를 분석하려면 다음 지표를 확인합니다:
|
||||
- **응답 시간 분포**: 90번째 백분위수(90% 요청이 이 시간 내 처리됨)가 중요한 기준.
|
||||
- **에러율**: 1% 이상이면 문제 원인을 조사.
|
||||
- **리소스 사용량**: CPU, 메모리, 디스크 I/O를 모니터링(예: `top`, Prometheus).
|
||||
|
||||
#### 예제 분석
|
||||
JMeter 결과:
|
||||
- 평균 응답 시간: 50ms
|
||||
- 90% 응답 시간: 70ms
|
||||
- 처리량: 200 req/s
|
||||
|
||||
Gatling 결과:
|
||||
- 최대 응답 시간: 120ms
|
||||
- 실패 요청: 0건
|
||||
|
||||
응답 시간이 100ms를 넘거나 실패가 발생하면 다음 조치를 고려합니다:
|
||||
- **쿼리 최적화**: 인덱스 추가, 불필요한 조인 제거.
|
||||
- **캐싱**: Redis나 Spring Cache 도입.
|
||||
- **스케일링**: 서버 인스턴스 추가.
|
||||
|
||||
#### 모니터링 도구 연동
|
||||
Spring Boot Actuator를 활성화해 성능 지표를 수집할 수 있습니다:
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
`application.properties`:
|
||||
```properties
|
||||
management.endpoints.web.exposure.include=health,metrics
|
||||
```
|
||||
|
||||
`/actuator/metrics` 엔드포인트로 HTTP 요청 수, 응답 시간 등을 확인하며 테스트와 연계합니다.
|
||||
|
||||
---
|
||||
|
||||
### 마무리
|
||||
|
||||
성능 및 부하 테스트는 스프링 부트 애플리케이션이 실전에서 안정적으로 동작할 수 있는지 확인하는 마지막 관문입니다. JMeter로 간단한 부하를 시뮬레이션하고, Gatling으로 복잡한 시나리오를 테스트하며, 결과를 분석해 병목을 개선할 수 있습니다. 다음 장에서는 CI/CD 파이프라인에 테스트를 통합해 개발 프로세스를 자동화하는 방법을 다루겠습니다. 성능 테스트로 여러분의 애플리케이션을 한 단계 업그레이드해 보세요!
|
||||
|
||||
---
|
||||
|
||||
이 글은 성능 테스트의 핵심 개념과 도구 사용법을 다루며, 실습과 분석을 통해 실무 적용 가능성을 높였습니다. 추가 요청이나 수정 사항이 있다면 말씀해 주세요!
|
||||
137
docs/test/09_테스트 피라미드와 실무 적용.md
Normal file
137
docs/test/09_테스트 피라미드와 실무 적용.md
Normal file
@@ -0,0 +1,137 @@
|
||||
아래는 **9장. 테스트 피라미드와 실무 적용**에 대한 글입니다. 이 장은 테스트 피라미드라는 이론적 프레임워크를 기반으로 스프링 부트 프로젝트에서 실무적으로 테스트를 적용하는 방법을 다룹니다. 실습 예제와 실무 팁을 포함해 실질적인 가치를 제공하도록 작성했습니다.
|
||||
|
||||
---
|
||||
|
||||
## 9장. 테스트 피라미드와 실무 적용
|
||||
|
||||
테스트는 단순히 코드를 검증하는 도구를 넘어, 프로젝트의 품질과 유지보수성을 높이는 전략입니다. 테스트 피라미드는 이를 체계적으로 설계하는 데 유용한 개념으로, 스프링 부트 프로젝트에서 어떻게 적용할 수 있는지 이 장에서 다룹니다. 실무에서의 전략, 흔한 실수, 그리고 성공적인 테스트 문화를 구축하는 방법까지 살펴보겠습니다.
|
||||
|
||||
### 9.1 테스트 피라미드 이해
|
||||
|
||||
테스트 피라미드는 테스트의 종류와 비율을 계층적으로 표현한 모델입니다. 아래에서 위로 갈수록 테스트 범위는 넓어지지만, 수와 실행 비용은 줄어듭니다. 주요 계층은 다음과 같습니다:
|
||||
|
||||
- **단위 테스트(Unit Tests)**: 가장 아래층으로, 개별 메서드나 클래스를 독립적으로 검증. 빠르고 수가 많음.
|
||||
- **통합 테스트(Integration Tests)**: 중간층으로, 여러 모듈이나 계층 간 상호작용을 테스트. 단위 테스트보다 느림.
|
||||
- **엔드투엔드 테스트(E2E Tests)**: 최상층으로, 사용자 관점에서 전체 시스템을 검증. 가장 느리고 수가 적음.
|
||||
|
||||
스프링 부트에서:
|
||||
- 단위 테스트: 서비스 로직, 유틸리티 메서드 (예: `UserService.getUserName()`).
|
||||
- 통합 테스트: 컨트롤러와 리포지토리 연동 (예: REST API 호출).
|
||||
- E2E 테스트: 브라우저나 클라이언트를 통한 전체 워크플로우.
|
||||
|
||||
피라미드의 핵심은 **단위 테스트를 기반으로 하되, 필요한 만큼 상위 테스트를 보완**하는 것입니다. 단위 테스트가 70-80%, 통합 테스트가 15-25%, E2E 테스트가 5-10% 정도가 일반적인 비율입니다.
|
||||
|
||||
### 9.2 실무에서의 테스트 전략 수립
|
||||
|
||||
스프링 부트 프로젝트에서 테스트 전략을 수립하려면 다음 단계를 따릅니다:
|
||||
|
||||
1. **프로젝트 요구사항 분석**:
|
||||
- 빠른 피드백이 중요한가? → 단위 테스트 강화.
|
||||
- 외부 시스템 연동이 많나? → 통합 테스트 필요.
|
||||
- UI 중심인가? → E2E 테스트 고려.
|
||||
|
||||
2. **테스트 범위 정의**:
|
||||
- 핵심 비즈니스 로직은 단위 테스트로 100% 커버.
|
||||
- API 엔드포인트는 통합 테스트로 검증.
|
||||
- 주요 사용자 시나리오는 E2E로 확인.
|
||||
|
||||
3. **도구 선택**:
|
||||
- 단위: JUnit, Mockito, AssertJ.
|
||||
- 통합: `@SpringBootTest`, Testcontainers.
|
||||
- E2E: Selenium, Cypress (웹 애플리케이션용).
|
||||
|
||||
#### 예제 전략
|
||||
간단한 사용자 관리 API 프로젝트:
|
||||
- **단위 테스트**: `UserService`의 `createUser()`, `getUserName()` 메서드.
|
||||
- **통합 테스트**: `/users` 엔드포인트 호출 및 DB 저장 확인.
|
||||
- **E2E 테스트**: 사용자 등록 후 이름 조회 시나리오.
|
||||
|
||||
```java
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
class UserApiIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private TestRestTemplate restTemplate;
|
||||
|
||||
@Test
|
||||
void createAndGetUser() {
|
||||
// 통합 테스트: 사용자 생성
|
||||
ResponseEntity<String> createResponse = restTemplate.postForEntity("/users", new User(null, "Alice"), String.class);
|
||||
assertThat(createResponse.getStatusCode()).isEqualTo(HttpStatus.CREATED);
|
||||
|
||||
// 통합 테스트: 사용자 조회
|
||||
ResponseEntity<String> getResponse = restTemplate.getForEntity("/users/1/name", String.class);
|
||||
assertThat(getResponse.getBody()).isEqualTo("Alice");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9.3 흔한 테스트 실수와 해결 방법
|
||||
|
||||
실무에서 자주 발생하는 실수와 대처법을 정리했습니다:
|
||||
|
||||
- **실수 1: 모든 것을 통합 테스트로 작성**
|
||||
- 문제: 느리고 유지보수가 어려움.
|
||||
- 해결: 단위 테스트로 분리 가능한 로직을 옮기고, 통합 테스트는 핵심 흐름에 집중.
|
||||
|
||||
- **실수 2: 모킹 과다 사용**
|
||||
- 문제: 실제 동작과 달라 신뢰성 하락.
|
||||
- 해결: 모킹은 외부 의존성에만 사용하고, 내부 로직은 실제 구현으로 테스트.
|
||||
|
||||
- **실수 3: 테스트 데이터 관리 부족**
|
||||
- 문제: 테스트 간 간섭으로 실패.
|
||||
- 해결: `@Transactional` 사용하거나, 각 테스트마다 고유 데이터 생성.
|
||||
|
||||
- **실수 4: 커버리지에 집착**
|
||||
- 문제: 의미 없는 테스트 증가.
|
||||
- 해결: 핵심 로직의 품질에 집중하고, 커버리지는 참고 지표로만 활용.
|
||||
|
||||
#### 예제: 실수 수정
|
||||
```java
|
||||
// 잘못된 예: 모든 것을 통합 테스트로
|
||||
@SpringBootTest
|
||||
class BadTest {
|
||||
@Test
|
||||
void testEverything() {
|
||||
// 너무 많은 검증
|
||||
}
|
||||
}
|
||||
|
||||
// 개선된 예: 단위 테스트로 분리
|
||||
class UserServiceTest {
|
||||
@Mock
|
||||
private UserRepository repository;
|
||||
|
||||
@InjectMocks
|
||||
private UserService service;
|
||||
|
||||
@Test
|
||||
void getUserName() {
|
||||
when(repository.findById(1L)).thenReturn(new User(1L, "Alice"));
|
||||
assertThat(service.getUserName(1L)).isEqualTo("Alice");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 9.4 성공적인 테스트 문화 구축
|
||||
|
||||
테스트를 프로젝트의 핵심 요소로 만들려면 팀 차원의 노력이 필요합니다:
|
||||
- **교육과 공유**: 신입 개발자에게 테스트 작성법을 가르치고, 코드 리뷰에서 테스트를 점검.
|
||||
- **자동화**: CI/CD에 테스트를 통합해 빠른 피드백 제공 (7장 참고).
|
||||
- **책임 분담**: 모든 팀원이 테스트 작성에 참여하도록 장려.
|
||||
- **측정과 개선**: 커버리지와 실패율을 모니터링하며 지속적으로 개선.
|
||||
|
||||
#### 실무 팁
|
||||
- **테스트 주도 개발(TDD)**: 요구사항을 테스트로 먼저 작성 후 구현.
|
||||
- **페어 프로그래밍**: 테스트 코드를 함께 작성하며 품질 향상.
|
||||
- **리팩토링 시간 확보**: 테스트가 없으면 리팩토링이 두려워지므로, 초기에 투자.
|
||||
|
||||
---
|
||||
|
||||
### 마무리
|
||||
|
||||
테스트 피라미드는 스프링 부트 프로젝트에서 테스트를 체계적으로 설계하는 가이드입니다. 단위 테스트로 기반을 다지고, 통합 테스트로 상호작용을 검증하며, E2E 테스트로 사용자 경험을 보장합니다. 실무에서 흔한 실수를 피하고, 팀 전체가 테스트 문화를 받아들이면 안정적이고 확장 가능한 애플리케이션을 만들 수 있습니다. 다음 장에서는 실제 사례를 통해 배운 교훈을 공유하며 이 책을 마무리하겠습니다!
|
||||
|
||||
---
|
||||
|
||||
이 글은 테스트 피라미드의 이론과 실무 적용을 균형 있게 다루며, 실습과 팁으로 실질적인 도움을 주고자 했습니다. 추가 요청이나 수정 사항이 있다면 말씀해 주세요!
|
||||
51
docs/test/11_애너테이션.md
Normal file
51
docs/test/11_애너테이션.md
Normal file
@@ -0,0 +1,51 @@
|
||||
아래는 스프링 부트 테스트에서 자주 사용되는 애너테이션들을 **JUnit**, **Spring Test**, **Mockito**로 구분하고, 표 형식으로 정리한 내용입니다. 각 애너테이션의 역할과 설명을 간결하게 작성했습니다.
|
||||
|
||||
---
|
||||
|
||||
### JUnit 관련 애너테이션
|
||||
| 애너테이션 | 설명 |
|
||||
|-----------------------|----------------------------------------------------------------------|
|
||||
| `@Test` | 테스트 메서드를 정의. 별도 설정 없이 실행 가능한 기본 단위 테스트. |
|
||||
| `@BeforeEach` | 각 테스트 메서드 실행 전에 실행되는 설정 메서드를 지정. |
|
||||
| `@AfterEach` | 각 테스트 메서드 실행 후에 실행되는 정리 메서드를 지정. |
|
||||
| `@BeforeAll` | 모든 테스트 실행 전에 한 번 실행되는 클래스 수준 설정 메서드를 지정. (static 필요) |
|
||||
| `@AfterAll` | 모든 테스트 실행 후에 한 번 실행되는 클래스 수준 정리 메서드를 지정. (static 필요) |
|
||||
| `@Disabled` | 특정 테스트 메서드나 클래스를 비활성화하여 실행에서 제외. |
|
||||
| `@DisplayName` | 테스트 메서드에 사용자 친화적인 이름을 지정해 가독성 향상. |
|
||||
| `@ParameterizedTest` | 동일한 테스트를 여러 데이터로 반복 실행. (예: 값 소스와 함께 사용) |
|
||||
| `@Nested` | 테스트 클래스 내에 계층적 구조를 만들어 관련 테스트를 그룹화. |
|
||||
|
||||
---
|
||||
|
||||
### Spring Test 관련 애너테이션
|
||||
| 애너테이션 | 설명 |
|
||||
|-------------------------|----------------------------------------------------------------------------------------|
|
||||
| `@SpringBootTest` | 스프링 부트 애플리케이션 컨텍스트를 로드하여 통합 테스트를 실행. (webEnvironment 설정 가능) |
|
||||
| `@WebMvcTest` | MVC 계층(컨트롤러)만 로드해 단위 테스트. 서비스나 리포지토리는 모킹 필요. |
|
||||
| `@DataJpaTest` | JPA 관련 테스트를 위해 리포지토리와 데이터베이스 설정만 로드. (H2 기본 사용) |
|
||||
| `@AutoConfigureMockMvc`| MockMvc를 자동 설정해 컨트롤러 테스트에 사용. |
|
||||
| `@Transactional` | 테스트 메서드 실행 후 트랜잭션을 롤백하여 데이터베이스 상태를 초기화. |
|
||||
| `@Rollback` | `@Transactional`과 함께 사용해 롤백 여부를 명시적으로 설정. (기본: true) |
|
||||
| `@Testcontainers` | Testcontainers를 사용해 Docker 기반 데이터베이스 등을 테스트 환경에 통합. |
|
||||
| `@DynamicPropertySource`| 테스트에서 동적으로 프로퍼티(예: 데이터베이스 URL)를 설정. (Testcontainers와 자주 사용) |
|
||||
| `@AutoConfigureRestDocs`| 스프링 REST Docs를 자동 설정해 API 문서 생성을 지원. |
|
||||
|
||||
---
|
||||
|
||||
### Mockito 관련 애너테이션
|
||||
| 애너테이션 | 설명 |
|
||||
|------------------------|----------------------------------------------------------------------------------------|
|
||||
| `@Mock` | 모킹된 가짜 객체를 생성. 외부 의존성을 대체할 때 사용. |
|
||||
| `@InjectMocks` | 모킹된 객체를 실제 객체에 주입해 테스트 대상 객체를 초기화. |
|
||||
| `@Spy` | 실제 객체를 감싸 일부 메서드만 모킹 가능. 전체 동작을 유지하면서 특정 동작만 변경. |
|
||||
| `@MockBean` | 스프링 컨텍스트에 모킹된 빈을 등록. `@SpringBootTest`나 `@WebMvcTest`에서 사용. |
|
||||
| `@ExtendWith(MockitoExtension.class)` | JUnit 5와 Mockito를 통합해 `@Mock`, `@InjectMocks` 등을 활성화. |
|
||||
|
||||
---
|
||||
|
||||
### 추가 참고
|
||||
- **JUnit**: 테스트 실행의 기본 프레임워크로, 단위 테스트와 통합 테스트 모두에서 사용됩니다.
|
||||
- **Spring Test**: 스프링 부트의 컨텍스트와 통합된 테스트를 지원하며, 계층별 테스트에 특화된 애너테이션을 제공합니다.
|
||||
- **Mockito**: 의존성을 모킹해 단위 테스트를 독립적으로 실행하거나, 통합 테스트에서 특정 빈을 대체하는 데 유용합니다.
|
||||
|
||||
이 표는 책의 **부록 A. 자주 사용하는 테스트 애너테이션 정리**에 포함시키기에 적합합니다. 필요하면 특정 애너테이션에 대한 예제나 더 자세한 설명을 추가할 수 있으니 말씀해 주세요!
|
||||
74
docs/test/README.md
Normal file
74
docs/test/README.md
Normal file
@@ -0,0 +1,74 @@
|
||||
스프링 부트에서의 테스트에 관한 책의 목차를 작성해 드리겠습니다. 이 목차는 초보자부터 중급 개발자까지 유용하게 사용할 수 있도록 구성했으며, 실무에서 자주 다루는 주제를 포함했습니다. 아래는 제안하는 목차입니다:
|
||||
|
||||
---
|
||||
|
||||
### 목차
|
||||
|
||||
#### 1장. 스프링 부트 테스트 개요
|
||||
- 1.1 테스트의 중요성과 스프링 부트에서의 역할
|
||||
- 1.2 스프링 부트 테스트 프레임워크 소개
|
||||
- 1.3 테스트 환경 설정 기초
|
||||
- 1.4 JUnit과 스프링 부트의 통합
|
||||
|
||||
#### 2장. 단위 테스트 작성
|
||||
- 2.1 단위 테스트란 무엇인가
|
||||
- 2.2 스프링 부트에서 단위 테스트 설정
|
||||
- 2.3 Mockito를 활용한 모킹
|
||||
- 2.4 컨트롤러, 서비스, 리포지토리 단위 테스트 예제
|
||||
|
||||
#### 3장. 통합 테스트
|
||||
- 3.1 통합 테스트의 개념과 필요성
|
||||
- 3.2 @SpringBootTest 애너테이션 이해
|
||||
- 3.3 테스트용 데이터베이스 설정 (H2, Testcontainers)
|
||||
- 3.4 REST API 통합 테스트 실습
|
||||
|
||||
#### 4장. 테스트 유틸리티와 도구
|
||||
- 4.1 AssertJ로 깔끔한 검증 작성
|
||||
- 4.2 Hamcrest를 활용한 유연한 매칭
|
||||
- 4.3 스프링 REST Docs로 API 문서화
|
||||
- 4.4 테스트 코드 리팩토링 팁
|
||||
|
||||
#### 5장. 테스트와 데이터 관리
|
||||
- 5.1 테스트 데이터 생성 전략
|
||||
- 5.2 @Transactional과 데이터 롤백
|
||||
- 5.3 Flyway 또는 Liquibase를 활용한 테스트 데이터 마이그레이션
|
||||
- 5.4 실제 데이터베이스와의 연동 테스트
|
||||
|
||||
#### 6장. 성능 및 부하 테스트
|
||||
- 6.1 스프링 부트 애플리케이션 성능 테스트 개요
|
||||
- 6.2 JMeter를 활용한 부하 테스트
|
||||
- 6.3 Gatling으로 시나리오 기반 테스트
|
||||
- 6.4 성능 테스트 결과 분석
|
||||
|
||||
#### 7장. 테스트와 CI/CD 통합
|
||||
- 7.1 CI/CD 파이프라인에서 테스트 실행
|
||||
- 7.2 GitHub Actions로 테스트 자동화
|
||||
- 7.3 Jenkins와 스프링 부트 테스트 연동
|
||||
- 7.4 테스트 커버리지 측정과 리포팅
|
||||
|
||||
#### 8장. 고급 테스트 기법
|
||||
- 8.1 비동기 코드 테스트 (@Async, CompletableFuture)
|
||||
- 8.2 스프링 시큐리티와 함께하는 보안 테스트
|
||||
- 8.3 웹소켓 테스트
|
||||
- 8.4 스프링 배치 테스트
|
||||
|
||||
#### 9장. 테스트 피라미드와 실무 적용
|
||||
- 9.1 테스트 피라미드 이해
|
||||
- 9.2 실무에서의 테스트 전략 수립
|
||||
- 9.3 흔한 테스트 실수와 해결 방법
|
||||
- 9.4 성공적인 테스트 문화 구축
|
||||
|
||||
#### 10장. 사례 연구 및 모범 사례
|
||||
- 10.1 실제 프로젝트에서의 테스트 사례
|
||||
- 10.2 오픈소스 프로젝트의 테스트 분석
|
||||
- 10.3 모범 사례와 교훈
|
||||
- 10.4 추가 학습 리소스
|
||||
|
||||
#### 부록
|
||||
- A. 자주 사용하는 테스트 애너테이션 정리
|
||||
- B. 테스트 관련 라이브러리 설치 가이드
|
||||
- C. 샘플 프로젝트 GitHub 링크
|
||||
|
||||
---
|
||||
|
||||
이 목차는 스프링 부트 테스트의 기초부터 고급 주제까지 다루며, 실무에서 바로 적용 가능한 내용을 중심으로 설계되었습니다. 특정 주제에 더 깊이 들어가고 싶거나 다른 섹션을 추가하고 싶다면 말씀해 주세요!
|
||||
230
docs/webclient.md
Normal file
230
docs/webclient.md
Normal file
@@ -0,0 +1,230 @@
|
||||
### 스프링 부트의 WebClient에 대한 설명
|
||||
|
||||
스프링 부트에서 `WebClient`는 스프링 5에서 도입된 **비동기(Asynchronous)** 및 **리액티브(Reactive)** HTTP 클라이언트로, 외부 REST API 호출을 처리하기 위한 현대적인 도구입니다. `RestTemplate`의 후속으로 설계된 `WebClient`는 리액티브 스트림(Reactive Streams)을 기반으로 동작하며, 비동기 처리와 높은 동시성을 요구하는 환경에서 뛰어난 성능을 제공합니다. 스프링 부트와 통합 시, 선언적이고 유연한 API로 HTTP 요청을 쉽게 구성할 수 있습니다. 아래에서 설정, 사용 방법, 주요 기능을 설명합니다.
|
||||
|
||||
---
|
||||
|
||||
#### 1. WebClient의 기본 개념
|
||||
- **비동기/리액티브**: 요청을 보내고 응답을 기다리지 않고, 결과를 `Mono`나 `Flux`로 비동기적으로 처리.
|
||||
- **HTTP 메서드 지원**: GET, POST, PUT, DELETE 등 모든 표준 메서드 지원.
|
||||
- **리액티브 스트림**: `Mono` (단일 결과), `Flux` (다중 결과)를 반환하여 데이터 스트리밍 가능.
|
||||
- **대체재**: `RestTemplate`을 대체하며, 스프링 5.0 이상에서 권장.
|
||||
|
||||
#### 2. 의존성
|
||||
`WebClient`는 `spring-boot-starter-webflux`에 포함되어 있습니다.
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
- **참고**: `WebFlux`는 리액티브 웹 애플리케이션 프레임워크로, `WebClient`를 포함.
|
||||
|
||||
---
|
||||
|
||||
#### 3. 기본 설정
|
||||
`WebClient`는 스프링 빈으로 등록하거나, 필요 시 커스터마이징하여 사용할 수 있습니다.
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
public class WebClientConfig {
|
||||
|
||||
@Bean
|
||||
public WebClient webClient(WebClient.Builder builder) {
|
||||
return builder
|
||||
.baseUrl("https://api.example.com") // 기본 URL 설정
|
||||
.defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) // 기본 헤더
|
||||
.clientConnector(new ReactorClientHttpConnector(
|
||||
HttpClient.create().responseTimeout(Duration.ofSeconds(10)) // 타임아웃 설정
|
||||
))
|
||||
.build();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- **`WebClient.Builder`**: 타임아웃, 기본 헤더, 필터 등을 설정.
|
||||
|
||||
---
|
||||
|
||||
#### 4. WebClient 사용 예시
|
||||
|
||||
##### (1) GET 요청
|
||||
```java
|
||||
@Service
|
||||
public class UserService {
|
||||
|
||||
private final WebClient webClient;
|
||||
|
||||
public UserService(WebClient webClient) {
|
||||
this.webClient = webClient;
|
||||
}
|
||||
|
||||
// 단일 사용자 조회 (Mono)
|
||||
public Mono<User> getUser(Long id) {
|
||||
return webClient.get()
|
||||
.uri("/users/{id}", id)
|
||||
.retrieve()
|
||||
.bodyToMono(User.class);
|
||||
}
|
||||
|
||||
// 모든 사용자 조회 (Flux)
|
||||
public Flux<User> getAllUsers() {
|
||||
return webClient.get()
|
||||
.uri("/users")
|
||||
.retrieve()
|
||||
.bodyToFlux(User.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
public class User {
|
||||
private Long id;
|
||||
private String name;
|
||||
private int age;
|
||||
}
|
||||
```
|
||||
|
||||
- **`Mono`**: 단일 결과 예상 시 사용.
|
||||
- **`Flux`**: 다중 결과(리스트 또는 스트림) 예상 시 사용.
|
||||
|
||||
##### (2) POST 요청
|
||||
```java
|
||||
public Mono<User> createUser(User user) {
|
||||
return webClient.post()
|
||||
.uri("/users")
|
||||
.bodyValue(user) // 요청 바디 설정
|
||||
.retrieve()
|
||||
.bodyToMono(User.class);
|
||||
}
|
||||
```
|
||||
|
||||
- **`bodyValue`**: 객체를 JSON으로 직렬화해 전송.
|
||||
|
||||
##### (3) PUT 요청
|
||||
```java
|
||||
public Mono<Void> updateUser(Long id, User user) {
|
||||
return webClient.put()
|
||||
.uri("/users/{id}", id)
|
||||
.bodyValue(user)
|
||||
.retrieve()
|
||||
.bodyToMono(Void.class); // 응답 본문 없음
|
||||
}
|
||||
```
|
||||
|
||||
##### (4) DELETE 요청
|
||||
```java
|
||||
public Mono<Void> deleteUser(Long id) {
|
||||
return webClient.delete()
|
||||
.uri("/users/{id}", id)
|
||||
.retrieve()
|
||||
.bodyToMono(Void.class);
|
||||
}
|
||||
```
|
||||
|
||||
##### (5) 커스텀 요청
|
||||
```java
|
||||
public Mono<User> getUserWithHeaders(Long id) {
|
||||
return webClient.get()
|
||||
.uri("/users/{id}", id)
|
||||
.header("Authorization", "Bearer token123")
|
||||
.accept(MediaType.APPLICATION_JSON)
|
||||
.retrieve()
|
||||
.bodyToMono(User.class);
|
||||
}
|
||||
```
|
||||
|
||||
- **헤더/쿼리 파라미터**: `.header()`, `.uri(builder -> builder.queryParam("key", "value").build())`로 추가.
|
||||
|
||||
##### (6) 컨트롤러에서 사용
|
||||
```java
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
public class UserController {
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
public UserController(UserService userService) {
|
||||
this.userService = userService;
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Mono<User> getUser(@PathVariable Long id) {
|
||||
return userService.getUser(id);
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public Flux<User> getAllUsers() {
|
||||
return userService.getAllUsers();
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Mono<User> createUser(@RequestBody User user) {
|
||||
return userService.createUser(user);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### 5. 주요 메서드 및 빌더 체인
|
||||
- **요청 빌더**:
|
||||
- `.get()`, `.post()`, `.put()`, `.delete()`: HTTP 메서드 지정.
|
||||
- `.uri(String uri, Object... uriVariables)`: URL 설정.
|
||||
- `.header(String name, String value)`: 헤더 추가.
|
||||
- `.bodyValue(Object body)`: 요청 바디 설정.
|
||||
- `.accept(MediaType... mediaTypes)`: 수용 가능한 응답 타입.
|
||||
|
||||
- **응답 처리**:
|
||||
- `.retrieve()`: 응답을 간단히 처리 (4xx, 5xx는 예외 발생).
|
||||
- `.bodyToMono(Class<T> elementClass)`: 단일 객체로 변환.
|
||||
- `.bodyToFlux(Class<T> elementClass)`: 다중 객체 스트림으로 변환.
|
||||
|
||||
---
|
||||
|
||||
#### 6. 에러 처리
|
||||
`WebClient`는 HTTP 오류를 `WebClientResponseException`으로抛출합니다. 이를 처리하거나 커스텀 로직을 추가할 수 있습니다.
|
||||
|
||||
```java
|
||||
public Mono<User> getUserWithErrorHandling(Long id) {
|
||||
return webClient.get()
|
||||
.uri("/users/{id}", id)
|
||||
.retrieve()
|
||||
.onStatus(HttpStatus::is4xxClientError, response ->
|
||||
Mono.error(new RuntimeException("Client Error: " + response.statusCode())))
|
||||
.onStatus(HttpStatus::is5xxServerError, response ->
|
||||
Mono.error(new RuntimeException("Server Error: " + response.statusCode())))
|
||||
.bodyToMono(User.class)
|
||||
.onErrorResume(e -> {
|
||||
log.error("Error occurred: {}", e.getMessage());
|
||||
return Mono.just(new User(-1L, "Unknown", 0)); // 기본값 반환
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- **`onStatus`**: 특정 상태 코드에 대한 처리.
|
||||
- **`onErrorResume`**: 예외 발생 시 대체 로직.
|
||||
|
||||
---
|
||||
|
||||
#### 7. 장점과 한계
|
||||
- **장점**:
|
||||
- 비동기/리액티브로 높은 동시성 처리.
|
||||
- 스트리밍 데이터 지원 (예: 대용량 데이터).
|
||||
- 선언적이고 체인 방식의 API.
|
||||
- **한계**:
|
||||
- 리액티브 프로그래밍에 익숙해야 함 (러닝 커브 존재).
|
||||
- 간단한 동기 작업에서는 `RestTemplate`보다 복잡할 수 있음.
|
||||
|
||||
---
|
||||
|
||||
#### 8. RestTemplate과의 비교
|
||||
- **`RestTemplate`**: 동기식, 간단한 사용, 스프링 5 이전 표준.
|
||||
- **`WebClient`**: 비동기/리액티브, 대규모 트래픽 및 스트리밍에 적합, 스프링 5 이후 권장.
|
||||
- **선택 기준**: 비동기와 성능이 중요한 경우 `WebClient`, 단순 동기 호출이면 `RestTemplate`.
|
||||
|
||||
---
|
||||
|
||||
#### 9. 결론
|
||||
`WebClient`는 스프링 부트에서 현대적인 REST API 호출을 위한 강력한 도구로, 비동기 및 리액티브 특성을 활용해 높은 확장성과 성능을 제공합니다. `Mono`와 `Flux`를 통해 유연한 데이터 처리가 가능하며, 에러 처리와 커스터마이징 옵션도 풍부합니다. 위 예시를 참고하면 기본적인 HTTP 요청부터 복잡한 시나리오까지 쉽게 구현할 수 있습니다. 추가 질문이 있다면 언제든 물어보세요!
|
||||
42
docs/webflux/01_웹플럭스란 무엇인가.md
Normal file
42
docs/webflux/01_웹플럭스란 무엇인가.md
Normal file
@@ -0,0 +1,42 @@
|
||||
아래는 요청하신 **"스프링부트 웹플럭스 시리즈"**의 **1장: 웹플럭스란 무엇인가?**에 대한 초안입니다. 자연스럽고 이해하기 쉬운 문체로 작성했으며, 초보자와 중급 개발자 모두에게 유용할 수 있도록 개념을 명확히 설명했습니다.
|
||||
|
||||
---
|
||||
|
||||
## 1. 웹플럭스란 무엇인가?
|
||||
|
||||
### 스프링 웹플럭스 소개
|
||||
|
||||
스프링 웹플럭스(Spring WebFlux)는 스프링 프레임워크 5에서 처음 도입된 반응형 웹 애플리케이션 개발을 위한 모듈입니다. 기존의 스프링 MVC가 동기적이고 블로킹 방식으로 동작하는 데 반해, 웹플럭스는 비동기와 논블로킹을 기반으로 설계되었습니다. 이는 대규모 트래픽이나 실시간 데이터 처리가 필요한 현대 애플리케이션의 요구사항을 충족하기 위해 탄생한 기술입니다.
|
||||
|
||||
웹플럭스는 리액티브 스트림(Reactive Streams) 사양을 구현하며, 데이터를 스트림 형태로 처리하는 새로운 패러다임을 제공합니다. 이를 통해 개발자는 높은 처리량과 낮은 지연 시간을 요구하는 시스템을 더 효율적으로 구축할 수 있습니다. 스프링부트와 결합하면 설정이 간편해지고, 스프링 생태계의 강력한 기능을 그대로 활용할 수 있다는 점도 큰 장점입니다.
|
||||
|
||||
### 기존 스프링 MVC와의 차이점
|
||||
|
||||
스프링 MVC는 오랫동안 자바 웹 개발의 표준으로 자리 잡아 왔습니다. 하지만 스프링 MVC는 기본적으로 서블릿(Servlet) 기반의 동기 처리 모델을 따릅니다. 즉, 클라이언트의 요청이 들어오면 스레드가 해당 요청을 처리하는 동안 다른 작업을 기다리게 됩니다. 이 방식은 스레드 풀의 크기에 따라 처리 가능한 요청 수가 제한되며, 데이터베이스 호출이나 외부 API 요청처럼 시간이 오래 걸리는 작업에서 병목 현상이 발생할 수 있습니다.
|
||||
|
||||
반면, 웹플럭스는 논블로킹 I/O와 이벤트 루프를 활용합니다. 요청이 들어오면 스레드가 블록되지 않고, 작업이 완료될 때까지 다른 요청을 처리할 수 있습니다. 이를 통해 적은 수의 스레드로도 훨씬 더 많은 요청을 동시에 다룰 수 있습니다. 아래 표는 두 프레임워크의 주요 차이점을 간단히 정리한 것입니다.
|
||||
|
||||
| **특징** | **스프링 MVC** | **스프링 웹플럭스** |
|
||||
|---------------------|---------------------------------|--------------------------------|
|
||||
| 처리 방식 | 동기 및 블로킹 | 비동기 및 논블로킹 |
|
||||
| 기반 기술 | 서블릿 API | Netty, Undertow 등 리액터 기반 |
|
||||
| 데이터 처리 | 단일 응답 (Mono 없음) | 스트림 (Mono, Flux) |
|
||||
| 적합한 환경 | 전통적인 웹 애플리케이션 | 대규모 트래픽, 실시간 처리 |
|
||||
|
||||
### 반응형 프로그래밍(Reactive Programming)의 기본 개념
|
||||
|
||||
웹플럭스를 이해하려면 반응형 프로그래밍의 기본 개념을 알아야 합니다. 반응형 프로그래밍은 데이터 스트림과 이벤트에 반응하며, 비동기적으로 데이터를 처리하는 프로그래밍 패러다임입니다. 전통적인 명령형 프로그래밍이 "어떻게(How)"에 초점을 맞춘다면, 반응형 프로그래밍은 "무엇(What)"에 집중합니다. 즉, 데이터가 발생하거나 변경될 때마다 시스템이 자동으로 반응하도록 설계됩니다.
|
||||
|
||||
스프링 웹플럭스에서는 이를 구현하기 위해 **Mono**와 **Flux**라는 두 가지 핵심 클래스를 사용합니다:
|
||||
- **Mono**: 0 또는 1개의 항목을 발행하는 리액티브 스트림입니다. 예를 들어, 단일 객체를 반환하는 API 호출에 적합합니다.
|
||||
- **Flux**: 0개 이상의 항목을 발행하는 리액티브 스트림입니다. 리스트나 실시간 데이터 스트리밍에 사용됩니다.
|
||||
|
||||
이 두 클래스는 리액티브 스트림의 기반이 되며, 데이터가 준비되면 구독자(Subscriber)가 이를 받아 처리하는 방식으로 동작합니다. 예를 들어, 데이터베이스에서 사용자 정보를 조회한다고 가정하면, Mono는 한 명의 사용자 데이터를, Flux는 여러 사용자 데이터를 스트림으로 제공할 수 있습니다.
|
||||
|
||||
### 마무리
|
||||
|
||||
스프링 웹플럭스는 단순히 "새로운 프레임워크" 이상의 의미를 가집니다. 이는 변화하는 웹 환경에서 더 나은 성능과 확장성을 제공하려는 스프링 팀의 노력의 결과물입니다. 다음 장에서는 웹플럭스가 왜 필요한지, 어떤 상황에서 빛을 발하는지에 대해 더 깊이 탐구해보겠습니다. 웹플럭스의 세계에 첫발을 내디딘 여러분, 준비되셨나요?
|
||||
|
||||
---
|
||||
|
||||
이 글은 개념적인 설명에 중점을 두었으며, 이후 장에서 실습과 코드 예제를 통해 내용을 보강할 수 있도록 여지를 남겼습니다. 추가로 수정하거나 보완하고 싶은 부분이 있다면 말씀해주세요!
|
||||
50
docs/webflux/02_왜 웹플럭스를 사용하는가.md
Normal file
50
docs/webflux/02_왜 웹플럭스를 사용하는가.md
Normal file
@@ -0,0 +1,50 @@
|
||||
아래는 **"스프링부트 웹플럭스 시리즈"**의 **2장: 왜 웹플럭스를 사용하는가?"**에 대한 초안입니다. 1장의 흐름을 이어가며 웹플럭스의 필요성과 실질적인 활용 사례를 중심으로 작성했습니다. 이해하기 쉽고 흥미롭게 읽을 수 있도록 자연스러운 문체를 유지했습니다.
|
||||
|
||||
---
|
||||
|
||||
## 2. 왜 웹플럭스를 사용하는가?
|
||||
|
||||
### 비동기와 논블로킹의 필요성
|
||||
|
||||
현대 웹 애플리케이션은 과거와는 비교할 수 없을 만큼 복잡하고 높은 요구사항을 충족해야 합니다. 사용자는 빠른 응답을 기대하고, 서버는 동시에 수천, 수만 명의 요청을 처리해야 합니다. 전통적인 동기 방식의 스프링 MVC는 이런 환경에서 한계를 드러냅니다. 예를 들어, 데이터베이스 쿼리나 외부 API 호출처럼 시간이 오래 걸리는 작업은 스레드를 점유한 채 대기하게 만들고, 이는 스레드 풀 고갈이나 응답 지연으로 이어질 수 있습니다.
|
||||
|
||||
여기서 비동기와 논블로킹이 등장합니다. 비동기 처리는 작업이 완료될 때까지 기다리지 않고, 다른 작업을 먼저 처리할 수 있게 해줍니다. 논블로킹은 스레드가 작업을 기다리는 동안 멈추지 않고 다른 요청을 처리할 수 있도록 설계된 방식입니다. 웹플럭스는 이 두 가지를 결합해 자원을 더 효율적으로 사용하며, 더 많은 동시 요청을 감당할 수 있는 구조를 제공합니다.
|
||||
|
||||
예를 들어보죠. 전통적인 서블릿 기반 서버에서 100개의 스레드로 100개의 요청을 처리한다고 가정하면, 한 번에 100명까지만 서비스할 수 있습니다. 하지만 웹플럭스는 단일 스레드나 소수의 스레드로도 수천 개의 요청을 처리할 수 있습니다. 이는 마치 레스토랑에서 웨이터 한 명이 여러 테이블을 동시에 관리하는 것과 비슷합니다. 손님(요청)이 음식(응답)을 기다리는 동안, 웨이터(스레드)는 다른 손님을 돕는 셈입니다.
|
||||
|
||||
### 웹플럭스의 주요 사용 사례
|
||||
|
||||
그렇다면 웹플럭스는 언제 사용하면 좋을까요? 아래는 웹플럭스가 특히 빛을 발하는 몇 가지 대표적인 상황입니다.
|
||||
|
||||
1. **대규모 트래픽 처리**
|
||||
- 스트리밍 서비스(넷플릭스, 유튜브)처럼 수많은 사용자가 동시에 데이터를 요청하는 환경에서, 웹플럭스는 자원을 효율적으로 활용해 높은 처리량을 제공합니다.
|
||||
|
||||
2. **실시간 데이터 처리**
|
||||
- 채팅 애플리케이션이나 주식 거래 시스템처럼 실시간으로 데이터를 주고받아야 하는 경우, 웹플럭스는 논블로킹 방식으로 지연을 최소화합니다.
|
||||
|
||||
3. **마이크로서비스 아키텍처**
|
||||
- 여러 서비스가 서로 비동기적으로 통신해야 하는 환경에서, 웹플럭스는 서비스 간 호출을 효율적으로 처리하며 병목 현상을 줄입니다.
|
||||
|
||||
4. **I/O 집약적인 작업**
|
||||
- 데이터베이스 조회, 파일 업로드, 외부 API 호출 등 시간이 오래 걸리는 작업이 많은 애플리케이션에서 웹플럭스는 스레드 낭비를 줄이고 응답성을 높입니다.
|
||||
|
||||
반면, 간단한 CRUD(Create, Read, Update, Delete) 작업만 처리하는 소규모 웹사이트라면 굳이 웹플럭스를 사용할 필요는 없을 수 있습니다. 스프링 MVC로도 충분히 빠르고 유지보수가 쉬울 테니까요. 즉, 웹플럭스는 "모든 상황에 맞는 만능 도구"는 아니지만, 특정 문제에 강력한 해결책을 제시합니다.
|
||||
|
||||
### 성능 이점과 한계
|
||||
|
||||
웹플럭스의 가장 큰 장점은 **높은 확장성**과 **자원 효율성**입니다. Netty와 같은 논블로킹 서버를 기반으로 작동하며, 리액터(Reactor) 프로젝트를 통해 리액티브 스트림을 구현합니다. 이로 인해 CPU와 메모리를 최대한 활용하면서도 응답 속도를 유지할 수 있습니다. 예를 들어, 벤치마크 테스트에서 웹플럭스는 스프링 MVC보다 동일한 하드웨어에서 더 많은 요청을 처리할 수 있는 것으로 나타났습니다.
|
||||
|
||||
하지만 장점만 있는 건 아닙니다. 웹플럭스에는 몇 가지 한계와 고려해야 할 점이 있습니다:
|
||||
- **학습 곡선**: 반응형 프로그래밍은 기존의 명령형 프로그래밍과 사고방식이 달라 초보자에게는 낯설 수 있습니다.
|
||||
- **디버깅의 복잡성**: 비동기 흐름을 추적하다 보면 에러가 어디서 발생했는지 파악하기 어려울 때가 있습니다.
|
||||
- **모든 환경에 적합하지 않음**: CPU 집약적인 작업(예: 복잡한 수학 연산)이 주를 이루는 경우, 논블로킹의 이점이 크지 않을 수 있습니다.
|
||||
|
||||
결국 웹플럭스를 선택할 때는 애플리케이션의 요구사항과 팀의 기술 수준을 고려해야 합니다. "최신 기술이니까 써본다"는 접근보다는, 실제로 문제를 해결할 수 있는 도구인지 냉정히 판단하는 게 중요합니다.
|
||||
|
||||
### 마무리
|
||||
|
||||
웹플럭스는 단순히 트렌드에 편승한 기술이 아니라, 현대 웹 개발의 도전 과제를 해결하기 위한 실용적인 도구입니다. 비동기와 논블로킹을 통해 자원을 아끼고 성능을 끌어올리는 이 프레임워크는, 특히 대규모 시스템에서 그 진가를 발휘합니다. 다음 장에서는 웹플럭스를 직접 사용해보는 첫걸음을 내딛으며, 이론을 실습으로 연결해보겠습니다. 준비되셨죠?
|
||||
|
||||
---
|
||||
|
||||
이 장은 웹플럭스의 필요성과 실무적 가치를 강조하며, 독자가 "왜 써야 하는지"에 공감할 수 있도록 구체적인 사례와 함께 설명했습니다. 추가로 다루고 싶은 내용이나 조정할 부분이 있다면 말씀해주세요!
|
||||
120
docs/webflux/03_스프링부트 웹플럭스 시작하기.md
Normal file
120
docs/webflux/03_스프링부트 웹플럭스 시작하기.md
Normal file
@@ -0,0 +1,120 @@
|
||||
아래는 **"스프링부트 웹플럭스 시리즈"**의 **3장: 스프링부트 웹플럭스 시작하기"**에 대한 초안입니다. Gradle을 사용한 설정 예시와 간략한 코드를 포함하여 실습 중심으로 작성했습니다. 초보자도 따라 할 수 있도록 단계별로 설명하며, 자연스럽고 친근한 문체를 유지했습니다.
|
||||
|
||||
---
|
||||
|
||||
## 3. 스프링부트 웹플럭스 시작하기
|
||||
|
||||
이제 이론을 넘어 직접 손을 움직여볼 시간입니다. 이 장에서는 스프링부트 웹플럭스를 사용해 간단한 애플리케이션을 만들어보며, 환경 설정부터 기본 라우팅까지 경험해보겠습니다. Gradle을 빌드 도구로 사용하며, 코드도 최대한 간결하게 유지하겠습니다. 자, 시작해볼까요?
|
||||
|
||||
### 개발 환경 설정 (Gradle 의존성 추가)
|
||||
|
||||
먼저, 스프링부트 웹플럭스 프로젝트를 만들려면 필요한 의존성을 추가해야 합니다. 스프링 이니셜라이저(Spring Initializr)를 사용하거나, 기존 프로젝트에 설정을 추가할 수 있습니다. 여기서는 Gradle 기반으로 진행합니다.
|
||||
|
||||
`build.gradle` 파일을 열고 아래 내용을 추가하세요:
|
||||
|
||||
```gradle
|
||||
plugins {
|
||||
id 'org.springframework.boot' version '3.2.4'
|
||||
id 'io.spring.dependency-management' version '1.1.4'
|
||||
id 'java'
|
||||
}
|
||||
|
||||
group = 'com.example'
|
||||
version = '0.0.1-SNAPSHOT'
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testImplementation 'io.projectreactor:reactor-test'
|
||||
}
|
||||
|
||||
test {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
```
|
||||
|
||||
- `spring-boot-starter-webflux`: 웹플럭스 기본 의존성으로, Netty 서버와 리액터 라이브러리를 포함합니다.
|
||||
- `spring-boot-starter-test`와 `reactor-test`: 테스트를 위한 의존성입니다.
|
||||
|
||||
의존성을 추가한 뒤, 터미널에서 `./gradlew build`를 실행해 의존성이 잘 다운로드되는지 확인하세요.
|
||||
|
||||
### 첫 번째 웹플럭스 애플리케이션 만들기
|
||||
|
||||
스프링부트는 기본적으로 실행 가능한 애플리케이션을 빠르게 만들 수 있도록 도와줍니다. 프로젝트 루트 패키지에 `Application` 클래스를 생성하고, 아래처럼 설정합니다:
|
||||
|
||||
```java
|
||||
package com.example.demo;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
@SpringBootApplication
|
||||
public class Application {
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
이제 애플리케이션을 실행해보세요. `./gradlew bootRun` 명령어를 입력하면 기본 포트(8080)에서 서버가 시작됩니다. 아직 아무 기능은 없지만, 웹플럭스 기반 애플리케이션이 구동된 겁니다!
|
||||
|
||||
### 기본적인 라우팅과 컨트롤러 설정
|
||||
|
||||
이제 간단한 엔드포인트를 추가해보겠습니다. 웹플럭스에서는 두 가지 방식으로 라우팅을 처리할 수 있는데, 여기서는 어노테이션 기반 컨트롤러를 먼저 사용해보겠습니다. 함수형 라우팅은 나중에 다룰게요.
|
||||
|
||||
`src/main/java/com/example/demo` 경로에 `HelloController` 클래스를 생성하고, 아래 코드를 추가하세요:
|
||||
|
||||
```java
|
||||
package com.example.demo;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@RestController
|
||||
public class HelloController {
|
||||
|
||||
@GetMapping("/hello")
|
||||
public Mono<String> sayHello() {
|
||||
return Mono.just("Hello, WebFlux!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `@RestController`: REST API를 처리하는 컨트롤러임을 나타냅니다.
|
||||
- `Mono<String>`: 단일 문자열을 비동기적으로 반환합니다.
|
||||
|
||||
애플리케이션을 다시 실행한 뒤, 브라우저나 Postman에서 `http://localhost:8080/hello`로 접속해보세요. "Hello, WebFlux!"라는 응답이 나타날 겁니다. 이렇게 간단한 코드로 비동기 엔드포인트를 만들어보았습니다!
|
||||
|
||||
혹시 더 실험해보고 싶다면, 아래처럼 약간의 지연을 추가해볼 수도 있습니다:
|
||||
|
||||
```java
|
||||
@GetMapping("/hello-delay")
|
||||
public Mono<String> sayHelloWithDelay() {
|
||||
return Mono.just("Hello with delay!")
|
||||
.delayElement(Duration.ofSeconds(2));
|
||||
}
|
||||
```
|
||||
|
||||
`/hello-delay`로 접속하면 2초 뒤에 응답이 오는 걸 확인할 수 있습니다. 이건 웹플럭스가 비동기적으로 동작한다는 걸 느끼게 해주는 작은 예제입니다.
|
||||
|
||||
### 잠깐 확인하기
|
||||
|
||||
지금까지 한 걸 정리해보면:
|
||||
1. Gradle로 웹플럭스 의존성을 설정했습니다.
|
||||
2. 기본 애플리케이션을 실행해봤고,
|
||||
3. 간단한 컨트롤러로 첫 번째 엔드포인트를 만들었습니다.
|
||||
|
||||
별로 어렵지 않죠? 스프링부트가 복잡한 설정을 대신 처리해주기 때문에, 우리는 핵심 로직에 집중할 수 있습니다. 웹플럭스는 처음엔 낯설 수 있지만, 이런 작은 성공 경험을 쌓다 보면 금방 익숙해질 겁니다.
|
||||
|
||||
### 마무리
|
||||
|
||||
이 장에서는 웹플럭스 애플리케이션의 기본 틀을 만들어보며 첫발을 내디뎠습니다. 다음 장에서는 `Mono`와 `Flux`를 더 깊이 파고들며, 리액티브 스트림의 매력을 본격적으로 탐구해보겠습니다. 직접 코드를 작성해본 느낌이 어떠신가요? 다음 단계로 넘어갈 준비가 되셨길 바랍니다!
|
||||
|
||||
---
|
||||
|
||||
이 글은 실습 위주로 간결하게 작성했으며, 초보자가 따라 하기 쉽도록 Gradle 설정과 코드 예제를 최소화했습니다. 추가 설명이나 다른 예제가 필요하면 말씀해주세요!
|
||||
91
docs/webflux/04_리액티브 데이터 스트림 이해.md
Normal file
91
docs/webflux/04_리액티브 데이터 스트림 이해.md
Normal file
@@ -0,0 +1,91 @@
|
||||
아래는 **"스프링부트 웹플럭스 시리즈"**의 **4장: 리액티브 데이터 스트림 이해"**에 대한 초안입니다. 3장에서의 실습을 바탕으로 `Mono`와 `Flux`를 중심으로 리액티브 스트림의 개념을 설명하고, 간단한 예제를 포함했습니다. 코드도 간략히 유지하며 초보자가 이해하기 쉽게 작성했습니다.
|
||||
|
||||
---
|
||||
|
||||
## 4. 리액티브 데이터 스트림 이해
|
||||
|
||||
3장에서 우리는 첫 번째 웹플럭스 애플리케이션을 만들며 `Mono`를 살짝 맛봤습니다. 이번 장에서는 웹플럭스의 핵심인 리액티브 데이터 스트림을 깊이 파고들어, `Mono`와 `Flux`가 무엇인지, 어떻게 동작하는지 알아보겠습니다. 실습도 곁들이며 개념을 확실히 잡아볼게요. 준비되셨죠?
|
||||
|
||||
### Mono와 Flux의 개념
|
||||
|
||||
리액티브 프로그래밍의 세계에서 데이터는 "스트림" 형태로 흐릅니다. 이 스트림을 다루는 두 가지 주요 도구가 바로 `Mono`와 `Flux`입니다. 둘 다 스프링 웹플럭스가 사용하는 리액터(Reactor) 라이브러리에서 제공되며, 비동기 데이터 처리를 가능하게 합니다.
|
||||
|
||||
- **Mono**: 0개 또는 1개의 항목을 발행하는 스트림입니다. 단일 값을 반환할 때 유용합니다. 예를 들어, 데이터베이스에서 한 명의 사용자 정보를 조회하거나, 외부 API에서 단일 응답을 받을 때 사용합니다.
|
||||
- **Flux**: 0개 이상의 항목을 발행하는 스트림입니다. 여러 개의 데이터를 순차적으로 처리하거나, 실시간으로 계속 들어오는 데이터를 다룰 때 적합합니다. 리스트나 스트리밍 데이터가 대표적인 예입니다.
|
||||
|
||||
쉽게 말해, `Mono`는 "하나 아니면 없음"이고, `Flux`는 "여러 개가 올 수도 있음"이라고 생각하면 됩니다. 이 둘은 리액티브 스트림의 기본 빌딩 블록으로, 데이터를 비동기적으로 생성하고 구독자가 이를 소비하는 구조를 만듭니다.
|
||||
|
||||
### 리액티브 스트림의 동작 원리
|
||||
|
||||
리액티브 스트림은 "발행-구독(Publish-Subscribe)" 모델을 따릅니다. 데이터가 준비되면 발행자(Publisher)가 이를 발행하고, 구독자(Subscriber)가 그 데이터를 받아 처리합니다. 이 과정에서 중요한 점은 **비동기성**과 **백프레셔(Backpressure)**입니다.
|
||||
|
||||
- **비동기성**: 데이터가 준비되는 즉시 처리하지 않고, 준비 완료 시점에 맞춰 반응합니다. 스레드가 블록되지 않으니 자원을 효율적으로 사용할 수 있죠.
|
||||
- **백프레셔**: 구독자가 데이터 처리 속도를 조절할 수 있게 해줍니다. 예를 들어, 데이터가 너무 빨리 쏟아지면 "천천히 보내줘"라고 요청할 수 있는 셈입니다.
|
||||
|
||||
`Mono`와 `Flux`는 이런 원리를 구현한 도구로, 우리가 작성하는 코드는 이 흐름을 정의하는 역할을 합니다.
|
||||
|
||||
### 간단한 예제로 배우는 Mono와 Flux
|
||||
|
||||
이제 직접 코드를 보며 느낌을 익혀보겠습니다. 3장에서 만든 프로젝트에 새 컨트롤러를 추가하거나, 별도의 클래스에서 테스트해볼 수 있습니다. 여기서는 간단히 컨트롤러로 예제를 작성합니다.
|
||||
|
||||
`src/main/java/com/example/demo`에 `StreamController`를 만들고 아래 코드를 추가하세요:
|
||||
|
||||
```java
|
||||
package com.example.demo;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@RestController
|
||||
public class StreamController {
|
||||
|
||||
@GetMapping("/mono")
|
||||
public Mono<String> getMono() {
|
||||
return Mono.just("This is a Mono!")
|
||||
.delayElement(Duration.ofSeconds(1));
|
||||
}
|
||||
|
||||
@GetMapping("/flux")
|
||||
public Flux<String> getFlux() {
|
||||
return Flux.just("This", "is", "a", "Flux!")
|
||||
.delayElements(Duration.ofSeconds(1));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `/mono` 엔드포인트: `Mono`로 단일 문자열을 1초 지연 후 반환합니다. 접속하면 1초 뒤에 "This is a Mono!"가 표시됩니다.
|
||||
- `/flux` 엔드포인트: `Flux`로 여러 문자열을 1초 간격으로 순차적으로 발행합니다. 접속하면 "This", "is", "a", "Flux!"가 1초씩 간격을 두고 나타납니다(클라이언트에 따라 한꺼번에 보일 수도 있음).
|
||||
|
||||
애플리케이션을 실행하고 `http://localhost:8080/mono`와 `http://localhost:8080/flux`를 브라우저나 Postman으로 테스트해보세요. `Mono`는 한 번에 끝나고, `Flux`는 여러 값을 순차적으로 보내는 걸 느낄 수 있습니다.
|
||||
|
||||
더 재미있는 예제로, 숫자 스트림을 만들어볼까요?
|
||||
|
||||
```java
|
||||
@GetMapping("/numbers")
|
||||
public Flux<Integer> getNumbers() {
|
||||
return Flux.range(1, 5)
|
||||
.delayElements(Duration.ofSeconds(1));
|
||||
}
|
||||
```
|
||||
|
||||
`/numbers`에 접속하면 1부터 5까지 숫자가 1초 간격으로 출력됩니다. 이런 식으로 `Flux`는 연속적인 데이터를 다룰 때 강력합니다.
|
||||
|
||||
### 잠깐 정리
|
||||
|
||||
- `Mono`: 단일 데이터(0 또는 1개)를 비동기적으로 처리.
|
||||
- `Flux`: 여러 데이터(0개 이상)를 스트림으로 처리.
|
||||
- 동작: 발행자가 데이터를 준비하면 구독자가 받아서 처리하며, 비동기와 백프레셔로 효율성을 높임.
|
||||
|
||||
이 예제들은 아주 기본적인 수준이지만, 리액티브 스트림의 흐름을 이해하는 데 큰 도움이 됩니다. 실제로는 데이터베이스나 외부 API와 연동할 때 더 빛을 발휘하죠.
|
||||
|
||||
### 마무리
|
||||
|
||||
`Mono`와 `Flux`는 웹플럭스의 심장과도 같습니다. 이 두 도구를 잘 다룰 수 있다면, 비동기 프로그래밍의 세계가 훨씬 친근하게 느껴질 겁니다. 다음 장에서는 웹플럭스의 라우팅 방식을 더 깊이 탐구하며, 함수형 접근법도 함께 살펴보겠습니다. 이번 장에서 스트림의 맛을 보셨으니, 다음 단계가 더 기대되시죠?
|
||||
|
||||
---
|
||||
|
||||
이 장은 개념 설명과 간단한 실습을 균형 있게 배치해 초보자가 부담 없이 따라 할 수 있도록 했습니다. 코드도 최소화했으며, 추가 예제나 설명이 필요하면 언제든 말씀해주세요!
|
||||
121
docs/webflux/05_웹플럭스에서 라우팅 처리.md
Normal file
121
docs/webflux/05_웹플럭스에서 라우팅 처리.md
Normal file
@@ -0,0 +1,121 @@
|
||||
아래는 **"스프링부트 웹플럭스 시리즈"**의 **5장: 웹플럭스에서 라우팅 처리"**에 대한 초안입니다. 4장의 흐름을 이어가며 웹플럭스의 두 가지 라우팅 방식(함수형 라우팅과 어노테이션 기반 컨트롤러)을 비교하고, 실습 예제를 간략히 포함했습니다. 초보자가 이해하기 쉽도록 단계별로 설명하며 자연스러운 문체를 유지했습니다.
|
||||
|
||||
---
|
||||
|
||||
## 5. 웹플럭스에서 라우팅 처리
|
||||
|
||||
지난 장에서 `Mono`와 `Flux`로 리액티브 스트림의 기본을 익혔습니다. 이제 이 스트림을 클라이언트 요청에 연결하는 방법을 배워볼 차례입니다. 웹플럭스에서는 라우팅을 처리하는 두 가지 주요 방식이 있습니다: **함수형 라우팅(RouterFunction)**과 **어노테이션 기반 컨트롤러**. 이번 장에서는 두 접근법을 살펴보고, 각각의 장단점을 비교하며 간단한 예제를 작성해보겠습니다. 자, 시작합시다!
|
||||
|
||||
### RouterFunction을 사용한 함수형 라우팅
|
||||
|
||||
웹플럭스는 함수형 프로그래밍 스타일을 지원하며, `RouterFunction`을 통해 라우팅을 정의할 수 있습니다. 이 방식은 선언적으로 경로와 핸들러를 연결하며, 코드가 간결하고 유연하다는 장점이 있습니다. 전통적인 컨트롤러보다 더 리액티브 철학에 가까운 접근법이죠.
|
||||
|
||||
먼저, 3장에서 만든 프로젝트에 새로운 클래스를 추가합니다. `src/main/java/com/example/demo` 경로에 `RouteConfig` 클래스를 만들고 아래 코드를 작성해보세요:
|
||||
|
||||
```java
|
||||
package com.example.demo;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.reactive.function.server.RouterFunction;
|
||||
import org.springframework.web.reactive.function.server.ServerResponse;
|
||||
|
||||
import static org.springframework.web.reactive.function.server.RequestPredicates.GET;
|
||||
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
|
||||
|
||||
@Configuration
|
||||
public class RouteConfig {
|
||||
|
||||
@Bean
|
||||
public RouterFunction<ServerResponse> routes() {
|
||||
return route(GET("/functional/hello"),
|
||||
request -> ServerResponse.ok().bodyValue("Hello from RouterFunction!"));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `@Configuration`: 스프링 설정 클래스임을 나타냅니다.
|
||||
- `RouterFunction<ServerResponse>`: 요청 경로와 응답을 정의합니다.
|
||||
- `route()`: `/functional/hello`로 들어오는 GET 요청에 대해 "Hello from RouterFunction!"를 반환합니다.
|
||||
|
||||
애플리케이션을 실행하고 `http://localhost:8080/functional/hello`로 접속해보세요. 간단한 문자열 응답이 표시될 겁니다. 이 방식은 컨트롤러 클래스 없이도 라우팅을 처리할 수 있어, 코드 구조를 간소화할 수 있습니다.
|
||||
|
||||
### 어노테이션 기반 컨트롤러와의 비교
|
||||
|
||||
4장에서 사용했던 어노테이션 기반 컨트롤러는 스프링 MVC와 비슷한 방식으로 동작합니다. `@RestController`, `@GetMapping` 같은 어노테이션을 사용해 직관적으로 엔드포인트를 정의하죠. 비교를 위해 이전 예제를 다시 보겠습니다:
|
||||
|
||||
```java
|
||||
package com.example.demo;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@RestController
|
||||
public class HelloController {
|
||||
|
||||
@GetMapping("/hello")
|
||||
public Mono<String> sayHello() {
|
||||
return Mono.just("Hello, WebFlux!");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
이제 두 방식의 차이를 표로 정리해보겠습니다:
|
||||
|
||||
| **특징** | **어노테이션 기반 컨트롤러** | **RouterFunction (함수형)** |
|
||||
|---------------------|----------------------------------|----------------------------------|
|
||||
| **문법** | `@GetMapping`, `@PostMapping` 등 | `route()`, `GET()`, `POST()` 등 |
|
||||
| **구조** | 클래스와 메서드 중심 | 함수와 람다 중심 |
|
||||
| **익숙함** | MVC 경험자라면 쉽게 적응 | 함수형 프로그래밍에 익숙해야 함 |
|
||||
| **유연성** | 정해진 구조로 간단히 작성 | 동적 라우팅이나 조건부 처리에 강점 |
|
||||
| **용도** | 전통적인 REST API에 적합 | 리액티브 철학을 살린 경량 애플리케이션 |
|
||||
|
||||
어노테이션 방식은 익숙하고 직관적이어서 초보자에게 추천할 만합니다. 반면, 함수형 라우팅은 더 가볍고 유연하며, 대규모 프로젝트에서 라우팅 로직을 한 곳에 모아 관리하기 좋습니다.
|
||||
|
||||
### 요청 처리와 응답 생성 실습
|
||||
|
||||
함수형 라우팅으로 조금 더 실습해보죠. 이번엔 요청 파라미터를 받아 응답을 만들어봅시다. `RouteConfig`에 새 경로를 추가합니다:
|
||||
|
||||
```java
|
||||
@Bean
|
||||
public RouterFunction<ServerResponse> routes() {
|
||||
return route(GET("/functional/greet/{name}"),
|
||||
request -> {
|
||||
String name = request.pathVariable("name");
|
||||
return ServerResponse.ok().bodyValue("Hello, " + name + "!");
|
||||
})
|
||||
.andRoute(GET("/functional/hello"),
|
||||
request -> ServerResponse.ok().bodyValue("Hello from RouterFunction!"));
|
||||
}
|
||||
```
|
||||
|
||||
- `/functional/greet/{name}`: URL 경로에서 `name` 변수를 추출해 맞춤형 인사말을 반환합니다.
|
||||
|
||||
`http://localhost:8080/functional/greet/Alice`로 접속하면 "Hello, Alice!"가 출력됩니다. 이렇게 함수형 라우팅은 요청 데이터를 쉽게 처리하고, `Mono`나 `Flux`로 응답을 확장할 수도 있습니다.
|
||||
|
||||
어노테이션 방식으로 동일한 기능을 구현한다면:
|
||||
|
||||
```java
|
||||
@GetMapping("/greet/{name}")
|
||||
public Mono<String> greet(@PathVariable String name) {
|
||||
return Mono.just("Hello, " + name + "!");
|
||||
}
|
||||
```
|
||||
|
||||
두 방식 모두 결과는 같지만, 코드 스타일과 구조에서 차이가 느껴지죠?
|
||||
|
||||
### 어떤 걸 선택해야 할까?
|
||||
|
||||
- **어노테이션 기반**: 팀이 MVC에 익숙하거나, 빠르게 REST API를 만들고 싶을 때.
|
||||
- **함수형 라우팅**: 리액티브의 장점을 극대화하거나, 경량화된 라우팅 로직이 필요할 때.
|
||||
|
||||
프로젝트 성격과 팀의 선호도에 따라 선택하면 됩니다. 처음엔 어노테이션 방식으로 시작해, 익숙해지면 함수형으로 넘어가보는 것도 좋은 전략입니다.
|
||||
|
||||
### 마무리
|
||||
|
||||
이번 장에서 웹플럭스의 라우팅 방식을 두 가지 맛으로 경험해봤습니다. 어노테이션 기반은 친숙하고, 함수형은 새롭고 강력하죠. 다음 장에서는 리액티브 데이터베이스와의 연동을 다루며, 웹플럭스의 실무적인 면모를 더 깊이 탐구해보겠습니다. 라우팅의 맛을 보셨으니, 이제 데이터와 연결해볼 준비 되셨나요?
|
||||
|
||||
---
|
||||
|
||||
이 장은 두 라우팅 방식의 차이를 명확히 설명하고, 간단한 실습으로 실질적인 이해를 돕도록 구성했습니다. 코드도 최소화했으며, 추가 예제나 조정이 필요하면 말씀해주세요!
|
||||
194
docs/webflux/06_리액티브 데이터베이스 연동.md
Normal file
194
docs/webflux/06_리액티브 데이터베이스 연동.md
Normal file
@@ -0,0 +1,194 @@
|
||||
아래는 **"스프링부트 웹플럭스 시리즈"**의 **6장: 리액티브 데이터베이스 연동"**에 대한 초안입니다. MariaDB를 예시로 사용하며, R2DBC를 통해 리액티브 방식으로 연동합니다. 설정은 `application.yaml`을 사용하고, 간단한 실습 코드를 포함했습니다. 초보자도 따라 할 수 있도록 단계별로 설명하며 친근한 문체를 유지했습니다.
|
||||
|
||||
---
|
||||
|
||||
## 6. 리액티브 데이터베이스 연동
|
||||
|
||||
이제 웹플럭스의 리액티브 특성을 데이터베이스와 연결해볼 시간입니다. 이번 장에서는 MariaDB를 리액티브하게 다루는 방법을 배워보겠습니다. 전통적인 JDBC 대신 R2DBC(Reactive Relational Database Connectivity)를 사용하며, 설정은 `application.yaml`로 깔끔하게 정리하겠습니다. 데이터 조회와 저장 예제도 함께 해보며 실습을 진행할게요. 준비되셨죠?
|
||||
|
||||
### R2DBC와 리액티브 리포지토리 설정
|
||||
|
||||
R2DBC는 관계형 데이터베이스를 비동기/논블로킹 방식으로 접근하게 해주는 드라이버입니다. 웹플럭스와 찰떡궁합이죠. 먼저, MariaDB와 R2DBC를 사용하기 위해 Gradle에 의존성을 추가합니다. `build.gradle`에 아래를 추가하세요:
|
||||
|
||||
```gradle
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
|
||||
implementation 'io.r2dbc:r2dbc-mariadb:1.1.3'
|
||||
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
|
||||
}
|
||||
```
|
||||
|
||||
- `spring-boot-starter-data-r2dbc`: R2DBC와 스프링 데이터 통합을 위한 스타터.
|
||||
- `r2dbc-mariadb`: MariaDB용 R2DBC 드라이버.
|
||||
|
||||
의존성을 추가한 뒤, `./gradlew build`로 빌드해줍니다.
|
||||
|
||||
다음으로, `application.yaml`을 설정합니다. `src/main/resources`에 `application.yaml` 파일을 만들고 아래 내용을 입력하세요:
|
||||
|
||||
```yaml
|
||||
spring:
|
||||
r2dbc:
|
||||
url: r2dbc:mariadb://localhost:3306/webflux_db
|
||||
username: root
|
||||
password: yourpassword
|
||||
data:
|
||||
r2dbc:
|
||||
repositories:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
- `url`: MariaDB 데이터베이스 연결 정보 (예: `webflux_db`라는 데이터베이스 사용).
|
||||
- `username`, `password`: MariaDB 접속 계정 정보 (환경에 맞게 수정하세요).
|
||||
|
||||
MariaDB에 `webflux_db` 데이터베이스를 미리 만들어야 합니다. MariaDB 클라이언트에서 다음 명령어를 실행하세요:
|
||||
|
||||
```sql
|
||||
CREATE DATABASE webflux_db;
|
||||
```
|
||||
|
||||
### MariaDB와의 연동 예제
|
||||
|
||||
이제 간단한 사용자(User) 데이터를 저장하고 조회하는 예제를 만들어보겠습니다. 먼저, 테이블을 생성합니다. MariaDB에서 아래 SQL을 실행하세요:
|
||||
|
||||
```sql
|
||||
USE webflux_db;
|
||||
CREATE TABLE users (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(255) NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
#### 엔티티 클래스 작성
|
||||
|
||||
`src/main/java/com/example/demo`에 `User` 클래스를 추가합니다:
|
||||
|
||||
```java
|
||||
package com.example.demo;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.data.annotation.Id;
|
||||
|
||||
@Data
|
||||
public class User {
|
||||
@Id
|
||||
private Long id;
|
||||
private String name;
|
||||
private String email;
|
||||
}
|
||||
```
|
||||
|
||||
- `@Id`: 기본 키를 나타냅니다.
|
||||
- `@Data`: Lombok으로 getter/setter 등을 자동 생성.
|
||||
|
||||
#### 리포지토리 인터페이스 생성
|
||||
|
||||
`src/main/java/com/example/demo`에 `UserRepository` 인터페이스를 만듭니다:
|
||||
|
||||
```java
|
||||
package com.example.demo;
|
||||
|
||||
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
public interface UserRepository extends R2dbcRepository<User, Long> {
|
||||
Mono<User> findByEmail(String email);
|
||||
}
|
||||
```
|
||||
|
||||
- `R2dbcRepository`: 리액티브 CRUD 작업을 지원.
|
||||
- `findByEmail`: 이메일로 사용자 조회 메서드 추가.
|
||||
|
||||
#### 데이터 저장 및 조회 실습
|
||||
|
||||
컨트롤러를 만들어 데이터를 다뤄봅시다. `src/main/java/com/example/demo`에 `UserController`를 추가합니다:
|
||||
|
||||
```java
|
||||
package com.example.demo;
|
||||
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/users")
|
||||
public class UserController {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public UserController(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Mono<User> createUser(@RequestBody User user) {
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Mono<User> getUserById(@PathVariable Long id) {
|
||||
return userRepository.findById(id);
|
||||
}
|
||||
|
||||
@GetMapping("/email/{email}")
|
||||
public Mono<User> getUserByEmail(@PathVariable String email) {
|
||||
return userRepository.findByEmail(email);
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public Flux<User> getAllUsers() {
|
||||
return userRepository.findAll();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `createUser`: 새 사용자를 저장.
|
||||
- `getUserById`: ID로 사용자 조회.
|
||||
- `getUserByEmail`: 이메일로 사용자 조회.
|
||||
- `getAllUsers`: 모든 사용자 목록 반환.
|
||||
|
||||
#### 테스트해보기
|
||||
|
||||
애플리케이션을 실행한 뒤, Postman이나 curl로 테스트해봅시다.
|
||||
|
||||
1. **사용자 추가**:
|
||||
```
|
||||
POST http://localhost:8080/users
|
||||
Content-Type: application/json
|
||||
{"name": "Alice", "email": "alice@example.com"}
|
||||
```
|
||||
응답으로 저장된 사용자 객체가 반환됩니다.
|
||||
|
||||
2. **사용자 조회 (ID)**:
|
||||
```
|
||||
GET http://localhost:8080/users/1
|
||||
```
|
||||
ID가 1인 사용자가 반환됩니다.
|
||||
|
||||
3. **사용자 조회 (이메일)**:
|
||||
```
|
||||
GET http://localhost:8080/users/email/alice@example.com
|
||||
```
|
||||
이메일로 조회한 결과가 나옵니다.
|
||||
|
||||
4. **모든 사용자 조회**:
|
||||
```
|
||||
GET http://localhost:8080/users
|
||||
```
|
||||
저장된 모든 사용자가 리스트로 표시됩니다.
|
||||
|
||||
### 잠깐 정리
|
||||
|
||||
- **R2DBC**: MariaDB를 리액티브하게 연결.
|
||||
- **`application.yaml`**: 간단한 설정으로 데이터베이스 연결 완료.
|
||||
- **리포지토리**: 비동기 CRUD 작업을 쉽게 처리.
|
||||
|
||||
이 과정에서 `Mono`와 `Flux`가 데이터베이스 작업에 자연스럽게 녹아든 걸 느끼셨을 겁니다. 전통적인 JDBC와 달리 스레드가 블록되지 않아 더 많은 요청을 효율적으로 처리할 수 있죠.
|
||||
|
||||
### 마무리
|
||||
|
||||
MariaDB와의 리액티브 연동을 성공적으로 해냈습니다! 데이터베이스 작업이 웹플럭스의 비동기 흐름과 어떻게 맞물리는지 실감 나셨나요? 다음 장에서는 이 데이터를 기반으로 REST API를 설계하며, 실무에서 더 유용한 기능을 추가해보겠습니다. 이번 실습이 재미있으셨길 바랍니다!
|
||||
|
||||
---
|
||||
|
||||
이 장은 MariaDB와 R2DBC를 활용한 실습 중심으로 구성했으며, `application.yaml` 설정과 간단한 코드를 포함했습니다. 추가 예제나 수정이 필요하면 말씀해주세요!
|
||||
171
docs/webflux/07_웹플럭스와 REST API 설계.md
Normal file
171
docs/webflux/07_웹플럭스와 REST API 설계.md
Normal file
@@ -0,0 +1,171 @@
|
||||
아래는 **"스프링부트 웹플럭스 시리즈"**의 **7장: 웹플럭스와 REST API 설계"**에 대한 초안입니다. 6장에서 MariaDB와의 연동을 바탕으로 REST API를 설계하며, 상태 코드와 에러 처리를 포함한 실습을 진행합니다. 코드도 간략히 유지하며 초보자가 따라 하기 쉽게 자연스러운 문체로 작성했습니다.
|
||||
|
||||
---
|
||||
|
||||
## 7. 웹플럭스와 REST API 설계
|
||||
|
||||
6장에서 MariaDB와 리액티브하게 연동하며 데이터를 다룰 준비를 마쳤습니다. 이제 이 데이터를 기반으로 실전에서 사용할 수 있는 REST API를 설계해보겠습니다. 단순히 데이터를 주고받는 데 그치지 않고, 상태 코드와 에러 처리까지 추가해 클라이언트와의 비동기 통신을 완성도 있게 구현해볼게요. 시작합시다!
|
||||
|
||||
### RESTful 엔드포인트 구현
|
||||
|
||||
REST API는 자원을 중심으로 설계되며, HTTP 메서드(GET, POST, PUT, DELETE)를 사용해 CRUD 작업을 처리합니다. 6장의 `UserController`를 확장해 RESTful 방식으로 개선해보겠습니다. 기존 코드를 조금 수정하고 기능을 추가합니다.
|
||||
|
||||
`src/main/java/com/example/demo/UserController.java`를 아래처럼 업데이트하세요:
|
||||
|
||||
```java
|
||||
package com.example.demo;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/users")
|
||||
public class UserController {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public UserController(UserRepository userRepository) {
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@ResponseStatus(HttpStatus.CREATED)
|
||||
public Mono<User> createUser(@RequestBody User user) {
|
||||
return userRepository.save(user);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public Mono<User> getUserById(@PathVariable Long id) {
|
||||
return userRepository.findById(id);
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public Flux<User> getAllUsers() {
|
||||
return userRepository.findAll();
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public Mono<User> updateUser(@PathVariable Long id, @RequestBody User user) {
|
||||
return userRepository.findById(id)
|
||||
.flatMap(existingUser -> {
|
||||
existingUser.setName(user.getName());
|
||||
existingUser.setEmail(user.getEmail());
|
||||
return userRepository.save(existingUser);
|
||||
});
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
public Mono<Void> deleteUser(@PathVariable Long id) {
|
||||
return userRepository.deleteById(id);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `POST /users`: 새 사용자 생성 (201 Created 상태 코드 반환).
|
||||
- `GET /users/{id}`: ID로 사용자 조회.
|
||||
- `GET /users`: 모든 사용자 목록 조회.
|
||||
- `PUT /users/{id}`: 기존 사용자 정보 업데이트.
|
||||
- `DELETE /users/{id}`: 사용자 삭제 (204 No Content 반환).
|
||||
|
||||
이제 API가 RESTful 원칙을 따르며, 각 엔드포인트가 명확한 역할을 수행합니다.
|
||||
|
||||
### 상태 코드와 에러 처리
|
||||
|
||||
REST API에서 상태 코드는 클라이언트에게 작업 결과를 알려주는 중요한 신호입니다. 하지만 데이터가 없거나 오류가 발생할 때도 적절히 대응해야죠. 예를 들어, 존재하지 않는 사용자를 조회하려 하면 404 Not Found를 반환하도록 해보겠습니다.
|
||||
|
||||
`UserController`에 에러 처리를 추가합니다:
|
||||
|
||||
```java
|
||||
@GetMapping("/{id}")
|
||||
public Mono<User> getUserById(@PathVariable Long id) {
|
||||
return userRepository.findById(id)
|
||||
.switchIfEmpty(Mono.error(new UserNotFoundException("User with ID " + id + " not found")));
|
||||
}
|
||||
|
||||
@ExceptionHandler(UserNotFoundException.class)
|
||||
@ResponseStatus(HttpStatus.NOT_FOUND)
|
||||
public Mono<String> handleUserNotFound(UserNotFoundException ex) {
|
||||
return Mono.just(ex.getMessage());
|
||||
}
|
||||
```
|
||||
|
||||
그리고 `UserNotFoundException` 클래스를 새로 만듭니다:
|
||||
|
||||
```java
|
||||
package com.example.demo;
|
||||
|
||||
public class UserNotFoundException extends RuntimeException {
|
||||
public UserNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `switchIfEmpty`: 데이터가 없으면 예외를 발생시킵니다.
|
||||
- `@ExceptionHandler`: 예외를 캐치해 404 상태 코드와 메시지를 반환.
|
||||
|
||||
이제 `http://localhost:8080/users/999` (존재하지 않는 ID)로 접속하면 "User with ID 999 not found" 메시지와 404 상태 코드가 반환됩니다.
|
||||
|
||||
삭제 시에도 비슷하게 처리할 수 있습니다:
|
||||
|
||||
```java
|
||||
@DeleteMapping("/{id}")
|
||||
@ResponseStatus(HttpStatus.NO_CONTENT)
|
||||
public Mono<Void> deleteUser(@PathVariable Long id) {
|
||||
return userRepository.findById(id)
|
||||
.switchIfEmpty(Mono.error(new UserNotFoundException("User with ID " + id + " not found")))
|
||||
.flatMap(user -> userRepository.deleteById(id));
|
||||
}
|
||||
```
|
||||
|
||||
존재하지 않는 ID로 삭제를 시도하면 404가 반환됩니다.
|
||||
|
||||
### 클라이언트와의 비동기 통신
|
||||
|
||||
웹플럭스의 REST API는 비동기적으로 동작하므로, 클라이언트도 이를 활용할 수 있어야 합니다. 예를 들어, Postman으로 테스트할 때 응답이 바로 오지 않고 스트림 형태로 전달될 수 있습니다. 특히 `Flux`를 반환하는 `/users` 엔드포인트는 여러 데이터를 순차적으로 보냅니다.
|
||||
|
||||
클라이언트에서 비동기 요청을 테스트하려면 curl로 해보세요:
|
||||
|
||||
```bash
|
||||
curl -v http://localhost:8080/users
|
||||
```
|
||||
|
||||
데이터가 많다면 스트림처럼 순차적으로 출력되는 걸 볼 수 있습니다. 실제 환경에서는 JavaScript(예: Fetch API나 Axios) 같은 클라이언트 라이브러리로 비동기 호출을 처리할 수 있습니다.
|
||||
|
||||
간단한 JavaScript 예제:
|
||||
|
||||
```javascript
|
||||
fetch('http://localhost:8080/users')
|
||||
.then(response => response.json())
|
||||
.then(data => console.log(data));
|
||||
```
|
||||
|
||||
`Flux`의 경우 Server-Sent Events(SSE)를 활용하면 실시간 스트리밍도 가능합니다. 이를 위해 컨트롤러에 SSE 지원을 추가할 수 있습니다:
|
||||
|
||||
```java
|
||||
@GetMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public Flux<User> streamUsers() {
|
||||
return userRepository.findAll().delayElements(Duration.ofSeconds(1));
|
||||
}
|
||||
```
|
||||
|
||||
`http://localhost:8080/users/stream`에 접속하면 1초 간격으로 데이터가 스트리밍됩니다.
|
||||
|
||||
### 테스트해보기
|
||||
|
||||
- **생성**: `POST /users`에 `{"name": "Bob", "email": "bob@example.com"}` 전송 → 201 확인.
|
||||
- **조회**: `GET /users/1` → Bob의 데이터 확인.
|
||||
- **업데이트**: `PUT /users/1`에 `{"name": "Bobby", "email": "bobby@example.com"}` → 수정된 데이터 확인.
|
||||
- **삭제**: `DELETE /users/1` → 204 확인.
|
||||
- **에러**: `GET /users/999` → 404 확인.
|
||||
|
||||
### 마무리
|
||||
|
||||
이제 웹플럭스로 완전한 REST API를 설계해봤습니다. 상태 코드와 에러 처리를 추가하며 클라이언트와의 비동기 통신도 자연스럽게 연결했죠. 다음 장에서는 이 API를 테스트하는 방법을 다루며, 웹플럭스 애플리케이션의 안정성을 높이는 법을 배워보겠습니다. REST API 설계의 맛을 느끼셨길 바랍니다!
|
||||
|
||||
---
|
||||
|
||||
이 장은 REST API의 기본 설계와 실무적인 에러 처리를 다루며, 6장의 데이터베이스 연동을 활용했습니다. 추가 기능이나 조정이 필요하면 말씀해주세요!
|
||||
166
docs/webflux/08_웹플럭스에서의 테스트.md
Normal file
166
docs/webflux/08_웹플럭스에서의 테스트.md
Normal file
@@ -0,0 +1,166 @@
|
||||
아래는 **"스프링부트 웹플럭스 시리즈"**의 **8장: 웹플럭스에서의 테스트"**에 대한 초안입니다. 7장에서 만든 REST API를 기반으로 `WebTestClient`를 활용한 테스트를 다루며, 단위 테스트와 통합 테스트 예제를 포함했습니다. 코드도 간략히 유지하며 초보자가 따라 하기 쉽게 자연스러운 문체로 작성했습니다.
|
||||
|
||||
---
|
||||
|
||||
## 8. 웹플럭스에서의 테스트
|
||||
|
||||
7장에서 REST API를 완성했으니, 이제 제대로 작동하는지 확인해볼 차례입니다. 웹플럭스는 비동기 특성 때문에 테스트 방식이 기존 스프링 MVC와 조금 다릅니다. 이번 장에서는 `WebTestClient`를 사용해 웹플럭스 애플리케이션을 테스트하는 방법을 배워보겠습니다. 단위 테스트와 통합 테스트를 실습하며, 모킹과 비동기 테스트 팁도 함께 다룰게요. 준비되셨죠?
|
||||
|
||||
### WebTestClient를 활용한 테스트 작성
|
||||
|
||||
`WebTestClient`는 웹플럭스 애플리케이션의 HTTP 엔드포인트를 테스트하기 위한 도구입니다. 실제 서버를 띄우거나 모킹된 환경에서 테스트할 수 있어 유연합니다. 먼저, 테스트 의존성이 `build.gradle`에 있는지 확인하세요 (3장에서 추가했어야 함):
|
||||
|
||||
```gradle
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testImplementation 'io.projectreactor:reactor-test'
|
||||
```
|
||||
|
||||
### 단위 테스트 예제
|
||||
|
||||
먼저, `UserController`의 로직을 단위 테스트로 검증해보겠습니다. 데이터베이스 호출을 모킹하여 컨트롤러만 테스트합니다. `src/test/java/com/example/demo`에 `UserControllerTest` 클래스를 만듭니다:
|
||||
|
||||
```java
|
||||
package com.example.demo;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@WebFluxTest(UserController.class)
|
||||
public class UserControllerTest {
|
||||
|
||||
@Autowired
|
||||
private WebTestClient webTestClient;
|
||||
|
||||
@MockBean
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Test
|
||||
public void testGetUserById() {
|
||||
User user = new User();
|
||||
user.setId(1L);
|
||||
user.setName("Alice");
|
||||
user.setEmail("alice@example.com");
|
||||
|
||||
when(userRepository.findById(1L)).thenReturn(Mono.just(user));
|
||||
|
||||
webTestClient.get()
|
||||
.uri("/users/1")
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(User.class)
|
||||
.isEqualTo(user);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testGetUserNotFound() {
|
||||
when(userRepository.findById(999L)).thenReturn(Mono.empty());
|
||||
|
||||
webTestClient.get()
|
||||
.uri("/users/999")
|
||||
.exchange()
|
||||
.expectStatus().isNotFound()
|
||||
.expectBody(String.class)
|
||||
.isEqualTo("User with ID 999 not found");
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `@WebFluxTest`: 컨트롤러만 테스트하도록 웹플럭스 환경을 설정.
|
||||
- `@MockBean`: `UserRepository`를 모킹하여 실제 DB 호출을 대체.
|
||||
- `WebTestClient`: GET 요청을 보내고 응답을 검증.
|
||||
|
||||
`testGetUserById`는 성공 케이스, `testGetUserNotFound`는 404 에러 케이스를 테스트합니다. `./gradlew test`로 실행하면 결과가 콘솔에 표시됩니다.
|
||||
|
||||
### 통합 테스트 예제
|
||||
|
||||
이번엔 실제 데이터베이스와 연동한 통합 테스트를 해보겠습니다. `src/test/java/com/example/demo`에 `UserControllerIntegrationTest`를 추가합니다:
|
||||
|
||||
```java
|
||||
package com.example.demo;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||
public class UserControllerIntegrationTest {
|
||||
|
||||
@Autowired
|
||||
private WebTestClient webTestClient;
|
||||
|
||||
@Autowired
|
||||
private UserRepository userRepository;
|
||||
|
||||
@Test
|
||||
public void testCreateAndGetUser() {
|
||||
User user = new User();
|
||||
user.setName("Bob");
|
||||
user.setEmail("bob@example.com");
|
||||
|
||||
webTestClient.post()
|
||||
.uri("/users")
|
||||
.body(Mono.just(user), User.class)
|
||||
.exchange()
|
||||
.expectStatus().isCreated()
|
||||
.expectBody(User.class)
|
||||
.value(savedUser -> {
|
||||
webTestClient.get()
|
||||
.uri("/users/" + savedUser.getId())
|
||||
.exchange()
|
||||
.expectStatus().isOk()
|
||||
.expectBody(User.class)
|
||||
.isEqualTo(savedUser);
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `@SpringBootTest`: 전체 애플리케이션 컨텍스트를 로드하며, 랜덤 포트로 서버를 띄움.
|
||||
- `testCreateAndGetUser`: 사용자를 생성하고, 생성된 ID로 조회까지 테스트.
|
||||
|
||||
이 테스트는 실제 MariaDB와 연동되므로, `application.yaml`의 설정이 올바른지 확인하세요.
|
||||
|
||||
### 모킹과 비동기 테스트 팁
|
||||
|
||||
1. **모킹 활용**: `Mockito`로 리포지토리나 외부 서비스를 모킹하면 의존성을 줄이고 빠르게 테스트할 수 있습니다. 단위 테스트에서 유용하죠.
|
||||
2. **비동기 처리 확인**: `StepVerifier`를 사용하면 `Mono`나 `Flux`의 비동기 흐름을 더 정밀하게 검증할 수 있습니다. 예를 들어:
|
||||
|
||||
```java
|
||||
@Test
|
||||
public void testMonoWithStepVerifier() {
|
||||
User user = new User();
|
||||
user.setId(1L);
|
||||
user.setName("Alice");
|
||||
|
||||
when(userRepository.findById(1L)).thenReturn(Mono.just(user));
|
||||
|
||||
Mono<User> userMono = userRepository.findById(1L);
|
||||
StepVerifier.create(userMono)
|
||||
.expectNext(user)
|
||||
.verifyComplete();
|
||||
}
|
||||
```
|
||||
|
||||
- `StepVerifier`: 리액티브 스트림의 이벤트를 단계별로 검증.
|
||||
|
||||
3. **타임아웃 설정**: 비동기 테스트가 너무 오래 걸리면 실패로 간주하도록 설정하세요. `WebTestClient`에 `.responseTimeout(Duration.ofSeconds(5))`를 추가할 수 있습니다.
|
||||
|
||||
### 테스트 실행해보기 _
|
||||
|
||||
`./gradlew test`를 실행하면 단위 테스트와 통합 테스트가 모두 실행됩니다. 콘솔에서 성공/실패 여부를 확인할 수 있습니다. 통합 테스트는 MariaDB가 실행 중이어야 하니, 서버가 켜져 있는지 체크하세요.
|
||||
|
||||
### 마무리
|
||||
|
||||
`WebTestClient`로 웹플럭스 API를 테스트하며, 단위 테스트와 통합 테스트의 차이도 경험해봤습니다. 비동기 환경에서의 테스트는 처음엔 낯설 수 있지만, 익숙해지면 애플리케이션의 안정성을 크게 높일 수 있습니다. 다음 장에서는 성능 최적화와 디버깅을 다루며, 웹플럭스를 실무에서 더 단단하게 다듬는 법을 배워보겠습니다. 테스트 작성의 재미를 느끼셨길 바랍니다!
|
||||
|
||||
---
|
||||
|
||||
이 장은 `WebTestClient`를 중심으로 실습을 구성했으며, 단위/통합 테스트와 비동기 팁을 간결히 다뤘습니다. 추가 예제나 수정이 필요하면 말씀해주세요!
|
||||
128
docs/webflux/09_성능 최적화와 디버깅.md
Normal file
128
docs/webflux/09_성능 최적화와 디버깅.md
Normal file
@@ -0,0 +1,128 @@
|
||||
아래는 **"스프링부트 웹플럭스 시리즈"**의 **9장: 성능 최적화와 디버깅"**에 대한 초안입니다. 8장에서 테스트를 마무리한 웹플럭스 애플리케이션을 기반으로 성능 분석, 최적화 기법, 그리고 디버깅 방법을 다룹니다. 실습 예제는 간략히 유지하며, 초보자도 이해할 수 있도록 자연스럽고 친근한 문체로 작성했습니다.
|
||||
|
||||
---
|
||||
|
||||
## 9. 성능 최적화와 디버깅
|
||||
|
||||
이제 웹플럭스 애플리케이션이 제법 모양을 갖췄습니다. 하지만 실무에서는 성능과 안정성이 중요하죠. 이번 장에서는 웹플럭스 애플리케이션의 성능을 분석하고 최적화하는 방법을 알아보고, 비동기 환경에서 디버깅하는 팁도 배워보겠습니다. 성능을 쥐어짜고 문제를 해결하는 재미를 느껴보세요. 시작합시다!
|
||||
|
||||
### 웹플럭스 애플리케이션의 성능 분석
|
||||
|
||||
성능을 최적화하려면 먼저 현재 상태를 파악해야 합니다. 웹플럭스는 비동기/논블로킹으로 작동하니, 병목 지점이 어디인지 찾는 게 핵심입니다. 간단한 방법부터 시작해보죠.
|
||||
|
||||
1. **부하 테스트**: `Apache JMeter`나 `wrk` 같은 도구로 애플리케이션에 부하를 줘봅니다. 예를 들어, 7장의 `/users` 엔드포인트를 테스트하려면:
|
||||
```bash
|
||||
wrk -t10 -c100 -d30s http://localhost:8080/users
|
||||
```
|
||||
- `-t10`: 10개 스레드, `-c100`: 100개 동시 연결, `-d30s`: 30초간 실행.
|
||||
결과로 초당 요청 수(Requests/sec)와 지연 시간(Latency)을 확인할 수 있습니다.
|
||||
|
||||
2. **메트릭 수집**: 스프링 부트 Actuator를 추가해 성능 지표를 모니터링합니다. `build.gradle`에 의존성을 추가하세요:
|
||||
```gradle
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
```
|
||||
`application.yaml`에 설정 추가:
|
||||
```yaml
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,metrics
|
||||
```
|
||||
애플리케이션을 실행 후 `http://localhost:8080/actuator/metrics`에 접속하면 요청 처리 시간, 메모리 사용량 등을 볼 수 있습니다.
|
||||
|
||||
3. **프로파일링**: VisualVM이나 IntelliJ Profiler로 CPU와 메모리 사용을 분석합니다. 예를 들어, `/users/stream` 엔드포인트가 느리다면 프로파일러로 호출 스택을 확인해보세요.
|
||||
|
||||
### 논블로킹 코드 최적화 기법
|
||||
|
||||
웹플럭스의 성능은 논블로킹 처리에 달려 있습니다. 몇 가지 최적화 팁을 적용해보죠.
|
||||
|
||||
1. **불필요한 블로킹 제거**:
|
||||
기존 코드에 동기 호출(예: `Thread.sleep`)이 있다면 제거하거나 비동기로 바꿉니다. 예를 들어:
|
||||
```java
|
||||
// 비추천
|
||||
public Mono<String> slowMethod() {
|
||||
Thread.sleep(1000); // 블로킹
|
||||
return Mono.just("Done");
|
||||
}
|
||||
|
||||
// 추천
|
||||
public Mono<String> fastMethod() {
|
||||
return Mono.just("Done").delayElement(Duration.ofSeconds(1));
|
||||
}
|
||||
```
|
||||
|
||||
2. **병렬 처리 활용**:
|
||||
`Flux`로 여러 작업을 병렬로 처리할 수 있습니다. 예를 들어, 사용자 목록을 가져와 외부 API로 추가 데이터를 조회한다고 가정하면:
|
||||
```java
|
||||
public Flux<User> enrichUsers() {
|
||||
return userRepository.findAll()
|
||||
.parallel()
|
||||
.runOn(Schedulers.parallel())
|
||||
.flatMap(user -> fetchAdditionalData(user))
|
||||
.sequential();
|
||||
}
|
||||
```
|
||||
- `parallel()`과 `Schedulers.parallel()`로 병렬 실행.
|
||||
|
||||
3. **캐싱 적용**:
|
||||
자주 조회되는 데이터는 캐싱으로 부하를 줄입니다. `reactor-cache`를 추가해 구현해보세요:
|
||||
```gradle
|
||||
implementation 'io.github.resilience4j:resilience4j-reactor'
|
||||
```
|
||||
```java
|
||||
private final CacheMono<String, User> userCache = CacheMono.from(Caffeine.newBuilder().build());
|
||||
public Mono<User> getCachedUser(Long id) {
|
||||
return userCache.lookup(id.toString(), userRepository.findById(id));
|
||||
}
|
||||
```
|
||||
|
||||
### 리액티브 애플리케이션 디버깅 방법
|
||||
|
||||
비동기 코드는 디버깅이 까다로울 수 있습니다. 스택 트레이스가 복잡하고, 문제가 어디서 발생했는지 찾기 어렵죠. 몇 가지 디버깅 팁을 소개합니다.
|
||||
|
||||
1. **로그 추가**:
|
||||
`doOn` 연산자로 흐름을 추적합니다. `UserController`의 `getUserById`에 적용해보죠:
|
||||
```java
|
||||
@GetMapping("/{id}")
|
||||
public Mono<User> getUserById(@PathVariable Long id) {
|
||||
return userRepository.findById(id)
|
||||
.doOnSubscribe(sub -> log.info("Fetching user with ID: {}", id))
|
||||
.doOnNext(user -> log.info("Found user: {}", user))
|
||||
.doOnError(err -> log.error("Error: ", err))
|
||||
.switchIfEmpty(Mono.error(new UserNotFoundException("User with ID " + id + " not found")));
|
||||
}
|
||||
```
|
||||
- `log`는 `org.slf4j.Logger`를 사용 (클래스 상단에 선언 필요).
|
||||
|
||||
2. **리액터 디버깅 모드**:
|
||||
`reactor-tools`를 추가해 스택 트레이스를 개선합니다:
|
||||
```gradle
|
||||
implementation 'io.projectreactor:reactor-tools'
|
||||
```
|
||||
애플리케이션 시작 시 활성화:
|
||||
```java
|
||||
public static void main(String[] args) {
|
||||
Hooks.onOperatorDebug();
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
```
|
||||
에러 발생 시 더 자세한 스택 트레이스가 출력됩니다.
|
||||
|
||||
3. **StepVerifier로 확인**:
|
||||
테스트 중에 디버깅할 때는 `StepVerifier`로 스트림을 단계별로 검증합니다. 8장의 예제를 참고하세요.
|
||||
|
||||
### 실습: 성능 개선 확인
|
||||
|
||||
`/users` 엔드포인트에 캐싱을 추가하고 부하 테스트로 성능 변화를 확인해보세요:
|
||||
1. 캐싱 없는 상태에서 `wrk`로 테스트.
|
||||
2. 캐싱 추가 후 다시 테스트.
|
||||
3. 결과를 비교하며 응답 시간 감소를 확인.
|
||||
|
||||
### 마무리
|
||||
|
||||
성능 분석과 최적화로 웹플럭스의 잠재력을 끌어올리고, 디버깅으로 문제를 빠르게 잡아냈습니다. 비동기 환경은 처음엔 어색할 수 있지만, 이런 도구와 기법을 익히면 실무에서 큰 힘이 됩니다. 다음 장에서는 실전 프로젝트로 마이크로서비스를 구축하며, 웹플럭스를 더 큰 무대에 올려보겠습니다. 이번 장에서 애플리케이션이 더 단단해진 느낌이 드시길 바랍니다!
|
||||
|
||||
---
|
||||
|
||||
이 장은 성능 분석과 디버깅 실무 팁을 중심으로 구성했으며, 간단한 실습으로 이해를 도왔습니다. 추가 예제나 조정이 필요하면 말씀해주세요!
|
||||
189
docs/webflux/10_실전 프로젝트.md
Normal file
189
docs/webflux/10_실전 프로젝트.md
Normal file
@@ -0,0 +1,189 @@
|
||||
아래는 **"스프링부트 웹플럭스 시리즈"**의 **10장: 실전 프로젝트: 웹플럭스로 마이크로서비스 구축"**에 대한 초안입니다. 9장까지의 내용을 바탕으로 간단한 마이크로서비스를 설계하고, 웹플럭스와 다른 스프링 모듈을 통합하며 배포와 모니터링 팁을 다룹니다. 실습은 간략히 유지하며 초보자도 따라 할 수 있도록 자연스러운 문체로 작성했습니다.
|
||||
|
||||
---
|
||||
|
||||
## 10. 실전 프로젝트: 웹플럭스로 마이크로서비스 구축
|
||||
|
||||
이제 웹플럭스의 기본기를 실전에서 활용해볼 시간입니다. 이번 장에서는 간단한 마이크로서비스를 설계하고, 웹플럭스를 중심으로 다른 스프링 모듈과 통합하며, 배포와 모니터링까지 다뤄보겠습니다. 작은 프로젝트지만 실무에서 유용한 패턴을 경험할 수 있을 거예요. 준비되셨죠? 출발합시다!
|
||||
|
||||
### 간단한 마이크로서비스 설계
|
||||
|
||||
마이크로서비스는 독립적으로 배포 가능한 작은 서비스 단위로, 여기서는 두 개의 서비스를 만들어보겠습니다:
|
||||
1. **User Service**: 사용자 정보를 관리 (6~7장에서 만든 기능 재사용).
|
||||
2. **Order Service**: 주문 데이터를 처리하며 User Service와 통신.
|
||||
|
||||
두 서비스는 REST API로 상호작용하며, 웹플럭스의 비동기 특성을 활용합니다.
|
||||
|
||||
#### User Service
|
||||
기존 프로젝트를 그대로 사용합니다. `UserController`와 `UserRepository`가 이미 준비되어 있죠. 포트를 명확히 하기 위해 `application.yaml`을 수정합니다:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
port: 8081
|
||||
spring:
|
||||
r2dbc:
|
||||
url: r2dbc:mariadb://localhost:3306/webflux_db
|
||||
username: root
|
||||
password: yourpassword
|
||||
```
|
||||
|
||||
#### Order Service
|
||||
새 프로젝트를 생성하거나, 같은 프로젝트 내에서 별도 패키지로 분리합니다. 여기서는 새 프로젝트로 진행한다고 가정하고, `build.gradle`을 설정합니다:
|
||||
|
||||
```gradle
|
||||
plugins {
|
||||
id 'org.springframework.boot' version '3.2.4'
|
||||
id 'java'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
|
||||
implementation 'io.r2dbc:r2dbc-mariadb:1.1.3'
|
||||
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
|
||||
}
|
||||
```
|
||||
|
||||
`application.yaml` 설정:
|
||||
|
||||
```yaml
|
||||
server:
|
||||
port: 8082
|
||||
spring:
|
||||
r2dbc:
|
||||
url: r2dbc:mariadb://localhost:3306/webflux_db
|
||||
username: root
|
||||
password: yourpassword
|
||||
```
|
||||
|
||||
주문 엔티티와 리포지토리를 추가합니다:
|
||||
|
||||
```java
|
||||
// Order.java
|
||||
package com.example.order;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.data.annotation.Id;
|
||||
|
||||
@Data
|
||||
public class Order {
|
||||
@Id
|
||||
private Long id;
|
||||
private Long userId;
|
||||
private String product;
|
||||
}
|
||||
|
||||
// OrderRepository.java
|
||||
package com.example.order;
|
||||
|
||||
import org.springframework.data.r2dbc.repository.R2dbcRepository;
|
||||
|
||||
public interface OrderRepository extends R2dbcRepository<Order, Long> {
|
||||
}
|
||||
```
|
||||
|
||||
Order Service의 컨트롤러는 User Service와 통신합니다:
|
||||
|
||||
```java
|
||||
package com.example.order;
|
||||
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/orders")
|
||||
public class OrderController {
|
||||
|
||||
private final OrderRepository orderRepository;
|
||||
private final WebClient webClient;
|
||||
|
||||
public OrderController(OrderRepository orderRepository) {
|
||||
this.orderRepository = orderRepository;
|
||||
this.webClient = WebClient.create("http://localhost:8081");
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public Mono<Order> createOrder(@RequestBody Order order) {
|
||||
return webClient.get()
|
||||
.uri("/users/{id}", order.getUserId())
|
||||
.retrieve()
|
||||
.bodyToMono(User.class)
|
||||
.flatMap(user -> orderRepository.save(order));
|
||||
}
|
||||
|
||||
@GetMapping("/user/{userId}")
|
||||
public Flux<Order> getOrdersByUser(@PathVariable Long userId) {
|
||||
return orderRepository.findAll()
|
||||
.filter(order -> order.getUserId().equals(userId));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `createOrder`: User Service에서 사용자 존재 여부를 확인 후 주문을 저장.
|
||||
- `getOrdersByUser`: 특정 사용자의 주문 목록 반환.
|
||||
|
||||
MariaDB에 `orders` 테이블 생성:
|
||||
|
||||
```sql
|
||||
CREATE TABLE orders (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id BIGINT NOT NULL,
|
||||
product VARCHAR(255) NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
### 웹플럭스와 다른 스프링 모듈 통합
|
||||
|
||||
1. **WebClient**: Order Service에서 User Service와 통신할 때 사용. 비동기 HTTP 클라이언트로, 웹플럭스와 잘 맞습니다.
|
||||
2. **Actuator**: 모니터링을 위해 추가:
|
||||
```gradle
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
```
|
||||
```yaml
|
||||
management:
|
||||
endpoints:
|
||||
web:
|
||||
exposure:
|
||||
include: health,metrics
|
||||
```
|
||||
`http://localhost:8082/actuator/health`로 상태 확인 가능.
|
||||
|
||||
3. **스프링 시큐리티** (선택): 간단한 인증을 추가하려면:
|
||||
```gradle
|
||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||
```
|
||||
기본 사용자/비밀번호로 엔드포인트 보호 가능.
|
||||
|
||||
### 배포 및 모니터링 팁
|
||||
|
||||
1. **배포**:
|
||||
- JAR 파일 생성: `./gradlew bootJar`.
|
||||
- 실행: `java -jar build/libs/order-service-0.0.1-SNAPSHOT.jar`.
|
||||
- Docker 사용 시:
|
||||
```dockerfile
|
||||
FROM openjdk:17-jdk-slim
|
||||
COPY build/libs/*.jar app.jar
|
||||
ENTRYPOINT ["java", "-jar", "/app.jar"]
|
||||
```
|
||||
|
||||
2. **모니터링**:
|
||||
- Actuator 메트릭을 Prometheus와 Grafana로 시각화.
|
||||
- 로그는 SLF4J와 Logback으로 관리하며, `doOn` 연산자로 디버깅 로그 추가.
|
||||
|
||||
### 테스트해보기
|
||||
|
||||
1. User Service 실행 (`port: 8081`).
|
||||
2. Order Service 실행 (`port: 8082`).
|
||||
3. 사용자 생성: `POST http://localhost:8081/users`에 `{"name": "Alice", "email": "alice@example.com"}`.
|
||||
4. 주문 생성: `POST http://localhost:8082/orders`에 `{"userId": 1, "product": "Book"}`.
|
||||
5. 주문 조회: `GET http://localhost:8082/orders/user/1`.
|
||||
|
||||
### 마무리
|
||||
|
||||
간단한 마이크로서비스를 웹플럭스로 구축하며, 서비스 간 통신과 통합의 맛을 봤습니다. 비동기 처리와 논블로킹의 장점을 실무에 적용하는 첫걸음이죠. 다음 장에서는 웹소켓을 다루며 실시간 통신까지 확장해보겠습니다. 이번 프로젝트로 웹플럭스의 실전 감각이 생기셨길 바랍니다!
|
||||
|
||||
---
|
||||
|
||||
이 장은 마이크로서비스 설계와 통합을 중심으로 실습을 구성했으며, 배포와 모니터링 팁을 간략히 다뤘습니다. 추가 예제나 수정이 필요하면 말씀해주세요!
|
||||
163
docs/webflux/11_웹플럭스와 웹소켓.md
Normal file
163
docs/webflux/11_웹플럭스와 웹소켓.md
Normal file
@@ -0,0 +1,163 @@
|
||||
아래는 **"스프링부트 웹플럭스 시리즈"**의 **11장: 웹플럭스와 웹소켓"**에 대한 초안입니다. 10장의 마이크로서비스 기반을 활용하여 웹소켓을 통해 실시간 통신을 구현하며, 간단한 채팅 애플리케이션 예제를 포함했습니다. 코드도 간략히 유지하며 초보자가 따라 하기 쉽게 자연스러운 문체로 작성했습니다.
|
||||
|
||||
---
|
||||
|
||||
## 11. 웹플럭스와 웹소켓
|
||||
|
||||
10장에서 마이크로서비스를 구축하며 웹플럭스의 비동기 특성을 실감했습니다. 이번 장에서는 한 단계 더 나아가 웹소켓(WebSocket)을 활용해 실시간 통신을 구현해보겠습니다. 웹플럭스와 웹소켓은 실시간 데이터 전송에 최적화된 조합으로, 채팅 같은 기능을 쉽게 만들 수 있습니다. 간단한 예제를 통해 실습해보죠. 준비되셨나요?
|
||||
|
||||
### 실시간 통신을 위한 웹소켓 구현
|
||||
|
||||
웹소켓은 클라이언트와 서버 간 양방향 통신을 가능하게 하는 프로토콜입니다. HTTP와 달리 연결을 유지하며, 데이터를 주고받는 데 지연이 적습니다. 웹플럭스는 이를 기본 지원하니, 추가 의존성 없이 바로 시작할 수 있습니다.
|
||||
|
||||
10장의 User Service를 확장해 웹소켓 기반 채팅 기능을 추가해보겠습니다. 먼저, 웹소켓 핸들러를 만듭니다.
|
||||
|
||||
#### 웹소켓 핸들러 작성
|
||||
`src/main/java/com/example/demo`에 `ChatWebSocketHandler` 클래스를 추가합니다:
|
||||
|
||||
```java
|
||||
package com.example.demo;
|
||||
|
||||
import org.springframework.web.reactive.socket.WebSocketHandler;
|
||||
import org.springframework.web.reactive.socket.WebSocketSession;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
public class ChatWebSocketHandler implements WebSocketHandler {
|
||||
|
||||
private final Flux<String> messageFlux;
|
||||
|
||||
public ChatWebSocketHandler() {
|
||||
// 간단한 메시지 스트림 예제
|
||||
this.messageFlux = Flux.interval(Duration.ofSeconds(1))
|
||||
.map(i -> "Message " + i);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(WebSocketSession session) {
|
||||
// 클라이언트로부터 메시지 수신
|
||||
Flux<String> input = session.receive()
|
||||
.map(message -> "Echo: " + message.getPayloadAsText());
|
||||
|
||||
// 클라이언트로 메시지 전송
|
||||
return session.send(messageFlux.mergeWith(input)
|
||||
.map(session::textMessage));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `messageFlux`: 서버에서 주기적으로 보내는 메시지 스트림 (예시용).
|
||||
- `handle`: 클라이언트 메시지를 받아 에코로 반환하고, 서버 메시지도 함께 전송.
|
||||
|
||||
#### 웹소켓 라우팅 설정
|
||||
`src/main/java/com/example/demo`에 `WebSocketConfig`를 추가합니다:
|
||||
|
||||
```java
|
||||
package com.example.demo;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.reactive.HandlerMapping;
|
||||
import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping;
|
||||
import org.springframework.web.reactive.socket.WebSocketHandler;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Configuration
|
||||
public class WebSocketConfig {
|
||||
|
||||
@Bean
|
||||
public HandlerMapping webSocketMapping() {
|
||||
Map<String, WebSocketHandler> map = new HashMap<>();
|
||||
map.put("/chat", new ChatWebSocketHandler());
|
||||
|
||||
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
|
||||
mapping.setUrlMap(map);
|
||||
mapping.setOrder(-1); // 다른 매핑보다 우선
|
||||
return mapping;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `/chat` 경로로 웹소켓 연결을 처리.
|
||||
|
||||
### 웹플럭스와 웹소켓의 조합 예제
|
||||
|
||||
User Service (`port: 8081`)에 위 코드를 추가한 뒤 실행합니다. 이제 클라이언트에서 웹소켓 연결을 테스트할 수 있습니다.
|
||||
|
||||
#### 클라이언트 테스트
|
||||
간단한 HTML/JS 클라이언트를 만들어 보죠. `src/main/resources/static`에 `index.html`을 추가합니다:
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>WebSocket Chat</title>
|
||||
</head>
|
||||
<body>
|
||||
<input id="message" type="text">
|
||||
<button onclick="sendMessage()">Send</button>
|
||||
<div id="messages"></div>
|
||||
|
||||
<script>
|
||||
const socket = new WebSocket("ws://localhost:8081/chat");
|
||||
const messagesDiv = document.getElementById("messages");
|
||||
|
||||
socket.onmessage = function(event) {
|
||||
const msg = document.createElement("p");
|
||||
msg.textContent = event.data;
|
||||
messagesDiv.appendChild(msg);
|
||||
};
|
||||
|
||||
function sendMessage() {
|
||||
const input = document.getElementById("message");
|
||||
socket.send(input.value);
|
||||
input.value = "";
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
`http://localhost:8081/index.html`에 접속하면:
|
||||
1. 서버에서 1초마다 "Message 0", "Message 1" 등이 표시.
|
||||
2. 입력창에 메시지를 입력하고 Send 버튼을 누르면 "Echo: [입력값]" 반환.
|
||||
|
||||
### 활용 사례 (채팅 애플리케이션 등)
|
||||
|
||||
위 예제는 기본적인 에코 서버지만, 이를 확장하면 실시간 채팅 애플리케이션을 만들 수 있습니다:
|
||||
- **다중 사용자 지원**: `Flux`를 공유 가능한 스트림으로 만들어 모든 클라이언트에 브로드캐스트.
|
||||
```java
|
||||
private final Sinks.Many<String> messageSink = Sinks.many().multicast().onBackpressureBuffer();
|
||||
private final Flux<String> messageFlux = messageSink.asFlux();
|
||||
|
||||
@Override
|
||||
public Mono<Void> handle(WebSocketSession session) {
|
||||
Flux<String> input = session.receive()
|
||||
.map(message -> session.getId() + ": " + message.getPayloadAsText())
|
||||
.doOnNext(messageSink::tryEmitNext);
|
||||
|
||||
return session.send(messageFlux.map(session::textMessage));
|
||||
}
|
||||
```
|
||||
- **상태 관리**: Redis나 데이터베이스로 채팅 기록 저장.
|
||||
- **인증**: 스프링 시큐리티로 웹소켓 연결에 토큰 기반 인증 추가.
|
||||
|
||||
채팅 외에도 주식 시세 업데이트, 실시간 알림 등 다양한 활용이 가능합니다.
|
||||
|
||||
### 테스트해보기
|
||||
|
||||
1. User Service 실행.
|
||||
2. 브라우저에서 `http://localhost:8081/index.html` 열기.
|
||||
3. 메시지 입력 후 Send 버튼 클릭 → 서버 메시지와 에코 확인.
|
||||
|
||||
### 마무리
|
||||
|
||||
웹플럭스와 웹소켓으로 실시간 통신의 세계에 입문했습니다. 비동기 스트림과 양방향 연결이 얼마나 강력한지 느끼셨나요? 다음 장에서는 웹플럭스의 현재와 미래를 돌아보며, 이 기술의 여정을 마무리하겠습니다. 이번 실습으로 실시간 애플리케이션의 가능성을 열어보셨길 바랍니다!
|
||||
|
||||
---
|
||||
|
||||
이 장은 웹소켓의 기본 구현과 채팅 예제를 중심으로 구성했으며, 실습과 활용 사례를 간략히 다뤘습니다. 추가 기능이나 수정이 필요하면 말씀해주세요!
|
||||
68
docs/webflux/12_웹플럭스의 현재와 미래.md
Normal file
68
docs/webflux/12_웹플럭스의 현재와 미래.md
Normal file
@@ -0,0 +1,68 @@
|
||||
아래는 **"스프링부트 웹플럭스 시리즈"**의 **12장: 마무리: 웹플럭스의 현재와 미래"**에 대한 초안입니다. 이 장은 시리즈의 마지막으로, 지금까지 배운 내용을 정리하고 웹플럭스의 현재 트렌드, 커뮤니티 활동, 그리고 추가 학습 로드맵을 다룹니다. 실습은 포함하지 않고, 독자에게 동기를 부여하며 자연스럽고 따뜻한 문체로 작성했습니다.
|
||||
|
||||
---
|
||||
|
||||
## 12. 마무리: 웹플럭스의 현재와 미래
|
||||
|
||||
여기까지 오느라 수고 많으셨습니다! 1장에서 웹플럭스의 개념을 처음 만난 순간부터, 11장에서 실시간 채팅 애플리케이션을 만들기까지, 우리는 리액티브 프로그래밍의 세계를 함께 탐험했습니다. 이번 장에서는 여정을 돌아보고, 웹플럭스가 현재 어디에 서 있는지, 앞으로 어떤 가능성을 열어갈지 이야기해보겠습니다. 그리고 다음 단계로 나아갈 수 있는 길잡이도 드릴게요. 잠시 숨을 고르며 마무리해볼까요?
|
||||
|
||||
### 여정 돌아보기
|
||||
|
||||
이 시리즈에서 우리는 웹플럭스의 핵심을 단계별로 파헤쳤습니다:
|
||||
- **기본 개념**: 웹플럭스와 리액티브 프로그래밍의 비동기/논블로킹 철학을 배웠습니다.
|
||||
- **핵심 도구**: `Mono`와 `Flux`로 데이터 스트림을 다루며, 라우팅과 REST API 설계를 실습했습니다.
|
||||
- **데이터 연동**: R2DBC와 MariaDB로 리액티브 데이터베이스를 연결했습니다.
|
||||
- **실전 적용**: 마이크로서비스와 웹소켓으로 실무에 가까운 프로젝트를 구현했습니다.
|
||||
- **안정성 강화**: 테스트, 성능 최적화, 디버깅으로 애플리케이션을 단단히 다졌습니다.
|
||||
|
||||
처음엔 낯설었을 `Mono`와 `Flux`가 이제 제법 친숙해지지 않았나요? 비동기 프로그래밍의 흐름을 이해하고, 이를 실제 코드로 표현한 경험이 여러분의 개발자 여정에 큰 자산이 될 겁니다.
|
||||
|
||||
### 스프링 웹플럭스의 최신 트렌드
|
||||
|
||||
2025년 현재, 웹플럭스는 스프링 생태계에서 중요한 위치를 차지하고 있습니다. 몇 가지 최신 트렌드를 짚어보면:
|
||||
- **마이크로서비스의 표준**: 클라우드 네이티브 환경에서 웹플럭스는 높은 확장성과 낮은 자원 소비로 주목받습니다. 쿠버네티스와 결합해 대규모 트래픽을 처리하는 기업들이 늘고 있죠.
|
||||
- **실시간 애플리케이션 확대**: 웹소켓과 Server-Sent Events(SSE)를 활용한 실시간 기능이 점점 보편화되고 있습니다. 채팅, 알림, 스트리밍 서비스 등에서 웹플럭스의 강점이 두드러집니다.
|
||||
- **Kotlin과의 시너지**: 코틀린 코루틴(Coroutines)과 웹플럭스의 결합으로 더 간결하고 선언적인 코드 작성이 가능해졌습니다. 코틀린을 배우고 싶다면 웹플럭스와 함께 시작해보는 것도 좋은 선택입니다.
|
||||
- **AOT 컴파일과 GraalVM**: 스프링 부트 3.x는 GraalVM과의 통합을 강화하며, 웹플럭스 애플리케이션의 시작 시간과 메모리 사용량을 줄이는 방향으로 나아가고 있습니다.
|
||||
|
||||
스프링 팀은 웹플럭스를 지속적으로 개선하며, 최신 버전에서 성능 최적화와 개발자 경험(DX)을 강화하고 있습니다. 예를 들어, `spring-boot-starter-webflux`의 최신 업데이트는 더 직관적인 에러 처리와 디버깅 도구를 제공하죠.
|
||||
|
||||
### 커뮤니티와 생태계 이야기
|
||||
|
||||
웹플럭스는 활발한 커뮤니티의 지원을 받고 있습니다. 몇 가지 참여할 만한 곳을 소개할게요:
|
||||
- **스프링 공식 블로그**: 최신 릴리스 정보와 튜토리얼을 확인할 수 있습니다. (spring.io/blog)
|
||||
- **Stack Overflow**: “spring-webflux” 태그로 질문과 답변을 주고받으며 실무 문제를 해결하세요.
|
||||
- **GitHub**: `spring-projects/spring-boot`와 `reactor/reactor-core` 저장소에서 소스 코드를 살펴보고, 이슈에 참여할 수 있습니다.
|
||||
- **컨퍼런스**: SpringOne이나 Devoxx 같은 행사에서 웹플럭스 관련 세션을 들으며 최신 트렌드를 접해보세요.
|
||||
|
||||
한국에서도 스프링 커뮤니티가 활발히 활동 중입니다. 온라인 포럼이나 오프라인 밋업에서 다른 개발자들과 경험을 나누며 성장할 기회를 찾아보세요.
|
||||
|
||||
### 다음 단계로 나아가기 위한 학습 로드맵
|
||||
|
||||
웹플럭스의 기초를 다졌으니, 이제 어디로 갈지 궁금하시죠? 몇 가지 추천 경로를 제안드립니다:
|
||||
1. **심화 학습**:
|
||||
- 리액터 프로젝트(Reactor): `StepVerifier`, `Sinks` 같은 고급 기능을 익혀보세요.
|
||||
- 스프링 시큐리티: 웹플럭스에 OAuth2나 JWT를 적용해 보안 강화.
|
||||
- 그래프QL: REST 대신 GraphQL로 리액티브 API 설계.
|
||||
|
||||
2. **확장 프로젝트**:
|
||||
- 10장의 마이크로서비스를 확장해, 서비스 디스커버리(Eureka)나 API 게이트웨이(Spring Cloud Gateway) 추가.
|
||||
- 웹소켓으로 더 복잡한 실시간 애플리케이션(예: 멀티플레이어 게임) 구현.
|
||||
|
||||
3. **실무 적용**:
|
||||
- 기존 프로젝트에 웹플럭스를 부분적으로 도입해 성능 비교.
|
||||
- 모니터링 툴(Prometheus, Grafana)로 운영 환경에서의 성능 분석.
|
||||
|
||||
4. **커뮤니티 기여**:
|
||||
- 오픈소스 프로젝트에 작은 PR을 보내보세요. 문서 번역도 좋은 시작입니다.
|
||||
- 블로그나 유튜브로 이번 시리즈에서 배운 걸 공유하며 다른 개발자를 도와주세요.
|
||||
|
||||
무엇보다 중요한 건 꾸준히 실험하고 질문하는 태도입니다. 리액티브 프로그래밍은 처음엔 복잡하지만, 익숙해질수록 새로운 가능성을 열어줍니다.
|
||||
|
||||
### 마무리
|
||||
|
||||
웹플럭스는 단순한 기술 이상의 의미를 가집니다. 이는 변화하는 웹 환경에서 더 빠르고 유연한 시스템을 만들려는 도전의 결과물입니다. 여러분은 이 시리즈를 통해 그 도전의 첫걸음을 내디뎠습니다. 이제 여러분만의 프로젝트로 이 기술을 더 깊이 탐구하고, 동료들과 지식을 나누며 성장해보세요. 이 여정이 여러분의 개발자 삶에 새로운 불꽃을 일으키길 바랍니다. 끝까지 함께해줘서 고마워요!
|
||||
|
||||
---
|
||||
|
||||
이 장은 시리즈의 마무리로서 독자에게 동기를 주고, 실질적인 학습 경로를 제시했습니다. 추가로 다루고 싶은 내용이나 조정이 필요하면 말씀해주세요!
|
||||
69
docs/webflux/README.md
Normal file
69
docs/webflux/README.md
Normal file
@@ -0,0 +1,69 @@
|
||||
스프링부트 웹플럭스(Spring Boot WebFlux)에 대한 연속적인 글을 위한 목차를 아래에 제안드립니다. 이 목차는 초보자부터 중급 개발자까지 이해할 수 있도록 기초부터 실전까지 다루며, 논리적인 흐름을 유지했습니다. 필요에 따라 주제의 깊이와 순서를 조정할 수 있습니다.
|
||||
|
||||
---
|
||||
|
||||
### 스프링부트 웹플럭스 시리즈 목차
|
||||
|
||||
1. **웹플럭스란 무엇인가?**
|
||||
- 스프링 웹플럭스 소개
|
||||
- 기존 스프링 MVC와의 차이점
|
||||
- 반응형 프로그래밍(Reactive Programming)의 기본 개념
|
||||
|
||||
2. **왜 웹플럭스를 사용하는가?**
|
||||
- 비동기와 논블로킹의 필요성
|
||||
- 웹플럭스의 주요 사용 사례
|
||||
- 성능 이점과 한계
|
||||
|
||||
3. **스프링부트 웹플럭스 시작하기**
|
||||
- 개발 환경 설정 (Maven/Gradle 의존성 추가)
|
||||
- 첫 번째 웹플럭스 애플리케이션 만들기
|
||||
- 기본적인 라우팅과 컨트롤러 설정
|
||||
|
||||
4. **리액티브 데이터 스트림 이해**
|
||||
- Mono와 Flux의 개념
|
||||
- 리액티브 스트림의 동작 원리
|
||||
- 간단한 예제로 배우는 Mono와 Flux
|
||||
|
||||
5. **웹플럭스에서 라우팅 처리**
|
||||
- RouterFunction을 사용한 함수형 라우팅
|
||||
- 어노테이션 기반 컨트롤러와의 비교
|
||||
- 요청 처리와 응답 생성 실습
|
||||
|
||||
6. **리액티브 데이터베이스 연동**
|
||||
- R2DBC와 리액티브 리포지토리 설정
|
||||
- MongoDB, PostgreSQL 등과의 연동 예제
|
||||
- 데이터 조회 및 저장 실습
|
||||
|
||||
7. **웹플럭스와 REST API 설계**
|
||||
- RESTful 엔드포인트 구현
|
||||
- 상태 코드와 에러 처리
|
||||
- 클라이언트와의 비동기 통신
|
||||
|
||||
8. **웹플럭스에서의 테스트**
|
||||
- WebTestClient를 활용한 테스트 작성
|
||||
- 단위 테스트와 통합 테스트 예제
|
||||
- 모킹과 비동기 테스트 팁
|
||||
|
||||
9. **성능 최적화와 디버깅**
|
||||
- 웹플럭스 애플리케이션의 성능 분석
|
||||
- 논블로킹 코드 최적화 기법
|
||||
- 리액티브 애플리케이션 디버깅 방법
|
||||
|
||||
10. **실전 프로젝트: 웹플럭스로 마이크로서비스 구축**
|
||||
- 간단한 마이크로서비스 설계
|
||||
- 웹플럭스와 다른 스프링 모듈 통합
|
||||
- 배포 및 모니터링 팁
|
||||
|
||||
11. **웹플럭스와 웹소켓**
|
||||
- 실시간 통신을 위한 웹소켓 구현
|
||||
- 웹플럭스와 웹소켓의 조합 예제
|
||||
- 활용 사례 (채팅 애플리케이션 등)
|
||||
|
||||
12. **마무리: 웹플럭스의 현재와 미래**
|
||||
- 스프링 웹플럭스의 최신 트렌드
|
||||
- 커뮤니티와 생태계 이야기
|
||||
- 다음 단계로 나아가기 위한 학습 로드맵
|
||||
|
||||
---
|
||||
|
||||
이 목차는 웹플럭스의 기초부터 실무 적용까지 단계적으로 다루며, 독자가 자연스럽게 개념을 익히고 실습할 수 있도록 구성했습니다. 특정 주제에 더 깊이 들어가고 싶거나 추가적인 내용을 원하시면 말씀해주세요!
|
||||
Reference in New Issue
Block a user