카카오톡 클론코딩(7) - S3Service

2025. 3. 14. 22:01Project

이번에는 그놈의 중복코드가 계속 나와서 만들었다던 S3Service를 보여줄 것이다. 그닥 보여줄게 많지는 않은 부분이기도 하고

진작에 보여줬어야 했을거 같지만 조금 늦은감이 있더라도 지금이라도 올려본다.

 

@Service
@RequiredArgsConstructor
@Slf4j
public class S3Service {

    private final AmazonS3 amazonS3;

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

    /**
     * S3에 파일 업로드 (임시 저장)
     */
    public String uploadFileToTemp(MultipartFile multipartFile) {
        return uploadFile(multipartFile, "temp/");
    }

    /**
     * S3에 파일 업로드 (최종 저장)
     */
    public String uploadFile(MultipartFile multipartFile, String path) {
        if (multipartFile == null || multipartFile.isEmpty()) {
            throw new CustomException(ExceptionCode.INVALID_FILE_FORMAT);
        }

        String fileName = path + createFileName(multipartFile.getOriginalFilename());
        ObjectMetadata objectMetadata = new ObjectMetadata();
        objectMetadata.setContentLength(multipartFile.getSize());
        objectMetadata.setContentType(multipartFile.getContentType());

        try (InputStream inputStream = multipartFile.getInputStream()) {
            amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata));
        } catch (IOException e) {
            throw new CustomException(ExceptionCode.FILE_UPLOAD_FAILED);
        }

        return amazonS3.getUrl(bucket, fileName).toString();
    }

    /**
     * 파일을 최종 저장소로 이동
     */
    public String moveFileToFinalLocation(String tempUrl) {
        String tempPath = tempUrl.substring(tempUrl.indexOf("/temp") + 1);
        String finalPath = tempPath.replace("temp/", "chat/");

        try {
            amazonS3.copyObject(bucket, tempPath, bucket, finalPath);
            amazonS3.deleteObject(bucket, tempPath);
        } catch (AmazonS3Exception e) {
            throw new CustomException(ExceptionCode.FILE_UPLOAD_FAILED);
        }

        return amazonS3.getUrl(bucket, finalPath).toString();
    }

    /**
     * S3에서 파일 삭제
     */
    public void deleteFileByUrl(String fileUrl) {
        if (!fileUrl.contains(bucket)) {
            throw new CustomException(ExceptionCode.INVALID_FILE_URL);
        }

        String fileName = fileUrl.substring(fileUrl.lastIndexOf("/") + 1);

        try {
            amazonS3.deleteObject(new DeleteObjectRequest(bucket, fileName));
            log.info("S3에서 파일 삭제 : " + fileName);
        } catch (Exception e) {
            throw new CustomException(ExceptionCode.FILE_DELETE_FAILED);
        }
    }

    /**
     * 파일 이름을 UUID 기반으로 생성
     */
    private String createFileName(String fileName) {
        return UUID.randomUUID().toString().concat(getFileExtension(fileName));
    }

    /**
     * 파일 확장자 추출
     */
    private String getFileExtension(String fileName) {
        try {
            return fileName.substring(fileName.lastIndexOf("."));
        } catch (StringIndexOutOfBoundsException e) {
            throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "잘못된 형식의 파일 : " + fileName + " 입니다.");
        }
    }
}

특별할것은 없고 단순히 보이는게 다인 코드다. 하지만 이 S3Service를 만들 생각도 안하고 생각없이 이미지 업로드 부분을

매번 Member면 MemberServcie에 ChatMessage 면 ChatService에 넣어버린 대가는 처참했다... 끔찍할 정도의 중복코드와

각 서비스가 해야할 역할 외에도 다른 역할까지 하면서 책임 분리가 확실하게 되지 않았다는 것이다. 그리고 해당 부분들을 리팩토링 하면서 버리는 시간은 덤.. 설계를 좀더 꼼꼼히 했어야 했는데 확실히 실수한 부분인것 같다.

이 다음글은 도메인단위 테스트를 순수 자바코드로 작성한후에 올릴것이다. 그 이후에는 이전 글에서 언급했듯이 mySql 이중화 작업을 진행 해볼것이다.