본문 바로가기
Backend/Spring

[Spring] 프로젝트 환경설정

by eyi-jin 2022. 6. 24.

프로젝트 환경설정

프로젝트 생성

라이브러리 살펴보기

View 환경설정

Build하고 실행하기



프로젝트 생성

이 게시글은 [인프런]스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의를 기반으로 작성되었으며 강의를 기억하기 위한 기록물입니다. 순수하게 복습하기 위한 용도로 작성되었음을 미리 밝힙니다.

1. 프로젝트 생성

Spring initializr 사용하여 스프링 프로젝트 생성합니다.

해당 페이지는 Spring Boot 기반으로 Spring 프로젝트를 만들어주는 사이트입니다.

아래의 선택지들을 하나씩 함께 살펴봅시다.

Project :

Maven VS. Gradle

여기서, 빌드 도구로 Maven 또는 Gradle을 선택할 수 있습니다.

빌드 도구는 필요한 라이브러리를 가져오고, 버전 설정해주고,
빌드하는 라이프 사이클을 관리해주는 도구입니다.

  • Maven으로 프로젝트를 생성하게 되면 pom.xml이 생성되고,
  • Gradle로 프로젝트를 생성하게 되면 build.gradle, settings.gradle이 생성됩니다.

예전에 하던 프로젝트나, legacy 프로젝트는 Maven을 많이 사용했지만, 요새는 대부분 Gradle로 넘어오고 있는 추세입니다.

그러므로 Gradle을 선택합니다.

Language

지금 java를 사용할 것이기 떄문에 언어는 java를 선택합니다.

Spring Boot

스프링 부트 버전을 선택하는 곳입니다.

SNAPSHOT → M (Milestone) → RC (Release Candidate) 순으로 개발 과정을 나타냅니다. 정식 릴리즈된 버전은 옆에 괄호 표시가 없습니다.

따라서 정식 릴리즈 버전 중 현재 가장 최신 버전인 2.7.0 버전을 선택합니다.

Project Metadata

  • Group에는 보통 기업 도메인 명을 적고,
  • Aritifact에는 빌드되어 나오는 결과물의 이름을 적습니다.
    즉, 프로젝트 명과 비슷하다고 볼 수 있습니다.
  • Packaging에서는 배포방법(JAR와 WAR)을 선택할 수 있습니다.
    JAR과 WAR는 애플리케이션을 쉽게 배포하고 동작시킬 수 있도록
    관련 파일을 패키징하는 방법입니다.
    현재 Spring boot 에서 가이드하는 표준은 JAR이므로 JAR을 선택합니다.
  • Java에는 현재 사용하는 Java 버전을 선택해줍니다. 해당 실습에서는 11버전으로 진행합니다.

Dependencies

지금 Spring boot 기반으로 프로젝트를 시작할 건데,
어떤 라이브러리를 가져와서 사용할 것인가를 정해주는 것입니다.

지금은 웹 프로젝트를 생성하는 것이므로 Spring Web과
html을 만들어주는 템플릿 엔진 중 하나인 Thymeleaf를 선택합니다.

아래와 같이 설정을 해줬으면 Generate를 눌러서 Spring boot 프로젝트를 다운로드 받습니다.

그 다음, 본인이 원하는 위치에서 압축을 풀어줍니다.

저는 C:\Users\eyi\SpringProject여기에 압축을 풀어줬습니다.

 

intellij에서 열기를 통해서 프로젝트를 열어줍니다.

2. 프로젝트 열기

파일을 열 때에는 아래의 그림과 같이 build.gradle을 선택하여 프로젝트로 열기를 선택하여 열어줍니다.

 

 

그리고 Open as Project를 선택해줍니다.

 

 

