1. CloudFront

CloudFront는 정적, 동적 컨텐츠를 빠르게 응답하기 위한 캐시 기능을 제공하는 CDN 서비스입니다.

캐싱을 지원하기 때문에 S3에 저장된 컨텐츠를 직접 접근하지 않아도 됩니다.

따라서 S3의 비용이 감소하며, 더 빠른 응답을 지원하므로 꼭 함께 적용해주는 것이 좋습니다.

 

 

2. 퍼블리싱

src/main/resources/templates/gallery.html

<body>
    <h1>파일 업로드</h1> <hr>

    <form th:action="@{/gallery}" method="post" enctype="multipart/form-data">
        제목 : <input type="text" name="title"> <br>
        파일 : <input type="file" name="file"> <br>
        <button>등록하기</button>
    </form>

    <hr>

    <div th:each="gallery : ${galleryList}">
        <div style="margin-bottom: 30px">
            <p th:inline="text">제목 : [[${gallery.title}]]</p>
            <img th:src="${gallery.imgFullPath}" style="width: 500px; height: 300px;">
        </div>

        <form th:action="@{/gallery}" method="post" enctype="multipart/form-data">
            <input type="hidden" name="id" th:value="${gallery.id}">
            <input type="hidden" name="title" th:value="${gallery.title}">
            <input type="hidden" name="filePath" th:value="${gallery.filePath}">
            파일 : <input type="file" name="file"> <br>
            <button>수정하기</button>
        </form>
        <hr>
    </div>

</body>
  • 기존에 존재하던 gallery.html 파일에 갤러리 정보를 노출하는 영역과 수정 영역을 추가했습니다.
  • hidden 값인 id, title, filePath는 이미지 수정시 사용하는 값이므로 같이 넘겨줍니다.

 

3. 이미지 조회

1] 이미지 조회 - Controller

src/main/java/com/victolee/s3exam/controller/GalleryController.java

@GetMapping("/gallery")
public String dispWrite(Model model) {
    List<GalleryDto> galleryDtoList = galleryService.getList();

    model.addAttribute("galleryList", galleryDtoList);

    return "/gallery";
}
  • 기존에 존재하던 dispWrite() 메서드를 수정하여, 갤러리 목록을 가져옵니다.

 

2] 이미지 조회 - S3Service

S3에 이미지를 조회하는 방법으로 AmazonS3Client.getObject() 메서드가 있지만, 이는 S3에 직접 요청하여 객체를 가져옵니다.

여기서는 S3에 직접 접근하는 것이 아닌, CloudFront을 통해 캐싱된 이미지를 가져올 것이므로 해당 메서드를 사용하지 않습니다.

 

src/main/java/com/victolee/s3exam/service/S3Service.java

public static final String CLOUD_FRONT_DOMAIN_NAME = "dq582wpwqowa9.cloudfront.net";

...

public String upload(MultipartFile file) throws IOException {
    String fileName = file.getOriginalFilename();

    s3Client.putObject(new PutObjectRequest(bucket, fileName, file.getInputStream(), null)
            .withCannedAcl(CannedAccessControlList.PublicRead));

    return fileName;
}
  • 기존에 존재하던 upload() 메서드를 수정합니다.
  • CLOUD_FRONT_DOMAIN_NAME

 

3] 이미지 조회 - GalleryService

src/main/java/com/victolee/s3exam/service/GalleryService.java

public List<GalleryDto> getList() {
    List<GalleryEntity> galleryEntityList = galleryRepository.findAll();
    List<GalleryDto> galleryDtoList = new ArrayList<>();

    for (GalleryEntity galleryEntity : galleryEntityList) {
        galleryDtoList.add(convertEntityToDto(galleryEntity));
    }

    return galleryDtoList;
}

private GalleryDto convertEntityToDto(GalleryEntity galleryEntity) {
    return GalleryDto.builder()
            .id(galleryEntity.getId())
            .title(galleryEntity.getTitle())
            .filePath(galleryEntity.getFilePath())
            .imgFullPath("https://" + s3Service.CLOUD_FRONT_DOMAIN_NAME + "/" + galleryEntity.getFilePath())
            .build();
}
  • convertEntityToDto() 메서드는 Controller <--> Service 통신 간에 dto 객체를 사용하기 위해, repository로 부터 얻은 entity 객체를 dto로 변환하는 메서드입니다. 
    • .imgFullPath("https://" + s3Service.CLOUD_FRONT_DOMAIN_NAME + "/" + galleryEntity.getFilePath())​
      • 앞에서도 언급했지만 파일을 업로드할 때, 파일명만 DB에 저장하므로 이는 S3 객체의 key 값이 됩니다.
      • s3Service에 정의된 상수(CLOUD_FRONT_DOMAIN_NAME)를 불러와서 "CloudFront URL + key"를 조합하여 이미지 full path를 dto에 정의합니다.

 

