템플릿 엔진

  • 템플릿 엔진

    • 지정된 템플릿 양식과 데이터가 합쳐져 HTML 문서를 출력하는 소프트웨어
  • 서버 템플릿 엔진

    • JSP(View의 역할만 하도록 구성할 경우), Freemarker 등
    • 서버에서 구동
    • 서버에서 Java 코드 문자열을 만든 뒤, 이 문자열을 HTML로 변환하여 브라우저로 전달
  • 클라이언트 템플릿 엔진

    • React, Vue, Angular
    • 브라우저에서 작동
    • SPA(Single Page Application)는 브라우저에서 화면을 생성
    • 이런 경우 서버에서 Json 또는 XML 형식의 데이터만 전달하고 클라이언트에서 조립
    • 자바스크립트 프레임워크에서 서버 사이드 렌더링(Server Side Rendering)을 지원
      (자바스크립트 프레임워크의 화면 생성 방식을 서버에서 실행하는 것)

Mustache

  • 머스테치의 장점

    • 문법이 다른 템플릿 엔진보다 심플
    • 로직 코드를 사용할 수 없어 View 의 역할과 서버의 역할을 명확하게 구분
    • Mustache.js와 Mustache.java 2가지가 있어, 하나의 문법으로 클라이언트/서버 템플릿을 모두 사용 가능
  • 그외 템플릿 엔진

    • JSP, Velocity
      • 스프링 부트에서는 권장하지 않는 템플릿 엔진
    • Freemarker
      • 템플릿 엔진으로는 과라게 많은 기능을 지원
      • 높은 자유도로 인하여 숙련도가 낮을 수록 Freemarker안에 비즈니스 로직이 추가될 확률이 높음
    • Thymeleaf
      • HTML 태그에 속성으로 템플릿 기능을 사용하는 방식이 어려울 수 있음
      • Vue.js 태그 속성 방식과 비슷

Mustache 의존성 추가

  • spring-boot-starter-mustache 의존성 추가
    • SpringBoot에서 공식 지원하는 템플릿 엔진
dependencies {
    ...
    compile('org.springframework.boot:spring-boot-starter-mustache')

    ...
} 

Mustache Plugin 설치

  • Mustache 플러그인
    • IntelliJ community 버전에서 지원
    • 문법체크, HTML 문법 지원, 자동완성을 지원

기본 페이지 만들기

  • IndexController
@Controller
public class IndexController {
    @GetMapping
    public String index() {
        return "index";
    }
}
  • header.mustache
<!DOCTYPE HTML>
<html>
    <head>
        <title>스프링부트 웹서비스</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />

        <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
    </head>
    <body>
  • footer.mustache
        <script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
        <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js"></script>

        <!--index.js 추가-->
        <script src="/js/app/index.js"></script>
    </body>
</html>
  • index.mustache
{{>layout/header}}

<h1>스프링 부트로 시작하는 웹 서비스 No. 3</h1>

{{>layout/footer}}

View Resolver

  • Mustache starter로 인한 View Resolver가 하는 일
    • 컨트롤러에서 문자열을 반환할 때 앞의 경로와 뒤의 파일 확장자는 자동으로 지정된다.
    • prefix: src/main/resources/templates
    • suffix: .mustache
    • index를 반환하는 경우 'src/main/resources/templates/index.mustache'로 전환

IndexControlerTest

  • 기본 페이지 테스트 코드
    • restTemplate.getForObject("/")를 호출하는 경우 index.mustache에 포함된 코드 확인
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = RANDOM_PORT)
public class IndexControllerTest {
    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void loadMainPage() {
        // when
        String body  = this.restTemplate.getForObject("/", String.class);

        // then
        assertThat(body).contains("스프링 부트로 시작하는 웹 서비스");
    }
}
  • IndexController 브라우저 확인

index.mustache 확인

'Spring > SpringBoot' 카테고리의 다른 글

[SpringBoot] 게시글 전체 조회  (0) 2020.05.29
[SpringBoot] 게시글 등록  (0) 2020.05.29
[SpringBoot] JPA Auditing  (0) 2020.05.28
[SpringBoot] Posts API 만들기  (2) 2020.05.28
[SpringBoot] 설정파일 yaml로 변경하기  (0) 2020.05.28

JPA Auditing으로 생성시간 / 수정시간 자동화하기

  • Entity에는 해당 데이터의 생성시간과 수정시간을 포함한다.
  • 공통으로 사용하게 되는 필드를 JPA Auditing을 통해 재사용하도록 한다.

LocalDate

  • Java8부터 LocalDate, LocalDateTime를 사용한다.
    • 8버전 이전에는 Date와 Calendar 클래스의 문제점
      • 불변 객체가 아니므로 멀티스레드 환경에서 문제가 발생할 가능성이 높다.
      • Calendar는 월(Month) 값 설계가 잘못되었다.
        • 10월을 나타내는 Calendar.OCTOBER는 숫자 값이 '9'이다.
    • Hibernate 5.2.10버전 이후, 데이터베이스에 제대로 매핑되지 않는 이슈 해결
    • SpringBoot 1.x 버전을 사용하는 경우 Hibernate 5.2.10 버전 이상을 사용하도록 설정 필요
    • SpringBoot 2.x 버전을 사용하는 경우 별다른 설정이 없이 바로 적용가능

