요구사항 분석   

 

 진행할 웹 애플리케이션의 요구사항은 다음과 같다. 

 

 


 

    프로젝트에 Spring Data JPA 적용하기   

 

 먼저, build.gradle에 의존성을 추가로 등록해야 한다. 

 

dependencies {
    //compile('org.springframework.boot:spring-boot-starter-web')
    //compile('org.projectlombok:lombok')
    compile('org.springframework.boot:spring-boot-starter-data-jpa')
    compile('com.h2database:h2')
    //testCompile('org.springframework.boot:spring-boot-starter-test')
}

 

 

1. spring-boot-start-data-jpa

  • 스프링부트용 Spring Data JPA 추상화 라이브러리이며 자동으로 라이브러리들의 버전을 관리해준다. 

2. h2

  • 인메모리 관계형 데이터베이스로 별도의 설치 없이 프로젝트 의존성으로만 관리할 수 있다.
  • 메모리에서 실행되기 때문에 애플리케이션을 재시작할 때마다 초기화된다. 보통 테스트 용도로 많이 사용한다.

 

 

 build.gradle에 의존성을 추가했다면 domain 패키지를 하나 추가한다. 이 패키지는 게시글, 댓글, 회원 등 각각의 요구사항인 도메인을 담을 패키지이다. 그 후에 posts 패키지와 Posts 클래스를 만든다. 그리고 다음 코드를 추가한다.

 

 

package com.daldalhada.springboot.domain.posts;

import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@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;
    }
}

 

 Posts 클래스는 실제 DB의 테이블과 매칭될 클래스이며 Entity 클래스라고도 한다. RDB와 달리, JPA에서는 쿼리를 날리기보다는 Entity 클래스의 수정을 통해 이루어진다. 

 

 

1. @Entity

  • 테이블과 매칭될 클래스이며 기본값으로 클래스의 카멜케이스 이름을 언더스코어()_로 테이블 이름을 매칭한다. 
  • UsersManager.java ▶ Users_manager table

2. @Id

  • 해당 테이블의 기본키(PK) 필드를 나타낸다.
  • 웬만하면 Long(RDB에서 BigInt) 타입의 auto_increment를 추천한다.

3. @GeneratedValue

  • 기본키의 생성 규칙을 나타낸다.
  • [참고] 스프링부트 2.0에서는 GenerationType.IDENTITY 옵션을 추가해야 auto_increment 옵션을 사용할 수 있다.

4. @Column

  • 테이블의 칼럼을 나타낸다.(굳이 선언하지 않더라도 해당 클래스의 필드는 모두 칼럼이 된다.)
  • 굳이 사용하는 이유는 기본값 외에 추가로 변경이 필요한 옵션이 있으면 사용한다.
  • 예) 문자열의 경우 VARCHAR(255)가 디폴트값이지만 사이즈를 500으로 늘리거나(Length=500), 타입을 변경하고 싶을 때(columnDefinition = "TEXT")

 

5. @NoArgsConstructor

  • 기본 생성자를 자동으로 추가해준다.(public Posts() { } 와 같은 효과)

6. @Getter

  • 클래스 내 모든 필드의 Getter 메소드를 자동으로 생성해준다.

7. @Builder

  • 클래스의 빌더 패턴 클래스를 생성해준다.
  • 생성자 상단에 선언 시 생성자에 포함된 필드만 빌더에 포함된다.

 

 서비스 초기 구축 단계에선 테이블 설계(여기서는 Entity)가 빈번하게 변경되는데 5~7번에 해당하는 롬복 어노테이션은 코드 변경량을 최소화시켜 주기에 적극적으로 사용하는 것이 좋다.

 

 


 

    데이터베이스에 값을 추가하는 방법   

 

 위의 Posts 클래스는 Getter 메소드는 설정했지만 Setter 메소드가 없다. Entity 클래스에서는 절대로 Setter 메소드를 만들지 않아야 하는 이유가 있는데, 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 명확하게 구분할 수 없기 때문이다. 따라서, 해당 필드 값 변경이 필요하면 그 목적과 의도를 나타낼 수 있는 메소드를 추가해야만 한다. 

 

 주문 취소 메소드를 예로 들면

 

// 잘못된 사용 예

public class Order{
	public void setStatus(boolean status){
    		this.status = status;
    }
}

public void cancel(){
	order.setstatus(false);
}
// 올바른 사용 예

public class Order{
	public void orderCancel(){
    		this.status = false;
    }
}

