잡동사니

스프링 부트 추천 강좌, 스프링 부트 개념과 활용 정리 및 후기 본문

IT/Spring

스프링 부트 추천 강좌, 스프링 부트 개념과 활용 정리 및 후기

yeTi 2020. 4. 2. 11:33

인프런스프링 부트 개념과 활용 - 백기선을 수강하면서 내용을 정리하고 간단한 후기를 남깁니다.

스프링 부트 시작하기

Spring Boot Reference DocumentationIntroducing Spring Boot에 따르면 스프링 부트는 스프링기반 독립적인 어플리케이션을 쉽게 만들 수 있도록 지원한다.

스프링이나 3th 파티 라이브러리들을 최소한의 노력으로 사용할 수 있다.

스프링 부트의 목적은 다음과 같다.

  • 스프링 개발자가 빠르고 폭넒게 개발할 수 있도록 제공한다.
  • 기본적으로 제공하는 설정을 빠르게 수정할 수 있다.
  • Embedded 서버나 보안, 메트릭 등과 같은 비기능적 요소들을 폭넒게 제공한다.
  • 더 이상 XML 설정이나 코드 generation을 하지 않는다.

스프링 부트 원리

의존성 관리

어떻게 스프링 부트를 사용할때 의존성을 하나만 추가했는데도 많은 의존성들이 추가될까?

이는 스프링 부트에서 의존성 관리기능을 제공하는데 스프링 부트 설정시 maven에서 의존성 설정할때 Springboot에서 제공하는 부모 의존성으로 상속받아 사용한다.

자세한 내용은 Spring Boot Reference DocumentationUsing Spring Boot 참고

자동 설정

Springboot가 Bean을 등록하는 단계는 두 단계로 이루어져있다. 바로 @ComponentScan@EnableAutoConfiguration에 각각 빈을 등록하는 과정이 이루어 진다.

그렇다면 둘간 차이점을 무엇일까?

@ComponentScan@Component@Configuration, @Repository, @Service, @Controller, @RestController 어노테이션을 찾아서 빈을 등록하고

@EnableAutoConfigurationspring-boot-autoconfigureMETA-INF -> spring.factories -> org.springframework.boot.autoconfigure.EnableAutoConfiguration의 값에 정의된 @Configuration 어노테이션을 찾아서 빈을 등록해준다. 단, @ConditionalOn...에 정의된 것에 따라 조건에 맞게 빈을 등록한다.

AutoConfigure 만들기 - Spring-Boot-Starter 모듈 생성

의존성 추가

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-autoconfigure</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-autoconfigure-processor</artifactId>
        <optional>true</optional>
    </dependency>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <optional>2.0.3.RELEASE</optional>
            </dependency>
        </dependencies>
    </dependencyManagement>
</dependencies>

사용할 클래스 구현

public class Holoman {
    String name;
    int howLong;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getHowLong() {
        return howLong;
    }
    public void setHowLong(int howLong) {
        this.howLong = howLong;
    }

    @Override
    public String toString() {
        return "Holoman{" +
               "name='" + name + '\'' +
               ", howLong=" + howLong +
               '}';
    }
}

Configuration 구현

@Configuration
public class HolomanConfiguration {
    @Bean
    @ConditionalOnMissingBean //외부에서 등록한 빈이 없을 경우에만 등록해라
    public Holoman holoman() {
        Holoman holoman = new Holoman();
        holoman.setHowLong(5);
        holoman.setName("hosung");
        return holoman;
    }
}

spring.factories에 Configuration 등록

# resources > META-INF > spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.ho.HolomanConfiguration

AutoConfigure 만들기 - Property를 활용한 빈 설정

의존성 추가

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
</dependency>

프로퍼티 클래스 구현

@ConfigurationProperties("holoman")
public class HolomanProperties {
    private String name;
    private int howLong;

    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getHowLong() {
        return howLong;
    }
    public void setHowLong(int howLong) {
        this.howLong = howLong;
    }
}

Configuration 구현

@Configuration
@EnableConfigurationProperties(HolomanProperties.class)
public class HolomanConfiguration {
    @Bean
    @ConditionalOnMissingBean //외부에서 등록한 빈이 없을 경우에만 등록해라
    public Holoman holoman(HolomanProperties properties) {
        Holoman holoman = new Holoman();
        holoman.setHowLong(properties.getHowLong());
        holoman.setName(properties.getName());
        return holoman;
    }
}

빈 사용시 application.properties에 정의하여 사용

# application.properties
holoman.name=hosung
holoman.how-long=6

내장 웹 서버

Springboot는 ServletContainer를 내장하고 있고, Tomcat, Jetty, Undertow를 지원한다.

Tomcat을 직접 띄워보려면 다음과 같이 구현하면 된다.

Tomcat tomcat = new Tomcat();
tomcat.setPort(8080);

