Spring Boot κΈ°λ°μ μΌμ κ΄λ¦¬ λ°±μλ μ ν리μΌμ΄μ
μ
λλ€.
JWT μΈμ¦, JPA, QueryDSL, Spring Security λ± λ€μν κΈ°μ μ€νμ νμ©νμ¬ κ°λ°.
λΆλ₯
κΈ°μ
Language
Java 17
Framework
Spring Boot 3.3.3
ORM
Spring Data JPA, QueryDSL
Security
Spring Security, JWT (jjwt 0.11.5)
DB
MySQL
Build
Gradle
μνΈν
BCrypt
src/main/java/org/example/expert
βββ aop/ # AOP κ΄λ ¨
βββ client/ # μΈλΆ API ν΄λΌμ΄μΈνΈ (λ μ¨)
βββ config/ # μ€μ (JWT, Filter, Security λ±)
βββ domain/
β βββ auth/ # μΈμ¦ (νμκ°μ
, λ‘κ·ΈμΈ)
β βββ comment/ # λκΈ
β βββ common/ # κ³΅ν΅ (μμΈ, DTO, μ΄λ
Έν
μ΄μ
)
β βββ manager/ # λ΄λΉμ
β βββ todo/ # μΌμ
β βββ user/ # μ μ
1. @Transactional μ΄ν΄ - ν μΌ μ μ₯ μ€λ₯ μμ
TodoServiceμ saveTodo() λ©μλμ @Transactionalμ΄ λλ½λμ΄ read-only νΈλμμ
μμ INSERTκ° μ€ν¨νλ λ¬Έμ λ₯Ό μμ νμ΅λλ€.
ν΄λμ€ λ 벨μ @Transactional(readOnly = true) μλμ λ©μλ λ λ²¨λ‘ @Transactionalμ μΆκ°νμ¬ ν΄κ²°νμ΅λλ€.
2. JWTμ λλ€μ μΆκ°
User μν°ν°μ nickname 컬λΌμ μΆκ°νμ΅λλ€.
νμκ°μ
μμ²(SignupRequest) λ° μλ΅ DTOμ λλ€μ νλλ₯Ό λ°μνμ΅λλ€.
JWT ν ν° μμ± μ nickname claimμ ν¬ν¨νλλ‘ JwtUtilμ μμ νμ΅λλ€.
3. JPQL κΈ°λ° μ‘°κ±΄ κ²μ μΆκ°
ν μΌ λͺ©λ‘ μ‘°ν μ weatherμ μμ μΌ(modifiedAt) λ²μλ₯Ό μ νμ μΌλ‘ νν°λ§ν μ μλλ‘ κ°μ νμ΅λλ€.
JPQLμ μ¬μ©νλ©°, κ° μ‘°κ±΄μ nullμ΄λ©΄ 무μλ©λλ€.
// μμ: weather + κΈ°κ° μ‘°κ±΄ κ²μ
GET /todos ?weather =Sunny &startDate =2024 -01 -01 T00 :00 :00 &endDate =2024 -12 -31 T23 :59 :59
4. 컨νΈλ‘€λ¬ ν
μ€νΈ μμ
todo_λ¨κ±΄_μ‘°ν_μ_todoκ°_μ‘΄μ¬νμ§_μμ_μμΈκ°_λ°μνλ€() ν
μ€νΈκ° μ€ν¨νλ μμΈμ, InvalidRequestException λ°μ μ μνμ½λκ° 400 BAD_REQUESTμμλ ν
μ€νΈμμ 200 OKλ₯Ό κΈ°λνκ³ μμκΈ° λλ¬Έμ
λλ€.
ν
μ€νΈ μ½λλ₯Ό μλμ κ°μ΄ μμ νμ¬ ν΅κ³ΌμμΌ°μ΅λλ€.
mockMvc .perform (get ("/todos/{todoId}" , todoId ))
.andExpect (status ().isBadRequest ())
.andExpect (jsonPath ("$.status" ).value (HttpStatus .BAD_REQUEST .name ()))
.andExpect (jsonPath ("$.code" ).value (HttpStatus .BAD_REQUEST .value ()))
.andExpect (jsonPath ("$.message" ).value ("Todo not found" ));
AdminAccessLoggingAspectμ ν¬μΈνΈμ»·μ΄ UserController.getUser()λ₯Ό κ°λ¦¬ν€κ³ μμμΌλ, μλλ UserAdminController.changeUserRole() μ€ν μ λ‘κΉ
μ΄μμ΅λλ€.
μ΄λλ°μ΄μ€λ₯Ό @After β @Beforeλ‘, ν¬μΈνΈμ»·μ changeUserRole()λ‘ λ³κ²½νμ΅λλ€.
@ Before ("execution(* org.example.expert.domain.user.controller.UserAdminController.changeUserRole(..))" )
public void logBeforeChangeUserRole (JoinPoint joinPoint ) { ... }
6. JPA Cascade - λ΄λΉμ μλ λ±λ‘
Todo μμ± μ μμ±μκ° λ΄λΉμ(Manager)λ‘ μλ λ±λ‘λμ΄μΌ ν©λλ€.
Todo μν°ν°μ managers 컬λ μ
μ cascade = CascadeType.PERSIST μ΅μ
μ μΆκ°νμ¬, Todo μ μ₯ μ Managerλ ν¨κ» μ μ₯λλλ‘ κ΅¬ννμ΅λλ€.
@ OneToMany (mappedBy = "todo" , cascade = CascadeType .PERSIST )
private List <Manager > managers = new ArrayList <>();
7. N+1 λ¬Έμ ν΄κ²° - λκΈ μ‘°ν
CommentRepository.findByTodoIdWithUser()μ JPQLμ JOIN FETCHλ‘ μμ νμ¬ μ°κ΄λ Userλ₯Ό ν λ²μ μΏΌλ¦¬λ‘ κ°μ Έμ€λλ‘ κ°μ νμ΅λλ€.
@ Query ("SELECT c FROM Comment c JOIN FETCH c.user WHERE c.todo.id = :todoId" )
List <Comment > findByTodoIdWithUser (@ Param ("todoId" ) Long todoId );
8. QueryDSL μ ν - Todo λ¨κ±΄ μ‘°ν
JPQLλ‘ μμ±λ findByIdWithUser()λ₯Ό QueryDSLλ‘ μ ννμ΅λλ€.
fetchJoin()μ μ¬μ©νμ¬ N+1 λ¬Έμ κ° λ°μνμ§ μλλ‘ μ²λ¦¬νμ΅λλ€.
return queryFactory
.selectFrom (todo )
.leftJoin (todo .user , user ).fetchJoin ()
.where (todo .id .eq (todoId ))
.fetchOne ();
10. QueryDSL + Projection κΈ°λ° κ²μ API
μλ‘μ΄ API GET /todos/searchλ₯Ό μΆκ°νμ΅λλ€.
κ²μ 쑰건: μ λͺ©(λΆλΆ μΌμΉ), μμ±μΌ λ²μ, λ΄λΉμ λλ€μ(λΆλΆ μΌμΉ)
Projections.constructorλ₯Ό νμ©νμ¬ νμν νλ(μ λͺ©, λ΄λΉμ μ, λκΈ μ)λ§ λ°νν©λλ€.
κ²°κ³Όλ νμ΄μ§ μ²λ¦¬λμ΄ λ°νλ©λλ€.
// μλ΅ μμ
{
"title" : "μ€νλ§ κ³΅λΆ" ,
"managerCount" : 3 ,
"commentCount" : 5
}
11. Transaction μ¬ν - λ§€λμ λ±λ‘ λ‘κ·Έ ----> ꡬν μμ
λ§€λμ λ±λ‘ μμ² μ log ν
μ΄λΈμ μμ² λ‘κ·Έλ₯Ό μ μ₯ν©λλ€.
@Transactional(propagation = Propagation.REQUIRES_NEW)λ₯Ό μ¬μ©νμ¬ λ§€λμ λ±λ‘ νΈλμμ
κ³Ό λ‘κ·Έ μ μ₯ νΈλμμ
μ λΆλ¦¬νμ΅λλ€.
λ§€λμ λ±λ‘μ΄ μ€ν¨νλλΌλ λ‘κ·Έλ λ°λμ μ μ₯λ©λλ€.
Method
URI
μ€λͺ
POST
/auth/signup
νμκ°μ
POST
/auth/signin
λ‘κ·ΈμΈ
Method
URI
μ€λͺ
κΆν
GET
/users/{userId}
μ μ μ‘°ν
μΈμ¦
PUT
/users
λΉλ°λ²νΈ λ³κ²½
μΈμ¦
PATCH
/admin/users/{userId}
μ μ μν λ³κ²½
ADMIN
Method
URI
μ€λͺ
κΆν
POST
/todos
μΌμ μμ±
μΈμ¦
GET
/todos
μΌμ λͺ©λ‘ μ‘°ν (νμ΄μ§)
μΈμ¦
GET
/todos/{todoId}
μΌμ λ¨κ±΄ μ‘°ν
μΈμ¦
GET
/todos/search
μΌμ κ²μ (QueryDSL)
μΈμ¦
Comment
Method
URI
μ€λͺ
κΆν
POST
/todos/{todoId}/comments
λκΈ μμ±
μΈμ¦
GET
/todos/{todoId}/comments
λκΈ λͺ©λ‘ μ‘°ν
μΈμ¦
Method
URI
μ€λͺ
κΆν
POST
/todos/{todoId}/managers
λ΄λΉμ λ±λ‘
μΈμ¦
GET
/todos/{todoId}/managers
λ΄λΉμ λͺ©λ‘ μ‘°ν
μΈμ¦
DELETE
/todos/{todoId}/managers/{managerId}
λ΄λΉμ μμ
μΈμ¦
@Transactional readOnlyμ μ°κΈ° μμ
λ¬Έμ : ν΄λμ€ λ 벨μ @Transactional(readOnly = true)κ° μ μΈλ μνμμ saveTodo()μ λ³λ @Transactionalμ΄ μμ΄ INSERT 쿼리 μ€ν μ μμΈ λ°μ
ν΄κ²° : μ°κΈ° μμ
μ΄ νμν λ©μλμ @Transactionalμ λͺ
μμ μΌλ‘ μΆκ°
λ¬Έμ : @After μ΄λλ°μ΄μ€κ° μλͺ»λ λμ λ©μλλ₯Ό κ°λ¦¬ν€κ³ μμ΄ μλν λμ λΆκ°
ν΄κ²° : ν¬μΈνΈμ»· λμκ³Ό μ΄λλ°μ΄μ€ μ’
λ₯(@Before)λ₯Ό λͺ¨λ μμ
λ¬Έμ : λκΈ μ‘°ν μ κ° λκΈλ§λ€ μ μ λ₯Ό λ³λ μΏΌλ¦¬λ‘ μ‘°ννμ¬ N+1 λ°μ
ν΄κ²° : JOIN FETCHλ‘ μ°κ΄ μν°ν°λ₯Ό ν λ²μ μ‘°ν