1. 개요
- Carryduo를 지속적으로 운영하면서, 이미지 관리에 대한 고민이 생겼다.
- Carryduo는 롤 듀오 데이터를 제공해주는 사이트이므로, 자연스레 롤 챔피언/스킬/티어 등 다양한 이미지들을 사용한다. 총 개수는 대략 1100개 가량 된다.
- 기존에는 그 이미지들을 라이엇에서 제공하는 이미지 url을 그대로 가져와서 사용했었다. (라이엇 이미지 url 예시)
- 롤을 플레이하면서 문득 든 생각이 "만약 라이엇 서버가 다운되면, 우리 사이트에서 보여지는 이미지들이 모두 오류 처리가 나겠구나"였다.
- 그래서 사이트의 안정적인 운영을 위해, 이미지를 사이트 자체적으로 관리할 필요성을 느껴, AWS S3에 라이엇 이미지를 자체적으로 저장하고 이를 FE에 제공하는 방식을 도입하기로 하였다.
* S3 관련 내용만 확인한다면, 4번 목차부터 확인하면 됩니다.
2. 작업 전 고려 사항
- 단순하게 라이엇에서 제공하는 이미지 파일들을 AWS S3에 저장해서, 이를 고정적으로 FE에 전달하면 작업이 편하겠지만, 라이엇은 주기적으로 롤 챔피언 업데이트, 리메이크, 리워크 패치를 진행한다.
- 즉, 이러한 패치 업데이트에 대응해서 주기적으로 AWS S3에 저장된 이미지들을 변경해줘야 했다.
- 이를 고려해, Carryduo에서 이미지 파일을 관리하는 프로세스는 다음과 같이 설정했다.
1. 라이엇 패치버전과 Carryduo에서 제공하고 있는 롤 데이터의 패치버전을 비교한다.
2. 라이엇 패치버전이 Carryduo에서 제공하는 패치버전보다 상위 버전이면, AWS S3에 새 패치버전 이미지를 저장한다.
3. 새 패치버전 이미지를 저장하면, 이전 패치버전 이미지는 삭제한다.
* 위 프로세스를 이탈할 상황에는 default 이미지를 삽입하여, 사이트에서 이미지 오류가 발생하지 않도록 한다.
- 이하 내용은 위 프로세스 순서에 따라 서술된다.
3. 라이엇 패치버전과 Carryduo의 패치버전 비교
- 롤과 관련한 대부분의 정보를 제공하고 있는 라이엇 API에서 여태까지 제공한 롤 패치버전들을 JSON 형식으로 제공하고 있다.(라이엇 API 공식문서)
- 다음 코드와 같이, axios를 이용해 라이엇에서 제공하고 있는 최신 패치버전과 현재 Carryduo에서 제공하고 있는 버전을 조회해서 그 값 크기를 비교했다.
- 한편, 라이엇 패치버전은 12.23 -> 13.1 -> 13.2 ... -> 13.9 -> 13.10 과 같은 순서로 패치 업데이트를 진행한다. 즉, 13.10이 13.9보다 높은 패치버전인 것이다. 코드 상에서는 13.9가 더 높은 값이기 때문에, 이를 처리해주기 위한 로직을 구현했다. 소수점 상위 숫자를 먼저 비교하고, 이후에 소수점 이후 숫자를 비교하는 것이다.
- 아래 함수에서 return으로 받는 version은 새 패치버전 이미지를 요청하는 데, oldVersion은 이전 패치버전 S3 폴더를 삭제하는데 활용된다.
exports.checkVersion = async () => {
try {
let param, version, oldVersion
// 라이엇에서 제공하는 패치 버전 조회
const riotVersion = await axios('https://ddragon.leagueoflegends.com/api/versions.json')
// 라이엇에서 제공하는 최신 패치버전
const recentRiotVersion = riotVersion.data[0].slice(0, 5)
// Carryduo에서 제공하는 패치버전 조회
const originData = await findVersion_combination_service()
const dbVersionList = getRecentDBversion(originData)
const dbVersion = dbVersionList[0]
// 패치버전 크기 비교
const recentRiotVersion_year = Number(recentRiotVersion.split('.')[0])
const dbVrsion_year = Number(dbVersion.split('.')[0])
if (recentRiotVersion_year > dbVrsion_year) {
param = 1
version = recentRiotVersion
oldVersion = dbVersion
} else if (recentRiotVersion_year === dbVrsion_year) {
const recentRiotVersion_week = Number(recentRiotVersion.split('.')[1])
const dbVersion_week = Number(dbVersion.split('.')[1])
if (recentRiotVersion_week > dbVersion_week) {
param = 1
version = recentRiotVersion
oldVersion = dbVersion
} else if (recentRiotVersion_week === dbVersion_week) {
param = 0
version = recentRiotVersion
oldVersion = dbVersion
} else {
param = 2
version = recentRiotVersion
oldVersion = dbVersion
}
} else {
param = 2
version = recentRiotVersion
oldVersion = dbVersion
}
if (version[version.length - 1] === '.') {
version = version.slice(0, -1)
}
if (oldVersion[oldVersion.length - 1] === '.') {
oldVersion = oldVersion.slice(0, -1)
}
// 패치버전 비교 상태에 따라 param과 그에 따른 패치버전들을 retrun
return { param, version, oldVersion }
} catch (err) {
logger.error(err, { message: '-from checkVersion' })
return err
}
}
4. 새 패치버전일 경우, AWS S3에 새 패치버전 이미지 저장
4-1. 라이엇 API에 새 패치버전에서 제공하는 모든 챔피언 이미지 요청하기
- 위와 마찬가지로, 라이엇 API에서는 롤의 모든 챔피언에 대한 정보를 JSON 형식으로 제공한다. 이 때, 패치버전이 paramater로 활용된다.
- Carryduo에 필요한 이미지는 챔피언 이미지 두 종류, 챔피언의 스킬/패시브 이미지였다.
- 이미지 파일의 데이터를 저장하기 위해서는 이미지의 buffer 값을 저장해야하는데, 이를 라이엇 API로부터 응답받기 위해서는 다음과 같은 순서가 필요하다.
1. 라이엇이 제공하는 모든 챔피언별로 챔피언 이미지, 스킬/패시브 이미지 paramter 값을 뽑는다.
2. 라이엇에서 이미지를 제공하는 url에 해당 parameter 값으로 요청을 보내, 이미지 파일의 buffer 값을 받는다.
- 예시 코드는 다음과 같다. fetch를 이용할 수도 있지만, node.js에서 fetch를 사용하기 위해서는 node-fetch 패키지를 다운받아야 하며, 이미 기존에 axios를 사용하고 있었으며, 이것이 코드 가독성이 더 좋다고 느껴 axios를 활용했다.
// 1. 라이엇에 모든 챔피언에 대한 정보를 요청하여, 챔피언 상세정보 조회를 위한 parameter 뽑기.
const riotResponse = await axios(`https://ddragon.leagueoflegends.com/cdn/${version}.1/data/ko_KR/champion.json`)
const riotChampList = Object.keys(riotResponse.data.data)
// 2. 챔피언 상세정보 조회 parameter로 챔피언 상세정보 요청
for (let i = 0; i < riotChampList.length; i++) {
const champName = riotChampList[i]
const originData = await axios(`https://ddragon.leagueoflegends.com/cdn/${version}.1/data/ko_KR/champion/${champName}.json`)
// 3. 이미지 파일 데이터 요청을 위한 paramter 뽑기
const champCommonImgKey = originData.data.data[`${champName}`].image.full
const champMainImgKey = `${champName}_0`
// 4. 챔피언 기본 이미지 요청 url에 챔피언 이미지 paramter를 삽입하여 요청.
const champCommonImgData = await axios({ url: `http://ddragon.leagueoflegends.com/cdn/${version}.1/img/champion/${champCommonImgKey}`, responseType: 'arraybuffer' })
const champMainImgData = await axios({ url: `http://ddragon.leagueoflegends.com/cdn/img/champion/loading/${champMainImgKey}.jpg`, responseType: 'arraybuffer' })
// 5. 챔피언 이미지별 buffer 데이터
const champCommonImg = champCommonImgData.data
const champMainImg = champMainImgData.data
}
4-2. 이미지 S3에 업로드하기
1) S3 버킷 생성
- AWS S3는 정적 데이터 관리를 위해 사용되는 툴이다.
- S3에 저장되는 데이터들은 Object 개념으로 관리되는데, 이 때 Object들에 대한 엑세스 권한을 Bucket 차원에서 ACL, , IAM, Bucket Policy로 설정할 수 있다.
S3에 대한 엑세스 권한은 ACL, BUCKET POLICY, IAM을 통해 조절할 수 있다. 관련해서는 다음 링크를 참조해보면 좋다.
요약하자면, 다음과 같은 엑세스 권한 상황에서 각 방법을 이용하면 좋은 듯 하다.
- BUCKET POLICY: 버킷 단위의 엑세스 권한 제한
- ACL: 객체 단위의 엑세스 권한 제한
- IAM: 사용자 단위의 엑세스 권한 제한
- 필자는 현재 시점에서는 Bucket에 대한 다른 AWS, IAM 계정에 엑세스 권한을 부여할 필요가 없으며, 오직 Object인 이미지 파일들에 대한 불특정 유저들의 엑세스 권한만 부여하면 됐다.
- 그래서 필자는 ACL를 활용하여 Object들에 대한 read 엑세스 권한만 활성화시켰다.(Bucket Policy에서 버킷에 대한 모든 read 엑세스 권한만 활성화시켜도 된다.)
- 위와 같은 맥락으로 S3 버킷을 생성한 흐름을 보자면 다음과 같다.
2) aws-sdk를 이용해 nodeJS 환경에서 S3에 파일 업로드하기
- nodeJS 환경에서 S3를 조작하기 위해서는 aws-sdk를 이용할 수 있다.
(1) S3에 접근하기
const aws = require('aws-sdk')
aws.config.update({ region: 'ap-northeast-2' })
const s3 = new aws.S3({ apiVersion: '2006-03-01' })
- 기본적으로 코드 상에서 aws 서비스에 접근하기 위해서는 접근하기 위한 계정의 access_key_id와 secret_access_key가 필요한데, 위 코드의 config에서는 이것이 없는 것을 확인할 수 있다.
- 이는 aws-sdk 모듈 자체적으로 .getenv()를 통해 프로젝트에 내장된 env에 접근하기 때문에, 굳이 config에서 이를 서술할 필요가 없는 것이다.(출처)
- 한편, region이 설정되어 있는데, 이는 추후에 설명될 aws-sdk 문법 중 하나인 await .promise()를 사용하는 중 간혹 region이 잘못 요청되는 경우를 방지하기 위해 설정해주었다.
(2) S3에 파일 업로드하기
const params = {
Bucket: `${process.env.BUCKET}/${version}`,
Key: `${champMainImgKey}.jpg`,
Body: champMainImg,
ACL: 'public-read',
ContentType: 'image/jpeg',
}
s3.upload(params, (err, result) => {
if (err) return err
return
})
- s3에 파일을 업로드할 때는 upload메소드를 이용하면 된다. 이 때, 업로드할 파일의 경로, ACL, ContentType, 객체 key 값 등을 param으로 설정해주어야 한다.
- Bucket: 파일을 저장할 S3 Bucket 이름
- Key: 파일을 저장할 object key의 이름
- Body: Key에 저장할 데이터(axios로 받은 데이터는 arrayBuffer 형식이다)
- ACL: 설정할 ACL의 속성(모든 유저가 READ만 가능하게 할 것이므로, public-read로 설정했다)
- ContentType: arrayBuffer 형식의 데이터를 ContentType을 지정하지 않으면 application/octet-stream 형태로 지정이 되는데, 이럴 경우, 파일이 열람되는 것이 아니라 default로 다운로드를 하게 된다. 이에, 이미지 파일에 맞게 image/png와 image/jpeg로 설정하였다.
5. 새 패치버전일 경우, 이전 패치버전 S3 폴더 삭제하기
- S3는 UI 상으로는 폴더와 폴더 안의 파일 개념으로 보이지만, 사실 폴더/파일 구분없이 S3 버킷에 들어있는 모든 것들은 다 Object로 간주된다.
- 이에, S3에서 특정 폴더를 삭제하기 위해서는 해당 폴더(Object)만을 삭제하는 것이 아니라, 하위 경로의 모든 Object들을 삭제해주어야 한다.
- 한편, S3에서 listObjects 메소드를 통해 조회할 수 있는 최대 Key는 1000개인 것으로 파악된다. MaxKeys라는 parameter를 listObjects에 삽입할 수 있는데, 삽입되지 않는다면 default로 1000이며, 1000 이상 숫자는 적용되지 않는 것을 확인할 수 있었다.
- Carryduo에서 한 패치버전 S3 폴더에 저장되는 이미지는 약 1100개 이상이고, 롤 패치가 지속적으로 이루어진다면, 개수는 더 늘어날 것이다.
- 이에, 필자는 다음과 같은 프로세스로 이전 패치버전의 S3 폴더를 삭제시키는 로직을 구현했다.
1. 패치버전의 S3 폴더에 있는 Object를 조회한다.
2. Object의 Key값을 paramater로 활용하여, 삭제시킨다.
3. 패치버전의 S3 폴더에 남은 Object가 있는지 한번 더 조회한다.
4. 남은 Object가 있다면 함수를 재실행하고, 없다면 마무리한다.
- 코드 상으로는 다음과 같다.
exports.deleteOutdatedS3Bucket = async (oldVersion) => {
try {
// 버킷에 들어있는 Object 조회
const data = await getObjectsFromS3Bucket(oldVersion)
// Object Key 값을 params에 넣어 delete 실행
for (let i = 0; i < data.Contents.length; i++) {
const params = {
Bucket: `${process.env.BUCKET}`,
Key: data.Contents[i].Key
}
s3.deleteObject(params, (err, result) => {
if (err) return err
})
}
// 남은 Object 체크
const status = await getObjectsFromS3Bucket(oldVersion)
// 남은 Object가 없다면 종료, 있다면 함수 재실행
if (status.Contents.length !== 0) {
logger.info(`${oldVersion} 폴더 삭제를 재실행합니다`)
await this.deleteOutdatedS3Bucket(oldVersion)
} else {
return
}
} catch (err) {
logger.error(err, { message: '- from deleteOutdatedS3Bucket' })
return err
}
}
// 버킷에 들어있는 Object 조회하는 함수
async function getObjectsFromS3Bucket(oldVersion) {
return await s3.listObjectsV2({ Bucket: `${process.env.BUCKET}`, Prefix: `${oldVersion}/` }).promise()
}
- 여기서 유의할 문법은 aws-sdk의 await .promise() 문법이다. aws-sdk는 aws에 요청을 보내는 모듈이기 때문에, 상황에 따라 비동기 처리를 해주어야 한다.
- 이를 위해 필자는 단순히 await만 붙였는데, 적용되지 않는 것을 확인할 수 있었다.
- aws-sdk에서 비동기 처리를 위해서는 이것이 제공하는 .promise()를 사용해야 했다.
- await .promise()와 관련되어 두 가지 오류를 만날 수 있었는데, 다음과 같다.
- AuthorizationHeaderMalformed: The authorization header is malformed; the region 'us-east-1' is wrong; expecting 'ap-northeast-2
=> aws confg 당시에 region을 설정해주지 않았을 때 발생한다.
- Cannot read properties of undefined (reading 'push')
=> promise()를 사용하는데, 또 다른 콜백함수가 코드에 포함되어 있어서 발생했다. 예시 코드는 다음과 같다.
=> 이미 promise() 처리를 했는데도 또 다른 콜백함수를 호출해서 발생한 문제인 것으로 추론된다.
await s3.listObjects(params, (err, data) => {
if(err) return err
}).promise()
6. 출처
- Riot API
- AWS S3
'Carryduo' 카테고리의 다른 글
Carryduo | 계층 분리를 위한 DTO, Entity 리팩토링 (0) | 2023.03.13 |
---|---|
Carryduo | AWS CodeDeploy 배포본 수 줄이기 (0) | 2023.02.06 |
Carryduo | 비동기와 try, catch 에러 핸들링 (0) | 2023.01.27 |
Carryduo | [nodeJS] child-process를 이용한 데이터 분석 자동화 (0) | 2023.01.26 |
Carryduo | ubuntu crontab을 이용한 pm2 로그 관리 스케줄링 (1) | 2023.01.25 |