- 공동구매 플랫폼인 ByTogether Auth 및 User모듈의 이미지 구성
AWS구성을 이용한 이미지 처리 - S3, Lambda, AWS Titan
- 백엔드 플랫폼 - Spring Boot
1. S3 파일 업로드 및 교체
1) Java SpringBoot
S3 Client 구성 ➡ 저장 객체 구성 ( 업로드 환경, 파일 구성 ) ➡ 객체 저장
(1) 의존성 및 Config구성
//aws의존성 ( build.gradle )
implementation 'io.awspring.cloud:spring-cloud-aws-starter-s3:3.0.3'
//s3 Config 및 Client구성
@Configuration
public class S3Config {
@Value("${spring.cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${spring.cloud.aws.credentials.secret-key}")
private String secretKey;
@Value("${spring.cloud.aws.region.static}")
private String region;
@Bean
public S3Client s3Client(){
AwsBasicCredentials awsBasicCredentials = AwsBasicCredentials.create(accessKey, secretKey);
return S3Client.builder()
.region(Region.of(region))
.credentialsProvider(StaticCredentialsProvider.create(awsBasicCredentials))
.build();
}
}
(2) 업로드 설정
filename구성 - PutObjectRequest로 송신용 객체 구성 - 객체 송신 ( 관련 Exception자동처리 )
//파일 구성
String filename = String.format(
"/static/user/image/%s_%s",
UUID.randomUUID(),file.getOriginalFilename()
);
//저장 객체 구성 (bucket, key, 파일 형식 등)
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
.bucket(bucketname)
.key(filename)
.contentType(file.getContentType())
.contentDisposition("inline")
.build();
//s3 Client로 저장 처리 (저장 객체와 데이터 ( 파일 스트림과 파일크기 )
s3Client.putObject(
putObjectRequest,
RequestBody.fromInputStream(file.getInputStream(), file.getSize())
);
//s3 Client로 저장된 이미지 삭제
s3Client.deleteObject(builder ->
builder.bucket(bucketname).key(oldAvatarString));
2. S3 업로드 파일로 썸네일 만들기 ( with Lambda)
S3 이미지 업로드 (Trigger) ➡ Lambda수행 (썸네일 생성) ➡ S3에 썸네일 이미지 업로드
1) Trigger 및 권한 설정
(1) s3에 Lambda에 대한 Trigge설정 ( 관련 권한 자동 설정 )
(2) Lambda에 s3에 대한 권한 추가 (getObject, putObject)
- "역할 이름"으로 접근 후 권한 정책에서 인라인 정책으로 권한 추가


