프로그래밍 언어에는 크게 두가지 타입이 있습니다.

1. 정적인 언어 : 컴파일 때 이미 변수에 대한 데이터 타입이 확정되어 다른 데이터 타입을 할당할 수 없는 언어입니다.

예를들어 C, C++, JAVA와 같은 언어가 여기에 속합니다.

2. 동적인 언어 : 여기 언어들은 변수에 다른 데이터 타입을 할당할 수 있습니다. Ruby, Python 등의 언어가 있죠.

 

자바는 정적인 언어이기 때문에 변수를 사용하려면 데이터 타입을 꼭 지정해줘야 합니다. 자바에서 사용되는 타입은 다음과 같습니다.

https://www.geeksforgeeks.org/data-types-in-java/?ref=lbp

자바의 데이터 타입은 크게 두 가지 Primitive 와 Non-Primitive 타입으로 나눌 수 있습니다. 

- Primitive Data Type : boolean, char, int, short, byte, long, float, double

- Non-Primitive Type : String, Array 등


Primitive Data Type

Primitive 데이터는 오직 한 값만 가지며 다른 특수한 기능이 없습니다. 그 값 자체라고 생각하면 됩니다.

boolean, byte, short, int, long, float, double 이렇게 8가지 데이터 타입이 있으며 하나씩 살펴보겠습니다.

 

먼저 각 자료형마다 표현할 수 있는 범위가 정해져 있습니다. boolean은 1bit, byte는 8bit 처럼 말이죠.

여기서 1bit는 0 또는 1로 표현되는 한자리 값입니다. 즉, 2^1 = 2 가지 값을 표현할 수 있습니다.

마찬가지로 1byte(= 8bit)는 2진수로 00000000 ~ 11111111 사이의 값을 가지며 2^8 = 256 가지 값을 표현합니다.

데이터의 표현 범위는 여기까지 하고 한 자료형씩 보겠습니다.

 

1. boolean : 1bit의 값으로 참/거짓(true/false)만을 나타내는 자료형입니다. 

True는 1, False는 0의 값으로 표현합니다. 두 가지 값만 필요하니 1bit로 모든 값을 표현할 수있습니다.

class Test {
    public static void main(String args[])
    {
        boolean b = true;
        if (b == true)
            System.out.println("Hi boolean"); // Hi boolean 출력
    }
}

 

 

2. byte : 8 bit(1 byte)의 값으로 -128 ~ 127 범위의 값을 표현할 수 있습니다.

정수 자료형 중 가장 작은 범위를 지니고 있습니다. 8bit 이므로 2^8=256 개의 값인 -128~127을 표현합니다. 범위안에 0이 있어 128까지가 아님에 유의합니다.  

class Test {
    public static void main(String args[])
    {
        byte a = 126;
 
        System.out.println(a); // 126 출력
 
        a++;
        System.out.println(a); // 127 출력
 
        // 표현 가능한 범위(-128 ~ 127)를 넘었으므로 -128로 돌아갑니다.
        a++;
        System.out.println(a); // -128 출력
 
        // -128에서 1이 증가합니다.
        a++;
        System.out.println(a); // -127 출력
    }
}

 

3. short : 16 bit(2 byte)의 값으로 -32,768 ~ 32,767 범위의 값을 표현합니다.

2^16= 65,536개의 값을 표현하며, byte와 마찬가지로 중간에 0을 포함하여 양수의 갯수가 하나 더 적습니다. 

 

4. int : 32 bit(4 byte)의 값으로 -2,147,483,648 to 2,147,483,647 범위의 값을 표현합니다.

정수형 중 가장 많이 사용되는 자료형입니다. 범위를 봤을 때 대부분의 변수사용이 이 범위로 커버가 가능하므로 가장 많이 사용됩니다. 만일 사용하는 변수가 21억 이상의 값이 할당될 가능성이 있다면 더 큰 자료형인 long을 사용해야 합니다.

 

5. long : 64 bit(8 byte)의 값으로 -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807 범위의 값을 표현합니다.

2^64개의 변수를 표현합니다. 값이 엄청나게 커진걸 볼 수 있습니다. 변수를 선언하게 되면 그 공간만큼 메모리가 할당되므로 사용하는 변수에 맞는 자료형을 선택해야 합니다.

물론 요즘 사용하는 하드웨어들이 메모리가 빵빵하기 때문에 자료형 몇개로 부족해지지는 않겠지만 습관을 들이는게 좋습니다.

 

6. float : 32 bit(4 byte)의 값으로 실수를 표현합니다.

위 5개 자료형과는 다르게 실수형 데이터 타입입니다. 실수형이란 소수점을 가진 값으로 이 소수점을 표현하기 위해 가진 bit중 일부를 사용합니다. 소수임을 알리기 위해 값에 f를 붙혀 할당합니다.

public class Main {
    public static void main(String[] args) {
        float number = 0.9f;
        System.out.println(number); // 0.9 출력
    }
}

 

7. double : 64 bit(8 byte)의 값으로 실수를 표현합니다.

float와 마찬가지로 소수점을 표현하기 위해 bit 일부를 사용하지만 f를 붙혀주지 않아도 됩니다.

public class Main {
    public static void main(String[] args) {
        double number = 0.09;
        System.out.println(number); // 0.09 출력
    }
}

 

8. char : 16 bit(2 byte)의 유니코드 문자를 표현합니다.

표현범위로는 '\u0000' (0) to '\uffff' (65535) 입니다. '\u' 는 유니코드라는 의미이고 16진수로 '0000'(0) ~ 'ffff'(65535) 까지 값이 할당된다는 의미입니다.  

자바에서는 문자를 표현할 때 2바이트를 사용하고 있지만 C, C++은 1바이트를 사용합니다. C, C++은 아스키(ASCII)로 문자를 표현하여 일부 문자가 표현이 불가능 합니다. 하지만 자바는 문자(Character)에 Unicode를 사용하므로 대부분 나라의 언어를 포함하여 표현이 가능합니다.

 