BaseTimeEntity

  • BaseTimeEntity 클래스

    • domain 패키지에 생성
    • 모든 Entity의 상위 클래스로 사용하여 createDate, modifiedDate를 자동으로 관리
    1. @MappedSuperclass

      • JPA Entity 클래스들이 BaseTimeEntity를 사용할 경우 필드들(createDate, modifiedDate)도 컬럼으로 인식하도록 한다.
    2. @EntityListeners(AuditingEntityListener.class)

      • BaseTimeEntity 클래스에 Auditing 기능을 포함
    3. @CreatedDate

      • Entity가 생성되어 저장될 때 자동 저장
    4. @LastModifiedDate

      • 조회한 Entity의 값을 변경할 때 시간이 자동 저장
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseTimeEntity {
    @CreatedDate
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime modifiedDate;
}
  • domain 클래스에서 BaseTimeEntity 상속
@Getter
@NoArgsConstructor
@Entity
public class Posts extends BaseTimeEntity {
    ...
}
  • JPA Auditing 어노테이션들을 활성화 할 수 있도록 Application 클래스에 활성화 어노테이션 추가

@EnableJpaAuditing // JPA Auditing 활성화  
@SpringBootApplication  
public class Application {  
    public static void main(String\[\] args) {  
        SpringApplication.run(Application.class, args);  
    }  
}

JPA Auditing 테스트 코드

  • PostsRepositoryTest 클래스
    • LocalDateTime.of()로 날짜 설정
@RunWith(SpringRunner.class)
@SpringBootTest
public class PostsRepositoryTest {

    @Autowired
    PostsRepository postsRepository;

    @After
    public void cleanup() {
        postsRepository.deleteAll();
    }

    // getBoard

    @Test
    public void regBaseTimeEntityTest() {
        // given
        LocalDateTime now = LocalDateTime.of(2020,5,28,19,28,0);
        postsRepository.save(Posts.builder()
                .title("title")
                .content("content")
                .author("author")
                .build());

        // when
        List<Posts> postsList = postsRepository.findAll();

        // then
        Posts posts = postsList.get(0);
        System.out.println(">>> createDate=" + posts.getCreatedDate() + ", modifiedDate=" + posts.getModifiedDate());

        assertThat(posts.getCreatedDate()).isAfter(now);
        assertThat(posts.getModifiedDate()).isAfter(now);
    }
}
  • createDate, modifiedDate 확인으로 정상적으로 JPA Auditing이 적용되었음을 알 수 있다.
    • 앞으로 생성되는 Entity들은 BaseTimeEntity를 상속받아 등록일/수정일을 자동화할 수 있다.

'Spring > SpringBoot' 카테고리의 다른 글

[SpringBoot] 게시글 등록  (0) 2020.05.29
[SpringBoot] Mustache  (0) 2020.05.28
[SpringBoot] Posts API 만들기  (2) 2020.05.28
[SpringBoot] 설정파일 yaml로 변경하기  (0) 2020.05.28
[SpringBoot] Spring Data JPA 설정  (0) 2020.05.28
  • Spring 웹 계층알아보기
  • 각 계층에 작성해야하는 로직 구분하기
  • API 만들기

API 만들기

  • save
  • update
  • get

Spring 웹 계층

  • Web Layer

    • Controller, JSP/Freemarker 등의 View Template영역
    • Filter(@Filter), Interceptor, Controller Advice(@ControllerAdvice) 등 외부 요청과 응답에 대한 전반적인 영역
  • Service Layer

    • @Service에 사용되는 서비스 영역
    • Controller와 Dao 중간 영역
    • @Transactional이 사용되는 영역
  • Repository Layer

    • Database와 같이 데이터 저장소에 접근하는 영역
  • Dtos

    • Dto(Data Transfer Object)는 계층 간에 데이터 교환을 위한 객체이며 Dtos는 해당 객체들의 영역을 이야기함
    • View Template Engine에서 사용될 객체나 Repository Layer에서 결과로 넘겨준 객체 등
  • Domain Model

    • 도메인이라 불리는 개발 대상을 모든 사람이 동일한 관점에서 이해할 수 있고, 공유할 수 있도록 단순화시킨 것
    • @Entity가 사용되는 영역
    • 데이터베이스의 테이블과 관계되는 것 뿐만아니라 VO처럼 값 객체들도 이 영역에 해당
    • 비즈니스 처리를 담당하는 영역

Save API 만들기

  1. Controller와 Service에서 사용할 Dto 클래스 생성
  2. Controller -> Service 순으로 작성
  3. save기능 테스트 코드 작성

web/dto/PostsSaveRequestDto

  • Dtos

    • Request 데이터를 받은 Dto
    • 계층 간에 데이터 교환을 위한 객체
  • PostsSaveRequestDto

    • Entity 클래스를 기준으로 테이블이 생성되고, 스키마가 변경되므로 Requst/Response 클래스로 사용해선 안된다.
    • Entity 클래스와 Controller에서 쓸 Dto 클래스는 분리되어 사용되야 한다.
@Getter
@NoArgsConstructor
public class PostsSaveRequestDto {
    private String title;
    private String content;
    private String author;

    @Builder
    public PostsSaveRequestDto(String title, String content, String author) {
        this.title = title;
        this.content = content;
        this.author = author;
    }

    public Posts toEntity() {
        return Posts.builder()
                .title(title)
                .content(content)
                .author(author)
                .build();
    }
}

