롬복의 @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

오류발생

새로운 Controller를 테스트하기 위해 테스트 소스를 작성하고 WebMvc와 관련된 빈만 등록해 테스트 하고자 했다.
제공되는 SliceTest 애노테이션인 @WebMvcTest를 테스트클래스에 마킹하고 실행했으나 초기화 단계에서 Exception이 발생하였다.

 

org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'entityManagerFactory' available

 

원인

@WebMvcTest로 WebMvc와 관련된 빈만 등록된 상태에서 메인클래스에 마킹한 @EnableJpaRepositories이 JPA와 관련된 빈을 찾아 작업을 하려할 때 오류가 발생하였다.

해결

QueryLookupStrategy 설정을 위해 메인클래스에 @EnableJpaRepositories를 마킹했으나, 굳이 필요한 설정은 아니므로 지우고 테스트를 실행하였다.(그저 학습의 이유로 디폴트 전략을 명시적으로 설정하였다.)
만일 필요한 옵션이라면 @EnableJpaRepositories를 통해서 하지않고 다른 방법으로 옵션을 줬어야 했을것이다.

@SpringBootApplication
//@EnableJpaRepositories(queryLookupStrategy = QueryLookupStrategy.Key.CREATE_IF_NOT_FOUND)
public class App {
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(App.class);
        app.setWebApplicationType(WebApplicationType.SERVLET);
        app.run(args);
    }
}

참고

링크 : github.com/spring-projects/spring-boot/issues/6844 

By using @EnableJpaRepositories you are explicitly telling Spring Boot’s auto-configuration to back off and that you’ll handle Spring Data yourself.
I think what’s happening is that @WebMvcTest is turning off JPA (i.e. not finding your @Entity classes) but @EnableJpaRepositories is still active so it complains that it can’t find any JPA models.

스프링부트의 Log 라이브러리

스프링부트는 Java Util Logging, Log4J2, Logback 이렇게 3가지에 대해 기본 설정을 제공하고 있다. spring-boot-starter-logging 의존성을 추가하면 로그관련 라이브러리들이 추가되는 것을 볼 수 있다.

이 중에서 스프링부트는 기본으로 Logback을 사용하며 자동으로 이루어지는 설정은 org.springframework.boot.logging.DefaultLogbackConfiguration 파일을 참고한다.
기본설정으로 아래와 같이 시간-로그레벨-프로세스ID-구분자-쓰레드명-로거명-로그내용 포맷이 출력된다.

디폴트 설정에서 변경을 원한다면 프로퍼티 설정파일에서 logging으로 시작하는 옵션을 지정하거나 별도의 로그설정파일(logback.xml 등)을 사용하여 디폴트 설정을 오버라이드 한다.


Log Level

스프링부트의 기본설정은 INFO 레벨 이상인 로그만 보여준다. 만일 상세한 로그가 보고 싶다면 debug모드로 어플리케이션을 기동하던지 출력되는 로그레벨을 프로퍼티에서 조정한다.

  • 디버그 모드로 기동
    • jar파일 실행시 --debug 옵션을 부여한다.
    • 프로퍼티 설정파일(application.properties)에 debug=true을 추가한다.
  • 어플리케이션의 출력로그 레벨을 조정
    • logging.level을 prefix로 하는 프로퍼티를 추가한다.
    • logging.level.<logger-name>=<level>
    • logging.level.com.yang.wind.mapper=TRACE : com.yang.wind.mapper 패키지 이하의 로그레벨을 TRACE로 한다.
    • logging.level.root=INFO : root 이하 모든 패키지의 로그레벨을 INFO로 한다.

Log Color

만약 ANSI를 지원하는 터미널을 사용한다면 프로퍼티 설정파일에 spring.output.ansi.enabled=detect 또는 always 옵션을 줘서 로그를 컬러풀하게 출력할 수 있다.


Log 파일출력

기본적으로 아무설정이 없다면 콘솔은 출력하지만 로그파일은 작성되지 않는다. 파일로 로그를 출력하고자 한다면 프로퍼티 설정파일에 logging.file.name  logging.file.path 프로퍼티를 추가한다.
name과 path 두 설정을 모두 지정할 경우에는 함께 동작하지 않는다. logback-spring.xml 과 같이 별도의 파일로 설정을 해줘야 할 듯 싶다.
프로퍼티 지정에 따른 동작방식은 아래를 참고하자.


Log Group

특정 로거들을 그룹핑하여 설정을 동일하게 변경이 가능하다. 예를들어 Tomcat에 관련된 로거들을 동시에 설정변경 하고자 한다면 각자의 패키지로 설정을 변경하기 보다는 그룹을 지정하여 간편하게 변경할 수 있다.

프로퍼티 설정파일에서 tomcat이라는 그룹을 생성하고 설정을 변경한다. 

  • logging.group.tomcat=org.apache.catalina, org.apache.coyote, org.apache.tomcat
  • logging.level.tomcat=TRACE