아래 테스트를 보면 char형으로 문자 한개를 표현할 수 있고, int 자료형이 4byte로 더 크므로 할당이 가능한 걸 볼 수 있습니다. 

public class Test {
    public static void main(String[] args) {
        char a = 'a';
        char b = 'b';
        System.out.println(a); // a 
        System.out.println(b); // b

        int intFromChar1 = a;
        int intFromChar2 = b;
        System.out.println(intFromChar1); // 97
        System.out.println(intFromChar2); // 98
        
        char x = 'ㄱ';
        char y = 'ㄲ';
        System.out.println(x); // ㄱ
        System.out.println(y); // ㄲ

        int intFromChar3 = x;
        int intFromChar4 = y;
        System.out.println(intFromChar3); // 12593
        System.out.println(intFromChar4); // 12594
    }
}

 

 


 

 

 

템플릿 엔진(Template Engine)

템플릿 엔진이란 템플릿 양식 입력 자료를 합성하여 결과 문서를 출력하는 소프트웨어를 말한다.
그 중 웹 템플릿 엔진은 브라우저에서 출력되는 문서를 위한 소프트웨어이다.

  1. 고정적으로 사용될 부분을 템플릿을 미리 작성해 놓고
  2. 동적으로 변경될 데이터 부분만 필요시 결합하여
  3. 화면(문서)을 완성한다.

어디서 결합하냐에 따라 서버 사이드 템플릿 엔진과 클라이언트 사이드 템플릿 엔진으로 분류할 수 있다.

  • 서버 사이드 템플릿 엔진 서버에서 데이터와 미리 정의된 템플릿으로 HTML을 완성하여 클라이언트에게 전달한다.
    Freemarker, Thymeleaf, Handlebars(Handlebars.java), JSP 등

  • 클라이언트 사이드 템플릿 엔진
    데이터와 템플릿을 합쳐 클라이언트에서 HTML을 완성한다.
    서버는 api 콜에대한 응답데이터만 제공하면서 화면 랜더링은 클라이언트가 담당하는 구조가 가능하다.
    Handlebars(Handlebars.js), EJS(Embedded Javascript Templates) 등

클라이언트 사이드 템플릿 엔진의 필요성

  • 계속해서 페이지를 이동하면서(서버에 데이터를 요청하면서) 화면이 변경되는 것이 아니라, 단일 화면에 특정 이벤트에 따라 화면이 변경되어야 하는 경우 javascript로 html을 변경해야 한다.
  • 조작해야할 코드량이 많아지면 관리가 어려우므로 직접 javascript로 DOM을 제어하는 대신에 클라이언트 사이드 템플릿 엔진을 이용하면 좋다.

람다표현에서는 바깥쪽 스코프의 변수(static 변수, 지역 변수, 인스턴스 변수)를 참조 할 수 있으며 이를 capturing lambda 라고 한다. 

그 중 지역 변수를 참조하는 경우에만 컴파일러는 해당 변수가 final 또는 effectively final 인지 체크하여 아니라면 컴파일 오류를 표출하게 되는데 이에 대해서 알아보고자 한다.

 

아래 메서드는 람다를 리턴한다. 하지만 람다표현식에서 함수의 파라미터(지역변수)를 사용하고 있으며 이를 증가시키고 있으므로 컴파일 오류를 표출한다.

Supplier<Integer> incrementer(int start) {
    return () -> start++; // local variables referenced from a lambda expression must be final or effectively final
}

위 코드의 start 지역변수는 final로 선언되지 않았으며 값을 변경했으므로 effectively final이 아니다.

 > effective final : final은 붙지 않았지만 할당 후 값을 변경하지 않아 final과 같은 효과

 

컴파일 되지 않는 기본적인 이유는 람다는 참조하는 지역변수에 대한 복사본을 가지고 동작하기 때문이다.


람다는 왜 참조하는 외부스코프 지역변수에 대해 복사본을 가지고 동작할까?

위 코드에서 incrementer 함수는 실행되면서 start를 증가시키지 않고 람다를 리턴할 뿐이다.

incrementer 함수의 실행이 끝난 후의 다른 시점에서 리턴한 람다표현식이 실행되어 start 지역변수가 증가할 수 있으며, 그 시점이 오기전에 GC에 의해서 start 지역변수가 정리될 수도 있다.(함수의 실행이 끝나면 지역변수는 GC에 의해 정리대상이 된다.) 그러므로 실행 시점에 지역변수가 사라질 것을 방지하지 위해서 람다는 복사본을 생성하여 동작하게 된다.(또한 지역변수는 Stack영역에 저장되기 때문에 공유자원이 아니므로 다른 thread에서 람다가 실행된다면 변수에 접근할 수 없다.)

 

복사본을 가지고 미래에 동작할 예정인데 복사본에서 값이 바뀌어 버리면(= final or effective final이 아니면, = 최종상태값과 복사본값이 다르면) 오류를 발생할 여지가 생기므로 컴파일 단계에서 막는 것이다.


static 변수와 인스턴스 변수의 경우에는?

private int start = 0;
Supplier<Integer> incrementer() {
    return () -> start++;
}

지역변수가 아닌 인스턴스 변수는 컴파일 에러도 나지 않으며 정상 동작한다. 인스턴스 변수는 Heap영역에 저장되고 static 변수는 Method영역에 저장되며 두 영역모두 공유자원이므로 다른 thread에서 접근이 가능하다. 자유롭게 접근이 가능하므로 실행되는 시점에 최종상태값을 읽어 증가시킬 수 있다. 그러니 변수에 대한 제약조건이 없는 것이다.

 

이에대한 간단한 예제 코드이다.

변수 holder가 참조하는 배열객체는 heap 영역에 저장되어 자유롭게 참조가 가능하다. 그러므로 sums.sum()로 람다가 실행되는 시점에 holder[0]의 최종상태값을 읽어들여 계산하므로 6이 리턴된다.

