AWS S3 : 프로필 이미지 저장
설계
사용자가 kakao, naver로 회원가입을 하지만 따로 프로필 이미지를 지정하고 싶은 경우
이미지 파일을 받는다. 반려견은 3장의 이미지 파일을 등록할 수 있다.
3장의 이미지 파일은 우선순위가 있다는 것이 주의할 점이다.
그래서 이미지와 사용자, 반려견의 연관관계에 대해 고민이 많았는데
나중에 서비스에 sns 기능을 추가할 생각이 있기 때문에
다음 두가지 방법 중 2번째 방법으로 구현하기로 했다.
방법1 : Image 테이블에 모든 정보 넣기
방법2 : 우선순위, 카테고리 별로 테이블 따로 두기
AWS S3
- 모든 종류의 데이터를 원하는 형식으로 저장 가능하다.
- 저장할 수 있는 데이터의 전체 볼륨과 객체 수에 제한이 없다.
- key 기반의 객체 스토리지, 데이터 저장 및 검색에 사용되는 고유 키가 할당된다.
Object Storage, S3
S3의 가장 큰 특징인 내구성과 가용성을 이해하기 위해 객체 스토리지(Object Storage)가 어떤 방식으로 설계되었는지 이해해보자.
다음은 일반적인 스토리지의 예시이다.
한 사용자가 데이터를 업로드하고 또 다른 사용자가 해당 데이터를 다운로드 하려는데
시간이 지나 수명이 다했더나 물리적인 손상으로 인해 데이터가 있는 영역이 손상되었다고 가정하자.
이제 그 공간에 놓인 데이터는 내구성이 손상되었으며, 사용할 수 없게 되었으므로 가용성 또한 훼손되었다.
물리적인 저장 공간이 어떻게 설계되었느냐에 따라 손상될 확률에 차이가 있겠지만
물리 장비의 한계상 결국 언젠가 데이터의 내구성과 가용성에 문제가 생길 수 밖에 없다.
그래서 이런 물리적인 한계를 논리적인 방식으로 극복하고자 한 구성이 객체 스토리지이다.
객체 스토리지는 기본적으로 내부 복제를 전제로 한다. (내부 복제가 고유 특징이라고 할 수는 없다.)
하나의 단위 객체가 업로드되면 자동적으로 내부의 여러 위치에 복제본을 생성한다.
S3의 경우 동일 Region 내의 여러 AZ에 걸쳐 복제본을 생성한다.
내부적으로 복제가 수행되면 어느 한 객체에 손상이 발생하더라도 손상되지 않은 복제본이 있기 때문에 내구성이 상승한다.
가용성 또한 향상된다. 복제본도 원본과 동일하게 실제 다운로드 요청에 응답하는데 사용되기 때문이다.
하지만 내부 복제에는 일정한 시간이 소요되기 때문에 내부 복제가 모두 완료(Fully Propagated)되기 이전에는
각기 다른 객체의 위치에서 응답하므로 사용자별로 일관되지 않은 응답이 발생할 수 있다.
- 새로 쓰기(create)의 경우 일부 요청에 객체 목록이 표시되지 않음
- 덮어쓰기의 경우 일부 요청에 이전 버전의 객체를 응답할 수 있음
- 삭제의 경우 일부 요청에 삭제되기 전의 객체가 표시되거나 응답할 수 있음
위와 같은 현상은 일시적인 것이며, 일정 시간이 지난 후에는 내부 복제가 모두 완료되어 모든 사용자에게 일관된 응답을 제공하게 된다.
이것을 Eventual Consistency(최종 일관성)를 제공한다고 말하며, 이는 객체 스토리지의 특성이자 S3의 특성이 된다.
객체 스토리지 및 S3의 공통적인 특성은 다음과 같다.
- 객체의 생성, 삭제만 지원한다. 수정은 지원하지 않는다.
- 덮어쓰기가 가능하지만 내부적으로 수정처리하는 것이 아니라 동일한 경로로 재생성하는 방식이다.
- 객체 데이터와 관련도니 부가정보는 객체 데이터 외부에 별도로 저장하여 관리한다.
부가정보를 Metadata라고 부르며 “Key-Value” 형태로 항목을 자유롭게 추가하여 관리할 수 있다. - HTTP(S) 프로토콜을 사용하여 업로드/다운로드할 수 있다.
참고 : https://acstory.tistory.com/33
aws s3 bulk upload
그동안 AWS S3를 2번 정도 사용해봤는데 여러 이미지를 저장해야 하는 경우가 없어서
putObject()
메소드만 사용해서 하나의 MultipartFile
만 저장했었다.
이번에는 반려동물의 프로필 이미지 3개를 한번에 저장해야 했기 때문에
이미지 리스트를 한번에 저장할 수 있는 기능이 있는지 찾아봤다.
TransferManager
를 사용하여 List<File>
을 한번에 넘겨주니
랜덤 문자열로 지정한 폴더와 이미지가 잘 저장된다.
참고 : https://docs.aws.amazon.com/ko_kr/AmazonS3/latest/dev/HLuploadFileJava.html
BULK INSERT
대용량 데이터를 로드하는 방법은 다양하다. 그 중 많이 사용하는 BULK INSERT에 대해 알아보기로 했다.
INSERT INTO T VALUES(a, b, c);
INSERT INTO T VALUES(d, e, f);
위 INSERT 는 다음과 같이 바꿀 수 있다.
INSERT INTO T VALUES (a, b, c), (d, e, f);
그러나 JPA에서 @GeneratedValue(strategy = GenerationType.IDENTITY)
이렇게 Auto Increment로 설정했을 때
saveAll()
을 사용해서 List로 저장하면 bulk insert로 저장될거라고 생각했는데 데이터 개수만큼 Insert 또는 Update 쿼리가 나간다.
Hibernate에서 Auto Increment인 경우 bulk insert를 지원하지 않는다고 하는데
Entity의 Id를 알 수 없는 경우 Transactional write behind
(쓰기 지연 : 트랜잭션이 커밋되기 전까지 쿼리 저장소에 모아뒀다가 한번에 실행)과 충돌이 발생하기 때문이다.
Ex. OneToMany의 Entity를 Insert하는 경우
(1) 부모 Entity를 Insert하고 생성된 Id 리턴
(2) 자식 Entity에서 부모의 Id를 전달받아 Fk에 채워서 Insert
이 과정에서 쿼리를 모아서 실행하는게 Hibernate의 방식인데
부모 Entity를 한번에 대량 등록하면, Fk에 어떤 부모의 Id를 매핑해야 되는지 알 수 없기 때문에 bulk insert가 불가능하다.
(그래서 Hibernate가 JDBC 수준에서 batch insert를 비활성화한다.)
왜 IDENTITY 방식을 권장할까?
bulk insert를 위해 TABLE이나 SEQUENCE(mysql에는 sequence가 없음) 방식으로 바꿔야 하나 잠시 고민했는데
채번에 따른 부하가 상당해서 IDENTITY 방식보다 더 느린 결과가 나올 수 있다고 한다. (+ 성능상 이슈와 Dead Lock에 대한 이슈)
참고 : JPA GenerationType에 따른 INSERT 성능 차이
어떤 변경없이 IDENTITY 방식을 그대로 사용할거지만
왜 IDENTITY를 사용해야 하는지 알아볼 수 있는 좋은 경험이었다.
참고
- https://vladmihalcea.com/why-you-should-never-use-the-table-identifier-generator-with-jpa-and-hibernate/
- https://jojoldu.tistory.com/507