mysql 버전을 확인하여 정상적으로 설치되었는지 확인 ( mysql --version )
3. mysql 을 활성화
설치된 mysql 을 brew 를 사용하여 활성화 시킨다 ( brew services list 를 통해 활성화된 서비스 확인 > brew servies start mysql 를 통해 서비스 활성화 )
4. mysql 접속
설치된 mysql 의 root 계정으로 접속 ( 최초 비밀번호 세팅을 하지 않았기 때문에 비밀번호 입력창에서 엔터만 입력 )
5. mysql 테이블 확인 및 생성
show databases 를 통해 현재 생성된 데이터베이스를 확인 후 create database 를 통해 DB 생성 후 다시 확인
6. mysql root 계정 비밀번호 설정 및 확인
ALTER USER 'root'@'localhost' IDENTIFIED BY '{변경할 비밀번호}'; 를 사용하여 root 계정의 비밀번호를 변경한 뒤 exit 으로 mysql 입력을 종료하고 다시 root 권한으로 로그인 시도하여 비밀번호가 변경된 것을 확인!
7. mysql 사용자 계정 생성 및 DB 접근 권한 부여
CREATE USER '{USER 이름}'@'localhost' IDENTIFIED BY '{비밀번호}'; 를 사용하여 user 계정을 생성한 뒤 GRANT ALL PRIVILEGES ON {권한을 부여할 DB 이름}.* TO '{권한을 부여받을 USER}'@'localhost'; 를 통해 생성된 muzinut 이라는 DB 에 muzinut USER 에게 모든 권한을 부여
8. 기존에 세팅되어 있던 H2 database 에서 MySQL 로 application.yml 설정 바꾸기
기존에 설정되어 있던 개발용 H2 데이터베이스에서 새로 설정한 mysql 데이터베이스로 설정 변경
9. 생성된 테이블 확인
USE {DB명}; 을 사용하여 사용할 DB 를 선택한 뒤 show tables; 명령어를 사용하여 생성된 테이블을 확인
프로젝트를 진행하면서 백엔드 서버에서 프론트 측으로 이미지 정보를 전송해줘야 하는 경우가 많이 발생하였다.
보통은 이미지 서버를 별도로 운영하여 원본의 데이터를 제공하지 않고 이미지 서버(Storage)에 저장된 이미지의 url 을 제공하여 이미지를 제공하지만 제한된 프로젝트 기한 내에 이미지 서버까지 구축하는 데는 시간적으로 촉박할거 같아 백엔드 서버에서 원본 데이터를 프론트로 전송해주기로 했다.
처음엔 프론트에서 이미지나 파일을 multipart/form-data 로 백엔드 서버로 전송하여 처리했기 때문에 동일하게 백엔드 서버도 multipart/form-data 를 사용하여 이미지를 프론트로 전송하면 된다고 생각했다.
그러나 multipart/form-data 로 이미지를 전송된 이미지를 프론트에서 처리할 수 있는 방법이 존재하는지 몰랐고, 여러가지 방법을 찾던 와중 Base64 형태로 이미지를 백엔드 측에서 인코딩하여 문자열로 프론트로 보내주게 되면 프론트에서는 문자열로 인코딩된 이미지 정보를 <img src="data:image/<이미지확장자>;base64,<data코드>"> 다음과 같은 형태로 인코딩된 이미지를 바로 출력할 수 있다는걸 알게 되었다.
결국, 서버에서 이미지를 전송할때 Base64 로 이미지를 인코딩하여 인코딩된 문자열을 전송해주기로 하였다.
DTO 의 이미지 파일 이름을 기준으로 파일을 불러와 인코딩하여 다시 DTO 에 저장 후 RestController 를 통해 응답
위와 같이 Base64 로 인코딩된 이미지를 바로 보낼 때 장점과 단점
- 장점 -
1. 서버에서 이미지를 저장할 때 이미지의 원본을 저장할 필요 없이 인코딩된 문자열을 저장하여 바로 문자열을 불러 응답해줄 수 있다
2. 렌더링될 때 HTML dom 을 통해 렌더링되기 때문에 이미지가 끊기지 않고 출력된다
- 단점 -
1. 프론트 코드의 길이가 매우 길어지므로 코드의 가독성이 떨어진다
2. 용량이 증가한다
( 256가지를 표현할 수 있는 바이트를 printable한 64가지를 사용해서 표현하니 당연하다 다시 말해, 8비트를 6비트로 표현하는 것이다 3개의 8비트는 4개의 6비트로 표현할 수 있다 따라서 Base64 인코딩을 사용하면 원본보다 33%의 크기 증가가 발생한다 )
( 현재는 이미지 원본을 보내서 useState 에 데이터 키 값을 통해 받은 이미지 값을 return 하는 함수를 생성하고 해당 State 를 이미지 src 부분에 사용하면 이미지가 출력되는 것을 알게되었으므로 추후에 서버측과 클라이언트 측의 이미지 처리 방식 코드의 변경이 필요하다 )
프로젝트를 진행하면서 매 주 혹은 매 일마다 유저 테이블의 특정 컬럼 값의 데이터를 초기화 시켜야하는 문제가 발생했다.
위 문제를 스케쥴러를 사용하여 처리하고자 하였는데 검색해보니 사용하려하는 MySQL 자체에서 제공하는 스케쥴러와 Java 에서 제공하는 스케쥴러 두 가지 방식이 존재하는 것을 알았다.
나는 위 두가지 방법 중 Java 에서 제공하는 스케쥴러 기능을 사용하기로 했는데 그 이유는 DB 에 스케쥴러를 적용할 경우 현재 개발 과정에서는 H2 데이터베이스를 사용하기 때문에 배포 이후 실제 사용하는 MySQL 데이터베이스에 스케쥴러를 설정하여 테스트를 진행해야 하는데 Java 에서 제공하는 스케쥴러를 사용하면 DB 가 변경되어도 메소드에서 호출하는 JPA 쿼리가 자동으로 변경된 데이터베이스로 쿼리를 변경하여 처리하기 때문이다.
1. @Scheduled 어노테이션을 사용하기 위해 메인 어플리케이션에 @EnableScheduling 설정
메인 어플리케이션에 @EnableScheduling 어노테이션을 적용해줘야 메소드에 사용하는 @Schedule 어노테이션이 정상적으로 작동한다
2. 반복적으로 실행하고자 하는 메소드에 @Scheduled 어노테이션 사용
사용하고자 하는 메소드에 @Scheduled 어노테이션을 사용하여 일정 시간 혹은 일정 간격마다 메소드를 반복 실행시킨다.
3. 메소드 반복 실행 주기를 설정 및 기준 시간 설정
반복 주기 설정 및 기준 시간 설정 ( 현재는 @Scheduled 속성 중 cron 표현식을 사용하였으며 매 월 1일 자정마다 메소드가 반복 실행되게끔 설정 )
반복 주기 표현은 @Scheduled 어노테이션의 속성 값 마다 표현 방법이 다르며 나는 cron 표현식을 사용하였다
비트코인 모의 투자를 구현하는 와중 실시간으로 데이터를 업비트 api 를 통해 가져와 Oracle Cloud DB 에 저장하고 해당 데이터를 꺼내 실시간으로 가격을 비교해서 출력해야 하는 과정이 필요했다.
문제 발생 : DB 안에 1초마다 쌓이는 각 코인들의 값을 select 쿼리로 1초마다 가져올때 ( 자바스크립트의 setInterval 메소드를 사용하여 Oracle Cloud DB 로 1초마다 데이터를 요청 ) Error: NJS-500: connection to the Oracle Database was broken 문구가 발생되면서 값을 제대로 가져오지 못하는 현상이 발생, 로그인 시도 시에도 동일한 현상이 발생
총 평가 금액을 실시간으로 localhost:3000/wallet/total_buy_coin_result_cost 경로로 요청하는 함수ejs 파일 내에서 위 함수를 1초마다 갱신
위 구문을 실행하면 서버에서 DB 에 접근하여 데이터를 받아오게끔 설계해 놨는데 아래가 해당 dao 의 코드이다
const oracledb = require("oracledb");
const dbConfig = require("../../../config/database/db_config");
oracledb.outFormat = oracledb.OBJECT;
oracledb.autoCommit = true;
const get = {
have_KRW : async (member_id) => {
const con = await oracledb.getConnection(dbConfig);
let result = await con.execute(`select money from member where member_id='${member_id}'`);
return result;
},
// 매수했던 각 코인의 갯수를 Object 형태로 담는 함수
had_coin_num : async (member_id) => {
let had_coin_num = {
btc : 0,
eth : 0,
shib : 0,
bch : 0,
etc : 0,
btg : 0,
sol : 0,
doge : 0,
xrp : 0,
id : 0,
pundix : 0,
stx : 0,
aave : 0,
dot : 0,
avax : 0,
gas : 0,
sbd : 0,
ong : 0,
sei : 0,
ont : 0
};
let coin_list = ["btc","eth","shib","bch","etc","btg","sol","doge","xrp","id","pundix","stx","aave","dot","avax","gas","sbd","ong","sei","ont"];
const con = await oracledb.getConnection(dbConfig);
for(let i=0; i<coin_list.length; i++){
// 구매 코인의 총 갯수를 가진 DB 결과 Object
let coin_num_object = await con.execute(`select coin_num from trade where coin_name='${coin_list[i]}' and member_id = '${member_id}' and status = 1`);
// 코인의 구매 이력이 없는 경우
if(coin_num_object.rows[0] === undefined){
had_coin_num[coin_list[i]] = 0;
continue;
}
let each_coin_num = 0;
coin_num_object.rows.forEach(coin => {
if(coin.COIN_NUM == null){
coin.COIN_NUM = 0;
}
each_coin_num += coin.COIN_NUM;
})
had_coin_num[coin_list[i]] = each_coin_num
}
return had_coin_num;
},
// 매수했던 각 코인의 가격의 합을 Object 형태로 담는 함수
had_coin_cost : async (member_id) => {
let had_coin_cost = {
btc : 0,
eth : 0,
shib : 0,
bch : 0,
etc : 0,
btg : 0,
sol : 0,
doge : 0,
xrp : 0,
id : 0,
pundix : 0,
stx : 0,
aave : 0,
dot : 0,
avax : 0,
gas : 0,
sbd : 0,
ong : 0,
sei : 0,
ont : 0
};
let coin_list = ["btc","eth","shib","bch","etc","btg","sol","doge","xrp","id","pundix","stx","aave","dot","avax","gas","sbd","ong","sei","ont"];
const con = await oracledb.getConnection(dbConfig);
for(let i=0; i<coin_list.length; i++){
// 구매 코인의 총 갯수를 가진 DB 결과 Object
let coin_cost_object = await con.execute(`select buy_cost from trade where coin_name='${coin_list[i]}' and member_id = '${member_id}' and status = 1`);
// 코인의 구매 이력이 없는 경우
if(coin_cost_object.rows[0] === undefined){
had_coin_cost[coin_list[i]] = 0;
continue;
}
let each_coin_cost = 0;
coin_cost_object.rows.forEach(coin => {
if(coin.BUY_COST == null){
coin.BUY_COST = 0;
}
each_coin_cost += coin.BUY_COST;
})
had_coin_cost[coin_list[i]] = each_coin_cost
}
return had_coin_cost;
},
// 현재 보유 중인 코인의 갯수를 구하는 함수
have_coin_num : async (member_id) => {
const con = await oracledb.getConnection(dbConfig);
let have_coin_num = await con.execute(`select btc,eth,shib,bch,etc,btg,sol,doge,xrp,id,pundix,stx,aave,dot,avax,gas,sbd,ong,sei,ont from member_account where member_id = '${member_id}'`);
return have_coin_num.rows[0];
},
// 현재 코인의 가격 20개를 구하는 함수
now_coin_cost : async () => {
const con = await oracledb.getConnection(dbConfig);
let now_coin_cost_object = await con.execute(`select * from (select * from coin_info order by order_column desc) where rownum <= 20`);
return now_coin_cost_object.rows;
}
}
module.exports = {get}
위의 코드 중 평가 금액은 현재 보유 중인 코인의 갯수를 구하는 함수와 현재 코인의 가격 20 개를 구하는 함수를 사용하는데, 이 부분을 반복적으로 돌리면 계속 DB의 연결이 끊기는 오류가 발생....
웹에서 NJS-500 오류 코드와 관련해서 내용을 찾아보다가 문득 DB 에 데이터를 넣는 부분과 DB 의 데이터를 꺼내오는 부분이 동시에 작동하여 문제가 생기지 않을까 하는 생각이 들었다.
( Oracle Cloud DB 와 연결이 끊어질때 1회성 쿼리가 발생됨으로 세션 타임아웃 문제는 아닐 것으로 예상, 고로 최대 연결 갯수의 초과이거나 커넥션의 과부하로 인한 연결 끊김으로 예상 )
▼
이 부분을 해결할 수 있는 방법을 찾다 보니 커넥션 풀(DBCP) 에 대한 부분이 나왔고 이를 사용해보기로 했다.
- 커넥션 풀(DataBase Connection Pool)이란? -
웹 컨테이너(WAS)가 실행되면서 미리 DB 와 연결(connection)된 객체를 pool 에 저장해 두었다가 클라이언트에게 요청이 오면 connection 을 빌려주고 처리가 끝나면 다시 connection 을 반납받아 pool 에 저장하는 방식을 말한다.
- 커넥션 풀의 특징 -
1. 웹 컨테이너(WAS)가 실행되면서 connection 객체를 미리 pool 에 생성해 둔다.
2. HTTP 요청이 들어오면 pool 에 저장된 connection 객체를 가져다 쓰고 반환한다.
3. 위 방식으로 물리적인 데이터베이스 connection 부하를 줄이고 연결을 관리한다
( 다중 접속의 경우 커넥션 풀을 사용하게 되면 이미 connection 을 다른 곳에서 사용 중인 경우 사용자는 pool 이 connection 을 반환받아 제공해줄때 까지 대기 상태로 기다린다. )
4. pool 에 미리 connection 이 생성되어 있기 때문에 DB 에 접근할 때마다 connection 을 생성하지 않아 연결 시간이 소비되지 않는다.
▼
커넥션 풀(DBCP)를 사용한 코드
const oracledb = require("oracledb");
const dbConfig = require("../../../config/database/db_config");
oracledb.outFormat = oracledb.OBJECT;
oracledb.autoCommit = true;
let pool;
async function initialize() {
try {
pool = await oracledb.createPool(dbConfig);
console.log("Connection pool 생성됨");
} catch (error) {
console.error("Connection pool 생성 중 오류:", error);
}
}
initialize();
const get = {
have_KRW : async (member_id) => {
let connection;
try{
connection = await pool.getConnection();
let result = await connection.execute(`select money from member where member_id='${member_id}'`);
await connection.close();
return result;
}catch(err){
console.log(err);
}
},
// 매수했던 각 코인의 갯수를 Object 형태로 담는 함수
had_coin_num : async (member_id) => {
let connection;
try{
let had_coin_num = {
btc : 0,
eth : 0,
shib : 0,
bch : 0,
etc : 0,
btg : 0,
sol : 0,
doge : 0,
xrp : 0,
id : 0,
pundix : 0,
stx : 0,
aave : 0,
dot : 0,
avax : 0,
gas : 0,
sbd : 0,
ong : 0,
sei : 0,
ont : 0
};
let coin_list = ["btc","eth","shib","bch","etc","btg","sol","doge","xrp","id","pundix","stx","aave","dot","avax","gas","sbd","ong","sei","ont"];
connection = await pool.getConnection();
for(let i=0; i<coin_list.length; i++){
// 구매 코인의 총 갯수를 가진 DB 결과 Object
let coin_num_object = await connection.execute(`select coin_num from trade where coin_name='${coin_list[i]}' and member_id = '${member_id}' and status = 1`);
// 코인의 구매 이력이 없는 경우
if(coin_num_object.rows[0] === undefined){
had_coin_num[coin_list[i]] = 0;
continue;
}
let each_coin_num = 0;
coin_num_object.rows.forEach(coin => {
if(coin.COIN_NUM == null){
coin.COIN_NUM = 0;
}
each_coin_num += coin.COIN_NUM;
})
had_coin_num[coin_list[i]] = each_coin_num
}
await connection.close();
return had_coin_num;
}catch(err){
console.log(err);
}
},
// 매수했던 각 코인의 가격의 합을 Object 형태로 담는 함수
had_coin_cost : async (member_id) => {
let connection;
try{
let had_coin_cost = {
btc : 0,
eth : 0,
shib : 0,
bch : 0,
etc : 0,
btg : 0,
sol : 0,
doge : 0,
xrp : 0,
id : 0,
pundix : 0,
stx : 0,
aave : 0,
dot : 0,
avax : 0,
gas : 0,
sbd : 0,
ong : 0,
sei : 0,
ont : 0
};
let coin_list = ["btc","eth","shib","bch","etc","btg","sol","doge","xrp","id","pundix","stx","aave","dot","avax","gas","sbd","ong","sei","ont"];
connection = await pool.getConnection();
for(let i=0; i<coin_list.length; i++){
// 구매 코인의 총 갯수를 가진 DB 결과 Object
let coin_cost_object = await connection.execute(`select buy_cost from trade where coin_name='${coin_list[i]}' and member_id = '${member_id}' and status = 1`);
// 코인의 구매 이력이 없는 경우
if(coin_cost_object.rows[0] === undefined){
had_coin_cost[coin_list[i]] = 0;
continue;
}
let each_coin_cost = 0;
coin_cost_object.rows.forEach(coin => {
if(coin.BUY_COST == null){
coin.BUY_COST = 0;
}
each_coin_cost += coin.BUY_COST;
})
had_coin_cost[coin_list[i]] = each_coin_cost
}
await connection.close();
return had_coin_cost;
}catch(err){
console.log(err)
}
},
// 현재 보유 중인 코인의 갯수를 구하는 함수
have_coin_num : async (member_id) => {
let connection;
try{
connection = await pool.getConnection();
let have_coin_num = await connection.execute(`select btc,eth,shib,bch,etc,btg,sol,doge,xrp,id,pundix,stx,aave,dot,avax,gas,sbd,ong,sei,ont from member_account where member_id = '${member_id}'`);
await connection.close();
return have_coin_num.rows[0];
}catch(err){
console.log(err)
}
},
// 현재 코인의 가격 20개를 구하는 함수
now_coin_cost : async () => {
let connection;
try{
connection = await pool.getConnection();
let now_coin_cost_object = await connection.execute(`select * from (select * from coin_info order by order_column desc) where rownum <= 20`);
await connection.close();
return now_coin_cost_object.rows;
}catch(err){
console.log(err);
}
}
}
module.exports = {get}
initiallize() 함수를 사용하여 애플리케이션 시작 시 connection 정보가 담긴 pool 을 미리 변수로 저장해 놓은 뒤 사용
▼
오류 없이 정상적으로 데이터를 받아오는 모습
결론 : DB 에 지속적으로 접근하여 데이터를 받아와야 하는 경우, 혹은 다량의 DB 접속이 발생할 경우에는 connection pool 을 사용하자
보완할 점 : Connection pool 을 생성하여 사용할때 유휴 Connection 을 확인하여 커넥션의 갯수를 확보하고, 최대 Connection pool 의 갯수를 지정하여 연결의 수를 제한하여 사용할 것