CODING/스파르타 내일배움캠프 TIL

Spring 플러스 주차 개인 과제 트러블슈팅 TIL 모음

codingTrip 2025. 3. 11. 22:58

2025.03.11(화)

Spring 플러스 주차 개인 과제 트러블슈팅

필수

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

Caused by: org.hibernate.query.SemanticException: Cannot compare left expression of type 'java.time.LocalDateTime' with right expression of type 'java.lang.Object'

Repository JPQL 쿼리 부분에서 발생했다.

처음에는 원인을 찾기 어려웠으나,

WHERE 조건의 범위를 주석 처리하면서 그 원인을 찾았다.

 

바로 DATE_ADD(DATE(:endDate),1)에서 발생했다.

DATE_ADD 메서드를 사용하고자 했던 이유는

t.modifiedAt < DATE(:endDate)

위의 부분에서 입력한 날짜에 1일을 더해야 조건이 부합되기 때문이었다.

ex) endDate=2025-03-11 -> 1일 더함 -> t.modifiedAt < 2025-03-12 -> 2025-03-11까지 모두 조회 가능함

 

해결 방법은 Service에서 1일을 더해주고, 이를 Repository 매개변수로 넘겨주었다.

아래와 같이 하니 내가 의도한대로 조건이 잘 작동되는 것을 알 수 있었다.

endDate = endDate!= null ? endDate.plusDays(1) : null;
Page<Todo> todos = todoRepository.findAllByOrderByModifiedAtDesc(pageable, weather, startDate, endDate);

endDate가 null일 경우에 대해서는 삼항 연산자로 처리했다.

null이 아닌 경우에 1일을 더하고 null인 경우에는 null 처리했다.


2025-03-11T18:17:08.501+09:00  WARN 9249 --- [expert] [nio-8080-exec-2] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type 'java.lang.String' to required type 'java.time.LocalDateTime'; Failed to convert from type [java.lang.String] to type [@org.springframework.web.bind.annotation.RequestParam java.time.LocalDateTime] for value [2025-03-11]]

Controller에서 파라미터에 대해 LocalDateTime으로 설정했었는데, 이것이 문제가 되었다.

@RequestParam(required = false) LocalDate startDate,
@RequestParam(required = false) LocalDate endDate,

위와 같이 LocalDate로 변경하니 해결되었다.


Caused by: org.hibernate.query.QueryArgumentException: Argument [2025-03-11] of type [java.time.LocalDate] did not match parameter type [java.time.LocalDateTime (n/a)]

Repository에서 :startDate, :endDate 파라미터에 DATE() 메서드를 사용하니 해결이 되었다.

 

완성된 코드는 아래와 같다.

@Query("SELECT t FROM Todo t LEFT JOIN FETCH t.user u " +
            "WHERE (coalesce(:weather, NULL) IS NULL OR t.weather = :weather) " +
            "AND CASE WHEN :startDate IS NOT NULL AND :endDate IS NOT NULL " +
            "THEN (t.modifiedAt >= DATE(:startDate) AND t.modifiedAt < DATE(:endDate)) " +
            "WHEN :startDate is null AND :endDate is not null " +
            "THEN (t.modifiedAt < DATE(:endDate)) " +
            "WHEN :startDate is not null AND :endDate is null " +
            "THEN (t.modifiedAt >= DATE(:startDate)) " +
            "WHEN :startDate is null AND :endDate is null " +
            "THEN (1=1) " +
            "END " +
            "ORDER BY t.modifiedAt DESC")
    Page<Todo> findAllByOrderByModifiedAtDesc(Pageable pageable, String weather, LocalDate startDate, LocalDate endDate);

CASE문을 사용하고 싶어서 사용해보았다.

시작일, 종료일을 각각 입력함에 따라 다양한 조건을 발생할 수 있도록 쿼리를 작성했다.

 


2025.03.14(금)

Spring 플러스 주차 개인 과제 트러블슈팅

도전

Lv 3-10. QueryDSL  사용하여 검색 기능 만들기

하나를 해결하면 하나가 문제가 되는 Error Chain...시간이었다.

error: incompatible types: StringPath cannot be converted to String
    @QueryProjection
    public TodoSearchResponse(String title, Long managerCount, Long commentCount) {
        this.title = title;
        this.managerCount = managerCount;
        this.commentCount = commentCount;
    }

=> dto에 @QueryProjections 넣고 CompileJava를 실행해서 Q클래스 만들어주었다.

 


content와 total 부분 개념 적립

    @Override
    public Page<TodoSearchResponse> search(TodoSearchCondition condition, Pageable pageable) {
        List<TodoSearchResponse> content = getTodoSearchResponse(condition, pageable);
        Long total = getTotal(condition);

        return PageableExecutionUtils.getPage(content, pageable, total::longValue);
    }

위의 코드에서 total은 조건을 만족하는 전체 조회 건수이고

이를 통해서 페이지네이션이 구현된다.

그런데 나는 이 부분을

  • 해당 일정의 담당자 수를 넣어주세요.
  • 해당 일정의 총 댓글 개수를 넣어주세요.

이 조건과 같이 섞어서 생각하면서 시행착오를 겪었다.


return PageableExecutionUtils.getPage(content, pageable, total::longValue);

