열정 넘치는 팀원들과 함께한 덕분에 좋은 성과를 낼 수 있었던 것 같다.
찌리릿을 통해서 많은 것들을 배웠고 개발 외적으로도 개인적인 성장을 할 수 있었다.
Figma 를 활용해 브레인스토밍을 하고 와이어프레임을 만들었다.
네이버 스트리밍 플랫폼 치지직의 커뮤니티 서비스를 제공하는 것
게시판의 정책으로 8일 이상 스트리머 게시판 내 업데이트 된 게시글이 없다면 게시판을 비활성화 상태값으로 변경시켜 프론트에서 보이지 않도록 구현해야했다.
일정 주기로 실행시켜야하므로 스프링 스케쥴러를 활용했고, 직관적으로 쿼리를 확인하고 복잡한 쿼리를 쉽게 작성할 수 있는 QueryDsl 을 활용했다.
@Transactional
@Scheduled(cron = "0 0 0 * * *")
fun boardScheduler() {
val inactiveBoardIdList = boardRepository.findInactiveBoardStatus()
boardRepository.updateBoardStatusToInactive(inactiveBoardIdList)
}
대량의 데이터를 효율적으로 일괄 업데이트 처리하기 위해 더티 체킹 방식이 아닌 QueryDsl update 방식을 사용했다.
override fun findBoardStatusToInactive(): List<Long> {
val checkInactiveDate = LocalDateTime.now().minusDays(8)
return queryFactory.select(board.id).distinct()
.from(post)
.leftJoin(board)
.on(board.id.eq(post.board.id))
.where(post.modifiedAt.loe(checkInactiveDate), board.boardType.eq(BoardType.STREAMER_BOARD))
.fetch()
}
override fun updateBoardStatusToInactive(inactiveBoardIdList: List<Long>) {
queryFactory
.update(board)
.set(board.boardActStatus, BoardActStatus.INACTIVE)
.where(board.id.`in`(inactiveBoardIdList))
.execute()
}
scale-out 한 다른 어플리케이션 서버에서 구동 중인 2번째 스케쥴러가 동시 작업 진행 시 스케쥴러 수행 자체의 중복이 발생할 수 있는 문제점을 발견했다.
중복 실행에 대한 해결을 위해 ShedLock 을 이용해 스케쥴링 Lock 을 설정했다.
Shedlock 정보 관리를 위한 테이블 생성
CREATE TABLE shedlock(name VARCHAR(64) NOT NULL, lock_until TIMESTAMP NOT NULL,
locked_at TIMESTAMP NOT NULL, locked_by VARCHAR(255) NOT NULL, PRIMARY KEY (name));
Bean 설정
@Configuration
@EnableScheduling
@EnableSchedulerLock(defaultLockAtMostFor = "15m")
class ScheduleConfig {
@Bean
fun lockProvider(dataSource: DataSource): LockProvider {
return JdbcTemplateLockProvider(
JdbcTemplateLockProvider.Configuration.builder()
.withJdbcTemplate(JdbcTemplate(dataSource))
.usingDbTime()
.build()
)
}
}
@SchedulerLock 적용
@Transactional
@Scheduled(cron = "0 0 0 * * *")
@SchedulerLock(name = "boardStatus_lock", lockAtLeastFor = "14m", lockAtMostFor = "14m")
fun boardScheduler() {
LockAssert.assertLocked()
val inactiveBoardIdList = boardRepository.findInactiveBoardStatus()
boardRepository.updateBoardStatusToInactive(inactiveBoardIdList)
logger.debug { "scheduled task" }
}
스트리머 게시판 신청 시 치지직 스트리머임을 인증하는 이미지를 제출해야하는 정책이 있어 AWS S3 를 이용했다.
AWS 비밀 키를 백엔드에서 안전하게 관리하고 클라이언트에 노출시키지 않게해 보안을 강화하기 위해서 백엔드에서 S3를 관리하도록 결정했다.
파일의 확장자를 확인해 이미지 파일만 받을 수 있게 구현했다.
fun String.isImageFileOrThrow() {
val fileExtension = this.split(".").let { it[it.lastIndex] }
check(fileExtension.contains("png") or fileExtension.contains("jpeg") or fileExtension.contains("jpg")) {
throw RestApiException(ErrorCode.NOT_IMAGE_FILE_EXTENSION)
}
}
다른 도메인에서도 사용할 수 있도록 메서드로 구현했다.
@Service
class S3Service(
private val amazonS3Client: AmazonS3Client,
) {
@Value("\${cloud.aws.s3.bucket}")
private lateinit var bucket: String
fun uploadFiles(dir: String, files: List<MultipartFile>): List<String> {
val imageUrls = ArrayList<String>()
for (file in files) {
val randomFileName = "$dir/${UUID.randomUUID()}${LocalDateTime.now()}${file.originalFilename}"
file.originalFilename?.isImageFileOrThrow()
val objectMetadata = ObjectMetadata()
objectMetadata.contentLength = file.size
objectMetadata.contentType = file.contentType
try {
val inputStream: InputStream = file.inputStream
amazonS3Client.putObject(bucket, randomFileName, inputStream, objectMetadata)
val uploadFileUrl = amazonS3Client.getUrl(bucket, randomFileName).toString()
imageUrls.add(uploadFileUrl)
} catch (e: IOException) {
e.printStackTrace()
}
}
return imageUrls
}
}
다수의 사용자로부터 동시에 S3 서비스 요청이 들어올 경우 서버의 스레드 소진 위험 가능성이 존재하는 것을 발견했고 이로 인해 타임아웃이 발생될 수 있다.
이에 따라 스레드 풀 설정을 적절하게 하고 타임아웃 문제를 해결하기 위해 코루틴을 이용했다.
백그라운드 스레드로 별도의 스레드 풀을 사용함으로써 비동기 작업을 할 수 있게 withContext와 Dispatchers.IO 로 읽기/쓰기 작업에 스레드를 최적화시켰다.
await 함수를 사용해 비동기 작업이 완료될때까지 기다리고 결과를 반환받기위해 async를 활용했다.
suspend fun imageUploadWithCoroutine(dir: String, files: List<MultipartFile>) = withContext(Dispatchers.IO) {
val imageUrls = ArrayList<String>()
val uploadImage = files.map {
val randomFileName = "$dir/${UUID.randomUUID()}${LocalDateTime.now()}${it.originalFilename}"
val uploadFileUrl = amazonS3Client.getUrl(bucket, randomFileName).toString()
it.originalFilename?.isImageFileOrThrow()
val objectMetadata = ObjectMetadata().apply {
this.contentType = it.contentType
this.contentLength = it.size
}
async {
val putObjectRequest = PutObjectRequest(
bucket,
randomFileName,
it.inputStream,
objectMetadata,
)
amazonS3Client.putObject(putObjectRequest)
imageUrls.add(uploadFileUrl)
}
}
uploadImage.await()
return@withContext imageUrls
클라이언트에서 여러 장의 이미지 파일 자체를 백엔드로 데이터를 보내주면 aws에 저장되고 클라이언트에게 다시 링크를 넘겨주고 있는데 이 과정에서 서버의 부하가 생길 수 있는 문제가 있다.
그래서 서버를 경유하지 않고 프론트에서 직접 파일을 업로드 할 수 있는 aws S3 의 Presigned Url 정책을 통해 해결하기로 결정했다.
fun getPreSignedUrl(fileNames: List<MultipartFile>): Map<String, Serializable>? {
val encodedFileName = fileNames.let { "${it}_${LocalDateTime.now()}" }
val expiration = Date()
var expTimeMillis: Long = expiration.time
expTimeMillis += (3 * 60 * 1000).toLong() // 3분
expiration.time = expTimeMillis // url 만료 시간 설정
val generatePresignedUrlRequest: GeneratePresignedUrlRequest =
GeneratePresignedUrlRequest(bucket, "test/${encodedFileName}")
.withMethod(HttpMethod.PUT).withExpiration(expiration)
return mapOf(
"preSignedUrl" to amazonS3Client.generatePresignedUrl(generatePresignedUrlRequest),
"encodedFileName" to encodedFileName
)
}
응답속도 비교를 위해 이미지 업로드 컨트롤러를 따로 만들어 포스트맨을 이용해 테스트를 진행했다.
1001 ms -> 452 ms (54.8% 감소)
452 ms -> 250 ms (44.6% 감소)
프론트에서 S3를 관리하면 업로드 속도가 제일 빠르다는 것을 알 수 있다.
@PostMapping("/imageUpload")
fun uploadImage(@RequestParam multipartFile: List<MultipartFile>): String {
return s3Service.uploadFiles(dir = "test", multipartFile)
}
@PostMapping("/imageUploadWithCoroutine")
suspend fun imageUploadWithCotoutine(@RequestParam multipartFile: List<MultipartFile>): String {
return s3Service.imageUploadWithCoroutine(dir = "test", multipartFile)
}
@PostMapping("/imageUploadWithPreSignedUrl")
fun uploadImageWithPreSignedUrl(@RequestParam multipartFile: List<MultipartFile>): Map<String, Serializable>? {
return s3Service.getPreSignedUrl(multipartFile)
}
앱 출시를 위해서 ios 개발자 분들과 협업을 하게 됐다
구현하고 싶었던 이벤트 도메인과 성능개선을 진행하고 싶고 앱 개발자들이 원하는 api 와 프론트-백 연결하는 법이 궁금하다 웹이랑 많이 다른가 싶고
재밌게 협업해보고 싶다!