Spring Boot (Chapter 3)[테스트와 로깅, 빌드]


3. 테스트와 로깅, 빌드

3.1 스프링 부트 테스트

단위 테스트란 자신이 작성한 클래스에 대한 테스트로서 테스트 단계 중에서 가장 기본이라 할 수 있다. 단위 테스트가 효율적으로 이루어지기 위해서는 테스트할 객체가 최대한 단순해야 한다. 하지만 웹 어플리케이션은 테스트 대상 객체가 특정 서버와 관련되어 있거나 다른 객체들과 연관되어 관계가 복잡한 경우가 일반적이다.

컨트롤러만 단독으로 테스트하거나 컨트롤러와 연관된 비지니스 컴포넌트를 실행하지 않고 컨트롤러만 독립적으로 테스트할 수 있는 환경이 필요하다.

3.1.1 스프링 부트에서 테스트하기

  1. 테스트 환경 만들기

스프링 부트를 이용하여 프로젝트를 생성하면 테스트 스타터는 자동으로 추가되는데 다음은 자동으로 추가된 의존성이다.

dependencies {
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

프로젝트 의존성에 테스트 스타터가 등록되어 있기 때문에 테스트에 필요한 여러 라이브러리들도 자동으로 추가된다.

  1. 기본 테스트 클래스 이해하기 스프링 부트는 프로젝트를 생성할 때, src/test/java 소스 폴더에 간단한 테스트 케이스를 제공한다.
@SpringBootTest
class LearnApplicationTests {

	@Test
	void contextLoads() {
	}

}

@SpringBootTest 어노테이션을 추가했는데, 이는 메인 클래스에 선언된 @SpringBootApplication과 비슷한 어노테이션이라고 생각하면 된다. @SpringBootApplication이 사용자가 작성한 빈과 자동설정 빈들을 모두 초기화하듯이, @SpringBootTest 역시 테스트 케이스가 실행될 때 테스트에 필요한 모든 설정과 빈들을 자동으로 초기화하는 역할을 수행한다.

@SpringBootTest에는 @ExtendWith(SpringExtension.class) 어노테이션이 있는데 Junit4까지의 @RunWith가 Junit5로 오면서 SpringBootTest에 @ExtendWith 선언되는 것으로 변경되었다.

  1. 테스트 케이스 실행 [Run As] -> [Junit Test]
테스트 케이스 만들기

src/test/java에 클래스를 작성한다.

@SpringBootTest
public class PropertiesTest {

    @Test
    public void testMethod(){
        
    }
}

복잡한 테스트 설정들을 자동으로 처리하고, 테스트 관련 객체들도 메모리에 올리기 위해서 개발자가 할 일은 테스트 케이스 클래스에 @SpringBootTest을 선언하는 것 뿐이다.
@SpringBootTest는 여러 속성을 가질 수 있는데 각 속성의 의미는 다음과 같다.

@SpringBootTest의 속성과 의미

속성 의미
properties 테스트가 실행되기 전에 테스트에 사용할 프로퍼티들을 key=value 형태로 추가하거나 properties 파일에 설정된 프로퍼티를 재정의한다.
classes 테스트할 클래스들을 등록한다. 만일 classes속성을 생략하면 어플리케이션에 정의된 모든 빈을 생성한다.
webEnvironment 어플리케이션이 실행될 때, 웹과 관련된 환경을 설정할 수 있다.

@SpringBootTest속성 중에서 properties 속성이 중요한데, 이 속성을 이용하면 외부에 설정된 프로퍼티(application.properties) 정보를 재정의하거나 새로운 프로퍼티를 등록하여 사용할 수 있다.

작성된 테스트 케이스를 실행하면 BoardController 객체가 생성되는 것을 확인할 수 있다. (@SpringBootTest 어노테이션을 통해 빈이 초기화 된 것)

2022-04-14 23:12:22.514  INFO 51883 --- [    Test worker] c.bys.sample.controller.BoardController  : ##### Create BoardController #####


외부 프로퍼티 사용하기

테스트 케이스를 작성하다 보면 여러 테스트에서 공통으로 사용하는 데이터들이 있다. 이런 데이터를 외부에 프로퍼티로 등록하면 테스트 데이터를 재사용하거나 변경하기가 쉽다.

  • 프로퍼티 참조

application.yaml

# Server Setting
server:
  port: 8080

# Test Setting
author:
  name: TESTER
  age: 53

PropertiesTest

@SpringBootTest
public class PropertiesTest {

    @Autowired
    Environment environment;

    @Test
    public void testMethod(){
        System.out.println("이름: " + environment.getProperty("author.name"));
        System.out.println("이름: " + environment.getProperty("author.age"));
        System.out.println("이름: " + environment.getProperty("author.nation"));
    }
}

Output

이름: TESTER
이름: 53
이름: null


  • 프로퍼티 재정의 @SpringBootTest를 이용하면 외부 프로퍼티 파일에 등록된 프로퍼티를 재정의할수도 있고 새로운 프로퍼티를 추가할 수도 있다. 프로퍼티 재정의를 테스트하기 위해 테스트 케이스를 수정한다.
@SpringBootTest(classes = BoardController.class, 
    properties = {"author.name=Tester"
                 ,"author.age=45"
                 ,"author.nation=Korea"
                 })
public class PropertiesTest {

    @Autowired
    Environment environment;

    @Test
    public void testMethod(){
        System.out.println("이름: " + environment.getProperty("author.name"));
        System.out.println("이름: " + environment.getProperty("author.age"));
        System.out.println("이름: " + environment.getProperty("author.nation"));
    }
}

추가된 @SpringBootTest 속성에서 classes는 테스트할 클래스를 지정할 때 사용한다. classes로 지정된 클래스는 컨테이너가 자동으로 메모리에 올린다. 하지만 classes에 등록되지 않은 클래스는 객체 생성되지 않기 때문에 테스트 과정에서 불필요한 메모리 낭비를 피할 수 있다.

그리고 properties 속성을 이용해서 application.properties 파일에 설정된 외부 프로퍼티를 재정의하면서 동시에 author.nation 이라는 새로운 프로퍼티도 추가했다.

Output

이름: Tester
이름: 45
이름: Korea

이렇게 테스트 환경에서만 사용되는 프로퍼티들을 새롭게 추가하거나 재정의해서 사용하면 된다.


3.1.2 MockMvc 이용해서 컨트롤러 테스트하기

Mock이라는 단어를 사전에서 찾아보면 ‘테스트를 위해 만든 모형’을 의미한다. 따라서 테스트를 위해 실제 객체와 비슷한 모의 객체를 만드는 것을 Mocking이라고 하며, Mocking한 객체를 메모리에서 얻어내는 과정을 Mock-up 이라고 한다.

객체를 테스트하기 위해서는 당연히 테스트 대상 객체가 메모리에 있어야 한다. 하지만 생성하는데 절차가 필요하거나 많은 시간이 소요되는 객체는 자주 테스트하기 어렵다. 또는 다른 소프트웨어의 도움이 반드시 필요한 객체도 있을 수 있다. 이런 복잡한 객체는 당연히 테스트 과정도 복잡하고 어려울 수 밖에 없다.

따라서 테스트하려는 실제 객체와 비슷한 가짜 객체를 만들어서 테스트에 필요한 기능만 가지도록 Mocking을 하면 테스트가 쉬워진다. 그리고 테스트하려는 객체가 복잡한 의존성을 가지고 있을 때, Mocking한 객체를 이용하면, 의존성을 단절시킬 수 있어서 쉽게 테스트할 수 있다. 웹 어플리케이션에서 컨트롤러를 테스트할 때, 서블릿 컨테이너를 모킹하기 위해서는 @WebMvcTest를 사용하거나 @AutoConfigureMockMvc를 사용하면 된다.

서블릿 컨테이너를 모킹한다는 말은 무슨 말일까? 우리가 웹 환경에서 컨트롤러를 테스트하려면 반드시 서블릿 컨테이너가 구동되고 DispatcherServlet 객체가 메모리에 올라가야 한다. 하지만 서블릿 컨테이너를 모킹하면 실제 서블릿 컨테이너가 아닌 테스트용 모형 컨테이너를 사용하기 때문에 간단하게 컨트롤러를 테스트할 수 있다.


  • @WebMvcTest 사용하기
@ExtendWith(SpringExtension.class)
@WebMvcTest
public class BoardControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testHello() throws Exception {
        mockMvc.perform(get("hello").param("name","둘리"))
                .andExpect(status().isOk())
                .andExpect(content().string("Hello: 둘리"))
                .andDo(print());
    }
}