public int workaroundSingleThread() {
    int[] holder = new int[] { 2 };
    IntStream sums = IntStream
      .of(1, 2, 3)
      .map(val -> val + holder[0]);

    holder[0] = 0;

    return sums.sum();
}

 

 


읽어주셔서 감사합니다. 도움이 되셨다면 광고 클릭 부탁드립니다.

모두 힘내세요! : )

 

 

 

'개발 > JAVA' 카테고리의 다른 글

NullpointerException 과 해결법  (0) 2020.12.09
[Java 기본] Java Primitive 데이터 타입  (0) 2020.12.09
equals, hashcode  (0) 2020.12.07
JVM, JRE, JDK 간단 개념  (0) 2020.12.07
ExecutorService 사용법  (0) 2020.12.01

롬복의 @EqualsAndHashCode 애노테이션을 보다가 문득 equals()와 hashCode() 메서드에 대해서 잘 모른다는 사실을 알고 한번 찾아보고 정리한다.

Object 클래스에 정의된 equals(Object obj) 메서드를 보면 ‘==’ 비교결과를 반환한다.

class Object{
    public boolean equals(Object obj) {
        return this == obj;
    }    
}

그럼 ‘==’ 비교는 무엇을 뜻하는 것일까? 참조형 변수끼리 ‘==’ 비교를 한다면 ‘동일’ 객체를 참조하고 있는지 확인하는 것이다.
(논리적으로 같은 객체가 아니다.)

class Game{
    int star;

    public Game(int star) {
        this.star = star;
    }
}
Game game1 = new Game(1);
Game game2 = game1;

// '동일' 객체를 참조하므로 game1 == game2
Game game1 = new Game(1);
Game game2 = new Game(1);

// 같은 프로퍼티를 가지므로 논리적으로 동일한 객체지만 
// '동일' 객체를 참조하지 않으므로 game1 != game2

하지만 개발을 하다보면 ‘동일’한 객체인지 체크하는 것보다 논리적으로 ‘동등’한지(모든 프로퍼티가 같은 객체인지) 체크할 상황이 더 많다.
이런 경우에는 보통 클래스에 Object.equals(Object obj)를 재정의한다.

class GameWithEquals{
    int star;

    public GameWithEquals(int star) {
        this.star = star;
    }

    @Override
    public boolean equals(Object game) {
        if(!(game instanceof GameWithEquals)) return false;
        return ((GameWithEquals) game).star == this.star;
    }
}
@Test
public void 재정의한_equals_동등비교(){
    GameWithEquals game1 = new GameWithEquals(1);
    GameWithEquals game2 = new GameWithEquals(1);

    assertTrue(game1.equals(game2));
}

단순히 ‘==’ 비교를 하던 Object.equals(Object obj)를 재정의 했고, 객체간의 동등비교가 필요하면 이 함수를 활용하면 되니 모든 문제가 해결되었다고 생각할 수 있지만 equals()와 짝으로 hashcode()메서드도 재정의 되어야 한다.

왜냐하면 컬렉션들(HashSet, HashMap, HashTable …)은 객체가 동등한지 비교할 때 equals()와 hashcode()를 둘다 체크하기 때문이다. 예를들어 equals()만 재정의한 경우에는 다음 테스트를 통과하지 못한다.

@Test
public void Map은_키값비교를_equals만으로_하지않는다(){
    HashMap<GameWithEquals, String> map = new HashMap<>();
    GameWithEquals game1 = new GameWithEquals(1);
    map.put(game1, "HI");

    GameWithEquals game2 = new GameWithEquals(1);
    assertTrue(game1.equals(game2)); // True
    assertEquals(map.get(game2), "HI"); // null반환
}

컬렉션은 키값 객체를 비교할 때 equals()와 hashcode()를 모두 체크하므로 재정의 하지 않은 hashcode()에 의해 테스트는 실패하게 된다.

 

Object.hashCode()는 native로 정의되어 있으며 실제 참조하는 객체에 대한 교유값이므로 ‘==’ 비교와 동일하게 다른객체를 참조한다면 서로 다른값이 반환된다. 논리적인 비교가 되기 위해서는 equals와 마찬가지로 가진 프로퍼티를 이용하여 hashCode를 반환하도록 오버라이드 한다.

 

class GameWithEqualsAndHashCode{
    int star;

    public GameWithEqualsAndHashCode(int star) {
        this.star = star;
    }

    @Override
    public boolean equals(Object game) {
        if(!(game instanceof GameWithEqualsAndHashCode)) return false;
        return ((GameWithEqualsAndHashCode) game).star == this.star;
    }

    @Override
    public int hashCode() {
        return Objects.hash(star);
    }
}
@Test
public void Map은_키값비교를_equals와_hashCode로_한다(){
    HashMap<GameWithEqualsAndHashCode, String> map = new HashMap<>();
    GameWithEqualsAndHashCode game1 = new GameWithEqualsAndHashCode(1);
    map.put(game1, "HI");

    GameWithEqualsAndHashCode game2 = new GameWithEqualsAndHashCode(1);
    assertTrue(game1.equals(game2)); // True
    assertEquals(map.get(game2), "HI"); // HI반환, True
}

정리하면,
객체간의 논리적인 비교를 위해서는 equals()와 hashCode()를 모두 재정의 해야한다.
재정의는 개발자가 작성해도 되지만 IDE의 자동완성이나 Lombok(@EqualsAndHashCode)의 도움으로 자동작성 할 수도 있다.

 

인텔리제이에서 Run/Debug Configuration을 들어가면 Configuration에서 지정할 수 있는 옵션중에 다음 3가지가 있다.
스프링부트에서 프로파일을 지정하고 싶어서 spring.profiles.active=dev 옵션을 주고 싶은데 지정할 포인트가 3가지나 있으니 혼란스러워 정리해보고자 한다.


VM options

JVM이 어플리케이션을 구동시키면서 참고할 옵션을 지정한다.
Intellij로 스프링부트 어플리케이션을 구동시 ‘dev’ 프로파일 설정을 주고 싶다면 -Dspring.profiles.active=dev 옵션을 지정한다.
콘솔화면 상단의 실행 커맨드에서 -D로 옵션이 지정되는걸 볼 수 있다.


