Level 1
1. @Transactional의 이해 (코드 개선 퀴즈)
문제 : 할 일 저장 기능을 구현한 API(/todos)를 호출할 때, 아래와 같은 에러가 발생.


컨트롤러는 문제가 없어 보이니 서비스에 가서 뭐가 문제인지 확인을 해보자!

이런!
클래스 전체에 @Transactional(readOnly = true)가 붙어있는데,
생성 메서드에 @Transactional이 붙어있지 않아서 생긴 문제였다!
클래스에 @Transactional(readOnly = true)를 선언하게 되면
해당 서비스에서는 모든 메서드가 기본적으로 읽기 전용 트랜잭션으로 동작한다!
그렇기 때문에 데이터 저장이 필요한 saveTodo()에서는
@Transactional를 따로 선언하여 읽기 전용이 아니라 쓰기 가능한 트랜잭션이라고 재정의 해줘야 한다.
2. JWT의 이해 (코드 추가 퀴즈)
문제 : User 테이블에 nickname 칼럼을 추가. nickname은 중복 가능. JWT에서 유저의 닉네임을 꺼내 화면에 보여주기.

일단 유저 엔티티에 닉네임을 추가해 주자!


유저 엔티티에 추가해 주고 정적 팩토리 메서드에도 추가해 주었으니 AuthUser에도 추가해 주면 된다!
그렇다면 이제 JWT에서도 꺼내줘야 하기 때문에 JWT를 건들어보자!

토큰을 생성하는 메서드에도 넣어주고!

JWT에 담긴 사용자 정보를 필터에서 추출하고 request attribute로 전달해서, 이후 로직에서 닉네임까지 사용할 수 있도록 했다!

JwtFilter에서 request attribute에 저장한 nickName 값을
ArgumentResolver에서 꺼내 AuthUser 생성 시 함께 전달할 수 있도록 했다.
쉽게 말해 들어온 값을 String으로 꺼내서 nickName에 전달해 주도록 한 것이다!

그 과정에서 signup 디티오에도 닉네임을 추가해 주고,

Auth Service도 수정해 줬다!

마지막으로 Todo 테스트 컨트롤러에도 추가해주면 된다!
3. JPA의 이해 (코드 개선 퀴즈)
문제
1. 할 일 검색 시 weather 조건으로도 검색할 수 있어야 함. weather 조건은 있을 수도 있고, 없을 수도 있음.
2. 할 일 검색 시 수정일 기준으로 기간 검색이 가능해야 함. 기간의 시작과 끝 조건은 있을 수도 있고, 없을 수도 있음.
3. JPQL을 사용하고, 쿼리 메서드명은 자유롭게 지정하되 너무 길지 않게 설정.

Todo 검색 시 weather 조건으로도 검색 할 수 있게 변경해주고 수정일 기준으로 기간 검색도 가능해야 한다.
그러기 위해서 먼저!

서치용 요청 DTO를 만들어준다! 그리고 이제 서비스, 레포지토리, 컨트롤러를 수정해주면 된다!


서비스에 검색 조건을 추가해주고 레포지토리도 수정해주면 된다!
마지막으로

컨트롤러까지 수정해주면 끝난다!
삼항 연산자 : 조건에 따라 값을 선택해서 변수에 넣는 문법이다.
LocalDateTime startDateTime = request.getStartDate() != null // 조건식
? request.getStartDate().atStartOfDay() // 참일 때 값
: null; // 거짓일 때 값
if문은 동작 중심이지만, 삼항 연산자는 값을 선택하는 것이 중심이다.
이번 과제에서는 조건에 따라 값을 넣는 단순한 로직이어서 삼항 연산자를 사용해봤다!
4. 컨트롤러 테스트의 이해(테스트 코드 퀴즈)
문제 : 테스트 패키지 org.example.expert.domain.todo.controller의 todo_단건_조회_시_todo가_존재하지_않아_예외가_발생한다() 테스트가 실패 중. 정상적으로 수행되어 통과할 수 있도록 수정이 필요함.

문제가 되는 테스트 코드다. 어디가 문제인지 딱 눈에 들어온다.

존재하지 않을 때 예외가 발생하는 테스트인데 왜 OK이가 있을까?

성공(OK) 검증을 에러(BAD_REQUEST) 검증으로 바꿔주면 끝난다!
5. AOP의 이해 (코드 개선 퀴즈)
문제
1. UserAdminController 클래스의 changeUserRole() 메서드가 실행 전 동작해야 됨.
2. AdminAccessLoggingAspect 클래스에 있는 AOP가 개발 의도에 맞도록 코드를 수정

