Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

다운받은 만화 기록용 db 설계 고려 #71

Open
occidere opened this issue Apr 30, 2018 · 5 comments
Open

다운받은 만화 기록용 db 설계 고려 #71

occidere opened this issue Apr 30, 2018 · 5 comments

Comments

@occidere
Copy link
Owner

다운받은 만화 기록용 db 설계 고려

DB 종류

  • RDBMS / NoSQL 등 별도의 db는 너무 용량 증가 이슈가 큼
  • 따라서 HashMap을 Serialize 하여 File로 기록한다.

DB용 HashMap 설계

@occidere
Copy link
Owner Author

occidere commented Apr 30, 2018

import java.io.*;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;

public class Test {
    public static void main(String[] args) throws Exception {
        String dataset[][] = {
                {"원피스", "원피스 2화"},
                {"원피스", "원피스 3화"},
                {"원피스", "원피스 5화"},
                {"나루토", "나루토 5화"},
                {"나루토", "나루토 9화"},
                {"데이터베이스", "단편"}
        };

        Database db = Database.getInstance();

        Arrays.stream(dataset)
                .forEach(data-> {
                    try {
                        db.insert(data[0], data[1]);
                        Thread.sleep(3000);
                    } catch(Exception e) {
                        e.printStackTrace();
                    }
                });

        db.close();
    }
}

class Database implements Serializable {
    private final Map<Integer, TreeSet<Integer>> DATABASE;
    private final transient ScheduledExecutorService DB_EXECUTOR = Executors.newScheduledThreadPool(1);;
    private transient AtomicBoolean executorRunning = new AtomicBoolean(false);

    private static final transient String DEFAULT_DB_PATH = "C:\\Users\\occid\\Desktop";
    private static final transient String DB_NAME = "MMDownloader.db";
    private transient String dbPath;

    public static transient Database instance;
    public static Database getInstance() {
        if(instance == null) {
            instance = new Database();
        }
        return instance;
    }

    private Database() {
        this(DEFAULT_DB_PATH + "/" + DB_NAME);
    }

    private Database(String dbPath) {
        this.dbPath = dbPath;
        DATABASE = getDatabase();

        saveAsync();

        System.out.println("[DB LOAD] " + DATABASE);
    }

    public void insert(String title, String episode) {
        int titleHash = getHashValue(title);
        int episodeHash = getHashValue(episode);

        TreeSet<Integer> episodeSet = DATABASE.getOrDefault(titleHash, new TreeSet<>());
        episodeSet.add(episodeHash);
        DATABASE.put(titleHash, episodeSet);

        System.out.println("[DB INSERT] " + DATABASE);
    }

    private Map<Integer, TreeSet<Integer>> getDatabase() {
        try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(dbPath))){
            return (HashMap) ois.readObject();
        } catch (ClassNotFoundException | IOException e) {
            System.err.println("DB 파일 로딩 실패");
            e.printStackTrace();
            return new HashMap<>();
        }
    }

    private synchronized void writeDatabase() {
        try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(dbPath))){
            System.out.println("Write: " + DATABASE);
            oos.writeObject(DATABASE);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * 비동기 스레드 db Write 작업
     * 한번만 호출되야 한다.
     */
    private void saveAsync() {
        if(executorRunning.compareAndSet(false, true)) {
            System.out.println("saveAsync");
            DB_EXECUTOR.scheduleAtFixedRate(()-> {
                writeDatabase();
            }, 0, 1, TimeUnit.SECONDS);
        }
    }

    private int getHashValue(String str) {
        return str.trim().hashCode();
    }

    public void close() {
        DB_EXECUTOR.shutdown();
        executorRunning.set(false);
        writeDatabase();
        instance = null;

        System.out.println("[DB CLOSED] " + DATABASE);
    }
}

@occidere
Copy link
Owner Author

occidere commented May 2, 2018

HashCode를 구할 대상 지정

image

  • 만화 제목(String title = "원피스")
  • 만화 회차(String episode = "원피스 21화")
  • 비고
    • 만화 제목이나 회차는 변동 가능성이 존재함
    • 따라서 URL에 들어가는 고유 아카이브 주소를 파싱하는 것도 고려해볼 것
      • 단, wasabisyrup 등은 변동 가능성이 매우 크기에, 그 이후의 resource 부분만 사용하는 방향 고려

@occidere
Copy link
Owner Author

occidere commented May 6, 2018

Key 값 선정방식 고려

  • 원래는 title로 정하려 하였으나, 추후 기능확장을 고려하여 각 만화의 고유 id를 사용하는 것이 최선인 듯.
  • 그러나 이 방법의 경우 다운로드 주소를 wasabisyrub 과 같은 archive 주소를 사용한 경우 찾아내기가 어려움.
    • title-subject 부분을 파싱해서 검색 쿼리로 날리는 방법을 고려해 보았으나, 검색 시스템의 품질이 낮아 적용이 어려움 (검색 결과 매칭 방식이 의문)
      image

@occidere
Copy link
Owner Author

occidere commented May 6, 2018

임시 확정 방식

Map<String, String> db = new HashMap<>();
db.put("Qen_ew8cyGg", "골든 카무이 150화");
  • 주의
    • 모든 이미지 다운로드 성공 시에만 DB에 쓰도록 한다. (중간에 1번이라도 Exception 발생 시 제외)

@occidere
Copy link
Owner Author

occidere commented May 6, 2018

[94216f6] 관련 기능 구현된 베타버전(v0.5.1.0) 배포 완료

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant