스프링 부트 추천 강좌, 스프링 부트 개념과 활용 정리 및 후기
인프런의 스프링 부트 개념과 활용 - 백기선을 수강하면서 내용을 정리하고 간단한 후기를 남깁니다.
스프링 부트 시작하기
Spring Boot Reference Documentation의 Introducing Spring Boot에 따르면 스프링 부트는 스프링기반 독립적인 어플리케이션을 쉽게 만들 수 있도록 지원한다.
스프링이나 3th 파티 라이브러리들을 최소한의 노력으로 사용할 수 있다.
스프링 부트의 목적은 다음과 같다.
- 스프링 개발자가 빠르고 폭넒게 개발할 수 있도록 제공한다.
- 기본적으로 제공하는 설정을 빠르게 수정할 수 있다.
- Embedded 서버나 보안, 메트릭 등과 같은 비기능적 요소들을 폭넒게 제공한다.
- 더 이상 XML 설정이나 코드 generation을 하지 않는다.
스프링 부트 원리
의존성 관리
어떻게 스프링 부트를 사용할때 의존성을 하나만 추가했는데도 많은 의존성들이 추가될까?
이는 스프링 부트에서 의존성 관리기능을 제공하는데 스프링 부트 설정시 maven
에서 의존성 설정할때 Springboot에서 제공하는 부모 의존성으로 상속받아 사용한다.
자세한 내용은 Spring Boot Reference Documentation의 Using Spring Boot 참고
자동 설정
Springboot가 Bean
을 등록하는 단계는 두 단계로 이루어져있다. 바로 @ComponentScan
와 @EnableAutoConfiguration
에 각각 빈을 등록하는 과정이 이루어 진다.
그렇다면 둘간 차이점을 무엇일까?
@ComponentScan
은 @Component
와 @Configuration
, @Repository
, @Service
, @Controller
, @RestController
어노테이션을 찾아서 빈을 등록하고
@EnableAutoConfiguration
은 spring-boot-autoconfigure
의 META-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);
}
}
어플리케이션을 실행한 뒤 다른 무엇인가를 실행하고 싶을 때는 ApplicationRunner
나 CommandLineRunner
를 사용하면 된다.
외부 설정
프로퍼티를 설정함에 있어 우선 순위에 따라 상위 순위를 가지는 값이 하위 순위를 가지는 값을 대체하여 사용한다. 이에 대한 자세한 내용은 Spring에서 제공하는 프로퍼티의 우선 순위를 참조하면 되고, 간단한 우선순위를 나열하면 다음과 같다.
- 테스트 프로퍼티 설정
- 커맨드라인 설정
- 환경변수 설정
application.properties
설정. 혹은YAML
application.properties
를 resources
외에서도 읽을 수 있는데 우선순위는 다음과 같다
file:./config/
file:./
classpath:/config/
classpath:/
사용할 수 있는 파일의 종류는 다음과${random.*}
을 활용하여 랜덤값을 활용할 수 있다..
@ConfigurationProperties("something for key")
를 활용하여 프로퍼티의 값들을 타입-세이프하게 bean
으로 묶어서 활용할 수 있다.
프로파일
Spring에는 프로파일에 따라 설정을 다르게 할 수 있도록 지원한다.
프로파일을 설정하는 방법은 properties
에 spring.profiles.active
에 프로파일명을 설정하면 되고 java
@Configuration @Profile("production")
어노테이션을 추가해서 프로파일에 따라 설정을 다르게 관리할 수 있다.
spring.profiles.include
를 활용하여 다른 프로퍼티 파일을 포함하여 사용할 수 있다.
로깅
스프링 5.0부터 Commons Logging
을 SLF4j
로 로깅 퍼사드를 변경했다고 한다. 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
가 동작한다.
HttpMessageConvertersAutoConfiguration
의 JacksonHttpMessageConvertersConfiguration
을 확인하면 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 Features나 Spring 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가 클래스패스에 있으면 자동 설정이 DataSource
와 JdbcTemplate
을 등록해준다.
인-메모리 데이터베이스의 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
그 밖에 스크립트를 사용할 수도 있는데 resources
에 schema.sql
과 data.sql
파일을 생성하면 스프링부트가 구동시에 순차적으로 읽어서 테이블을 생성하고 데이터를 추가해준다.
이 때, 스프링부트가 구동시마다 SQL이 호출되니 관리를 잘해야한다.
또 한, 프로퍼티에 spring.datasource.plaform=postgresql
을 설정해서 schema-{platform}.sql
과 data-{platform}.sql
과 같이 플랫폼에 따라 SQL을 관리할 수도 있다.
설정을 하는데 있어서 추천하는 방법은 개발시에는 update
로 사용하다가 배포할때쯤 validate
로 사용하고, 운영으로 넘어가면 스크립트 파일로 관리하는것을 추천한다.
데이터베이스 마이그레이션 툴
스프링부트에는 데이터베이스 마이그레이션 툴도 지원을 하는데 Flyway 와 Liquibase가 있습니다.
의존성을 추가한다.
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
에 의하여 SecurityAutoConfiguration
과 UserDetailsServiceAutoConfiguration
을 등록하여 기본설정으로 Security를 적용해 준다.
SecurityAutoConfiguration
은 SpringBootWebSecurityConfiguration
을 import하고 DefaultAuthenticationEventPublisher
빈을 등록 한다.
SpringBootWebSecurityConfiguration
은 WebSecurityConfigurerAdapter
를 사용하는데 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-test
를 test
스코프로 의존성을 추가한 후 @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서비스의 호출을 위해 RestTemplate
과 WebClient
를 제공하는데, 스프링부트에서는 이를 지원하기 위해 각각 RestTemplateBuilder
와 WebClient.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
는 스프링부트에서 모니터링 및 운영 환경에 유용한 기능을 제공한다.
JMX
와 HTTP
를 통해서 접근이 가능하고, 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를 참고하면 된다.
후기
근래에 국가적으로나 개인적으로 뒤숭숭한 분위기여서 그런지 집중해서 짧은 시간에 보지 못하고 틈틈히 오랜 기간을 가지고 본 강의였습니다.
백기선님의 강의를 볼 때마다 단순한 지식의 전달에 그치치 않고, 레퍼런스를 참고하여 이해하는 방식이나 원리부터 설명해주시는점. 그리고 부연설명까지 해주시는게 단순한 강의를 보는것이 아니라 좋은 개발자의 지식을 전달받는다는 느낌이 들어 좋습니다.
이번 스프링 부트 개념과 활용 - 백기선에서도 스프링부트의 원리부터 활용, 운영에 도움이 될만한 것들까지 알려주셔서 의미있게 본 강의였고, 스프링부트뿐만이 아니라 스프링에 대한 개념도 잡아갈 수 있었습니다.
스프링부트를 처음 접하시거나 제대로 개념을 잡고 싶으신 분들께 추천하는 강의입니다.