여기는 또 뭐가 문제일까?
changeUserRole() 실행 전 동작해야 된다고 했는데 @After로 되어있다!
이 부분을 @Before로 고쳐주면 된다!
그리고
execution(* org.example.expert.domain.user.controller.UserController.getUser(..))
이 코드!
UserAdminController의 changeUserRole()을 넣어야 하는데 유저가 들어가있다.

요로코롬 바꿔주면 된다!
AOP란 무엇인가?
핵심 기능은 아닌데 여러 곳에 반복되는 것을 공통 기능으로 따로 분리해서 필요한 곳에 끼워 넣는 방식이다!
즉, 핵심 코드 안에 로그 코드를 다 넣지 않고, 바깥에서 공통으로 처리하는 것이다.
AOP를 사용하면 중복 코드가 줄어들고, 핵심 로직이 더 깔끔해지고, 유지보수가 쉬워진다.
Level 2
6. JPA Cascade
문제 : 할 일을 새로 저장할 시, 할 일을 생성한 유저는 담당자로 자동 등록되어야 함. JPA의 cascade 기능을 활용해할 일을 생성한 유저가 담당자로 등록.

표시된 부분에 cascade가 없다!
그런데 생성자를 보면
this.managers.add(new Manager(user, this));
Todo 만들 때 Manager 객체도 리스트에 넣고있다!
이 말은 즉 Todo 저장할 때 Manager도 같이 저장되도록 cascade만 추가해주면 된다는 것이다!

이렇게 써주면 끝난다!
cascade 기능이란?
연관된 엔티티를 같이 저장/삭제되게 전파하는 기능이다.
예를 들어 Todo를 저장할 때 그 안에 연결된 Manager도 같이 저장되게 하고 싶을 때 사용하는 기능이다.
7. N+1



자 어떻게 수정해줘야 할지 보자!
서비스를 보니 레포지토리에서 무슨 문제가 있겠다는 걸 알 수 있었다!

오 조인은 있는데 패치가 없다!
서비스 코드를 다시 보면 댓글을 조회할 때 댓글을 작성한 유저도 응답에 들어가는데,
레포지토리에서는 JOIN FETCH가 아니라 JOIN만 사용하여
서비스에서 유저를 불러올 때마다 추가 쿼리를 발생 시키던 것이었다!

이 부분을 JOIN FETCH로 수정해주면 끝이다!
8. QueryDSL
문제 : JPQL로 작성된 findByIdWithUser를 QueryDSL로 변경. N+1 문제가 발생하지 않도록 유의.

7번 문제와 비슷하다!
N+1 문제가 발생하지 않기 위해 서비스에서 유저를 가져온다는 것을 미리 확인해준다!
그리고 이제 공사를 시작해주면 된다~

기존 JPQL findByIdWithUser() 메서드 삭제해준다!
그리고

TodoRepositoryCustom 인터페이스 생성해준다!
TodoRepository는 인터페이스라서 복잡한 QueryDSL 구현 코드를 직접 넣지 않는 구조다.
그렇기 때문에 직접 구현할 메서드가 있다고 선언하는 인터페이스를 따로 만들어주는 것이다!
생성 해준 후에는

TodoRepository 선언부에 TodoRepositoryCustom 추가해준다.

그리고 TodoRepositoryImpl 도 생성해준다!
TodoRepositoryCustom에서는 실제 내용 없이 선언만 해줬기 때문에
QueryDSL 코드를 넣는 구현 클래스가 필요하기 때문이다!
Todo를 조회하고
Todo.user도 같이 조인하고 (작성자 정보도 연결해서 보겠다)
fetchJoin()으로 user까지 한 번에 가져오고 (Todo랑 User를 처음부터 같이 바로 가져오기 -> N+1 문제 해결)
id가 맞는 todo만 찾는다 (요청한 번호의 Todo만 찾는다)
=> 할 일 1개를 찾는데, 작성자 정보도 나중에 따로 찾지 말고 처음부터 같이 한 번에 가져와라

