스프링부트 웹 개발
스프링 프레임워크는 웹 기술을 spring-web, spring-webmvc, spring-websocket, spring-webmvc-portlet 모듈로 지원한다.
spring-web 모듈은 멀티파트 파일업로드, (서블릿 리스너를 이용한)스프링 컨테이너 초기화, 웹 애플리케이션 컨텍스트 등 무릇 웹이라면 갖춰야 할 공통 요소를 제공한다.
spring-mvc 모듈(웹 서버 모듈)은 스프링 MVC(모델-뷰-컨트롤러) 및 웹애플리케이션용 REST 서비스 구현체를 지니고 있다.
두 모듈에는 아주 강력한 JSP 태그 라이브러리, 커스텀 바인딩 및 검증, 유연한 모델 전송, 커스텀 핸들러와 뷰 리졸버 등 다 새로운 기능이 포함되어 있다.
스프링 MVC의 핵심은 org.springframework.web.servlet.DispacherServlet 서블릿 클래스이다.
다른 MVC 웹 프레임워크와는 달리 아주 유연면서도 기능이 탄탄하다.
DispatcherServlet만 있으면 뷰 리졸버, 로케일 리졸버, 테마 리졸버, 예외 핸들러 등의 리졸빙 전략을 바로 적용할 수 있다.
DispatcherServlet은 일단 HTTP 요청을 받아 올바른 핸들러(@Controller를 붙인 클래스에서 @RequestMapping으로 URL을 매핑한 메서드)로 넘기고 올바른 뷰(JSP)가 화면에 표시되도록 감독한다.
스프링 부트 웹 애플리케이션 프로젝트 생성
spring init -d=web, thymeleaf, data-jpa, data-rest -g=com.apress.spring -a=spring-boot-journal-test --package-name=com.apress.spring -name=spring-boot-journal-test -x
윈도우 버전
spring init -dependencies=web,thymeleaf,data-jpa,data-rest spring-boot-journal-test
application.properties
spring.datasource.url=jdbc:mysql://localhost:3306/journal spring.datasource.username=springboot spring.datasource.password=springboot spring.datasource.tomcat.test-while-idle=true spring.datasource.tomcat.validation-query=select 1 spring.datasource.tomcat.connection-properties=useUnicode=true;characterEncoding=utf-8 spring.jpa.show-sql=true spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.hibernate.naming.strategy=org.hibernate.cfg.ImprovedNamingStrategy spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect |
spring.jpa.hibernate.ddl-auto=create-drop
#DB테이블과 필드의 명명전략
spring.jpa.hibernate.naming.strategy=org.hibernate.cfg.ImprovedNamingStrategy
#MySQL DB엔진에 최적화한 SQL를 생성
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect
domain
package com.apress.spring.domain; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import javax.persistence.Transient; import com.apress.spring.utils.JsonDateSerializer; import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonSerialize; @Entity @Table(name="entry") public class JournalEntry { @Id @GeneratedValue(strategy=GenerationType.AUTO) private Long id; private String title; private Date created; private String summary;
@Transient private final SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
public JournalEntry(String title, String summary, String date) throws ParseException { this.title = title; this.summary = summary; this.created = format.parse(date); }
public JournalEntry() {} public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } @JsonSerialize(using=JsonDateSerializer.class) public Date getCreated() { return created; } public void setCreated(Date created) { this.created = created; } public String getSummary() { return summary; } public void setSummary(String summary) { this.summary = summary; } @JsonIgnore public String getCreatedAsShort() { return format.format(created); }
public String toString() { StringBuilder value = new StringBuilder("JournalEntity("); value.append("Id : "); value.append(id); value.append(", 제목 : "); value.append(title); value.append(", 요약 : "); value.append(summary); value.append(", 일자 : "); value.append(getCreatedAsShort()); value.append(")"); return value.toString();
}
} |
@Table(name="entry") 하이버네이트는 entry라는 테이블 생성
@JsonSerialize(using=JsonDateSerializer.class) JsonDateSerializer라는 커스텀 클래스로 데이터를 직렬화
날짜를특정 형식으로 나타낼때 필요한 애노테이션 표준 ISO.DATE 형식의 yyyy-MM-dd 패턴으로 표시
@JsonIgnore JSON문자열로 나타낼 때 대상에서 제외
JSON변환기는 필요한 시점에 JsonDateSerializer 클래스를 불러쓰는데, 스프링 MVC의 HttpMessageConverter<T> 클래스 내부에서 알아서 처리된다.
JsonDateSerializer는 JsonSerializer를 상속하므로 직렬화할 serialize 메서드를 재정의해야하며, 직렬화 로직은 JSON 잭슨 라이브러리를 기반으로 한다.
이 라이브러리는 spring-boot-starter-web 폼에 이미 포함되어 있다.
utils
package com.apress.spring.utils; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.Date; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.JsonSerializer; import com.fasterxml.jackson.databind.SerializerProvider; public class JsonDateSerializer extends JsonSerializer<Date>{ private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyy-MM-dd");
@Override public void serialize(Date date, JsonGenerator gen, SerializerProvider provider) throws IOException, JsonProcessingException { String formattedDate = dateFormat.format(date); gen.writeString(formattedDate);
} } |
repository
package com.apress.spring.repository; import org.springframework.data.jpa.repository.JpaRepository; import com.apress.spring.domain.JournalEntry; public interface JournalRepository extends JpaRepository<JournalEntry, Long> { } |
localhost:8080 접속
브라우저가 HAL+JSON 응답을 받는다.
HAL(Hypertext Application Language)은 링크 등 매체(media)를 나타내는 표현형으로, HATEOAS(Hypermedia as the Engine of Application State)에서 매체 링크를 통해 REST 끝점을 관리하는 방법이다.
pom.xml에 정의한 spring-boot-starter-data-rest 의존체가 REST형 API에서 JPA 모델을 HATEOAS 매체 링크를 통해 표출하기 때문이다.
application/hal+json 응답 결과를 브라우저에서 볼때
터미널 창에서 cURL 명령어로 HAL+JSON 타입 형식의 결과를 볼수 있다.
curl -i http://localhost:8080
또는 크롬에서 JSONView라는 애드온을 설치하면된다.
{ _links: { journalEntries: { href: "http://localhost:8080/journalEntries{?page,size,sort}", templated: true }, profile: { href: "http://localhost:8080/profile" } } } |
JSON문자열을 보면 _links 키 하위에 일기 레코드에 해당하는 journalEntries(도메인 클래스 JournalEntry의 복수형)와 profile이 있고 http://localhost:8080/journalEnries 같은 링크값이 섞여 있다.
첫번째 링크 journalEntries(http://localhost:8080/journalEntries{?page,size,sort})는 클릭시 에러가 나기 때문에 http://localhost:8080/journalEntries만 접속하게끔 조정해야 한다.
실제 링크에 기본값을 주면 된다.
http://localhost:8080/journalEntries 접속
spring-starter-data-rest, spring-boot-starter-data-jpa 두 스타터 폼을 적용한 다음 JpaRepository의 확장 인터페이스를 구현한 결과이다.
실제로 MySQL 서버에서 데이터를 끌어오는 부분은 _embedded/journalEntires이다.
스프링 데이터 REST는 기본적으로 엔티티를 복수형으로 생성하기 때문에 JournalEntry 클래스는 곧 journalEntries 컬렉션이 된다.
mysql 쉘로 MySQL 서버에 접속해서 확인해보면 JournalEntry 클래스에 붙인 @Table 애노테이션의 속성값인 entry 테이블이 생성되어있다.
data.sql 파일에 쿼리문을 작성한다.
INSERT INTO ENTRY(title, summary, created) VALUES ('스프링 부트 입문','오늘부터 스프링 부트를 배웠다.','2016-01-02 00:00:00'); INSERT INTO ENTRY(title, summary, created) VALUES ('간단한 스프링 부트 프로젝트','스프링 부트 프로젝트를 처음 만들어보았다.','2016-01-03 00:00:00'); INSERT INTO ENTRY(title, summary, created) VALUES ('스프링 부트 해부 ','스프링 부트를 자세히 살펴보았다.','2016-02-02 00:00:00'); INSERT INTO ENTRY(title, summary, created) VALUES ('스프링 부트 클라우드 ','클라우드 파운드리를 응용한 스프링 부트를 공부했다.','2016-02-05 00:00:00'); |
http://localhost:8080/journalEntries/1 접속
REST형 API로 전송할경우
curl -i -X POST -H "Content-Type:application/json" -d '{"title":"클라우드 파운드리", "summary":"클라우드 파운드리가 무엇인지 배웠다.", "created":"2018-03-01"}' http://localhost:8080/journalEntries
http://localhost:8080/journalEntries라는 URL을 GET, POST, PUT, PATCH, DELETE 등 다양한 HTTP 메서드로 실행할 수 있다.
검색기능추가
public interface JournalRepository extends JpaRepository<JournalEntry, Long> { List<JournalEntry> findByCreatedAfter(@Param("after") @DateTimeFormat(iso=ISO.DATE) Date date);
List<JournalEntry> findByCreatedBetween(@Param("after") @DateTimeFormat(iso=ISO.DATE) Date after, @Param("before") @DateTimeFormat(iso=ISO.DATE) Date before);
List<JournalEntry> findByTitleContaining(@Param("word") String word);
List<JournalEntry> findBySummaryContaining(@Param("word") String word); } |
@Param 안에 URL의 쿼리 파라미터명을 넣는다.
@DateTimeFormat은 파라미터가 날짜 타입일 때 ISO 날짜 포맷에 따라 yyyy-MM-dd 형태로 변환한다.
_links에 search 링크가 추가되었다.
_links: { self: { href: "http://localhost:8080/journalEntries{?page,size,sort}", templated: true }, profile: { href: "http://localhost:8080/profile/journalEntries" }, search: { href: "http://localhost:8080/journalEntries/search" } }, |
http://localhost:8080/journalEntries/search
접속 해보기
http://localhost:8080/journalEntries/search/findBySummaryContaining?word=%ED%81%B4%EB%9D%BC%EC%9A%B0%EB%93%9C
http://localhost:8080/journalEntries/search/findByCreatedAfter?after=2016-02-01
http://localhost:8080/journalEntries/search/findByCreatedBetween?after=2016-02-01&before=2016-03-01
컨트롤러 작성
web
package com.apress.spring.web; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.ModelAndView; import com.apress.spring.repository.JournalRepository; @RestController public class JournalController { private static final String VIEW_INDEX = "index";
@Autowired JournalRepository repo;
@RequestMapping(value="/", method=RequestMethod.GET) public ModelAndView index(ModelAndView modelAndView) { modelAndView.setViewName(VIEW_INDEX); modelAndView.addObject("journal", repo.findAll()); return modelAndView; }
} |
index.html
<!DOCTYPE html> <html lang="en-US" xmlns="http://www.thymeleaf.org"> <head> <meta charset="utf-8"></meta> <meta http-equiv="Content-Type" content="text/html"></meta> <title>스프링부트 일기</title> <link rel="stylesheet" type="text/css" media="all" href="css/bootstrap.min.css"></link> <link rel="stylesheet" type="text/css" media="all" href="css/bootstrap-glyphicons.css"></link> <link rel="stylesheet" type="text/css" media="all" href="css/styles.css"></link> </head> <body> <div class="container"> <h1>스프링 부트 일기</h1> <ul class="timeline"> <div th:each="entry, status :${journal}"> <li th:attr="class=${status.odd}?'timeline-inverted':''"> <div class="tl-circ"></div> <div class="timeline-panel"> <div class="tl-heading"> <h4><span th:text="${entry.title}">제목</span></h4> <p><small class="text-muted"><i class="glyphicon glyphicon-time"></i> <span th:text="${entry.createdAsShort}">에 작성.</span></small></p> </div> <div class="tl-body"> <p><span th:text="${entry.summary}">요약</span></p> </div> </div> </li> </div> </ul> </div> </body> </html> |
http://localhost:8080/ 접속
http://localhost:8080/jouranlEntries에 접속하면 HAL+JSON 응답을 받는데, REST형 API는 /api로 경로를 분리하는 편이 좋다.
구성이 간편한 스프링 부트는 application.properties에 추가한다.
spring.data.rest.base-path=/api |
http://localhost:8080/api에 접속하면 HAL+JSON 응답이 나온다.
일기 레코드를 추가하려면 http://localhost:8080/api/journalEntries에 POST 전송하면된다.
journalEntries 경로가 너무 긴 경우
JouranlRepository 인터페이스 수정
package com.apress.spring.repository; import java.util.Date; import java.util.List; import javax.transaction.Transactional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.repository.query.Param; import org.springframework.data.rest.core.annotation.RepositoryRestResource; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat.ISO; import com.apress.spring.domain.JournalEntry; @Transactional @RepositoryRestResource(collectionResourceRel="entry", path="journal") public interface JournalRepository extends JpaRepository<JournalEntry, Long> { List<JournalEntry> findByCreatedAfter(@Param("after") @DateTimeFormat(iso=ISO.DATE) Date date);
List<JournalEntry> findByCreatedBetween(@Param("after") @DateTimeFormat(iso=ISO.DATE) Date after, @Param("before") @DateTimeFormat(iso=ISO.DATE) Date before);
List<JournalEntry> findByTitleContaining(@Param("word") String word);
List<JournalEntry> findBySummaryContaining(@Param("word") String word); } |
@Transactional는 REST 호출에 트랜잭션을 걸어 API 동시 호출시 데이터 정합성에 문제가 없도록 보호
@RepositoryRestResource(collectionResourceRel="entry", path="journal")
경로를 journal로 바꾸고 컬렉션 리소스는 복수형 대신 entry로 한다.
http://localhost:8080/api/journal
http://localhost:8080/api/journal/search
save, delete, find, update 같은 메서드를 일일이 구현하지 않고 spring-data-rest는 다재다능한 웹 애플리케이션 솔루션이다.
HAL 브라우저 갖고 놀기
spring-data-rest 최선버전에는 바로 사용가능한 HAL 브라우저가 있다.
<dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-rest-hal-browser</artifactId> </dependency> |
http://localhost:8080/api/browser
REST API를 브라우저를 통해 쉽게 사용할수 있다.
'JAVA > Spring Boot' 카테고리의 다른 글
스프링부트 보안 OAuth2 (0) | 2018.04.12 |
---|---|
스프링 부트 보안 (예제) (0) | 2018.04.07 |
Spring Data JPA (0) | 2018.04.02 |
스프링 부트 데이터 액세스 (0) | 2018.02.12 |
스프링 부트 테스트 (0) | 2018.02.09 |