프로그래밍/JAVA

[JAVA] 이미지 업로드 회전 문제 해결 -- EXIF Orientation

이슬먹는 개발자 2026. 2. 21. 16:52
728x90
반응형

📸 서버에 올리면 사진이 옆으로 눕는다? Java 이미지 업로드의 숨겨진 함정

"분명히 세로로 찍었는데, 서버에 올리면 왜 가로로 눕지?"

이 글을 클릭한 당신, 아마 이 문장에 공감해서 들어왔을 겁니다. 저도 처음 이 문제를 만났을 때 꽤 오래 머리를 싸맸거든요. 오늘은 이 황당하고 은근히 까다로운 문제의 원인부터 해결책까지, 누구나 이해할 수 있도록 싹 정리해드리겠습니다.


🤔 먼저 현상을 정확히 파악해보자

시나리오는 이렇습니다.

  1. 사용자가 스마트폰으로 세로 방향으로 사진을 찍습니다.
  2. 그 사진을 웹사이트에 업로드합니다.
  3. 갤러리에서는 분명히 세로로 잘 보입니다.
  4. 그런데 서버를 통해 처리된 이미지는 가로로 90도 누워버립니다.

처음엔 "내가 뭘 잘못 코딩한 건가?" 싶지만, 사실 이건 여러분의 실수가 아닙니다. Java 기본 라이브러리의 한계와 카메라의 동작 방식이 충돌해서 생기는 고전적인 문제입니다.


🕵️ 범인 찾기: EXIF와 Orientation 태그

이 현상을 이해하려면 먼저 스마트폰 카메라가 사진을 어떻게 저장하는지 알아야 합니다.

스마트폰 카메라의 비밀

여러분이 스마트폰을 세로로 들고 사진을 찍을 때, 카메라 센서는 사실 항상 가로 방향으로 이미지를 캡처합니다. 이건 카메라 하드웨어 자체의 특성 때문입니다.

그래서 스마트폰은 이 문제를 꽤 영리하게 해결하는데, 방법이 이렇습니다.

"픽셀 데이터는 가로로 저장하되, 메모장에 '이 사진은 90도 돌려서 봐야 해' 라고 적어두자!"

이때 그 "메모장" 역할을 하는 것이 바로 EXIF(Exchangeable Image File Format) 데이터입니다. EXIF는 사진 파일에 덧붙여지는 메타데이터로, 촬영 날짜, 카메라 기종, GPS 위치 같은 정보와 함께 Orientation(회전 방향) 정보도 담겨 있습니다.

쉽게 비유하자면, 사진 파일 = 실제 그림 + 포스트잇 메모 라고 생각하면 됩니다.

  • 실제 그림 (픽셀 데이터): 가로로 눕혀진 원본 이미지
  • 포스트잇 메모 (EXIF): "이거 왼쪽으로 90도 돌려서 봐줘!"

갤러리 앱이나 브라우저는 이 포스트잇 메모를 읽고 자동으로 화면에 올바르게 보여주기 때문에, 우리 눈에는 세로 사진처럼 보입니다.

그렇다면 Java는?

문제는 Java의 기본 이미지 처리 클래스인 ImageIO.read()입니다. 이 녀석은 이미지를 읽을 때 포스트잇 메모(EXIF)를 완전히 무시합니다. 오직 픽셀 데이터만 읽어서 처리하죠.

결과적으로 서버에서 이미지를 처리하면, EXIF 메모 없이 가로로 누운 픽셀 데이터만 남게 되는 겁니다. 이게 바로 사진이 옆으로 눕는 이유입니다.


🛠️ 해결책: EXIF를 직접 읽고, 직접 돌려라

해결 방법은 명확합니다. Java가 무시한 EXIF 정보를 우리가 직접 읽어서, 그에 맞게 이미지를 강제로 회전시켜주면 됩니다.

이를 위해 metadata-extractor라는 라이브러리를 사용합니다. EXIF 정보를 손쉽게 읽어올 수 있게 해주는 고마운 라이브러리입니다.

Step 1. 의존성 추가 (build.gradle)

implementation 'com.drewnoakes:metadata-extractor:2.18.0'

Maven을 쓴다면 pom.xml에 아래를 추가하세요.

<dependency>
    <groupId>com.drewnoakes</groupId>
    <artifactId>metadata-extractor</artifactId>
    <version>2.18.0</version>
</dependency>

Step 2. 이미지 처리 로직에 EXIF 보정 추가

기존 코드에서 ImageIO.read()로 이미지를 읽어온 직후, 아래 보정 로직을 끼워 넣습니다.

