ABOUT ME

진심은 있으되 진심밖에 없는 태도는 취하지 말고, 불확실한 삶에서 내가 내릴 수 있는 가장 자유로운 선택은 오늘 하루를 행복하게 사는 것:)

  • Spring Multi Module 구현
    [공부] 프로그래밍/Spring・Spring Boot (JAVA) 2024. 5. 16. 16:22

     
    ♧ 전체 코드 : https://github.com/woodisco/multi-module

    GitHub - woodisco/multi-module

    Contribute to woodisco/multi-module development by creating an account on GitHub.

    github.com

     

    Git Flow 전략
    git을 사용하는 개발 환경에서 branch간 문제없이 배포를 안전적으로 할 수 있도록 branch를 관리하는 전략이다.

    ・ main(master) : 실제 운영 환경에 있는 코드 브랜치
    ・ dev : main 브랜치를 베이스로 생성한 브랜치, 다음 배포에 나갈 코드 브랜치
    ・ feature : 각 개발 브랜치, dev 브랜치에 머지
    ・ release : dev 브랜치를 베이스로 생성된 배포 브랜치, 모든 작업 완료 후에 main에 머지
    ・ hotfix : 장애발생 시 main 브랜치를 베이스로 생성한 브랜치

     Spring Multi Module 개념

    필요한 기능별로 module을 생성하여 조립하며 N개의 module이 조립되어 있는 프로젝트를 multi module 프로젝트라고 부른다.

    exception 핸들링 :
    프레임워크에서 발생한 exception은 반드시 커스텀하게 wrapping하여 처리한다.
    @RestControllerAdvice 어노테이션을 사용하여 모든 예외를 해당 클래스에서 클라이언트와 사전에 정의한 값으로 재정의 한다.

    프로젝트 생성 & 모듈 생성

    ① multimodule 프로젝트 생성

    multimodule 프로젝트에서 module-api 모듈 생성

    multimodule 프로젝트에서 module-common 모듈 생성

    ④ settings.gradle 수정

    => 추가한 모듈 설정해주기

    rootProject.name = 'multi-module'
    include 'module-api'
    include 'module-common'

    module-api의 build.gradle 수정 

    => module-api에서 module-common을 사용하기 때문에 설정 추가해주기

    implementation project(':module-common')

    module-api에서 module-common을 참조하는지 테스트
    : Java 객체 참조 테스트

    module-common에 enum 작성

    module-api에서 DemoController, DemoService를 작성

    ③ curl 명령어로 테스

    => curl localhost:8080/save

    curl 명령어
    curl은 "Client URL"의 약자로, 명령줄에서 HTTP 요청을 수행할 수 있는 오픈 소스 도구입니다. 주로 웹 서버와 상호 작용할 때 사용되며, 다양한 프로토콜을 지원합니다. curl을 사용하면 웹 페이지를 가져오거나, API에 요청을 보내는 등의 작업을 쉽게 할 수 있습니다.

    module-api에서 module-common을 참조하는지 테스트
    : Spring Bean 참조 테스트

    module-common에서 CommonDemoService 작성

    module-api/DemController에 CommonDemoService 추가

    CommonDemoService의 Bean이 생성되지 않는 에러 발생

    ♤ 해결방법 2가지

    1. 실행시키는 패키지 기준으로 컨포넌트 스캔을 하게 되는데 이때 하위 모듈에도 똑같은 패스로 컴포넌트 스캔이 이루어지게 된다. module-commo과 module-api에는 dev-de까지 폴더가 동일하고 module-commo에는 moduleapi라는 폴더가 없기 때문에 Bean을 주입받지 못하게 된다. 그렇기 때문에 module-api의 ModuleApiApplication의 위치가 중요해지는데 dev-de의 폴더로 이동을 시키게 되면 CommonDemoService의 Bean을 주입받을 수 있게 된다.

    2. ModuleApiApplication에 지정해주기
    @SpringBootApplication( scanBasePackages = { "dev.be.moduleapi", "dev.be.modulecommon" } )

    Exception 핸들링
    : module-api

    ① CommonResponse 작성

    CustomException 작성

    DemoController, DemoService에 메소드 추가

    => CustomException을 발생시키기

    public String exception() {
        if (true) {
            throw new CustomException(CodeEnum.UNKNOWN_ERROR);
        }
        return "exception";
    }

    ④ GlobalExceptionHandler 작성

    @RestControllerAdvice
    전역적으로 RESTful 웹 애플리케이션에서 발생할 수 있는 예외를 처리하는 클래스에 적용됩니다. 이 애노테이션을 사용하면 예외 처리를 담당하는 클래스를 정의할 때 특정 컨트롤러나 패키지에 제한되지 않고 전역적으로 적용할 수 있습니다. 

    @RestControllerAdvice 애노테이션이 적용된 클래스는 다음과 같은 기능을 수행할 수 있습니다: 
    ・ 전역 예외 처리: 모든 컨트롤러에서 발생하는 예외를 처리할 수 있습니다.
    ・ 특정 예외 타입에 대한 처리: 특정 예외 타입에 대한 처리 로직을 정의할 수 있습니다.
    ・ 특정 컨트롤러나 패키지에 대한 예외 처리: basePackages 또는 basePackageClasses 속성을 사용하여 특정 패키지나 컨트롤러에서만 발생하는 예외를 처리할 수 있습니다.

    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(CustomException.class)
        public CommonResponse handlerCustomException(CustomException e) {
            return CommonResponse.builder()
                    .resultCode(e.getReturnCode())
                    .resultMessage(e.getReturnMessage())
                    .build();
        }
    
        @ExceptionHandler(Exception.class)
        public CommonResponse handlerException(Exception e) {
            return CommonResponse.builder()
                    .resultCode(CodeEnum.UNKNOWN_ERROR.getCode())
                    .resultMessage(CodeEnum.UNKNOWN_ERROR.getMessage())
                    .build();
        }
    }
    예외가 발생한 경우, CustomException으로 한번 매핑을 해준뒤에 throw가 던져지고 그 throw는 @RestControllerAdvice에 의해 잡히게 된다. 그 다음 @ExceptionHandler에 선언 해놓은 클래스가 있을 경우에 해당 메소드가 실행된다. 

    DB 연동

    module-common에 docker-compose.yml 작성

    # Docker Compose의 버전 정보
    version: '3.8'
    
    services:
      mysql: # 서비스의 이름
        container_name: multimodule_local
        image: mysql:latest
        volumes:
          - ./db/conf.d:/etc/mysql/conf.d
        ports:
          - "3306:3306"
        environment: # 환경 변수
          - MYSQL_DATABASE=multimodule_local
          - MYSQL_USER=
          - MYSQL_PASSWORD=
          - MYSQL_ROOT_PASSWORD=
          - TZ=Asia/Tokyo

    module-common/db/conf.d 패키지를 생성한 뒤, my.cnf 작성

    # MySQL8 default character set는 utf8mb4 이므로 client만 선언합니다.
    [client]
    default-character-set = utf8mb4
    
    # MySQL8 default authentication policy는 `caching_sha2_password`입니다.
    # 이를 지원하지 않는 DB Client로 접속하기 위해서는 기존 정책인 `mysql_native_password`로 설정합니다.
    [mysqld]
    authentication-policy = mysql_native_password

    module-api/resources/application.yml 작성

    server:
      port: 8080
    
    spring:
      application:
        name: multi-module
      datasource:
        url: jdbc:mysql://localhost:3306/{{컨테이너이름}}?zeroDateTimeBehavior=convertToNull&characterEncoding=UTF-8&serverTimezone=Asia/Tokyo
        username: 
        password: 
        driver-class-name: com.mysql.cj.jdbc.Driver

    도커 파일 실행하기

    docker-compose up -d --build --force-recreate

    ⑤ DB연동 테스트를 위해 module-api/DemoService 수정

    public String save() {
        Member newMember = memberRepository.save(Member.builder()
                .name(Thread.currentThread().getName())
                .build());
    
        return "save";
    }
    
    public String find() {
    
        return String.valueOf(memberRepository.findAll().size());
    }

     ⑥ MemberRepository의 Bean 주입 에러 발생

    => ModuleApiApplication에 지정해주기

    @EntityScan("dev.be.modulecommon.domain")
    @EnableJpaRepositories(basePackages = "dev.be.modulecommon.repositories")

     ⑦ 현시점 실제 DB에 Member 엔티티가 존재하지 않으므로 module-api/resources/application.yml에 아래를 추가

     => ddl-auto: create : 실제 DB에 생성해주는 것을 로그에서 확인 가능

      jpa:
        database: mysql
        database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
        hibernate:
          ddl-auto: create

    테이블이 만들어진것을 확인 가능

     ⑧ curl localhost:8080/save or find 를 통해 DB연동 테스트하기


     빌드 및 배포
    ◇ gradle로 빌드를 해야하는 이유
    로컬에서 개발을 하는 경우는 인텔리제이의 도움을 받아 서버를 띄울 수 있다. 하지만 실제 운영 서버의 경우에는 인텔리제이의 도움을 받을 수 없고 jar파일을 넘겨서 실행을 시켜야 한다. 그래서 gradle이나 maven과 같은 빌드 풀을 사용하여 jar파일을 만들어야 한다. 만들어진 jar파일을 운영 서버로 넘겨 실행시켜야 한다.

    ① module-common/build.gradle에 추가

    tasks.bootJar { enabled = false }
    tasks.jar { enabled = true }
    bootJar의 기본값은 true이며 true로 설정할 경우 **.jar라는 파일이 형성되게 된다. module-common의 경우는 다른 모듈에서 참조하는 목적의 모듈이기 때문에 실행 가능한 jar파일을 만들 필요는 없다. 만약 true로 설정할 경우 메인 클래스를 찾게 되는데 module-common는 module-api처럼 메인 클래스가 없기에 에러가 발생하게 된다.

    jar의 기본값도 true이며 true로 설정할 경우 **-plain.jar라는 파일이 형성되게 된다. **.jar와의 차이점으로는 dependency를 갖고 있지 않게 된다. 즉 클래스와 리소스만 포함하고 있어서 서버를 실행 시킬 수 없다.

    ② 실행 : ./gradlew clean :module-api:buildNeeded --stacktrace --info --refresh-dependencies -x test

    [Gradle 빌드 명령어 + jar 파일 실행 CLI]
    
    - Gradle 빌드 명령어 :: root project
      -> ./gradlew clean :module-api:buildNeeded --stacktrace --info --refresh-dependencies -x test
    ・ --info :
    debug -> info -> warn -> error 이 단계로 info를 설정할 경우, info -> warn -> error 전부 출력을 하겠다는 의미

    ・ -x test :
    테스트 코드는 스킵

    jar파일 생선 완료

    ③ jar파일로 실제 서버 구동
    ・jar파일로 이동하기
    - profile 지정 X
      -> java -jar module-api-0.0.1-SNAPSHOT.jar
    - profile 지정 O
      -> java -jar -Dspring.profiles.active=local module-api-0.0.1-SNAPSHOT.jar

     

    ・실행 : java -jar module-api-0.0.1-SNAPSHOT.jar

    ./gradlew clean 실행 시 build 폴더가 삭제

    profile 설정 - 1

    환경별로 설정해야하는 profile이 다르기 때문에 N개의 profile설정이 필요하다

     ① yml 파일 이름 변경 및 수정

    • application-local.yml
    • application-beta.yml
    profile-name: "local"

    module-api/DemoService에서 테스트

    @Value("${profile-name}")
    private String name;

    ③ 해당 에러 발생

    Factory method 'dataSource' threw exception with message: Failed to determine a suitable driver class

    => 스프링을 실행하면 profile이 디폴트로 설정되어 application-local.yml을 읽어 들이지 못하고 있다.

    해결방법은 Active profiles를 수정하기

    Add VM option을 선택 후, 아래와 같이 추가하기

    -Dspring.profiles.active=local

     profile 설정 - 2

     실제 운영 서버에서는 jar파일을 사용해서 서버를 돌리기 때문에 java의 jvm 옵션을 통해 profile 값을 설정

    ① 파일을 수정했기 때문에 빌드 및 배포 다시 하기

    java -jar -Dspring.profiles.active=local module-api-0.0.1-SNAPSHOT.jar
    java -jar -Dspring.profiles.active=beta module-api-0.0.1-SNAPSHOT.jar

     

    출처 : 패스트캠퍼스 10개 프로젝트로 완성하는 백엔드 웹개발(Java/Spring) 초격차 패키지 Online

    '[공부] 프로그래밍 > Spring・Spring Boot (JAVA)' 카테고리의 다른 글

    Feign Client 구현 - 1  (0) 2024.05.21
    비동기 프로그래밍 구현  (0) 2024.05.20
    Spring Batch 구현 - 7  (0) 2024.05.14
    Spring Batch 구현 - 6  (0) 2024.05.09
    Spring Batch 구현 - 5  (0) 2024.05.08
Designed by Tistory.