[내일배움캠프] - 2026.04.06(월)ㅣ스프링 플러스 프로젝트

2026. 4. 6. 19:56·TIL & 트러블 슈팅

과제에 들어가기 앞서..

 

이번 과제는 효율적으로 문제를 해결하기 위해 마일스톤을 참고하여 진행했으며, 문제 번호는 마일스톤 기준으로 나열하였습니다.


1. 코드 개선 퀴즈 - @Transactional의 이해

할 일 저장 기능을 구현한 API(/todos)를 호출할 때, 아래와 같은 에러가 발생하고 있어요.

 

에러 로그 원문

에러가 발생하지 않고 정상적으로 할 일을 저장 할 수 있도록 코드를 수정해주세요.

 

문제 풀이

 

클래스 레벨에 @Transactional(readOnly = true) 가 선언되어 있어서 saveTodo() 메서드까지 읽기 전용으로 동작했었습니다 그로 인해 오류가 발생되고 있었고 해결 하기위해 기존 클래스에 선언되어있던 @Transactional을 saveTodo() 메서드에만 별도로 선언해주었습니다.


3. 코드 개선 퀴즈 - JPA의 이해

🚨 기획자의 긴급 요청이 왔어요! 아래의 요구사항에 맞춰 기획 요건에 대응할 수 있는 코드를 작성해주세요.
  • 할 일 검색 시 weather 조건으로도 검색할 수 있어야해요.(weather 조건은 있을 수도 있고, 없을 수도 있어요!)
  • 할 일 검색 시 수정일 기준으로 기간 검색이 가능해야해요.(기간의 시작과 끝 조건은 있을 수도 있고, 없을 수도 있어요!)
  • JPQL을 사용하고, 쿼리 메소드명은 자유롭게 지정하되 너무 길지 않게 해주세요
💡 필요할 시, 서비스 단에서 if문을 사용해 여러 개의 쿼리(JPQL)를 사용하셔도 좋습니다.

 

문제 풀이

 

getTodos 메서드에서 문제 요구사항에 맞게 weather 조건으로 검색, 수정일 기준으로 기간 검색이 가능하게 weather, startDate, endDate 파라미터를 추가 후 @RequestParam 어노테이션을 적용해주었습니다.

 

 

이후 Repository에서는 JPQL을 활용하여 동적 쿼리를 작성했습니다. 요구사항에서 각 조건이 있을 수도 있고 없을 수도 있다고 했기 때문에 :weather IS NULL OR t.weather = :weather 와 같은 방식으로 파라미터가 null이면 조건을 무시하고, 값이 있을 때만 필터링되도록 구현했습니다. startDate와 endDate도 동일한 방식으로 처리했습니다.

 

 

getTodos 메서드에서 Controller에서 받은 weather, startDate, endDate 파라미터를 그대로 Repository의 findByConditions에 전달했습니다.

 

페이징 처리는 PageRequest.of(page - 1, size) 로 구현하였습니다. 또한 JPA의 페이지 번호는 0부터 시작하기 때문에 사용자가 1페이지를 요청하면 내부적으로 0페이지로 변환되어야 합니다. 이를 위해 PageRequest.of(page - 1, size)와 같이 작성하였습니다.


6. JPA Cascade

🤔 앗❗ 실수로 코드를 지웠어요!
  • 할 일을 새로 저장할 시, 할 일을 생성한 유저는 담당자로 자동 등록되어야 합니다.
  • JPA의 cascade 기능을 활용해 할 일을 생성한 유저가 담당자로 등록될 수 있게 해주세요.

 

문제 풀이

 

기존 코드의 경우 Todo 생성자에서 managers.add(new Manager(user, this))로 담당자를 리스트에 추가했는데, 문제는 Todo를 저장할 때 Manager는 별도로 저장하지 않으면 DB에 반영되지 않는다는 점이었습니다. 따라서 @OneToMany 어노테이션에 cascade = CascadeType.PERSIST를 추가하여 Todo가 저장될 때 연관된 Manager도 함께 저장되게 하여 별도로 managerRepository.save()를 호출하지 않아도 자동으로 담당자가 등록되게 하였습니다.


11. Transaction 심화

👉 매니저 등록 요청 시 로그를 남기고 싶어요! @Transactional의 옵션 중 하나를 활용하여 매니저 등록과 로그 기록이 각각 독립적으로 처리될 수 있도록 해봅시다.
  • 매니저 등록 요청을 기록하는 로그 테이블을 만들어주세요.
    • DB 테이블명: log
  • 매니저 등록과는 별개로 로그 테이블에는 항상 요청 로그가 남아야 해요.
    • 매니저 등록은 실패할 수 있지만, 로그는 반드시 저장되어야 합니다.
    • 로그 생성 시간은 반드시 필요합니다.
    • 그 외 로그에 들어가는 내용은 원하는 정보를 자유롭게 넣어주세요.

 