build.gradle을 보시면 아까 선택했던 사항들이 자동으로 들어가 있음을 알 수 있습니다.

  • plugin에는 아까 선택했던 2.7.0버전이 들어가 있음을 확인할 수 있습니다.
  • group 도 아까 설정했던 eyi-jin으로 되어있습니다.
  • sourceCompatibility는 11로 아까 선택했던 자바 버전을 나타냅니다.
  • dependencies에서는 Thymeleaf와 Spring Web라이브러리를 볼 수 있습니다.
  • repositories에는 dependencies에서 봤던 라이브러리를 mavenCentral이라는 해당 사이트에서 다운받으라는 것을 지정해주고 있습니다.

 

또한 .gitignore에는 깃헙에 올라가면 안되는 코드들을 start.spring.io에서 자동으로 설정해줍니다.

 

깃헙에는 build된 결과등을 올라가면 안되는데, 해당 파일을 .gitignore에서 설정해줌으로써 정할 수 있습니다.

Spring boot 실행하기

src.main.java.eyijin.hellospring.HelloSpringApplication.java 를 열어서 왼편의 작은 초록색 실행 버튼을 눌러서 main 메소드를 실행합니다.

 

실행후, 웹 브라우저에 들어가서 localhost:8080 를 입력해주면 해당 에러가 보이면 성공입니다!

 

여기까지가 기본 Spring boot 프로젝트 생성과 실행을 성공한 것입니다!

번외

Gradle을 통해서 실행하면, 느릴때가 있습니다. 이를 변경해주도록 하겠습니다.

그래서 Settings-Build,Execution,Deployment-Build Tools-Gradle에서

Gradle project영역에서 빨간색으로 표시한 것처럼 두 곳을 모두 Intellij IDEA로 변경해줍니다.

이렇게 변경을 하면, Gradle을 통하지 않고 바로 intellij에서 바로 java를 실행하기 때문에 훨씬 빠릅니다.



라이브러리 살펴보기

2-1. 라이브러리 살펴보기

Gradle은 의존관계가 있는 라이브러리를 함께 다운로드합니다.

핵심적인 라이브러리는 아래와 같습니다.

Spring Boot 라이브러리

  • spring-boot-starter-web
    • spring-boot-starter-tomcat: 톰캣 (웹서버)
    • spring-webmvc: 스프링 웹 MVC
  • spring-boot-starter-thymeleaf: 타임리프 템플릿 엔진(View)
  • spring-boot-starter(공통): 스프링 부트 + 스프링 코어 + 로깅
    • spring-boot
      • spring-core
    • spring-boot-starter-logging
      • logback
      • slf4j

테스트 라이브러리

  • sping-boot-starter-test
    • junit : 테스트 프레임워크
    • mockito : 목 라이브러리
    • assertj : 테스트코드를 좀 더 편하게 작성할 수 있도록 도와주는 라이브러리
    • spring-test : 스프링 통합 테스트 지원

아까 분명 spring-webthymeleaf만 설치한 것 같은데 왜 이렇게 많이 늘어난 걸까요?

라이브러리들이 서로 의존관계를 가지기 때문에, 의존 관계에 있는 모든 라이브러리를 다 가져와서 사용합니다. 그래서 이렇게 라이브러리가 늘어난 것입니다.

이제 이 라이브러리들을 직접 프로젝트에서 하나씩 살펴봅시다.

build.gradle에서 볼 수 있는 라이브러리는
우리가 가져오도록 설정한 thymeleafspring-web
자동으로 들어가 있는test 라이브러리 입니다.

하지만 오른쪽 프로젝트에서 외부 라이브러리에 들어가보면 엄청나게 많은 것들이 자동으로 들어가있음을 볼 수 있습니다.

이렇게 되는 이유는 아까도 말했듯이, 우리가 가져온 라이브러리의존관계를 가지고 있기 때문입니다. 그러면, Gradle은 해당 의존관계에 해당하는 것들을 자동으로 가져옵니다.

엄청 많아진 외부 라이브러리

Gradle 좀 더 살펴보기

인텔리제이 오른편에 새로로 Gradle로 되어있는 부분을 들어가봅니다.

Dependencies에 우리가 가져왔던 thymeleafspring web을 볼 수 있습니다.

