플러스 주차 개인 과제를 진행하며 기록을 남겨보기 위해 글을 작성합니다.
이번 과제는 레거시 코드를 리펙토링해보는 과제입니다.
레거시 코드
-. 레거시 코드는 개발된 지 오래되어서 최신 기술이나 표준과는 다소 맞지 않는 코드를 뜻합니다.
레거시 코드를 리펙토링해보면
-. 레거시 코드를 리펙토링 하기 위해서는 먼저 코드를 확실히 이해하고있어야 하기 때문에 리펙토링을 해보며 코드를 이해하고 분석하는 능력을 키울 수 있습니다.
Level. 2
6. JPA Cascade
앗! 실수로 코드를 지웠어요!
@OneToMany(mappedBy = "todo")
private List<Manager> managers = new ArrayList<>();
할 일을 새로 저장할 시, 할 일을 생성한 유저는 담당자로 자동 등록되어야 합니다.
JPA의 cascade 기능을 활용해 할 일을 생성한 유저가 담당자로 등록될 수 있게 해주세요.
문제 해결 과정
Cascade Type.PERSIST를 추가
@OneToMany(mappedBy = "todo", cascade = CascadeType.PERSIST)
private List<Manager> managers = new ArrayList<>();
7. N + 1
CommentController
클래스의 getComments()
API를 호출할 때 N+1 문제가 발생하고 있어요. N+1 문제란, 데이터베이스 쿼리 성능 저하를 일으키는 대표적인 문제 중 하나로, 특히 연관된 엔티티를 조회할 때 발생해요.
해당 문제가 발생하지 않도록 코드를 수정해주세요.
N + 1 로그
Hibernate: select c1_0.id,c1_0.contents,c1_0.created_at,c1_0.modified_at,c1_0.todo_id,c1_0.user_id from comments c1_0 where c1_0.todo_id=?
Hibernate: select u1_0.id,u1_0.created_at,u1_0.email,u1_0.modified_at,u1_0.nickname,u1_0.password,u1_0.user_role from users u1_0 where u1_0.id=?
문제 해결 과정
CommentRepository의 Query를 fetch join으로 변경
N + 1 문제를 해결하는 데에는 fetch join 외에도 여러 가지 방법이 있지만 현재 주어진 코드에서 가장 간편하게 해결할 수 있는 방법이 fetch join을 사용하는 것이라고 생각했습니다.
@Query("SELECT c FROM Comment c JOIN FETCH c.user WHERE c.todo.id = :todoId")
변경 후 로그
Hibernate: /* SELECT c FROM Comment c JOIN FETCH c.user WHERE c.todo.id = :todoId */ select c1_0.id,c1_0.contents,c1_0.created_at,c1_0.modified_at,c1_0.todo_id,c1_0.user_id,u1_0.id,u1_0.created_at,u1_0.email,u1_0.modified_at,u1_0.nickname,u1_0.password,u1_0.user_role from comments c1_0 join users u1_0 on u1_0.id=c1_0.user_id where c1_0.todo_id=?
8. QueryDSL
JPQL로 작성된 findByIdWithUser
를 QueryDSL로 변경합니다.
7번과 마찬가지로 N+1 문제가 발생하지 않도록 유의해 주세요!
문제 해결 과정
fetchJoin을 사용하여 N+1문제 방지
1. QueryDSL 설정
1-1. build.gredle 의존성 추가
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"
1-2. QClass 생성
Gradle의 clean, compileJava을 실행
1-3. Q파일 생성 확인 후 Application 1회 실행하여 QueryDSL 설정 완료
2. TodoRepositoryQuery와 TodoRepositoryQueryImpl 추가
TodoRepositoryQuery
public interface TodoRepositoryQuery {
// weather 조건과 기간 조건이 모두 있는 경우
Page<Todo> findByWeatherAndDate(String weather, LocalDateTime startDate, LocalDateTime endDate, Pageable pageable);
// weather 조건만 있는 경우
Page<Todo> findByWeatherOrOrderByModifiedAt(String weather, Pageable pageable);
// 기간 조건만 있는 경우
Page<Todo> findByDateOrderByModifiedAt(LocalDateTime startDate, LocalDateTime endDate, Pageable pageable);
// 아무 조건도 없는 경우 전체 조회
Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable);
// 특정 일정 조회
Optional<Todo> findByIdWithUser(Long todoId);
}
TodoRepositoryQueryImpl
public class TodoRepositoryQueryImpl implements TodoRepositoryQuery {
private final JPAQueryFactory queryFactory;
private final QTodo todo = QTodo.todo;
private final QUser user = QUser.user;
public TodoRepositoryQueryImpl(EntityManager em) {
this.queryFactory = new JPAQueryFactory(em);
}
// weather 조건과 기간 조건이 모두 있는 경우
@Override
public Page<Todo> findByWeatherAndDate(String weather, LocalDateTime startDate, LocalDateTime endDate, Pageable pageable) {
List<Todo> todos = queryFactory
.selectFrom(todo)
.leftJoin(todo.user, user).fetchJoin()
.where(todo.weather.eq(weather).and(todo.modifiedAt.between(startDate, endDate)))
.orderBy(todo.modifiedAt.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
// 페이징 처리를 위한 조회된 일정을 카운트
long total = Optional.ofNullable(queryFactory
.select(todo.count())
.from(todo)
.where(todo.weather.eq(weather).and(todo.modifiedAt.between(startDate, endDate)))
.fetchOne())
.orElse(0L);
return new PageImpl<>(todos, pageable, total);
}
... 외 Qeury
}
3. TodoRepository
public interface TodoRepository extends JpaRepository<Todo, Long>, TodoRepositoryQuery {
}
9. Spring Security
Spring Security를 도입하기로 결정했어요!
기존 Filter
와 Argument Resolver
를 사용하던 코드들을 Spring Security로 변경해주세요.
- 접근 권한 및 유저 권한 기능은 그대로 유지해주세요.
- 권한은 Spring Security의 기능을 사용해주세요.
토큰 기반 인증 방식은 유지할 거예요. JWT는 그대로 사용해주세요.
문제 해결 과정
1. Spring Security 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-security'
2. 필요한 클래스 추가
SecurityConfig
SecurityFilterChain을 사용해 접근 권한을 관리하고 JWT 인증 필터를 추가합니다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtUtil jwtUtil;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// JWT 인증을 사용하기 때문에 비활성화
.csrf(csrf -> csrf.disable())
// JWT 인증을 사용하기 때문에 세션 관리 비활성화
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(new JwtAuthenticationFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
JwtAuthenticationFilter
OncePerRequestFilter를 상속하여 모든 요청에서 JWT 토큰을 검사하고 인증 정보를 설정합니다.
-. OncePerRequestFilter
: Spring에서 제공하는 필터 클래스로 한 요청당 한 번만 실행되는 필터를 구현할 때 사용됩니다.
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
String token = bearerToken.substring(7);
try {
Claims claims = jwtUtil.extractClaims(token);
String email = claims.getSubject();
String userRole = claims.get("userRole", String.class);
String nickname = claims.get("nickname", String.class);
if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
email, null, List.of(new SimpleGrantedAuthority("ROLE_" + userRole))
);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
request.setAttribute("userId", Long.parseLong(claims.getSubject()));
request.setAttribute("email", email);
request.setAttribute("userRole", userRole);
request.setAttribute("nickname", nickname);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid JWT token");
return;
}
}
filterChain.doFilter(request, response);
}
}
3. 필요없는 클래스 제거
WebConfig, JwtFilter, FilterConfig, AuthUserArgumentResolver
'내일배움캠프 > 내일배움캠프' 카테고리의 다른 글
[내일배움캠프] 트러블 슈팅 (0) | 2024.11.20 |
---|---|
[내일배움캠프] TIL : 관점 지향 프로그래밍 AOP (0) | 2024.11.18 |
[내일배움캠프] TIL : RawJPA 기본 (0) | 2024.11.14 |
[내일배움캠프] TIL : Transaction (0) | 2024.11.13 |
[내일배움캠프] TIL : Projection (0) | 2024.11.13 |
댓글