또한 스프링부트는 미리 설정된 그룹을 제공하기 때문에 편리하게 로그레벨 설정이 가능하다.


Custom Log 설정

설정한 로그시스템에 따라 아래의 파일이 로드되어 환경이 구성된다. 디테일한 커스텀 설정을 위해서는 로그시스템 별로 아래 xml 파일이나 properties 파일을 작성한다.

스프링부트에서 테스트는 일단 spring-boot-starter-test 의존성을 추가하고, @SpringBootTest가 마킹된 테스트 클래스를 작성한다. 이때 컨트롤러에 대한 테스트라면 MockMvc나 내장톰캣을 구동하여 테스트를 진행할 수 있으며, @WebMvcTest 등을 활용하여 특정 레이어만 테스트할 수도 있다.

 

하나씩 알아본다.

spring-boot-starter-test 스프링부트에서 테스트를 위해 추가하는 의존성

스프링부트에서는 테스트를 위해 제공하는 라이브러리가 있으며 코어 라이브러리와 Auto-Configuration을 포함하는 Starter(라이브러리 및 자동설정을 포함)를 제공한다. Stater 안에는 Spring 테스트모듈을 포함하여 JUnit, Jupiter, AssertJ, Hamcrest 등의 테스트시 유용한 라이브러리가 포함되어 있다.

 

메이븐을 사용하다면 아래와 같이 pom.xml spring-boot-starter-test를 다음과 같이 추가한다.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>

Test Class 작성

스프링부트 어플리케이션 테스트를 작성할 클래스에는 테스트의 성격에 따라서 스프링에서 제공하는 애노테이션을 조합하여 마킹할 수 있다.
가장 기본적으로는 @SpringBootTest가 있는데, 이는 스프링의 @ContextConfiguration 애노테이션을 대체하면서 몇 가지 기능을 제공한다. (스프링에서는 테스트를 위한 환경설정을 위해 @ContextConfiguration를 사용한다.)

 

아래는 기본적인 스프링부트 테스트 클래스이다.

Junit의 스프링 테스트를 위해 @Runwith가 마킹되어 있고, @SpringBootTest는 테스트를 위한 환경설정 및 부가기능을 제공한다. 각 @Test 메서드마다 단위 테스트를 작성한다.

@RunWith(SpringRunner.class)
@SpringBootTest // 테스트를 위한 환경설정 및 부가기능을 제공한다.
public class SampleTest {

    @Test
    public void test1(){
        //
    }

    @Test
    public void test2(){
        //
    }
}

@SpringBootTest의 기능

  • 기본 ContextLoader로써 SpringBootContextLoader를 사용한다.
  • 자동으로 @SpringBootConfiguration을 찾는다. @SpringBootConfiguration은 @SpringBootApplication(메인 클래스) 안에 있다.
  • 커스텀 Environment 프로퍼티를 정의할 수 있다.
  • 어플리케이션 구동 시 설정하는 application argument를 테스트 프로그램에서 정의할 수 있다.
    @SpringBootTest(args="--app.test=one")
  • WebEnvironment를 지정할 수 있다.
    • WebEnvironment.MOCK : 아무런 설정이 없을 시 적용되는 디폴트 설정이다. mock 서블릿 환경으로 내장톰캣이 구동되지 않는다.(브라우저에서 접속되지 않는다.)
    • WebEnvironment.RANDOM_PORT : 스프링부트를 직접 구동시킨 것처럼 내장톰캣이 구동되나 랜덤포트로 구동된다.
    • WebEnvironment.DEFINED_PORT : 정의된 포트로 내장톰캣이 구동된다.
    • WebEnvironment.NONE : WebApplicationType.NONE으로 구동된다.
  • 테스트를 위한 TestRestTemplate 빈과 WebTestClient 빈을 등록할 수 있다.
    만일 WebEnvironment.MOCK 이라면 TestRestTemplate과 WebTestClient빈은 등록되지 않는다. 실제 내장톰캣이 구동되지 않으므로 사용할 여지가 없고 어플리케이션에 요청을 보내고 싶다면 MockMvc로 테스트하면 되기 때문이다.

WebEnvironment 설정에서 디폴트 값이 MOCK 이므로 내장톰캣이 구동되지 않고 테스트가 수행되는 것에 유의한다.


@AutoConfigureMockMvc

디폴트 설정이 서버가 실행되지 않고 Mock 환경에서 진행되므로 컨트롤러처럼 엔드포인트가 있는 테스트를 진행할 때는 MockMvc를 활용한다.
MockMvc는 가상의 클라이언트로 어플리케이션에 요청을 날리는 역할을 한다. MockMvc를 생성하는 방법에는 여러 가지 있지만 테스트클래스에 @AutoConfigureMockMvc을 마킹하고 주입받는 것이 간단하다.

MockMvc를 사용하여 mock 환경에서 컨트롤러를 테스트하는 코드이다.

