플러스 주차 개인 과제를 진행하면서 겪은 트러블슈팅과 회고를 작성해보겠습니다.
https://github.com/mannaKim/spring-plus
GitHub - mannaKim/spring-plus: JPA 심화, 테스트코드, 성능최적화 과제
JPA 심화, 테스트코드, 성능최적화 과제. Contribute to mannaKim/spring-plus development by creating an account on GitHub.
github.com
문제별로 작업 내용은 위 github 레포지토리의 커밋 메세지별로 구분되어 있습니다.
[Level 1] 2. 코드 추가 퀴즈 - JWT의 이해
JWT의 claims에 `nickname`을 추가하는 과정에서 H2 DB 콘솔을 확인하려고 할 때, 오류가 발생했습니다.
해결 과정은 아래 링크에 따로 기재했습니다.
H2 콘솔 접속 시 Whitelabel Error Page (400 Bad Request) 해결 과정 + spring boot에서 active profile 선택하기 (Inte
Whitelabel Error Page (400 Bad Request)Spring Boot에서 DB 연결할 때 로컬 MySql만 사용해봤었는데 최근에 H2 DB 사용법을 배워서 적용해보고있었다. DB 연결은 잘 됐는데 H2 콘솔에 접속하자 Whitelabel Error Pag
mannakingdom.tistory.com
[Level 2] 6. JPA Cascade
`cascade = CascadeType.PERSIST`가 어떻게 동작하는지 디버깅하며 확인해봤습니다.
Todo를 저장하는 작업에서 `casecade = CascadeType.PERSIST`가 적용되기 전에는 `manager` 엔티티의 id가 null인 것을 확인할 수 있었습니다.
- savedTodo.managers 내부의 Manager 객체에 id 값이 없음 → 데이터베이스에 저장되지 않음
- Todo는 저장되었지만, Manager 엔티티가 함께 저장되지 않음
cascade를 적용한 후에는 manager에 제대로 id가 들어가고, DB에도 매니저 정보가 저장된 것을 확인할 수 있었습니다.
- savedTodo.managers 내부의 Manager 객체에 id가 정상적으로 부여됨
- DB에도 MANAGERS 테이블에 TODO_ID와 USER_ID 값이 삽입됨
➡️ CascadeType.PERSIST를 적용함으로써 연관된 엔티티가 함께 저장되도록 설정할 수 있었습니다.
➡️ Todo 생성 시 연관된 Manager도 자동으로 저장되며, 추가적인 save 없이 연관 엔티티를 관리할 수 있습니다.
[Level 2] 7. N+1
하나의 todo에 3명의 유저가 각각 댓글을 작성한 경우의 데이터를 조회한 결과 확인했습니다.
`localhost:8080/todos/1/comments`로 todo id가 1일 때의 댓글들을 조회하도록 api를 호출했습니다.
발생하는 로그는 다음과 같았습니다.
todo_id = 1인 댓글을 조회할 때, 각 user_id별로 개별 쿼리가 실행됩니다.
1개의 메인 쿼리(todo_id = 1 댓글 조회) + N개의 유저 조회 쿼리가 발생하여 N+1 문제가 있는 것을 확인했습니다.
N+1 문제를 해결하기 위해 FETCH JOIN을 적용하여 연관된 user 정보를 한 번의 쿼리로 조회되도록 수정했습니다.
변경된 쿼리 로그는 다음과 같습니다.
총 4번의 쿼리를 실행하던 이전과 달리 쿼리 실행 한번으로 모든 댓글과 연관된 유저 정보까지 조회할 수 있게 되었습니다.
[Level 2] 8. QueryDSL
(1) Gradle Wrapper 재설정
https://cactuslog.tistory.com/14
스프링부트 3 querydsl 설정
starter 설정 gradle 설정 build.gradle > dependencies에 다음을 추가한다. // querydsl setting implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['query
cactuslog.tistory.com
위 글을 참고하여 QueryDSL 실행 환경을 설정하던 중, Build 설정에서 다음과 같은 알림을 발견했습니다.
git clone할 때 gradle-wrapper.properties을 직접 생성해줬는데 Gradle Wrapper 파일이 인식이 안되었던걸로 보입니다.
gradle을 설치해서 gradle wrapper 명령어로 greadke wraooer를 다시 생성해주는 과정을 진행했습니다.
(2) compileJava 오류
dependencies {
// queryDSL
implementation 'com.querydsl:querydsl-jpa'
annotationProcessor 'com.querydsl:querydsl-apt'
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
}
gradle.build에 위와 같이 의존성을 추가하고 complieJava를 실행했습니다.
빌드는 성공되나, Q클래스가 생성되지 않는 것을 확인했습니다.
의존성을 추가할 때, 버전을 명시하고 다시 실행하니 빌드가 실패하는 것을 확인했습니다.
QueryDSL JPA의 의존성을 버전과 jakarta를 명시하여 오류를 수정했습니다.
참고: https://l4279625.tistory.com/146
dependencies {
// queryDSL
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
}
빌드 실패 및 Q클래스 생성 안된 이유:
QueryDSL 5.0 이상에서는 기본적으로 javax.persistence를 사용합니다.
하지만 프로젝트가 jakarta.persistence(JPA 3.x)를 사용하면 QueryDSL도 jakarta 버전(com.querydsl:querydsl-jpa:5.0.0:jakarta)을 명시적으로 사용해야 함을 확인했습니다.
QueryDSL이 javax.persistence가 아니라 jakarta.persistence를 참조하도록 지정하면서, JPA 버전 불일치 문제가 해결되어 빌드가 정상적으로 되었습니다.
(3) QueryDsl 적용
상속 없이 JpaQueryFactory 직접 사용하는 방식을 보고, 적용해보려고 했다가,, 서비스단에서 각각 의존성을 추가해서 쓰면 되는지, 그렇게 하는게 적절한지 고민이 너무 되고 어려워서 일단 스프링 공식 문서에 적힌 CustomRepository 사용 방식대로 구현했습니다.
https://docs.spring.io/spring-data/jpa/reference/repositories/custom-implementations.html
Custom Repository Implementations :: Spring Data JPA
The approach described in the preceding section requires customization of each repository interfaces when you want to customize the base repository behavior so that all repositories are affected. To instead change behavior for all repositories, you can cre
docs.spring.io
BooleanBuilder와 BooleanExpression 사용에도 고민이 있었습니다.
현재는 비교적 조건이 단순해서 BooleanBuilder로 사용을 선택했습니다.
[Level 2] 9. Spring Security
spring-advanced 과제에서 Spring Security JWT를 수정한 것과 동일한 방식으로 수정했습니다.
2025.03.21 - [Java Study/Frameworks] - Spring Security를 활용한 JWT 인증/인가 | Stateless하게 제대로 적용하기!
Spring Security를 활용한 JWT 인증/인가 | Stateless하게 제대로 적용하기!
2025.02.27 - [Java Study/Frameworks] - Spring Security를 활용한 JWT 인증 적용해보기 🖥️🔑 Spring Security를 활용한 JWT 인증 적용해보기 🖥️🔑필터(JwtFilter)를 직접 등록하고, HttpServletRequest에서 토큰을 추
mannakingdom.tistory.com
[Level 3] 10. QueryDSL 을 사용하여 검색 기능 만들기
엇,,,, 문제 조건의 Projections 활용을 지금 봐서 적용을 못했습니다,,,,,,,,,,,,,,, 🤯
(1) countDistinct()
각 Todo 항목에 대해 고유한 매니저와 댓글 수를 함께 조회하기 위해서 countDistinct()를 사용했습니다.
List<TodoSearchResponse> result = queryFactory
.select(new QTodoSearchResponse(
todo.title,
manager.countDistinct(),
comment.countDistinct()
))
.from(todo)
.leftJoin(manager).on(manager.todo.eq(todo))
.leftJoin(comment).on(comment.todo.eq(todo))
.where(builder)
.groupBy(todo.id)
.orderBy(todo.createdAt.desc())
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();
하나의 Todo 항목이 여러 Manager 객체와 연관될 수 있고, Todo 하나가 여러 개의 Comment 객체를 가질 수 있기 때문에 countDistinct()를 사용하여 각 Todo 항목에 연결된 관리자 수와 댓글 수를 계산했습니다.
+ 매니저 등록하는 과정에서 동일한 아이디로 매니저 지정이 가능해서 이 부분 추가로 수정했습니다.
(2) PageableExecutionUtils.getPage()
PageImpl로 반환할 때는 항상 항상 전체 데이터 개수를 조회하는 쿼리가 실행됩니다.
PageableExecutionUtils.getPage()에서는 result 리스트의 크기가 pageable.getPageSize()보다 작으면 (마지막 페이지라면), 전체 데이터 개수를 조회하는 쿼리를 실행하지 않고 그대로 반환합니다.
따라서 PageableExecutionUtils.getPage()를 사용해서 반환하도록 했습니다.
return PageableExecutionUtils.getPage(result, pageable, () ->
Optional.ofNullable(queryFactory
.select(todo.count())
.from(todo)
.leftJoin(manager).on(manager.todo.eq(todo))
.leftJoin(comment).on(comment.todo.eq(todo))
.where(builder)
.fetchOne())
.orElse(0L)
);
[Level 3] 11. Transaction 심화
로그를 저장할 테이블 Log 엔티티를 만들고, 로그 저장 서비스를 구현했습니다.
@Service
@RequiredArgsConstructor
public class LogService {
private final LogRepository logRepository;
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveLog(Long userId, String requestUrl, String method, LogStatus responseStatus) {
Log log = new Log(userId, requestUrl, method, responseStatus);
logRepository.save(log);
}
}
메인 트랜잭션(매니저 등록)이 롤백되더라도 로그 저장 로직이 진행되게 하기 위해서 `@Transactional(propagation = Propagation.REQUIRES_NEW)`을 사용했습니다.
AOP를 사용해서 로그가 기록되도록 했습니다.
`@DBLoggingApi`라는 커스텀 어노테이션을 만들어서, 이 어노테이션을 붙인 메서드의 실행 전후로 로그를 자동으로 기록하게 했습니다.
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class DBLoggingAspect {
private final HttpServletRequest request;
private final LogService logService;
@Around("@annotation(org.example.expert.aop.annotation.DBLoggingApi)")
public Object logDBRequest(ProceedingJoinPoint joinPoint) throws Throwable {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
AuthUser authUser = (AuthUser) authentication.getPrincipal();
String requestUrl = request.getRequestURI();
Long userId = authUser.getId();
String method = joinPoint.getSignature().getName();
try {
Object result = joinPoint.proceed();
logService.saveLog(userId, requestUrl, method, LogStatus.SUCCESS);
return result;
} catch (Exception e) {
logService.saveLog(userId, requestUrl, method, LogStatus.ERROR);
throw e;
}
}
}
현재 요청의 URL, 요청 유저ID, 메서드명을 가져오고, 메서드 실행 후 성공 여부에 따라 로그를 저장하게 하도록 했습니다.
메서드 실행 결과(ERROR/SUCCESS)를 저장하기 위해서 `@Around`로 구현했습니다.
실행 결과는 다음과 같습니다.
[Level 3] 12. AWS 활용
아래 링크에 자세한 내용 기재했습니다.
2025.03.21 - [Java Study/Frameworks] - Windows 개발 환경에서 AWS 활용 과제 | Spring Boot v3.3.3 & Gradle 8.12
Windows 개발 환경에서 AWS 배포 실습 | Spring Boot v3.3.3 & Gradle 8.12
약간 얼렁뚱땅 진행한거같지만,, 다음에 덜 헤매기위해 기록을 해보겠습니다.1. EC2(1) 인스턴스에 SSH(Secure Shell) 접속인스턴스를 생성하고 생성된 인스턴스에 SSH(Secure Shell) 접속하는 방법.(WSL에
mannakingdom.tistory.com
새롭게 학습한게 많아서 어려웠지만 의미있는 과제였던것 같습니다!
'TIL (Today I Learned)' 카테고리의 다른 글
동시성 제어는 어떻게 할까❓ (0) | 2025.03.26 |
---|---|
Windows 개발 환경에서 AWS 활용 과제 | Spring Boot v3.3.3 & Gradle 8.12 (0) | 2025.03.21 |
Spring Security를 활용한 JWT 인증 | Stateless하게 제대로 적용하기! (0) | 2025.03.21 |
H2 콘솔 접속 시 Whitelabel Error Page (400 Bad Request) 해결 과정 + spring boot에서 active profile 선택하기 (IntelliJ) (0) | 2025.03.12 |
H2 데이터베이스 설치 방법 | Server / In-memory / Embedded Mode❔ (0) | 2025.03.10 |