이 부분은 트러블 슈팅은 아니고 단순히 궁금해서 찾아본 점을 적어본다.

PageImpl과 다른 점이 궁금했는데

위의 메서드가 더 성능 최적화에 좋다고 한다.

 

LongSupplier타입과 Long 타입 아니라는 부분

=> total::longValue를 사용하며 해결했다.


java.lang.IllegalArgumentException: manager.todo is not a root path

2025-03-14T17:39:47.639+09:00 ERROR 9831 --- [expert] [nio-8080-exec-5] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.InvalidDataAccessApiUsageException: manager.todo is not a root path] with root cause
.from(todo)
.leftJoin(todo, manager.todo).fetchJoin()
.leftJoin(todo, comment.todo).fetchJoin()

원인은 조인 시 왼쪽에는 조인 대상, 오른쪽에는 alias가 와야 하는데 그렇지 못해서 였다.

.from(todo)
.leftJoin(todo.managers, manager).fetchJoin()
.leftJoin(todo.comments, comment).fetchJoin()

위와 같이 수정하니 잘 되었다.


org.hibernate.query.SemanticException: Query specified join fetching, but the owner of the fetched association was not present in the select list [SqmListJoin(org.example.expert.domain.todo.entity.Todo(todo).managers(manager))]

2025-03-14T17:50:33.751+09:00 ERROR 10198 --- [expert] [nio-8080-exec-3] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.InvalidDataAccessApiUsageException: org.hibernate.query.SemanticException: Query specified join fetching, but the owner of the fetched association was not present in the select list [SqmListJoin(org.example.expert.domain.todo.entity.Todo(todo).managers(manager))]] with root cause

원인은 엔티티가 아닌 dto 상태에서 fetch join한 것이 문제였다.

.from(todo)
.leftJoin(todo.managers, manager)
.leftJoin(todo.comments, comment)

java.sql.SQLSyntaxErrorException: In aggregated query without GROUP BY, expression #1 of SELECT list contains nonaggregated column 'expert.t1_0.title'; this is incompatible with sql_mode=only_full_group_by