web/PostsAPIController

  • Controller Layer (web)

    • 외부 요청과 응답에 대한 전반적인 로직
  • PostsAPIController

    1. @RequiredArgsConstructor
      • final이 선언된 모든 필드를 인자값으로 하는 생성자를 대신 생성
      • 생성자로 Bean을 주입받아 해당 클래스의 의존성 관계가 변경될 때마다 생성자 코드를 계속해서 수정하는 번거로움을 해결하기 위함
      • 해당 컨트롤러에 새로운 서비스를 추가하거나, 기존 컴포넌트를 제거하는 등의 상황이 발생해도 생성자 코드를 수정하지 않아도 된다.
@RequiredArgsConstructor
@RestController
public class PostsAPIController {

    private final PostsService postsService;

    @PostMapping("/api/v1/posts")
    public Long save(@RequestBody PostsSaveRequestDto requestDto) {
        return postsService.save(requestDto);
    }
}

service/PostsService

  • Service Layer
    • 트랜잭션
    • 도메인 간 순서 보장 (@Transactional)
@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostsRepository postsRepository;

    @Transactional
    public Long save(PostsSaveRequestDto requestDto) {
        return postsRepository.save(requestDto.toEntity()).getId();
    }
}

PostsControllerTest

  • Posts.save 테스트
    • @WebMvcTest 대신 @SpringBootTest와 TestRestTemplate을 사용
    • @WebMvcTest는 JPA 기능이 작동하지 않는다.
    • @SpringBootTest와 TestRestTemplat을 통해 Controller와 ControllerAdvice 등 외부 연동과 관련된 부분만 사용
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsAPIControllerTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private PostsRepository postsRepository;

    @After
    public void tearDown() throws Exception {
        postsRepository.deleteAll();
    }

    @Test
    public void testSave() {
        // given
        String title = "title";
        String content = "content";

        PostsSaveRequestDto requestDto =
                PostsSaveRequestDto.builder()
                .title(title)
                .content(content)
                .author("author")
                .build();

        String url = "http://localhost:" + port + "/api/v1/posts";

        // when
        ResponseEntity<Long> responseEntity = restTemplate.postForEntity(url, requestDto, Long.class);

        // then
        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(responseEntity.getBody()).isGreaterThan(0L);

        List<Posts> all = postsRepository.findAll();

        assertThat(all.get(0).getTitle()).isEqualTo(title);
        assertThat(all.get(0).getContent()).isEqualTo(content);
    }
}
  • 로그 확인
    • WebEnvironment.RANDOM_PORT를 통해 tomcat의 포트가 3421로 구동된 것을 확인
    • insert 쿼리 실행 확인

Update / findById API 만들기

PostsResponseDto

  • PostsReponseDto
    • Entity의 필드 중 일부를 사용하므로 생성자로 Entity를 받아 필드에 대입
    • 모든 필드를 가진 생성자가 필요하지 않으므로 Dto는 Entity를 받아 처리
@Getter
public class PostsResponseDto {
    private Long id;
    private String title;
    private String content;
    private String author;

    public PostsResponseDto(Posts entity) {
        this.id = entity.getId();
        this.title = entity.getTitle();
        this.content = entity.getContent();
        this.author = entity.getAuthor();
    }
}

PostsAPIController

  • PostsAPIController
    • Posts의 id값과 update할 PostsUpdateRequestDto 값을 json 데이터 타입으로 전달하여 호출
    • 해당 Posts의 값을 조회하기 위한 findById 메서드를 정의
@RequiredArgsConstructor
@RestController
public class PostsAPIController {

    private final PostsService postsService;

    // save

    @PutMapping("/api/v1/posts/{id}")
    public Long update(@PathVariable Long id, @RequestBody PostsUpdateRequestDto requestDto) {
        return postsService.update(id, requestDto);
    }

    @GetMapping("/api/v1/posts/{id}")
    public PostsResponseDto findById(@PathVariable Long id) {
        return postsService.findById(id);
    }
}

PostsService

  • PostsService
    • update 기능에서 데이터베이스에 쿼리를 날리는 부분이 없다.
    • JPA의 영속성 컨텍스트, 엔티티를 영구 저장하는 환경
    • JPA의 엔티티 매니저가 활성화된 상태(Spring Data JPA의 기본 옵션)로 트랜잭션안에서 데이터베이스에서 데이터를 가져오는 경우 이 데이터는 영속성 컨텍스트가 유지된 상태이다.
    • 이 상태에서 데이터의 값을 변경하면 트랜잭션이 끝나는 시점에 해당 테이블에 변경분을 반영
    • Entity 객체의 값만 변경하면 별도로 Update 쿼리를 날릴 필요가 없다. (더티 체킹)
@RequiredArgsConstructor
@Service
public class PostsService {
    private final PostsRepository postsRepository;

    ...

    @Transactional
    public Long update(Long id, PostsUpdateRequestDto requestDto) {
        Posts posts = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시물이 존재하지 않습니다. id=" + id));
        posts.update(requestDto.getTitle(), requestDto.getContent());
        return id;
    }

    public PostsResponseDto findById(Long id) {
        Posts entity = postsRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("해당 게시물이 존재하지 않습니다. id=" + id));
        return new PostsResponseDto(entity);
    }
}

