개발 일지/Spring 44

[JPA] N+1 문제에 대해서

위 로직은 이력서 도메인과 1:N 관계의 entity들이다. 1개의 이력서에는 기술 스택, 교육 사항, 프로젝트, 경력 등을 여러 개 입력할 수 있다. 여기서 하나의 이력서(CV)를 조회할 때, N+1 문제가 발생하는 것을 확인했다. 로그를 보면, 하나의 이력서(CV)를 조회하는 쿼리가 날아가고 추가적으로 연관된 객체들이 조회되는 것을 확인할 수 있다. 만약 여러 개의 이력서를 조회한다고 가정하고 이력서의 개수가 N개라면 N * (1 + 8)개의 쿼리가 발생한다. 극단적으로 예를 들면 10000개의 이력서가 있고 findAll()로 이력서들을 조회하면 90000개의 쿼리가 발생할 것이다. 따라서 1번의 쿼리에 N번의 쿼리가 추가 실행되므로, 쿼리 실행 횟수가 증가하여 성능에 부담을 주고 데이터베이스에 부..

Service 로직에서 List를 다룰 때 발생하는 index 문제 해결

프로젝트에서 맡은 이력서 CRUD 작업의 기본 로직을 다 만들어 두고 추가로 수정할 부분이 없나 하고 찾아보다가 문제를 하나 발견했다. Postman으로 여러 데이터를 입력해보고 테스트를 진행했을 때는 발생했던 오류가 아니어서 작업하면서는 찾지 못했던 것 같다. 기존에 작성했던 코드는 다음과 같다. 이력서를 수정하는 코드 안에 포함된 로직이다. 이 로직을 추가한 이유는 사용자가 이력서를 수정할 때, 기존의 이력서에는 경력사항이나 프로젝트 등이 들어가 있었지만 해당 데이터들을 지우거나 수정한 경우에 수정된 이력서에 들어가 있는 경력 사항 등만 두고 기존의 데이터는 지우려고 했기 때문이다. 테스트를 할 때는 저 로직에서 발생하는 예외상황을 생각하지 못했었는데, 위 remove에서 밑줄 그인 부분이 신경쓰여서..

Controller 내부 API 요청 메소드에 트랜잭션 적용

위 코드는 기존에 작성되었던 코드이다. 컨트롤러 내부에서 dto를 엔티티 객체로 변환하고 변환된 객체를 service 로직에서 사용하고 있다. 이렇게 쿼리가 실행된다면 발생하는 문제점이 데이터의 안전성이 보장되지 않는다는 것이다. 예를 들어, service 클래스의 createCv가 실행되어 새로운 Cv가 생성되고 repository에 저장되었는데 아래 injectLowDomain에서 실패하게 되면 작업은 종료되어 적절한 응답을 주지 못했지만 저장소에는 새로운 Cv가 있을 것이다. 즉, 데이터의 일관성을 보장하기 위한 작업이 필요했고 helper라는 클래스를 만들어서 트랜잭션을 적용시켜줬다. 트랜잭션은 한 번에 실행되어야 하는 일련의 연산들을 모아놓은 것이다. 예를 들어, 여러 개의 작업이 실행되는 경우..

data.sql 초기화 설정 후 Error creating bean with name 'dataSourceScriptDatabaseInitializer' 에러

진행 중인 프로젝트에서 데이터 값을 초기 설정하고 싶어서 data.sql을 만들고 설정을 해줬더니 이런 에러가 발생했다. data.sql Error creating bean with name 'dataSourceScriptDatabaseInitializer' defined in class path resource Table "SKILL_STACK" not found; SQL statement: 해결 과정 시도 1. 테이블이 없다?.. yml 파일에 ddl-auto를 create로 설정해줬는데 그럼 테이블이 생겨야 하는 거 아닌가.. 라는 생각에 mysql을 확인해보니 테이블은 있었다. 직접 쿼리를 작성해서 테이블을 만들어주고 ddl-auto를 update로 변경 후 재실행했다. -> 실패 시도 2. 찾아..

[Gradle] build와 bootJar의 차이

Spring rest docs를 포함하여 테스트를 진행하고 html 파일을 만들기 위한 작업을 진행하던 중 build랑 bootJar 둘 다 html 파일을 만들어주는데 왜 둘로 나뉘어 있는걸까? 둘의 차이점은 무엇일까? 하는 궁금증이 생겨서 찾아보았다. 먼저 Gradle build와 Gradle bootJar는 모두 Gradle 빌드 도구를 사용하여 소프트웨어를 빌드하는 프로세스를 의미한다. 둘의 큰 차이를 보면 build는 모든 테스트 실행, 프로덕션 아티팩트 생성 등 모든 것을 구축하기 위한 것이고 bootJar는 Spring Boot 애플리케이션을 실행 가능한 JAR 파일로 패키징하는 특정한 빌드 프로세스만 수행하는 것이다. 즉, Gradle build는 프로젝트의 빌드와 관련된 모든 작업을 수행..