public void cancel(){
	order.orderCalncel();
}

 

 

 그러면 Setter가 없는 상황에서 값을 데이터베이스에 어떻게 삽입(Insert)해야 할까? 기본적인 구조는 생성자를 통해 값을 채운 후 DB에 삽입하고 값 변경이 필요하면 정의한 public 메소드를 호출하여 변경한다. 

 

 여기서는 생성자 대신에 @Builder를 통해 제공되는 빌더 클래스를 이용한다. 생성자나 빌더나 생성 시점에 값을 채워주는 역할은 똑같지만 생성자의 경우에는 지금 채워야 할 필드가 무엇인지 명확히 지정할 수 없다. 

 

 예를 들어 다음과 같은 생성자가 있다면 개발자가 new User(c, b, a)처럼 매개 변수의 위치를 변경해도 코드를 실행하기 전까지 문제를 찾을 수 없다. 즉, a에 c값이, c에 a값이 들어간다.

 

public User(String a, String b, String c){
    this.a = a;
    this.b = b
    this.c = c;
}

 

 하지만, 빌더를 사용하면 어느 필드에 어떤 값을 채워야 할지 명확하게 인지할 수 있다. 

 

User.builder()
    .a(a)
    .b(b)
    .c(c)
    .build();

 

 Posts 클래스 생성이 끝났으면 Posts 클래스로 데이터베이스에 접근하게 해줄 JPARepository를 생성한다. 보통 MyBatis 등에서 DAO라고 불리는 DB Layer 접근자이다. JPA에선 Repository라고 불리며 인터페이스로 생성한다. 인터페이스 생성 후 JpaRepository<Entity Class, PK Type>를 상속하면 기본적인 CRUD 메소드가 자동으로 생성된다. 

 

 주의할점은 Entity 클래스와 Repository는 같은 폴더 위치에 있어야 한다는 점이다. 다른 위치에 있다면 제대로 수행할 수가 없다. 

 

 

package com.daldalhada.springboot.domain.posts;

import org.springframework.data.jpa.repository.JpaRepository;

public interface PostRepository extends JpaRepository<Posts, Long> {

}

 


    테스트 코드 작성하기   

 

 위의 작성된 코드들을 테스트 코드로 기능을 검증해봐야 하므로 test 디렉토리에 domain.posts 패키지를 생성하고, 테스트 클래스는 PostsRepositoryTest란 이름으로 생성하고 다음과 같은 코드를 추가한다. 

 

package com.daldalhada.springboot.domain.posts;

import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringRunner.class)
@SpringBootTest
public class PostsRepositoryTest {
    @Autowired
    PostsRepository postsRepository;

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

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

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

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

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

 

1. @After

  • JUnit에서 단위 테스트가 끝날 때마다 수행되는 메소드를 지정한다. 
  • 보통 배포 전 전체 테스트를 수행할 때 테스트 간 데이터 침범을 막기 위해 사용한다. (여러 테스트가 동시에 실행되면 데이터베이스에 데이터가 그대로 남아 있을 수 있어 다음 테스트가 실패할 수 있으므로 보통 delete 해준다.)

2. postRepository.save

  • 위에서 인터페이스에 CRUD 메소드가 저절로 생성되었다고 하였는데, save는 테이블에 insert/update 쿼리를 실행한다.
  • id가 있으면 update가 id가 없으면 insert 쿼리가 실행된다.

3. postRepository.findAll

  • 테이블에 있는 모든 데이터를 조회해오는 메소드이다. 

4. @SpringBootTest

  • 별다른 설정 없이 H2 데이터베이스를 자동으로 샐행해준다.

 

 

 [참고] 흔히 할 수 있는 실수로 인터페이스명의 이름이 다른 경우 인식을 하지 못하므로 조심해야 한다. 필자는 Posts에서 's'가 없었다.

 

 

 

 테스트 코드를 실행해보면 테스트가 통과되는 것을 확인할 수 있다.

 

 

 

 그런데, 실제로 실행된 쿼리는 어떤 형태일지 궁금할 수 있다. 실행된 쿼리를 로그로 확인할 수 있는데 Java 클래스로 구현하거나 appication.properties 파일의 코드 수정으로 가능하다. 저번에 생성했던 resources 폴더의 application.properties로 들어가 다음 코드를 추가해준다. 

 

spring.jpa.show_sql=true

 

 

 다시 테스트를 수행해보면 콘솔에서 쿼리 로그를 확인할 수 있다. 

 

 

 

 만약 출력되는 쿼리 로그를 MySQL 버전으로 변경해보고 싶다면 appication.properties에서 다음 코드를 추가해준다. 

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect

 

 

 잘 적용되었음을 확인할 수 있다.

'스프링 부트 프로젝트 > 데이터베이스 다루기' 카테고리의 다른 글

7. API 만들기  (0) 2020.11.19
5. JPA  (0) 2020.10.07

 

    스프링부트에서 데이터베이스 다루기   

 

