Spring Projcect/[팀플] In & Out 가계부

회원 수정 - 이미지 업로드 API

계란💕 2022. 11. 1. 12:52

 

 

  • 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 .. 두 개의 오류가 발생할 수 있다.
  • 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