Program arguments

자바 메인함수에 String[] args에 바인딩 될 프로그램 파라미터이다.

Intellij에서 옵션을 지정하면 콘솔화면 상단의 실행 커맨드에서 메인클래스 뒤에 인자로 들어가는걸 볼 수 있다.

 

spring.profiles.active=dev 로 지정시 메인함수의 String[] args 안에 spring.profiles.active=dev 문자열 자체가 들어가기는 하지만 스프링부트에서 프로파일로 인식되지는 않는다.(기본 default 프로파일로 동작한다.)
스프링부트에서 외부설정으로 인식되게 하기 위해서는 --spring.profiles.active=dev 처럼 ‘-‘를 2개 붙혀서 옵션을 줘야한다. 1개도 안된다 꼭 2개로 주자.


Environment variables

어플리케이션 구동시 OS의 환경변수에 더해서 지정해 줄 수 있는 key=value 옵션이다.
여러개를 지정하고 싶으면 세미콜론(;)으로 구분하여 지정한다. 설정에서 OS의 환경변수를 모두 제외시킬 수도 있다.
스프링부트 프로파일 옵션을 주고 싶다면 spring.profiles.active=dev 을 입력하며 된다.


3가지 방법으로 모두 프로파일 지정이 가능하며, 각기 다른 프로파일을 지정해서 우선순위를 테스트 해보면 Program arguments로 지정한 프로파일로 스프링부트가 동작한다.
두번째는 VM options, 마지막은 Environment variables 이다.

  • VM options : -Dspring.profiles.active=dev
  • Program arguments : —spring.profiles.active=dev « 우선순위가 제일 높다.
  • Environment variables : spring.profiles.active=dev

 


읽어주셔서 감사합니다. 도움이 되셨다면 광고 클릭 부탁드립니다.

모두 힘내세요! : )

 

 

 

JVM

  • 컴파일된 바이트 코드(.class)파일을 OS에서 실행 가능한 코드로 변환하여 실행한다. 코드 변환을 위해서 인터프리터와 JIT 컴파일러가 동작한다.
  • JVM은 표준스팩이 존재하며 스팩에 따라 구현한 여러 구현체들이 있다.
  • OS에 종속적이기 때문에 윈도우용 JVM, 맥용 JVM 이런식으로 여러 종류가 있다.(밴더에 따라서도 구분됨)
  • JVM은 단독으로 배포되지 않고 JRE에 포함되어 배포된다.

JRE

  • Library를 포함하여 자바프로그램 실행에 필요한 것들을 모아둔 배포판이다.
  • JVM과 핵심 라이브러리, 프로퍼티, 리소스 파일 등을 가지도 있다.

JDK

  • JRE + 개발에 필요한 툴

JVM Architecture

JVM은 컴파일된 클래스파일(바이트 코드)을 OS 위에서 실행하며 실행을 위해 다음 3가지 파트로 나눠진다.

1. 클래스로더 시스템

Loading, Linking, Initialization 3가지 동작으로 클래스를 로드하고 정보를 저장한다.

  • Loading : 클래스 파일을 읽고 클래스 정보를 메소드 영역에 저장한다. 로딩이 완료된 클래스는 해당 클래스 타입의 Class 객체를 생성하여 힙 영역에 저장한다.
  • Linking : Verify, Prepare, Resolve 3가지 단계로 이루어져 있다.
  • Initialization : static 변수의 값을 할당하고 static 블록을 실행한다.

클래스파일(.class)을 로드하는 클래스로더(ClassLoader)는 계층형 구조를 가진다.

  • Bootstrap ClassLoader : $JAVA_HOME/jre/lib/rt.jar 의 클래스 파일들을 로드한다. JVM을 실행하기 위한 핵심 파일들이 로드된다.
  • Extension ClassLoader : $JAVA_HOME/jre/lib/ext 또는 java.ext.dirs 옵션에 정의된 디렉터리의 클래스 파일들을 로드한다.
  • Application ClassLoader : Classpath로 지정한 디렉터리의 클래스 파일을 로드한다. 보통 개발한 클래스 파일들이 여기서 로드된다.

로드한 클래스를 찾을 때는 Application ClassLoader를 시작으로 해서 못찾으면 상위 ClassLoader에게 찾는 클래스를 위임한다. 최상단의 Bootstrap ClassLoader에서도 클래스를 찾지 못하면 익숙한 ClassNotFoundException 이 발생한다.


2. 다양한 메모리

  • 메소드 영역

클래스 레벨의 정보들이 저장된다.(클래스 이름, 부모 클래스 이름, 메서드, 변수, static 변수 등의 정보)
JVM마다 오직 하나의 메소드 영역을 가지며 공유자원이다.
공유자원 : 다른 영역의 메모리에서도 참조가 가능하다.


  • 힙 영역

모든 객체(인스턴스)들의 정보가 힙에 저장된다.
JVM마다 하나의 힙 영역을 가지며 메소드 영역과 같이 공유자원이다. 메소드, 힙 영역은 공유자원으로 여러 쓰레드에서 접근하여 데이터를 가져갈 수 있다. (thread-safe 하지 않는다.)


  • 스택 영역

JVM은 쓰레드마다 하나의 런타임 스택을 생성해주며 이 스택들이 스택 영역에 저장된다.
쓰레드가 가진 스택은 메서드 콜들이 스택 프레임이라 부르는 블럭으로 쌓여있다.(오류가 발생하면 콘솔에 찍히는 그 형태)
공유자원이 아니므로 Thread-safe하며 Thread가 종료될 때 해당 스택은 JVM에 의해 제거된다.


  • PC 레지스터 영역

각 쓰레드마다 분리된 PC 레지스터를 가지며 현재 실행중인 스택 프레임을 가리키는 포인터가 저장된다. 쓰레드가 다음 실행 스택 프레임으로 넘어가면 PC 레지스터도 업데이트 된다.


  • 네이티브 메소드 스택 영역

