'MySQL'에 해당하는 글 10건

API Gateway + Lambda

Server/AWS 2024. 12. 18. 21:39

API 가 딱 한 개만 필요하다면... 어떤 방법으로 api 서버를 구축하는 것이 좋을까. ec2 + framework 는 생각만 해도... 생각하고 싶지도 않고... 역시나  API Gateway + Lambda  이다. DB(Mysql) 에 insert 할 api 를 하나 만들어야 하는 상황인데... API Gateway 로 요청 받고 람다를 실행시켜서 DB에 insert 시킨 후 응답해 주면 완료...

 

 

1. API Gateway

 

  • API 타입 http-api, rest-api 중 요청 제한을 위해 적어도 api-key 정도는 설정하는 것이 좋겠다. http-api 는 api-key 를 지원하지 않으므로 rest-api 선택. (그 외에도 res-api 를 선택할 이유는 많지만, 정말 아~무것도 필요없다면 http-api 쓰면 됨.)
  • resource 에서 경로 만들고 요청 메서드 하나 만들고 api key 췍~ 스테이지 배포 후 API 키 / 실행 계획 작성 / 스테이지 연결
  • api 키는 프론트 요청 헤더(x-api-key) 에 담아서 전달 받으면 됨. 그렇지 않으면 요청 거부됨.'

 

{
    "message": "Forbidden"
}

 

 

 

2. Lambda Layer

 

  • 람다 함수 런타임은 간단하게 Nodejs 사용할 생각.
  • mysql 연결을 위해 mysql2 패키지를 설치해야 하는데 재사용을 위해 레이어를 하나 생성한다.
  • 호환 런타임 잘 선택하고, 아래 zip 파일 업로드. (기타 필요한 라이브러리도 설치)

 

$ mkdir nodejs
$ cd nodejs
$ npm init -y
$ npm install mysql2
$ zip -r mysql2-layer.zip .

 

 

nodejs 최신버전의 경우 index.mjs 파일이 생성되므로, ESM 모듈 사용하려면, 압축하기 전에 package.json 에 아래 타입을 명시한다.

 

{
    "type": "module"
}

 

 

require is not defined in ES module scope, you can use import instead 에러 시 참고.

 

 

3. Lambda Function

 

nodejs 람다 함수를 하나 생성하고, 코드 가장 하단 설정에서 위에 업로드 한 layer 를 선택한다. index.mjs 파일 작성 후 배포.

 

import mysql from 'mysql2/promise';

const pool = mysql.createPool({
  host: 'host',
  user: 'user',
  password: 'password',
  database: 'database',
  port: 'port'
});

export const handler = async (event) => {

  const { id, name } = JSON.parse(event.body);

  try { 
    const query = 'INSERT INTO user (id, name) VALUES (?, ?)';

    const [results] = await pool.execute(query, [id, name]);
    return {
      statusCode: 200,
      body: JSON.stringify({ code: 200, message: 'ok', data: results })
    };
  } catch (err) {
    return {
      statusCode: 200,
      body: JSON.stringify({ code: 500, message: 'error: db insert', error: err.message })
    };
  }
};

 

 

4. API Gateway - Lambda 연결

 

API Gateway 해당 리소스의 통합 요청에서 생성한 람다 함수를 연결한다. request payload 를 람다 함수에 전달하기 위해  [Lambda 프록시 통합] 활성화 . 설정 후 [API 배포].

 

 

5. 테스트

 

postman 에서 header 에 api 키를 담아 stage url/resource 경로로 api 테스트를 진행한다.

 

{
    "code": 200
    "message": "ok",
    "data": {
        "fieldCount": 0,
        "affectedRows": 1,
        "insertId": 0,
        "info": "",
        "serverStatus": 2,
        "warningStatus": 0,
        "changedRows": 0
    }
}

 

 

6. 디버깅

 

만약 정상적인 응답을 받지 못했다면, 스테이지에서 cloudwatch logs 를 켜고, 로그를 확인한다. 처음 실행할 때는 cloudwatch 로깅에 대한 role 이 필요하다. IAM 에서 AmazonAPIGatewayPushToCloudWatchLogs 정책으로 role 하나 만들고, API Gateway 좌측 메뉴 설정에서 해당 role 선택.

 

 