PostsAPIControllerTest

  • PostsAPIControllerTest
    • Update 기능을 테스트 하기 위해서 savePosts를 통해 Insert 쿼리 호출
    • Update할 값을 설정하여 PostsUpdateRequestDto를 통해 데이터를 만들기
    • restTemplate.exchange(url.toString(), HttpMethod.PUT, requestEntity, Long.class)로 update 실행
    • assertThat을 통해 정상 호출 확인
    • postsRepository.findAll()를 이용하여 데이터 호출
    • 입력된 값이 기대값과 같은지 확인 후 완료
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class PostsAPIControllerTest {

    @LocalServerPort
    private int port;

    @Autowired
    private TestRestTemplate restTemplate;

    @Autowired
    private PostsRepository postsRepository;

    @After
    public void tearDown() throws Exception {
        postsRepository.deleteAll();
    }

    // save

    @Test
    public void testUpdate() {
        // 변경하기 전 데이터 입력
        Posts savePosts = postsRepository.save(
                Posts.builder()
                        .title("title")
                        .content("content")
                        .author("author")
                        .build()
        );

        Long updateId = savePosts.getId();
        String exceptedTitle = "title2";
        String exceptedContent = "content2";
        // 데이터 변경을 위한 Dto 생성
        PostsUpdateRequestDto requestDto = PostsUpdateRequestDto.builder()
                .title(exceptedTitle)
                .content(exceptedContent)
                .build();

        StringBuilder url = new StringBuilder();

        url.append("http://localhost:");
        url.append(port);
        url.append("/api/v1/posts/");
        url.append(updateId);

        HttpEntity<PostsUpdateRequestDto> requestEntity = new HttpEntity<>(requestDto);

        // when
        ResponseEntity<Long> responseEntity = restTemplate.exchange(url.toString(), HttpMethod.PUT, requestEntity, Long.class);

        // then
        assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(responseEntity.getBody()).isGreaterThan(0L);

        List<Posts> all = postsRepository.findAll();
        assertThat(all.get(0).getTitle()).isEqualTo(exceptedTitle);
        assertThat(all.get(0).getContent()).isEqualTo(exceptedContent);

    }
}

update Test

H2 데이터베이스 웹 콘솔에서 확인하기

insert into posts 
(author, content, title)
values
('author', 'content', 'title');
  • GET: /api/v1/posts/1 URL 호출로 findById 기능 확인

  • PUT: /api/v1/posts/1 URL 호출로 update 기능 확인
    • insomnia라는 프로그램으로 실행하여 테스트

 

 

  • 추가 PostsUpdateRequestDto

'Spring > SpringBoot' 카테고리의 다른 글

[SpringBoot] Mustache  (0) 2020.05.28
[SpringBoot] JPA Auditing  (0) 2020.05.28
[SpringBoot] 설정파일 yaml로 변경하기  (0) 2020.05.28
[SpringBoot] Spring Data JPA 설정  (0) 2020.05.28
[SpringBoot] lombok 설정 및 테스트  (0) 2020.05.28

application.properties를 application.yaml파일로 변경하기

  • spring-boot-starter를 사용하는 경우, SnakeYAML를 사용할 수 있다.
  • SpringApplication 클래스는 SnakeYAML라이브러리를 가지고 있다면 properties를 대체할 수 있도록 YAML을 지원한다.
  • YAML 불러오기
    • SpringFramework는 YAML document를 불러와 사용할 수 있는 클래스를 제공한다.
    • YamlPropertiesFactoryBean은 properties로써 YAML을 불러온다.
    • YamlMapFactoryBean은 YAML을 Map으로 불러온다.

application.properties

  • 서버포트 설정 및 h2 console, jpa sql, query 변경하는 설정

application.properties

application.yaml

  • 위 properties파일과 동일한 설정을 yaml 파일로 설정
# server setting
server:
  port: 8085

# jpa setting
spring:
  h2:
    console:
      enabled: true
  jpa:
    show_sql: true
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect

 

application.yaml

'Spring > SpringBoot' 카테고리의 다른 글

[SpringBoot] JPA Auditing  (0) 2020.05.28
[SpringBoot] Posts API 만들기  (2) 2020.05.28
[SpringBoot] Spring Data JPA 설정  (0) 2020.05.28
[SpringBoot] lombok 설정 및 테스트  (0) 2020.05.28
[SpringBoot] UnitTest 환경 만들기  (0) 2020.05.27

JPA(Java Persistence API)

  • MyBatis(SQL Mapper) vs JPA(Object Relational Mapping)

    • 관계형 데이터베이스와 객체지향 프로그래밍 언어의 패러다임 차이

    • 관계형 데이터베이스는 데이터를 어떻게 저장할지에 초점을 맞춘 기술

      • 데이터베이스 쿼리 작성
      • 객체 모델링보다는 테이블 모델링에만 집중, 객체를 단순히 테이블에 맞추어 데이터 전달 역할만 하는 형태
      • 관계형 데이터베이스가 SQL만 인식 가능하기 때문에 각 테이블마다 기본적인 CRUD SQL를 매번 생성해야하는 상황
    • 객체지향 프로그래밍 언어는 메시지를 기반으로 기능과 속성을 한 곳에서 관리하는 기술

      • 상속, 1:N 등 다양한 객체 모델링을 데이터베이스로는 구현이 불가능
  • JPA

    • 서로 지향하는 바가 다른 2개 영역(객체지향 프로그래밍 언어와 관계형 데이터베이스)을 중간에서 패러다임을 일치를 시켜주기 위한 기술
    • 개발자는 객체지향적으로 프로그래밍을 하고, JPA가 이를 관계형 데이터베이스에 맞게 SQL을 대신 생성해서 실행