여기에서 중요한 것은 @WebMvcTest 어노테이션이다. @WebMvcTest는 @Controller, @RestController가 설정된 클래스들을 찾아 메모리에 생성한다. 반면 @Service, @Repository가 붙은 객체들은 테스트 대상이 아닌 것으로 처리되기 때문에 생성되지 않는다. 그러면 이제 @WebMvcTest가 설정된 테스트 케이스에서는 서블릿 컨테이너를 모킹한 MockMvc 타입의 객체를 목업하여 컨트롤러에 대한 테스트 코드를 작성할 수 있다.

Output

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /hello
       Parameters = {name=[둘리]}
          Headers = []
             Body = null
    Session Attrs = {}

Handler:
             Type = com.bys.sample.controller.BoardController
           Method = com.bys.sample.controller.BoardController#getBoard(String)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"text/plain;charset=UTF-8", Content-Length:"12"]
     Content type = text/plain;charset=UTF-8
             Body = Name: 둘리
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

Response content expected:<Hello: 둘리> but was:<Name: 둘리>
Expected :Hello: 둘리
Actual   :Name: 둘리


  • @AutoConfigureMockMvc 사용하기

@WebMvcTest와 비슷하게 사용할 수 있는 어노테이션으로 @AutoConfigureMockMvc가 있다. 먼저 @SpringBootTest에는 웹 어플리케이션 테스트를 지원하는 webEnvironment 속성이 있다. 이 속성을 생략하면 기본 값으로 WebEnvironment.MOCK이 설정되어 있는데, 이 설정에 의해서 서블릿 컨테이너가 모킹된다. 즉 테스트 케이스 실행 시에 서블릿 컨테이너를 구동하지 않는다는 말이다. 그리고 @SpringBootTest(webEnvironment=WebEnvironment.MOCK) 설정으로 모킹한 객체를 의존성 주입 받으려면 @AutoConfigureMockMvc를 클래스 위에 추가해야 한다.

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
public class BoardControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    public void testHello() throws Exception {
        mockMvc.perform(get("/hello").param("name","둘리"))
                .andExpect(status().isOk())
                .andExpect(content().string("Hello: 둘리"))
                .andDo(print());
    }
}