 스프링부트에서 데이터베이스를 다루는 방법에는 MyBatis와 같은 SQL 매퍼(SQL Mapper)를 이용해서 데이터베이스 쿼리를 작성하는 방법과 JPA와 같은 자바 표준 ORM(Object Relational Mapping)을 이용해서 객체지향적으로 프로그래밍 하는 방법이 있다. ORM은 객체를 매핑하는 것이고, SQL 매퍼는 쿼리를 매핑하는 것이다. 

 


 

    관계형 데이터베이스와 JPA   

 

 웹 애플리케이션을 개발하다보면 Oracle, MySQL과 같은 관계형 데이터베이스(Relational Database, RDB)를 많이 사용한다. 그러다 보니 객체를 관계형 데이터베이스에서 관리하는 것이 중요하다.

 

 관계형 데이터베이스가 웹 서비스의 중심이 되면 모든 코드는 SQL 중심으로 돌아간다. 즉, 애플리케이션 코드보다 SQL 코드가 더 많아진다는 것이다. 이는 관계형 데이터베이스가 SQL을 통해서만 인식하기에 각 테이블마다 기본적인 CRUD(Create, Read, Update, Delete) SQL을 매번 생성해야 한다. 핵심은 관계형 데이터베이스를 사용해야만 하는 상황에서는 매번 생성해야 하는 SQL을 피할 수 없다는 것이다. 

 

 이러한 SQL은 두 가지 문제점이 있는데 첫 번째는 위에서 설명한 CRUD SQL의 반복적인 작업을 해야하는 것이고 두 번째는 패러다임 불일치 문제이다. 패러다임 불일치 문제는 관계형 데이터베이스와 객체지향 프로그래밍 언어의 패러다임이 서로 다르다는 점에서 발생한다. 

 

 관계형 데이터베이스는 어떻게 데이터를 저장할지에 초점을 맞추는데 반해 객체지향 프로그래밍 언어는 기능과 속성을 한 곳에서 관리한다. 

 

 아래 코드는 객체지향 프로그래밍에서 부모가 되는 객체를 가져오는 방법이다. 

 

User user = findUser();
Group group = user.getGroup();

 

 여기서는 User와 Group의 관계가 부모-자식 관계임을 알 수 있다. group은 User가 속한 Group을 가져온 코드라고 알 수 있기 때문이다. 하지만, 여기서 관계형 데이터베이스가 추가된다면 다음과 같다.

 

User user = userDao.findUser();
Group group = groupDao.findGroup(user.getGroupId());

 

 여기서는 User와 Group을 따로 조회하기 때문에 User와 Group이 상속, 1:N 등 어떤 관계인지 알 수가 없다. 따라서, 다양한 객체 모델링을 데이터베이스로는 구현할 수 없다는 것이다. 

 

 JPA는 이러한 문제점을 해결하기 위해 등장했는데, 서로 다른 패러다임을 일치시켜주는 역할을 한다. 즉, 객체지향적으로 프로그래밍을 하면서 관계형 데이터베이스에 맞게 SQL을 대신 생성해서 실행해주는 역할을 한다. 객체 중심으로 개발을 하게 되면 생산성이 향상되고 유지 보수하기가 쉬워진다. 

 


 

    Spring Data JPA   

 

  JPA(Java Persistence API)는 인터페이스이면서 자바 표준 명세서이다. JPA를 사용하기 위해서는 Hibernate, Eclipse Link와 같은 구현체가 필요하다. 하지만 Spring에서는 JPA를 사용할 때 이 구현체를 직접 다루진 않는다. 

 

 구현체들을 좀 더 쉽게 사용하고자 추상시Spring Data JPA라는 모듈을 이용하여 다룬다. 이들의 관계는 다음과 같다.

 

 

 

  • 구현체 교체의 용이: Hibernate 외에 다른 구현체로 쉽게 교체하기 위함. 트렌드가 바뀌어 새로운 JPA 구현체가 떠오를 때, Spring Data JPA를 쓴다면 아주 쉽게 교체할 수 있다. 이는 Spring Data JPA 내부에서 구현체 매핑을 지원해주기 때문이다.
  • 저장소 교체의 용이: 관계형 데이터베이스 외에 다른 저장소로 쉽게 교체하기 위함. 초기에는 트래픽이 적어 모든 기능을 관계형 데이터베이스로 처리했지만 트래픽이 점점 많아져 감당이 안될 때 NoSQL로 교체가 필요하다. Spring Data JPA를 사용하면 의존성 교체만 해주면 된다. 이는 Spring Data의 하위 프로젝트들은 기본적인 CRUD의 인터페이스가 같기 때문에 가능하다. Spring Data JPA, Spring Data MongoDB, Spring Data Redis 등 하위 프로젝트들은 save(), findAll() 등을 인터페이스로 갖고 있다. 

 

