[F-Lab 모각코 챌린지 22일차] 해시, enum, 난수

부추·2023년 6월 22일
0

F-Lab 모각코 챌린지

목록 보기
22/66

TIL

  1. HASH란? 왜 쓰는 걸까?
  2. 객체지향적 enum : 로직과 값 묶기
  3. 난수와 의사난수, 진정한 난수를 생성하는 방법?!



1. Hash

Hash, 해시, 해시 함수 등이란 임의의 길이를 갖는 데이터를 고정된 길이의 데이터로 매핑하는 함수, 혹은 그 알고리즘을 말한다.

해시는 다음과 같은 성격을 가진다.

  1. 단방향성 : 원본 데이터에 대해 한 번 해시화된 결과 digest는 다시 원본 데이터로 돌릴 수 없다.
  2. 눈사태 효과 : 원본 데이터의 차이가 아주 작아도 전혀 다른 해시 결과값이 출력된다.

눈사태 효과의 실제 적용을 보자.

MD5("The quick brown fox jumps over the lazy dog") = 9e107d9d372bb6826bd81d3542a419d6 
MD5("The quick brown fox jumps over the lazy dog.") = e4d909c290d0fb1ca068ffaddf22cbd0

마지막의 "." 유무가 결과값의 큰 차이를 불러오는 것을 알 수 있다!

해시는 원본 데이터를 블락 단위로 쪼개서 각 단위로 연쇄적 해시를 적용하는 방식으로 원본 데이터를 박살낸다.


# 왜 해시를 사용하죠

해시를 사용해야할 이유는 너무! 많기 때문에 약간 우문이라 생각하지만 .. 당연하기 때문에 오히려 정리해보겠다.

  1. 암호화를 위해서 : 비밀번호 같은 데이터를 DB에 저장할 때 원본값을 저장하는 것은.. 소매치기가 많이 발생하는 지역 한가운데서 현금을 쥐고 걷고있는 거나 다름없다고 할까. DB 자체가 한 번 털려버리면 돌이킬 수 없는 결과를 낳기 때문에 대부분의 서비스에선 해시로 암호화한 비밀번호를 저장한다.
  2. 데이터 무결성을 확인하기 위해 : 해시 결과값으로 원본 데이터를 알 순 없지만, 해시 결과값을 비교해서 원본 데이터가 같았는지 여부를 확인하는 것은 가능하다. 이를 통해 현재 도착한 데이터에 누군가 조작을 가했는지 여부를 알 수 있다.
  3. 해시 자료구조를 위해 : 자바의 HashMap, HashSet등은 컬렉션 내 데이터 검색을 일반적으로 O(1) 시간 내에 처리한다. 이는 검색하고자 하는 데이터를 해시값으로 저장하고 있기 때문이다. 같은 데이터의 해시값은 항상 같으므로, 해시값을 index로 사용하면 해시 충돌이 나지 않는 일반적인 선에서 매우 빠른 자료 검색이 가능하다.

# 해시 충돌?

해시 충돌이란, 다른 원본 값에 대해 같은 해시 결과값이 출력되는 현상이다. 어떤 해시값과 동일한 원본값을 찾긴 힘들지만, 해시 값이 같은 두 원본값을 찾는 것은 상대적으로 쉽다. (생일 문제 참조)

해시 충돌 쌍이 되는 파일을 만들 수 있으면 할 수 있는 공격이 많아진다. 민감 정보를 해시로 보관하는 DB의 데이터를 바꿔버릴 수도 있고, 전자 서명의 무결성에도 흠집을 낼 수 있다.


# 몇 가지 해시 알고리즘들

일반적으로 MD5와 SHA family가 유명하다. 그러나 상대적으로 짧은 길이를 가진 위의 두 MD5와 SHA-1은 해시 충돌로 인한 크래킹이 가능해서 현대에 와선 사용하지 않는다고 한다!


해시는 사용하는 이유도 타당하고, 전자서명이나 암호화에 매우 많이 사용되기 때문에 하드웨어 자체에서 해시 구현을 위한 가속을 지원한다. (PowerShell의 Get-FileHash, 리눅스의 sha256sum 등)




2. Enum

한마디로 "정적 클래스"라고 부를 수 있다. enum 안에 정의된 클래스들은 싱글톤이다. 예를들어, Game이라는 enum 클래스 안에 여러 개의 게임을 정의한다고 해보자. 기본 문법은 다음과 같다. 클래스 이름을 ","로 구분하는 것이다.

public enum Game {
    MAPLE_STORY,
    LOST_ARK,
    ANIMAL_CROSSING,
    OVER_WATCH,
    LEAGUE_OF_LEGEND;
}