이전에 사용했던 @WebMvcTest와 가장 큰 차이점은 @AutoConfigureMockMvc는 컨트롤러뿐만 아니라 테스트 대상이 아닌 @Service, @Repository가 붙은 객체들도 모두 메모리에 올린다는 것이다. 따라서 간단하게 컨트롤러만 테스트하기 위해서는 @AutoConfigureMockMvc가 아닌 @WebMvcTest를 사용해야 한다. @WebMvcTest는 @SpringBootTest와 같이 사용될 수 없다. 왜냐하면 각자 서로의 MockMvc를 모킹하기 때문에 충돌이 발생하기 때문이다.

MockMvc 메서드 이해하기

MockMvc가 제공하는 perform() 메서드를 사용하면 마치 브라우저에서 서버에 URL 요청을 하듯 컨트롤러를 실행시킬 수 있다. 그리고 andExpect() 메서드를 이요하면 서버의 응답 결과도 검증할 수 있다.

@Test
public void testHello() throws Exception {
    mockMvc.perform(get("/hello").param("name","둘리"))
            .andExpect(status().isOk())
            .andExpect(content().string("Hello: 둘리"))
            .andDo(print());
}

웹 요청에 해당하는 MockMvc의 perform() 메서드는 RequestBuilder 객체를 인자로 받는데, RequestBuilder 객체는 MockMvcRequestBuilders의 정적 메서드를 이용해서 생성한다. MockMvcRequestBuilder의 메서드들은 get(), post(), put(). delete() 메서드를 제공한다. 이 메서드들은 MockHttpServletRequestBuilder 객체를 리턴하는데, 이 객체에 브라우저가 HTTP 요청 프로토콜에 요청 관련 정보(파라미터, 헤더, 쿠키 등)를 설정하듯 다양한 정보들을 설정할 수 있다. 예를들어 param() 메서드를 이용하면 ‘키=값’의 파라미터를 여러 개 전달할 수 있다. MockHttpServletRequestBuilder의 메서드는 MockHttpServletRequestBuilder 객체를 다시 리턴하기 때문에 메시지 체인을 구성하여 복잡한 요청을 설정할 수 있다.