7. 게이트웨이 응답

 

필요시 [게이트웨이 응답] 메뉴에서 request 에러시 응답값을 커스텀 할 수 있다.

 

 

8. CORS error

 

Access to fetch at 'https://11111111.execute-api.ap-northeast-2.amazonaws.com/dev/request' from origin 'http://localhost:8888' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

 

실전은 다르다. 매번 ok 사인 주는 postman 은 실전과 다르다. 프로젝트 할 때마다 괴롭히는 cors 에러... 메서드 선택 후 CORS 를 활성화 시키면 되구용...

 

 

 

 프록시 통합(proxy integrations)  이 설정되었기 때문에, 람다에서의 응답 헤더가 적용된다. 하여 람다에 적절한 응답 헤더가 추가되어야 한다.

 

return {
  statusCode: 200,
  headers: {
    "Access-Control-Allow-Headers" : "Content-Type,X-Api-Key",
    "Access-Control-Allow-Origin": "*",
    "Access-Control-Allow-Methods": "OPTIONS"
  },
  body: JSON.stringify({ code: 200, message: 'ok', data: results })
};

 

. 끝 .


WRITTEN BY
손가락귀신
정신 못차리면, 벌 받는다.

,

Spring Boot 프로젝트의 jar 배포 테스트를 위해 구동 테스트를 했다.

  1. bootRun (O)
  2. cmd - bootJar 실행 (X)
  3. docker - bootJar 실행 (O)

 

bootRun 을 통과하고 bootJar 를 만들어 cmd 에서 실행을 했는데 db connection 타이밍에 에러가 발생했다.

 

> java -jar example-0.0.1-SNAPSHOT.jar
...
com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Exception during pool initialization.

java.sql.SQLNonTransientConnectionException: Could not create connection to database server. Attempted reconnect 3 times. Giving up.
Caused by: com.mysql.cj.exceptions.CJCommunicationsException: Communications link failure.
...
The last packet sent successfully to the server was 0 milliseconds ago. The driver has not received any packets from the server.
Caused by: javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)

 

mysql-ssl

 

ㅋㅋ 아니 뭐 요점만 간단히 알려주든가, '왜' 에러 났는지를 알려줘야지 거참...

 

bootRun 은 되는데, jar 실행에서 db 연결이 안된다? ... ㅡㅡ;; 우선 구글을 겁나 뒤졌다. 하... 그 많은 게시물들 중 DB 연결해 놓고 jar 실행한 사람이 하나 없냐... 다들 톰캣 띄운 Hello World... 나도 헬로월드 좋아하긴 하지만... 개발군 헬창들... 저 에러메시지는 너무 광범위하고, 내 상황을 검색어로 표현하기는 더 어려웠다. 구글 검색 페이지를 넘기다넘기다 중국말까지 나오기 시작했다. 아니... 이 아이러니한 상황이 납득이가 안갔다. 이렇게 시간을 낭비할 수는 없어 docker 를 일단 돌려봤다. 엥... 도커는 또 잘됨; 최종 목적이 도커라 로컬은 그냥 넘겨도 되지만 또또... 오늘한 삽질이 아까워 그 끝을 보기로 했다.

 

spring-boot-starter-web
spring-boot-starter-data-jdbc
mysql-connector-java:8.0.25

 

우선 톰캣만 띄웠더니 문제없다. 바로 dataSource 연결했더니 똑같은 오류...! 아니 원래 로컬에서 DB 연결이 안됐었나; 더 해볼게 없었지만 마지막으로, 혹시나 하는 마음에 connector 버전을 바꿔보았다.

 

mysql-connector-java:5.1.49

 

당연히 같은 자리에서 오류가 발생하기는 했는데, 해결책이 나왔다!

 

Establishing SSL connection without server's identity verification is not recommended. According to MySQL 5.5.45+, 5.6.26+ and 5.7.6+ requirements SSL connection must be established by default if explicit option isn't set. For compliance with existing applications not using SSL the verifyServerCertificate property is set to 'false'. You need either to explicitly disable SSL by setting useSSL=false, or set useSSL=true and provide truststore for server certificate verification.

 