해당 부분을 클릭하여 더 자세히 알아봅시다.

 

spring-web부분을 클릭하여 살펴봤는데,

tomcat이라고 서버가 내장되어 있음을 알 수 있습니다.

 

thymeleaf 부분을 클릭하여 살펴보면,

spring-boot-starter를 가져옵니다.
spring-boot-starter는 spring 프로젝트를 하면 기본적으로 쓰이는 라이브러리이기 때문에 thtmeleaf에서 가져오게 되었습니다.

그리고 그 내부에는 spring-boot-corelogging등 여러가지 의존성을 가진 라이브러리들를 더 가져옵니다.

 

💡log와 관련된 insight 
logging 부분을 들어가보면 `logback(구현체)`과 `slf4j(인터페이스)`가 들어 있습니다. 사람들이 이 2개의 조합을 많이 쓰기 때문에 아예 기본설정으로 만들어 놓은 건데요, 현업에서는 에러를 잡기위해 `System.out.println()` 보다 `log`를 사용해야 하기 때문에 추가로 더 알아보면 좋습니다.


View 환경설정

welcome page(정적 페이지) 참고 사이트

src > main > resources > static > index.html

//index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Hello</title>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
Hello
<a href="/hello">hello</a>
</body>
</html>

 

 

thymeleaf 템플릿 엔진(동적 페이지)

  • thymeleaf 공식 사이트 :
  • 스프링 공식 튜토리얼 :
  • 스프링부트 메뉴얼 :

 

 

// hello > helloSpring > controller > HelloController
@Controller
public class HelloController {

     @GetMapping("hello")
    public String hello(Model model) {
         model.addAttribute("data", "hello!!");
        return "hello";
     }
}
<!-- src > resources > templates > hello.html -->
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org"><head><title>Hello</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /></head><body><p th:text="'안녕하세요. ' + ${data}" >안녕하세요. 손님</p></body></html>
  • thymeleaf 템플릿엔진 동작확인1. 웹브라우저가 /hello라는 경로를 요청하면 Spring Boot는 내장 톰캣 서버를 통해 스프링 컨테이너에서 해당 경로를 가진 컨트롤러를 찾는다.2. model에 데이터를 저장하고 컨트롤러에서 리턴값으로 문자값을 반환하면 viewResolver가 화면을 찾아서 처리한다.

 

2. 스프링 웹 개발 기초

2-1. 정적 컨텐츠

URL로 접속하면 서버가 파일을 브라우저로 바로 내려주는 방식ex) welcome page

2-2. MVC와 템플릿 엔진

서버를 거쳐서 페이지를 변형한 뒤 브라우저로 전송하는 방식(뷰 반환)controller : 비지니스 로직을 작성하는 등 내부의 작업에 집중해야함view : 화면을 그리는데에 집중해야함

@Controller
public class HelloController {
     @GetMapping("hello-mvc")
    public String helloMvc(@RequestParam("name") String name, Model model) {
         model.addAttribute("name", name);
         return "hello-template";
    }
}
<html xmlns:th="http://www.thymeleaf.org"><body><p th:text="'hello ' + ${name}">hello! empty</p></body></html>
  1. http://localhost:8080/hello-mvc?name=spring
  2. url에 있는 hello-mvc를 컨트롤러에서 찾아서 파라미터를 모델에 담은 뒤 view name을 리턴해준다.
  3. 템플릿 엔진은 모델에 담긴 파라미터 값을 찾아 화면에 반환한다.

2-3. API

반환값으로 JSON을 사용하여 클라이언트에게 데이터 전달(데이터 반환)화면은 클라이언트가 알아서 그려야함

@Controller
public class HelloController {
     @GetMapping("hello-api")
    @ResponseBody
     public Hello helloApi(@RequestParam("name") String name) {
         Hello hello = new Hello();
         hello.setName(name);
        return hello; //객체반환
     }

static class Hello {
     private String name;
    public String getName() {
         return name;
    }
    public void setName(String name) {
         this.name = name;
     }
    }
}
  • @ResponseBody를 사용하고, 객체를 반환하면 객체가 JSON으로 변환됨
  • HTTP의 BODY부에 데이터를 직접 반환한다.
  • viewResolver 대신에 HttpMessageConverter가 동작한다.
    • 기본 문자처리 : StringHttpMessageConverter
    • 기본 객체처리 : MappingJackson2HttpMessageConverter