마지막으로 perform() 메서드를 이용하여 요청을 전송하면, 그 결과로 ResultActions 객체를 리턴하는데, ResultActions는 응답 결과를 검증할 수 있는 andExpect() 메서드를 제공한다. andExpect()가 요구하는 ResultMatchers는 MockMvcResultMatchers에 정의된 정적 메서드를 통해 생성할 수 있다.

컨트롤러의 동작을 테스트하기 위해서는 요청도 중요하지만 사실 컨트롤러가 어떤 결과를 전송했는지 검증하는 것이 가장 중요하다. 서버의 응답 결과는 MockMvcResultMatchers 객체의 메서드를 이용하여 검증할 수 있다.

MockMvcResultMatchers 메서드의 기능

  1. 응답 상태 코드 검증
    MockMvcResultMatchers의 status() 메서드는 StatusResultMatchers 객체를 리턴하는데 이 객체를 이용하면 응답 상태 코드를 검증할 수 있다.

    메서드 설명
    isOk() 응답 상태 코드가 정상적인 처리에 해당하는 200인지 확인한다.
    isNotFound() 응답 상태 코드가 404 Not Found인지 확인한다.
    isMethodNotAllowed() 응답 상태 코드가 메서드 불일치에 해당하는 405인지 확인한다.
    isInternalServerError() 응답 상태 코드가 예외 발생에 해당하는 500인지 확인한다.
    is(int status) 몇 번 응답 상태 코드가 설정되어 있는지 확인한다. Ex) is(200), is(400)
  2. 뷰/리다이렉트 검증
    컨트롤러가 리턴하는 뷰를 검증할 때는 view() 메서드를 사용한다. andExpect(view().name(“hello”)) 코드는 컨트롤러가 리턴한 뷰 이름이 “hello” 인지 검증한다. 만약 요청 처리 결과가 리다이렉트 응답이라면 redirectUrl() 메서드를 사용하면 된다. andExpect(redirectUrl(“/index”))코드는 “/index” 화면으로 리다이렉트했는지를 검증하는 것이다.

  3. 모델 정보 검증
    컨트롤러에서 저장한 모델의 정보들을 검증하고 싶으면 MockMvcResultMatchers.model() 메서드를 사용한다.

    메서드 설명
    attributeExists(String name) name에 해당하는 데이터가 Model에 포함되어 있는지 검증한다.
    attribute(String name, Object value) name에 해당하는 데이터가 value 객체인지 검증한다.

    이 외에도 응답 헤더 정보를 검증하거나 응답 결과로 생성되는 쿠키를 검증할 수 있는 메서드들도 제공된다.

  4. 요청/응답 전체 메시지 확인하기
    마지막으로 MockMvc를 이용해서 테스트를 진행할 때, 실제로 생성된 요청과 응답 메시지를 모두 확인해보고 싶은 경우에는 perform() 메서드가 리턴하는 ResultActions의 andDo(ResultHandler handler)메서드를 사용하면 된다. MockMvcResultHandlers.print() 메서드는 ResultHandler를 구현한 ConsolePrintingResultHandler 객체를 리턴한다. ConsolePrintingResultHandler를 andDo() 메서드 인자로 넘겨주면 콘솔에 요청/응답과 관련된 정보를 모두 출력한다. 그래서 BoardControllerTest 실행 결과로 요청/응답 메세지가 모두 출력됐던 것이다.

