일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 백엔드 설정
- react
- MySQL
- 팀프로젝트
- toyproject
- controller
- 배열
- 네티 클라이언트
- Security
- 네티 서버
- Spring
- 프로젝트
- 스프링부트
- 자료형
- 자바
- Spring Boot
- service
- 도커
- Repository
- 기초설정
- 코틀린
- recoil
- JWT
- 채팅
- 클래스
- netty
- springboot
- Java
- Kotlin
- axios
- Today
- Total
hyuko
Spring boot WAS 와 Django 서버 간의 이미지 전송 최적화 이야기 본문
문제 상황: 방화벽과 보안 제약으로 인한 접근성 문제
우리 프로젝트에서 대시보드 시스템을 구성하는 과정에서 흥미로운 문제가 발생했씁니다.
Django 서버에서 생성되는 이미지를 최종 사용자에게 효율적으로 전달하기 위해서 브라우저에서 바로 접근하는 형식으로 구성했었습니다.
하지만 보안 정책으로 인해서 몇몇 pc 에서 사용자 브라우저에서 Django 서버에 직접 접근하는 것이 불가능 했습니다.
보안 이슈로 인해서 방화벽 규칙을 변경하는 것은 불가능했습니다.
따라서 사용자의 브라우저와 Django 서버사이의 중개자 역할을 할 솔루션이 필요했습니다.
시도 1 : Base64 인코딩 - 간단하지만 비효율적
첫 번째 접근 방식은 Spring Boot WAS 에서 Django 서버로부터 이미지를 가져온후 Base64로 인코딩 한 뒤, JSON 응답에 포함시켜 클라이언트에 전달하는 방식이었습니다.
예를들자면 아래와 같은 코드가 될 수 있겠습니다.
@GetMapping("/dashboard")
public ResponseEntity<ResponseData<DashboardDto>> getDashboard() {
// 대시보드 데이터 가져오기
DashboardDto dashboardData = dashboardService.getDashboardData();
// Django 서버에서 이미지 가져와 Base64로 인코딩
byte[] imageBytes = djangoImageService.getImageBytes("chart1");
String base64Image = Base64.getEncoder().encodeToString(imageBytes);
// 이미지 데이터를 DTO에 포함
dashboardData.setChartImageBase64(base64Image);
ResponseData<DashboardDto> response = new ResponseData<>(dashboardData);
return ResponseEntity.ok(response);
}
이 방식은 구현이 간단하면서 하나의 HTTP 요청으로 모든 데이터를 가져올 수 있다는 장점이 있었습니다.
하지만 이 방식은 실제 운영환경에서 문제가 있었는데요.
- 서버 응답 시간이 약 2~3초로 지연됨
- 클라이언트에서 Base64 디코딩에 추가로 3초정도 소요가됨
- 대용량의 이미지의 경우 응답 크기가 30~40% 증가하여 네트워크 부하가 가중됨
결과적으로 사용자 경험자체가 크게 저하되어서 다른 방식을 찾아야 했습니다.
이 때 첫번째로 구상한 방식은 리사이즈를 해볼까? 였지만, 이방식 또한 이미지의 픽셀을 수정한다던지 압축을 하는 과정에서
이미지가 깨질 가능성이 커서 불가능했습니다.
시도 2 : 프록시 스트리밍 - 최적화된 솔루션
두 번째 접근 방식은 RESTful 디자인 원칙에 조금 더 충실한 방식으로, 데이터와 이미지를 별도의 엔드포인트로 분리하여 제공하기 였습니다.
@RestController
@RequestMapping("/dashboard")
public class DashboardController {
private final DashboardService dashboardService;
private final DjangoImageService djangoImageService;
// 생성자 주입
@GetMapping
public ResponseEntity<ResponseData<DashboardDto>> getDashboardData() {
DashboardDto data = dashboardService.getDashboardData();
// 이미지 URL을 데이터에 포함 (클라이언트가 이 URL로 별도 요청)
data.setChartImageUrl("/dashboard/image/chart1");
ResponseData<DashboardDto> response = new ResponseData<>(data);
return ResponseEntity.ok(response);
}
@GetMapping(value = "/image/{imageId}", produces = MediaType.IMAGE_JPEG_VALUE)
public ResponseEntity<Resource> getDashboardImage(@PathVariable String imageId) {
// Django 서버에서 이미지를 가져와 스트림으로 전달
Resource imageResource = djangoImageService.getImageAsResource(imageId);
return ResponseEntity.ok()
.contentType(MediaType.IMAGE_JPEG)
.body(imageResource);
}
}
- 이미지 서비스의 구현은 다음과 같이 Django 서버로부터 이미지를 가져와서 스트림 형태로 제공합니다.
@Service
public class DjangoImageService {
private final RestTemplate restTemplate;
private final String djangoServerUrl;
// 생성자 및 의존성 주입
public Resource getImageAsResource(String imageId) {
try {
// Django 서버에서 이미지 가져오기
String imageUrl = djangoServerUrl + "/api/images/" + imageId;
HttpHeaders headers = new HttpHeaders();
// 필요한 경우 인증 정보 추가
HttpEntity<String> entity = new HttpEntity<>(headers);
ResponseEntity<byte[]> response = restTemplate.exchange(
imageUrl,
HttpMethod.GET,
entity,
byte[].class
);
// 응답을 Resource로 변환하여 반환
byte[] imageBytes = response.getBody();
return new ByteArrayResource(imageBytes) {
@Override
public String getFilename() {
return imageId;
}
};
} catch (Exception e) {
throw new RuntimeException("Image retrieval failed: " + imageId, e);
}
}
}
성능 비교 및 결과
두 방식의 성능을 비교한 결과는 차이가 많이나는 것을 확인했습니다.
지표 | Base64 인코딩 방식 | 프록시 스트리밍 방식 |
서버 응답 시간 | 2~3초 | 0.3~0.5초 |
클라이언트 처리 시간 | 3초 | 거의 없음 |
총 로딩 시간 | 5~6초 | 0.5~0.8초 |
응답 크기 증가 | 30~40% | 없음 |
프록시 스트리밍으로 얻을 수 있는 장점은 다음과 같습니다.
- 응답 시간 단축: 서버와 클라이언트 모두에서 처리시간이 대폭 단축된 것을 볼 수 있습니다.
- 효율적인 메모리 사용: 이미지를 인코딩/디코딩 하는 과정이 없어 메모리 사용이 최적화 되었습니다.
- 레이지 로딩 가능: 클라이언트는 필요한 이미지만 선택적으로 로딩할 수 있게 되었습니다.
- HTTP 캐싱 활용: 브라우저의 기본 이미지 캐싱 메커니즘 활용 가능.
기술적 구현 포인트
- Spring 의 Resource 인터페이스 활용: 메모리 효율적인 방식으로 이미지 데이터를 처리했습니다.
- 적절한 Content-Type 설정: 각 리소스 유형에 맞는 Content-Type을 지정하여 브라우저가 올바르게 해석할 수 있도록 구성
- RestTemplate 최적화: 연결 및 읽기 타임아웃 설정으로 장애 상황에 대비
- 예외 처리: 장고 서버 연결 실패등의 상황에 대한 적절한 예외처리를 구현했습니다.
회고 및 결론
이번에 이런 경험을 통해서 RESTful API 설계 원칙에 대해서 충실한 접근 방식이 단순해 보이는 솔루션보다는 성능과 사용자 경험 측면에서 우수할 수 있다는 것을 확인한 시간이었습니다.
특히 바이너리 데이터와 같은 대용량 콘텐츠를 처리할 때에는 스트리밍 방식이 훨씬 효율적이라는 것을 느꼈습니다.
또한, WAS가 프록시 역할을 하면서 보안 정책을 그대로 유지하면서 필요한 리소스에 접근할 수 있는 아키텍처를 구성했습니다.
추후에 마이크로서비스 아키텍처에서 서비스간 통신이나 레거시 시스템 통합을 할 때에도 써볼 수 있을 것 같습니다.
혹시나 제가 겪었던 이런 이슈 때문에 고생하시는 다른 개발자 분들에게 도움이 됬으면 좋겠습니다.