문제 풀이

 

먼저 매니저 요청시 로그를 기록하기 위해 Log Entity, LogRepository, LogService를 만들어 초기 세팅을 해주었습니다.

 

 

처음으로 Log Entity를 작성해주었습니다. 기본적으로 로그 코드, 로그 내용, 로그 발생 시간으로 구성했습니다.

 

 

LogRepository는 JpaRepository를 상속받아 별도의 메서드 없이 기본 CRUD 기능만 사용했습니다.

 

 

LogService의 saveLog 메서드의 경우 @Transactional(propagation = Propagation.REQUIRES_NEW)를 적용했습니다. 이렇게 하면 매니저 등록 트랜잭션과 완전히 독립된 새로운 트랜잭션으로 실행되기 때문에 매니저 등록이 실패해서 롤백되더라도 로그는 반드시 저장됩니다.


7. N+1

CommentController 클래스의 getComments() API를 호출할 때 N+1 문제가 발생하고 있어요. N+1 문제란, 데이터베이스 쿼리 성능 저하를 일으키는 대표적인 문제 중 하나로, 특히 연관된 엔티티를 조회할 때 발생해요.

  • 해당 문제가 발생하지 않도록 코드를 수정해주세요.
  • N+1 로그

 

문제 풀이

 

기존 코드에서는 쿼리문에 FETCH가 없었습니다. 이 때문에 댓글 목록을 조회할 때 각 댓글마다 연관된 유저를 별도 쿼리로 조회하는 N+1문제가 발생 했던것입니다. 해당 문제를 해결하기 위해 JOIN FETCH를 추가하였습니다.

JOIN FETCH는 연관된 엔티티를 한 번의 쿼리로 함께 조회합니다. 즉, 댓글 100개를 조회하더라도 단 한번의 쿼리만 실행됩니다.


8. QueryDSL

  • JPQL로 작성된 findByIdWithUser 를 QueryDSL로 변경합니다.
  • 7번과 마찬가지로 N+1 문제가 발생하지 않도록 유의해 주세요!

 

문제 풀이

 

QueryDSL을 사용하기 위해 일단 의존성을 먼저 추가해주었습니다.

 

 

TodoQueryDslRepository 인터페이스에 searchTodos() 메소드를 선언했습니다. 검색 조건으로 제목, 닉네임, 생성일 범위를 받고 페이징 처리된 TodoSearchResponse 목록을 반환하도록 로직을 구성하였습니다.

 

 

searchTodos() 메소드에서 QTodoSearchResponse Projections를 사용하여 문제 요구사항에 맞게 제목, 담당자 수, 댓글 수만 조회할 수 있게 하였습니다. 또한 동적 검색 조건은 BooleanBuilder를 활용해 null인 조건은 자동으로 무시되도록 처리하였으며, 결과는 PageImpl로 감싸 페이징 처리하였습니다.

 

 

TodoRepository에 TodoQueryDslRepository를 상속 추가했습니다. 이렇게 하면 기존 JPA 메소드와 QueryDSL 메서드를 하나의 Repository에서 함께 사용할 수 있습니다.

 

 

findByIdWithUser() 메소드는 leftJoin(QTodo.todo.user).fetchJoin()을 사용해 Todo와 User를 한 번의 쿼리로 조회하여 요구사항에 맞게끔 N+1 문제를 방지했습니다.

 

searchTodos() 메소드에서는 QTodoSearchResponse Projections를 활용해 필요한 필드인 제목, 담당자 수, 댓글 수만 선택적으로 조회했습니다. 동적 검색 조건은 buildSearchCondition 메서드에서 BooleanBuilder로 처리했으며, 각 파라미터가 null이면 해당 조건을 무시하고 값이 있을 때만 조건에 추가됩니다. 마지막으로 PageImpl로 감싸 페이징 처리된 결과를 반환했습니다.


10. QueryDSL을 사용하여 검색 기능 만들기

👉 일정을 검색하는 기능을 만들고 싶어요! 검색 기능의 성능 및 사용성을 높이기 위해 QueryDSL을 활용한 쿼리 최적화를 해보세요.