내장 톰캣으로 테스트하기

지금까지는 테스트 케이스에서 MockMvc 객체를 목업해서 테스트했기 때문에 톰캣 서버를 사용하지 않고도 테스트할 수 있었다. 정상적으로 서블릿 컨테이너를 구동하고 테스트 결과를 확인하고 싶으면 @SpringBootTest에서 webEnvironment 속성 값을 RANDOM_PORT나 DEFINED_PORT로 변경하면 된다.

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class BoardControllerTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void testHello() throws Exception {
        String result = restTemplate.getForObject("/hello?name=둘리", String.class);
        Assertions.assertEquals("Name: 둘리", result);
    }
}

webEnvironment속성 값을 WebEnvironment.RANDOM_PORT로 지정하면 더 이상 서블릿 컨테이너를 모킹하지 않기 때문에 MockMvc 객체를 목업할 수 없다. 따라서 MockMvc 객체 대신 실제 컨트롤러를 실행해줄 TestRestTemplate 객체를 주입해서 컨트롤러를 요청해야 한다.

getForObject() 메서드의 첫 번째 인자로 서버에 요청할 URL을 지정했고, 두 번째로 응답 결과의 타입 클래스를 지정했다. 그리고 assertEquals 메서드를 이용하여 응답 결과 메세지를 확인했다.

성공Output

5:17:35 오후: Task execution finished ':test --tests "com.bys.sample.controller.BoardControllerTest"'.

실패 Output

expected: <Name : 둘리> but was: <Name: 둘리>
Expected :Name : 둘리
Actual   :Name: 둘리
<Click to see difference>

org.opentest4j.AssertionFailedError: expected: <Name : 둘리> but was: <Name: 둘리>
	at org.junit.jupiter.api.AssertionUtils.fail(AssertionUtils.java:55)
	at org.junit.jupiter.api.AssertionUtils.failNotEqual(AssertionUtils.java:62)
	at org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:182)
	at org.junit.jupiter.api.AssertEquals.assertEquals(AssertEquals.java:177)
	at org.junit.jupiter.api.Assertions.assertEquals(Assertions.java:1141)
	at com.bys.sample.controller.BoardControllerTest.testHello(BoardControllerTest.java:24)

단순히 문자열을 리턴하지 않고 BoardVO 같은 객체를 리턴하는 메서드도 동일한 방법으로 테스트 할 수 있다.

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class BoardControllerTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void testHello() throws Exception {
        BoardVO board = restTemplate.getForObject("/getBoard", BoardVO.class);
        Assertions.assertEquals("Tester", board.getWriter());
    }
}


3.1.3 서비스 계층을 연동하는 컨트롤러 테스트하기

가끔 인터페이스 없이 구현 클래스만 만들어서 사용하는 경우도 있는데, 이는 유지보수 과정에서 비즈니스 클래스를 다른 클래스로 변경하지 않겠다는 것을 의미한다. 정상적인 경우라면 Service 인터페이스를 먼저 만들고 ServiceImpl 클래스를 구현해야 한다. 그리고 Service 인터페이스를 컴포넌트를 사용하는 클라이언트에게 제공하면 된다.

springboot_3_1

@Slf4j
@RestController
public class BoardController {

    @Autowired
    BoardService boardService;

    public BoardController(){
        log.info("##### Create BoardController #####");
    }

    @GetMapping("/hello")
    public String getBoard(String name){
        return "Name: " + name;
    }


    @GetMapping("/getBoard")
    public BoardVO getBoard(){
        return boardService.getBoard();
    }

    @GetMapping("/getBoardList")
    public List<BoardVO> getBoardList(){
        return boardService.getBoardList();
    }
}


@Service
public class BoardServiceImpl implements BoardService {