4] 이미지 조회 - GalleryDto

src/main/java/com/victolee/s3exam/dto/GalleryDto.java

private String imgFullPath;

...

@Builder
public GalleryDto(Long id, String title, String filePath, String imgFullPath) {
    this.id = id;
    this.title = title;
    this.filePath = filePath;
    this.imgFullPath = imgFullPath;
}
  • 멤버 변수 imgFullPath를 추가하고, 빌더에도 정의해줍니다.

 

6] 이미지 조회 - GalleryRepository

src/main/java/com/victolee/s3exam/domain/repository/GalleryRepository.java

public interface GalleryRepository extends JpaRepository<GalleryEntity, Long> {
    @Override
    List<GalleryEntity> findAll();
}

 

 

5] 이미지 조회 - 테스트

 

이미지를 업로드하고 개발자도구로 src 애트리뷰트를 확인해보면 CloudFront URL로부터 이미지를 가져온 것을 확인할 수 있습니다.

 

 

 

"개발자 도구 - Network" 탭에서 Response Headers 정보를 살펴보면 cloudfront에서 Cache가 Hit된 것을 확인할 수 있습니다.

 

 

4. 이미지 수정/삭제

 

이미지 수정은 업로드와 마찬가지로 putObject() 메서드를 사용하여 객체를 수정합니다.

따라서 아무런 코드를 수정하지 않고 S3 이미지를 수정할 수 있습니다.

 

그런데 파일명이 같은 이미지를 수정했을 경우, S3에는 정상적으로 수정된 이미지로 업로드가 되지만 CloudFront는 캐시되어 있는 상태이므로 페이지에 새로운 이미지로 반영되지 않습니다.

CloudFront 배포 설정에 따라 캐싱되는 시간에 차이가 있는데, 기본 값은 1일 입니다.

어쨋든 이미지를 수정했음에도 바로 반영이 안되므로 이는 서비스 운영 관점에서 이슈입니다.

 

따라서 이를 해결하려면 아래의 작업이 필요하며, 그 이유는 다음과 같습니다.

  • 고유한 파일명(객체의 key)으로 S3에 업로드
  • 업로드 시, 해당 키의 객체가 존재하면 삭제 후 업로드

 

 

1] 이미지 수정/삭제 - Controller

src/main/java/com/victolee/s3exam/controller/GalleryController.java

@PostMapping("/gallery")
public String execWrite(GalleryDto galleryDto, MultipartFile file) throws IOException {
    String imgPath = s3Service.upload(galleryDto.getFilePath(), file);
    ...
}
  • service.upload()를 호출할 때, 기존의 파일명을 파라미터로 전달합니다.
  • 이 값은 gallery.html에 hidden 값으로 정의되어 있습니다.

 

2] 이미지 조회 - S3Service

src/main/java/com/victolee/s3exam/service/S3Service.java

public String upload(String currentFilePath, MultipartFile file) throws IOException {
    // 고유한 key 값을 갖기위해 현재 시간을 postfix로 붙여줌
    SimpleDateFormat date = new SimpleDateFormat("yyyymmddHHmmss");
    String fileName = file.getOriginalFilename() + "-" + date.format(new Date());

    // key가 존재하면 기존 파일은 삭제
    if ("".equals(currentFilePath) == false && currentFilePath != null) {
        boolean isExistObject = s3Client.doesObjectExist(bucket, currentFilePath);

        if (isExistObject == true) {
            s3Client.deleteObject(bucket, currentFilePath);
        }
    }

    // 파일 업로드
    s3Client.putObject(new PutObjectRequest(bucket, fileName, file.getInputStream(), null)
            .withCannedAcl(CannedAccessControlList.PublicRead));

    return fileName;
}
  • fileName 변수
  • doesObjectExist()
  • putObject()

 

2] 테스트

 

1) 기존 파일

 

 

2) 수정 업로드

 

동일한 파일명이지만 다른 그림을 업로드 했을 때, postfix로 인해서 CloudFront의 Cache가 적용되지 않은 것을 확인할 수 있습니다.

DB에도 잘 key값이 잘 반영되었네요.

 

 

3) S3에서 delete 되었는지 확인

 