각 쓰레드마다 분리된 네이티브 메소드 스택 영역을 가진다. 네이티브 메소드 정보가 저장된다.
네이티브 메소드 : 메소드 선언에 native 키워드가 붙어있고 구현은 C나 C++같이 자바가 아닌 다른 언어로 구현되어 있는 메소드


3. 실행 영역

인터프리터, JIT 컴파일러, Garbage Collector로 구성되어 있다.

  • 인터프리터 : 바이트코드는 인터프리터로 한줄씩 네이티브 코드로 바꿔가며 어플리케이션이 실행된다.
  • JIT 컴파일러 : 매번 라인을 새로 읽으며 실행하지 않고 반복되는 코드는 JIT 컴파일러가 모두 네이티브 코드로 바꿔둔다. (효율성을 위해서)
  • GC : 더이상 사용되지 않는(참조되지 않는) 객체들은 GC에 의해 정리된다.

 


읽어주셔서 감사합니다. 도움이 되셨다면 광고 클릭 부탁드립니다.

모두 힘내세요! : )

 

 

 

컨트롤러 핸들러메소드의 인자에 HTTP요청의 데이터를 자동으로 바인딩 받을 수 있다.
바인딩 받을 변수중에 LocalDateTime 타입의 변수가 있다면, 약속된 패턴으로 스트링 데이터를 보내주거나, VO에서 별도의 애너테이션(@JsonFormat, @DateTimeFormat) 설정을 해줘야 하는데 이에 관해 정리하고자 한다. 결과가 궁금한경우 테스트코드를 건너뛰고 결론만 봐도 된다.

RequestBody(JSON 데이터를 수신하는 경우)

  • yyyy-MM-ddTHH:mm:ss 패턴으로 JSON 데이터를 보내면 VO에 애노테이션을 지정하지 않아도 바인딩된다.
// VO
public class TestVo {
    private LocalDateTime ldt;
    private String name;
}

// Controller
@RequestMapping(path = "/requestBody", method = RequestMethod.POST)
@ResponseBody
public String rbPost(@RequestBody TestVo dto){
    System.out.println("dto : " + dto);
      return "good";
}

// Test Code
@Test
public void POST_리퀘스트바디_기본패턴() throws Exception {
    mockMvc.perform(post("/requestBody")
                    .contentType(MediaType.APPLICATION_JSON_UTF8)
                    .content("{\"name\":\"yangs\", \"ldt\":\"2018-12-15T10:11:22\"}"))
            .andExpect(status().isOk())
            .andDo(print());
}
TestVo(ldt=2018-12-15T10:11:22, name=yangs)

yyyy-MM-ddTHH:mm:ss 패턴이 아니라면 @JsonFormat을 VO에 지정해줘야 한다.

  • @JsonFormat은 스프링부트에서 json 파싱에 사용되는 기본 라이브러리인 Jackson에 포함되어있다.
  • 예를들어 날짜와 시간사이에 공백이 들어간 yyyy-MM-dd HH:mm:ss 패턴으로 데이터를 보내는 경우
    @JsonFormat pattern 속성에 동일하게 세팅만 되면 정상적으로 파싱 및 바인딩 된다.
// VO
public class TestVo {
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
    private LocalDateTime ldt;
    private String name;
}

// Controller
@RequestMapping(path = "/requestBody", method = RequestMethod.POST)
@ResponseBody
public String rbPost(@RequestBody TestVo dto){
    System.out.println("dto : " + dto);
      return "good";
}

// Test Code
@Test
public void POST_리퀘스트바디_기본패턴() throws Exception {
    mockMvc.perform(post("/requestBody")
                    .contentType(MediaType.APPLICATION_JSON_UTF8)
                    .content("{\"name\":\"yangs\", \"ldt\":\"2018-12-15 10:11:22\"}"))
            .andExpect(status().isOk())
            .andDo(print());
}
TestVo(ldt=2018-12-15T10:11:22, name=yangs)

스프링 라이브러리의 @DateTimeFormat 으로 패턴을 준 경우는 제대로 동작하지 않고 오류난다.

  • Jackson에서 json파싱시 @DateTimeFormat을 바라보지 않는다.(Jackson 라이브러리 내에서 파싱하므로 스프링의 @DateTimeFormat을 알지 못한다.)
// VO
public class TestVo {
  @DateTimeFormat(pattern = "yyyy/MM/dd HH:mm:ss")
  private LocalDateTime ldt;
  private String name;
}

// Controller
@RequestMapping(path = "/requestBody", method = RequestMethod.POST)
@ResponseBody
public String rbPost(@RequestBody TestVo dto){
  System.out.println("dto : " + dto);
  return "good";
}

// Test Code
@Test
public void POST_리퀘스트바디_기본패턴() throws Exception {
  mockMvc.perform(post("/requestBody")
                  .contentType(MediaType.APPLICATION_JSON_UTF8)
                  .content("{\"name\":\"yangs\", \"ldt\":\"2018-12-15 10:11:22\"}"))
          .andExpect(status().isOk())
          .andDo(print());
}
JSON parse error: Cannot deserialize value of type `java.time.LocalDateTime` from String "2018-12-15 10:11:22"

RequestBody로 Json을 바인딩 받을때는 @JsonFormat을 사용한다!


RequestParam

  • yyyy-MM-ddTHH:mm:ss 패턴에 아무런 VO설정이 없으면 String을 LocalDateTime으로 변경할 수 없다는 메시지로 실패한다.(RequestBody는 아무런 설정없이 동작했다.)
  • Json과 관련이 없기 때문에 @JsonFormat을 지정해도 위 케이스와 같은 오류로 실패한다.
// VO
public class TestVo {
    private LocalDateTime ldt;
    private String name;
}

// Controller
@RequestMapping(path = "/requestParam", method = RequestMethod.GET)
@ResponseBody
public String rpGet(@RequestParam TestVo dto){
    System.out.println("dto : " + dto);
    return "good";
}