@Controller
public class SampleController { // 테스트 대상의 컨트롤러
    @GetMapping("/sample")
    @ResponseBody
    public String sample(){
        return "sample's return";
    }
}
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringRunner.class)
@SpringBootTest
@AutoConfigureMockMvc // MockMvc를 생성한다.
public class SampleTest {
    @Autowired
    MockMvc mockMvc; // 생성된 MockMvc 빈을 주입받아 테스트에 사용한다.

    @Test
    public void sampleController_Test() throws Exception {
        mockMvc.perform(get("/sample"))
                .andExpect(handler().handlerType(SampleController.class))
                .andExpect(handler().methodName("sample"))
                .andExpect(status().isOk())
                .andExpect(content().string("sample's return"))
                .andDo(print());
    }

}

요청을 받는 클래스나 메서드명부터 결과 상태코드, 리턴값 등 다양한 검증이 가능하다. 


내장톰캣 구동 테스트

Mock서블릿 환경이 아닌 내장톰캣을 구동하여 테스트를 하기 위해서는 @SpringBootTest에 WebEnvironment를 설정한다.

 

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

 

내장 톰캣으로 구동이 되면 MockMvc 로는 테스트할 수 없으니 테스트를 위한 HTTP 클라이언트를 사용해야 한다. 여기서 사용할 수 있는 클라이언트가 TestRestTemplate다. 여기에 더해서 여러 레이어 간의 동작을 보는 통합 테스트가 아니라 단위 테스트인 경우에는 MockBean을 실제 빈 대신에 주입한 고립 테스트가 가능하다.

아래 컨트롤러를 테스트해보자.

@Controller
public class SampleController {
    @Autowired
    SampleService service; // 테스트대상(SampleController)이 다른 빈에 의존성을 가진다.

    @GetMapping("/sample")
    @ResponseBody
    public String sample(){
        return service.service(); // 서비스빈 호출
    }
}

SampleService가 어떻게 동작하는지 상관없이 SampleController의 구현을 대상으로 하는 테스트라면 @MockBean을 활용하여 실제 빈을 대체할 수 있다.

import static org.mockito.Mockito.when;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class SampleTest {
    @Autowired
    TestRestTemplate testRestTemplate;

    @MockBean
    SampleService mockSampleService;

    @Test
    public void sampleController_mockService_Test() throws Exception {
        // SampleService의 리턴을 사전에 정의한다.
                when(mockSampleService.service()).thenReturn("mock service");

        String result = testRestTemplate.getForObject("/sample", String.class);
        Assert.assertEquals(result, "mock service");
    }
}

Mock 서블릿 환경이 아닌 실제 내장톰캣이 랜덤포트로 구동됐으므로MockMvc는 사용할 수 없다. 그 대신 주입받은 테스트용 클라이언트인 TestRestTemplate를 활용하여 테스트 요청을 보내고 정의한 리턴 값을 확인한다.


Slice Test

@SpringBootTest는 자동 설정을 포함한 프로젝트의 전체 빈을 모두 등록하여 테스트한다.(그러므로 무겁다.) 만일 컨트롤러처럼 MVC만 테스트하고 싶거나, JPA만 테스트하고 싶은 경우 특정 레이어의 빈만 등록하여 가볍게 테스트할 수 있도록 애노테이션을 제공하고 있다.

@WebMvcTest, @WebFluxTest, @DataJpaTest 등 제공되는 애노테이션들이 있지만 @WebMvcTest만 살펴본다.

@WebMvcTest

스프링 MVC 컨트롤러에 관련된 환경구성만 진행하므로 @Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, HandlerInterceptor, WebMvcConfigurer, 그리고 HandlerMethodArgumentResolver 만 빈으로 등록된다.

다른 의존성이 있다면 테스트에서 빈등록 위해 명시해주거나 MockBean으로 대체해야 한다.

@RunWith(SpringRunner.class)
@WebMvcTest(SampleController.class) // 테스트대상 클래스만 빈으로 등록한다.
public class SampleTest {
    @MockBean
    SampleService mockSampleService;

    @Autowired
    MockMvc mockMvc;

    @Test
    public void sampleController_Test() throws Exception {
        // SampleService의 리턴을 사전에 정의한다.
        when(mockSampleService.service()).thenReturn("mock service");

        mockMvc.perform(get("/sample"))
                .andExpect(handler().handlerType(SampleController.class))
                .andExpect(handler().methodName("sample"))
                .andExpect(status().isOk())
                .andExpect(content().string("mock service"))
                .andDo(print());
    }
}

요약하면

  • 스프링부트에서는 테스트를 위해 starter 라이브러리를 제공하고 있다.
  • classpath에 starter 의존성만 추가하면 자동 설정까지 완료된다.
  • 스프링부트 테스트에서는 @SpringBootTest를 활용하여 테스트를 위한 초기설정이나 환경설정을 추가/변경 할 수 있다.
  • 테스트하고자 하는 대상이 다른 빈을 참조하고 있다면 Mock 빈의 사용을 고려하자.
  • @SpringBootTest와 다르게 일부 레이어 테스트를 위한 @WebMvcTest 등을 제공하고 있다.

+ Recent posts