신규/수정 업로드를 각각 수행했지만, 최종적으로는 마지막에 업로드한 파일만 존재하고 있는 것을 확인할 수 있습니다.

이상으로 Cloud Front를 적용했을 때 S3에 파일 업로드/조회 하는 방법에 대해 알아보았습니다.

 

 

출처: https://victorydntmd.tistory.com/336

# 서버 이중화와 세션 유지

 

세션은 외부 서버에 저장하도록 별도로 설계하지 않는 이상 기본적으로 웹 서버 영역이나 WAS 영역에 저장됩니다. 운영하는 서버가 한 대일 경우 모든 접속자가 하나의 서버에 접속하여 세션 정보를 저장하게 되므로 별 문제가 없습니다. 

 

하지만 실제 웹 서비스를 하면서 싱글 서버를 구성하여 운영하는 경우는 거의 없다고 해도 무방합니다. 대부분 2대 이상으로 설계된 서버 이중화 구성을 하고 있지요. 이렇게 여러 대의 서버로 구성된 서비스는 서버의 로드 밸런싱(부하 분산)을 위하여 L4 스위치를 이용하여 사용자의 접속을 각 서버마다 고르게 배분합니다. 그리고 고르게 배분하는 과정에서 사용자의 각 요청이 처음 접속한 서버와는 다른 서버로 연결된다면 이 서버에서는 사용자의 상태 정보가 존재하지 않기 때문에 상태 정보를 유지할 수 없는 문제가 발생합니다. 만약 서버가 5대로 구성되어 있다면 로그인을 한 사람이 접속하는 페이지에서 제대로 로그인되어 있을 확율은 1/5 정도가 되겠네요.  

 

그렇다고 해서 각 서버마다 모두 세션을 생성해줄 수도 없는 일입니다. 그러면 실제 서비스 상에서는 이런 문제를 어떻게 해결하는 걸까요? 

일반적으로 로드 밸런싱을 위한 L4 스위치는 아래 3가지의 구성 방식이 있습니다.

 

 

방식 

의미 

 Hash 방식

IP를 서버의 대수로 나누어 연결한 후 이후로는 계속 동일한 서버로 접속 

 Round-Robin 방식

L4와 연결된 서버에 순차적으로 접속하여 서비스 수행

 Least-Connection 방식

L4에서 현재 연결이 제일 적은 서버로 접속  

 

이 중, Round-Robin  방식이나 Least-Connection 방식의 경우 로드밸런싱이라는 본래 목적에는 적합하지만 매 요청마다 사용자를 서로 다른 서버에 연결해버리므로 HTTP 세션을 사용할 수 없습니다. 그렇다고 Hash 방식으로 구성하자면 부하분산 능력이 떨어져서 일부 서버로 요청이 집중될 확률이 너무 커집니다. 그래서 Round-Robin 방식이나 Least-Connection 방식을 사용할 경우 L4에서는 Sticky Mode 라고 하여 일정 시간동안 사용자를 동일한 서버로 계속 연결시켜 줄 수 있도록 지원하고 있습니다. 

 

물론 L4에서 Sticky Mode 를 사용하면 일차적으로는 문제점이 해결되겠지만 특정 서버가 다운될 경우 그 서버에 저장되어 있던 세션 정보는 모조리 소실될 수 있습니다. 로드 밸런싱에 의해  다른 서버로 옮겨진 사용자는 세션 정보가 없으므로 로그아웃된 상태가 될 것입니다. 이러한 문제점을 해결하기 위한 방책으로 WAS에서는 세션 클러스터링을 지원합니다. 말 그대로 세션 정보를 클러스터링 처리하여 모든 서버가 공유할 수 있도록 구성하는 것입니다. 별도로 프로그래밍을 통하여 처리하기보다 WAS에 포함되어 있는 기능을 활용하여 구성하는 것이 대부분입니다. 

 

# AWS 에서의 세션 소실 문제 

 

AWS에서도 이와 같은 문제가 동일하게 발생합니다. 두 개 이상의 인스턴스로 구성된 서버 어플리케이션은 도메인을 어플리케이션에 연결하여 주는 ELB에 의하여 관리되는데, 이 ELB는 서버 부하에 따라 개별 서버 인스턴스에 요청을 배분합니다. (이때 배분하는 방식은 수정된 Round-Robin 방식입니다)이로 인하여 웹 사이트에 접속하고 일정 시간이 지나면 다른 서버로 연결이 전환되어 세션이 끊어지는 일이 발생합니다. (측정해보았더니 약 3분 간격으로 연결되는 서버가 달라지더군요) 이러한 문제를 해결하기 위해 아마존 AWS 에서는 L4 스위치와 동일하게  SLB에 Sticky Mode를 적용이 가능한 기능을 제공합니다. 

 