// Test Code
@Test
public void POST_리퀘스트파람_기본패턴() throws Exception {
    mockMvc.perform(get("/ma?name=yangs&ldt=2020-03-18T18:25:40"))
            .andExpect(status().isOk())
            .andDo(print());
}
Failed to convert from type [java.lang.String] to type [java.time.LocalDateTime] for value '2020-03-18T18:25:40';

  • yyyy-MM-ddTHH:mm:ss 패턴은 @DateTimeFormat에서 T 문자열 때문에 오류난다.
// VO
public class TestVo {
    @DateTimeFormat(pattern = "yyyy-MM-ddTHH:mm:ss")
    private LocalDateTime ldt;
    private String name;
}

// Controller
@RequestMapping(path = "/requestParam", method = RequestMethod.GET)
@ResponseBody
public String rpGet(@RequestParam TestVo dto){
    System.out.println("dto : " + dto);
    return "good";
}

// Test Code
@Test
public void POST_리퀘스트파람_기본패턴() throws Exception {
    mockMvc.perform(get("/ma?name=yangs&ldt=2020-03-18T18:25:40"))
            .andExpect(status().isOk())
            .andDo(print());
}
java.lang.IllegalArgumentException: Unknown pattern letter: T
  • T대신 공백을 추가한 yyyy-MM-dd HH:mm:ss 패턴으로 @DateTimeFormat을 지정하면 정상 바인딩된다.
    yyyy/MM/dd HH-mm-ss, yyyyMMddHHmmss, dd-MM-yyyy HH:mm:ss 패턴들도 @DateTimeFormat을 지정시 정상 바인딩된다.
// VO
public class TestVo {
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime ldt;
    private String name;
}

// Controller
@RequestMapping(path = "/requestParam", method = RequestMethod.GET)
@ResponseBody
public String rpGet(@RequestParam TestVo dto){
    System.out.println("dto : " + dto);
    return "good";
}

// Test Code
@Test
public void POST_리퀘스트파람_기본패턴() throws Exception {
    mockMvc.perform(get("/ma?name=yangs&ldt=2020-03-18 18:25:40"))
            .andExpect(status().isOk())
            .andDo(print());
}
TestVo(ldt=2020-03-18T18:25:40, name=yangs)

RequestParam으로 바인딩 받을때는 @DateTimeFormat을 사용하되 T문자열을 제외한 패턴을 사용한다.


ModelAttribute

  • RequestParam과 같은 결과가 나온다. @DateTimeFormat 설정시에만 정상 바인딩 된다.(yyyy-MM-ddTHH:mm:ss 는 안된다.)
// VO
public class TestVo {
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime ldt;
    private String name;
}

// Controller
@RequestMapping(path = "/modelAttribute", method = RequestMethod.POST)
@ResponseBody
public String maPost(@ModelAttribute TestVo dto){
    System.out.println("dto : " + dto);
    return "good";
}

// Test Code
@Test
public void POST_모델어트리뷰트() throws Exception {
    mockMvc.perform(post("/modelAttribute")
                        .param("name", "yangs")
                        .param("ldt", "2020-03-18 18:25:40")
                        .contentType(MediaType.APPLICATION_FORM_URLENCODED))
            .andExpect(status().isOk())
            .andDo(print());
}
TestVo(ldt=2020-03-18T18:25:40, name=yangs)

ResponseBody

  • Jackson이 JSON으로 VO를 직렬화 해야하므로 @JsonFormat 설정을 따라간다.
  • 만일 VO에 아무런 애너테이션도 없다면 yyyy-MM-ddTHH:mm:ss 패턴의 스트링 값으로 직렬화되며, @DateTimeFormat 설정이 있어도 무시하고 yyyy-MM-ddTHH:mm:ss 패턴으로 직렬화한다. 둘 다 설정된 경우는 당연히 @JsonFormat을 따라간다.
// VO
public class TestVo {
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyyMMddHHmmss", timezone = "Asia/Seoul")
    @DateTimeFormat(pattern = "yyyy/MM/dd HH:mm:ss")
    private LocalDateTime ldt;
    private String name;

    public TestVo(String name, LocalDateTime ldt) {
        this.name = name;
        this.ldt = ldt;
    }
}

// Controller
@RequestMapping(path = "/responseBody", method = RequestMethod.GET)
@ResponseBody
public TestVo rbGet(){
    return new TestVo("yangs", LocalDateTime.now());
}