2025-03-14T18:00:29.369+09:00  WARN 10291 --- [expert] [nio-8080-exec-7] o.h.engine.jdbc.spi.SqlExceptionHelper   : SQL Error: 1140, SQLState: 42000
2025-03-14T18:00:29.372+09:00 ERROR 10291 --- [expert] [nio-8080-exec-7] o.h.engine.jdbc.spi.SqlExceptionHelper   : In aggregated query without GROUP BY, expression #1 of SELECT list contains nonaggregated column 'expert.t1_0.title'; this is incompatible with sql_mode=only_full_group_by
2025-03-14T18:00:29.378+09:00 ERROR 10291 --- [expert] [nio-8080-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.InvalidDataAccessResourceUsageException: JDBC exception executing SQL
[/* select todo.title, count(manager), count(comment)
from Todo todo
left join todo.managers as manager
left join todo.comments as comment
order by todo.createdAt desc
*/ select t1_0.title,count(m1_0.id),count(c1_0.id)
from todos t1_0 l
eft join managers m1_0 on t1_0.id=m1_0.todo_id l
eft join comments c1_0 on t1_0.id=c1_0.todo_id
order by t1_0.created_at desc limit ?,?]
[In aggregated query without GROUP BY, expression #1 of SELECT list contains nonaggregated column 'expert.t1_0.title'; this is incompatible with sql_mode=only_full_group_by] [n/a]; SQL [n/a]] with root cause

count 쿼리인데 group by를 쓰지 않은 점이 문제가 되었다.

    private List<TodoSearchResponse> getTodoSearchResponse(TodoSearchCondition condition, Pageable pageable) {
        List<TodoSearchResponse> content = queryFactory
                .select(new QTodoSearchResponse(
                        todo.title,
                        manager.count(),
                        comment.count()
                ))
                .from(todo)
                .leftJoin(todo.managers, manager)
                .leftJoin(todo.comments, comment)
                .where(
                        titleContains(condition.getTitle()),
                        createdAtGoe(condition.getCreatedAtGoe()),
                        createdAtLoe(condition.getCreatedAtLoe()),
                        managerNicknameContains(condition.getManagerNickname())
                )
                .orderBy(todo.createdAt.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();
        return content;
    }

    private Long getTotal(TodoSearchCondition condition) {
        Long total = queryFactory
                .select(todo.count())
                .from(todo)
                .leftJoin(todo.managers, manager)
                .leftJoin(todo.comments, comment)
                .where(
                        titleContains(condition.getTitle()),
                        createdAtGoe(condition.getCreatedAtGoe()),
                        createdAtLoe(condition.getCreatedAtLoe()),
                        managerNicknameContains(condition.getManagerNickname())
                )
                .fetchOne();

        return total;
    }

따라서 아래와 같이 서브 쿼리로 해당하는 조건의 개수를 불러왔다.

.select(new QTodoSearchResponse(
                        todo.title,
                        JPAExpressions.select(manager.count()).from(manager).where(manager.todo.eq(todo)),
                        JPAExpressions.select(comment.count()).from(comment).where(comment.todo.eq(todo))
                ))

where절에 미리 설정했던 검색 조건이 작동하지 않는 상태

Hibernate: 
    /* select
        todo.title,
        (select
            count(manager) 
        from
            Manager manager 
        where
            manager.todo = todo),
        (select
            count(comment) 
        from
            Comment comment 
        where
            comment.todo = todo) 
    from
        Todo todo   
    left join
        todo.managers as manager   
    left join
        todo.comments as comment 
    order by
        todo.createdAt desc */ select
            t1_0.title,
            (select
                count(m2_0.id) 
            from
                managers m2_0 
            where
                m2_0.todo_id=t1_0.id),
            (select
                count(c2_0.id) 
            from
                comments c2_0 
            where
                c2_0.todo_id=t1_0.id) 
        from
            todos t1_0 
        left join
            managers m1_0 
                on t1_0.id=m1_0.todo_id 
        left join
            comments c1_0 
                on t1_0.id=c1_0.todo_id 
        order by
            t1_0.created_at desc 
        limit
            ?, ?
Hibernate: 
    /* select
        count(todo) 
    from
        Todo todo   
    left join
        todo.managers as manager   
    left join
        todo.comments as comment */ select
            count(t1_0.id) 
        from
            todos t1_0 
        left join
            managers m1_0 
                on t1_0.id=m1_0.todo_id 
        left join
            comments c1_0 
                on t1_0.id=c1_0.todo_id

where 조건이 전혀 보이지 않아서 당황했었다.

@Getter
@AllArgsConstructor
public class TodoSearchCondition {

    private String title;
    private LocalDateTime createdAtGoe;
    private LocalDateTime createdAtLoe;
    private String managerNickname;
}

request dto에 생성자를 추가하니 아래와 같이 조회가 되었다.

Hibernate: 
    /* select
        todo.title,
        (select
            count(manager) 
        from
            Manager manager 
        where
            manager.todo = todo),
        (select
            count(comment) 
        from
            Comment comment 
        where
            comment.todo = todo) 
    from
        Todo todo   
    left join
        todo.managers as manager   
    left join
        todo.comments as comment 
    where
        todo.title like ?1 escape '!' 
    order by
        todo.createdAt desc */ select
            t1_0.title,
            (select
                count(m2_0.id) 
            from
                managers m2_0 
            where
                m2_0.todo_id=t1_0.id),
            (select
                count(c2_0.id) 
            from
                comments c2_0 
            where
                c2_0.todo_id=t1_0.id) 
        from
            todos t1_0 
        left join
            managers m1_0 
                on t1_0.id=m1_0.todo_id 
        left join
            comments c1_0 
                on t1_0.id=c1_0.todo_id 
        where
            t1_0.title like ? escape '!' 
        order by
            t1_0.created_at desc 
        limit
            ?, ?
Hibernate: 
    /* select
        count(todo) 
    from
        Todo todo   
    left join
        todo.managers as manager   
    left join
        todo.comments as comment 
    where
        todo.title like ?1 escape '!' */ select
            count(t1_0.id) 
        from
            todos t1_0 
        left join
            managers m1_0 
                on t1_0.id=m1_0.todo_id 
        left join
            comments c1_0 
                on t1_0.id=c1_0.todo_id 
        where
            t1_0.title like ? escape '!'

한글로 제목을 검색하면 발생하는 문제

튜터님과 1시간 30분 정도 씨름했으나 나왔던 결론은 위와 같다...

신기했던 점은 똑같이 포스트맨에서 실행했는데(title=아)

계속 다르게 조회되었다는 점이었다.

전체 조회가 되기도 하고, 빈 배열이 나오기도 하고, 잘 조회가 되기도 했다.

혹시나 해서 영어로 제목 검색을 해보니 이 때에는 잘 조회가 되었다.

한글로 조회했을 때는 사이에 이상한 문자가 들어가기도 했다.(포스트맨에서 육안으로는 보이지 않는)

 

결론은 위와 같지만 그래도 튜터님과 같이 씨름하면서 얻게 된 팁들에 대해 적어보고자 한다.

 

1. 검색 시, 파라미터가 잘 바인딩 되어있는지 확인하기

콘솔에서 보면 파라미터가 모두 ?으로 되어 있어

내가 원하는 대로 값이 잘 매칭되고 있는지를 확인하기 어려웠다.

logging.level.org.hibernate.orm.jdbc.bind=TRACE

application.properties 파일에서 위와 같이 설정하고 나니

아래처럼 파라미터를 확인할 수 있었다.

2025-03-14T23:04:08.840+09:00 TRACE 14660 --- [expert] [nio-8080-exec-6] org.hibernate.orhttp://m.jdbc.bind : binding parameter (1:VARCHAR) <- [%j%]

이를 보았을 때는 쿼리 상의 문제는 없어 보였다.

 

 ❌ DI

@Configuration
public class JPAConfiguration {

    @PersistenceContext
    private EntityManager entityManager;

    @Bean
    public JPAQueryFactory jpaQueryFactory() {
        return new JPAQueryFactory(entityManager);
    }
}

위처럼 config를 생성해서 bean을 등록하지 않아서 발생한 문제인가? 했으나 이도 아니었다.

 

@ModelAttribute

@ModelAttribute TodoSearchCondition condition

컨트롤러에서 ModelAttribute 설정을 하지 않았던 것이 문제가 되었나?고 했지만
(물론 다른 곳에서 문제가 될 수도 있지만)

일단 지금의 문제가 해결되지는 않았다.

 

 pageable

@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size

컨트롤러 Pageable pageable을 직접 받았으나

위와 같이 수정해보았다.

그러나 직접적인 원인 해결은 되지 못했다.

 

2. 포스트맨 외 확인 방법

curl -X GET "https://example.com/api" -H "Authorization: Bearer YOUR_TOKEN" -H "Content-Type: application/json"

포스트맨과 말고도 터미널에서 위와 같은 형식으로 조회할 수 있다고 말씀해주셨다.

그러나 생각만큼 잘 조회가 되지 않고 400 에러 발생으로 다른 방법을 찾기로 했다.

 

3. 디버그 모드 실행

 

문제의 원인으로 보이는 코드를 break point 걸고 디버그 모드로 실행해서 확인한다.

콘솔 로그로 확인해보니

title이 "아"로 내가 검색한 그대로 받아질 때가 있고

""처럼 나올 때도 있었다.

계속 같은 조건으로 조회했다고 생각했는데도 계속 결과가 달라지니

정말로 내 눈을 믿을 수가 없었다.

 

=> 튜터님께서 문제를 해결하시는 그 흐름을 같이 느낄 수 있어서 재미있는 시간이었다.

내가 영속성 컨텍스트에 대해서 확실하게 개념이 적립된 상태가 아니라서

쿼리를 잘못 짜서

문제인가 짐작하고 갔으나 생각보다 그렇지 않았던 점이 신기했다.


같은 값이 중복

결과가 중복으로 나오는 문제가 발생했다.

이를 해결하고자 일정의 아이디로 group by해서 문제를 해결하고자 했으나 아래와 같은 오류가 발생했다.

2025-03-14T21:42:27.759+09:00 ERROR 13718 --- [expert] [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: cohttp://m.querydsl.core.NonUniqueResultException: jakarta.persistence.NonUniqueResultException: Query did not return a unique result: 3 results were returned] with root cause

사실... 좀 더 생각해보면 나오는 문제였다.

검색 조건과 정확히 값이 일치하는 결과가 아닌

like문을 사용했기 때문에(포함)

여러 일정들이 나올 것이고

그 일정 아이디로 그룹화를 해도 각 아이디별로 전체 개수가 나올 것이다.

 

    @Override
    public Page<TodoSearchResponse> search(TodoSearchCondition condition, Pageable pageable) {
        List<TodoSearchResponse> content = getTodoSearchResponse(condition, pageable);
        Long total = getTotal(condition);

        return PageableExecutionUtils.getPage(content, pageable, total::longValue);
    }

    private List<TodoSearchResponse> getTodoSearchResponse(TodoSearchCondition condition, Pageable pageable) {
        List<TodoSearchResponse> content = queryFactory
                .select(new QTodoSearchResponse(
                        todo.title,
                        JPAExpressions.select(manager.count()).from(manager).where(manager.todo.eq(todo)),
                        JPAExpressions.select(comment.count()).from(comment).where(comment.todo.eq(todo))
                ))
                .from(todo)
                .where(
                        titleContains(condition.getTitle()),
                        createdAtGoe(condition.getCreatedAtGoe()),
                        createdAtLoe(condition.getCreatedAtLoe()),
                        managerNicknameContains(condition.getManagerNickname())
                )
                .orderBy(todo.createdAt.desc())
                .offset(pageable.getOffset())
                .limit(pageable.getPageSize())
                .fetch();
        return content;
    }

    private Long getTotal(TodoSearchCondition condition) {
        Long total = queryFactory
                .select(todo.count())
                .from(todo)
                .where(
                        titleContains(condition.getTitle()),
                        createdAtGoe(condition.getCreatedAtGoe()),
                        createdAtLoe(condition.getCreatedAtLoe()),
                        managerNicknameContains(condition.getManagerNickname())
                )
                .fetchOne();

        return total;
    }

    private BooleanExpression titleContains(String title) {
        return hasText(title) ? todo.title.contains(title) : null;
    }

    private BooleanExpression createdAtGoe(LocalDateTime createdAtGoe) {
        return createdAtGoe != null ? todo.createdAt.goe(createdAtGoe) : null;
    }

    private BooleanExpression createdAtLoe(LocalDateTime createdAtLoe) {
        return createdAtLoe != null ? todo.createdAt.loe(createdAtLoe) : null;
    }

    private BooleanExpression managerNicknameContains(String managerNickname) {
        return hasText(managerNickname) ? manager.user.nickname.contains(managerNickname) : null;
    }

위는 현재 상태에서 최종적으로 작성한 버전이다.

이미 서브쿼리로 관리자 수와 댓글 수를 count하고 있기 때문에

left join이 필요가 없다고 판단했다.

위와 같이 하고 나니 중복 값은 발생하지 않았다.

 

물론 지금 상황도 완벽하다고 생각하지는 않다.

이 상태에서 월요일에 튜터님께 피드백을 받고자 한다.

 

사실 과제를 하다보면, 개발을 하다보면

예기치 못한 일들을 마주하게 된다.

원인이 정말 이해하기 어렵거나 예상치 못한 곳에서 발견하기도 한다.

이 과정들이 쉽지는 않지만 힘이 들지만

그래도 나름? 재미로 느껴지기에

이 분야를 진로로 진지하게 임하고 있는 것이다.

 


2025.03.19(수)

Spring 플러스 주차 개인 과제 트러블슈팅

도전

Lv 3-12. AWS 활용

로컬에서 ./gradlew clean build을 했으나 아래와 같은 문제가 발생했다.

Error creating bean with name 'jwtAuthenticationFilter' defined in file [/Users/mac/Desktop/spring-plus/spring-plus/build/classes/java/main/org/example/expert/config/JwtAuthenticationFilter.class]: Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type 'org.example.expert.config.JwtUtil' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

 

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.example.expert.config.JwtUtil' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

mac@macs-MacBook-Pro spring-plus % ./gradlew clean build

> Task :test

ExpertApplicationTests > contextLoads() FAILED
    java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:180
        Caused by: org.springframework.beans.factory.BeanCreationException at AbstractAutowireCapableBeanFactory.java:1806
            Caused by: jakarta.persistence.PersistenceException at AbstractEntityManagerFactoryBean.java:421
                Caused by: org.hibernate.exception.GenericJDBCException at StandardSQLExceptionConverter.java:63
                    Caused by: java.sql.SQLException at SQLError.java:130

OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended

> Task :test FAILED

TodoControllerTest > todo_단건_조회에_성공한다() FAILED
    java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:180
        Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException at ConstructorResolver.java:795
            Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException at DefaultListableBeanFactory.java:1880

TodoControllerTest > todo_단건_조회_시_todo가_존재하지_않아_예외가_발생한다() FAILED
    java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:145

3 tests completed, 3 failed

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///Users/mac/Desktop/spring-plus/spring-plus/build/reports/tests/test/index.html

* Try:
> Run with --scan to get full insights.

BUILD FAILED in 7s
8 actionable tasks: 8 executed

 

테스트 코드에서 문제가 발생했다.

 

❌ 환경설정문제

처음에는 아래와 같은 설정 문제인 줄 알았으나 직접적인 원인은 아니었다.

  1. build.gradle
  2. application.properties

전에 문제 발생 시에는 jwt.secret.key 등

환경변수 설정만 잘해주면 다시 실행이 잘 되었어서

시도했으나 실패했다.

 

✅ @MockBean 설정하기

TodoControllerTest 파일에 JWTUtil을 설정해주었다.

 @MockBean
    private JwtUtil jwtUtil;

이전에는 따로 설정 없이도 잘 되었으나

Spring Security 도입 후, JWT 인증이 추가되었기 때문에 문제가 발생한 것으로 보인다.

 

💢 수정 후 추가 오류 발생

mac@macs-MacBook-Pro spring-plus % ./gradlew clean build
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2025-03-19T17:01:44.191+09:00  INFO 13409 --- [expert] [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2025-03-19T17:01:44.196+09:00  INFO 13409 --- [expert] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...

> Task :test

TodoControllerTest > todo_단건_조회에_성공한다() FAILED
    java.lang.AssertionError at TodoControllerTest.java:60

TodoControllerTest > todo_단건_조회_시_todo가_존재하지_않아_예외가_발생한다() FAILED
    java.lang.AssertionError at TodoControllerTest.java:76

2025-03-19T17:01:44.229+09:00  INFO 13409 --- [expert] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

> Task :test FAILED

3 tests completed, 2 failed

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':test'.
> There were failing tests. See the report at: file:///Users/mac/Desktop/spring-plus/spring-plus/build/reports/tests/test/index.html

* Try:
> Run with --scan to get full insights.

BUILD FAILED in 27s
9 actionable tasks: 9 executed

HTTP 상태 코드가 예상했던 200(OK)이 아닌 401(Unauthorized)로 반환되었기 때문에 테스트가 실패했다.

 

✅  SecurityMockMvcRequestPostProcessors.user 생성

import org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors;
...

mockMvc.perform(get("/todos/{todoId}", todoId)
        .with(SecurityMockMvcRequestPostProcessors.user("user")))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.id").value(todoId))
        .andExpect(jsonPath("$.title").value(title));

security Mock user 객체를 만들어주니 아래와 같이 해결이 되었다.

mac@macs-MacBook-Pro spring-plus % ./gradlew clean build
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2025-03-19T17:19:35.777+09:00  INFO 13713 --- [expert] [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2025-03-19T17:19:35.782+09:00  INFO 13713 --- [expert] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown initiated...
2025-03-19T17:19:35.807+09:00  INFO 13713 --- [expert] [ionShutdownHook] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Shutdown completed.

BUILD SUCCESSFUL in 28s
9 actionable tasks: 9 executed

 


파일 전송 문제

scp -i /Users/mac/Downloads/spring-plus-server-key-pair.pem build/libs/expert-0.0.1-SNAPSHOT.jar ec2-user@52.79.139.63:/home/ec2-user/

위의 형식으로 로컬에 있는 snapshot.jar 파일을 aws PC에 전송하고자 했다.

그러나 작동하지 않았다.

 

  절대경로, 상대경로

결론은 상대경로가 아닌 절대경로로 해야 한다는 것이었다.

 scp -i ~/Downloads/spring-plus-server-key-pair.pem ~/Desktop/spring-plus/spring-plus/build/libs/expert-0.0.1-SNAPSHOT.jar ubuntu@52.79.139.63:/home/ubuntu

또한 aws pc의 위치도 pwd를 통해 제대로 설정해주어야 한다.

 


2025.03.20(목)

Spring 플러스 주차 개인 과제 트러블슈팅

도전

Lv 3-12. AWS 활용

aws pc에서 java -jar expert-0.0.1-SNAPSHOT.jar 실행 시 문제 발생

2025-03-20T00:53:21.644Z ERROR 4655 --- [expert] [           main] o.s.b.web.embedded.tomcat.TomcatStarter  : Error starting Tomcat context. Exception: org.springframework.beans.factory.UnsatisfiedDependencyException. Message: Error creating bean with name 'jwtAuthenticationFilter' defined in URL [jar:nested:/home/ubuntu/expert-0.0.1-SNAPSHOT.jar/!BOOT-INF/classes/!/org/example/expert/config/JwtAuthenticationFilter.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'jwtUtil': Injection of autowired dependencies failed
2025-03-20T00:53:21.713Z  INFO 4655 --- [expert] [           main] o.apache.catalina.core.StandardService   : Stopping service [Tomcat]
2025-03-20T00:53:21.756Z  WARN 4655 --- [expert] [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Unable to start web server
2025-03-20T00:53:21.789Z  INFO 4655 --- [expert] [           main] .s.b.a.l.ConditionEvaluationReportLogger : 

Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2025-03-20T00:53:21.827Z ERROR 4655 --- [expert] [           main] o.s.boot.SpringApplication               : Application run failed

org.springframework.context.ApplicationContextException: Unable to start web server
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:165) ~[spring-boot-3.3.3.jar!/:3.3.3]
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:619) ~[spring-context-6.1.12.jar!/:6.1.12]
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.3.3.jar!/:3.3.3]
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) ~[spring-boot-3.3.3.jar!/:3.3.3]
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456) ~[spring-boot-3.3.3.jar!/:3.3.3]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:335) ~[spring-boot-3.3.3.jar!/:3.3.3]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1363) ~[spring-boot-3.3.3.jar!/:3.3.3]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1352) ~[spring-boot-3.3.3.jar!/:3.3.3]
        at org.example.expert.ExpertApplication.main(ExpertApplication.java:14) ~[!/:0.0.1-SNAPSHOT]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:na]
        at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[na:na]
        at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:na]
        at java.base/java.lang.reflect.Method.invoke(Method.java:569) ~[na:na]
        at org.springframework.boot.loader.launch.Launcher.launch(Launcher.java:102) ~[expert-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
        at org.springframework.boot.loader.launch.Launcher.launch(Launcher.java:64) ~[expert-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
        at org.springframework.boot.loader.launch.JarLauncher.main(JarLauncher.java:40) ~[expert-0.0.1-SNAPSHOT.jar:0.0.1-SNAPSHOT]
Caused by: org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat
        at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:147) ~[spring-boot-3.3.3.jar!/:3.3.3]
        at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.<init>(TomcatWebServer.java:107) ~[spring-boot-3.3.3.jar!/:3.3.3]
        at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getTomcatWebServer(TomcatServletWebServerFactory.java:516) ~[spring-boot-3.3.3.jar!/:3.3.3]
        at org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory.getWebServer(TomcatServletWebServerFactory.java:222) ~[spring-boot-3.3.3.jar!/:3.3.3]
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.createWebServer(ServletWebServerApplicationContext.java:188) ~[spring-boot-3.3.3.jar!/:3.3.3]
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:162) ~[spring-boot-3.3.3.jar!/:3.3.3]
        ... 15 common frames omitted
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'jwtAuthenticationFilter' defined in URL [jar:nested:/home/ubuntu/expert-0.0.1-SNAPSHOT.jar/!BOOT-INF/classes/!/org/example/expert/config/JwtAuthenticationFilter.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'jwtUtil': Injection of autowired dependencies failed
        at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:795) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:237) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1375) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1212) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:562) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:205) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.boot.web.servlet.ServletContextInitializerBeans.getOrderedBeansOfType(ServletContextInitializerBeans.java:211) ~[spring-boot-3.3.3.jar!/:3.3.3]
        at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addAsRegistrationBean(ServletContextInitializerBeans.java:174) ~[spring-boot-3.3.3.jar!/:3.3.3]
        at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addAsRegistrationBean(ServletContextInitializerBeans.java:169) ~[spring-boot-3.3.3.jar!/:3.3.3]
        at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addAdaptableBeans(ServletContextInitializerBeans.java:154) ~[spring-boot-3.3.3.jar!/:3.3.3]
        at org.springframework.boot.web.servlet.ServletContextInitializerBeans.<init>(ServletContextInitializerBeans.java:87) ~[spring-boot-3.3.3.jar!/:3.3.3]
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.getServletContextInitializerBeans(ServletWebServerApplicationContext.java:266) ~[spring-boot-3.3.3.jar!/:3.3.3]
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.selfInitialize(ServletWebServerApplicationContext.java:240) ~[spring-boot-3.3.3.jar!/:3.3.3]
        at org.springframework.boot.web.embedded.tomcat.TomcatStarter.onStartup(TomcatStarter.java:52) ~[spring-boot-3.3.3.jar!/:3.3.3]
        at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:4414) ~[tomcat-embed-core-10.1.28.jar!/:na]
        at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164) ~[tomcat-embed-core-10.1.28.jar!/:na]
        at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1203) ~[tomcat-embed-core-10.1.28.jar!/:na]
        at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1193) ~[tomcat-embed-core-10.1.28.jar!/:na]
        at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[na:na]
        at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75) ~[tomcat-embed-core-10.1.28.jar!/:na]
        at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145) ~[na:na]
        at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:749) ~[tomcat-embed-core-10.1.28.jar!/:na]
        at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:772) ~[tomcat-embed-core-10.1.28.jar!/:na]
        at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164) ~[tomcat-embed-core-10.1.28.jar!/:na]
        at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1203) ~[tomcat-embed-core-10.1.28.jar!/:na]
        at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1193) ~[tomcat-embed-core-10.1.28.jar!/:na]
        at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[na:na]
        at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75) ~[tomcat-embed-core-10.1.28.jar!/:na]
        at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:145) ~[na:na]
        at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:749) ~[tomcat-embed-core-10.1.28.jar!/:na]
        at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:203) ~[tomcat-embed-core-10.1.28.jar!/:na]
        at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164) ~[tomcat-embed-core-10.1.28.jar!/:na]
        at org.apache.catalina.core.StandardService.startInternal(StandardService.java:415) ~[tomcat-embed-core-10.1.28.jar!/:na]
        at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164) ~[tomcat-embed-core-10.1.28.jar!/:na]
        at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:874) ~[tomcat-embed-core-10.1.28.jar!/:na]
        at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164) ~[tomcat-embed-core-10.1.28.jar!/:na]
        at org.apache.catalina.startup.Tomcat.start(Tomcat.java:437) ~[tomcat-embed-core-10.1.28.jar!/:na]
        at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:128) ~[spring-boot-3.3.3.jar!/:3.3.3]
        ... 20 common frames omitted
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jwtUtil': Injection of autowired dependencies failed
        at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:515) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1439) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:599) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:337) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:335) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1443) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1353) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:904) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:782) ~[spring-beans-6.1.12.jar!/:6.1.12]
        ... 61 common frames omitted
Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'JWT_SECRET_KEY' in value "${JWT_SECRET_KEY}"
        at org.springframework.util.PropertyPlaceholderHelper.parseStringValue(PropertyPlaceholderHelper.java:180) ~[spring-core-6.1.12.jar!/:6.1.12]
        at org.springframework.util.PropertyPlaceholderHelper.replacePlaceholders(PropertyPlaceholderHelper.java:126) ~[spring-core-6.1.12.jar!/:6.1.12]
        at org.springframework.core.env.AbstractPropertyResolver.doResolvePlaceholders(AbstractPropertyResolver.java:239) ~[spring-core-6.1.12.jar!/:6.1.12]
        at org.springframework.core.env.AbstractPropertyResolver.resolveRequiredPlaceholders(AbstractPropertyResolver.java:210) ~[spring-core-6.1.12.jar!/:6.1.12]
        at org.springframework.core.env.AbstractPropertyResolver.resolveNestedPlaceholders(AbstractPropertyResolver.java:230) ~[spring-core-6.1.12.jar!/:6.1.12]
        at org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertyResolver.getProperty(ConfigurationPropertySourcesPropertyResolver.java:80) ~[spring-boot-3.3.3.jar!/:3.3.3]
        at org.springframework.boot.context.properties.source.ConfigurationPropertySourcesPropertyResolver.getProperty(ConfigurationPropertySourcesPropertyResolver.java:61) ~[spring-boot-3.3.3.jar!/:3.3.3]
        at org.springframework.core.env.AbstractEnvironment.getProperty(AbstractEnvironment.java:552) ~[spring-core-6.1.12.jar!/:6.1.12]
        at org.springframework.context.support.PropertySourcesPlaceholderConfigurer$1.getProperty(PropertySourcesPlaceholderConfigurer.java:153) ~[spring-context-6.1.12.jar!/:6.1.12]
        at org.springframework.context.support.PropertySourcesPlaceholderConfigurer$1.getProperty(PropertySourcesPlaceholderConfigurer.java:149) ~[spring-context-6.1.12.jar!/:6.1.12]
        at org.springframework.core.env.PropertySourcesPropertyResolver.getProperty(PropertySourcesPropertyResolver.java:85) ~[spring-core-6.1.12.jar!/:6.1.12]
        at org.springframework.core.env.PropertySourcesPropertyResolver.getPropertyAsRawString(PropertySourcesPropertyResolver.java:74) ~[spring-core-6.1.12.jar!/:6.1.12]
        at org.springframework.util.PropertyPlaceholderHelper.parseStringValue(PropertyPlaceholderHelper.java:153) ~[spring-core-6.1.12.jar!/:6.1.12]
        at org.springframework.util.PropertyPlaceholderHelper.replacePlaceholders(PropertyPlaceholderHelper.java:126) ~[spring-core-6.1.12.jar!/:6.1.12]
        at org.springframework.core.env.AbstractPropertyResolver.doResolvePlaceholders(AbstractPropertyResolver.java:239) ~[spring-core-6.1.12.jar!/:6.1.12]
        at org.springframework.core.env.AbstractPropertyResolver.resolveRequiredPlaceholders(AbstractPropertyResolver.java:210) ~[spring-core-6.1.12.jar!/:6.1.12]
        at org.springframework.context.support.PropertySourcesPlaceholderConfigurer.lambda$processProperties$0(PropertySourcesPlaceholderConfigurer.java:200) ~[spring-context-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.support.AbstractBeanFactory.resolveEmbeddedValue(AbstractBeanFactory.java:964) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1374) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1353) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:785) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:768) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:145) ~[spring-beans-6.1.12.jar!/:6.1.12]
        at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:509) ~[spring-beans-6.1.12.jar!/:6.1.12]
        ... 73 common frames omitted

 