[Spring Security] WebMvcTest 에서 401/403 에러 해결하기

slice 테스트로 controller의 post 테스트 로직을 작성하고 실행했더니 401 Unauthorized 에러가 발생했다. test 로직에서도 post 요청을 할 때 권한을 같이 넘겨줘야 한다는 것 같은데, WebMvcTest가 컨트롤러 테스트할 때 스프링 시큐리티가 자동으로 구성하는 Configuration 파일들을 불러와서 그렇다고 한다. 해당 문제는 다음과 같이 임의의 UserDetails를 만들어서 해결할 수 있다. @WithMockUser @Test public void postCvTest() throws Exception { } 또는 다음과 같이 excludeFilters를 활용해서 해당 Configurer를 회피하는 방법도 있다. @WebMvcTest(value = CvControl..

[Spring] Mapstruct 클래스타입 변환 / N:M 관계 매핑 에러

Mapstruct는 Builder를 통해 손수 객체 간 변환을 할 필요 없이 간단하게 변환을 적용해 주는 라이브러리이다. 평소 코드를 작성하면서 Dto 와 Entity 간에 변환이 필요할 때면 mapstruct를 이용해서 작업을 진행했고 이번에도 mapstrurct를 통해 매핑을 하는데 에러가 발생했다. Dto 필드가 많아서 문제가 발생한 코드를 하나 가져와보면, 아래 이너 클래스는 원시타입이 아니라 ProjectDto.Add 클래스 타입의 list를 필드로 갖고 있다. @Getter public static class Post { private List projects; } ProjectDto.Add 또한 내부에 클래스 타입의 List를 가지고 있다. @Data public static class Ad..

[JPA] @ElementCollection

JPA에서 @ElementCollection 애너테이션은 관계형 데이터베이스에서 값 타입을 컬렉션으로 매핑하기 위해 사용된다. 이를 통해서 개체에 속하는 값들을 한번에 모아서 저장할 수 있다. 어 그러면 그냥 Entity에 List를 만들어서 값을 저장하면 되는 거 아닌가?? 안된다. 관계형 데이터베이스는 일반적으로 컬렉션을 담을 수 있는 구조를 가지고 있지 않기 때문에 @ElementCollection 애너테이션을 통해서 값 타입 컬렉션을 매핑하는 것이다. 다음과 같은 경우에 해당 애너테이션을 사용할 수 있을 것이다. 예를 들어, 주문 엔티티는 여러 개의 주문 상품을 포함할 수 있지만 주문 상품 자체는 엔티티로서의 의미가 없는 값 타입일 것이다. 이런 경우에 엔티티를 또 사용해서 @OneToMany로 ..

[Spring Boot] web.xml

web.xml은 Java 웹 애플리케이션에서 사용하는 설정 파일 중 하나로, 웹 애플리케이션의 구성 요소와 동작 방식을 정의한다. 웹 애플리케이션이 배포되면 웹 애플리케이션 서버에서 web.xml 파일을 읽어서 애플리케이션을 구성하고 초기화한다. web.xml 파일은 다음과 같은 정보를 포함할 수 있다. Servlet 정의 : Servlet 이름, Servlet 클래스, Servlet 매핑 정보, 초기화 매개변수 등 Servlet과 관련된 설정 정보를 포함한다. filter 정의 : filter 이름, filter 클래스, URL 패턴, 초기화 매개변수 등 필터와 관련된 설정 정보를 포함한다. filter는 Servlet 요청과 응답을 중간에서 가로채서 처리할 수 있다. Listener 정의 : list..

[Sprgin JPA] FetchType

JPA에서는 연관된 엔티티를 조회할 때 어떤 방식으로 가져올지를 'FetchType' 옵션으로 지정할 수 있다. FetchType.EAGER : 해당 엔티티를 조회할 때 즉시 관련 엔티티도 함께 조회한다. 엔티티를 조회하는 쿼리를 실행할 때 연관된 엔티티도 함께 조회하기 때문에 성능 상 이점이 있다. 하지만 조회할 엔티티의 개수가 많아질 경우 부담을 줄 수 있으며, 엔티티 간의 연관 관계가 복잡해지면서 쿼리가 복잡해질 수 있다. 또한 원하지 않는 데이터를 로딩하는 경우가 발생할 수 있다. FetchType.LAZY : 연관된 엔티티를 실제로 사용할 때만 조회한다. 엔티티를 로딩하는 쿼리를 실행하지 않기 때문에 초기 로딩 시간을 단축할 수 있다. 하지만 엔티티를 사용할 때마다 연관된 엔티티를 조회해야 하므..