본문 바로가기

JAVA/Spring Boot

스프링부트 웹 개발 (샘플)

반응형

스프링부트 웹 개발

스프링 프레임워크는 웹 기술을 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


#테이블을 @Entity를 붙인 엔티티 클래스 설정에 따라 자동생성했다가 앱이 종료되면 삭제하라는 뜻

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