// Test Code
@Test
public void GET_리스폰스바디() throws Exception {
    mockMvc.perform(get("/responseBody")
                    .accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andDo(print());
}
MockHttpServletResponse:
    Status = 200
    Error message = null
    Headers = [Content-Type:"application/json"]
    Content type = application/json
    Body = {"ldt":"20200319005823","name":"yangs"}

정리

@RequestBody로 JSON데이터를 LocalDateTime 변수에 바인딩하고 싶다면? 설정없이 yyyy-MM-ddTHH:mm:ss 로 JSON데이터를 보낸다. 또는 LocalDateTime 변수에 @JsonFormat을 지정한다.

  • 테스트 데이터의 패턴이 yyyy-MM-ddTHH:mm:ss 면서, VO 변수에 설정이 없는경우 : 성공
  • 테스트 데이터의 패턴이 yyyy-MM-ddTHH:mm:ss 이 아니면서, VO 변수에 @JsonFormat 설정인 경우 : 성공
  • 테스트 데이터의 패턴이 yyyy-MM-ddTHH:mm:ss 이 아니면서, VO 변수에 @DateTimeFormat 설정인 경우 : 실패

@RequestParam, @ModelAttribute, @Pathvariable로 LocalDateTime 변수에 바인딩하고 싶다면? LocalDateTime 변수에 @DateTimeFormat을 지정한다.

  • 테스트 데이터의 패턴이 yyyy-MM-ddTHH:mm:ss 면서, VO 변수에 설정이 없는경우 : 실패
  • 테스트 데이터의 패턴이 yyyy-MM-ddTHH:mm:ss 이 아니면서, VO 변수에 @JsonFormat 설정인 경우 : 실패
  • 테스트 데이터의 패턴이 yyyy-MM-ddTHH:mm:ss 면서, VO 변수에 @DateTimeFormat 설정인 경우 : 실패
  • 테스트 데이터의 패턴이 yyyy-MM-dd HH:mm:ss, yyyy/MM/dd HH-mm-ss, yyyyMMddHHmmss, dd-MM-yyyy HH:mm:ss 면서, VO 변수에 @DateTimeFormat 설정인 경우 : 성공

@ResponseBody로 VO의 LocalDateTime 변수를 JSON 데이터로 보내고 싶다면? 설정없이 yyyy-MM-ddTHH:mm:ss 으로 데이터를 보낸다. 또는 LocalDateTime 변수에 @JsonFormat을 지정한다.

  • VO에 아무런 설정이 없는경우 : yyyy-MM-ddTHH:mm:ss 패턴의 스트링 반환
  • VO의 LocalDateTime 변수에 @DateTimeFormat 설정인 경우 : 설정이 무시되며 yyyy-MM-ddTHH:mm:ss 패턴의 스트링 반환
  • VO의 LocalDateTime 변수에 @JsonFormat 설정인 경우 : 설정된 패턴으로 스트링 반환(yyyy-MM-dd HH:mm:ss, yyyy/MM/dd HH-mm-ss, yyyyMMddHHmmss, dd-MM-yyyy HH:mm:ss)

 

 


읽어주셔서 감사합니다. 도움이 되셨다면 광고 클릭 부탁드립니다.

모두 힘내세요! : )

 

 

 

threshold

일단 위 단어를 찾으면 제일 먼저 나오는 뜻은 ‘문지방’이지만 프로그램에서 사용하는 의미는 임계치, 시작점 정도로 이해하면 될 듯 하다.

 

발단

SpringBoot + Quartz 로 배치관련 어플리케이션을 개발중에 스케쥴링 된 트리거를 정지(pause) 후 재개(resume)하는 기능을 개발하고 있었다. (Trigger는 Job이 실행될 시점이라고 이해하자.)
등록된 Trigger는 Quartz의 Scheduler 인터페이스 구현체에 의해 제어될수 있는데 다음과 같은 호출로 정지/재개한다.

 

서비스 클래스 코드의 일부이다. 생성된 스케쥴러 빈을 주입받아 TriggerKey를 인자로 등록된 Trigger를 제어한다.

@Autowired
Scheduler scheduler;

public void pause(TriggerKey triggerkey) throws SchedulerException {
    scheduler.pauseTrigger(triggerkey);
}

public void resume(TriggerKey triggerkey) throws SchedulerException {
    scheduler.resumeTrigger(triggerkey);
}

문제는 pause 후 몇 초가 지나고 resume을 해보면 이미 Trigger에 세팅된 시작시점이 지난 Trigger가 resume과 함께 시작해버린다. (실행 되어야 하나 실행하지 못하는 것은 misfire라 하며, Trigger에 misfire 정책을 MISFIRE_INSTRUCTION_DO_NOTHING 으로 했음에도 발생)

 

예를들어 매분 10초마다 실행되는 Trigger-Job이 있다고 하면,

  • 0초에 해당 배치를 pause → 10초에 pause 상태이므로 실행 안됨 → 20초에 resume → 다음 분 10초에 실행

이런 동작을 기대했지만 실제로는 아래와 같이 되어버린다.

  • 0초에 해당 배치를 pause → 10초에 pause 상태이므로 실행 안됨 → 20초에 resume과 함께 10초에 실행되어야 할 Trigger-Job이 실행 → 다음 분 10초에 실행

해결

Quartz에서는 misfireThreshold 값이 존재하고 디폴트가 60초로 세팅된다.
Trigger가 재개될 때는 두 가지 스탭으로 실행여부를 결정하는 것 같다.

  1. nextFireTime(다음 실행시간)이 (현재시간 - misfireThreshold) 크면 실행한다. 작다면 2번에 따른다.
  2. Trigger에 설정된 misfire 정책에 따라 실행한다.(MISFIRE_INSTRUCTION_DO_NOTHING이면 실행 안함)

즉, 나는 misfireThreshold가 60초 디폴트 설정 된 상태에서 60초가 지나기 전에 pause를 풀었으므로 misfire정책에 상관없이 즉시 실행이 되었던 것이다.

그래서 해결법은 misfireThreshold 를 짧게 설정 하던지, pause 후에 60초가 지나서 resume을 하면 된다.
misfireThreshold는 properties 설정으로 다음과 같이 설정할 수 있다.

quartz:
    properties:
        org.quartz.jobStore.misfireThreshold: 10000   ## misfire라고 판단하는 기준시간

 

패스워드 암호화의 이유

패스워드를 평문으로 저장하면 안 되는 이유는 데이터가 노출되었을 때 저장된 패스워드를 알아볼 수 없게 하기 위함이다. DB가 직접적으로 뚫렸거나, DB를 덤프한 파일이 유출되었거나 등 데이터가 유출되는 경로는 무수히 많다.

단방향 암호화 vs 양방향 암호화

단방향 암호화는 평문을 암호화한 값만 저장하고 복호화는 하지 않는(할 수 없는) 것이다. 공격으로 데이터가 뚫렸을 때 복호화가 불가능한 암호화문만 노출되므로 안전하다. 하지만 양방향 암호화의 경우 데이터와 함께 알고리즘과 키값이 함께 노출된다면 모든 데이터를 복호화할 수 있음으로 평문으로 저장한 것과 다르지 않게 된다. 그래서 비밀번호와 같이 암호화가 필요한 데이터는 보통 SHA-256을 포함한 단방향 해쉬 함수를 써서 저장한다. 사용자가 로그인을 시도하면 입력한 값의 암호화문과 저장된 암호화문을 비교하여 판단한다.

해시의 크랙