Spring Data JPA

  • JPA는 인터페이스로서 자바 표준명세서
    • 인터페이스인 JPA를 사용하기 위해서는 구현체가 필요하다.
      • 대표적으로 Hibernate, Eclipse Link 등
      • 구현체들을 좀 더 쉽게 사용하고자 추상화시킨 Spring Data JPA라는 모듈을 이용하여 JPA 기술을 다룬다.
      • JPA <- Hibernate <- Spring Data JPA
      • Spring 쪽에서는 Spring Data JPA를 개발하고 권장하고 있다.
      • Spring Data JPA가 등장한 이유
      • 구현체 교체의 용이성
        • Hibernate 외에 다른 구현체로 쉽게 교체하기 위함
        • Spring Data Redis를 사용하는 경우, Redis -> Jedis -> Lettuce로 쉽게 가능
      • 저장소 교체의 용이성
        • 관계형 데이터베이스 외에 다른 저장소로 쉽게 교체하기 위함
        • 관계형 데이터베이스에서 MongoDB로 교체가 필요한 경우 Spring Data JPA -> Spring Data MongoDB 의존성 교체로 사용가능
        • Spring Data의 하위 프로젝트들은 기본적인 CRUD의 인터페이스가 같기 때문이다.

Spring Data JPA 적용

  • spring-boot-starter-data-jpa와 com.h2database:h2 의존성 등록
    1. spring-boot-starter-data-jpa
      • 스프링 부트용 Spring Data JPA 추상화 라이브러리
      • 스프링 부트 버전에 맞춰 자동으로 JPA 관련 라이브러리들의 버전을 관리
    2. h2
      • 인메모리 관계형 데이터베이스
      • 별도의 설치가 필요없이 프로젝트 의존성만으로 관리할 수 없다.
      • 메모리에서 실행되기 때문에 어플리케이션을 재시작할 때마다 초기화된다는 점을 이용하여 테스트 용도로 사용

의존성 추가 및 확인

JPA 기능을 테스트 하기위한 기반 코드 작성

  • domain 패키지

    • 소프트웨어에 대한 요구사항 혹은 문제영역
    • xml에 쿼리를 담고, 클래스는 오로지 쿼리의 결과만 담던 일을 모두 도메인 클래스라고 불리는 곳에서 해결
  • Posts

    • 실제 DB의 테이블과 매칭될 클래스로 Entity 클래스라고 한다.
    • DB 데이터에 작업할 경우 실제 쿼리를 날리기보다, Entity 클래스의 수정을 통해 작업한다.
    1. JPA 어노테이션

      • @Entity

        • 테이블과 링크될 클래스

        • 기본값으로 클래스의 CamelCase 이름을 언더스코어 네이밍(_) 으로 테이블 이름을 매칭
          (SalesManager.java -> sales_manager)

        • 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 명확하게 구분할 수 없기 때문에, 차후 기능 변경 시 복잡해 진다.

        • 그래서 Entity 클래스에서는 Setter 메서드를 만들지 않는다.

        • 대신 해당 필드의 값 변경이 필요할 경우 명확히 그 목적과 의도를 나타낼 수 있는 메서드를 추가

        • Setter가 없음에도 값을 채워 DB에 삽입할 수 있는 이유는 생성자 대신 @Builder를 통해 제공되는 빌더 클래스를 사용한다.

        • 생성자와 빌더의 차이

          • 생성자

            • 지금 채워야 할 필드가 무엇인지 명확히 지정할수 없다.

              public Example(String a, String b) {
                this.a = a;
                this.b = b;
              }
              new Example(b, a); // 로 호출하게 되는 경우 문제
          • 빌더

            • 필드별 set메서드를 제공하고 있어 명확히 구분된다.

              Example.builder()
                .a(a)
                .b(b)
                .build();
      • @Id

        • 해당 테이블 PK 필드를 나타낸다.
      • @GeneratedValue

        • PK의 생성 규칙
        • 스프링부트 2.0에서는 GenerationType.IDENTITY 옵션을 추가해야 auto_increment가 된다.
      • @Column

        • 테이블의 컬럼을 나타내며 굳이 선언하지 않더라도 해당 클래스의 필드는 모두 컬럼이 된다.
        • 사용하는 이유는, 기본 값 외에 추가로 변경이 필요한 옵션이 있으면 사용
        • 문자열의 경우 VARCHAR(255)가 기본값인데, 사이즈를 500으로 늘리고 싶은경우, 타입을 TEXT로 변경하고 싶은 경우 사용
    2. Lombok 어노테이션

      • @Getter

        • 클래스 내 모든 필드의 Getter 메서드를 자동생성
      • @NoArgsConstructor

        • 기본 생성자 자동 추가
        • public Posts() {} 와 같은 효과
      • @Builder

        • 해당 클래스의 빌더 패턴 클래스를 생성
        • 생성자 상단에 선언 시 생성자에 포함된 필드만 빌더에 포함
@Getter
@NoArgsConstructor
@Entity
public class Posts {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(length = 500, nullable = false)
    private String title;

    @Column(columnDefinition = "TEXT", nullable = false)
    private String content;

    private String author;