Mysql 5.5.45+, 5.6.26+, 5.7.6+ 부터는 ssl 접속이 default 라서, useSSL=false 를 명시하지 않으면 자동으로 ssl 접속을 시도한다는 것이다.

 

jdbc:mysql://example.ap-northeast-2.rds.amazonaws.com:3306/example?...&useSSL=false

 

설마 이거 때문일 줄은 몰랐다. 이렇게 해결되니, 그래도 편히 눈을 감을 수 있게 됐다. 친절한 mysql connector 구버전에게 감사를 표한다.하지만 여전히 황당하기는 하다. ssl 이유라면 bootRun 으로도 되지 말았어야지. 도커에서도 되지 말았어야지...ㅜ

 

지금 보니 처음 발생했던 에러의 저 마지막 줄이 힌트긴 했네...

 

Caused by: javax.net.ssl.SSLHandshakeException: No appropriate protocol (protocol is disabled or cipher suites are inappropriate)

 

 


WRITTEN BY
손가락귀신
정신 못차리면, 벌 받는다.

,

데이터베이스에서 replication 은 데이터베이스간의 데이터 복제를 의미한다. 하나의 데이터베이스에서 읽기와 쓰기 작업을 동시에 진행할 경우 병목 현상이 발생할 확률이 높다. 이를 방지하기 위해 흔히 Master(Write) 와 Slave(Read) 로 나누어, Master 에서 변경된 데이터를 Slave 로 복제하는 것을 replication 이라고 한다.

 

Java 어플리케이션에서 분기 처리하는 방법은 커넥션 풀(DataSource) 을 Master 와 Slave 로 나누어 주고, JDBC 의 Connection.setReadOnly 메소드나 Spring 의 @Transactional 를 사용하는 것이다.

 

 

Spring Boot 에서의 read / write 분기 처리

 

1. DB 설정 (application.yml)

 

spring:
  datasource:
    master:
      hikari:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://dev.cluster-abc.rds.amazonaws.com:3306/kps
        read-only: false
        username: username
        password: password

    slave:
      hikari:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://dev.cluster-ro-abc.rds.amazonaws.com:3306/kps
        read-only: true
        username: username
        password: password

 

 

2. DataSource Bean 등록

 

@Configuration
public class DataSourceConfiguration {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master.hikari")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create()
                .type(HikariDataSource.class)
                .build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave.hikari")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create()
                .type(HikariDataSource.class)
                .build();
    }
}

 

 

3. AbstractRoutingDataSource 구현

: Read-Write 가 동시에 필요하거나 복제 지연으로의 이슈를 막기 위해 AbstractRoutingDataSource 구현.

 

public class routingDataSourceImpl extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) ? "slave" : "master";
    }
}

 

 

4. Lazy 설정

: LazyConnectionDataSourceProxy 설정으로 실제 쿼리 직전에 connection 을 생성하여 불필요한 커넥션 점유 방지.

 

@Configuration(proxyBeanMethods = false)
public class DataSourceConfiguration {

    @Primary
    @Bean
    public DataSource dataSource(@Qualifier("routingDataSource") DataSource routingDataSource) {
        return new LazyConnectionDataSourceProxy(routingDataSource);
    }

    @Bean
    public DataSource routingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
                                        @Qualifier("slaveDataSource") DataSource slaveDataSource) {
        RoutingDataSourceImpl routingDataSourceImpl = new RoutingDataSourceImpl();
        Map<Object, Object> targetDataSource = new HashMap<>();
        targetDataSource.put("master", masterDataSource);
        targetDataSource.put("slave", slaveDataSource);
        routingDataSourceImpl.setTargetDataSources(targetDataSource);
        routingDataSourceImpl.setDefaultTargetDataSource(masterDataSource);
        return routingDataSourceImpl;
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.master.hikari")
    public DataSource masterDataSource() {
        return DataSourceBuilder.create()
                .type(HikariDataSource.class)
                .build();
    }

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.slave.hikari")
    public DataSource slaveDataSource() {
        return DataSourceBuilder.create()
                .type(HikariDataSource.class)
                .build();
    }
}

 

 