  이러한 용이성들 때문에 Spring 팀에서도 Spring Data 프로젝트를 권장하고 있다. 하지만, 실무에서 JPA를 잘 사용하지 않은 이유는 높은 러닝 커브를 들 수 있다. 객체지향 프로그래밍과 데이터베이스를 둘 다 이해해야 하기 때문이다. 하지만 그만큼 JAP를 사용해서 얻는 보상이 크기 때문에 많이 바뀌고 있는 추세이다. 

    롬복(Lombok)이란?   

 

 롬복은 자바로 개발할 때 자주 사용되는 코드인 Getter, Setter, 기본 생성자, toString 등을 어노테이션으로 자동 생성해준다. 참고로 이클립스의 경우에는 롬복 설치가 번거롭지만, 인텔리제이에서는 플러그인 덕분에 쉽게 설정이 가능하다. 

 


 

    롬복(Lombok) 플러그인 설치하기   

 

 롬복 플러그인을 설치하기 전에 build.gradle에 다음의 코드를 추가해야 한다.

 

 

 

 build.gradle에 코드를 추가했다면 맨 오른쪽에 Gradle 버튼을 누르고 아래 빨간색으로 표시된 새로고침 버튼을 눌러주어 라이브러리(의존성)를 내려 받는다.

 

 

 라이브러리를 다 받았다면 롬복 플러그인을 설치해야 한다. 윈도우는 [ctrl + shift + A], 맥은 [command + shift + A]를 눌러  Action 탭을 선택한다. 그리고 plugins를 검색하고 선택한다. 그 후 Marketplace 탭에서 "lombok"을 검색하고 [install] 버튼을 눌러 설치한다. 설치가 완료되면 아래 오른쪽에 팝업창이 뜨면서 Enable annotation processing이 뜬다. 클릭 해주고 인텔리제이를 재시작 해준다.

 

 

 만약 팝업창이 뜨지 않았다면 [File]-[Settings]-[Build, Execution, Deployment]-[Compiler]-[Annotation Processor]로 가서 [Enable annotation processing]에 체크를 해준다.

 

 


 

    기존 코드 롬복으로 리팩토링 하기   

 

 먼저, main 폴더의 web 패키지에 dto 패키지를 추가하고 HelloResponseDto를 생성하고 다음과 같이 코드를 작성한다.

 

 

package com.daldalhada.springboot.web.dto;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

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

 

 

1. @Getter

  • 선언된 모든 필드의 get 메소드를 생성해준다.

2. @RequiredArgsConstructor

  • 선언된 모든 final 필드가 포함된 생성자를 생성해 준다.
  • final이 없는 필드는 생성자에 포함되지 않는다. 

 

 

 그리고 잘 작동하는지 확인하기 위해 간단한 테스트 코드를 작성한다. test 폴더의 web 패키지에 dto 패키지를 추가하고 HelloResponseDtoTest를 생성하고 다음과 같이 코드를 작성한다.

 

 

 

package com.daldalhada.springboot.web.dto;

import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThat;

public class HelloResponseDtoTest {
    @Test
    public void lombok_test() {
        //given
        String name = "test";
        int amount = 1000;

        //when
        HelloResponseDto dto = new HelloResponseDto(name, amount);

        //then
        assertThat(dto.getName()).isEqualTo(name);
        assertThat(dto.getAmount()).isEqualTo(amount);
    }
}

 

1. assertThat

  • assertj라는 테스트 검증 라이브러리의 검증 메소드로 검증하고 싶은 대상을 메소드 인자로 받는다.
  • 메소드 체이닝이 지원되어 isEqualTo와 같은 메소드를 이어서 사용할 수 있다.

2. isEqualTo

  • assertThat에 있는 값과 isEqualTo 값을 비교해서 같을 때만 성공한다.

※참고: 여기서 Junit의 기본 aseertThat이 아닌 assertj의 assertThat을 사용한 이유는 추가적인 라이브러리가 필요하지 않고 자동완성이 좀 더 확실하게 지원되기 때문이다. 

 

작성된 테스트 메소드를 실행해보면 정상적으로 기능이 수행되는 것을 알 수 있다. @Getter로 get 메소드가, @RequiredArgsConstructor로 생성자가 자동으로 생성되었다. 

 

 

 

이제 HelloController에도 새로 만든 ResponseDto를 사용하도록 코드를 추가한다.

 

package com.daldalhada.springboot.web;

import com.daldalhada.springboot.web.dto.HelloResponseDto;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }

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

 

1. @RequestParam

  • 외부에서 API로 넘긴 파라미터를 가져오는 어노테이션이다.
  • 여기서는 name이란 이름으로 넘긴 파라미터를 String name에 저장한다. amount도 마찬가지이다.

 

 이제 HelloController도 테스트를 하기 위해 HelloControllerTest에도 다음과 같이 코드를 추가한다. 

 

 

package com.daldalhada.springboot;

