공공부하자개발 · 영어 학습 노트
자바
실무 확장Excel · 파일 업로드 · DB 연동0/22 완료
  • 01Excel(XLSX) 구조와 순수 JDK로 읽기/쓰기
  • 02Apache POI로 Excel 업로드/다운로드
  • 03파일 업로드/다운로드 서버 (HttpServer)
  • 04JDBC 기초와 트랜잭션 (H2)
  • 05MyBatis 어노테이션 매퍼로 쿼리 연동
  • 06MyBatis XML 매퍼 · Oracle 방언 · PageHelper · Spring Boot
  • 07REST API 서버와 JSON
  • 08Vue 3 SPA 와 Java 서버 연동
  • 09@Scheduled 운영
  • 10로깅 실무: 레벨·계층, MDC 추적, 예외·성능, 마스킹, 롤링, JSON 로그
  • 11외부 API 연동
  • 12테스트 실무
  • 13암호화·개인정보 보호
  • 14인코딩·한글 실무
  • 15@Transactional 심화
  • 16긴 작업 비동기 처리와 진행률
  • 17SFTP·FTP 파일 연계
  • 18로컬 캐시와 @Cacheable
  • 19메일·알림 발송
  • 20웹 보안 체크리스트
  • 21빌드 도구와 폐쇄망 의존성 반입
  • 22성능 측정: p50·p95·p99, 측정 계층, JFR, JMH 함정, 자체 부하 테스트, 병목 순위
사이트 소개개인정보처리방침연락처
© 2026 공부하자
홈 › 실무 확장 › 03 / 22

파일 업로드/다운로드 서버 (HttpServer)

섹션 7진행 0 / 22
1왜 배우는가2핵심 원리3코드 예제4응용 변형 예제5자주 하는 실수 (Tip)6연습 문제7정리‹ 이전다음 ›

6. 연습 문제

문제 1

FileStore.save 에 중복 업로드 방지를 추가하세요. 파일 내용의 SHA-256 을 스트리밍 중에 계산해(DigestInputStream 또는 MessageDigest.update) 메타에 4번째 줄로 기록하고, 같은 해시의 파일이 이미 있으면 새 파일을 저장하지 않고 기존 StoredFile 을 돌려주세요(임시 파일은 삭제). 데모에서 par-0.txt 를 두 번 올려 같은 id 가 나오는지 확인하세요.

정답 보기
java
// FileStore.save 안: 복사 루프를 DigestInputStream 으로 감싼다
MessageDigest md = MessageDigest.getInstance("SHA-256");
try (OutputStream out = new BufferedOutputStream(Files.newOutputStream(tmp));
     InputStream in = new DigestInputStream(body, md)) {
    byte[] b = new byte[8192];
    int n;
    while ((n = in.read(b)) > 0) { /* 기존 검증 로직 그대로 */ size += n; if (size > maxBytes) throw ...; out.write(b, 0, n); }
}
String hash = HexFormat.of().formatHex(md.digest());
StoredFile dup = findByHash(hash);                          // .meta 4번째 줄 비교
if (dup != null) { Files.deleteIfExists(tmp); return dup; }
Files.move(tmp, dest, StandardCopyOption.ATOMIC_MOVE);
Files.writeString(dir.resolve(id + ".meta"), name + "\n" + type + "\n" + size + "\n" + hash, StandardCharsets.UTF_8);

StoredFile findByHash(String hash) throws IOException {
    for (StoredFile f : list()) {
        String[] lines = Files.readString(dir.resolve(f.id() + ".meta"), StandardCharsets.UTF_8).split("\n");
        if (lines.length > 3 && lines[3].equals(hash)) return f;
    }
    return null;   // 데모 한계: 전체 스캔. 파일이 수만 개면 hash → id 인덱스(Map 또는 DB 유니크 컬럼)
}
// 출력: par-0.txt 두 번 업로드 → 같은 id, 목록 파일 수 변화 없음

문제 2