Context context = tomcat.addContext("/", "/");

HttpServlet servlet = new HttpServlet() {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        PrintWriter writer = resp.getWriter();
        writer.println("<html><head><title>");
        writer.println("Hey, Tomcat");
        writer.println("</title></head>");
        writer.println("<body><h1>Hello Tomcat</h1></body>");
        writer.println("</html>");
    }
};

String servletName = "helloServlet";
tomcat.addServlet("/", servletName, servlet);
context.addServletMappingDecoded("/hello", servletName);

tomcat.start();
tomcat.getServer().await();

이러한 과정을 Springboot에서는 AutoConfiguration을 활용하여 등록하는 과정을 수행한다. ServletContainer는 ServletWebServerFactoryAutoConfiguration에서 등록하고 DispatcherServlet은 DispatcherServletAutoConfiguration에서 등록한다.

서버를 커스터마이즈하고 싶다면 TomcatServletWebServerFactoryCustomizer를 활용하면 된다.

Customize servlet container

서블릿 컨테이너의 변경

의존성 변경

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <exclusions>
        <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-tomcat</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jetty</artifactId>
</dependency>
웹서버 사용하지 않기

자바 코드로 구현

// App.java main()
SpringApplication application = new SpringApplication(Application.class);
application.setWebApplicationType(WebApplicationType.NONE);
application.run(args);

프로퍼티 활용

spring.main.web-application-type=none
포트

프로퍼티를 활용하여 포트 변경

# 포트를 7070으로 변경
server.port=7070

# 포트를 랜덤으로 설정
server.port=0

서버의 포트를 자바 코드로 확인하고 싶을 때는 다음과 같이 구현할 수 있다.

@Component
public class PortListener implements ApplicationListener<ServletWebServerInitializedEvent> {
    @Override
    public void onApplicationEvent(ServletWebServerInitializedEvent servletWebServerInitializedEvent) {
        ServletWebServerApplicationContext applicationContext = servletWebServerInitializedEvent.getApplicationContext();
        System.out.println(applicationContext .getWebServer().getPort());
    }
}

HTTPS 설정

프로퍼티에 다음과 같이 SSL 정보를 설정한다.

server.ssl.key-store=keystore.p12
server.ssl.key-store-type=PKCS12
server.ssl.key-store-password=123456
server.ssl.key-alias=spring

server.port=8443

여기서 HTTPS를 설정하면 HTTP는 사용할 수 없게 된다. 이에 다른 포트를 활용하여 HTTP를 사용하려면 다음과 같이 ServerFactory를 만들면 된다.

@Bean
public ServletWebServerFactory serverFactory() {
    TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory ();
    tomcat.addAdditionalTomcatConnectors(createStandardConnector());
    return tomcat;
}

private Connector createStandardConnector() {
    Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocal");
    connector.setPort(8080);
    return connector;
}

HTTP2는 아래와 같이 간단히 설정을 추가하면 적용할 수 있다. 단, 서버에 따라 추가적인 작업이 필요할 수도 있기 때문에 레퍼런스를 참고하는것을 추천한다.

server.http2.enabled=true

스프링 부트 활용

SpringApplication

ApplicationContext의 lifecycle에 따라 이벤트를 호출할 수 있다. 특이점은 ApplicationContext가 만들어지기전에 발생하는 이벤트는 @Bean으로 등록하여 사용할 수가 없어 직접 등록해줘야한다.

그 예시는 다음과 같다.

public class SampleListener implements ApplicationListener<ApplicationStartingEvent> {
    @Override
    public void onApplicationEvent(ApplicationStartingEvent applicationStartingEvent) {
        System.out.println("=======================");
        System.out.println("Application is starting");
        System.out.println("=======================");
    }
}

@Component
public class SampleListener implements ApplicationListener<ApplicationStartedEvent> {
    @Override
    public void onApplicationEvent(ApplicationStartedEvent applicationStartedEvent) {
        System.out.println("=======================");
        System.out.println("Application is started");
        System.out.println("=======================");
    }
}

@SpringBootApplication
public class SpringinitApplication {
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(SpringinitApplication.class);
        app.addListeners(new SampleListener());
        app.run(args);
    }
}

어플리케이션을 실행한 뒤 다른 무엇인가를 실행하고 싶을 때는 ApplicationRunnerCommandLineRunner를 사용하면 된다.

외부 설정

프로퍼티를 설정함에 있어 우선 순위에 따라 상위 순위를 가지는 값이 하위 순위를 가지는 값을 대체하여 사용한다. 이에 대한 자세한 내용은 Spring에서 제공하는 프로퍼티의 우선 순위를 참조하면 되고, 간단한 우선순위를 나열하면 다음과 같다.

  1. 테스트 프로퍼티 설정
  2. 커맨드라인 설정
  3. 환경변수 설정
  4. application.properties 설정. 혹은 YAML