5. @Transactional 명시

 

@Service
public class ExampleService {

    ...

    @Transactional(readOnly = true)
    public List<ExampleEntity> getList() {
        return exampleRepository.getList();
    }

    @Transactional  // default : readOnly = false
    public Integer insertExample() {
        return exampleRepository.insertExample();
    }
}

 

 

6. 테스트

 

위의 코드를 예로 들면 updateExample() 를 실행하여 데이터 삽입을 확인하고, 

getList() 를 실행했을 때 slave dataSource 가 구성되며 삽입한 데이터가 출력되면 성공.

 

...
HikariPool-1 - configuration:
HikariPool-1 - Starting...
...
HikariPool-2 - configuration:
HikariPool-2 - Starting...
...

 

 


WRITTEN BY
손가락귀신
정신 못차리면, 벌 받는다.

,

jOOQ gradle 설정

Programming/Jooq 2021. 11. 25. 02:12

jooq

 

jOOQ 는 데이터베이스로부터 Java code 를 생성하여 Typesafe (SQL 구문 오류가 없는) 쿼리를 사용할 수 있다.

 

jooq 설정시 maven 을 사용한다면 사이트 메뉴얼을 참조하면 될 것이고, gradle 을 사용한다면 두가지 선택지가 있다.

 

 

 

 

Running the code generator with Gradle

Anything else you'd like to tell us? How did you hear from jOOQ? What made you try jOOQ? What are your expectations?

www.jooq.org

 

GitHub - etiennestuder/gradle-jooq-plugin: Gradle plugin that integrates jOOQ.

Gradle plugin that integrates jOOQ. . Contribute to etiennestuder/gradle-jooq-plugin development by creating an account on GitHub.

github.com

 

 

programmatic 설정은 왠지 간단해 보였는데 code generate 는 실패했다.ㅋㅋ; 어쨌든 공식 사이트에서도 서드 파티 플러그인을 추천하니 어쩔수 없이...

 

Spring Boot + Jooq + Mysql 로 테스트 했지만, 아래에 Spring Boot 관련 라이브러리는 대부분 제외했다. Spring Boot 연동도 문제없음.

 


1. dependency 설정

 

build.gradle - jooq 로 자바 코드를 생성하고 DSL 을 사용하는데 필요한 plugin 과 DB 커넥터 추가. 

 

// Jooq / Mysql connector
plugins {
    id 'java'
    id 'nu.studer.jooq' version '6.0.1'  // gradle-jooq-plugin
}

sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    runtimeOnly 'mysql:mysql-connector-java:8.0.25'
    jooqGenerator('mysql:mysql-connector-java:8.0.25')
}

 

 

2. Code generation 설정

 

org.jooq.codegen 으로 DB 의 테이블에 매핑되는 클래스를 생성하도록 설정이 필요하다. JDBC 설정부터 DB의 어느 테이블을 클래스로 변환하고 제외할 것인지 상세하게 설정할 수 있다. codegen 으로 생성된 클래스로 DSL 질의를 실행할 수 있다. build.gradle 에 이어서 추가...

 