❗Projections를 활용해서 필요한 필드만 반환할 수 있도록 해주세요.❗

 

  • 새로운 API로 만들어주세요.
  • 검색 조건은 다음과 같아요.
    • 검색 키워드로 일정의 제목을 검색할 수 있어요.
      • 제목은 부분적으로 일치해도 검색이 가능해요.
    • 일정의 생성일 범위로 검색할 수 있어요.
      • 일정을 생성일 최신순으로 정렬해주세요.
    • 담당자의 닉네임으로도 검색이 가능해요.
      • 닉네임은 부분적으로 일치해도 검색이 가능해요.
  • 다음의 내용을 포함해서 검색 결과를 반환해주세요.
    • 일정에 대한 모든 정보가 아닌, 제목만 넣어주세요.
    • 해당 일정의 담당자 수를 넣어주세요.
    • 해당 일정의 총 댓글 개수를 넣어주세요.
  • 검색 결과는 페이징 처리되어 반환되도록 합니다.

 

문제 풀이

 

처음으로는 검색 결과로 반환할 DTO생성 후 로직을 구성하였습니다. 요구사항에 따라 모든 정보가 아닌 제목, 담당자 수, 댓글 수만 필드로 구성했습니다. 생성자에 @QueryProjection을 붙여 QueryDSL에서 타입 안전하게 Projections을 사용할 수 있도록 했습니다.

 

 

이후 TodoQueryDslRepository 클래스를 만든 후 searchTodos 메서드를 선언 후 안에 제목, 닉네임, 생성일 범위를 검색 조건으로 받게 하고, 페이징 처리된 TodoSearchResponse를 반환하도록 설계하였습니다.

 

 

searchTodos 메서드에서는 QTodoSearchResponse Projections로 필요한 필드만 조회하고, managers와 comments를 leftJoin으로 연결해 담당자 수와 댓글 수를 count()로 집계하도록 하였습니다. 동적 검색 조건은 BooleanBuilder로 처리하여 null인 조건은 자동으로 무시되도록 했으며, 결과는 PageImpl로 감싸서 반환하도록 하였습니다.

 

 

마지막으로 기존에 있던 TodoRepository에 TodoQueryDslRepository를 상속 추가하여 기존 JPA 메서드와 QueryDSL 메서드를 하나의 Repository에서 함께 사용할 수 있도록 했습니다.


2. 코드 추가 퀴즈 - JWT의 이해

🚨 기획자의 긴급 요청이 왔어요! 아래의 요구사항에 맞춰 기획 요건에 대응할 수 있는 코드를 작성해주세요.

 

  • User의 정보에 nickname이 필요해졌어요.
    • User 테이블에 nickname 컬럼을 추가해주세요.
    • nickname은 중복 가능합니다.
  • 프론트엔드 개발자가 JWT에서 유저의 닉네임을 꺼내 화면에 보여주길 원하고 있어요.

 

문제 풀이

 

User 엔티티에 nickName 필드를 추가했습니다. 요구사항에서 닉네임은 중복 가능하다고 했기 때문에 @Column(unique = true)는 적용하지 않았으며, 생성자에도 nickName 파라미터를 추가하여 회원가입 시 닉네임이 저장될 수 있도록 했습니다.

 

 

JWT 생성 시 .claim("nickName", nickName)을 추가하여 토큰 안에 닉네임 정보를 포함시켰습니다. 이렇게 하면 프론트엔드에서 토큰을 디코딩해 별도의 API 호출 없이 닉네임을 바로 꺼내 사용할 수 있습니다.

 

 

JWT를 파싱할 때 claims.get("nickName", String.class)로 닉네임을 꺼내 httpRequest.setAttribute로 저장했습니다. 이렇게 저장된 값은 이후 ArgumentResolver에서 AuthUser를 생성할 때 사용됩니다.

 

 

AuthUser에는 nickName 필드와 생성자 파라미터를 추가했습니다. 또한 Spring Security 적용으로 인해 UserDetails를 구현하여 Security Context에서 인증 정보로 활용될 수 있도록 하였습니다.

 

 

JWT에서 파싱한 정보로 AuthUser를 생성하고 UsernamePasswordAuthenticationToken에 담아 SecurityContextHolder에 저장되게 하였습니다. 이렇게 하면 이후 요청 처리 과정에서 SecurityContext를 통해 인증된 사용자 정보를 가져올 수 있습니다.


4. 테스트 코드 퀴즈 - 컨트롤러 테스트의 이해

  • 테스트 패키지 org.example.expert.domain.todo.controller의 todo_단건_조회_시_todo가_존재하지_않아_예외가_발생한다() 테스트가 실패하고 있어요.

  • 테스트가 정상적으로 수행되어 통과할 수 있도록 테스트 코드를 수정해주세요.

 