    @Builder
    public Posts(String title, String content, String author) {
        this.title = title;
        this.content = content;
        this.author = author;
    }
}
  • PostsRepository
    • JpaRepository를 상속받은 PostsRepository 인터페이스를 생성
    • MyBatis등에서 Dao라 불리는 DB Layer로 JPA에서는 Repository라 부른다.
    • JpaRepositoy<Entity 클래스, PK 타입> 를 상속하면 기본적인 CRUD 메서드가 자동으로 생성된다.
    • @Repository를 추가할 필요가 없다.
    • 다만, Entity 클래스와 기본 Entity Repository 클래스는 같은 위치(domain 패키지)에 있어야 한다.
    • Entity 클래스는 기본 Repositorty 없이 제대로된 역할을 할 수 없다.
public interface PostsRepository extends JpaRepository<Posts, Long> {
}

Spring Data JPA 테스트 코드 작성

  • PostsRepositoryTest 클래스
    1. @SpringBootTest
      • 해당 어노테이션을 선언하는 경우 H2 데이터베이스를 자동으로 실행
      • 테스트도 H2 데이터베이스를 기반으로 실행된다.
    2. @After
      • Junit에서 단위 테스트가 끝날 때마다 수행되는 메서드를 지정
      • 보통 배포 전 전체 테스트를 수행할 때 테스트간 데이터 침범을 막기 위해서 사용
      • 여러 테스트가 동시에 수행되면 테스트용 데이터베이스인 H2에 데이터가 그대로 남아 있어 다음 테스트 실행 시 테스트 실패할 수 있다.
    3. postsRepository.save
      • 테이블 posts에 insert/update 쿼리를 실행
      • id 값이 있다면 update, id 값이 없다면 insert 쿼리가 실행된다.
    4. postsRepository.findAll
      • 테이블 posts에 있는 모든 데이터를 조회해오는 메서드
@RunWith(SpringRunner.class)
@SpringBootTest
public class PostsRepositoryTest {

    @Autowired
    PostsRepository postsRepository;

    @After
    public void cleanup() {
        postsRepository.deleteAll();
    }

    @Test
    public void getBoard() {
        // given
        String title = "테스트 게시글";
        String content = "테스트 본문";

        postsRepository.save(
                Posts.builder()
                .title(title)
                .content(content)
                .author("seok@gmail.com")
                .build()
        );

        // when
        List<Posts> postsList = postsRepository.findAll();

        // then
        Posts posts = postsList.get(0);
        Assertions.assertThat(posts.getTitle()).isEqualTo(title);
        Assertions.assertThat(posts.getContent()).isEqualTo(content);
    }
}

쿼리로그 확인 방법

  • src/main/resources 경로의 application.properties 파일에 설정 추가
# jpa setting
spring.jpa.show_sql=true

spring.jpa.show_sql=true 설정

  • create table 쿼리에 id bigint generated by default as identity라는 옵션으로 생성
    • 이는 H2의 쿼리 문법이 적용되었기 때문이다.
    • H2 쿼리 문법에서 MySQL 버전으로 변경
# jpa setting
spring.jpa.show_sql=true
# change QueryLog format from h2 to mysql
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

  • id bigint not null auto_increment로 변경됨을 확인

Lombok 설정 및 테스트

  • VO(DTO) 생성 시 Getter, Setter, Contructor, toString 등을 어노테이션으 자동 생성

build.gradle dependency 추가

  • lombok 의존성 추가
dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    compile("org.projectlombok:lombok")
    testCompile('org.springframework.boot:spring-boot-starter-test')
}    

lombok plugin 설치

  • lombok plugin 설치

lombok 설정

  • lombok 플러그인은 한 번만 설치
  • build.gradle에 라이브러리 추가와 Enable annotation.processing체크는 프로젝트마다 설정해야 한다.

lombok으로 Refactoring

  • web 패키지에 dto 패키지를 추가

com.seok.sample.web.dto.HelloResponseDto 추가

  • dto 패키지에 HelloResponseDto 추가

    1. @Getter

      • 선언된 모든 필드의 get 메서드를 생성
    2. @RequiredArgsContructor

      • 선언된 모든 final 필드가 포함된 생성자를 생성

      • final이 없는 필드는 생성자에 포함되지 않는다.

@Getter
@RequiredArgsConstructor
public class HelloResponseDto {
    private final String name;
    private final int amount;
}

lombok 테스트

  • 테스트 코드

    1. assertThat
      • assertj라는 테스트 검증 라이브러리의 검증 메서드
      • 검증하고 싶은 대상을 메서드 인자로 받는다.
      • 메서드 체이닝이 지원되어 isEqualTo와 같이 메서드를 이어서 사용할 수 있다.
    2. isEqualTo
      • assertj의 동등 비교 메서드
      • assertThat에 있는 값과 isEqualTo의 값을 비교해서 같은 경우 성공
  • assetj vs Junit

    • CoreMatchers와 달리 추가적으로 라이브러리가 필요하지 않다.
      • Junit의 assertThat을 쓰게 되면 is()와 같이 CoreMatchers 라이브러리가 필요하다.
    • 자동완성이 좀 더 확실하게 지원
      • IDE에서는 CoreMatchers와 같은 Matcher 라이브러리의 자동완성 지원이 약하다.
  • HelloResponseDtoTest

    • given, when, then의 순서로 테스트 코드를 작성
    1. Given
      • HelloResponseDto 클래스에 생성자로 주입될 값을 설정
      • 테스트 기반 환경을 구축하는 단계
    2. When
      • HelloResponseDto 클래스 생성 및 초기화
      • 테스트 하고자 하는 행위 선언
    3. Then
      • 테스트의 결과를 검증
      • assertThat 메서드를 통해 HelloResponseDto instance의 name, amount의 값을 확인
      • assertThat 성공으로 lombok의 @Getter를 통해 get 메서드, @RequiredArgsContructor로 생성자가 자동으로 생성
