Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,8 @@ public class Pet {
public void setMember(Member member) {
this.member = member;
}

public void updateImage(String imageUrl) {
this.imageUrl = imageUrl;
}
}
2 changes: 2 additions & 0 deletions module-member/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,10 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.findify:s3mock_2.12:0.2.4'
}

tasks.register("prepareKotlinBuildScriptModel"){}
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ public static void main(String[] args) {
SpringApplication.run(ModuleMemberApplication.class, args);
}

static {
System.setProperty("com.amazonaws.sdk.disableEc2Metadata", "true");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.gaethering.modulemember.config;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@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 awsCreds = new BasicAWSCredentials(accessKey, secretKey);
return (AmazonS3Client) AmazonS3ClientBuilder.standard()
.withRegion(region)
.withCredentials(new AWSStaticCredentialsProvider(awsCreds))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.gaethering.modulemember.controller;

import com.gaethering.modulemember.exception.pet.ImageNotFoundException;
import com.gaethering.modulemember.service.pet.PetServiceImpl;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("${api-prefix}")
@RequiredArgsConstructor
public class PetController {

private final PetServiceImpl petService;

@PatchMapping("/mypage/pets/{petId}/image")
public ResponseEntity<String> updatePetImage(@PathVariable("petId") Long id,
@RequestPart("file") MultipartFile multipartFile) {

return ResponseEntity.ok(petService.updatePetImage(id, multipartFile));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.gaethering.modulemember.exception.errorcode;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum PetErrorCode {

IMAGE_NOT_FOUND("E1001", "사진이 존재하지 않습니다."),
INVALID_IMAGE_TYPE("E1002", "사진 형식이 잘못되었습니다."),
FAILED_UPLOAD_IMAGE("E1003", "사진 업로드에 실패하였습니다."),
PET_NOT_FOUND("E1004", "반려동물이 존재하지 않습니다.");

private final String code;
private final String message;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.gaethering.modulecore.exception.ErrorResponse;
import com.gaethering.modulemember.exception.errorcode.MemberErrorCode;
import com.gaethering.modulemember.exception.member.MemberException;
import com.gaethering.modulemember.exception.pet.PetException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
Expand Down Expand Up @@ -36,4 +37,15 @@ public ResponseEntity<ErrorResponse> handleMailSendException(MailSendException e
return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}

@ExceptionHandler(PetException.class)
public ResponseEntity<ErrorResponse> handlePetException(PetException e) {

ErrorResponse response = ErrorResponse.builder()
.code(e.getErrorCode().getCode())
.message(e.getMessage())
.build();

return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST);
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.gaethering.modulemember.exception.pet;

import com.gaethering.modulemember.exception.errorcode.PetErrorCode;

public class FailedUploadImageException extends PetException {

public FailedUploadImageException() {
super(PetErrorCode.FAILED_UPLOAD_IMAGE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.gaethering.modulemember.exception.pet;

import com.gaethering.modulemember.exception.errorcode.PetErrorCode;

public class ImageNotFoundException extends PetException {

public ImageNotFoundException() {
super(PetErrorCode.IMAGE_NOT_FOUND);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.gaethering.modulemember.exception.pet;

import com.gaethering.modulemember.exception.errorcode.PetErrorCode;

public class InvalidImageTypeException extends PetException {

public InvalidImageTypeException() {
super(PetErrorCode.INVALID_IMAGE_TYPE);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.gaethering.modulemember.exception.pet;

import com.gaethering.modulemember.exception.errorcode.PetErrorCode;
import lombok.Getter;

@Getter
public class PetException extends RuntimeException {

private final PetErrorCode errorCode;

protected PetException(PetErrorCode petErrorCode) {
super(petErrorCode.getMessage());
this.errorCode = petErrorCode;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.gaethering.modulemember.exception.pet;

import com.gaethering.modulemember.exception.errorcode.PetErrorCode;

public class PetNotFoundException extends PetException {

public PetNotFoundException() {
super(PetErrorCode.PET_NOT_FOUND);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.gaethering.modulemember.service;

import org.springframework.web.multipart.MultipartFile;

public interface ImageUploadService {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pet 뿐만 아니라 Member에서도 사용할 수 있기 때문에 메소드 이름에 Pet을 빼는게 어떨까요?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PetService에서 ImageUploadService에 의존하여 image를 업로드하는 것을 따로 뺀 것은 좋은 것 같습니다. 하지만 둘 다 service 계층으로 정의해둘 경우 PetService에서 다른 service에 의존하는게 조금 어색해보입니다. service말고 그냥 component처럼 빈 등록을 하면 어떨까요?


String uploadImage(MultipartFile multipartFile);

void removeImage(String filename);

String createFileName(String filename);

String getFileExtension(String filename);

void validateFileExtension(String filename);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package com.gaethering.modulemember.service;

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.gaethering.modulemember.exception.pet.FailedUploadImageException;
import com.gaethering.modulemember.exception.pet.ImageNotFoundException;
import com.gaethering.modulemember.exception.pet.InvalidImageTypeException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.ArrayList;
import java.util.UUID;

@Service
@RequiredArgsConstructor
@Transactional
public class ImageUploadServiceImpl implements ImageUploadService {

private final AmazonS3 amazonS3;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 클래스가 AmazonS3에 직접적으로 의존하고 있는 것 같습니다. 만약 이미지 업로드를 다른 방식으로 하게되면 해당 클래스를 직접 수정해야하는 상황이 발생할 것 같습니다. 중간에 인터페이스를 둔 클래스 분리를 통해 의존성을 숨기는게 어떨까요?


@Value("${cloud.aws.s3.bucket}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@value가 필드에 직접 들어가게 되어있고 Setter도 없으면 테스트하기 어려운 코드가 될 것 같습니다. 테스트 코드에서도 spring에 무조건 의존하게 될 것 같습니다. 혹시 생성자의 파라미터에 @value를 통해 받게 하도록 해서 테스트하기 쉽게 만드는 것이 어떨까요??

private String bucket;

@Value("${dir}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위에 코멘트랑 똑같은 생각입니다!

private String dir;

@Override
public String uploadImage(MultipartFile multipartFile) {

String fileName = createFileName(multipartFile.getOriginalFilename());

ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(multipartFile.getSize());
objectMetadata.setContentType(multipartFile.getContentType());

try {
amazonS3.putObject(new PutObjectRequest(bucket + "/" + dir, fileName, multipartFile.getInputStream(), objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead));

} catch(IOException e) {
throw new FailedUploadImageException();
}

return amazonS3.getUrl(bucket, dir + "/" + fileName).toString();
}

@Override
public void removeImage(String filename) {
amazonS3.deleteObject(bucket,
dir + "/" + filename.substring( filename.lastIndexOf("/") + 1));
}

@Override
public String createFileName(String filename) {
return UUID.randomUUID().toString().concat(getFileExtension(filename));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

내부적으로만 사용하는 메소드 같은데 인터페이스를 통해 public으로 열어놓은 이유가 있으실까요??


@Override
public String getFileExtension(String filename) {
if (filename.length() == 0) {
throw new ImageNotFoundException();
}
validateFileExtension(filename);

return filename.substring(filename.lastIndexOf("."));
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위에 코멘트랑 똑같은 의견입니다!


@Override
public void validateFileExtension(String filename) {
ArrayList<String> fileValidate = new ArrayList<>();
fileValidate.add(".jpg");
fileValidate.add(".jpeg");
fileValidate.add(".png");
fileValidate.add(".JPG");
fileValidate.add(".JPEG");
fileValidate.add(".PNG");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이미지 파일 확장자를 저장하는 리스트를 따로 상수로 빼던가 클래스를 분리 시키는게 좋을 것 같습니다! 확장자가 추가된다면 코드를 직접 수정해야하는 번거로움이 있을 것 같습니다!


String idxFileName = filename.substring(filename.lastIndexOf("."));

if (!fileValidate.contains(idxFileName)) {
throw new InvalidImageTypeException();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.gaethering.modulemember.service.pet;

import org.springframework.web.multipart.MultipartFile;

public interface PetService {

String updatePetImage(Long id, MultipartFile multipartFile);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.gaethering.modulemember.service.pet;

import com.gaethering.moduledomain.domain.member.Pet;
import com.gaethering.moduledomain.repository.pet.PetRepository;
import com.gaethering.modulemember.exception.pet.ImageNotFoundException;
import com.gaethering.modulemember.exception.pet.PetNotFoundException;
import com.gaethering.modulemember.service.ImageUploadService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

@Service
@Transactional
@RequiredArgsConstructor
public class PetServiceImpl implements PetService {

private final ImageUploadService imageUploadService;
private final PetRepository petRepository;

@Value("${default.image-url}")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

위에 @value에 대한 의견과 같습니다!

private String defaultImageUrl;

@Override
public String updatePetImage(Long id, MultipartFile multipartFile) {
if (multipartFile.isEmpty()) {
throw new ImageNotFoundException();
}

Pet pet = petRepository.findById(id)
.orElseThrow(PetNotFoundException::new);

if (!defaultImageUrl.equals(pet.getImageUrl())) {
imageUploadService.removeImage(pet.getImageUrl());
}

String newImageUrl = imageUploadService.uploadImage(multipartFile);
pet.updateImage(newImageUrl);

return newImageUrl;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.gaethering.modulemember.config;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.AnonymousAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import io.findify.s3mock.S3Mock;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;

@TestConfiguration
public class AwsS3MockConfig {

@Value("${cloud.aws.s3.bucket}")
private String bucket;

@Value("${cloud.aws.region.static}")
private String region;

@Bean
public S3Mock s3Mock() {
return new S3Mock.Builder().withPort(8001).withInMemoryBackend().build();
}

@Primary
@Bean
public AmazonS3 amazonS3(S3Mock s3Mock){
s3Mock.start();

AwsClientBuilder.EndpointConfiguration endpoint =
new AwsClientBuilder.EndpointConfiguration("http://127.0.0.1:8001", region);

AmazonS3 client = AmazonS3ClientBuilder
.standard()
.withPathStyleAccessEnabled(true)
.withEndpointConfiguration(endpoint)
.withCredentials(new AWSStaticCredentialsProvider(new AnonymousAWSCredentials()))
.build();
client.createBucket(bucket);

return client;
}
}
Loading