문제 풀이

 

코드를 살펴보면 테스트 이름이 예외가 발생했을 때의 기준으로 만들어져있는데 200을 기대하고 있습니다. 그로 인해 오류가 발생하던것이었습니다. 따라서 기대값을 BAD_REQUEST로 줘서 오류를 해결하였습니다.

 


5. 코드 개선 퀴즈 - AOP의 이해

😱 AOP가 잘못 동작하고 있어요!
  • UserAdminController 클래스의 changeUserRole() 메소드가 실행 전 동작해야해요.
  • AdminAccessLoggingAspect 클래스에 있는 AOP가 개발 의도에 맞도록 코드를 수정해주세요.

 

문제 풀이

 

기존에는 @After라는 어노테이션이 붙어 있었고 경로 또한 잘못되어있었습니다. @After라는 어노테이션은 "~이후에" 라는 뜻 처럼 실행전이 아닌 실행후에 동작하며, 경로 자체도 changeUserRole()메소드가 아닌 getUser()메소드를 찾고 있기 때문에 위 사진 처럼 변경해주었습니다.


9. Spring Security

Spring Security를 도입하기로 결정했어요!
  • 기존 Filter와 Argument Resolver를 사용하던 코드들을 Spring Security로 변경해주세요.
    • 접근 권한 및 유저 권한 기능은 그대로 유지해주세요.
    • 권한은 Spring Security의 기능을 사용해주세요.
  • 토큰 기반 인증 방식은 유지할 거예요. JWT는 그대로 사용해주세요.

 

문제 풀이

 

Spring Security도 사용하기전에 의존성 설정을 해야 사용할 수 있기때문에 의존성을 추가해주었습니다.

 

 

과제는 JWT 기반 인증 방식을 유지하기 때문에 세션은 STATELESS로 설정하고 CSRF는 비활성화했습니다. /auth/**와 /health는 permitAll(), /admin/**은 hasRole("ADMIN")으로 권한을 설정하고 JwtAuthenticationFilter를 Security 필터 체인에 등록했습니다

 

 

OncePerRequestFilter를 상속받아 요청당 한 번만 실행되도록 하였습니다. JWT를 파싱해 AuthUser를 생성하고 SecurityContextHolder에 저장하며, 토큰이 유효하지 않으면 401을 반환히도록 설계하였습니다.

 

 

UserDetails를 구현하고 getAuthorities()에서 ROLE_ prefix를 붙여 Spring Security가 권한을 인식할 수 있도록 했습니다.

 

 

기존 request.getAttribute() 방식에서 SecurityContextHolder.getContext().getAuthentication().getPrincipal()로 변경하여 AuthUser를 컨트롤러에 주입받도록 했습니다.


 

'TIL & 트러블 슈팅' 카테고리의 다른 글

[내일배움캠프] - 2026.03.31(화)  (0) 2026.03.31
[내일배움캠프] - 2026.03.30(월)  (0) 2026.03.30
[내일배움캠프] - DAY21 클라우드 아키텍처 설계 & 배포 프로젝트 트러블 슈팅  (0) 2026.03.12
[내일배움캠프] - DAY20 IP와 네트워크 통신에 대해 알아보자  (0) 2026.03.10
[내일배움캠프] - DAY19 AOP(Aspect Oriented Programming)에 대해 알아보자  (0) 2026.03.06
'TIL & 트러블 슈팅' 카테고리의 다른 글
  • [내일배움캠프] - 2026.03.31(화)
  • [내일배움캠프] - 2026.03.30(월)
  • [내일배움캠프] - DAY21 클라우드 아키텍처 설계 & 배포 프로젝트 트러블 슈팅
  • [내일배움캠프] - DAY20 IP와 네트워크 통신에 대해 알아보자
N_HYUN
N_HYUN
안녕하세요! 현이의 개발 공부방입니다.
  • N_HYUN
    현이의 개발 공부방
    N_HYUN
  • 전체
    오늘
    어제
    • 분류 전체보기 (29)
      • FrontEnd (0)
      • BackEnd (0)
        • Java (0)
      • DataBase (1)
      • TIL & 트러블 슈팅 (24)
      • Etc (4)
        • Theoretical Computer Scienc.. (1)
        • Web & CS (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
N_HYUN
[내일배움캠프] - 2026.04.06(월)ㅣ스프링 플러스 프로젝트
상단으로

티스토리툴바