import org.assertj.core.api.Assertions;
import org.junit.Test;

public class HelloResponseDtoTest {
    @Test
    public void lombok_test() {
        // Given
        String name = "test";
        int amount = 1000;
        // when
        HelloResponseDto dto = new HelloResponseDto(name, amount);
        // then
        Assertions.assertThat(dto.getName()).isEqualTo(name);
        Assertions.assertThat(dto.getAmount()).isEqualTo(amount);
    }
}

HelloController에 ResponseDto 사용

  • HelloController
    • @RequestParam
      • 외부에서 API로 넘긴 파라미터를 가져오는 어노테이션
      • name (@RequestParam("name"))이란 이름으로 넘긴 파라미터를 메서드 파라미터 name(String name)에 저장
@RestController
public class HelloController {

    ...

    @GetMapping("/hello/dto")
    public HelloResponseDto helloDto(
            @RequestParam("name") String name,
            @RequestParam("amount") int amount) {
        return new HelloResponseDto(name, amount);
    }

}

HelloController 테스트

  • HelloControllerTest
    1. param
      • API 테스트 할 때 요청 파라미터를 설정
      • 단, 그 값은 String만 허용
      • 숫자/날짜 등의 데이터도 등록하는 경우 문자열로 변경이 필요 (String.valueOf(value))
    2. jsonPath
      • JSON 응답값을 필드별로 검증할 수 있는 메서드
      • $를 기준으로 필드명을 명시
      • 여기서는 name, amount를 검증하니 $.name, $.amount로 검증
@RunWith(SpringRunner.class)
@WebMvcTest
public class HelloControllerTest {

    @Autowired
    private MockMvc mvc;

    ...

    @Test
    public void return_helloDto() throws Exception {
        String name = "hello";
        int amount = 1000;

        mvc.perform(
                get("/hello/dto")
                .param("name", name)
                .param("amount", String.valueOf(amount))
        )
        .andDo(print())
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.name", is(name)))
        .andExpect(jsonPath("$.amount", is(amount)));
    }
}
  • 결과 테스트
    • MockHttpServletResponse.Body
    MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [Content-Type:"application/json;charset=UTF-8"]
     Content type = application/json;charset=UTF-8
             Body = {"name":"hello","amount":1000}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

단위테스트

  • 단위테스트 vs TDD

    • TDD: 테스트가 주도하는 개발 (레드 그린 사이클)

      1. 항상 실패하는 테스트 먼저 작성 (Red)
      2. 테스트가 통과하는 프로덕션 코드 작성 (Green)
      3. 테스트가 통과하면 프로덕션 코드를 리펙토링 (Refactor)
    • 단위테스트: 기능 단위의 테스트 코드를 작성하는 것 (순수하게 테스트 코드를 작성하는 것)

  • 단위 테스트의 장점

    • 단위 테스트는 개발 단계 초기에 문제를 발견하게 도와준다.
    • 단위 테스트는 개발자가 나중에 코드를 리펙토링하거나 라이브러리 업그레이드 등에서 기존 기능이 올바르게 작동하는지 확인할 수 있다.
    • 단위 테스트는 기능에 대한 불확실성을 감소시킬 수 있다.
    • 단위 테스트는 시스템에 대한 실제 문서를 제공한다.
      (즉, 단위 테스트 자체가 문서로 사용할 수 있다.)

스프링 부트 실행 Application 클래스 작성

  • @SpringBootApplication

    • 프로젝트 최상단에 위치하는 클래스
    • 스프링 부트는 @SpringBootApplication이 있는 위치부터 설정을 읽어간다.
    • 스프링 부트의 자동설정, 스프링 Bean 읽기와 생성을 자동으로 설정
  • SpringApplication.run

    • 내장 WAS (Web Application Server, 웹 어플리케이션 서버) 실행
    • 스프링 부트로 만들어진 Jar 파일(실행 사능한 Java 패키징 파일)로 실행 가능
  • 내장 WAS를 사용하는 이유

    • 항상 서버에 톰캣을 설치할 필요가 없다.
    • 언제 어디서나 같은 환경에서 스프링 부트를 배포할 수 있다.
    • 외부 WAS를 쓸 경우 모든 서버는 WAS의 종류와 버전, 설정을 일치시켜야 한다.
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

컨트롤러 관련 클래스를 저장할 패키지 생성

  • 패키지 및 컨트롤러 생성
    • web
    • HelloController
      • @RestController
        • 컨트롤러를 JSON 데이터 타입을 반환하는 컨트롤러로 설정
        • 기존 Spring Framework의 경우 @ResponseBody를 각 메서드에 선언
      • @GetMapping
        • Http Method인 Get의 요청을 받을 수 있는 API 설정
        • 기존 Spring Framework의 경우 @RequestMapping(method= RequestMethod.GET)으로 사용
        • "/hello"로 요청이 오는 경우 문자열 hello을 반환
@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