application.propertiesresources외에서도 읽을 수 있는데 우선순위는 다음과 같다

  1. file:./config/
  2. file:./
  3. classpath:/config/
  4. classpath:/

사용할 수 있는 파일의 종류는 다음과
${random.*}을 활용하여 랜덤값을 활용할 수 있다..

@ConfigurationProperties("something for key")를 활용하여 프로퍼티의 값들을 타입-세이프하게 bean으로 묶어서 활용할 수 있다.

프로파일

Spring에는 프로파일에 따라 설정을 다르게 할 수 있도록 지원한다.

프로파일을 설정하는 방법은 propertiesspring.profiles.active에 프로파일명을 설정하면 되고 java @Configuration @Profile("production")어노테이션을 추가해서 프로파일에 따라 설정을 다르게 관리할 수 있다.

spring.profiles.include를 활용하여 다른 프로퍼티 파일을 포함하여 사용할 수 있다.

로깅

스프링 5.0부터 Commons LoggingSLF4j로 로깅 퍼사드를 변경했다고 한다. Commons Logging에는 개발자들을 괴롭히는 많은 문제점들이 있었는데 스프링 5.0이전에는 SLF4j를 위해 pom.xml에 부가적인 설정이 필요했다고 한다. 현재 사용하고 있는 Spring-v2.2에서는 다음과 같이 Log 의존성을 가지고 있다.

\--- org.springframework.boot:spring-boot-starter-web -> 2.2.4.RELEASE
     +--- org.springframework.boot:spring-boot-starter:2.2.4.RELEASE
     |    +--- org.springframework.boot:spring-boot-starter-logging:2.2.4.RELEASE
     |    |    +--- ch.qos.logback:logback-classic:1.2.3
     |    |    |    +--- ch.qos.logback:logback-core:1.2.3
     |    |    |    \--- org.slf4j:slf4j-api:1.7.25 -> 1.7.30
     |    |    +--- org.apache.logging.log4j:log4j-to-slf4j:2.12.1
     |    |    |    +--- org.slf4j:slf4j-api:1.7.25 -> 1.7.30
     |    |    |    \--- org.apache.logging.log4j:log4j-api:2.12.1
     |    |    \--- org.slf4j:jul-to-slf4j:1.7.30
     |    |         \--- org.slf4j:slf4j-api:1.7.30

위에서 확인한 바와같이, 로깅 퍼사드로 Slf4j를 사용하고 있고 로거로 Logback을 사용하고 있다.

로그 정보를 커스텀한 파일로 설정하기 위해서는 다음과 같이 파일을 추가하여 사용하면 된다.

  • Logback : logback-spring.xml
  • Log4j : log4j-spring.xml
  • JUL(비추) : logging.properties

로거를 변경하고 싶을때는 기존의 로거를 exclude하고 새로운 로거를 디펜던시에 추가하면 된다. 다음에 Log4j2로 로거를 변경하는 예제를 보자.

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <exclusions>
            <exclusion>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-logging</artifactId>
            </exclusion>
        </exclusions>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-log4j2</artifactId>
    </dependency>
</dependencies>

테스트

spring-boot-starter-test의 의존성을 보면, 다음과 같이 테스트를 위한 다양한 의존성을 가지는것을 확인할 수 있다.

\--- org.springframework.boot:spring-boot-starter-test -> 2.2.4.RELEASE
     +--- com.jayway.jsonpath:json-path:2.4.0
     +--- jakarta.xml.bind:jakarta.xml.bind-api:2.3.2
     +--- org.junit.jupiter:junit-jupiter:5.5.2
     +--- org.mockito:mockito-junit-jupiter:3.1.0
     +--- org.assertj:assertj-core:3.13.2
     +--- org.hamcrest:hamcrest:2.1
     +--- org.mockito:mockito-core:3.1.0 (*)
     +--- org.skyscreamer:jsonassert:1.5.0
     +--- org.xmlunit:xmlunit-core:2.6.3
     \--- ...

기본적으로 테스트를 위해서 @SpringBootTest를 활용할 수 있다. 옵션에 따라 내장 톰캣의 사용여부를 설정할 수 있고 MockBean을 활용하여 빈들을 목킹하여 테스트할 수 있도록 지원한다.

@SpringBootTest의 경우 컨텍스트에 있는 모든 빈을 등록하므로 테스트를 하기에 무거울 수 있다. 이를 위해 @WebMvcTest, JsonTest, DataJpaTest 등 슬라이스 테스트를 위한 어노테이션들을 지원한다.

간단한 샘플예제는 Github에서 확인해 볼 수 있고 보다 자세한 내용은 Spring Boot Features를 참고하면 된다.

Spring Web MVC

HttpMessageConverters

