ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • Spring-Boot에 Spring-Data-JPA 적용하기 (Feat. Querydsl)
    백엔드 2024. 2. 23. 11:31
    728x90
    반응형
    SMALL

    현재 백엔드 관련 지식을 쌓기 위해 영상 강의를 시청중에 있습니다.

    해당 프로젝트의 실제 스펙은 Spring Boot를 사용하고 MyBatis를 활용해 DB에 접근할 수 있도록 해주는 간단한 게시판 프로젝트입니다. 이후 많은 트래픽에 대비한 여러 요소들까지 확인할 수 있다고 알고 있어 계속 시청하고 있습니다.

    하지만, 시청하고 있는 강의 그대로 코드를 작성하여도 MyBatis 설정에 문제가 있는 것인지, mapper에 문제가 있는 것인지 더 이상 진행을 할 수 없는 상황이었습니다..

    그래서 계속해서 학습을 하고 있는 JPA를 대신 적용해서 해보자는 생각이 들었고, MyBatis에서 JPA로 ORM을 변경하도록 했습니다.

    또한, Spring에서 JPA를 더욱 더 잘 활용할 수 있도록 해주는 Spring Data JPA를 적용하기로 했고 동적 쿼리에 훨씬 강점을 가지고 있는 Querydsl까지 포함시키게 되었습니다.

     

    리팩토링

    1. MyBatis 삭제, Spring Data JPA, Querydsl 적용

    우선 MyBatis를 적용하기 위해 필요했던 mybatis-config.xml과 DatabaseConfig, MySQLConfig, Mapper 클래스를 모두 제거해주었습니다.

    그 다음, Spring Data JPA를 적용해주기 위해 dependency에 패키지를 추가해주었습니다.

     

    • build.gradle
    dependencies {
        implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
        ...
    }

     

    • applicaion.yml
    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/{테이블명}
        username: {유저명}
        password: {비밀번호}
    
      jpa:
        database-platform: org.hibernate.dialect.MySQL8Dialect
        hibernate:
          ddl-auto: validate
        properties:
          hibernate:
            format_sql: true
    
    logging.level:
        org.hibernate.SQL: debug

     

    DB Table은 직접 DDL을 작성해 생성할 것이기 때문에 ddl-auto: validate로 선언해주었습니다.

    이 후 Querydsl 관련 패키지를 추가해주었습니다.

     

    • build.gradle
    plugins {
        ...
        id "com.ewerk.gradle.plugins.querydsl" version "1.0.10"
    }
    
    dependencies {
        //Querydsl 추가
        implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
        annotationProcessor "com.querydsl:querydsl-apt:${dependencyManagement.importedProperties['querydsl.version']}:jakarta"
        annotationProcessor "jakarta.annotation:jakarta.annotation-api"
        annotationProcessor "jakarta.persistence:jakarta.persistence-api"
    }
    
    //querydsl 추가 시작
    def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile
    querydsl {
        jpa = true
        querydslSourcesDir = querydslDir
    }
    sourceSets {
        main.java.srcDir querydslDir
    }
    configurations {
        querydsl.extendsFrom compileClasspath
    }
    compileQuerydsl {
        options.annotationProcessorPath = configurations.querydsl
    }
    //querydsl 추가 끝

     

    위와 같이 Querydsl에 필요한 패키지를 추가해주고, plugin도 추가해줍니다.

    또한, 아래 설정을 작성해주면 Querydsl관련 compile을 통해 Q클래스 빌드 결과물이 생성될 장소를 정해주게 됩니다.

     

    • MainClass
    @SpringBootApplication
    public class BoardserverApplication {
        ...
        @Bean
        public JPAQueryFactory jpaQueryFactory(EntityManager em){
            return new JPAQueryFactory(em);
        }
    }

    Querydsl이 쿼리 실행 시 필요한 JPAQueryFactory를 빈으로 생성해서 Repository에서 주입 받아 사용할 수 있도록 해주었습니다.

     

    JpaRepository 추가

    이전의 MyBatis mapper를 사용했던 코드들을 전부 JpaRepository를 활용한 코드로 변경해주었습니다.

    • UserRepository
    public interface UserRepository extends JpaRepository<User,Long>, UserRepositoryCustom {
    
        int countByUserId(String id);
    
    }
    
    • UserRepositoryCustom
    public interface UserRepositoryCustom {
        public UserDTO findByUserIdAndPassword(String userId, String password);
        public User findAllInfoByUserIdAndPassword(String userId, String password);
        public UserDTO findByUserId(String userId);
    
    }
    

    JpaRepository를 선언하고, Querydsl을 구현할 Repository 인터페이스를 상속하도록 했습니다.

    JpaRepository는 쿼리 메서드와 @Query를 통해 JPQL 기능들로 이루어질 수 있는 쿼리를 실행합니다.

    UserRepositoryCustom에서는 dto로 반환할 쿼리나 Querydsl을 활용한 쿼리를 직접 구현할 경우 사용하게 됩니다.

     

     

    AOP 적용

    spring-boot-starter-validation을 통해 요청 값의 유효성 검증을 시도한 뒤, ExceptionHandler로 이를 한 번에 처리할 수 있는 로직을 구성해보고 싶었습니다.

    • build.gradle
    dependencies {
        implementation group: 'org.springframework.boot', name: 'spring-boot-starter-validation'
        ...
    }

     

    @Valid가 선언된 body값에 유효하지 않은 값이 들어오는 경우 Spring에서 정의MethodArgumentNotValidException 예외가 발생하게 됩니다. 이 예외를 처리해줄 ExceptionHandler를 정의해줍니다.

    • ControllerAdvice
    @RestControllerAdvice
    @Log4j2
    public class ControllerAdvice {
    
        ...
    
        @ExceptionHandler(MethodArgumentNotValidException.class)
        public ResponseEntity<ErrorDTO> MethodArgumentNotValidExceptionHandler(MethodArgumentNotValidException ex) {
            log.error(ex.getMessage());
            return ResponseEntity.badRequest().body(new ErrorDTO(ex.getMessage()));
        }
    }
    

     

    @RestControllerAdvice는 Spring에서 전역적으로 예외를 처리할 수 있도록 해줍니다. @ControllerAdvice어노테이션에 @ResponseBody가 추가된 형태인 @RestControllerAdvice는 응답 값을 json으로 처리할 수 있습니다.

     

    @ExceptionHandler를 통해 어떤 예외를 처리할 것인지 정해줄 수 있습니다. MethodArgumentNotValidException 예외가 발생할 경우 해당 ExceptionHandler 메서드로 전달되게 되고 처리된 값을 응답으로 전해주게 됩니다.

     

    이 때, 에러 메시지를 client에 전송을 해주는 것이 좋을 것이라고 생각해서 우선은 message 값만 담은 json형식을 반환할 수 있도록 ErrorDTO를 선언해주었습니다.

     

    또한, 여러 계층의 다양한 로직 중 유효하지 않은 값이 넘어오거나 의도치 않은 상황이 발생할 경우 예외를 발생시켜 한 번에 처리할 수 있도록 해주었습니다.

     

    • ControllerAdvice
    @RestControllerAdvice
    @Log4j2
    public class ControllerAdvice {
    
        @ExceptionHandler(RuntimeException.class)
        public ResponseEntity<ErrorDTO> RuntimeExceptionHandler(RuntimeException ex) {
            log.error(ex.getMessage());
            return ResponseEntity.badRequest().body(new ErrorDTO(ex.getMessage()));
        }
        ...
    }
    

     

    여러 상황에서 예외를 발생시켜 전역적으로 처리할 수 있도록 해주었습니다.

    이 과정에서, 다양한 상황을 모두 커스텀 예외를 생성해 예외를 처리하는 것이 어떨까라는 생각이 들었습니다.

    그래서 다를 사람들의 의견을 찾던 도중 다음 블로그 글을 읽게 되었습니다. (블로그 글)

     

    여러 상황들에 대한 커스텀 예외를 모두 작성해서 사용할 경우

    • 코드를 보면서 해당 예외 이름만으로도 예외 상황을 파악할 수 있다.
    • 커스텀 정의를 통해 해당 예외에 대한 상세 정보 제공을 통해 빠르게 예외 상황을 인지할 수 있다.
    • 표준 예외를 통해 예외를 처리한다면, 어디에서 예외가 발생한지 파악하기 힘들 수 있다.. 다른 패키지 내에서 발생한 것인지, 아니면 내 로직 상에서 발생한 것인지..
    • 많은 비용이 발생하는 stack trace 생성 비용을 줄일 수 있다.

     

    위와 같은 장점들이 존재합니다. 예외 처리에 대해서 항상 어렵게만 생각했었는데 이러한 내용을 알게 되니 더 어려워진 것 같긴합니다.🥲

    저와 같은 경우 예외의 message를 작성하는 용도로 사용하고 있기 때문에, 커스텀 예외를 생성하지 않고 RuntimeException으로 모든 예외를 처리해주었습니다.

    하지만, 위와 같이 예외가 발생한 상세 이유와 같은 것도 포함시키고 또, 예외들을 응집도있게 관리하기 위해서 하나의 추상화된 Exception을 만들고 관리하는 것도 좋을 것 같다는 생각이 들었고, 프로젝트가 끝나기 전까지 한 번 고민해보는 것도 좋을 것 같습니다.

     

     

    후기

    오늘은 리팩토링과 관련된 글을 작성해보았습니다. 매번 들었던 것처럼 리팩토링은 시간날 때 무조건 해두어야된다는 말을 이번에야 따라본 것 같습니다. Controller, Service, Repository가 모두 자신의 로직에만 집중을 할 수 있도록 변경하고, 예외 처리와 관련된 로직을 전역적으로 처리해줄 수 있었습니다.
    글을 작성하면서도 더 리팩토링 할 수 있을 것 같은 부분이 많아 아쉬운 점이 많습니다. 강의를 진행하면서 나름대로의 방식으로 게속 나아가야될 것 같습니다.

    반응형
    LIST

    댓글

Designed by Tistory.