📝느낀 점
이번 프로젝트를 통해 AWS S3와 MySQL을 활용한 이미지 관리 효율성을 체감할 수 있었습니다. 클라우드 스토리지와 관계형 데이터베이스를 결합하여 성능 측면에서 만족스러운 결과를 얻을 수 있었습니다. 앞으로도 이와 같은 설계를 바탕으로 더욱 효율적이고 안정적인 시스템을 구축해 나갈 수 있을 것이라고 생각하였습니다. SNS를 개발할 때 이미지 저장 방식은 중요한 고려 사항입니다. 이 글에서는 AWS S3를 활용해 이미지를 저장하고 MySQL 데이터베이스에 해당 URL을 저장하는 방법에 대해 상세히 설명하도록 하겠습니다.
AWS S3란 무엇인가?
AWS S3는 Amazon Web Services에서 제공하는 객체 스토리지 서비스입니다.
S3는 대용량 데이터를 안전하게 저장하고 필요할 때 언제든지
접근할 수 있는 확장 가능하고 신뢰성 있는 스토리지를 제공합니다.
S3의 주요 특징 입니다.
확장성: 필요에 따라 저장 용량을 자동으로 확장할 수 있습니다.
내구성 및 가용성: 높은 내구성과 가용성을 제공하여 데이터 손실 없이 안정적인 서비스 운영이 가능합니다.
보안: 다양한 보안 기능과 접근 제어를 제공하여 데이터의 안전성을 보장합니다.
통합: AWS의 다른 서비스와 쉽게 통합하여 다양한 애플리케이션을 구현할 수 있습니다.
이미지 저장 방식: MySQL 데이터베이스 vs AWS S3
MySQL 데이터베이스에 이미지 저장
이미지를 MySQL 데이터베이스에 저장하는 방법은 BLOB (Binary Large Object) 타입을 사용하는 것입니다.
예를 들어, MEDIUMBLOB 타입을 사용하여 이미지를 저장할 수 있습니다.
CREATE TABLE Images (
id INT PRIMARY KEY AUTO_INCREMENT,
image MEDIUMBLOB NOT NULL
);
하지만 이 방법은 몇 가지 단점이 있습니다.
- 데이터베이스 크기 증가: 이미지 데이터가 많아질수록 데이터베이스 크기가 급격히 증가합니다.
- 쿼리 성능 저하: 대용량 바이너리 데이터를 다루면 데이터베이스 쿼리 성능이 저하됩니다.
- 백업 및 복원 복잡성: 대용량 데이터를 포함한 데이터베이스를 백업하고 복원하는 과정이 복잡해집니다.
AWS S3에 이미지 저장
AWS S3는 대용량 데이터를 저장하고 관리하는 데 적절한 클라우드 스토리지 서비스입니다.
이미지를 S3에 저장하고 그 URL을 MySQL 데이터베이스에 저장하는 방법을 설명 하겠습니다.
- 이미지 업로드: 이미지를 S3에 업로드 합니다. 업로드 후 S3는 각 이미지에 유니크한 URL을 할당합니다.
- URL 저장: 이미지의 S3 URL을 MySQL 데이터베이스에 저장합니다.
테이블 설계는 다음과 같습니다:
CREATE TABLE ImageTable (
id INT PRIMARY KEY AUTO_INCREMENT,
image_url TEXT NOT NULL
);
이 방식의 장점은 다음과 같습니다:
- 저장 공간: 이미지를 S3에 저장함으로써 데이터베이스 크기를 최소화할 수 있습니다.
- 로딩 속도: 클라이언트는 S3 URL을 통해 이미지를 직접 로드할 수 있어 DB의 쿼리 응답 속도가 향상됩니다.
- 확장성: S3는 자동으로 확장 가능하며, 대용량 데이터를 손쉽게 관리할 수 있습니다.
- CDN 사용 가능: S3는 CDN과 통합되어 전 세계 어디서나 빠르게 이미지를 제공할 수 있습니다.
- 백업 및 복원 용이성: 이미지를 별도로 관리함으로써 데이터베이스 백업 및 복원 과정이 단순해집니다.
이미지 저장 및 관리 프로세스
스프링을 사용하여 이미지를 S3에 업로드하고 URL을 데이터베이스에 저장하는 과정은 다음과 같습니다.
먼저, Spring Boot 프로젝트를 설정합니다.
build.gradle 파일에 필요한 의존성을 추가
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'com.amazonaws:aws-java-sdk-s3'
runtimeOnly 'mysql:mysql-connector-java'
}
AWS S3 설정
AWS S3 버킷과 자격 증명을 설정합니다.
application.properties 파일에 AWS 자격 증명을 추가합니다.
cloud.aws.credentials.accessKey=YOUR_ACCESS_KEY
cloud.aws.credentials.secretKey=YOUR_SECRET_KEY
cloud.aws.region.static=YOUR_AWS_REGION
cloud.aws.s3.bucket=YOUR_S3_BUCKET_NAME
S3 설정 클래스 작성
AWS S3 클라이언트를 구성하고 Bean으로 등록합니다.
@Configuration
public class AwsS3Config {
@Value("${cloud.aws.credentials.access-key}")
private String accessKey;
@Value("${cloud.aws.credentials.secret-key}")
private String secretKey;
@Value("${cloud.aws.region.static}")
private String region;
@Bean
public AmazonS3Client amazonS3Client() {
BasicAWSCredentials awsCredentials = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCredentials))
.build();
}
}
S3 서비스 클래스 작성
AWS S3에 이미지를 업로드하고 URL을 반환하는 서비스를 작성합니다.
@Component
@RequiredArgsConstructor
public class AwsS3Uploader implements FileUploader {
private final AmazonS3Client amazonS3Client;
@Value("${cloud.aws.s3.bucket}")
private String bucket;
@Override
public String uploadFile(MultipartFile multipartFile, String dirName) {
try {
String fileName = generateFileName(dirName, multipartFile.getOriginalFilename());
ObjectMetadata metadata = createObjectMetadata(multipartFile);
amazonS3Client.putObject(new PutObjectRequest(
bucket,
fileName,
multipartFile.getInputStream(),
metadata).withCannedAcl(CannedAccessControlList.PublicRead));
return amazonS3Client.getResourceUrl(bucket, fileName);
} catch (IOException e) {
log.error("파일 업로드 실패", e);
throw new RuntimeException("파일 업로드에 실패했습니다.", e);
}
}
private String generateFileName(String dirName, String originalFilename) {
return dirName + "/" + UUID.randomUUID().toString() + "_" + originalFilename;
}
private ObjectMetadata createObjectMetadata(MultipartFile multipartFile) {
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(multipartFile.getContentType());
metadata.setContentLength(multipartFile.getSize());
return metadata;
}
}
컨트롤러 및 서비스 구현
이미지를 업로드하고 URL을 데이터베이스에 저장하는 컨트롤러와 서비스를 구현합니다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/profile")
public class ProfileController {
private final ProfileFacade profileFacade;
@GetMapping("/auth/member-info")
public ResponseEntity<ProfileInfoResponse> getProfileInfo() {
MemberCurrentDTO memberCurrentDTO = profileFacade.getUserInfo();
ProfileInfoResponse response = ProfileInfoResponse.toProfileInfoResponse(memberCurrentDTO);
return ResponseEntity.ok(response);
}
@PostMapping("/update")
public ResponseEntity<ProfileUpdatedResponse> updateProfile(
@Validated @ModelAttribute ProfileDetailsRequest profileDetailsRequest,
@RequestParam(value = "image", required = false) MultipartFile image) {
ProfileUpdateDTO profileUpdateDTO = ProfileDetailsRequest.toUpdateProfileCommand(profileDetailsRequest);
profileFacade.updateProfile(profileUpdateDTO, image);
ProfileUpdatedResponse response = ProfileUpdatedResponse.toProfileUpdateResponse(profileUpdateDTO.getMemberId());
return ResponseEntity.ok(response);
}
}
데이터베이스 엔티티 및 리포지토리 설정
이미지 URL을 저장할 데이터베이스 엔티티와 JPA 리포지토리를 설정합니다.
@Entity
@Getter
public class Profile extends AuditingFields {
...
@Column(name = "image_url", columnDefinition = "TEXT")
private String imageUrl;
...
public void updateImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
}
public interface ProfileRepository extends JpaRepository<Profile, Long> {
Optional<Profile> findByMemberId(Long memberId);
}
결론
이번 프로젝트를 통해 MySQL과 AWS S3를 결합한 이미지 관리 시스템의 설계와 구현해 보았습니다.
이를 통해 저장 공간을 효율적으로 관리하고, 데이터베이스의 성능을 최적화 할 수 있었습니다. 특히 대규모 데이터를 처리하는 SNS 환경에서 이 접근 방식은 매우 중요하다고 생각하며, 성능 향상에 도움을 줄 수 있다고 생각합니다. 다양한 요구 사항과 상황에 맞게 테이블 구조와 쿼리를 조정하여 최적의 결과를 얻을 수 있도록 더욱 공부해야 겠다는 생각도 하였습니다. 추가적으로, 이미지 외에도 다양한 미디어 파일을 효율적으로 관리하는 방법을 생각해 볼 수 있을것 같으며, AWS의 S3가 아닌, 다른 서비스들을 통해 더욱 개선된 시스템을 구축할 수 있을것이라 생각합니다.
'프로젝트(project)' 카테고리의 다른 글
비밀번호 검증 및 데이터 무결성 유지 (0) | 2024.07.25 |
---|---|
비밀번호 해싱과 Security로 보안 높이기 (0) | 2024.07.25 |
트랜잭션 및 외래키 제약 조건 문제 (0) | 2024.07.24 |
NonUniqueResultException 해결하기 (0) | 2024.07.24 |
비밀번호 찾기 및 재설정과 임시 비밀번호 문제 (0) | 2024.07.24 |