CascadeType.PERSIST
persist() 하면, 연관된 자식 엔티티도 자동으로 persist() 된다► Todo
@OneToMany(mappedBy = "todo", cascade = CascadeType.PERSIST)
private List<Manager> managers = new ArrayList<>();


N+1 문제 발생✔️ 1(comments) + N(작성자(user)) 조회

public interface CommentRepository extends JpaRepository<Comment, Long> {
@EntityGraph(attributePaths = {"user"}, type = EntityGraph.EntityGraphType.FETCH)
@Query("SELECT c FROM Comment c JOIN c.user WHERE c.todo.id = :todoId")
List<Comment> findByTodoIdWithUser(@Param("todoId") Long todoId);
}
@EntityGraph 를 적용하여 Comment 조회 시 한 번에 user 데이터까지 조회되도록 수정

findByWithUser 를 QueryDSL 로 변경 ▶︎ build.gradle: dependencies 추가
// QueryDsl
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
annotationProcessor "jakarta.annotation:jakarta.annotation-api"
annotationProcessor "jakarta.persistence:jakarta.persistence-api"
▶︎ QueryDslConfig
@Configuration
public class QueryDslConfig {
@PersistenceContext
private EntityManager entityManager;
@Bean
public JPAQueryFactory jpaQueryFactory() {
return new JPAQueryFactory(entityManager);
}
}
▶︎ TodoRepository
public interface TodoRepository extends JpaRepository<Todo, Long>, TodoRepositoryQuery {
// ...
}
▶︎ TodoRepositoryQueryImpl : TodoRepositoryQuery 구현체
@RequiredArgsConstructor
public class TodoRepositoryQueryImpl implements TodoRepositoryQuery {
private final JPAQueryFactory jpaQueryFactory;
@Override
public Optional<Todo> findByIdWithUserQuery(Long todoId) {
return Optional.ofNullable(jpaQueryFactory.select(todo)
.from(todo)
.leftJoin(todo.user).fetchJoin()
.where(todo.id.eq(todoId))
.fetchOne());
}
}

▶︎ build.gradle: dependencies 추가
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
▶︎ JwtAuthenticationToken: AbstractAuthenticationToken 상속
public class JwtAuthenticationToken extends AbstractAuthenticationToken {
private final AuthUser authUser;
public JwtAuthenticationToken(AuthUser authUser) {
super(authUser.getAuthorities());
this.authUser = authUser;
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return null; // session 스펙
}
@Override
public Object getPrincipal() {
return authUser;
}
}
▶︎ JwtAuthenticationFilter:
doFilterInternal()
@Override
protected void doFilterInternal(HttpServletRequest httpRequest,
@NonNull HttpServletResponse httpResponse,
@NonNull FilterChain chain) throws ServletException, IOException {
String authorizationHeader = httpRequest.getHeader("Authorization");
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
String jwt = jwtUtil.substringToken(authorizationHeader);
try {
Claims claims = jwtUtil.extractClaims(jwt);
if (SecurityContextHolder.getContext().getAuthentication() == null) {
setAuthentication(claims);
}
} // ...
}
chain.doFilter(httpRequest, httpResponse);
}
SecurityContextHolder.getContext().setAuthentication()
private void setAuthentication(Claims claims) {
Long userId = Long.valueOf(claims.getSubject());
String email = claims.get("email", String.class);
UserRole userRole = UserRole.of(claims.get("userRole", String.class));
String nickname = claims.get("nickname", String.class);
AuthUser authUser = new AuthUser(userId, email, userRole, nickname);
JwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(authUser);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
▶︎ UserRole: ENUM 수정 👉 prefix "ROLE_" 추가
@Getter
@RequiredArgsConstructor
public enum UserRole {
// Spring Security 에서 ROLE 을 사용하려면 반드시 prefix 로 "ROLE_" 을 붙여야 한다.
ROLE_ADMIN(Authority.ADMIN), ROLE_USER(Authority.USER);
private final String userRole;
public static UserRole of(String role) {
return Arrays.stream(UserRole.values())
.filter(r -> r.name().equalsIgnoreCase(role))
.findFirst()
.orElseThrow(() -> new InvalidRequestException("유효하지 않은 UserRole"));
}
public static class Authority {
public static final String USER = "ROLE_USER";
public static final String ADMIN = "ROLE_ADMIN";
}
}
▶︎ ~~Controller: 기존 Filter와 ArgumentResolver 👉 @Auth 어노테이션 + AuthUser → @AuthenticationPrincipal
@PostMapping("/todos")
public ResponseEntity<TodoSaveResponse> saveTodo(
@AuthenticationPrincipal AuthUser authUser,
@Valid @RequestBody TodoSaveRequest todoSaveRequest
) {
return ResponseEntity.ok(todoService.saveTodo(authUser, todoSaveRequest));
}
▶︎ UserAdminController: 접근 권한 및 유저 권한 기능 👉 "ROLE_ADMIN"만 접근 가능
@Secured(UserRole.Authority.ADMIN)
@PatchMapping("/admin/users/{userId}")
public void changeUserRole(@PathVariable long userId, @RequestBody UserRoleChangeRequest userRoleChangeRequest) {
userAdminService.changeUserRole(userId, userRoleChangeRequest);
}
→ @Secured(UserRole.Authority.ADMIN) : 권한 필터링