최종프로젝트 11일차 - 경기장 예약 완료, ORM 쿼리 복잡해질 때
전날 작업한다는 경기장 예약 화면은 다 만들었습니다. 하루 지나고 오늘까지 계속 작업한 끝에 화면 구현과 API 연동까지 마쳤습니다. 하지만 일련의 사건이 일어나게 되는데요.. 이 소식과 함께 오늘 작업한 현황과 ORM을 사용하며 쿼리가 복잡해질 때 대처방안에 대해 소개하겠습니다.
경기장 예약 화면 구현 그리고
전날 작업한 내용을 적어놓았습니다
전날 아니 다음날(오늘) 새벽까지 작업을 해도 끝내지 못한 경기장 예약 화면이었습니다. 아래 화면이 전날 작업한 사진인데 경기장 목록은 다 만들었지만 문제는 여기서 경기장별 일정 확인 시 나타나는 화면이었습니다.
일정화면을 누르면 아래처럼 경기장 신청이 가능하도록 화면을 만드려고 했는데 전날은 다 마무리 하지 못했습니다. 하루가 넘어가고 점점 더 피곤해지면서 잠을 택했기 때문입니다.
자고나서 오늘이 되어 하루 반나절을 계속 매달린 끝에 경기장 예약 화면도 마무리 할 수 있었습니다.
완성을 마친 경기장 예약화면
현재까지 만든 경기장 예약 화면은 바로 이렇습니다.
경기장 썸네일과 경기장 정보, 달력과 아래에는 상태팀 구단을 드롭다운으로 선택할 수 있게 하고 선택에 따른 팀 로고가 드롭다운 아래에 보여지게 됩니다.(왼쪽 로고가 본인 팀, 오른쪽 로고가 상대팀입니다) 또한, 오른쪽에는 달력에서 클릭한 날짜에 따른 예약 가능한 시간대를 볼 수 있습니다. 예약 가능한 시간대이면 아래처럼 예약 가능 글씨가 써지게 되며, 클릭하면 아래 사진처럼 해당 시간대가 선택되는 것을 볼 수 있습니다.
위의 화면을 예로들어 만약 난지천인조잔디축구장에 1월 25일 18:00:00자로 이미 예약되어 있으면 위 리스트에서 18:00:00은 클릭할 수 없게 비활성화 되어 조회되게 됩니다. 이처럼 화면 구현 뿐만 아니라 화면 뒤에서 이루어지는 API 연동까지 모두 마쳤습니다.
마지막으로 필요 항목 전부 입력한 후 '경기 요청' 버튼을 클릭하게 되면 '경기 요청하시겠습니까?' 라는 알림창이 한 번 뜨면서 여기서 확인 클릭하면 최종적으로 상대팀 구단에게 경기 수락 여부를 묻는 이메일이 전송됩니다.
그러면 상대팀 이메일에는 아래와 같은 경기 제안에 따른 수락을 묻는 이메일을 받게 되고 여기서 수락 클릭시 최종적으로 경기 예약이 이루어지게 됩니다.
경기장 정보에서 mysql로 넣는 작업도 완료
전날은 서울시 공공API에서 필요한 데이터만 console.log로 찍어보기만 했다면 오늘은 mysql에 있는 테이블 안으로 데이터 넣는 코드까지 만들었습니다. 테이블에 자료를 넣으려고 보니 테이블 하나가 아니라 두개를 넣어야 해서 조금 헤매고 있었지만 방법을 찾는데는 그리 오래 걸리지 않았습니다. 작성한 코드는 아래와 같습니다.
import axios from 'axios';
import { parseString } from 'xml2js';
import { getAddressFromCoordinates } from './coordToAddr.js';
import { config } from 'dotenv';
import mysql from 'mysql';
// 환경 변수 파일(.env)을 로드
config();
// MySQL 데이터베이스 연결 설정
const db = mysql.createConnection({
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_DATABASE
});
// 데이터베이스 연결
db.connect(err => {
if (err) {
console.error('Database connection failed: ' + err.stack);
return;
}
console.log('Connected to database.');
});
const url = `http://openapi.seoul.go.kr:8088/${process.env.SEOUL_FIELD_API_KEY}/xml/ListPublicReservationSport/1/300/축구장`;
axios.get(url)
.then(response => {
const xml = response.data;
const parseXml = xml => {
return new Promise((resolve, reject) => {
parseString(xml, (err, result) => {
if (err) reject(err);
else resolve(result);
});
});
};
return parseXml(xml); // XML 파싱 Promise 반환
})
.then(async result => {
const rows = result.ListPublicReservationSport.row;
const queries = [];
const processedAddresses = new Set(); // 처리된 주소를 추적하기 위한 Set
for (const item of rows) {
const address = await getAddressFromCoordinates(item.Y[0], item.X[0]);
// 이미 처리된 주소인 경우 건너뛰기
if (processedAddresses.has(address)) {
continue;
}
processedAddresses.add(address); // 주소를 처리된 주소 Set에 추가
const addressParts = address.split(' ');
// 주소 처리 및 soccer_fields 테이블에 정보 삽입
const queryPromise = new Promise((resolve, reject) => {
const checkQuery = 'SELECT * FROM location WHERE address = ?';
db.query(checkQuery, [address], (err, results) => {
if (err) reject(err);
let locationId;
if (results.length === 0) {
// location 테이블에 새 주소 삽입
const insertLocationQuery = 'INSERT INTO location (state, city, district, address) VALUES (?, ?, ?, ?)';
db.query(insertLocationQuery, [addressParts[0], addressParts[1], addressParts[2], address], (err, results) => {
if (err) reject(err);
locationId = results.insertId; // 새로 삽입된 주소의 ID
insertSoccerFields(locationId);
});
} else {
locationId = results[0].id; // 기존 주소의 ID
insertSoccerFields(locationId);
}
function insertSoccerFields(locationId) {
// soccer_fields 테이블에서 locationId로 데이터가 있는지 확인
const checkSoccerFieldQuery = 'SELECT * FROM soccer_fields WHERE location_id = ?';
db.query(checkSoccerFieldQuery, [locationId], (err, results) => {
if (err) reject(err);
if (results.length === 0) {
// soccer_fields 테이블에 해당 locationId가 없으면 새로운 데이터 삽입
const insertSoccerFieldsQuery = 'INSERT INTO soccer_fields (location_id, field_name, image_url, district, phone_number, x_coord, y_coord) VALUES (?, ?, ?, ?, ?, ?, ?)';
db.query(insertSoccerFieldsQuery, [locationId, item.PLACENM[0], item.IMGURL[0], item.AREANM[0], item.TELNO[0], item.X[0], item.Y[0]], (err, results) => {
if (err) reject(err);
console.log(`New soccer field inserted for location ID ${locationId}: ${item.PLACENM[0]}`);
resolve();
});
} else {
// 이미 존재하는 경우, 삽입하지 않고 넘어감
console.log(`Soccer field already exists for location ID ${locationId}. Skipping insertion.`);
resolve();
}
});
}
});
});
queries.push(queryPromise);
}
// 모든 쿼리가 완료될 때까지 기다린 후 연결 종료
return Promise.all(queries);
})
.then(() => {
console.log('End to database.');
db.end(); // 모든 작업 후 데이터베이스 연결 종료
})
.catch(error => {
console.error(error);
db.end(); // 에러 발생 시에도 연결 종료
});
구체적인 단계는 다음과 같습니다
- 환경 설정 파일 로딩: .env 파일에서 환경 변수들을 로드합니다. 이 환경 변수들은 데이터베이스 연결과 API 키 정보 등을 포함합니다.
- MySQL 데이터베이스 연결: 환경 변수를 사용하여 MySQL 데이터베이스에 연결합니다. 연결 실패 시 에러 메시지를 출력하고, 성공 시 데이터베이스 연결 완료 메시지를 출력합니다.
- 축구장 데이터 요청: 서울시 공공 스포츠 예약 시스템의 API를 사용하여 축구장 데이터를 요청합니다. 이 때 사용되는 API 키는 환경 변수에서 가져옵니다.
- XML 데이터 파싱: API로부터 받은 데이터는 XML 형식이므로, 이를 JSON 형식으로 변환하기 위해 xml2js 라이브러리를 사용합니다.
- 축구장 데이터 처리 및 데이터베이스 삽입: 변환된 JSON 데이터를 사용하여 각 축구장의 정보를 처리합니다. 각 축구장의 좌표를 사용하여 주소를 얻고, 이 주소를 데이터베이스의 location 테이블에 저장합니다. 이미 데이터베이스에 해당 주소가 존재하는 경우, 새로운 주소를 추가하지 않습니다.
- 축구장 정보 삽입: location 테이블에 주소가 존재하거나 새로 추가된 후, 해당 위치의 축구장 정보를 soccer_fields 테이블에 삽입합니다. 만약 이미 해당 위치에 대한 축구장 정보가 존재한다면, 중복 삽입을 피하기 위해 넘어갑니다.
- 모든 작업 완료 후 데이터베이스 연결 종료: 모든 축구장 데이터 처리가 완료되면, 데이터베이스 연결을 종료합니다. 만약 중간에 에러가 발생하면, 에러 메시지를 출력하고 데이터베이스 연결을 종료합니다.
전반적으로 이 코드는 외부 API로부터 축구장 데이터를 가져와서, 주소 정보를 처리하고, 이를 통해 축구장 정보를 데이터베이스에 저장하는 프로세스를 수행합니다.
작업을 마치고..
작업을 마치고 수정한 코드들을 깃허브에 올렸습니다. 그리고 그간 못받은 다른 팀원들 작업이 담겨 있는 main 브랜치에 있는 코드들을 pull했습니다. 그러더니 갑자기 로그인이 안되기 시작했습니다. 아이디 비밀번호 맞게 적었는데 자꾸 401 에러가 뜨는게 왜그런가 싶었는데 로그인쪽 담당한 팀원이 수정하다가 오류가 발생했다는 말을 한걸 그제서야 떠오랐습니다.
이미 필자 자료는 merge 한 상태였고, (merge 간에 이상이 없었던건, 필자 파일들은 다른 팀원들이 수정할 일 없던 파일들이었기에 문제 없이 자료를 올릴 수 있었습니다) main 브랜치에 오류가 있었다는 걸 들었었는데도 그걸 잊은채 main 자료를 받아와 필자 프로젝트에 덮어 씌웠으니.. 조금 난감해졌습니다.
이 오류가 신기한게 401 오류가 나는데 어디서 나는지를 필자도 못찾겠습니다. 그렇다고 섣불리 수정하다가 팀원과 충돌 이슈가 일어날 수도 있기에 지켜보기로 했습니다. 내일 오전 팀원이 어떻게 진행했는지에 따라 내일 일정이 달라질 듯 합니다.
ORM 사용하며 쿼리가 복잡해질 때
ORM이란
ORM (Object-Relational Mapping)으로 객체와 관계형 데이터베이스 매핑, 객체와 DB의 테이블이 매핑을 이루는 것을 말합니다. SQL Query가 아닌 직관적인 코드(메서드)로서 데이터를 조작할 수 있다는 장점이 있습니다. 객체와 쿼리를 분리하여 복잡도를 줄이고, 트랜잭션 처리나 기타 데이터베이스 관련작업들을 좀 더 편리하게 처리할 수 있는 방법입니다.
해결책 1. JPA의 Native Query
우선 JPA란 Java Persistence API로 자바 ORM기술에 대한 API표준 명세로 쉽게 말해 ORM을 사용하기 위한 인터페이스를 모아둔 것을 말합니다. (데이터베이스와 객체 지향 프로그래밍 사이의 매핑을 쉽게 해주는 API)
JPA의 장점
- 생산성이 뛰어나고 유지보수가 용이하다. 객체 중심 설계에 더 집중할 수 있기 때문이다.
- DBMS에 대한 종속성이 줄어든다.⇒ DB쿼리문을 코드단에서 해결이 가능하다. DBMS에서 DB만 생성하고, 테이블 생성&관리등 많은 부분들을 코드단에서 관리가 가능하다.
JPA의 단점
- JPA를 학습하려면 많은 노력이 든다.
- 복잡한 쿼리를 사용할 경우 불리하다.⇒ 하나의 테이블에서는 이 정보, 다른 테이블에서는 이 정보를 가져와 하나의 데이터 객체로 만들기가 어렵다. SQL문에서는 join함수로 쿼리 한 줄이면 끝난다.
- 잘못 사용하면 성능이 떨어질 수도 있다.
Native Query란
Native Query는 JPA 에서 직접 SQL 을 작성한 쿼리로, JPA를 사용하는 애플리케이션에서도 특정 상황에서 필요한 복잡하거나 특수한 쿼리를 실행할 수 있게 해줍니다.
Native Query 장점
- 유연성과 성능: Native Query는 데이터베이스에 직접 작성되므로, JPA의 일반 쿼리보다 더 복잡하고 세밀한 쿼리를 작성할 수 있어 유연성과 성능 측면에서 우수합니다.
- 복잡한 조인 및 서브쿼리 처리: ORM에서는 구현하기 어려운 복잡한 조인이나 서브쿼리도 Native Query를 사용하면 쉽게 처리할 수 있습니다.
Native Query 단점
- 플랫폼 종속성: Native Query는 특정 데이터베이스 시스템에 특화된 쿼리를 사용하기 때문에, 데이터베이스 시스템이 변경될 경우 쿼리를 재작성해야 할 수 있습니다.
- 유지보수의 어려움: JPA의 ORM 기능을 사용하지 않기 때문에, 객체와 데이터베이스 테이블 간의 매핑을 수동으로 관리해야 하며, 이는 유지보수를 복잡하게 만듭니다.
- 일관성 유지 문제: JPA의 캐싱과 같은 기능과의 연동이 원활하지 않을 수 있어, 일관성 유지가 어려워질 수 있습니다.
해결책 2. 저장 프로시저 사용
- 정의: 저장 프로시저는 데이터베이스에 미리 작성되어 저장된 일련의 SQL 명령어들입니다. 이러한 프로시저는 복잡한 로직이나 연산을 포함하고 있으며, 필요할 때 호출되어 실행됩니다.
- ORM과의 결합: ORM에서는 저장 프로시저를 호출하는 기능을 제공합니다. 복잡한 쿼리나 트랜잭션 처리가 필요한 경우, ORM을 통해 저장 프로시저를 호출하여 데이터베이스의 로직을 실행할 수 있습니다.
- 장점: 저장 프로시저는 데이터베이스 서버에서 직접 처리되므로, 네트워크 트래픽을 줄이고 전체 성능을 향상시킬 수 있습니다. 또한, 보안 측면에서도 코드의 노출을 줄이는 장점이 있습니다.
해결책 3. 캐싱 사용
- 정의: 캐싱은 자주 사용되는 데이터를 임시 저장소에 보관하여, 동일한 요청에 대해 빠르게 응답할 수 있게 하는 기술입니다.
- ORM과의 결합: 많은 ORM 프레임워크는 내부적으로 캐싱 기능을 제공합니다. 이를 통해 데이터베이스에서 이미 조회한 데이터를 캐시에 저장하고, 동일한 요청이 있을 때는 캐시에서 데이터를 빠르게 가져올 수 있습니다.
- 장점: 캐싱을 사용하면 데이터베이스의 부하를 줄이고, 응답 시간을 단축시킬 수 있습니다. 특히, 읽기 위주의 애플리케이션에서 성능 향상에 크게 기여할 수 있습니다.
이밖에도 다양한 방법
▼ 이전 진행한 프로젝트들 ▼
'내일배움캠프 > 축구팀 관리 프로젝트' 카테고리의 다른 글
축구팀 관리 프로젝트 13일차 - 포메이션 관리 화면, ts(2339) 오류 (1) | 2024.01.25 |
---|---|
최종프로젝트 12일차 경기 일정 조회 작업, 쿼리 최적화에 대해 (0) | 2024.01.23 |
최종프로젝트 10일차 - 경기장 예약 화면 작성 중, 좌표에서 주소 변환 (0) | 2024.01.21 |
최종프로젝트 9일차 - 리엑트 공부 시작, 리엑트 부트스트랩 사용 (1) | 2024.01.20 |
최종프로젝트 8일차 - 끝도 없는 API 수정, 리엑트 디자인 추천 사이트 (0) | 2024.01.19 |