Spring Boot 3 + JPA + WebFlux + H2 λ‘ κ΅¬νν κ°λ¨ν βμ±
곡μ νλ«νΌβ λ°±μλμ
λλ€.
νμ μΈμ¦λΆν° μ±
CRUD, κ·Έλ¦¬κ³ OpenAI Images APIλ₯Ό μ΄μ©ν μλ νμ§ μμ± κΈ°λ₯κΉμ§ ν¬ν¨λμ΄ μμ΅λλ€.
| κ΅¬λΆ | μ€λͺ | HTTP |
|---|---|---|
| νμ | νμκ°μ / λ‘κ·ΈμΈ | POST /api/members/** |
| μ± λͺ©λ‘ | μ μ²΄Β·λ΄ μμ¬Β·νμ΄μ§ | GET /api/books |
| μ± κ²μ | μ λͺ©Β·μ μ ν€μλ | GET /api/books/?keyword= |
| μ± μμΈ | λ¨μΌ μ‘°ν | GET /api/books/{bookId} |
| μ± μμ± | μ λͺ©Β·λ΄μ©Β·νμ§ μλ μμ± | POST /api/books |
| μ± μμ | μ λͺ©Β·λ΄μ© μμ | PUT /api/books/{bookId} |
| μ± μμ | μ νν μ± μμ | DELETE /api/books/{bookId} |
| νμ§ μμ± | OpenAI(DALL-E 2) Β· 1024Γ1024 | POST /api/books/cover |
νμ§ μλν¬μΈνΈ λ°λ
{ "title": "Walking Library", "content": "A shy student walks into pages that become city streetsβ¦" }
- μΈμ΄: Java 17
- νλ μμν¬: Spring Boot 3.x
- Spring Web, Spring WebFlux (WebClient), Spring Data JPA, Spring Security (JWT)
- λ°μ΄ν°λ² μ΄μ€: H2
- λΉλ λꡬ: Gradle
- μ¬μ© λΌμ΄λΈλ¬λ¦¬:
- OpenAI Images API μ°λ β Spring WebFlux(WebClient), Jackson
- JWT λ°κΈ/κ²μ¦ β spring-security-jwt
- μ΄μ νκ²½: AWS EC2
src/main/resources/application.yml
spring:
jackson:
time-zone: Asia/Seoul
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:testdb;
username: sa
password: ********
h2:
console:
enabled: true
path: /h2-console
settings:
web-allow-others: true
jpa:
hibernate:
ddl-auto: update
properties:
hibernate:
show_sql: true
format_sql: true
use_sql_comments: true
default_batch_fetch_size: 1000
open-in-view: false
jwt:
secret: sGk+***************************
openai:
api-key: sk-**************************
image:
model: dall-e-2
size: 1024x1024βββ main
βββ java
β βββ com.example.aivle
β βββ AivleApplication.java // λ©μΈ μ ν리μΌμ΄μ
ν΄λμ€
β βββ domain
β β βββ book
β β βββ controller
β β β βββ BookController.java // λμ API μλν¬μΈνΈ
β β βββ dto
β β β βββ BookRequest.java // λμ λ±λ‘/μμ μμ² DTO
β β β βββ BookResponse.java // λμ μμΈ μ‘°ν μλ΅ DTO
β β β βββ BookSummaryResponse.java // λμ λͺ©λ‘ μ‘°ν μλ΅ DTO
β β β βββ CoverRequest.java // νμ§ μμ± μμ² DTO
β β β βββ CoverResponse.java // νμ§ μμ± μλ΅ DTO
β β βββ entity
β β β βββ Book.java // λμ μν°ν°
β β βββ repository
β β β βββ BookRepository.java // JpaRepository<Book, Integer>
β β βββ service
β β βββ BookService.java // λμ μλΉμ€ μΈν°νμ΄μ€
β β βββ BookServiceImpl.java // λμ μλΉμ€ ꡬν체
β βββ member
β β βββ presentation
β β β βββ MemberController.java // νμ API μλν¬μΈνΈ
β β βββ dto
β β β βββ LoginRequest.java // νμ κ°μ
/λ‘κ·ΈμΈ μμ² DTO
β β β βββ LoginResponse.java // νμ κ°μ
/λ‘κ·ΈμΈ μλ΅ DTO
β β βββ entity
β β β βββ Member.java // νμ μν°ν°
β β βββ repository
β β β βββ MemberRepository.java // JpaRepository<Member, Integer>
β β βββ service
β β βββ MemberService.java // νμ μλΉμ€ μΈν°νμ΄μ€
β β βββ MemberServiceImpl.java // νμ μλΉμ€ ꡬν체
β βββ global
β βββ base
β β βββ BaseEntity.java // μμ±μΌμΒ·μμ μΌμ μλ κ΄λ¦¬
β βββ config
β β βββ JpaConfig.java // JPA μ€μ
β β βββ SecurityConfig.java // Spring Security μ€μ (JWT ν¬ν¨)
β β βββ WebConfig.java // CORS λ± μΉ μ€μ
β βββ exception
β β βββ CoverGenerationException.java // νμ§ μμ± μ€ν¨ μμΈ
β β βββ InvalidApiKeyException.java // μλͺ»λ/λλ½λ OpenAI API ν€
β β βββ OrganizationAuthException.java // μ‘°μ§(Org) κΆν λΆμ‘± μμΈ
β β βββ UnsupportedParameterException.java // μ§μνμ§ μλ νλΌλ―Έν° μμΈ
β βββ openai
β β βββ AiCoverClient.java // OpenAI Images API μ°λ ν΄λΌμ΄μΈνΈ
β βββ response
β β βββ CustomException.java // λλ©μΈλ³ 컀μ€ν
μμΈ κ³΅ν΅ λΆλͺ¨
β β βββ ErrorCode.java // κΈ°λ₯λ³ μ€λ₯ μ½λ(enum)
β β βββ GlobalExceptionHandler.java // μ μ μμΈ μ²λ¦¬κΈ° (@RestControllerAdvice)
β β βββ Response.java // API μλ΅ νμ€ λνΌ
β β βββ SuccessCode.java // μ±κ³΅ λ©μμ§ μ½λ(enum)
β βββ util
β βββ jwt
β βββ JwtTokenFilter.java // JWT μΈμ¦/μΈκ° νν°
β βββ JwtTokenUtils.java // JWT λ°κΈ/κ²μ¦ μ νΈ
β βββ ResponseUtils.java // κ³΅ν΅ μλ΅ μμ± μ νΈ
βββ resources
βββ static
βββ templates
βββ application.yml // κ³΅ν΅ μ€μ
βββ application-dev.yml // κ°λ° νκ²½ μ€μ
βββ application-local.yml // λ‘컬 νκ²½ μ€μ
νμ(Member) λλ©μΈ κ΄λ ¨ κΈ°λ₯μ λ΄λΉν©λλ€.
- μ£Όμ κΈ°λ₯
- νμ κ°μ (Signup), λ‘κ·ΈμΈ(Login)
- JWT κΈ°λ° μΈμ¦ λ° μΈμ κ΄λ¦¬
domain/member
ββ controller
β ββ MemberController.java βΆ νμ κ΄λ ¨ API μ 곡
β β’ signup() : νμ κ°μ
β β’ login() : λ‘κ·ΈμΈ (JWT λ°κΈ)
β β’ findMember() : νμ μ‘°ν
β
ββ dto
β ββ LoginRequest.java βΆ νμ κ°μ
/λ‘κ·ΈμΈ μμ² DTO
β β β’ νλ: loginId, password
β β
β ββ LoginResponse.java βΆ νμ κ°μ
/λ‘κ·ΈμΈ μλ΅ DTO
β β’ νλ: memberId
β
ββ entity
β ββ Member.java βΆ νμ μν°ν°
β β’ νλ: id, loginId, password
β β’ BaseEntity μμ(μμ±μΌ/μμ μΌ μλ κ΄λ¦¬)
β
ββ repository
β ββ MemberRepository.java βΆ `JpaRepository<Member, Integer>`
β β’ λ©μλ: findByLoginId(String loginId) β νμ μ‘°ν
β
ββ service
ββ MemberService.java βΆ νμ μλΉμ€ μΈν°νμ΄μ€
β β’ signup(LoginRequest) : νμ κ°μ
(μ€λ³΅ κ²μ¬ ν μ μ₯)
β β’ login(LoginRequest) : λ‘κ·ΈμΈ (ID/PW νμΈ β JWT λ°κΈ)
β β’ findMember(Integer memberId) : νμ μ‘°ν
β
ββ MemberServiceImpl.java βΆ νμ μλΉμ€ ꡬν체
β’ λΉμ¦λμ€ λ‘μ§ μ€μ ꡬν (μ€λ³΅ κ²μ¬, μνΈν, ν ν° μμ± λ±)
λμ(Book) λλ©μΈ κ΄λ ¨ κΈ°λ₯μ λ΄λΉν©λλ€.
- μ£Όμ κΈ°λ₯
- λμ λ±λ‘, μ‘°ν, μμ , μμ
- νμ§ μ΄λ―Έμ§(AI) μμ±
domain/book
ββ controller
β ββ BookController.java βΆ λμ κ΄λ ¨ API μ 곡
β β’ addBook() : λμ λ±λ‘
β β’ findBook() / findBooks() : λμ μ‘°ν
β β’ updateBook() : λμ μμ
β β’ deleteBook() : λμ μμ
β β’ generateCover() : λμ νμ§(AI) μμ±
β
ββ controller/dto
β ββ BookRequest.java βΆ λμ λ±λ‘/μμ μμ² DTO
β β β’ νλ: title, author, content, coverImageUrl
β β
β ββ BookResponse.java βΆ λμ μμΈ μ‘°ν μλ΅ DTO
β β β’ νλ: bookId, memberId, title, author, content, createdAt, updatedAt
β β
β ββ BookSummaryResponse.java βΆ λμ λͺ©λ‘ μ‘°ν μλ΅ DTO
β β β’ νλ: bookId, title, author, createdAt, coverImageUrl
β β
β ββ CoverRequest.java βΆ νμ§ μ΄λ―Έμ§ μμ± μμ² DTO
β β β’ νλ: title, content
β β
β ββ CoverResponse.java βΆ νμ§ μ΄λ―Έμ§ μμ± κ²°κ³Ό DTO
β β’ νλ: success, message, imageUrl
β
ββ entity
β ββ Book.java βΆ λμ μν°ν°
β β’ νλ: id, title, author, content, coverImageUrl
β β’ κ΄κ³: μμ±μ(Member)μ @ManyToOne μ°κ΄κ΄κ³
β
ββ repository
β ββ BookRepository.java βΆ `JpaRepository<Book, Integer>`
β β’ ν€μλ(μ λͺ©, μ μ) κΈ°λ° κ²μ λ©μλ μ 곡 (`findByTitleContainingIgnoreCaseOrAuthorContainingIgnoreCase`)
β
ββ service
ββ BookService.java βΆ λμ μλΉμ€ μΈν°νμ΄μ€
β β’ findBook(Integer bookId) : λ¨κ±΄ μ‘°ν
β β’ findBooks(String keyword) : 리μ€νΈ μ‘°ν (ν€μλ κ²μ)
β β’ addBook(BookRequest, HttpSession) : λ±λ‘
β β’ updateBook(Integer, BookRequest, HttpSession) : μμ (λ³ΈμΈλ§ κ°λ₯)
β β’ deleteBook(Integer, HttpSession) : μμ (λ³ΈμΈλ§ κ°λ₯)
β
ββ BookServiceImpl.java βΆ λμ μλΉμ€ ꡬν체
β’ λΉμ¦λμ€ λ‘μ§ μ€μ ꡬν (Repository νΈμΆ, κΆν κ²μ¦ λ±)
νλ‘μ νΈ μ λ°μμ μ¬μ©λλ κ³΅ν΅ κΈ°λ₯, μ€μ , μμΈ μ²λ¦¬, OpenAI μ°λ λ±μ λ΄λΉν©λλ€.
global
ββ base
β ββ BaseEntity.java βΆ κ³΅ν΅ λ² μ΄μ€ μν°ν°
β β’ @MappedSuperclass
β β’ νλ: createdAt(@CreatedDate), updatedAt(@LastModifiedDate)
β β’ λͺ¨λ μν°ν°κ° μμλ°μ μμ±/μμ μΌμ μλ κ΄λ¦¬
β
ββ openai
β ββ AiCoverClient.java βΆ OpenAI Images API νΈμΆ ν΄λΌμ΄μΈνΈ
β β’ μν : λμ μ λͺ©/λ΄μ©μ λ°μ νμ§ μμ± μμ²
β β’ WebClient μ΄κΈ°ν β Bearer {API-KEY} ν€λ ν¬ν¨
β β’ Prompt κ΅¬μ± β λͺ¨λΈ(dall-e-2), ν¬κΈ°(1024Γ1024) JSON μ μ‘
β β’ μλ΅ JSON β data[0].url μΆμΆ ν λ°ν
β β’ λ΄λΆ DTO: OpenAiImageRequest, OpenAiImageResponse (record)
β β’ μμ‘΄μ±: Spring WebFlux(WebClient), Jackson
β
ββ response
β ββ CustomException.java ⢠컀μ€ν
μμΈ κ³΅ν΅ λΆλͺ¨
β β β’ νλ: ErrorCode, message
β β β’ λλ©μΈλ³ μμΈκ° μμνμ¬ μ¬μ©
β β
β ββ ErrorCode.java βΆ κΈ°λ₯λ³ μ€λ₯ μ½λ(enum)
β β β’ κ° μ½λ: HTTP μν + κΈ°λ³Έ λ©μμ§
β β
β ββ GlobalExceptionHandler.java βΆ μ μ μμΈ μ²λ¦¬κΈ° (@RestControllerAdvice)
β β β’ CustomException, κ²μ¦ μ€λ₯(MethodArgumentNotValidException) λ± μ²λ¦¬
β β
β ββ Response.java βΆ API μλ΅ νμ€ λνΌ
β β β’ success(), error() ν©ν 리 λ©μλ μ 곡
β β β’ κ³΅ν΅ μλ΅ JSON ꡬ쑰 ν΅μΌ
β β
β ββ SuccessCode.java βΆ μ±κ³΅ λ©μμ§ μ½λ(enum)
β β’ OK(200, "μ±κ³΅μ
λλ€") λ± μ¬μ¬μ© κ°λ₯ν μ±κ³΅ λ©μμ§
β
ββ util
ββ jwt
ββ JwtTokenFilter.java βΆ JWT μΈμ¦/μΈκ° νν°
β β’ μμ² ν€λμ Bearer ν ν° κ²μ¦
β β’ μ ν¨ μ SecurityContextμ μΈμ¦ μ 보 μ μ₯
β
ββ JwtTokenUtils.java βΆ JWT μ νΈλ¦¬ν° ν΄λμ€
β β’ AccessToken / RefreshToken μμ±
β β’ ν ν° κ²μ¦, λ§λ£ μκ° μ€μ λ±
β
ββ ResponseUtils.java βΆ κ³΅ν΅ μλ΅ μμ± μ νΈ
⒠컨νΈλ‘€λ¬μμ κ°λ¨νκ² `Response.success(...)` νΈμΆ κ°λ₯