jwt.secret.key를 application.properties에서 환경변수로 설정했었는데

이 정보가 jar 파일에는 설정되지 않아서 발생한 문제로 보인다.

다시 이 부분을 수정하고 build하니 잘 되었다.

 

rds 생성해서 연결해주니

아래와 같이 잘 연결된 것을 확인할 수 있다.


Lv 3-13. 대용량 데이터 처리

JPA Repository saveAll()이 아닌 JdbcTemplate를 사용해야 하는 이유

 

수정 전

100만건 데이터 입력에 JPA Repository saveAll()을 사용했었다.

@SpringBootTest
@Commit
@Transactional
class GenerateUsersTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private EntityManager entityManager;

    @Test
    void 유저_100만건_생성() {
        List<User> users = new ArrayList<>();
        for (int i = 0; i < 1_000_000; i++) {
            UserRole userRole = i % 2 == 0 ? UserRole.ROLE_USER : UserRole.ROLE_ADMIN;
            users.add(new User("email" + i, "password" + i, userRole, "nickname" + i));

            if (i % 10_000 == 0 && i > 0) {
                userRepository.saveAll(users);
                entityManager.flush(); // 즉시 DB 반영
                users.clear();
            }
        }

        // 마지막 남은 데이터 저장
        if (!users.isEmpty()) {
            userRepository.saveAll(users);
            entityManager.flush();
        }
    }
}

 

