- AWS S3(Amazon S3, Simple Storage Service)란?
- 아마존 웹 서비스에서 제공하는 온라인 스토리지 웹 서비스를 말한다.
- 같은 팀원이 S3 버킷을 생성했다.
- AWS에서 IAM key도 생성 완료
- build.gradle에 디펜던시를 추가한다.
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
- properties에도 다음과 같은 설정이 필요하다.
- 최대로 올릴 수 있는 파일의 용량을 뜻한다.
spring.servlet.multipart.max-file-size= 100MB
spring.servlet.multipart.max-request-size= 100MB
# ??? S3 bucket region ??
cloud.aws.region.static=ap-northeast-2
cloud.aws.stack.auto=false
logging.level.com.amazonaws.util.EC2MetadataUtils= error
<hide/>
# AWS S3? ?? IAM ?? KEY
cloud.aws.credentials.access-key= "액세스 key"
cloud.aws.credentials.secret-key= "시크릿 key"
# AWS S3 BUCKET ??
cloud.aws.bucket.name="버켓 이름"
- Member
@Column(name = "member_s3_image_key")
private String memberS3ImageKey;
- MemberDto
private String s3ImageUrl;
- Service
- getS3ImageUrl(): 이미지 key를 매개변수로 넣어서 amazon의 url을 반환하는 메서드
- 회원 조회를 하면 회원 수정에서 저장한 이미지를 불러와야한다.
- 따라서, getS3ImageUrl()에 member의 key를 넣으면 반환되는 url을 회원 조회의 s2ImageUrl에 저장하도록 한다.
- try-catch 구문에 withCannedAcl() 부분을 빼먹어서 파일을 저장하는데 오류가 났다.
- FileUploadException, Invalida ?? Exception .. 두 개의 오류가 발생할 수 있다.
- getS3ImageUrl(): 이미지 key를 매개변수로 넣어서 amazon의 url을 반환하는 메서드
- AwsServiceImpl
- 다이어리와 회원 모두 이미지 업로드 기능을 사용하기 때문에 인터페이스를 하나 따로 뺐다.
- dir은 디렉토리를 의미하며 member 또는 diary가 들어간다.
- 그러면 key에는 폴더명+ '/' + 날짜 + 파일명이 저장된다.
<hide/>
package com.rezero.inandout.awss3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.rezero.inandout.exception.AwsS3Exception;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.time.LocalDateTime;
@Service
@RequiredArgsConstructor
public class AwsS3ServiceImpl implements AwsS3Service{
private final AmazonS3Client amazonS3Client;
@Value(value = "${cloud.aws.bucket.name}")
private String s3Bucket;
@Override
public String getImageUrl(String s3ImageKey) {
return amazonS3Client.getUrl(s3Bucket, s3ImageKey).toString();
}
// 폴더dir 안에 key( = 날짜 + 파일이름)가 저장된다.
@Override
public String addImageAndGetKey(String dir, MultipartFile file) {
String key = dir + "/" + LocalDateTime.now() + file.getOriginalFilename();
ObjectMetadata objectMetaData = new ObjectMetadata();
objectMetaData.setContentType(file.getContentType());
objectMetaData.setContentLength(file.getSize());
// S3에 업로드
try {
amazonS3Client.putObject(
new PutObjectRequest(s3Bucket, key, file.getInputStream(), objectMetaData)
.withCannedAcl(CannedAccessControlList.PublicRead)
);
} catch (IOException e) {
throw new AwsS3Exception(e.getMessage());
}
return key;
}
@Override
public void deleteImage(String key) {
amazonS3Client.deleteObject(s3Bucket, key);
}
}
- MemberService
<hide/>
@Override
public MemberDto getInfo(String email) {
if (email == null) {
throw new MemberException(CANNOT_GET_INFO);
}
Member member = memberRepository.findByEmail(email).get();
// key가 비어있으면 ?
// dto에 있는 url이 "..com" 까지 밖에 안 나온다.
// if 문이 true인 경우는 key값이 있는 경우이므로
// key를 가지고 이미지 url을 가져올 수 있다.
String s3ImageUrl = "";
if (!member.getMemberS3ImageKey().isEmpty() || member.getMemberS3ImageKey() != null || member.getMemberS3ImageKey() != "") {
s3ImageUrl = awsS3Service.getImageUrl(member.getMemberS3ImageKey());
}
// key가 없으면 그냥 공백이 들어간다.
return MemberDto.builder().nickName(member.getNickName()).phone(member.getPhone())
.gender(member.getGender()).address(member.getAddress()).birth(member.getBirth())
.s3ImageUrl(s3ImageUrl).build();
}
@Override
public void updateInfo(String email, UpdateMemberInput input, MultipartFile file) {
Member member = memberRepository.findByEmail(email).get();
String previousUsedPhone = member.getPhone();
String previousUsedNickname = member.getNickName();
if (input.getNickName().contains(" ") || input.getPhone().contains(" ") || input.getGender()
.contains(" ")) {
throw new MemberException(CONTAINS_BLANK);
}
String inputPhone = input.getPhone();
String inputNickname = input.getNickName();
if (!previousUsedPhone.equals(inputPhone)) {
Optional<Member> existPhoneMember = memberRepository.findByPhone(inputPhone);
if (existPhoneMember.isPresent()) {
throw new MemberException(PHONE_EXIST);
}
}
if (!previousUsedNickname.equals(inputNickname)) {
Optional<Member> existNicknameMember = memberRepository.findByNickName(inputNickname);
if (existNicknameMember.isPresent()) {
throw new MemberException(NICKNAME_EXIST);
}
}
// update하기 전에 회원의 기존 사진을 삭제한다.
// key가 있으면 삭제해야하는 상황
// s3 서버에서 사진을 지운다.
if (!member.getMemberS3ImageKey().isEmpty() || member.getMemberS3ImageKey() != "") {
awsS3Service.deleteImage(member.getMemberS3ImageKey());
}
// key를 초기화
String s3ImageKey = "";
// 이미지 파일을 넣은 경우 (선택이니까 안 넣어됌)
if (file != null) {
s3ImageKey = awsS3Service.addImageAndGetKey(dir, file);
}
member.setNickName(input.getNickName());
member.setPhone(input.getPhone());
member.setBirth(input.getBirth());
member.setAddress(input.getAddress());
member.setGender(input.getGender());
member.setMemberS3ImageKey(s3ImageKey);
memberRepository.save(member);
}
- Controller
- 여기에서 입력 정보가 하나이고 입력값이 필수일 때는 @RequestBody를 쓰면 된다.
- 그런데, 회원 정보 수정할 때, 입력 정보는 필수이고 사진은 선택이다.
- 따라서, UpdateInput 앞에만 @RequestPart를 쓰도록 한다.
- 만약, 이 자리에 RequestBody를 쓰면 에러가 난다.
<hide/>
@PutMapping("/member/info")
@ApiOperation(value = "회원 정보 수정 API", notes = "회원이 자신의 정보를 수정하거나 프로필 이미지 사진 등록 가능하다.")
public ResponseEntity<?> updateInfo(Principal principal,
@ApiParam(value = "수정할 회원 정보 입력")
@RequestPart UpdateMemberInput input,
// @RequestPart // 사진 필수
MultipartFile file) {
String email = principal.getName();
memberService.updateInfo(email, input, file);
String message = "회원 정보를 변경했습니다.";
return new ResponseEntity<>(message, HttpStatus.OK);
}
- API Test
- 기존에 post man을 써왔지만 사용법을 잘 몰라서 알 수 없는 오류가 많이 나서 advanced rest client로 갈아탔다ㅜ
Ex) 회원 수정
- header에 다음과 같이 header name에 cookie와 value를 넣어줘야 한다.
- content-type은 multipart/form-data로 설정한다.
- body에는 컨트롤러의 updateInfo() 메서드 내의 매개변수명(file, input)과 일치시킨다
- 파일을 첨부하고, input은 jason형식으로 데이터를 넣어준다.
Ex) 회원 조회
Note) 실행 결과 - 링크를 누르면 회원 수정을 통해 AWS의 S3에 올라간 프로필 사진을 조회할 수있다.
- Test
- 이미지 넣은 기능에 대한 Mock 테스트를 위해 MockMultipartFile 라는 객체가 필요하다.
- 기존에는 perform안에 바로 get이나 post같은 메서드를 쓸 수 있었는데 이번에는 HttpMethod.PUT을 이용해야 수정이 가능하다.
<hide/>
@Test
@DisplayName("회원 정보 수정 - 성공")
void updateInfo() throws Exception {
// given
Member member = Member.builder().email("egg@naver.com")
.password("123abc?!")
.build();
User user = new User(member.getEmail(), member.getPassword(),
AuthorityUtils.NO_AUTHORITIES);
TestingAuthenticationToken testingAuthenticationToken = new TestingAuthenticationToken(user,
null);
MockMultipartFile file = new MockMultipartFile("file",
"dog.png",
"image/png",
"«‹png data>>".getBytes());
MockMultipartFile input = new MockMultipartFile("input",
"", "application/json",
("{ \"nickName\" : \"계란\","
+ " \"phone\" : \"010-1111-1111\","
+ " \"address\" : \"서울특별시\","
+ " \"gender\" : \"여\"}").getBytes(StandardCharsets.UTF_8));
mockMvc.perform(
multipart(HttpMethod.PUT, "/api/member/info")
.file(file)
.file(input)
.principal(testingAuthenticationToken)
).andExpect(status().isOk()).andDo(print());
ArgumentCaptor<UpdateMemberInput> captorInput = ArgumentCaptor.forClass(
UpdateMemberInput.class);
ArgumentCaptor<MultipartFile> captorFile = ArgumentCaptor.forClass(MultipartFile.class);
verify(memberServiceImpl, times(1)).updateInfo(anyString(), captorInput.capture(),
captorFile.capture());
}
<hide/>
@Test
@DisplayName("회원 정보 수정 - 성공")
void updateInfo() {
// given
String s3ImageKey = "imageKey";
Member member = Member.builder().email("egg@naver.com").address("서울특별시")
.phone("010-2222-0000").birth(LocalDate.from(LocalDate.of(2000, 9, 30))).gender("남")
.nickName("강동원").password("abc!@#12").memberS3ImageKey("2022-10-31T17:36:50.822 diary 강아지.jpg").build();
given(memberRepository.findByEmail(anyString())).willReturn(Optional.of(member));
given(awsS3Service.addImageAndGetKey(any(), any()))
.willReturn(s3ImageKey);
// when
UpdateMemberInput input = UpdateMemberInput.builder().address("강원도").nickName("치킨")
.phone("010-1111-2313").birth(LocalDate.now()).gender("여")
.address("강원도").build();
MockMultipartFile file = new MockMultipartFile("file",
"test.png",
"image/png",
"«‹png data>>".getBytes());
// then
memberService.updateInfo(member.getEmail(), input, file);
}
@Test
@DisplayName("회원 정보 수정(공백 포함) - 실패 (1)")
void updateInfo_fail_blank() {
// given
String s3ImageKey = "imageKey";
Member member = Member.builder().email("egg@naver.com").address("서울특별시")
.phone("010-2222-0000").birth(LocalDate.from(LocalDate.of(2000, 9, 30))).gender("남")
.nickName("강동원").password("abc!@#12").memberS3ImageKey(s3ImageKey).build();
given(memberRepository.findByEmail(anyString())).willReturn(Optional.of(member)); // 이메일로 회원을 조회한다.
UpdateMemberInput input = UpdateMemberInput.builder().address("강원도").nickName("치킨")
.phone("010-11 11-2313").birth(LocalDate.now()).gender("여")
.address("강원도").build();
MockMultipartFile file = new MockMultipartFile("file",
"test.png",
"image/png",
"«‹png data>>".getBytes());
// when
MemberException exception = assertThrows(MemberException.class,
() -> memberService.updateInfo(member.getEmail(), input, file));
// then
assertEquals(MemberErrorCode.CONTAINS_BLANK.getDescription(),
exception.getErrorCode().getDescription());
}
HTTP와 HTTP의 차이는?
- HTTP(HyperText Transfer Protocol, 하이퍼 텍스트 전송 프로토콜)
- 웹 서버와 웹 브라우저 상호 간의 데이터 전송을 위한 응용 계층 프로토콜이다.
- 인터넷을 작동시키는 역할을 한다.
- 상태를 가지지 않는 stateless 프로토콜이다.
- HTTPS(HyperText Transfer Protocol Secure, 하이퍼 텍스트 전송 프로토콜 보안)
- 표준 HTTP와 동일한 방식으로 작동한다.
- HTTP의 보안 문제를 해결하기 위해 등장했다.
- 서버와 주고 받는 데이터가 암호화되기 때문에 웹 사이트에 추가적인 보호를 제공한다.
- 따라서 개인 데이터를 훔치거나 볼 수 없도록 작동한다.
- 암호화, 복호화 과정 때문에 HTTP보다 속도가 느리다.
'Spring Projcect > [팀플] In & Out 가계부' 카테고리의 다른 글
회원 유효성 검증 (0) | 2022.11.10 |
---|---|
회원 비밀번호 초기화 API (0) | 2022.10.31 |
회원 탈퇴 API (0) | 2022.10.30 |
회원 이메일 인증 API (0) | 2022.10.28 |
회원 가입 API (0) | 2022.10.27 |