[Spring Cloud] 스프링 클라우드 설정 및 기본적인 upload 예시

WOOK JONG KIM·2022년 12월 12일
0

패캠_java&Spring

목록 보기
101/103
post-thumbnail

APi Gateway 구현체

  • Zuul(netflix)
  • Zuul2
  • Spring cloud gateway

Service registry

  • DNS & IP VS Native Cloud(12 factor, 고정 IP를 가지지 않고도 제대로 동작, 유레카 이용)

Config Server(여러 api 서버, 필요에 따라 DB 연동 및 서로 요청을 매핑)

  • Spring cloud Config
  • Spring cloud event bus(이를 통해 변경 사항을 전달)
  • Spring Vault(암호화 관련 : DB 접속 정보, id/PW 등등)

Distributed Tracing

  • MDC(Mapped Diagnostic Context) : 로깅 방식
  • Spring cloud sleuth / zipkin : 로깅 추적 방식

Service Discovery

마이크로서비스 아키텍처(MSA)로 구성되어 있는 서비스들은 각자 다른 IP와 Port를 가지고 있다
-> 이러한 서로 다른 서비스들의 IP와 Port 정보에 대해서 저장하고 관리할 필요가 있는데 이것이 Service Discovery

사용 이유

여러 서비스들을 운용하기 위해서 클라우드 환경에서 인스턴스를 생성하여 구축

->이때 클라우드 환경에서 인스턴스는 AutoScaling, 생성, 삭제, 확장 등을 거치면서 IP나 Port들이 동적으로 변경될 가능성이 많음

-> 그때마다 서비스 변경사항에 대해서 일일이 알아내고 수정하고 하기에는 수십 ~ 수백개의 서비스들을 일일이 관리하기 어려울 뿐더러 클라이언트가 원하는 정보를 얻기 위해 요청 할 서비스를 찾기에 어려움을 겪을 것

AutoScaling : 사용자가 정의한 주기나 이벤트에 따라 서버를 자동으로 생성하거나 삭제

구현 방법

  • 클라이언트 사이드 디스커버리 패턴(Client-Side Discovery pattern)

서비스 클라이언트가 Service register에서 서비스의 위치를 찾아서 호출하는 방식

장점

  1. 비교적 간단하다
  2. 클라이언트가 사용 가능한 서비스 인스턴스에 대해서 알고 있기 때문에 각 서비스별 로드 밸런싱 방법을 선택할 수 있다

단점

  1. 클라이언트와 서비스 레지스트리가 연결되어 있어서 종속적이다
  2. 서비스 클라이언트에서 사용하는 각 프로그래밍 언어 및 프레임 워크에 대해서 클라이언트 측 서비스 검색 로직을 구현해야 한다
  • 서버 사이드 디스커버리 패턴(Server-Side Discovery pattern)

로드 밸런서는 컴퓨터 네트워크에서 사용되는 기술로, 네트워크 트래픽을 여러 서버에 분산하는 역할

호출 되는 서비스 앞에 로드밸런서를 넣는 방식이고, 클라이언트는 로드밸런서를 호출하면 로드밸런서가 Service Register로 부터 등록된 서비스의 위치를 전달하는 방식

장점

  1. discovery의 세부 사항이 클라이언트로부터 분리되어있다(의존성 ↓)
  2. 분리 되어 있어 클라이언트는 단순히 로드 밸런서에 요청만 한다 따라서 각 프로그래밍 언어 및 프레임 워크에 대한 검색 로직을 구현할 필요가 없다
  3. 일부 배포환경에서는 이 기능을 무료로 제공한다

단점

  1. 로드밸런서가 배포환경에서 제공되어야 한다
  2. 제공되어있지 않다면 설정 및 관리해야하는 또 다른 고 가용성 시스템 구성 요소가 된다

클라이언트 사이드 디스커버리 패턴에는 대표적으로 Netflix OSS(Netflix Open Source Software)에서 Client-Side discovery Pattern을 제공하는 Netflix Eureka(service registry component)

Server-Side Discovery의 예로는 AWS Elastic Load Balancer(ELB), Kubernetes


프로젝트 세팅

MultiPartConfig

@Slf4j
@Configuration
public class MutlpartConfig {

    @Value("${photoapp.file.defaultPath}")
    public String defaultPath ="";


    @Bean
    public CommonsMultipartResolver multipartResolver() {
        CommonsMultipartResolver multipartResolver = new CommonsMultipartResolver();
        multipartResolver.setResolveLazily(true);
        multipartResolver.setMaxUploadSize(1024 * 1024 * 10); // 10메가
        multipartResolver.setDefaultEncoding(StandardCharsets.UTF_8.displayName());
        try {
            LOG.info("path:" + defaultPath);
            multipartResolver.setUploadTempDir(new FileSystemResource(defaultPath));
        } catch (IOException e) {
           LOG.error("init error", e);
        }
        return multipartResolver;
    }


}