    public String hello(String name) {
        return "Hello: " + name;
    }

    @Override
    public BoardVO getBoard() {
        BoardVO board = new BoardVO();
        board.setSeq(1);
        board.setTitle("Ttile");
        board.setWriter("테스터");
        board.setContent("Content");
        board.setCreateDate(new Date());
        board.setCnt(0);
        return board;
    }

    @Override
    public List<BoardVO> getBoardList() {
        List<BoardVO> boardList = new ArrayList<BoardVO>();
        for (int i = 0; i < 10; i++) {
            BoardVO board = new BoardVO();
            board.setSeq(i);
            board.setTitle("Ttile " + i);
            board.setWriter("테스터");
            board.setContent(i + " 번 내용입니다.");
            board.setCreateDate(new Date());
            board.setCnt(0);
            boardList.add(board);
        }
        return boardList;
    }
}

기존에 작성했던 BoardControllerTest를 다시 실행하면 동일한 실행 결과를 얻을 수 있다.

현재 진행 중인 RestTemplate을 이용하여 비즈니스 컴포넌트의 메서드까지 호출을 전달한다. 하지만 만약 비즈니스 컴포넌트를 생성하는 데 많은 시간과 자원이 필요하거나, 아직 비즈니스 컴포넌트가 완성되지 않아 인터페이스만 제공되는 경우도 있을 수 있다. 스프링에서는 이런 문제를 해결하기 위해서 비즈니스 컴포넌트를 모킹해서 테스트하는 방법을 제공한다.

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
@AutoConfigureMockMvc
public class BoardControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private BoardService boardService;

    @Test
    public void testHello() throws Exception {
        when(boardService.hello("둘리")).thenReturn("Name: 둘리");

        mockMvc.perform(get("/hello").param("name", "둘리"))
                .andExpect(status().isOk())
                .andExpect(content().string("Name: 둘리"))
                .andDo(print());
    }
}

서블릿 컨테이너를 모킹하고 MockMvc 객체를 목업하기 위해서 클래스 선언부에 @AutoConfigureMockMvc 어노테이션을 추가했다. 그리고 @MockBean을 이용해서 BoardService 타입의 객체를 사용하고 있는데, @MockBean은 특정 타입의 객체를 모킹할 수 있기 때문에 비즈니스 객체(BoardServiceImpl)를 생성하지 않고도 테스트 케이스를 작성할 수 있다.

testHello() 에서는 가장 먼저 목업한 BoardService의 hello() 메서드를 Mockito의 정적 메서드인 when()을 통해 호출했다. 그리고 hello() 메서드를 실행했을 때, 실행 결과가 “Name: 둘리” 라는 문자열이 리턴되도록 했다.


3.2 스프링 부트 로깅

스프링 부트가 사용하는 SLF4J(Simple Logging Facade for Java)는 로깅 프레임워크가 아니다. 단지 복잡한 로깅 프레임워크들을 쉽게 사용할 수 있도록 도와주는 퍼사드에 불과하다. 스프링 부트는 SLF4J(Simple Logging Facade for Java)라는 퍼사드를 통해 궁극적으로는 LogBack을 사용한다.

스프링 부트는 logging 스타터를 이용하여 Java Util Logging, Log4j2, LogBack을 SLF4J 구현체로 가지고 있다.

- org.springframework.boot:spring-boot-starter-logging:2.6.6
  - ch.qos.logback:logback-classic:1.2.11
    - ch.qos.logback:logback-core:1.2.11
    - org.slf4j:slf4j-api:1.7.36
  - org.apache.logging.log4j:log4j-to-slf4j:2.17.2
    - org.apache.logging.log4j:log4j-api:2.17.2
    - org.slf4j:slf4j-api:1.7.36
  - org.slf4j:jul-to-slf4j:1.7.36
    - org.slf4j:slf4j-api:1.7.36

스프링 부트는 이 중에서 LogBack을 이용하여 로그를 출력한다.


3.2.2 스프링 부트 로깅 수정하기