jooq {
    version = '3.15.1'  // default (can be omitted)
    edition = nu.studer.gradle.jooq.JooqEdition.OSS  // default (can be omitted)

    configurations {
        main {  // name of the jOOQ configuration
            generateSchemaSourceOnCompilation = true  // default (can be omitted)

            generationTool {

                logging = 'WARN' // TRACE, DEBUG, INFO, WARN, ERROR, FATAL
                onError = 'LOG'  // FAIL, LOG, SILENT

                jdbc {
                    driver = 'com.mysql.cj.jdbc.Driver'
                    url = 'jdbc:mysql://example-db-cluster.cluster-clas4lq5kfel.ap-northeast-2.rds.amazonaws.com:3306/portal'
                    user = 'user'
                    password = 'password'
                    properties {
                        property {
                            key = 'ssl'
                            value = 'true'
                        }
                    }
                }

                generator {
                    name = 'org.jooq.codegen.DefaultGenerator'
                    strategy {
                        // org.jooq.codegen.KeepNamesGeneratorStrategy - DB 네이밍 유지: 언더바, 대소문자 등... 기본값은 PascalCaseNames
                        name = 'org.jooq.codegen.DefaultGeneratorStrategy'
                    }
                    database {
                        // https://www.jooq.org/doc/3.15/manual/code-generation/codegen-advanced/codegen-config-database/codegen-database-name/
                        name = 'org.jooq.meta.mysql.MySQLDatabase'
                        inputSchema = 'portal'

                        includes = '.*' // 정규표현식
                        excludes = 'test_.* | temp_.*'
                    }
                    generate {
                        generatedSerialVersionUID = 'CONSTANT'
                        javaTimeTypes = true    // java.time.*

                        deprecated = false
                        records = true
                        immutablePojos = true
                        fluentSetters = true
                    }
                    target {
                        // packageName = 'nu.studer.sample'
                        packageName = 'com.example.api.jooqgen'
                        // directory = 'build/generated-src/jooq/main'  // default (can be omitted)
                        directory = 'src/main/java'  // default (can be omitted)
                    }
                }
            }
        }
    }
}

 

공식 예제에서 약간의 내용만 바꿨다. 눈치채지 못할 만큼...ㅋ DB 로부터 Java code 를 생성하기 위한 스크립트이기 때문에, jooq 버전, DBCP 설정도 들어가고, 생성 전략, 코드 생성할 디렉토리 등등... 조금 길어보이긴 하지만 별거 없다. 만약 DB 스키마가 이미 존재한다면 여기까지 하고 generateJooq 태스크를 실행하면 설정한 디렉토리에  Java 클래스들이 생성되는 것을 확인할 수 있다. 별 문제가 없다면...

 

 

3. DSL 테스트

 

portal.account 테이블에 id, email 이 있다 치고, entity, controller 대충 만들고 DSL 로 쿼리 뽑기.

 

package com.example.api;

import com.example.joooq.gen.tables.Account;
import org.jooq.DSLContext;
import org.jooq.impl.DSL;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public class AccountRepository {
    private final DSLContext dsl;

    public AccountRepository(DSLContext dsl) {
        this.dsl = dsl;
    }

    public List<Account> getList1() {
        return dsl.select(DSL.field("id"), DSL.field("email"))
                .from(DSL.table("account")).fetchInto(AccountEntity.class);
    }

    public List<Account> getList2() {
        return dsl.select(Account.ACCOUNT.IDX, Account.ACCOUNT.EMAIL)
                .from(Account.ACCOUNT).fetchInto(AccountEntity.class);
    }
}

 

뙇!

getList1 의 쿼리 결과가 잘 나온다면 DB 연동이 잘 된것이고,

getList2 의 쿼리 결과가 잘 나온다면 생성된 jooq 코드가 잘 연동된 것이고.

 



* 연동 후기

 

처음에 programmatic 방식으로 접근했다가 codegen 에 실패했는데... 에러도 안나고 뭐... 그래서 plugin 방식으로 설정을 마쳤다. spring 이고 jooq plugin 이고 다 최신버전으로 설치했는데, jooq 버전을 비롯하여 중간중간 중복된 라이브러리들이 많이 거슬렸다. 가이드를 보고 spring 의 dependency-management 와 버전을 맞추면 해결된다.

 

jooq 3.15 버전부터 R2dbc 라이브러리가 추가되서 자동구성시 제외하려면 추가.

 

@SpringBootApplication(exclude = {R2dbcAutoConfiguration.class})

 

target directory 이슈... codegen 에서 기본 타겟 디렉토리는 build/generated-src/jooq/main 이며 모든 개발자가 각각 빋드하여 코드를 생성해야 한다. 이를 소스셋(src/main/java) 로 변경한다면 버전관리가 가능할 수 있지만 해당 디렉토리를 삭제하고 generate 하기 때문에 target 디렉토리를 src/main/java 로 했다가는 그 안의 소스 패키지가 빌드할 때마다 다 날아간다. 이를 막기 위한 옵션으로 clean 속성을 제공하기는 하나, 정작 해당 값을 false 로 지정하면 에러가 발생한다.

 

