피카소 프로젝트 자체가
미술 경매 사이트였기 때문에
파일 업로드는 필수 불가결한 기능이다.
필자의 팀은 서비스에 올라오는 모든 사진을
NCP의 Object Storage에 올려놓고 관리하기로 하였다.
만약 파일이 업로드 될 경우,
자동으로 피카소 팀의 서버와 연결된 Object Storage에
파일이 저장되게 하는 것이다.
코드는 아래 PR에 자세히 나와있다.
[feat] 파일업로드 기능 구현 (공통기능) by donsonioc2010 · Pull Request #26 · donsonioc2010/picasso
data.sql을 통한 최초 SpringBoot실행시 데이터 기입하는 파일 추가 Server 실행별 포트분리 개발서버 : 80번 포트사용 로컬서버 : 8080번 포트 사용 의존성 추
github.com
0) application.yml 파일의 값 Properties 파일로 받아오기
@Getter
@ToString
@AllArgsConstructor
@ConfigurationProperties(prefix = "naver.storage") // yml 파일에서의 범위 지정
public class NaverObjectStorageProperties {
private String endPoint;
private String regionName;
private String accessKey;
private String secretKey;
private String bucketName;
}
naver:
storage:
end-point: https://kr.object.ncloudstorage.com
region-name: kr-standard
bucket-name: picasso-bucket
application.yml의 값을
@Configuration
어노테이션을 이용하여
받아오고 있다.
NaverObjectStorageProperties
의 필드 중
endPoint, regionName, bucketName
을 application.yml에서 받아오며
나머지 accessKey
와 secretKey
는 VM option으로 지정해주었다.
물론 자동 배포 과정에서는 스크립트 파일에 VM option으로 지정해주었다,,
이렇게 사용할 수 있는 이유는
@Configuration
@EnableConfigurationProperties(
{
NaverObjectStorageProperties.class,
PicassoProperties.class
}
)
public class ConfigurationPropertiesConfig {}
NaverObjectStorageProperties
에 @Configuration
어노테이션을 붙여
Configuration
객체가 되어 Bean이 될 수 있게 하고
EnableConfigurationProperties
에
사용자가 지정한 Properties
클래스의 class 정보를 주어
하위 Properties로 지정된 Configuration
클래스들이
Bean으로 등록될 수 있게 했기 때문이다.
1) build.gradle 의존성 추가
피카소 프로젝트는 멀티모듈로 진행되었다.
파일 업로드 기능 같은 경우는
모든 모듈에서 공통적으로 사용하는 기능이기 때문에
Common 모듈에 작성하였다.
dependencies {
api 'commons-io:commons-io:2.13.0'
api 'com.amazonaws:aws-java-sdk-s3:1.12.530'
}
Common 모듈의 build.gradle에 위와 같이 의존성을 추가해준다.
commons-io의 경우 파일의 확장자를 편하게 추출하기 위해 사용했고
aws 의존성 같은 경우 NCP는 오픈소스로 구현된 Object Storage를 사용하다 보니
그대로 사용이 가능했다.
2) 파일 업로드 코드 작성
2-1) NaverObjectStorageConfig
@Slf4j
@RequiredArgsConstructor
@Configuration
public class NaverObjectStorageConfig {
private final NaverObjectStorageProperties naverObjectStorageProperties;
@Bean
public AmazonS3 storageObject() {
log.info("Create NaverObjectStorageConfig Bean");
return AmazonS3ClientBuilder.standard()
.withEndpointConfiguration(this.getEndpointConfig())
.withCredentials(this.getCredentialsProvier())
.build();
}
private AwsClientBuilder.EndpointConfiguration getEndpointConfig() {
log.info(
"Create EndPoint Object >>> EndPoint : {}, RegionName : {}",
this.naverObjectStorageProperties.getEndPoint(),
this.naverObjectStorageProperties.getRegionName()
);
return new AwsClientBuilder.EndpointConfiguration(
this.naverObjectStorageProperties.getEndPoint(),
this.naverObjectStorageProperties.getRegionName()
);
}
private AWSStaticCredentialsProvider getCredentialsProvier() {
log.info(
"Create CredentialsProvider >>> AccessKey : {}, SecretKey : {}",
this.naverObjectStorageProperties.getAccessKey(),
this.naverObjectStorageProperties.getSecretKey()
);
return new AWSStaticCredentialsProvider(
new BasicAWSCredentials(
naverObjectStorageProperties.getAccessKey(),
naverObjectStorageProperties.getSecretKey()
)
);
}
}
위 의존성 주입에서 말했듯이
NCP Object Storage는 오픈소스를 구현하여 만든 것이므로
우리는 AmazonS3 객체를 만들어 Object Storage에서 필요한 함수를
갖다 쓸 것이다.
위 Config 클래스에서는 EndPoint와 CredentialsProvider로
EndPoint와 AccessKey, SecretKey를 가져와 AmazonS3 객체에 주입해준다.
그렇다면 bucketName은 어디에서 주입받을까?
2-2) NaverObjectStorageUtil
@Slf4j
@RequiredArgsConstructor
@Component
public class NaverObjectStorageUtil {
private final AmazonS3 storageObject;
private final NaverObjectStorageProperties naverObjectStorageProperties;
private final Environment environment;
/**
* 사용처, MultipartFile만 제공하면 업로드가 가능하다.
*
* @param usageType
* @param file
* @return
* @throws FileUploadException
*/
public String storageFileUpload(NaverObjectStorageUsageType usageType, MultipartFile file){
try {
String filePath = getPath(usageType, getFileUUIDNameByMultipartFile(file));
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(file.getContentType());
metadata.setContentLength(file.getBytes().length);
uploadFile(
usageType, filePath, file.getInputStream(), metadata
);
log.info("File Upload : {}", filePath);
return filePath;
} catch (IOException e) {
throw FileIOException.EXCEPTION;
} catch (Exception e) {
log.error(e.getMessage());
throw FileUploadException.EXCEPTION;
}
}
/**
* 파일 삭제를 진행할 경우 사용한다
*
* @param filePath
* @throws FileDeleteException
*/
public void storageFileDelete(String filePath) throws FileDeleteException {
try {
storageObject.deleteObject(
naverObjectStorageProperties.getBucketName(), // bucketName 입력
filePath
);
log.info("File Delete : {}", filePath);
} catch (Exception e) {
throw FileDeleteException.EXCEPTION;
}
}
/**
* 실제 File을 NaverObject Storage에 Upload
*
* @param usageType
* @param filePath
* @param uploadTarget
* @param metadata
*/
private void uploadFile(NaverObjectStorageUsageType usageType, String filePath, InputStream uploadTarget, ObjectMetadata metadata) {
storageObject.putObject(
new PutObjectRequest(
naverObjectStorageProperties.getBucketName(), // bucketName 입력
filePath,
uploadTarget,
metadata
).withCannedAcl(CannedAccessControlList.PublicRead)
);
}
/**
* UUID.png 등으로 제작하기 위함
*
* @param file
* @return
*/
private String getFileUUIDNameByMultipartFile(MultipartFile file) {
return UUID.randomUUID() + // 파일 이름을 랜덤적으로 제작하기 위함
"." +
FilenameUtils.getExtension(file.getOriginalFilename());
}
/**
* ObjectStorage에 저장될 File Path
* usageType은 프로필 사진 or 경매용 그림 파일을 지정한다.
* enum의 필드 값은 "profile" or "picture"이다.
*
* @param usageType
* @param fileName
* @return
*/
private String getPath(NaverObjectStorageUsageType usageType, String fileName) {
return environment.getProperty("spring.profiles.active", "default") + "/"
+ usageType.getPath() + "/"
+ fileName;
}
}
답은 여기에 있다.
NaverObjectStorageUtil
클래스는
AmazonS3 객체를 주입받고,
이 AmazonS3 객체는 NaverObjectStorageConfig
에서
Bean으로 주입한 AmazonS3 객체이다.
즉, 전 단계에서
AccessKey, SecretKey, EndPoint, RegionName
을 이미 주입받고
Bean으로 등록된 AmazonS3 객체를 사용하며
bucketName은 실제로
파일을 업로드하거나 삭제할 때 입력받는다.
Environment에 대해서는 아래 더보기에 작성해놓았다.
위 코드의 getPath 메소드에 주목해보자.
environment.getProperty("spring.profiles.active", "default") + "/" + usageType.getPath() + "/" + fileName;
해당 코드에서 environment
객체에는 어떤 Property들이 받아와질까?
Environment
객체는 Spring이 startup 될 때
자동으로 Bean으로 생성된다.
소스코드 내의 모든 application.yml 파일의 내용이
Environment
내에 저장되며
위 코드에서 지정한 yml 파일 내의 범위에 있는 내용이
getProperty 함수에 의해 불러와질 수 있다.
위 코드의 spring.profiles.active
같은 경우에는
Api 모듈의 application.yml에 존재한다.
2-3) PictureController
@Slf4j
@Controller
@RequestMapping("/pictures")
@RequiredArgsConstructor
public class PictureController {
private final PictureService pictureService;
private final UserService userService;
private final NaverObjectStorageUtil naverObjectStorageUtil;
private final PictureBidHistoryService pictureBidHistoryService;
/**
* 경매품 등록
*
* @param requestDto
* @param model
* @param session
* @return
*/
@PostMapping
public String add(PictureDTO requestDto, Model model, HttpSession session) {
User sessionUser = (User) session.getAttribute("loginUser");
if (sessionUser == null) {
return "redirect:/auth/login"; //로그인 페이지로
}
Optional<User> optionalUser = userService.findById(sessionUser.getId());
if(optionalUser.isPresent()){
User user = optionalUser.get();
Picture picture = new Picture();
picture.setPictureId(requestDto.getPictureId());
picture.setPictureName(requestDto.getPictureName());
picture.setPainterName(requestDto.getPainterName());
picture.setSize(requestDto.getSize());
picture.setDetails(requestDto.getDetails());
picture.setStartingPrice(requestDto.getStartingPrice());
picture.setIncrementAmount(requestDto.getIncrementAmount());
picture.setBidStartDate(requestDto.getBidStartDate());
picture.setPictureStatus(PictureStatus.BEFORE_APPROVE);
//희망 경매일자 + 7일
picture.setBidEndDate(requestDto.getBidStartDate().plusDays(7));
List<String> imageUrls = new ArrayList<>();
if (requestDto.getImageFile() != null && !requestDto.getImageFile().isEmpty()) {
String imageUrl = naverObjectStorageUtil.storageFileUpload(NaverObjectStorageUsageType.PAINT, requestDto.getImageFile());
// UsageType이 Picture!
picture.setImgUrl(imageUrl);
imageUrls.add(imageUrl);
model.addAttribute("imageUrls", imageUrls);
model.addAttribute("imgURL", imageUrl);
}
picture.setUser(user);
return "redirect:/pictures/"+pictureService.saveItem(picture).getPictureId();
}
return "redirect:/pictures/list?page=0&pageSize=10&status=BEFORE_APPROVE";
}
위 컨트롤러는 새 경매 그림을 등록한다.
세션에 저장되어있는 User의 정보를 받아오고
새 picture 객체를 만든 후
NaverObjectStorageUtil
의 storageFileUpload
를 이용하여
우리 서버의 ObjectStorage에 파일을 업로드한다.
한가지 아쉬운건,
컨트롤러 객체에 Service에 들어갈 만한 로직을
모두 작성해놓은 것이다.
PictureService
라는 Service 객체를 만들어
그 객체 안에 로직을 구성해놓는 것이 낫지 않았나 싶다.
다음 협업에서는 이러한 규칙을 잘 정의하고
팀원 간 코드 리뷰가 원활하게 진행되도록 해야겠다.
2-4) MyPageService
@Service
@Transactional
@RequiredArgsConstructor
public class MypageService {
private final UserRepository userRepository;
private final NaverObjectStorageUtil naverObjectStorageUtil;
/**
*
* @param session 현재 존재하는 Session
* @param updateRequestDto 수정해야 하는 정보
* @return
*/
public User updateUserInfo(HttpSession session, MyPageRequestDto updateRequestDto) {
User sessionLoginUser = (User) session.getAttribute("loginUser");
User databaseUserInfo = userRepository.findById(sessionLoginUser.getId()).orElseThrow(()-> NotFoundException.EXCEPTION);
if(sessionLoginUser.getEmail().equals(databaseUserInfo.getEmail())) {
if(!databaseUserInfo.getNickName().equals(updateRequestDto.getChgNickName())) {
databaseUserInfo.setNickName(updateRequestDto.getChgNickName());
}
if(!updateRequestDto.getProfile().isEmpty()) {
databaseUserInfo.setProfile(
naverObjectStorageUtil.storageFileUpload(
NaverObjectStorageUsageType.PROFILE, updateRequestDto.getProfile()
// UsageType이 PROFILE!
)
);
}
return databaseUserInfo;
}
return null;
}
}
위 PictureController
에서 아쉬운 부분을 정확히 고친
MyPageService
객체이다.
사용자의 프로필 사진을 수정하는 로직을 구현해놓았다.
원래 있던 사용자의 정보를 불러오고
그 사용자의 프로필 이미지를 updateRequestDto
안의
profile을 get하여 업데이트한다.
'🎉 프로젝트 > 🍀 Naver Cloud' 카테고리의 다른 글
[NCP] public area - private area 간의 통신 가능하게 하기 with NAT Gateway (1) | 2024.01.30 |
---|---|
[NCP] 무중단 배포 환경 구축하기 with Jenkins (0) | 2024.01.15 |
[NCP] Oriental Unity 프로젝트 (0) | 2024.01.15 |
[NCP] CI/CD 환경 구축 with Github Action, DBDocs (0) | 2023.10.02 |
[NCP] 피카소 프로젝트 (0) | 2023.10.02 |