Game이라는 enum 타입 안에 MAPLE_STORY, LOST_ARK, ..., LEAGUE_OF_LEGEND라는 클래스가 정의되었다. 각각의 enum 객체들은 전술했듯 싱글톤이기 때문에 자바 프로세스가 실행되는 동안 한 개 존재하며, 여기저기서 상수처럼 쓰일 수 있다. 기본적으로 toString()은 enum 클래스 이름을 return한다.

public class Client {
    public static void main(String[] args) {
    	// ANIMAL_CROSSING
        System.out.println(Game.ANIMAL_CROSSING);
    }
}

enum 안에 있는 클래스 역시 클래스이기 때문에 필드 값을 가질 수 있다. 영어로 된 게임 enum type에 대해 한국어 이름의 필드값을 추가하고자 한다. 다음과 같이 만든다.

public enum Game {
    MAPLE_STORY("메이플 스토리"),
    LOST_ARK("로스트 아크"),
    ANIMAL_CROSSING("동물의 숲"),
    OVER_WATCH("오버워치"),
    LEAGUE_OF_LEGEND("리그 오브 레전드");
    
    final String koreanName;
    
    Game(String koreanName) { this.koreanName = koreanName; }
}

추가하고 싶은 필드와 관련된 생성자를 선언한다. 그리고 enum 클래스를 작성할 때 생성자를 호출하는 형식이다. enum의 필드값은 바뀌는 경우가 잘 없기 때문에, (값이 자주 바뀐다면 enum을 사용하는 이유가 없어지게 된다. 설계 미스) 보통 필드값은 final로 많이 사용한다.


enum타입의 기본 메소드인 values(), name()은 각각 enum class 배열 자체와 특정 enum class의 이름을 return한다. enum 객체의 필드를 직접 접근할 수 있다.

public class Client {
    public static void main(String[] args) {
        for (Game game : Game.values()) { // values() : enum class 자체를 return
            System.out.println("game.name() : " + game.name()); // name() : 클래스 이름 return
            System.out.println("game.koreanName : " + game.koreanName); // 필드값 접근 가능
        }
    }
}

혹은 아래처럼 메소드로 getter을 작성한 뒤 외부에서 호출할 수 있다.

public enum Game {
    MAPLE_STORY("메이플 스토리"),
    LOST_ARK("로스트 아크"),
    ANIMAL_CROSSING("동물의 숲"),
    OVER_WATCH("오버워치"),
    LEAGUE_OF_LEGEND("리그 오브 레전드");

    private final String koreanName;

    Game(String koreanName) { this.koreanName = koreanName; }
    
    // getter
    String getKoreanName() {
        return koreanName;
    }
}

public class Client {
    public static void main(String[] args) {
        for (Game game : Game.values()) { // values() : enum class 자체를 return
            System.out.println("game.name() : " + game.name()); // name() : 클래스 이름 return
            
            // 메소드 호출
            System.out.println("game.koreanName : " + game.getKoreanName());
        }
    }
}

물론! 필드 값을 여러개 둘 수 있다. 이 경우, 생성자 역시 수정해주어야 한다. 롬복을 이용했다.

@RequiredArgsConstructor
public enum Game {
    MAPLE_STORY("메이플 스토리",20),
    LOST_ARK("로스트 아크",5),
    ANIMAL_CROSSING("동물의 숲",22),
    OVER_WATCH("오버워치",7),
    LEAGUE_OF_LEGEND("리그 오브 레전드",14);

    private final String koreanName;
    private final int year;
}

# Enum엔 메소드도 작성 가능하다

enum에 대해 충분히 알고있다고 생각했는데, 이 사실을 몰랐다. 아니 어렴풋이 알고 있었는데(생성자가 있다는 사실에서부터 이미) 유용하게 사용될 수 있다는 사실을 몰랐다. 이 글을 쓴 이유이기도 하다..

Game enum 클래스에 printInfo() 메소드를 추가했다.

void printInfo() {
    System.out.println("출시된지 " + year + "년 된 유명한 게임, " + koreanName + "!");
}

클라이언트 프로그램에서 각 enum 객체의 printInfo()를 자연스럽게 호출할 수 있다.

public class Client {
    public static void main(String[] args) {
        for (Game game : Game.values()) {
            game.printInfo();
        }
    }
}

혹은, enum 클래스 안에abstract 메소드를 두고 각 enum 객체가 이를 구현하도록 할 수 있다. 익명 클래스/인터페이스를 구현하는 방법과 같다.

@RequiredArgsConstructor
public enum Game {
    MAPLE_STORY("메이플 스토리",20) {
        void printInfo() {
            System.out.println("엔젤릭버스터 출동!");
        }
    },
    LOST_ARK("로스트 아크",5) {
        void printInfo() {
            System.out.println("나는 97돌 언제 만들어보나..");
        }
    },
    ANIMAL_CROSSING("동물의 숲",22) {
        void printInfo() {
            System.out.println("최애 주민은 쭈니");
        }
    },
    OVER_WATCH("오버워치",7) {
        void printInfo() {
            System.out.println("사실 사놓고 몇 판 안함");
        }
    },
    LEAGUE_OF_LEGEND("리그 오브 레전드",14) {
        void printInfo() {
            System.out.println("여기에 쏟아부은 시간동안 다른걸 했다면..");
        }
    };