스프링 부트가 제공하는 기본 로그 설정을 사용하지 않고 직접 관리하고 싶다면 logback.xml 파일을 직접 추가하면 된다.

src/main/resource/logback.xml

<?xml version="1.0" encoding="UTF-8"?>

<configuration>
    <include resource="org/springframework/boot/logging/logback/base.xml" />
    <logger name="com.bys.sample" level="DEBUG" />
</configuration>

위 설정은 스프링 부트가 제공하는 기본 설정파일(base.xml) 파일을 포함하여 (include) 새로운 로깅 설정을 하겠다는 것이다. 포함된 base.xml 설정 파일에는 Appender를 비롯하여 로깅 관련한 다양한 설정들이 기본적으로 설정되어 있다. 그리고 com.bys.sample 패키지에서 출력하는 모든 로그를 출력하도록 logger를 설정했다. 그리고 이 로거의 레벨은 DEBUG로 지정했기 때문에 DEBUG 이상의 로그 메세지들이 출력된다.

만약 스프링 부트가 기본으로 제공하는 설정파일(base.xml)을 사용하지 않고 직접 모든 것을 제어하고 싶다면 아래와 같이 logback.xml 을 작성한다.

<?xml version="1.0" encoding="UTF-8"?>

<configuration>

<appender name="fileAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
	<file>src/main/resources/logs/board_log.log</file>
	
	<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
		<fileNamePattern>
			src/main/resources/logs/myboard.%d{yyyy-MM-dd}.log.gz
		</fileNamePattern>
		<maxHistory>30</maxHistory>
	</rollingPolicy>
	<encoder>
		<pattern>
		%d{yyyy:MM:dd HH:mm:ss.SSS} %-5level --- [%thread] %logger{35} : %msg %n
		</pattern>
	</encoder>
</appender>

<appender name="consoleAppender" class="ch.qos.logback.core.ConsoleAppender">
	<encoder>
		<pattern>
		%d{yyyy:MM:dd HH:mm:ss.SSS} %-5level --- [%thread] %logger{35} : %msg %n
		</pattern>
	</encoder>
</appender>

<logger name="com.rubypaper" level="warn" additivity="false">
	<appender-ref ref="consoleAppender" />
	<appender-ref ref="fileAppender" />
</logger>

<root level="error">
	<appender-ref ref="consoleAppender" />
</root>

</configuration>

로그백 설정은 크게 appender와 logger 설정으로 구성된다. 어펜더는 어디에, 어떤 패턴으로 로그를 출력할 것인지를 결정한다. 위 설정에서 fileAppender는 RollingFileAppender 클래스를 이용해서 특정 파일에 로그를 출력하는 어펜더다. 그리고 consoleAppender라는 이름의 어펜더는 ConsoleAppender 클래스를 이용해서 콘솔에 로그를 출력한다.

<file>을 이용하면 로그 파일의 위치와 이름을 지정할 수 있다. 위 설정은 src/main/resources 소스 폴더에 logs/board_log.log 파일을 생성하고 로그 메세지를 출력하라는 설정이다.

<rollingPolicy>는 로깅 정책을 설정할 때 사용한다. TimeBasedRollingPolicy 클래스를 이용하면 일정한 시간을 기준으로 롤링되는 로그 파일을 생성할 수 있다. <maxHistory>는 롤링 파일이 만들어지는 시간 기준이다. 이 때 압축되는 로그 파일의 패턴을 <fileNamePattern>을 이용하여 지정할 수 있다. 그리고 <encoder>를 사용하여 출력할 로그의 패턴을 지정할 수 있는데, 로그 패턴을 설정할 때는 <pattern>을 사용한다.

마지막으로 <logger>를 이용해서 우리가 작성하는 어플리케이션에서 사용할 로거를 등록한다.

패턴 의미
%d 시간 (yyyy-MM-dd HH:mm:ss,SSS 형태)
%date{format} 원하는 형태로 시간 정보 출력. Ex) %date{yyyy-MM-dd}
%logger{length} Logger이름. {length}는 최대 자릿수. Length에 따라 로거 이름이 축약됨
%thread 현재 스레드 이름
%-5level 로그 레벨. 5는 출력 고정폭 값
%msg 로그 메세지
%n 개행 처리