generator.target.clean must not be set to false. Disabling the cleaning of the output directory can lead to unexpected behavior in a Gradle build.

 

기본값대로 build 디렉토리를 이용하거나, 만약 소스가 포함되지 않은 별도의 타겟 디렉토리 구성이 필요하다면 독립된 디렉토리나 별도의 모듈에 추가하는 것을 추천한다.

 

 


WRITTEN BY
손가락귀신
정신 못차리면, 벌 받는다.

,

TLSv1 deprecated

Daily/Prog 2021. 11. 24. 01:59

20년이 지나도 멈추지 않는 삽질 이야기.

 

WARN: This connection is using TLSv1 which is now deprecated and will be removed in a future release of Connector/J

 

warning 은 신호등에 비유하면 주황색과 같다. 가도 그만 멈추어도 그만인데 난 될수 있으면 멈추는 스타일이다. 그 바람에 또 소중한 시간들을 낭비한다. 위 경고는 TLSv1 / TLSv1.1 이 deprecated 되었으며, 이 프로토콜을 사용한 연결시 MySQL Connector/J Version 8.0.26 버전부터 발생하는 메시지다.

 

참고: https://docs.oracle.com/cd/E17952_01/connector-j-8.0-relnotes-en/news-8-0-26.html

 

3 Changes in MySQL Connector/J 8.0.26 (2021-07-20, General Availability)

3 Changes in MySQL Connector/J 8.0.26 (2021-07-20, General Availability) Version 8.0.26 is the latest General Availability release of the 8.0 series of MySQL Connector/J. It is suitable for use with MySQL Server versions 8.0, 5.7, and 5.6. It supports the

docs.oracle.com

해결은 간단하다. 난 8.0.27 버전을 사용중이었지만 8.0.26 아래 버전으로 다운그레이드 하면 되는데... 이제부터 삽질 시작이다.



Intellij 에서 gradle + java 프로젝트.

모듈은 두개. DB 와 API.

 

// DB module : build.gradle
dependencies {
    ...
    runtimeOnly 'mysql:mysql-connector-java:8.0.25'
    ...
}

// API module : build.gradle
dependencies {
    implementation project(':DB')
}

 

간단하다. DB 모듈에서 MySQL Connector/J 8.0.25 를 정의하고, API 모듈에서 그걸 가져다 썼다. 하지만 경고는 계속해서 나타났다. External Libraries 를 확인해보니 8.0.25 / 8.0.27 두 개 버전이 모두 import 되어 있었다. cache 문제겠거니 하고 기본적인 캐시 삭제 작업들을 시작했다.

 

  • Invalidate Caches -> Invalidate and Restart
  • Project Settings -> Libraries -> MySQL Connector/J 8.0.27 버전 삭제
  • $USER\.gradle\caches 디렉토리 삭제

 

gradle-library-duplicate

 

하지만 이 짓들을 다 해도 MySQL Connector/J 8.0.27 는 계속해서 살아났고 경고가 콘솔에 도배됐다. Find Usages 에서 확인해보니 API 모듈에서 사용중이란다. DB 모듈에서는 8.0.25 를 불러오는데, API 모듈에서 그걸 불러오면 8.0.27 로 변신한다니 이게 대체 무슨...

 

혹시라도 다른 라이브러리에 중복되어 있는지 dependency tree 도 확인해 봤다.

 

 

(DB)  mysql:mysql-connector-java:8.0.25

(API) mysql:mysql-connector-java:8.0.25 -> 8.0.27

 

다른 곳에서 정의 되어 있는건 한개도 없는데 그냥 바뀌어 버린다.ㅋㅋ

 

한참을 삽질한 끝에 밝혀낸 원인은...

gradle 에 정의한 두개의 플러그인 간에 의존성들이 달라 발생한 문제였다.

결국 재정의 했다...

 

dependencies {
    runtimeOnly 'mysql:mysql-connector-java:8.0.25'
    implementation project(':DB')
}

 

 


WRITTEN BY
손가락귀신
정신 못차리면, 벌 받는다.

,