import com.daldalhada.springboot.web.HelloController;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import static org.hamcrest.Matchers.is;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

@RunWith(SpringRunner.class)
@WebMvcTest(controllers = HelloController.class)
public class HelloControllerTest {

    @Autowired
    private MockMvc mvc;

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

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

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

        mvc.perform(
                get("/hello/dto")
                        .param("name", name)
                        .param("amount", String.valueOf(amount)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name", is(name)))
                .andExpect(jsonPath("$.amount", is(amount)));
    }
}

 

1. param

  • API 테스트할 때 사용될 요청 파라미터를 설정한다. (단, 값은 String 값만 허용)
  • 따라서 숫자/날짜 등의 데이터도 문자열로 변경해야 한다.

2. jsonPath

  • JSON의 응답값을 필드별로 검증할 수 있는 메소드
  • $를 기준으로 필드명을 명시한다. 여기서는 name과 amount를 $.name과 $.amount로 검증한다. 

 

 

 이제 추가된 API도 테스트를 실행해보면

 

 

 JSON이 리턴되는 API도 통과하는 것을 볼 수 있다.

    (1)  그레이들(gradle) 버전 확인 및 변경   

 

  그레이들(gradle)의 버전을 확인하기 위해  [gradle]-[wrapper] 폴더의 gradle-wrapper.properties 파일을 찾아가 빨간색으로 표시된 부분을 확인한다. 

 

 

 만약 그레이들의 버전을 변경해야 한다면 터미널 창에서 다음 명령어를 입력해 준다.

 

    gradlew wrapper --gradle-version [버전명]

 


 

    (2) 깃과 깃허브 연동    

 

 먼저 윈도우에서는 ctrl + shift + A, 맥에서는 command + shift + A를 사용해 Action 검색창을 열어 share project on github를 검색한다. 

 

 

 해당 Action을 선택하면 깃허브 로그인 화면이 나온다. 본인의 깃허브 계정을 로그인한다. 

 

 

 로그인에 성공하면 다음과 같은 화면이 뜨는데 Repository name을 수정하고 share 버튼을 누른다.

 

 

 로그인에 성공하면 다음과 같은 화면이 뜨는데 Repository name을 수정하고 share 버튼을 누른다. 그 후에 .idea 폴더를 제외하고 Commit Message를 작성한 뒤 Add 버튼을 누른다. 

 

 

 로그인에 성공하면 다음과 같은 화면이 뜨는데 Repository name을 수정하고 share 버튼을 누른다. 그 후에 .idea 폴더를 제외하고 Commit Message를 작성한 뒤 Add 버튼을 누른다. 

 

 만약 다음과 같은 화면이 나오는데 여기서는 .gitignore이라는 파일을 추가할 것인지에 대한 것이므로 cancel을 눌러주고 위에서 다시 .gitignore 파일을 체크 해제를 한 뒤 커밋을 하면 된다. 

 

 

 여기까지 진행되었으면 github 레포지토리가 생성된 것을 확인할 수 있다.

 

 


 

    (4) .gitignore 세팅    

 

 깃허브와 동기화는 되었으면 이제 처음 커밋했을 때 제외했던 .idea 폴더를 앞으로의 모든 커밋 대상에서 제외되도록 처리를 해야한다. 만약 .gitignore가 생성되지 않았다면 플러그인에서 .gitignore를 지원 받아야 한다. 

 

 플러그인에 대한 정보는 여기서 참조하면 된다.

 

2020/09/16 - [IDE/IntelliJ] - 2. 플러그인(plugin)

 

2. 플러그인(plugin)

플러그인 인텔리제이에서 특정 기능에 대한 지원이 없으면 플러그인(plugin)에서 지원을 받는다. 예를 들어, .gitignore 파일에 대해 지원받고 싶다면 .ignore 플러그인에서 지원을 받으면 된다.  먼저

almotjalal.tistory.com

 

 

 윈도우는 Ctrl + Shift + A, 맥은  Command + Shift + A를 사용해 [Actions]탭 검색창을 열어 plugins를 검색하고 선택한다.

 

 

 

 [Marketplace]탭은 설치 가능한 플러그인 목록을 보여주며, [Installed] 탭은 이미 설치된 플러그인 목록을 나타낸다. [Marketplace]탭을 선택하고 .ignore을 검색하고 [install]을 클릭하여 설치한다. 

 

 

 

 설치가 완료 되었으면 인텔리제이를 다시 시작해야만 설치한 플러그인이 적용되므로 재시작을 한다. 

 

 

 

 재시작을 하면 프로젝트 왼쪽 위의 프로젝트 이름을 선택한 뒤, 마우스 오른쪽 버튼을 누르거나 단축키를 눌러 생성 목록을 연다. 단축키는 윈도우 Alt + Insert, 맥 Command + N이다. 생성 목록 아래에 [.ignore file], [gitignore file(git)]을 차례로 선택해 .gitignore 파일을 생성한다. 

 

 