consoleAppender Sample

2022:04:20 20:31:03.051 INFO  --- [http-nio-8080-exec-1] c.b.s.service.impl.BoardServiceImpl : test 


3.3 독립적으로 실행 가능한 JAR

3.3.1 스프링 부트 빌드 이해하기

springboot_3_2

패키징 파일 구조 이해하기

jar파일 압축을 해제하면 아래와 같은 폴더 구조가 나온다.

springboot_3_3

가장 먼저 보이는 BOOT-INF 폴더는 classes와 lib 폴더로 구성되어 있는데, classes 폴더에는 src/main/java에서 컴파일한 클래스 파일들과 src/main/resources에 작성한 여러 설정 파일들이 모두 포함되어있다. 그리고 가장 중요한 lib 폴더에는 프로젝트에 등록된 모든 라이브러리들이 포함되어 있는 것을 확인할 수 있다.

JAR파일에는 반드시 어플리케이션의 메타데이터가 저장된 메페스트 파일이 있어야 한다. 그리고 그 위치는 META-INF 폴더로 정해져 있다.

MANIFEST.MF

Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Main-Class: org.springframework.boot.loader.JarLauncher
Start-Class: com.bys.sample.LearnApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Version: 2.6.6

위 설정을 보면 Main-Class는 어플리케이션.jar 실행을 위한 메인 클래스가 JarLauncher 클래스라는 의미다. 그리고 Start-Class 정보는 실제로 com.bys.sample.LearnApplication 클래스를 시작으로 어플리케이션을 실행한다는 의미다. 그리고 스프링 부트에서 컴파일한 클래스들은 BOOT-INF/classes/ 폴더에 있다는 것과 어플리케이션 수행에 필요한 모든 JAR파일들은 BOOT-INF/lib/ 폴더에 있다는 것도 알려준다.

스프링 부트는 이 메타데이터 파일을 기반으로 어플리케이션 수행에 필요한 다양한 정보들을 해석하고 처리하는 것이다.


3.3.2 Runnable JAR 실행하기

어떻게 웹 어플리케이션을 WAR가 아닌 JAR 파일로 패키징하여 실행할 수 있었던 것일까?

스프링 부트 로더 확인하기

원래 기본적으로 JAR파일은 또 다른 JAR파일을 포함할 수 없다. JAR파일들이 제공하는 클래스들을 굳이 사용하고 싶다면 JAR파일들을 모두 압축해제하고 특정 폴더에 통합한 다음 다시 JAR파일로 압축하면 된다. 하지만 이런 식으로 JAR 파일들을 사용한다면 사용해야 하는 JAR파일들이 많을수록 해야 할 일들도 많고 관리하기도 복잡하다.

스프링 부트는 패키징된 JAR 파일 안에 있는 또 다른 JAR 파일을 읽어서 클래스들을 로딩하는 유틸리티 클래스를 제공한다. 이 클래스의 정확한 위치는 org/springframework.boot/loader/jar 폴더다. org/springframework.boot/loader/jar 폴더에 JarFile이라는 로더 클래스가 어플리케이션에서 BOOT-INF/lib 폴더에 있는 수많은 JAR파일들을 사용할 수 있도록 로딩해주는 역할을 한다.

springboot_3_4

위에 서 언급된 JarLauncher 클래스도 org/springframework.boot/loader/ 폴더에 있다. JarLauncher 클래스는 application.jar 파일의 메타데이터 정보가 저장된 META-INF/MANIFEST.MF 파일로부터 관련된 정보를 읽어 main() 메서드가 있는 메인 클래스를 찾아 실행하도록 구현되어 있다.





Reference

  • 스프링 부트 (채규태)

Tag: [ book  programming  spring  framework  springboot  @springboottest  mockmvc  @webmvctest  @autoconfiguremocktest  mockmvcresultmatchers  slf4j  jar  ]