    private final String koreanName;
    private final int year;
    
    abstract void printInfo();
}

위와 같이 분리하면 각 enum 객체에 독립적인 로직을 수행하게 할 수 있다. 당연히! 구현하게 할 메소드 역시 여러개를 둘 수 있다.


# 그래서 Enum을 사용하면 뭐가 좋냐?

좋으니까 닥치고 써!!!!

가 아니라 , 하나의 enum 클래스 자체에 여러가지 메타 정보를 담을 수 있다는 장점이 있다. 완벽하진 않지만 조금 더 상세히 설명하자면.. 클래스 자체에 대한 정보와 더불어 클래스가 가진 고유의 동작까지도 한번에 저장 가능하다.


위에 예시로 썼던 Game enum을 계속 사용하겠다. 만약 enum 타입 없이 Game 객체를 선언했다면 전반적 내용은 다음과 같을 것이다.

@AllArgsConstructor
public class Game {
    String name;
    String koreanName;
    int year;
}

그리고 Game 객체를 생성하기 위해 런타임에 new를 통해 객체를 하나하나 생성해야 할 것이다. 그럼 그 객체느 싱글톤이 아니게 되고.. 싱글톤으로 구현하려면 동시성 처리를 따로 해줘야하고.. 기타 신경쓸게 많지만 그런건 상관하지 말고 로직 자체를 보자.

Game 객체의 이름에 따라 장르를 나누는 상황이라고 가정해보자. 메이플과 로스트아크는 rpg, 오버워치는 fps, 동물의 숲은 시뮬레이션, 롤은.. 모르겠다. 아무튼 각 게임의 이름에 따라 장르를 출력하는 프로그램을 만들어봤다.

public class Client {
    public static void main(String[] args) {
        Game [] games = {new Game("MAPLE_STORY","메이플스토리",20),
                new Game("LOST_ARK","로스트 아크", 5),
                new Game("ANIMAL_CROSSING","동물의 숲", 22),
                new Game("OVER_WATCH","오버워치", 7),
                new Game("LEAGUE_OF_LEGEND","리그 오브 레전드",14)};

        for (Game game : games) {
            if ("MAPLE_STORY".equals(game.name) || "LOST_ARK".equals(game.name)) {
                System.out.println("RPG 장르");
            } else if ("OVER_WATCH".equals(game.name)) {
                System.out.println("FPS 장르");
            } else if ("ANIMAL_CROSSING".equals(game.name)) {
                System.out.println("시뮬레이션 장르");
            } else {
                System.out.println("기타 장르");
            }
        }
    }
}

만약? games에 객체가 늘어난다면? if로 분기해야하는 장르의 개수가 늘어난다면? if 안에 or로 추가해야하는 게임들이 자꾸만 늘어난다면?!

enum을 사용하면 더 간단해진다. 기존에 사용했던 Game enum은 그대로 둔 채, Genre enum을 추가하자!

@RequiredArgsConstructor
public enum Genre {
    RPG("rpg 장르", Arrays.asList("MAPLE_STORY","LOST_ARK")),
    FPS("fps 장르", Arrays.asList("OVER_WATCH")),
    SIMULATION("시뮬레이션 장르", Arrays.asList("ANIMAL_CROSSING")),
    OTHER("기타 장르", Arrays.asList("LEAGUE_OF_LEGEND")),;

    final String description;
    final List<String> gameList;

    public static Genre getGenre(String gameName) {
        return Arrays.stream(Genre.values()) // 각 장르에 대해
                .filter(genre -> genre.hasGame(gameName)) // gameName을 포함하는 장르가 있다?
                .findAny() // return 해 
                .orElse(OTHER);
    }

    boolean hasGame(String gameName) {
        // gameName에 해당하는 장르 enum return
        return gameList.stream().anyMatch(gameName::equals);
    }
    
    public static void printGenre(String gameName) {
        System.out.println(gameName + "의 장르는 " + getGenre(gameName) + "입니다.");
    }
}

장르에 대한 enum을 추가했다. 각 enum 클래스 내부엔 장르가 포함하는 game을 추가했다. Client main 코드는 다음과 같이 바뀐다.

public class Client {
    public static void main(String[] args) {
        Genre.printGenre("MAPLE_STORY");
        Genre.printGenre("LOST_ARK");
        Genre.printGenre("ANIMAL_CROSSING");
    }
}

같은 기능을 하지만, if문이 없으니 훨씬 깔끔하다!