복호화가 어려운 해시함수를 사용하여 데이터를 저장한다 해도 100% 안전한 것은 아니다. 하드웨어의 발전으로 연산시간이 짧아짐에 따라 무차별적으로 때려 넣는 Brute Force Attack으로 뚫릴 수도 있다. 또한 해쉬 알고리즘이 복잡하여 Brute Force Attack으로는 시간이 오래 걸린다 하더라도 미리 가능한 패스워드 조합을 계산한 테이블(Rainbow Table)을 가지고 단순 비교만 수행하면 알고리즘의 복잡도와는 상관없이 빠른시간 안에 해킹될 가능성이 크다.

Salt Password

해쉬의 가장 큰 적인 Rainbow Table 에 대항하기 위한 효과적인 방법은 패스워드에 salt값(해싱에 추가하기 위한 임의의 값)을 추가하는 것이다.
평문 + salt 의 조합으로 해쉬값을 생성하기 때문에 Rainbow Table의 경우의 수를 엄청나게 늘리게 된다. 여기에 salt값의 길이도 최소 128bit 이상에 고정값이 아니라 랜덤값으로 사용한다면 더욱 안전해진다.

알고리즘

데이터를 공격하기 위한 노력과 비례하여 보호하기 위한 방법도 여러 가지가 있으며 이미 예전부터 많이 사용되는 암호화 알고리즘들이 개발되어 있다. 그중에서 골라 쓰면 된다!
추천 알고리즘 : PBKDF2, bcrypt, scrypt

SHA-256 (참고용으로 간단정리)

SHA(Secure Hash Algorithm)의 한 종류로 256비트로 구성되며 64자리 문자열을 반환한다. 경우의 수가 2^256 이므로 무차별 대입을 통한 공격에 비교적 안전하다.(SHA-512의 경우의 수는 2^512)
아주 작은 확률로 입력값이 다름에도 출력값이 같은 경우가 있으며 이를 충돌이라고 한다. 충돌의 발생 확률이 낮을수록 좋은 함수로 평가된다.
단방향(One-Way) 방식의 암호화기 때문에 암호화된 값을 다시 복호화 하는 것은 불가능하다.

'개발 > 기타' 카테고리의 다른 글

인텔리제이 어플리케이션 외부 옵션 및 변수  (0) 2020.12.07
Quartz misfireThreshold  (2) 2020.12.06
LastModified 헤더를 이용한 파일변경 체크  (0) 2020.12.06
Maven 기본  (0) 2019.11.12
UTC, GMT, Epoch Time  (0) 2019.10.11

하고싶은 작업

  • 로컬에 이미지 파일을 가지고 있으며 해당 이미지 파일은 다른 곳에서 최신화가 이뤄진다.
  • 원격의 이미지 파일이 최신화가 되었는지 HTTP 응답의 Lastmodified 헤더를 체크하여 로컬 이미지 파일을 최신화 한다.

RestTemplate

  • 스프링부트 웹프로젝트이므로 이미 의존성으로 들어온 RestTemplate를 이용한다. 비동기 방식이지만 어플리케이션에 크게 영향을 주지 않으므로 사용한다.
  • RestTemplate으로 HTTP 요청을 날려 응답의 LAST_MODIFIED 헤더를 체크한다.

ZonedDateTime, LocalDateTime

  • HTTP 응답의 헤더는 타임존이 GMT로 되어있다.
  • Asia/Seoul 지역의 시간으로 변경하여 기준일자와 비교한다.

테스트코드

@RunWith(SpringRunner.class)
@SpringBootTest
public class RestTemplateTest {
    @Autowired
    RestTemplateBuilder restTemplateBuilder;

    @Test
    public void RestTemplate를_이용하여_이미지_최신화() throws URISyntaxException {
        // 체크할 이미지 파일의 URL주소
        String imgSrc = "http://steamcdn-a.akamaihd.net/steam/apps/359550/capsule_sm_120.jpg";
        
        // 스프링부트의 빌더를 이용하여 RestTemplate 생성
        RestTemplate restTemplate = restTemplateBuilder.build();

        // 헤더만을 가져오도록 제공되는 메서드 사용
        HttpHeaders httpHeaders = restTemplate.headForHeaders(new URI(imgSrc));
        String lastModified = httpHeaders.get(HttpHeaders.LAST_MODIFIED).get(0);
        
        // 응답의 헤더는 GMT 시간
        System.out.println("lastModified : " + lastModified); // Fri, 21 Feb 2020 19:40:42 GMT

        // 응답의 시간에 맞는 포맷터를 지정하여 ZonedDateTime(Asia/Seoul) 객체 생성 
        ZonedDateTime lastModifiedZDT = ZonedDateTime.parse(lastModified, DateTimeFormatter.RFC_1123_DATE_TIME)
                                                     .withZoneSameInstant(ZoneId.of("Asia/Seoul"));
        System.out.println("lastModifiedZDT : " + lastModifiedZDT); // 2020-02-22T04:40:42+09:00[Asia/Seoul]

        // ZonedDateTime -> LocalDateTime
        LocalDateTime lastModifiedLDT = lastModifiedZDT.toLocalDateTime();
        System.out.println("lastModifiedLDT : " + lastModifiedLDT); // 2020-02-22T04:40:42

        // 기준일시를 생성하여 체크!
        LocalDateTime stdLDT = LocalDateTime.now().minusDays(1); // 기준일시(하루전) 생성
        if(lastModifiedLDT.isAfter(stdLDT)){ // 기준일시 이후에 변경이 일어나면
            System.out.println("다운로드(최신화) 필요함");
        }else{
            System.out.println("기존과 같음");
        }
    }
}

 

'개발 > 기타' 카테고리의 다른 글

Quartz misfireThreshold  (2) 2020.12.06
패스워드 암호화에 대해서  (0) 2020.12.06
Maven 기본  (0) 2019.11.12
UTC, GMT, Epoch Time  (0) 2019.10.11
Ajax에 관하여  (0) 2018.12.13

+ Recent posts