Test API 작성

  • HelloController를 테스트 할 HelloControllerTest 클래스 작성
    1. @RunWith(SpringRunner.class)
      • 테스트를 진행할 때 JUnit에 내장된 실행자 외에 다른 실행자를 실행
      • SpringRunner는 스프링 실행자
      • 즉, 스프링 부트 테스트와 JUnit 사이에 연결자 역할을 한다.
    2. @WebMvcTest
      • 여러 스프링 테스트 어노테이션 중, Web(Spring MVC)에 집중할 수 있는 어노테이션
      • @Controller, @ControllerAdvice 등을 사용할 수 있다.
      • @Service, @Component, @Repository 등은 사용할 수 없다.
    3. @Autowired
      • 스프링이 관리하는 빈(Bean)을 주입
    4. MockMvc mvc
      • 웹 API 테스트 시 사용
      • 스프링 MVC 테스트의 시작점
      • HTTP GET, POST 등에 대한 API를 테스트 할 수 있다.
    5. mvc.perform(get("/hello"))
      • MockMvc를 통해 /hello 주소로 Http GET 요청 가능
      • 체이닝을 지원하여 여러 검증 기능을 이어서 선언할 수 있다.
    6. .andExcept(status().isOk())
      • mvc.perform의 결과를 검증
      • HTTP Header의 Status를 검증
    7. .andExcept(content().string(hello))
      • mvc.perform의 결과를 검증
      • 응답 본문의 내용을 검증
    8. .andDo(print())
      • MockHttpServletRequest
      • Handler
      • Async
      • Resolved Exception
      • ModelAndView
      • FlashMap
      • MockHttpServletResponse 등의 내용의 결과 값을 확인 할 수 있다.
@RunWith(SpringRunner.class)
@WebMvcTest
public class HelloControllerTest {

    @Autowired
    private MockMvc mvc ;

    @Test
    public void return_hello() throws Exception {
        String hello = "hello";

        mvc.perform(get("/hello"))
                .andDo(print())
                .andExpect(status().isOk())
                .andExpect(content().string(hello));
    }
}

테스트 코드 검증

  • 테스트 코드 결과 값 확인

수동 실행으로 검증 (이슈)

  • 오라클 설치로 인한 포트 중복 이슈

  • SpringBoot 실행 시 Apache Tomcat의 기본 포트는 8080이기 때문에 포트를 변경하여 실행

application.properties 파일 생성

  • application.properties 파일 내에 apache tomcat port 수정을 위한 설정
# server setting
server.port=8085

port 중복으로 인한 이슈 처리완료 및 /hello URL에 접근하여 "hello" 확인

SpringBoot 실행 로그 확인
URL 접근하여 페이지 확인

IntelliJ로 SpringBoot프로젝트 생성하기

  1. Gradle Java 프로젝트 생성
  2. Gradle 버전 정보 수정하기
  3. Gradle 프로젝트를 SpringBoot 프로젝트로 Convert하기

Gradle Java 프로젝트 생성

  • 프로젝트 유형선택
    • Gradle 선택

프로젝트 유형 선택 (Gradle)

  • 프로젝트 location 선택 및 GroupId, ArtifactId 작성
    • 프로젝트 명과 ArtifactId는 같은 값으로 설정되어야 함

디렉토리 위치 선택 및 프로젝트명 작성

  • 프로젝트 생성
    • Gradle을 통해 build

프로젝트 생성 및 빌드

Gradle 버전 변경

  • 2020.05.27 일자로 프로젝트 생성 시 Gradle 6.1 버전이 설치되는데 이를 프로젝트의 원할한 진행을 위해 4.8로 downgrade 하도록 한다.
    • 이후에 lombok 사용 시 오류가 발생 할 수 있다.

Gradle 버전 확인

  • Gradle 변경 명령어

$ gradlew wrapper --gradle-version 4.10.2

Gradle 버전 확인

  • Gradle 버전 downgrade 후 확인

Gradle 프로젝트를 SpringBoot 프로젝트로 변경하기

  • build.gradle 파일 확인

SpringBoot으로 변경하기 위해 설정하기

  • SpringBootVersion 정보 설정하기 (ext)
    • ext 키워드를 통해 전역변수 설정
    ext {
        springBootVersion = '2.1.9.RELEASE'
    }
  • 프로젝트 개발에 필요한 의존성들을 선언하기 (dependencies)
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        testCompile('org.springframework.boot:spring-boot-starter-test')
    }
  • 플로그인 의존성 적용을 위한 필수 플로그인 설정
    • io.spring.dependency-management 플러그인은 스프링 부트의 의존성들을 관리해주는 플러그인으로 중요
apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
  • build.gradle 설정 완료
buildscript {
    ext {
        springBootVersion = '2.1.9.RELEASE'
    }
    repositories {
        mavenCentral()
        jcenter()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'

group 'com.seok'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
    jcenter()
}

dependencies {
    compile('org.springframework.boot:spring-boot-starter-web')
    testCompile('org.springframework.boot:spring-boot-starter-test')
}

Gradle 설정 확인

  • 우측의 Gradle 모듈 탭을 눌러 Dependencies를 확인

jcenter vs mavenCentral

  • repositories는 각종 의존성(라이브러리)들을 어떤 원격 저장소에서 받을지 정하게 된다.
  • 이 저장소의 역할을 mavenCentral과 jcenter가 한다.
  • mavenCentral은 본인이 만든 라이브러리를 업로드하기 위해서 많은 과정과 설정이 필요하다.
  • jcenter는 이런 문제점을 개선하여 라이브러리 업로드를 간단하게 한다.
  • jcenter에 라이브러리를 업로드하면 mavenCentral에도 업로드될 수 있도록 자동화 할 수 있다.

+ Recent posts