// 이미지 읽기
BufferedImage originalImage = ImageIO.read(file.getInputStream());

// ✅ [추가] EXIF 회전 정보 확인
int orientation = 1; // 기본값: 정방향
try {
    Metadata metadata = ImageMetadataReader.readMetadata(file.getInputStream());
    ExifIFD0Directory directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
    if (directory != null && directory.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) {
        orientation = directory.getInt(ExifIFD0Directory.TAG_ORIENTATION);
    }
} catch (Exception e) {
    log.warn("EXIF 정보를 읽을 수 없습니다: {}", e.getMessage());
}

// ✅ [추가] EXIF 값에 따라 이미지 회전
if (orientation == 6) {
    originalImage = rotateImage(originalImage, 90);   // 시계방향 90도
} else if (orientation == 3) {
    originalImage = rotateImage(originalImage, 180);  // 180도
} else if (orientation == 8) {
    originalImage = rotateImage(originalImage, 270);  // 시계방향 270도
}

// 기타 작업 실행....

Step 3. 회전 메서드 구현

클래스 내에 아래 유틸리티 메서드를 추가합니다.

private BufferedImage rotateImage(BufferedImage img, int angle) {
    int w = img.getWidth();
    int h = img.getHeight();

    // 90도, 270도 회전 시 가로세로 크기가 뒤바뀜
    boolean isRotated90or270 = (angle == 90 || angle == 270);
    int newW = isRotated90or270 ? h : w;
    int newH = isRotated90or270 ? w : h;

    BufferedImage rotated = new BufferedImage(newW, newH, img.getType());
    Graphics2D g = rotated.createGraphics();

    // 이미지 중심을 기준으로 회전
    g.translate((newW - w) / 2.0, (newH - h) / 2.0);
    g.rotate(Math.toRadians(angle), w / 2.0, h / 2.0);
    g.drawRenderedImage(img, null);
    g.dispose();

    return rotated;
}

💡 EXIF Orientation 값이 뭘 의미하는지 궁금하다면?

Orientation 태그 값은 1부터 8까지 있으며, 각각의 의미는 다음과 같습니다.

의미 필요한 처리
1 정방향 (기본) 처리 없음
3 180도 뒤집힘 180도 회전
6 시계방향 90도 기울어짐 시계방향 90도 회전
8 반시계방향 90도 기울어짐 시계방향 270도 회전

실전에서 가장 자주 마주치는 케이스는 6입니다. 스마트폰을 세로로 들고 찍은 사진이 대부분 여기에 해당합니다.


⚠️ 주의사항: InputStream은 한 번만 읽을 수 있다

코드를 보면 file.getInputStream()을 두 번 호출하는 것을 눈치채셨을 겁니다. 한 번은 이미지 읽기에, 한 번은 EXIF 읽기에 사용하죠.

InputStream은 한 번 읽으면 소비되기 때문에 재사용이 불가능합니다. 따라서 두 번 getInputStream()을 호출할 수 없는 환경이라면, 미리 바이트 배열로 저장해두고 재사용해야 합니다.

// InputStream을 바이트 배열로 미리 저장
byte[] fileBytes = file.getBytes();

// ByteArrayInputStream으로 필요한 만큼 재사용
BufferedImage originalImage = ImageIO.read(new ByteArrayInputStream(fileBytes));
Metadata metadata = ImageMetadataReader.readMetadata(new ByteArrayInputStream(fileBytes));

이렇게 처리하면 getInputStream()을 여러 번 호출하는 문제를 깔끔하게 피할 수 있습니다.


🎯 정리하면

구분 내용
문제 Java ImageIO가 EXIF의 Orientation 태그를 무시함
원인 스마트폰은 픽셀 데이터(가로)와 회전 정보(메타데이터)를 분리 저장함
해결 metadata-extractor로 EXIF를 직접 읽어 프로그래밍 방식으로 회전 처리

이 문제는 Java 백엔드로 이미지를 처리하는 프로젝트라면 언젠가 반드시 마주치는 고전적인 함정입니다. 저처럼 뒤늦게 삽질하지 마시고, 이미지 업로드 기능을 구현할 때 미리 적용해두시길 강력히 추천합니다.


728x90
반응형

'프로그래밍 > JAVA' 카테고리의 다른 글

[JPA] 연관관계(2)  (0) 2021.07.19
[JPA] 연관관계 (1)  (0) 2021.07.18
[JPA] EntityListeners 활용  (0) 2021.07.17
[JPA] QueryMethod 활용  (0) 2021.07.17
[Spring-Boot] JPA를 활용하여 게시판 페이징 처리 하기  (0) 2021.07.16