2) Lambda 구성
import { S3Client, GetObjectCommand, PutObjectCommand } from "@aws-sdk/client-s3";
import { streamToBuffer } from "./utils.mjs";
import sharp from "sharp";
const s3Client = new S3Client({region: 'ap-northeast-2'});
const WIDTH = 200;
const HEIGHT = 200;
export const handler = async (event) => {
try {
const bucket = event.Records[0].s3.bucket.name;
for (const record of event.Records) {
//from static/user/image/userId/uuid_originalFilename
//to static/user/thumbnail/userId/uuid_originalFilename
const key = decodeURIComponent(record.s3.object.key.replace(/\+/g, ' '));
const path = "static/user/thumbnails/";
console.log(`Processing: ${key}`);
// userId
let length = "static/user/images/".length;
let index = key.indexOf("/", length); //length에서 첫번째 /까지
const userId = key.substring(length, index); //userid 추출
const filename = key.substring(index + 1);
const filenameWithoutExtension2 = filename.substring(filename.lastIndexOf(".")) // .png
const newFileName = filename.replace(filenameWithoutExtension2,"");
const filenameWithoutExtension = filename.split('.')[0]
const toSaveKey = `${path}${userId}/${newFileName}.jpg`;
console.log("### to save key");
console.log(toSaveKey);
// 원본 이미지 가져오기
const s3Object = await s3Client.send(new GetObjectCommand({
Bucket: bucket,
Key: key
}));
const imageBuffer = await streamToBuffer(s3Object.Body);
// 이미지 리사이징
const resizedImage = await sharp(imageBuffer)
.resize(WIDTH, HEIGHT, {
fit: 'cover', // 비율 유지하면서 크롭
position: 'center'
})
.jpeg({quality: 85})
.toBuffer();
// S3에 업로드
await s3Client.send(new PutObjectCommand({
Bucket: bucket,
Key: toSaveKey,
Body: resizedImage,
ContentType: `image/jpeg`,
}));
}
return {
statusCode: 200,
body: JSON.stringify({
message: 'Image processing completed',
processedCount: event.Records.length
}),
};
} catch (e) {
console.error('Error processing image:', e);
throw e; // Lambda가 에러를 인식하도록 throw
}
};
3. 사용자 default 이미지 생성 (by Lamdba)
이미지 생성함수 호출 ➡ Lambda구동 완료 및 Callback ➡ Callback결과반영 내부 로직처리
** 이미지 관련 로직 처리 중 이미지 생성 함수 호출
getAvatarFromAI(user.getId(), user.getNickname())요청 - nickname, userId(경로정보)
1) 이미지 로직 처리 중 이미지 생성함수 호출
(1) 반환할 url, 이미지 생성용 s3Key를 생성
(2) 이미지 생성함수 호출 (lambda)
invokeCreateAvatarFunction(Long userId, String nickname, String s3Key)
(3) lambda의 url을 반환
2) 이미지 생성함수 구동
(1) 이미지 생성을 위해 전달할 payload생성
(2) invokeRequest생성 후 lambda에 invoke메서드 호출
람다의 함수 이름과 버킷의 값을 설정에서 참조하여 실행
(3) S3 bucket에 저장 후 URL반환
(4) 백엔드 콜백 ( 이미지 URL 전송 )
** 이미지 생성 함수의 환경변수
API_KEY (백엔드 서버로 콜백 인증키 ), BACKEND_URL, BUCKET_NAME,
FONTCONFIG_PATH (글자 생성을 위해 커스텀 폰트 경로 위치 설정, sharp라이브러리)
import {BedrockRuntimeClient, InvokeModelCommand} from '@aws-sdk/client-bedrock-runtime';
import {S3Client, PutObjectCommand} from '@aws-sdk/client-s3';
import sharp from 'sharp';
const bedrockClient = new BedrockRuntimeClient({region: 'us-west-2'});
const s3Client = new S3Client({region: 'ap-northeast-2'});
const BUCKET_NAME = process.env.BUCKET_NAME;
const BACKEND_URL = process.env.BACKEND_URL;
const API_KEY = process.env.API_KEY;
export const handler = async (event) => {
const { userId, nickname, s3Key } = event;
try{
console.log('event: ', event);
//1. 이미지 생성 (프롬프트 설정)
const nicknameLetters = nickname.slice(0, 4)
const prompt =`create friendly, fruit or vegetable related image named "${nicknameLetters}". Style: simple, colorful, minimalist, suitable for agricultural market user profile`
console.log('start to generate AI image');
const imageBase64 = await generateAIImage(prompt)
const imageBuffer = Buffer.from(imageBase64, 'base64');
//2. 이미지 리사이즈
const resizedImageBuffer = await sharp(imageBuffer)
.resize ( 200, 200,{
fit: 'cover',
position: 'center'
})
.jpeg({quality: 90 })
.toBuffer();
//3. 업로드
console.log('start to upload image to s3');
await s3Client.send(new PutObjectCommand({
Bucket: BUCKET_NAME,
Key: s3Key,
Body: resizedImageBuffer,
ContentType: 'image/jpeg',
}))
const avatarUrl = `https://${BUCKET_NAME}.s3.ap-northeast-2.amazonaws.com/${s3Key}`;
console.log('avatarUrl: ', avatarUrl);
//백엔드 호출
await callBackend(userId, avatarUrl, true)
return {
statusCode: 200,
body: JSON.stringify({
userId,
nickname,
avatarUrl
})
}
}catch(error){
await callBackend(userId, null, false);
throw error;
}
};
async function generateAIImage(prompt) {
//이미지 생성 실패시 사용할 기본이미지
const payload = {
taskType: "TEXT_IMAGE",
textToImageParams: {
text: prompt,
negativeText: "nsfw, inappropriate, ugly, distorted, watermark, blurry"
},
imageGenerationConfig: {
numberOfImages: 1,
quality: "standard",
height: 512,
width: 512,
cfgScale: 8.0,
seed: Math.floor(Math.random()*1000000)
}
};
const command = new InvokeModelCommand({
modelId: "amazon.titan-image-generator-v1",
contentType: "application/json",
accept: "application/json",
body: JSON.stringify(payload)
})
const response = await bedrockClient.send(command);
const responseBody = JSON.parse(new TextDecoder().decode(response.body));
return responseBody.images[0];
}
async function callBackend(userId, avatarUrl, success){
if(!BACKEND_URL || !API_KEY){
console.log('콜백의 기본적인 설정없음');
return;
}
try{
if(BACKEND_URL != "http://localhost:8080"){
const response = await fetch(`${BACKEND_URL}/api/v1/user/public/${userId}/avatar/callback`,
{ method: "PUT",
headers: {
'Content-Type': 'application/json',
'X-API-Key': API_KEY
},
body: JSON.stringify({
userId: userId,
avatarUrl: avatarUrl,
success: success
})
}
)
if (response.ok){
console.log("콜백 성공");
}else{
console.error("콜백 실패")
}
}else {
console.log("Skip callback for local operation")
}
}catch(error){
console.log(error)
}
}

3) Lambda의 callback받기
@PutMapping("/public/{userId}/avatar/callback")
public ResponseEntity<Void> avatarCallback(
@PathVariable Long userId,
@RequestBody AvatarCallbackRequest request,
@RequestHeader("X-API-Key") String apiKey
){
if(!lambdaApiKey.equals(apiKey)){
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
//User가 없는 경우에 대한 예외처리 ??
User user = userService.getOptionalUserById(userId).orElse(null);
if( user == null || !user.getVerify() || !(user.getProvider() == InitialProvider.LOCAL)){
log.warn("이메일 가입 사용자가 아니거나 이메일 인증이 완료되지 않았습니다.");
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
}
if(request.isSuccess ()){
log.info("생성성공");
user.setAvatar(request.getAvatarUrl());
userRepository.save(user);
}else {
log.error("생성실패");
//생성 실패시 대체 이미지
}
return ResponseEntity.ok().build();
}

_______________________________________________________________________
** Lambda 수행 시 Timeout에 유의 ( 기본 3초, 초과시 강제 종료 )

_________________________________________________________________________
** Lambda configuration 화면
'Business Ops' 카테고리의 다른 글
| Local_TripTags (0) | 2025.11.12 |
|---|---|
| 반도체의 유리기판 ? (0) | 2025.02.15 |