GET /files?format=json 에 페이징과 정렬을 붙이세요. ?page=0&size=20&sort=size,desc 를 받아 {"total":N,"page":0,"items":[...]} 를 돌려줍니다. sort 는 name, size 두 가지, 방향은 asc/desc. 잘못된 값은 400 을 반환하세요.

정답 보기
java
server.createContext("/files", ex -> {
    Map<String, String> q = HtmlPages.query(ex.getRequestURI().getRawQuery());
    try {
        int page = Integer.parseInt(q.getOrDefault("page", "0"));
        int size = Integer.parseInt(q.getOrDefault("size", "20"));
        String[] sort = q.getOrDefault("sort", "name,asc").split(",");
        Comparator<FileStore.StoredFile> cmp = switch (sort[0]) {
            case "name" -> Comparator.comparing(FileStore.StoredFile::originalName);
            case "size" -> Comparator.comparingLong(FileStore.StoredFile::size);
            default -> throw new IllegalArgumentException("sort=" + sort[0]);
        };
        if (sort.length > 1 && sort[1].equals("desc")) cmp = cmp.reversed();
        else if (sort.length > 1 && !sort[1].equals("asc")) throw new IllegalArgumentException("방향=" + sort[1]);
        if (page < 0 || size <= 0 || size > 100) throw new IllegalArgumentException("page/size 범위");

        List<FileStore.StoredFile> all = store.list();
        List<FileStore.StoredFile> items = all.stream().sorted(cmp).skip((long) page * size).limit(size).toList();
        HtmlPages.send(ex, 200, "application/json; charset=UTF-8",
                "{\"total\":" + all.size() + ",\"page\":" + page + ",\"items\":" + HtmlPages.listJson(items) + "}");
    } catch (IllegalArgumentException e) {                   // NumberFormatException 포함
        HtmlPages.send(ex, 400, "text/plain; charset=UTF-8", "잘못된 파라미터: " + e.getMessage());
    }
});
// 출력: GET /files?format=json&page=0&size=2&sort=size,desc
//   {"total":10,"page":0,"items":[{"id":"...","name":"회원명단.csv","size":64,...},{"id":"...","name":"par-7.txt","size":6,...}]}
// GET /files?sort=date → 400 잘못된 파라미터: sort=date

문제 3

DownloadHandler 에 조건부 요청을 붙이세요. 응답에 ETag: "<id>" 와 Last-Modified 를 넣고, 요청에 If-None-Match 가 같은 ETag 이면 본문 없이 304 Not Modified 를 반환합니다. Range 요청에 If-Range 가 있고 ETag 와 다르면 Range 를 무시하고 200 전체를 보내세요.

정답 보기
java
String etag = "\"" + f.id() + "\"";                                          // 내용 불변(UUID = 버전) 이므로 id 가 곧 ETag
h.set("ETag", etag);
h.set("Last-Modified", DateTimeFormatter.RFC_1123_DATE_TIME.format(
        Files.getLastModifiedTime(f.path()).toInstant().atZone(ZoneOffset.UTC)));

if (etag.equals(ex.getRequestHeaders().getFirst("If-None-Match"))) {
    ex.sendResponseHeaders(304, -1);                                          // 본문 없음
    ex.close();
    return;
}

String range = ex.getRequestHeaders().getFirst("Range");
String ifRange = ex.getRequestHeaders().getFirst("If-Range");
if (range != null && ifRange != null && !ifRange.equals(etag)) range = null;  // 버전이 바뀌었으면 전체 전송
// 이후 기존 Range 처리 그대로
// 출력:
//   GET (If-None-Match: "6089a292-...")           → 304, 본문 없음
//   GET (Range: bytes=0-9, If-Range: "다른값")     → 200, 64바이트 전체
//   GET (Range: bytes=0-9, If-Range: "6089a292-...") → 206, 10바이트
연습 문제
  • 문제 1
  • 문제 2
  • 문제 3
이전 섹션5 자주 하는 실수 (Tip)6 / 7다음 섹션7 정리