3. 회원 관리 예제

3-1. 비즈니스 요구사항 정리

  • 간단한 예제를 통해 스프링의 전반적인 개념을 알기위한 강좌이므로 가장 단순한 비즈니스 요구사항을 전제로 한다.
  • 데이터 : 회원 아이디, 이름
  • 기능 : 회원 등록, 조회
  • 데이터 저장소가 선정되지 않은 가상의 시나리오
  • 일반적인 웹 애플리케이션 계층구조
    • 컨트롤러 : 웹 MVC의 컨트롤러 역할
    • 서비스 : 핵심 비즈니스 로직 구현
    • 리포지토리 : 데이터베이스에 접근, 도메인 객체를 DB에 저장하고 관리
    • 도메인 : 비즈니스 도메인 객체
  • 클래스 의존 관계
    • 아직 데이터 저장소가 선정되지 않아서, 인터페이스로 구현 클래스를 변경할 수 있도록 설계
    • 개발을 진행하기 위해서 초기 개발단계에서는 구현체로 가벼운 메모리 기반의 데이터 저장소 사용

3-2. 회원 도메인과 리포지토리 만들기

//회원 도메인 객체
package hello.hellospring.domain;
public class Member {

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

}
//회원 리포지토리 인터페이스
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.List;
import java.util.Optional;
public interface MemberRepository {

 //멤버 저장
 Member save(Member member);
 //아이디 찾기
 Optional<Member> findById(Long id);
 //이름 찾기
 Optional<Member> findByName(String name);
 //모두 찾기
 List<Member> findAll();

}
//회원 리포지토리 메모리 구현체
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import java.util.*;

public class MemoryMemberRepository implements MemberRepository {
   //메모리 저장용 map
   private static Map<Long, Member> store = new HashMap<>();
   //ID값 생성을 위한 시퀀스
   private static long sequence = 0L;

   @Override
   public Member save(Member member) {
       member.setId(++sequence);
       store.put(member.getId(), member);
       return member;
   }

   @Override
   public Optional<Member> findById(Long id) {
       return Optional.ofNullable(store.get(id));
   }

   @Override
   public List<Member> findAll() {
         return new ArrayList<>(store.values());
   }

   @Override
   public Optional<Member> findByName(String name) {
         return store.values().stream()
         .filter(member -> member.getName().equals(name))
         .findAny();
   }

   //저장소 초기화
   public void clearStore() {
       store.clear();
   }
}

3-3. 회원 리포지토리 테스트케이스 작성

//회원 리포지토리 테스트케이스
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.*;

class MemoryMemberRepositoryTest {

     MemoryMemberRepository repository = new MemoryMemberRepository();

     @AfterEach
     public void afterEach() {
         repository.clearStore();
     }

     @Test
     public void save() {
         //given
         Member member = new Member();
         member.setName("spring");
         //when
         repository.save(member);
         //then
         Member result = repository.findById(member.getId()).get();
         //org.assertj.core.api
         assertThat(member).isEqualTo(result);
     }

     @Test
     public void findByName() {
         //given
         Member member1 = new Member();
         member1.setName("spring1");
         repository.save(member1);
         Member member2 = new Member();
         member2.setName("spring2");
         repository.save(member2);
         //when
         Member result = repository.findByName("spring1").get();
         //then
         assertThat(result).isEqualTo(member1);
     }

