컨트롤러 핸들러메소드의 인자에 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

+ Recent posts