멀티파트 요청은 인터넷상에서 서버에 여러 개의 파일을 요청하는 것
-> 웹 페이지 로딩 속도를 높이기 위해 주로 사용

CommonsMultipartResolver 클래스는 HTTP POST 요청으로 전송된 여러 일반 파일과 단일 이미지/비디오 파일을 처리하는데 사용
-> 이를 통해 쉽게 파일을 업로드 할 수 있고, 다양한 설정을 통해 업로드된 파일의 크기와 형식을 제한할 수 있다

multipartResolver.setResolveLazily
-> 멀티파트 요청을 처리할때, 각 부분을 요청할때(필요할때)까지 처리를 미루는 기능을 설정하는 메서드

@Slf4j
@Configuration
@Setter
@Getter
public class PhotoAppProperties {
    @Value("${photoapp.file.defaultPath}")
    private String defaultPath;

    @PostConstruct
    private void init() {
        LOG.info("path:: {}",this.defaultPath);
    }
}

@Value 어노테이션으로 인해 필드가 실행될때 스프링이 photoapp.file.defaultPath속성에서 값을 가져오 주입

@Configuration
@EnableSwagger2
public class SwaggerConfig {
    @Bean
    public UiConfiguration uiConfig() {
        return UiConfigurationBuilder.builder()
                .deepLinking(true)
                .displayOperationId(false)
                .defaultModelExpandDepth(3)
                .defaultModelExpandDepth(3)
                .docExpansion(DocExpansion.LIST)
                .displayRequestDuration(false)
                .supportedSubmitMethods(UiConfiguration.Constants.DEFAULT_SUBMIT_METHODS)
                .validatorUrl(null)
                .build();
    }

    @Bean
    public Docket api() {
        return new Docket(DocumentationType.SWAGGER_2)
                .groupName("photo api")
                .apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
                .paths(PathSelectors.any())
                .build();
    }


    private ApiInfo apiInfo() {
        return new ApiInfoBuilder()
                .title("photo upload api")
                .description("사진 업로드 api")
                .version("1.0.0")
                .build();
    }
}

Image File

@Getter
@Builder
public class ImageFile {
    private String fileId;
    private Long fileSize;
    private String fileName;
    private String fileType;
    private String filePath;
}

ImageFile Upload Result

@Builder
@Getter
public class ImageFileUploadResult {
    private String fileId;
    private String fileName;
    private Long fileSize;
}

File writer

@Slf4j
@Service
public class FileWriter {
    @Autowired
    PhotoAppProperties photoAppProperties;

    public long writeFile(MultipartFile multipartFile, String filePath) {
        try {
            multipartFile.transferTo(new File(filePath));
        } catch (IllegalStateException ile) {
          throw new RuntimeException("file write error");
        } catch (IOException ioe) {
            throw new RuntimeException("ioe error");
        }
        return multipartFile.getSize();
    }

    public String getFilePath(String fileId, MultipartFile sourceFile) {
        return photoAppProperties.getDefaultPath() +"/" + dateStr() + "/" + fileId + "." + getMimeType(sourceFile.getOriginalFilename());
    }


    private static String getMimeType(String filePath) {
        return FilenameUtils.getExtension(filePath);
    }

    public static String dateStr() {
        LocalDateTime now = LocalDateTime.now();
        DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMMdd");
        return now.format(dateTimeFormatter);
    }
}

transferTo를 통해 파일을 지정된 경로에 write 하려 함

getMimeType은 확장자 정보 얻기

Controller

@RestController
public class FileUploadController {

    @Autowired
    FileUploadService fileUploadService;

    @ApiOperation("이미지 파일 업로드")
    @PostMapping("/v1.0/upload/image")
    public ImageFileUploadResult transfer(
            @RequestParam("userKey") String userKey,
            @RequestPart("imageFile") MultipartFile multipartFile) {
        ImageFile imageFile = fileUploadService.upload(multipartFile);
        return ImageFileUploadResult.builder()
                .fileName(imageFile.getFileName())
                .fileId(imageFile.getFileId())
                .fileSize(imageFile.getFileSize())
                .build();
    }
}

@RequestPart
-> file 객체와 VO(DB 테이블에 필드와 대응되는, 데이터를 저장하기 위한 객체) 객체를 한번에 받을 때 사용
하나의 api에서 MediaType을 지정하여 Json과 MultipartFile을 한번에 전달 받을 수 있다

Service

@Slf4j
@Service
public class FileUploadService {
    @Autowired
    private FileWriter fileWriter;

    public ImageFile upload(MultipartFile sourceFile) {
        String fileId = UUID.randomUUID().toString();
        String filePath = fileWriter.getFilePath(fileId, sourceFile);
        LOG.info("filePath:: {}", filePath );
        fileWriter.writeFile(sourceFile, filePath);

        return ImageFile.builder()
                .fileName(sourceFile.getName())
                .filePath(filePath)
                .fileId(fileId)
                .fileSize(sourceFile.getSize())
                .build();
    }
}

profile
Journey for Backend Developer

0개의 댓글