이외에, 앞서 추상 메소드인 printInfo() 메소드를 각 enum 클래스에서 정의했던 것을 생각해보자. 하나의 추상 메소드를 구상 클래스에서 구현.. 그리고 그것을 호출한 구상 클래스의 타입에 따라 다른 메소드 호출..? 이거 완전 다형성 아닌가?


그렇다. 구상 클래스에 따라 호출되는 메소드가 달라지고, 이것을 클라이언트 측에서 신경쓰지 않는 객체지향의 특성, 다형성!!! 을 구현하기 위해 상위 인터페이스/클래스를 이용한 상속만이 사용되는 것은 아니다!

상속을 통한 다형성 구현에 비해 enum 자체가 가지고 있는 장점이라고 본다면,, JPA를 사용했을 때, @Enumerate를 통해 DB에 enum 값과 더불어 enum 내의 메소드 자체도 저장할 수 있다는 점일듯 하다. 이미 산더미처럼 테이블이 쌓여있는 프로젝트에서 "특정 필드 값이 xx일때 XX로직을 해주세요~" 했을 때 생각없이 짜면 수없이 늘어날 if문 때문에 머리아플 것 같다. 그러나 "enum 타입이 xx일때 XX로직 수행하기" 라고 정해두고 enum 클래스에 각 메소드 구현체를 두면, 특정 값과 특정 로직이 관련있다 라는 코드 가독성도 늘어나고 if문 쭉 쓰거나 간지나게 상속 써보겠다고 인터페이스 새로 만드는것보다 훨씬 간단하게 기능을 구현할 수 있지 않을까???

당연한 얘기지만! 위대하신 멘토님 가로되 if를 쓰는 것이 더 적절한 상황 역시 있다고 한다. 모든 것은 케바케
이게 맞고 저게 맞고가 아니라, 이 상황에서 더 적절하게 적용될 수 있는 것이 무엇인지, 그때 어떤 객체지향적 사고를 할 수 있는지 고민하는 것이 더 중요하다.




3. 난수

난수란? 무작위로 만들어진 수열이다. 무작위라는 생성될 수 범위 내의 모든 수가 등장할 확률이 같다는걸 의미하고(갑자기 메이플 환생의 불꽃 사태가 떠오른다), 이는 어떤 방식으로든지 다음에 나올 수를 예측할 수 없음을 의미한다.

그치만 컴퓨터 과학에서 위와 같이 정의된 "진정한 난수"를 만드는 것은 불가능하다. 특정 상황이나 난수 생성에 들어가는 seed값이 같다면 난수를 생성하는 컴퓨터의 함수는 항상 같은 수를 반환하기 때문이다. 진정한 난수가 아니라 시스템적인 "의사 난수"가 생성된 것이라고 볼 수 있다.

의사 난수란 진정한 난수는 아니지만, 난수로 취급 가능한 수열을 의미한다. 그 값이 충분히 예측할 수 없고 무작위하다면 어느정도 난수로서 지위를 가진 의사 난수라고 부를 수 있는 것이다.


컴퓨터는 의사 난수를 생성하기 위해 정말 다양한 값을 seed로써 사용한다. 현재 시간 이외에도 컴퓨터가 켜져있는 시간 (ns), CPU 메모리의 클럭/온도, pid 혹은 tid 등, 값으로 사용하는 것은 뭐든 사용한다고 보면 된다. 그림을 보면 직전 난수 생성에 사용한 값 역시 새로운 난수 생성에 사용하는 것도 가능하다. 보통 bcrypt 등의 해시를 사용할 때 salt값을 받는 것처럼 사용자의 입력을 seed값으로 사용할 수도 있다. 난수 생성을 위한 또다른 난수
언제 어떤 상황에서든 100% 난수인 진정한 난수는 생성하지 못하더라도, 이런식으로 적당한 난수를 생성하여 반환할 수 있다.


중앙제곱법, 선형합동법 등의 수학적 방법을 이용해서 의사 난수를 생성할 순 있지만 컴퓨터가 진정한 난수를 생성하기 위해선 진정한 난수를 입력으로 받아야 한다. 이 때 자연의 무작위성을 입력으로 받을 수 있다. Cloudflare는 라바 램프를 촬영한 화상의 노이즈 엔트로피를 이용해 난수를 생성한다고 한다.......
헐 ㄱ-..




REFERENCE

https://blog.humminglab.io/posts/tls-cryptography-10-hash/

https://techblog.woowahan.com/2527/

https://bcp0109.tistory.com/334

https://todayscoding.tistory.com/19

https://blog.cloudflare.com/lavarand-in-production-the-nitty-gritty-technical-details/

profile
부추튀김인지 부추전일지 모를 정도로 빠싹한 부추전을 먹을래

0개의 댓글