     @Test
     public void findAll() {
         //given
         Member member1 = new Member();
         member1.setName("spring1");
         repository.save(member1);
         Member member2 = new Member();
         member2.setName("spring2");
         repository.save(member2);
         //when
         List<Member> result = repository.findAll();
         //then
         assertThat(result.size()).isEqualTo(2);
     }
}
  • 테스트가 정상적으로 진행되었을 때
  • https://velog.velcdn.com/images%2Fhwana%2Fpost%2F315d5c97-3940-4b36-b29e-1a8d58db19a6%2Fimage.png
  • 테스트가 비정상적으로 진행되었을 때
  • https://velog.velcdn.com/images%2Fhwana%2Fpost%2F1e0676f1-64bc-4bf7-b99f-ac64b5c0f1ac%2Fimage.png
  • 모든 테스트는 순서에 상관없이 메소드 별로 따로 동작하게 설계해야함
  • @AfterEach : 하나의 메소드의 동작이 끝날 때마다 실행하게 되는 메소드, 여기에서는 저장소에 저장된 데이터를 삭제하는 역할을 한다.(한번에 여러개의 테스트를 진행하다보면 db에 직전 테스트 결과가 남아 있을 수 있는데, 이 결과는 다음 테스트에 영향을 줄 수도 있기 때문에 저장소 청소를 해줘야한다.)
  • 테스트 주도 개발 : 테스트 클래스를 먼저 작성한 뒤 구현 클래스를 작성할 수도 있다.(틀을 미리 작성하는 것)
  • given - when - then : 뭔가가 주어지면서(데이터) 이걸 실행했을때(검증 해야할 것) 결과가 이게 나와야함(검증하는 부분)

3-4. 회원 서비스 개발

package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import java.util.List;
import java.util.Optional;

public class MemberService {
     private final MemberRepository memberRepository = new MemoryMemberRepository();
     /**
     * 회원가입
     */
     public Long join(Member member) {
         validateDuplicateMember(member); //중복 회원 검증
         memberRepository.save(member);
         return member.getId();
     }

     private void validateDuplicateMember(Member member) {
         memberRepository.findByName(member.getName())
         .ifPresent(m -> {
         throw new IllegalStateException("이미 존재하는 회원입니다.");
         });
     }

     /**
     * 전체 회원 조회
     */
     public List<Member> findMembers() {
         return memberRepository.findAll();
     }

     public Optional<Member> findOne(Long memberId) {
         return memberRepository.findById(memberId);
     }
}

3-5. 회원 서비스 테스트케이스 작성

package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemoryMemberRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;

class MemberServiceTest {
     MemberService memberService;
     MemoryMemberRepository memberRepository;

     @BeforeEach
     public void beforeEach() {
         memberRepository = new MemoryMemberRepository();
         memberService = new MemberService(memberRepository);
     }

     @AfterEach
     public void afterEach() {
         memberRepository.clearStore();
     }

     @Test
     public void 회원가입() throws Exception {
         //Given
         Member member = new Member();
         member.setName("hello");
         //When
         Long saveId = memberService.join(member);
         //Then
         Member findMember = memberRepository.findById(saveId).get();
         assertEquals(member.getName(), findMember.getName());
     }

     @Test
     public void 중복_회원_예외() throws Exception {
         //Given
         Member member1 = new Member();
         member1.setName("spring");
         Member member2 = new Member();
         member2.setName("spring");
         //When
         memberService.join(member1);
         IllegalStateException e = assertThrows(IllegalStateException.class,() -> memberService.join(member2));//예외가 발생해야 한다.
         assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
     }
}
  • @BeforeEach : 각 테스트 실행전에 호출된다. 테스트가 서로 영향이 없도록하고 항상 새로운 객체를 생성한다.


Build하고 실행하기

Buiald하고 실행하기

./gradlew.bat build
cd build/libs
ls
java -jar hello-spring-0.0.1-SNAPSHOT.jar
  • ./gradlew.bat build

cf) 잘 안되면 ./gradlew.bat clean build를 해보면 된다

 

  • cd build/libs 이후 ls

  • java -jar hello-spring-0.0.1-SNAPSHOT.jar

 

서버 배포를 하려면 jar파일만 올려서 돌려주면 된다!

댓글