그리고 config에 QuerydslConfig도 만들어주면 끝난다!
내용은 QueryDSL 쿼리 작성할 때 쓸 JPAQueryFactory 를 스프링 빈으로 등록한다는 뜻이다!
9. Spring Security
문제 : 기존 Filter와 Argument Resolver를 사용하던 코드들을 Spring Security로 변경. 접근 권한 및 유저 권한 기능은 그대로 유지. 권한은 Spring Security의 기능을 사용. JWT는 그대로 사용.
자 대공사를 위해 먼저! 필요없어질 클래스부터 정리해보자!
삭제할 클래스 리스트!
- JwtFilter 삭제
- FilterConfig 삭제
- AuthUserArgumentResolver 삭제
- @Auth 삭제
추가할 클래스 리스트!
- JwtAuthenticationFilter 추가
- SecurityConfig 추가
수정 및 변경할 리스트!
- AuthUser를 UserDetails로 수정
- 컨트롤러를 @AuthenticationPrincipal로 변경
자 먼저 추가한 내용들을 살펴보자!
첫번째로 기존 JwtFilter를 삭제한 대신,
Spring Security 방식으로 동작하는 JwtAuthenticationFilter를 새로 추가했다.
@Slf4j
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String url = request.getRequestURI();
// 로그인/회원가입 요청은 토큰 검사 없이 통과
if (url.startsWith("/auth")) {
filterChain.doFilter(request, response);
return;
}
String bearerJwt = request.getHeader("Authorization");
if (bearerJwt == null) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "JWT 토큰이 필요합니다.");
return;
}
try {
String jwt = jwtUtil.substringToken(bearerJwt);
Claims claims = jwtUtil.extractClaims(jwt);
if (claims == null) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "잘못된 JWT 토큰입니다.");
return;
}
Long userId = Long.parseLong(claims.getSubject());
String email = claims.get("email", String.class);
String nickName = claims.get("nickName", String.class);
UserRole userRole = UserRole.of(claims.get("userRole", String.class));
AuthUser authUser = new AuthUser(userId, email, nickName, userRole);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
authUser,
null,
authUser.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
} catch (SecurityException | MalformedJwtException e) {
log.error("Invalid JWT signature", e);
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "유효하지 않는 JWT 서명입니다.");
} catch (ExpiredJwtException e) {
log.error("Expired JWT token", e);
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "만료된 JWT 토큰입니다.");
} catch (UnsupportedJwtException e) {
log.error("Unsupported JWT token", e);
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "지원되지 않는 JWT 토큰입니다.");
} catch (Exception e) {
log.error("Internal server error", e);
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
}
예전에는 request에 사용자 정보를 저장했다면
이제는 Spring Security가 사용하는 인증 저장소(SecurityContext) 에 저장하도록 바꿔줬다!
JwtAuthenticationFilter의 역할!
- Authorization 헤더에서 JWT 추출
- JWT 검증
- 토큰에서 사용자 정보 추출
- AuthUser 객체 생성
- UsernamePasswordAuthenticationToken 생성
- 인증 정보를 SecurityContextHolder에 저장
두번째로 Spring Security를 사용하려면 보안 설정을 담당하는 클래스가 필요하다.
그래서 SecurityConfig를 추가했다!
@Configuration
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtUtil jwtUtil;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(
new JwtAuthenticationFilter(jwtUtil),
UsernamePasswordAuthenticationFilter.class
)
.build();
}
}
SecurityConfig의 역할!
- CSRF 비활성화
- formLogin 비활성화
- httpBasic 비활성화
- 세션 사용 안 함 (STATELESS)
- /auth/**는 인증 없이 허용
- /admin/**는 ADMIN 권한만 허용
- 나머지 요청은 인증 필요
- JwtAuthenticationFilter를 Spring Security 필터 체인에 등록
이번에는 수정하고 변경한 사항들을 살펴보자!
첫번째로 기존 AuthUser가 UserDetails를 구현하도록 수정했다.
Spring Security에서는 principal 객체가 권한 정보도 함께 제공할 수 있어야 한다.
하지만 기존 AuthUser는 단순히 로그인 사용자 정보를 담는 DTO였기 때문에 수정해줬다!
두번째!
@Auth를 @AuthenticationPrincipal로 변경했다.
Spring Security 방식으로 바꾼 후에는 인증 정보가 SecurityContext에 저장되므로,
컨트롤러에서는 Spring Security가 제공하는 표준 어노테이션인
@AuthenticationPrincipal을 사용해야 하기 때문에 변경했다!
@Auth AuthUser authUser
라고 적힌 코드들을
@AuthenticationPrincipal AuthUser authUser
이렇게 변경해주었다!
배운 점
이번 과제를 진행하며 Spring/JPA/Spring Security는 단순히 기능만 구현하는 것이 아니라,
트랜잭션, 연관관계, 인증/인가, 조회 성능 같은
동작 원리를 이해하고 적용하는 것이 중요하다는 점을 배웠다.
또한 N+1, Cascade, QueryDSL, 테스트 코드 수정 등을 통해
작동하는 코드를 넘어서 성능과 유지보수성까지 고려하는 백엔드 개발의 중요성을 느꼈다.
전체적으로 이번 과제는 기능 구현부터 검증, 보안, 문서화까지
백엔드 개발의 전체 흐름을 경험하고 이해하기 위한 과정이라고 생각이 든다.
'💻 Backend > Bootcamp 과제' 카테고리의 다른 글
| 클라우드_아키텍처 설계 & 배포 트러블슈팅 (0) | 2026.03.11 |
|---|---|
| CH 3 심화 Spring_코드 개선 과제 [필수] (0) | 2026.03.04 |