[Spring] 플러스 주차 개인과제 - Level2

Yuri·2025년 3월 19일

Spring

목록 보기
19/21

6. JPA Cascade

  • 할 일을 생성한 유저가 담당자로 자동 등록

🤓 코드 해석

CascadeType.PERSIST

  • 부모 엔티티를 저장 persist() 하면, 연관된 자식 엔티티도 자동으로 persist() 된다
  • CascadeType.PERSIST 의 경우, 삭제하거나 수정할 때 자식 엔티티에는 영향을 주지 않는다.

✏️ 코드 수정

► Todo

@OneToMany(mappedBy = "todo", cascade = CascadeType.PERSIST)
private List<Manager> managers = new ArrayList<>();
  • 부모 엔티티(할일)를 저장하면 자식(담당자)도 자동으로 저장된다

👀 결과

► Todo 저장

► Manager 조회

7. N+1

  • comments 조회 시 N+1 문제 발생

✔️ 1(comments) + N(작성자(user)) 조회

✏️ 코드 수정

► CommentRepository

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 데이터까지 조회되도록 수정

8. QueryDSL

  • JPQL로 작성된 findByWithUser 를 QueryDSL 로 변경
  • N+1 문제가 발생하지 않도록 유의

🤓 코드 해석

  • 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());
    }
}

👀 결과

9. Spring Security

  • 기존 Filter와 Argument Resolver를 사용하던 코드들을 Spring Security로 변경
    • 접근 권한 및 유저 권한 기능은 그대로 유지
    • 권한은 Spring Security의 기능을 사용
    • JWT 기반 인증 방식 유지 (stateless)

✏️ 코드 수정

▶︎ 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) : 권한 필터링

profile
안녕하세요 :)

0개의 댓글