그러나 튜터님께서 saveAll() 메서드를 잘 살펴보라고 하셔서 들어가서 보았다.

for문을 통해서 일일이 리스트에 저장하고 있는 것을 확인할 수 있었다.

이런 상태에서는 100만 건의 데이터를 넣을 수 없다는 것을 알 수 있었다.

@Transactional
public <S extends T> List<S> saveAll(Iterable<S> entities) {
    Assert.notNull(entities, "Entities must not be null");
    List<S> result = new ArrayList();

    for(S entity : entities) {
        result.add(this.save(entity));
    }

    return result;
}

 

추가로 공식문서를 보면 아래와 같이JPA saveAll()은 Batch Insert 방식으로 작동하지 않는 이유에 대해서도 알 수 있었다.

"@GeneratedValue(strategy = GenerationType.IDENTITY)의 경우 Hibernate는 JDBC 수준에서 batch insert를 비활성화한다"

출처: https://twosky.tistory.com/62

 

수정 후

따라서 JPA Repository가 아닌 JdbcTemplate Repository를 가지고 100만 건을 생성했다.

@Repository
@RequiredArgsConstructor
public class UserJdbcRepository {

    private final JdbcTemplate jdbcTemplate;

    public void saveAllUsers(List<User> users) {
        String sql = "INSERT INTO users (email, password, user_role, nickname) VALUES (?, ?, ?, ?)";
        jdbcTemplate.batchUpdate(sql, new BatchPreparedStatementSetter() {
            @Override
            public void setValues(PreparedStatement ps, int i) throws SQLException {
                User user = users.get(i);
                ps.setString(1, user.getEmail());
                ps.setString(2, user.getPassword());
                ps.setString(3, user.getUserRole().name());
                ps.setString(4, user.getNickname());
            }

            @Override
            public int getBatchSize() {
                return users.size();
            }
        });
    }
}

 

@SpringBootTest
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class GenerateUsers {

    @Autowired
    private UserJdbcRepository userJdbcRepository;

    private static final int USER_INSERT_COUNT = 1_000_000;
    private static final int BATCH_SIZE = 1_000;

    @Test
    @Transactional
    @Rollback(value = false)
    void 유저_100만건_생성() {
        List<User> users = new ArrayList<>();
        for (int i = 0; i < USER_INSERT_COUNT; i++) {
            UserRole userRole = i % 2 == 0 ? UserRole.ROLE_USER : UserRole.ROLE_ADMIN;
            users.add(new User("email" + i + "@naver.com", "Password" + i, userRole, "nickname" + i));

            if (i % BATCH_SIZE == 0 && i > 0) {
                userJdbcRepository.saveAllUsers(users);
                users.clear();
                /* 배치 사이즈마다 0.5초씩 sleep 처리 */
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        }

        if (!users.isEmpty()) {
            userJdbcRepository.saveAllUsers(users);
        }
    }
}