HttpMessageConverters란 HTTP 요청의 body를 객체로 변경하거나 객체를 HTTP 응답의 body로 변경할 때 사용한다. @RequestBody@ResponseBody를 사용하면 HttpMessageConverters가 동작한다.

HttpMessageConvertersAutoConfigurationJacksonHttpMessageConvertersConfiguration을 확인하면 JSON Converter와 XML Converter가 만들어지는것을 확인할 수 있다.

자세한 내용은 Spring Boot Features를 참고하면 된다.

ViewResolver

클라이언트의 요청한 헤더의 accept 필드의 값에 따라 응답하는 형태가 달라지는데, 이러한 동작이 ContentNegotiatingViewResolver를 통해서 이루어진다.

자세한 내용은 Spring reference를 참고하면 된다.

정적 리소스 지원

스프링부트에서는 정적 리소스를 맵핑해주는 디렉토리를 제공한다.

  • /static
  • /public
  • /resources
  • /META-INF/resources

리소스에 접근하는 URI 경로나 디렉토리 경로는 다음과 같이 프로퍼티로 설정할 수 있다.

spring.mvc.static-path-pattern=/resources/**
spring.mvc.static-locations=/m/

하지만 spring.mvc.static-locations을 사용하게 되면 스프링부트가 기본적으로 지원하는 경로는 사용할 수 없기 때문에 다음과 같이 WebMvcConfigurer를 활용하여 리소스를 추가하는 방식을 추천한다.

@Component
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/m/**")
            .addResourceLocations("classpath:/m/")
            .setCachePeriod(20);
    }

}

Webjars는 웹 클라이언트의 라이브러리들을 Jar형태로 읽어들여 사용할 수 있는데 디펜던시를 추가하면 JQuery나 Bootstrap과 같은 클라이언트 라이브러리들을 사용할 수 있다.

// JQuery webjar 추가
implementation 'org.webjars.bower:jquery:3.4.1'
<script src="/webjars/jquery/3.4.1/dist/jquery.min.js"></script>
<script type="text/javascript">
    $(function() {
        alert("ready");
    })
</script>

자세한 내용은 Spring Boot Features를 참고하면 된다.

웰컴페이지는 index.html을 추가하면 되고, 파비콘은 favicon.ico파일을 추가하면 스프링부트가 인식한다.

템플릿 엔진

스프링부트에서는 다음과 같은 동적 템플릿 엔진을 지원한다.

참고적으로 JSP는 권장하지 않는다.

템플릿 엔진 중 하나인 Thymeleaf를 활용한 예제는 다음과 같다.

// 동적 뷰를 위한 Thymeleaf 추가
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
@Controller
public class ViewController {

    @GetMapping("/hello")
    public String hello(Model model) {
        model.addAttribute("name", "hosung");
        return "hello";
    }
}
<!-- Hello.html -->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="EUC-KR">
<title>Insert title here</title>
</head>
<body>
<h1 th:text="${name}">Name</h1>
</body>
</html>

자세한 내용은 Spring Boot Features를 참고하면 된다.

HTML을 보다 자세하게 테스트하고 싶은 경우에는 HtmlUnit 사용하면 된다.

ExceptionHandler

Spring Web MVC에서는 @ControllerAdvice@ExceptionHandler를 활용하는 어노테이션기반 에러처리를 할 수 있다.

@ExceptionHandler(UserException.class)
public @ResponseBody AppError appError(UserException userException) {
    AppError appError = new AppError();
    appError.setMessage("App error");
    appError.setReason("IDK IDK IDK");
    return appError;
}

스프링부트에서는 BasicErrorController에서 에러처리에 대한 로직이 들어있다. 이를 상속받아서 커스터마이즈를 할 수 있고, 정적 리소스에 /error폴더를 추가하여 에러코드별 노출 페이지를 커스터마이즈 할 수 있다.

자세한 내용은 Spring Boot Features를 참고하면 된다.

HATEOAS

HATEOAS는 Hypermedia as the Engine of Application State의 약자로 RestAPI에서 리소스정보를 클라이언트에게 제공하는 컴포넌트이다.

스프링부트에서는 이를 간단한 의존성 추가만으로 쉽게 사용할 수 있고 다음 예제를 통해 간단한 활용법을 확인할 수 있다.

// HATEOAS 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-hateoas'
@GetMapping("/users")
public EntityModel<User> getUsers() {
    User user = new User();
    user.setUsername("hosung");
    user.setPassword("123");

    EntityModel<User> userModel = new EntityModel<>(user);

//        @Deprecated
//        import static org.springframework.hateoas.server.mvc.ControllerLinkBuilder.linkTo;
//        import static org.springframework.hateoas.server.mvc.ControllerLinkBuilder.methodOn;
//        userModel.add(linkTo(methodOn(UserController.class).getUsers()).withSelfRel());

//        Using Link
    userModel.add(new Link("/users"));

    return userModel;
}
@Test
void testGet() throws Exception {
    mockMvc.perform(get("/users"))
        .andDo(print())
        .andExpect(status().isOk())
        .andExpect(jsonPath("$._links.self").exists());
}

자세한 내용은 Spring Boot FeaturesSpring HATEOAS를 참고하면 된다.

CORS

Origin이란 scheme, host, port 로 구성된 URI로 http://localhost:8080을 기반으로한 예제는 다음과 같다.

  • Scheme : http or https
  • Host : localhost
  • Port : 8080

사용자가 웹 사이트에 악의적인 행동을 하는 것을 예방하기 위해서 다른 origin으로부터의 접근을 막는 정책을 사용하는데 이를 SOP(Same-Origin Policy)라고 하고 웹 표준이다.

이에 반하여 다른 도메인에서도 접근할 수 있는 방법을 제공하는데, 이를 CORS(Cross-Origin Resource Sharing)이라고 하고 W3C에서 제공하는 비표준 정책이다. 그럼에도 많이들 사용해서 그런지 스프링부트에서 CORS를 간단한 설정만으로 사용할 수 있도록 지원한다.

@CrossOrigin을 활용하여 특정 API에 CORS를 설정할 수 있다.

@CrossOrigin(origins = "http://localhost:8082")
@GetMapping("/users/1")
public User getUser1() {
    User user = new User();
    user.setUsername("hosung1");
    user.setPassword("111");

    return user;
}

글로벌로 설정하고 싶은경우 WebMvcConfigurer를 사용하면 된다.

@Component
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
            .allowedOrigins("http://localhost:8082");
    }

}

Spring Data

인메모리 데이터베이스

지원하는 인-메모리 데이터베이스는 다음과 같다.

  • H2(추천, 콘솔을 지원하기 때문에)
  • HSQL
  • Derby

spring-boot-starter-jdbc의 존성을 추가하여 Spring data를 사용할 수 있는데, Spring-JDBC가 클래스패스에 있으면 자동 설정이 DataSourceJdbcTemplate을 등록해준다.

인-메모리 데이터베이스의 default value를 확인하고 싶으면, DataSourceProperties클래스를 확인하면 된다.

스프링부트에서는 H2에 접근할 수 있는 콘솔을 지원하는데 spring-boot-devtools의존성을 추가하거나 spring.h2.console.enable=true프로퍼티를 설정하면 사용할 수 있다. 접근 URL은 /h2-console이다.

자세한 내용은 Spring Boot Features를 참고하면 된다.

MySQL

스프링부트에서는 DBCP(DabaBase Connetion Pool)로 HikariCP를 사용한다. 그 밖에 Tomcat connection pool이나 Commons DBCP2를 사용할 수 있다.

각 DB pool별로 다음과 같이 프로퍼티를 설정할 수 있다.

  • spring.datasource.hikari.*
  • spring.datasource.tomcat.*
  • spring.datasource.dbcp2.*

테스트를 위해서 다음과 같이 도커로 MySQL을 구동할 수 있다.

docker run -p 3306:3306 --name mysql -e MYSQL_ROOT_PASSWORD=1 -e MYSQL_DATABASE=testdb -e MYSQL_USER=testuser -e MYSQL_PASSWORD=testpass -d mysql

docker exec -i -t mysql bash
# mysql -u testuser -p
구현

MySql DB 커넥터 의존성을 추가한다.

implementation 'mysql:mysql-connector-java'

Datasourse를 설정한다.

spring.datasource.url=jdbc:mysql://localhost/testdb?useSSL=false&allowPublicKeyRetrieval=true
spring.datasource.username=testuser
spring.datasource.password=testpass
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
DB의 선택

MySQL의 경우 GPL 라이센스를 가지고 있어 상용으로 사용할 경우에는 엔터프라이즈를 구매하여 사용하여야 한다.

이에 MySQL의 오픈소스 커뮤니티 버전인 MariaDB가 있으나, GPLv2 라이센스로 상용 서비스로 사용할 수 있지만 소스코드를 오픈해야하는 의무가 발생할 수 있다.

따라서 PostgreSQL이 BSD, MIT 라이센스를 가지고 있어 가장 안전한 선택으로 생각된다.

PostgreSQL

PostgreSQL에 접속하는 방법은 다음과 같다.

도커를 활용하여 PostgreSQL 컨테이너를 시작한다.

docker run -p 5432:5432 -e POSTGRES_DB=testdb -e POSTGRES_USER=testuser -e POSTGRES_PASS=testpass --name=postgres_test -d postgres

docker exec -it postgres_test bash

/# psql --username testuser --dbname testdb
testdb=# \list
testdb=# \dt
구현

PostgreSQL DB 커넥터 의존성을 추가한다.

implementation 'org.postgresql:postgresql'

Datasourse를 설정한다.

spring.datasource.url=jdbc:postgresql://localhost/testdb
spring.datasource.username=testuser
spring.datasource.password=testpass

JPA

스프링 데이터 JPA에 대한 설명은 이전에 작성한 김영한님의 JPA 강의 후기Spring Boot Features에 자세하게 되어있다.

여기서는 복습하는 차원에서 간단하게 언급하자면, JPA는 자바에서 ORM을 제공하기 위한 표준이고 JPA의 구현체로 하이버네이트를 사용하고 있다. 또한, 하이버네이트는 JDBC를 사용하여 DB에 접근한다.

spring-boot-starter-data-jpa 의존성을 추가하는 것 만으로도 AutoConfigutation이 많은 설정들을 등록해줘서 JPA를 간편하게 사용할 수 있도록 해준다.

데이터베이스의 초기화

운영시에는 자동 생성 기능을 사용하면 안된다.

spring.jpa.generate-ddl=false
spring.jpa.hibernate.ddl-auto=validate

그 밖에 스크립트를 사용할 수도 있는데 resourcesschema.sqldata.sql파일을 생성하면 스프링부트가 구동시에 순차적으로 읽어서 테이블을 생성하고 데이터를 추가해준다.

이 때, 스프링부트가 구동시마다 SQL이 호출되니 관리를 잘해야한다.

또 한, 프로퍼티에 spring.datasource.plaform=postgresql을 설정해서 schema-{platform}.sqldata-{platform}.sql과 같이 플랫폼에 따라 SQL을 관리할 수도 있다.

설정을 하는데 있어서 추천하는 방법은 개발시에는 update로 사용하다가 배포할때쯤 validate로 사용하고, 운영으로 넘어가면 스크립트 파일로 관리하는것을 추천한다.

데이터베이스 마이그레이션 툴

스프링부트에는 데이터베이스 마이그레이션 툴도 지원을 하는데 FlywayLiquibase가 있습니다.

의존성을 추가한다.

implementation 'org.flywaydb:flyway-core'

마이그레이션 디렉토리에 SQL파일을 추가한다. db/migration/V1__init.sql 또는 db/migration/mysql/V1__init.sql와 같은 형식으로 사용할 수 있고, 프로퍼티에 설정을 추가하여 경로를 변경할 수도 있습니다.

spring.flyway.locations=classpath:db/migration/{vendor}

자세한 내용은 “How-to” Guides를 참고하면 된다.

Redis

도커에 Redis 컨테이너를 시작한다.

> docker run -p 6379:6379 --name redis -d redis
> docker exec -it redis redis-cli

keys *
get [key]
hget [key] [column]
hgetall [key]

의존성을 추가한다.

implementation 'org.springframework.boot:spring-boot-starter-data-redis'

Runner를 추가한다.

@Autowired
private StringRedisTemplate redisTemplate;

@Autowired
AccountRepository accountRepository;

@Override
public void run(ApplicationArguments args) throws Exception {
    ValueOperations<String, String> opsForValue = redisTemplate.opsForValue();
    opsForValue.set("key1", "value1");
    opsForValue.set("key2", "value2");
    opsForValue.set("key3", "value3");

    Account account = new Account();
    account.setEmail("hosung@email.com");
    account.setUsername("hosung");

    accountRepository.save(account);

    Optional<Account> findById = accountRepository.findById(account.getId());
    System.out.println(findById.get().getUsername());
    System.out.println(findById.get().getEmail());
}

Account 클래스를 추가한다.

@RedisHash("accounts")
public class Account {

    @Id
    private String id;
    private String username;
    private String email;
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }

}

Repository클래스를 추가한다.

public interface AccountRepository extends CrudRepository<Account, String> {
}

자세한 내용은 Spring Boot Features를 참고하면 된다.

MongoDB

도커를 활용하여 MongoDB를 설치한다.

> docker run -p 27017:27017 --name mongo -d mongo
> docker exec -it mongo bash

# mongo

> db.accounts.find({})

의존성을 추가한다. testImplementation 'de.flapdoodle.embed:de.flapdoodle.embed.mongo'같은 경우에는 임베디드 MongoDB를 설치하여 테스트시에 활용하도록 해준다.

// MongoDB 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb'
testImplementation 'de.flapdoodle.embed:de.flapdoodle.embed.mongo'

Runner를 추가한다.

@Autowired
private MongoTemplate mongoTemplate;

@Autowired
private AccountMongoRepository accountRepository;

@Override
public void run(ApplicationArguments args) throws Exception {
    Account account = new Account();
    account.setEmail("hosung@email.com");
    account.setUsername("hosung");

    mongoTemplate.insert(account);

    List<Account> accounts = accountRepository.findByEmail(account.getEmail());
    System.out.println(accounts.size());
    System.out.println(accounts.get(0).getUsername());
    System.out.println(accounts.get(0).getEmail());
}

Account 클래스를 추가한다.

@Document(collection = "accounts")
public class Account {

    @Id
    private String id;
    private String username;
    private String email;
    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }

}

Repository클래스를 추가한다.

public interface AccountMongoRepository  extends MongoRepository<Account, String> {
    List<Account> findByEmail(String email);
}

테스트코드를 추가한다.

@DataMongoTest
class AccountMongoRepositoryTest {

    @Autowired
    private AccountMongoRepository accountRepository;

    @Test
    void testFindByEmail() {
        Account account = new Account();
        account.setEmail("hosung@email.com");
        account.setUsername("hosung");
        accountRepository.insert(account);

        Optional<Account> findById = accountRepository.findById(account.getId());
        assertNotNull(findById.get());
        assertEquals("hosung", findById.get().getUsername());

        List<Account> accounts = accountRepository.findByEmail(account.getEmail());
        assertEquals(1, accounts.size());
    }

}

자세한 내용은 Spring Boot Features, mongo - Docker Hub를 참고하면 된다.

Neo4j

How-To: Run - Neo4j을 참고하여 도커로 Neo4j를 구동합니다. Neo4j는 GUI를 제공하고, URL: bolt://localhost:7687, ID/PW: neo4j/neo4j로 접근할 수 있습니다.

docker run --name neo4j -p7474:7474 -p7687:7687 -d neo4j

의존성을 추가한다.

// Neo4j 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-data-neo4j'

Properties를 추가한다.

## Neo4j
spring.data.neo4j.username=neo4j
spring.data.neo4j.password=testpass

Runner를 추가한다.

//@Autowired
//private SessionFactory sessionFactory;

@Autowired
private AccountNeo4jRepository accountRepository;

@Override
public void run(ApplicationArguments args) throws Exception {
    Role role = new Role();
    role.setName("admin");

    Account account = new Account();
    account.setEmail("hosung@email.com");
    account.setUsername("hosung");
    account.getRoles().add(role);

//    Session session = sessionFactory.openSession();
//    session.save(account);
//    session.clear();
//    sessionFactory.close();

    Account account2 = new Account();
    account2.setEmail("user2@email.com");
    account2.setUsername("user2");
    account2.getRoles().add(role);

    accountRepository.save(account);
    accountRepository.save(account2);

    List<Account> findByUsername = accountRepository.findByUsername("user2");

    System.out.println(findByUsername.size());
    System.out.println(findByUsername.get(0).getUsername());
    System.out.println(findByUsername.get(0).getEmail());
}

Account 클래스를 추가한다.

@NodeEntity
public class Account {

    @Id @GeneratedValue
    private Long id;
    private String username;
    private String email;

    @Relationship(type = "has")
    private Set<Role> roles = new HashSet<>();

    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getUsername() {
        return username;
    }
    public void setUsername(String username) {
        this.username = username;
    }
    public String getEmail() {
        return email;
    }
    public void setEmail(String email) {
        this.email = email;
    }
    public Set<Role> getRoles() {
        return roles;
    }
    public void setRoles(Set<Role> roles) {
        this.roles = roles;
    }

}

Role 클래스를 추가한다.

@NodeEntity
public class Role {

    @Id @GeneratedValue
    private Long id;
    private String name;

    public Long getId() {
        return id;
    }
    public void setId(Long id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

}

Repository클래스를 추가한다.

public interface AccountNeo4jRepository extends Neo4jRepository<Account, Long> {

    List<Account> findByUsername(String username);
}

자세한 내용은 Spring Boot Features를 참고하면 된다.

Security

spring-boot-starter-security 의존성을 추가하여 스프링부트에서 Security를 적용할 수 있는데, spring-security-config, spring-security-web에 대한 의존성을 가지고 있어 내부적으로 Spring Security를 사용한다.

의존성을 추가하면 AutoConfiguration에 의하여 SecurityAutoConfigurationUserDetailsServiceAutoConfiguration을 등록하여 기본설정으로 Security를 적용해 준다.

SecurityAutoConfigurationSpringBootWebSecurityConfiguration을 import하고 DefaultAuthenticationEventPublisher 빈을 등록 한다.

SpringBootWebSecurityConfigurationWebSecurityConfigurerAdapter를 사용하는데 Spring web security의 설정을 적용해서 모든 요청에 대한 권한 요구, 폼 인증 지원, HTTP Basic 인증 지원을 하도록 하고, DefaultAuthenticationEventPublisher는 인증에 대한 오류 이벤트를 핸들링할 수 있도록 해준다.

UserDetailsServiceAutoConfiguration은 In-Memory 저장소에 기본 사용자를 생성하여 인증을 할 수 있도록 해준다.

spring-boot-starter-security를 추가하면 권한없이는 웹 페이지에 접근이 안되고, 접근시 401, Unauthorized, WWW-Authenticate:"Basic realm="Realm"" 를 반환하여 내장된 로그인 form을 띄우도록 한다. HTTP 헤터를 Accept:"text/html"로 설정하면 302, http://localhost/login으로 리다이렉트하여 로그인페이지로 유도한다.

Security를 사용함에 있어 테스트케이스가 깨질 수 있는데, 이 때는 spring-security-testtest스코프로 의존성을 추가한 후 @WithMockUser를 테스트케이스에 선언함으로써 권한을 획득한 상태로 만들 수 있다.

Security 설정

spring-boot-starter-security 의존성을 추가하면 모든 요청에 대해 인증을 요구합니다.

Request에 따라 인증여부를 설정하려면 다음과 같이 WebSecurityConfigurerAdapter를 상속받아 설정하면 된다.

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
        .authorizeRequests()
            .antMatchers("/security/index.html", "/hello").permitAll()
            .anyRequest().authenticated()
            .and()
        .formLogin()
            .and()
        .httpBasic();
    }
}

사용자를 인증하는 부분을 다음과 같이 UserDetailsService를 구현하면 된다.

@Service
public class AccountService implements UserDetailsService {

    @Autowired
    private AccountRepository accountRepository;

    @Autowired
    private PasswordEncoder passwordEncoder;

    public Account createAccount(String username, String password) {
        Account account = new Account();
        account.setUsername(username);
        account.setPassword(passwordEncoder.encode(password));

        return accountRepository.save(account);
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<Account> findByUsername = accountRepository.findByUsername(username);
        Account account = findByUsername.orElseThrow(() -> new UsernameNotFoundException(username));
        return new User(account.getUsername(), account.getPassword(), authorities());
    }

    private Collection<? extends GrantedAuthority> authorities() {
        return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
    }

}

@Bean
public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

자세한 내용은 Spring Boot Features, Spring Security Reference를 참고하면 된다.

Spring REST Client

스프링에는 Rest서비스의 호출을 위해 RestTemplateWebClient를 제공하는데, 스프링부트에서는 이를 지원하기 위해 각각 RestTemplateBuilderWebClient.Builder를 제공한다.

각 클래스는 RestTemplateAutoConfiguration, WebClientAutoConfiguration에 의해 빈이 등록되고, 두 클래스의 가장 큰 차이점은 RestTemplate은 Blocking I/O기반의 Synchronous API이고, WebClient는 Non-Blocking I/O기반의 Asynchronous API인 것이다.

관련된 샘플소스를 참고 할 수 있고 레퍼런스는 다음을 참고하면 된다. Calling REST Services with RestTemplate, Calling REST Services with WebClient

스프링 부트 운영

Actuator

Actuator는 스프링부트에서 모니터링 및 운영 환경에 유용한 기능을 제공한다.

JMXHTTP를 통해서 접근이 가능하고, JMX로 접근할 때는 JConsole이나 VisualVM을 사용하면 된다.

Spring Boot Admin

Spring Boot Admin은 Spring에서 제공하는 Actuator의 정보를 모아 웹 서비스로 제공해주는 서비스입니다.

해당 어드민 서버를 별도의 서비스로 구동합니다.

// 의존성을 추가합니다.
implementation 'de.codecentric:spring-boot-admin-starter-server:2.2.2'

// 어노테이션을 추가합니다.
@EnableAdminServer

모니터링할 서비스에 설정을 해줍니다.

// 의존성을 추가합니다.
implementation 'de.codecentric:spring-boot-admin-starter-client:2.2.2'

// 프로퍼티를 추가합니다.
spring.boot.admin.client.url=http://localhost:8080
management.endpoints.web.exposure.include=*

http://localhost:8081와 같이 Admin Server에 접근한다.

자세한 내용은 Spring Boot Actuator를 참고하면 된다.

후기

근래에 국가적으로나 개인적으로 뒤숭숭한 분위기여서 그런지 집중해서 짧은 시간에 보지 못하고 틈틈히 오랜 기간을 가지고 본 강의였습니다.

백기선님의 강의를 볼 때마다 단순한 지식의 전달에 그치치 않고, 레퍼런스를 참고하여 이해하는 방식이나 원리부터 설명해주시는점. 그리고 부연설명까지 해주시는게 단순한 강의를 보는것이 아니라 좋은 개발자의 지식을 전달받는다는 느낌이 들어 좋습니다.

이번 스프링 부트 개념과 활용 - 백기선에서도 스프링부트의 원리부터 활용, 운영에 도움이 될만한 것들까지 알려주셔서 의미있게 본 강의였고, 스프링부트뿐만이 아니라 스프링에 대한 개념도 잡아갈 수 있었습니다.

스프링부트를 처음 접하시거나 제대로 개념을 잡고 싶으신 분들께 추천하는 강의입니다.

Comments