Sticky Mode는 아마존 클라우드 서비스 관리자 콘솔에서 설정하므로 홈페이지의 관리자 콘솔로 접속합니다. 

 

 

관리자 콘솔로 접속하면 아마존 클라우드 서비스의 다양한 항목들이 나열되어 있습니다. 이 중에서 빨간 색으로 박스표시된 Elastic Beanstalk을 클릭하여 어플리케이션 화면으로 이동합니다. 보이시는 화면의 위쪽에도 EC2, S3, Elastic Beanstalk, Route 53 등의 메뉴가 있을텐데요, 이것은 자주 사용하는 메뉴를 따로 모을 수 있도록 해놓은 맞춤 기능이라고 생각하시면 됩니다. 

 

 

Elastic Beanstalk 어플리케이션 화면으로 접속되었습니다. 우리가 서비스할 어플리케이션을 선택하여 클릭하면 어플리케이션에 대한 대시보드 페이지로 연결되는데 이 중에서 우리는 환경 설정에 관한 옵션을 설정해야 하므로 페이지의 왼편 메뉴 중에서 두번째에 있는 [Configuration] 을 클릭해줍니다. 

 

 

이제 여러 항목으로 구분된 설정창 박스들이 여러 개 나타납니다. 웹 영역에 대한 환경 설정 정보를 관리하는 Web Tier도 보이고 네트워크에 관련된 설정 정보를 관리하는 Network Tier 도 보이네요. 우리에게 필요한 항목은 로드밸런싱 환경설정입니다. [Network Tier]의 [Load Balancing] 항목을 클릭해줍니다. 

 

 

자, 이제 세 개의 필드셋으로 구분된 로드밸런싱 환경 설정 페이지가 보이시나요? 위에서부터 차례로 로드 밸런싱할 포트 등 기본 정보를 세팅하는 [Load Balancer] 항목, 로드밸런싱을 하기 위한 헬스체크 정보를 세팅하는 [EC2 Instance Health Check], 마지막으로 우리가 설정해야 할 [Session] 항목이 있습니다. 

 

 

마지막 항목을 좀 더 자세히 살펴보겠습니다. 

 

해석하자면 다음 설정은 로드 밸런서가 세션 쿠키를 어떻게 다룰 것인지 컨트롤할 수 있게 해 줍니다. 라는 의미입니다. 

AWS에서 SLB는 L4 스위치에서  Sticky Mode가 작동하는 것과 동일하게 동작할 수 있습니다. Enable Session Stickiness 옵션에 체크를 하게 되면 SLB 내에 있는 자체 데이터베이스에 쿠키를 통해 접속자의 정보를 저장해두고, 설정된 시간동안 사용자의 접속을 기억해두었다가 기존에 요청을 보냈던 서버로 계속 요청이 전달될 수 있도록 하여 세션의 끊어짐을 방지합니다. 

 

이제 Enable Session Stickiness 에 체크를 해 줍니다. 그리고 우리는 웹 서버에서 설정된 세션 시간만큼 (톰캣의 경우 3600 초) SLB 장비에서도 사용자를 다른 서버로 보내지 않고 붙들어주어야 하므로 Cookie Expiration Period ( 쿠키 만료 시한 : 초)에 시간을 입력해줍니다. (예를 들어, 3600) 

 

이제 Save 버튼을 누르면 끝입니다. 웹사이트에서 체크를 해보면 아까와는 달리 세션이 유지되는 것을 확인할 수 있을 겁니다. 사용자의 요청이 세션 만료 시각이 지나기 전에는 기존의 인스턴스로 계속 유지되기 때문입니다.   

 

 

(참고로 싱글 인스턴스로 동작하는 웹서비스에서는 이런 현상이 일어나지 않습니다) 

 

 

출처: https://m.blog.naver.com/PostView.nhn?blogId=sqlpro&logNo=220006382480&proxyReferer=https%3A%2F%2Fwww.google.com%2F

'Server > AWS' 카테고리의 다른 글

[Node.js] AWS install nodejs  (0) 2020.07.31
[AWS] How to Install MariaDB on Ubuntu 18.04  (0) 2020.03.18
[강연] AWS 설치 및 간단 사용법 + Wordpress  (0) 2015.10.24
[Amazon Web Service] EC2 참고  (0) 2014.09.16
[AWS] 관련 서버 셋팅  (0) 2014.02.25

+ Recent posts