    (1)  참고 사항   

 

  인텔리제이(IntelliJ)에는 이클립스(Eclipse)의 워크 스페이스와 같은 개념이 없고 프로젝트와 모듈의 개념만 있다. 즉, 모든 프로젝트를 한 번에 불러올 수 없으므로 한 화면에서는 하나의 프로젝트만 열린다. 

 


 

    (2) 프로젝트 생성    

 

 아래 그림에서 빨간 색으로 표시된 New Project를 선택한다. 

 

 

 왼쪽 목록에서 그레이들(Gradle)을 선택해주고 Next 버튼을 누른다. 

 

 

 

 다음은 프로젝트에 해당하는 이름을 적어준다. Name 항목과 ArtifactId 항목은 같은 값으로 입력이 된다. 다 입력 되었으면 Finish 버튼을 누른다. 

 

 


 

    (3) 그레이들(Gradle) 프로젝트를 스프링 부트 프로젝트로 변경    

 

 인텔리제이에서 build.gradle 파일을 열어보면 다음 화면과 같다.

 

 

 코드를 자세히 살펴보면 아직 자바 개발에 가장 기초적인 설정만 되어 있는 상태이다. 이제 여기에 스프링 부트에 필요한 설정들을 하나씩 추가해야 한다.

 

plugins {
    id 'java'
}

group 'com.daldalhada.book'
version '1.0-SNAPSHOT'

repositories {
    mavenCentral()
}

dependencies {
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

 

 

 코드를 자세히 살펴보면 아직 자바 개발에 가장 기초적인 설정만 되어 있는 상태이다. 이제 여기에 스프링 부트에 필요한 설정들을 하나씩 추가해야 한다.

 

 물론 스프링 이니셜라이저(start.spring.io/)를 통해 진행할 수 있지만 bulid.gradle 파일의 코드들이 각각 무슨 역할을 하는지, 이니셜라이저 외에 추가로 의존성 추가가 필요하면 어떻게 해야 하는지에 대해 알아보기 위해 이니셜라이저를 쓰지 않았다.

 

 먼저 build.gradle 파일 맨 위에 위치할 코드는 다음과 같다.

buildscript {
    ext {
        springBootVersion = '2.1.7.RELEASE'
    }
    repositories {
        mavenCentral()
        jcenter()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

 위의 코드는 이 프로젝트의 플러그인 의존성 관리를 위한 설정이다. ext라는 키워드는 build.gradle 파일에서 사용하는 전역변수를 설정하겠다는 의미이다. 즉, 위에 코드에서는 spring-boot-gradle-plugin이라는 스프링 부트 그레이들 플러그인의 2.1.7.RELEASE를 의존성으로 받겠다는 의미이다. 

 

 다음은 앞서 선언한 플러그인 의존성들을 적용할 것인지를 결정하는 코드이다.

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

 io.spring.dependency-management 플러그인은 스프링 부트의 의존성들을 관리해주는 플러그인이라 꼭 추가해야만 한다. 위의 3개를 포함하여 총 4개의 플러그인은 자바와 스프링 부트를 사용하기 위해 필수이므로 항상 추가해야 한다. 

 

repositories {
    mavenCentral()
}

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

 

 repositories는 각종 의존성(라이브러리)들을 어떤 원격 저장소에서 받을지를 정한다. mavenCentral을 많이 사용하지만 최근에는 라이브러리 업로드 난이도 때문에 jcenter도 많이 사용한다고 한다. 

 

 mevenCentral은 이전부터 많이 사용하는 저장소지만, 본인이 만든 라이브러리를 업로드하기 위해서는 정말 많은 과정과 설정이 필요하다. 그러다보니 개발자들이 직접 만든 라이브러리를 업로드 하는 것이 힘들어 점점 공유가 안되는 상황이 발생한다. 

 

 최근에 나온 jcenter는 이런 문제점을 개선하여 라이브러리 업로드를 간단하게 할 수 있다. 또한, jcenter에 라이브러리를 업로드 하면 mavenCentral에도 업로드 될 수 있도록 자동화 할 수 있다. 

 

 여기서는 mavenCentral과 jcenter 둘 다 등록해서 사용할 것이다. 

 

 dependencies는 프로젝트 개발에 필요한 의존성들을 선언하는 곳이다. compile 메소드 안에 라이브러리의 이름의 앞부분만 추가한 뒤 자동완성(ctrl + space)을 사용하면 라이브러리의 목록을 볼 수 있다. 의존성 코드를 작성할 때 주의해야 할 점은 버전을 명시해야 하지 않아야 한다. 그래야만 맨 위에 작성한 classpath 메소드의 ${springBootVersion}의 버전을 따라가며 버전 충돌 문제가 발생하지 않는다. 

 

 지금까지의 최종 작성된 코드는 다음과 같다.

 

buildscript {
    ext {
        springBootVersion = '2.1.7.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.daldalhada.book'
version '1.0-SNAPSHOT'
sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

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

 

 

 맨 오른쪽에서 Gradle 버튼을 클릭해서 의존성을 확인한다.

    (1)  테스트 코드   

 

 TDD(Test Driven Development)는 테스트가 주도하는 개발을 말하며 테스트 코드를 먼저 작성하는 것부터 시작한다. 아래 그림과 같이 RED-GREEN Cycle을 말한다.

 

  1. RED: 항상 실패하는 테스트를 먼저 작성한다.
  2. GREEN: 테스트가 통과하는 프로덕션 코드를 작성한다.
  3. REFACTOR: 테스트가 통과하면 프로덕션 코드를 리팩토링한다. 

 

출처: https://hyem-study.tistory.com/98?category=893981

 

 반면 단위 테스트는 TDD의 첫 번째 단계인 기능 단위의 테스트 코드를 작성하는 것을 말한다. TDD와 달리 테스트 코드를 꼭 먼저 작성하는 것이 아니고 리팩토링 작업도 포함되지 않는다. 순수하게 테스트 코드만 작성하는 것을 말한다. 

 


 

    (2) 단위테스트 장점    

 

 위키피디아에서의 단위 테스트의 장점은 다음과 같다.

 

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

 

 이 밖에도 더 많은 장점이 있는데 단위 테스트를 하기 전의 개발 방식은 보통 다음가 같은 것이다. 

 

  1. 테스트 코드를 작성한다.
  2. 프로그램(Tocmat 등)을 실행한다.
  3. Postman과 같은 API 테스트 도구로 HTTP 요청한다.
  4. 요청 결과를 출력(console.log, System.out.println 등)하여 눈으로 검증한다.
  5. 결과가 다르면 다시 프로그램을 중지하고 코드를 수정한다. 

 여기서 2~5번은 매번 코드를 수정할 때마다 반복해야 한다. 

 


 

    (3)  테스트 코드 작성하기    

 

 프로젝트 내 [src] - [main] - [java] 폴더의 마우스 오른쪽 버튼을 클릭하여 [New ▶ Package]를 차례로 선택해서 생성한다. 일반적으로 패키지명은 웹 사이트 주소의 역순으로 한다고 한다. 예를 들어, admin.daldalhade.com이라는 사이트라면 패키지명은 com.daldalhade.admin으로 하면 된다. 

 

 

 패키지가 생성됐으면 패키지를 마우스 오른쪽 버튼으로 클릭하고 [New ▶ Java Class]를 차례로 선택하면 된다. 클래스의 이름은 Application으로 설정한다.

 

 

 

그리고 클래스의 코드를 다음과 같이 작성한다. 

 

package com.daldalhada.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

 

 방금 생성한 Application 클래스는 프로젝트의 메인 클래스가 된다. @springBootApplication으로 인해 스프링 부트의 자동 설정, 스프링 Bean 읽기와 생성이 모두 자동으로 설정된다. 특히,  @springBootApplication이 있는 위치부터 설정을 읽기 때문에 이 클래스는 항상 프로젝트의 최상단에 위치해야 한다. 

 

 main 메소드에서 실행하는 SpringApplication.run으로 인해 내장 WAS를 실행한다. 여기서 내장 WAS는 별도로 외부에 WAS를 두지 않고 애플리케이션을 실행할 때 내부에서 WAS를 실행하는 것을 이야기 한다. 이러면 항상 서버에 톰캣을 설치할 필요가 없게 되고, 스프링 부트로 만들어진 Jar 파일(실행 가능한 Java 패키징 파일)로 실행하면 된다. 

 

 스프링 부트는 언제 어디서나 같은 환경에서 스프링 부트를 배포할 수 있기 때문에 내장 WAS를 사용하는 것을 권장하고 있다. 외장 WAS를 쓰게 되면 모든 서버는 WAS의 종류와 버전, 설정일 일치시켜야 하기에 복잡하다. 1대면 문제가 없지만 30대의 서버가 존재할 때 WAS의 버전을 올린다고 하면 실수할 여지가 많고 시간적인 면에서도 비효율적이 된다. 

 

 

 이번에는 테스트를 위한 Controller를 만들기 위해 패키지 하위에 web이라는 패키지를 만든다.

 

 

 그리고 테스트를 해 볼 Hello Cotroller를 하나 생성한다.

 

 

 

package com.daldalhada.springboot.web;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "hello";
    }
}

 

 위의 코드 중 @RestController는 JSON을 반환하는 컨트롤러로 만들어 주는 역할을 하고 @GetMapping은 HTTP 메서드인 Get의 요청을 받을 수 있는 API를 만들어 준다. 

 

 작성한 코드가 제대로 작동하는지 테스트를 하기 위해 WAS를 실행하지 않고 테스트 코드로 검증해보기 위해 [src]-[test]-[java] 디렉토리에 앞에 생성했던 패키지를 그대로 다시 생성한다. 그리고 테스트 코드를 작성할 클래스를 생성한다. 일반적으로 테스트 클래스는 대상 클래스 이름에 Test를 붙인다고 한다. 따라서, HelloControllerTest로 생성한다. 

 

 

 생성되면 다음과 위의 사진과 같게 되고 HelloControllerTest에 코드를 추가한다. 

 

package com.daldalhada.springboot;

import com.daldalhada.springboot.web.HelloController;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@WebMvcTest(controllers = HelloController.class)
public class HelloControllerTest {

    @Autowired
    private MockMvc mvc;

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

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

 

 위의 코드는 다음과 같다.

 

1. @RunWith(SpringRunner.class)

  • 테스트를 진행할 때 JUnit에 내장된 실행자 외에 다른 실행자를 실행시키는데 여기서는 SpringRunners라는 스프링 실행자를 사용한다.
  • 즉, 스프링 부트 테스트와 JUnit 사이를 연결하는 역할을 한다.

2. @WebMvcTest

  • 수 많은 스프링 테스트 어노테이션 중, Web(Spring MVC)에 집중할 수 있는 어노테이션이다. 
  • 스프링이 관리하는 모든 빈을 등록시켜 테스트하는 @SpringBootTest와 달리 @WebMvcTest는 web 관련 빈들만 등록하므로 가볍다.

3. @Autowired

  • 스프링이 관리하는 빈을 주입 받는다.

4. private MockMvc mvc

  • 웹 API를 테스트 할 때 사용되고 스프링 MVC 테스트의 시작점이다. 
  • 이 클래스를 통해 HTTP의 메서드인 GET, POST 등에 대한 API 테스트를 할 수 있다. 

5. mvc.perform(get("/hello"))

  • MockMvc를 통해 /hello 주소로 HTTP GET 요청을 한다. 
  • 체이닝이 지원되어 .andExpect에 여러 검증 기능을 이어서 선언할 수 있다.

6. andExpect(status().isOk())

  • mvc.perform의 결과를 검증하며 특히, HTTP Header의 Status를 검증한다. 
  • isOk()는 Status code가 200인지 아닌지 검증한다.

7. andExpect(content().string(hello))

  • 응답 본문의 내용을 검증한다.
  • Controller에서 "hello"를 리턴하기 때문에 값이 맞는지 검증하는 역할을 한다.

 

 HelloControllerTest.java에서 빨간색으로 표시된 화살표 부분을 클릭하고 Run HelloControllerTest.java를 클릭해 테스트 코드를 실행해보면 테스트가 통과되었다면 Tests passed가 뜬다. 

 

 HelloControllerTest.java에서 빨간색으로 표시된 화살표 부분을 클릭하고 Run HelloControllerTest를 클릭해 테스트 코드를 실행해보면 테스트가 통과되었다면 Tests passed가 뜬다. 

 

 이번에는 Application.java로 가서 수동으로 실행해 정상적으로 값이 출력되는지 확인해본다. 빨간색으로 표시된 화살표 버튼을 클릭하고 Run Application을 클릭하면 톰캣 서버가 8080포트로 실행되었다는 것이 콘솔 창에 출력된다. 

 

 

 

 

 

 이제 웹 브라우저에서 localhost:8080/hello로 접속하여 문자열 hello가 잘 나오는지 확인한다. 웹 브라우저를 열어 localhost:8080/hello로 접속한다.

 

 하지만, localhost:8080/login으로 바뀌어 다음과 같은 화면이 뜨는 경우가 발생할 수 있다. 처음에는 포트 충돌 문제인지 알고 포트를 변경하는 방법을 시도했다.

 

 포트를 변경하는 방법은 먼저, [build]-[resources]-[main] 또는 [src]-[main]-[resource]의  application.properties을 찾는다. 

 

 

 

 application.properties을 열어 server.port 부분을 원하는 포트 번호로 수정한다. 

 

 

 하지만, 포트를 변경해도 소용없었고 구글링 하던 중 스택 오버 플로우에서 Username은 user로 Password는 콘솔창에 나와있는 password를 치면 된다고 한다.

 

 

 결과는 정상적으로 출력되었고 테스트 코드의 결과와 같은 것을 알 수 있다.

 

'스프링 부트 프로젝트 > 테스트 코드 작성' 카테고리의 다른 글

4. 롬복 설치하기  (0) 2020.10.05