'React.js, 스프링 부트, AWS로 배우는 웹 개발 101'을 읽고 정리한 글입니다.
백엔드 개발을 위해 사용할 도구들은 다음과 같다.
- 스프링 부트
- Gradle
- Lombok
- Postman
- REST
- Layered Architecture
도구의 사용법을 아는 것도 중요하지만 이 도구들을 사용하는 이유를 아는 것도 중요하다.
이 도구들이 어떤 문제를 해결하는지, 또 어떻게 해결하는지에 대해 알아볼 예정이다.
Todo 서비스는 Todo 생성/검색/수정/삭제 라는 네 가지 기본적인 기능을 제공한다.
생성, 검색, 수정, 삭제의 네 가지 기본 기능을 CURD Create, Retrieve, Update, Delete 라고 한다.
이 기능을 REST API의 형태로 구현하고 그 과정에서 아키텍처 디자인, 아키텍처 패턴, JPA 등을 알아본다.
학습 내용
- 스프링 부트와 의존성 주입
- 디스패처 서블릿
- 빌드 자동화 툴
실습 내용
- 스프링 부트 프로젝트 설정
- 빌드 자동화 툴을 이용한 라이브러리 설정
- Lombok
- 포스트맨을 이용한 API 테스팅
2.1.1 자바 8 설치
2.1.2 이클립스 설치
2.1.3 스프링 프레임워크와 의존성 주입
스프링이란 오픈 소스의 경량 프레임워크다.
프레임워크를 사용할 때 메모리나 CPU 자원이 많이 들지 않거나 사용이 쉽고 간편한 경우를 경량 프레임워크라고 한다.
프레임워크 Framework
프레임워크는 개발자들이 확장해서 사용할 수 있는 코드다.
확장해서 사용한다는 것은
- 프레임워크가 제공하는 클래스나 라이브러리를 사용하거나
- 프레임워크가 제공하는 클래스나 인터페이스를 상속 및 구현해 코드를 프레임워크의 일부로 실행하는 것을 의미한다.
스프링 프레임워크의 핵심은 무엇일까?
스프링 프레임워크의 핵심을 한 단어로 표현해 보라 한다면 의존성 주입(Dependency Injection, DI)을 말할 것이다.
의존성 주입은 IoC(Inversion of Control)와 함께 많이 언급된다.
- IoC는 제어를 역전하는 것을 보편적으로 설명하는 단어이고
- 의존성 주입은 디자인 패턴으로 IoC를 구현하는 방법 중 하나다.
의존성 주입이 무엇이고 스프링 프레임워크가 어떤 문제를 해결해 주는지에 대해 알아보자.
의존성 주입
먼저 의존성이 무엇이고 의존성 주입이 왜 필요한지 알아보자.
Todo 애플리케이션을 예로 들어보자.
public class TodoService {
private final FileTodoPersistence persistence;
public TodoService() {
this.persistence = new FileTodoPersistence();
}
public void create(...) {
...
persistence.create(...);
}
}
Todo 애플리케이션은 TodoService 클래스가 있고 이 클래스가 Todo 목록을 관리하는 기능을 제공한다고 가정하자.
FileTodoPersistence가 파일에 Todo 목록을 저장할 수 있도록 도와주는 클래스라면,
TodoService는 FileTodoPersistence 없이는 제대로 기능을 못한다.
따라서 TodoService는 FileTodoPersistence에 의존한다.
이 경우 FileTodoPersistence에 의존하는 TodoService가 FileTodoPersistence 오브젝트를 생성하고 관리한다.
public static void main(String[] args) {
TodoService service = new TodoService();
}
위 코드는 지금 당장은 문제가 없어 보인다.
그런데 어느 날 파일에 저장하지 말고 데이터베이스에 저장하려한다. 그래서 DatabaseTodoPersistence를 구현하기 시작한다.
TodoService에서 FileTodoPersistence 대신 DatabaseTodoPersistence로 자료형을 바꾸고 생성자에서 DatabaseTodoPersistence를 생성한다.
그런데 몇 주 후 데이터베이스가 비싸니 가격이 좀 더 저렴한 AWS S3를 이용하려 한다. 뿐만 아니라 그 사이 개발이 진전돼 DatabaseTodoPersistence를 사용하는 서비스가 100개 정도 생겼다. 이제 100개의 클래스를 돌아다니면서 DatabaseTodoPersistence 대신 S3TodoPersistence를 생성하도록 수정해야 한다.
위 코드와 같이 구현하는 경우 유닛 테스트 Unit Test 작성이 어렵다. 유닛 테스트에서 실제 애플리케이션이 사용하는 퍼시스턴스를 그대로 사용하긴 힘들다. 따라서 껍데기만 있는 클래스(Mock 클래스)를 만들어 써야 된다. 그런데 생성자 내부에서 FileTodoPersistence를 생성하기 때문에 Mock 클래스를 만들어도 TodoService를 사용할 수 없다.
이런 문제점을 해결하는 것이 의존성 주입이다.
의존성 주입이란 이 클래스가 의존하는 다른 클래스들을 외부에서 주입시킨다는 뜻이다.
이를 구현하는 방법에는 생성자를 이용해 주입하는 방법과 Setter를 이용해 주입하는 방법이 있다.
생성자를 이용한 의존성 주입
public class TodoService {
private final ITodoPersistence persistence; // 인터페이스
public TodoService(ITodoPersistence persistence) {
this.persistence = persistence;
}
public void create(...) {
...
persistence.create(...);
}
}
public static void main(String[]args){
ITodoPersistence persistence = new FileTodoPersistence();
TodoService service = new TodoService(persistence);
}
위 코드와 같이 한 Object가 의존하는 Object를 생성하는 것이 아니라 외부에서 넘겨받는 것을 의존성 주입이라고 한다.
Setter를 이용한 의존성 주입
public class TodoService {
private final ITodoPersistence persistence; // 인터페이스
public void setITodoPersistence(ITodoPersistence persistence) {
this.persistence = persistence;
}
}
메인 메서드에서 사용할 때 일단 오브젝트를 초기화한 후 그 다음 줄에서 Setter를 이용해 의존하는 오브젝트를 주입한다.
public static void main(String[]args){
ITodoPersistence persistence = new FileTodoPersistence();
TodoService service = new TodoService(persistence);
service.setITodoPersistence(persistence);
}
의존성 주입을 구현하는 것은 유닛 테스트에서 껍데기만 있는 Mock 오브젝트를 주입하는 데도 유용하다.
@Test
public void test() {
ITodoPersistence persistence = new MockTodoPersistence();
TodoService servce = new TodoService(persistence);
}
유닛 테스트 작성 시 위처럼 Mock 오브젝트를 초기화하고 이 오브젝트를 테스팅할 오브젝트에 주입할 수 있다.
그냥 생성자나 Setter를 사용하는 것을 의존성 주입이라고 부르는 것이라고 할 수 있다.
이론은 어렵게 느껴지지만 실제로 구현 자체는 복잡하지 않다.
이런 의존성 주입을 전문적으로 해주는 것이 의존성 주입 컨테이너 Dependency Inject Container이고
그 의존성 주입 컨테이너 중 하나가 바로 스프링 프레임워크다.
2.1.4 스프링 프레임워크와 디스패처 서블릿
스프링이 웹 애플리케이션 측면에서 어떤 기능을 제공할까?
자바 웹 애플리케이션은 대부분 자바 서블릿 Java Servlet을 기반으로 한다.
서블릿 기반의 서버를 사용하려면?
- javax.servlet.http.HttpServlet을 상속받는 서브 클래스를 작성해야 한다.
- 서블릿 컨테이너 Servlet Container가 서블릿 서브 클래스를 실행시킨다.
위 그림처럼 http 요청이 서버로 전달되면 웹 서버는 받은 요청을 해석해 해당되는 서블릿 클래스를 실행한다.
서블릿 클래스 구현
package com.example.Demo;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class Hello extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response) throws
ServletException, IOException {
// parameter 해석
String name = request.getParameter("name");
// business logic 실행
process(name);
// response 구축
response.setContentType("text/html");
PrintWriter out = response.getWriter();
out.print("<html>");
// UI 부분
out.print("</html>");
}
private void process(String name) {
// business logic
}
}
- HttpServlet을 상속하는 서브 클래스를 만들고 doGet() 메서드를 구현해야 한다.
- 매개변수로 넘어오는 HttpServletRequest에서 원하는 정보를 추출한다.
- 비즈니스 로직인 process()를 실행하고 반환할 정보를 HttpServletResponce에 담는다.
시간을 투자하고 싶은 로직이 process() 메서드 하나뿐이더라도 매개변수 해석과 응답 부분은 항상 작성해 줘야 한다.
또 API를 하나 만들 때마다 이 작업을 반복해야 한다.
반복 작업과 코드를 최소화하기 위해 스프링 부트는 어노테이션과 서브 클래스를 이용할 수 있다.
스프링 부트는 DispatcherServlet이라는 서블릿 서브 클래스를 이미 구현하고 있기 때문에 개발자가 서블릿 클래스를 작성하지 않아도 된다. 대신 개발자는 스프링 부트가 제공하는 어노테이션과 인터페이스를 이용해 스프링이 비즈니스 로직을 이해할 수 있도록 내부 기능을 구현하면 된다.
@RestController // JSON을 리턴하는 웹 서비스임을 명시
public class HelloController {
@GetMapping("/test") // path 설정, GET 메서드 사용
public String process(@RequestParam String name) {
// 비즈니스 로직
return "test" + name;
}
}
위 코드 처럼 서블릿 클래스를 작성하지 않아도 된다.
스프링을 사용하면
- HttpServlet을 상속받지 않아도 되고
- doGet을 오버라이드하지 않아도 되고
- HttpServletRequest를 직접 파싱하지 않아도 되고
- HttpServletResponse를 작성하지 않아도 된다.
2.1.5 스프링 부트프로젝트 설정
GENERATE를 클릭해 프로젝트 압축 파일을 받고 원하는 곳에 압축 해제한다.
인텔리제이에서 파일 => 열기 => 폴더 선택
2.1.6 메인 메서드와 @SpringBootApllication
package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@SpringBootApplication
- 이 어노테이션은 해당 클래스가 스프링 부트를 설정하는 클래스임을 의미한다.
- 스프링은 @SpringBootApplication 어노테이션이 달린 클래스가 있는 패키지를 베이스 패키지로 간주한다.
@SpringBootApplication은 무엇을 할까?
스프링의 중요 기능 중 하나는 의존성 주입 컨테이너다.
- 스프링은 베이스 패키지와 그 하위 패키지에서 자바 빈을 찾아서
- 스프링의 의존성 주입 컨테이너 오브젝트, 즉 ApplicationContext에 등록한다.
- 그리고 애플리케이션 실행 중 어떤 오브젝트가 필요한 경우 의존하는 다른 오브젝트를 찾아 연결해 준다.
@Autowired
자동으로 다른 오브젝트를 찾아 연결해 주는 어노테이션이다.
스프링은 어떻게 애플리케이션 컨텍스트 ApplicationContext에 등록한 자바 빈을 찾는 것일까?
@Component
- @Component는 스프링에게 이 클래스를 자바 빈으로 등록시키라고 알려주는 어노테이션이다.
- 그러면 @Component를 클래스에 달기만 하면 무조건 스프링이 검색해 등록해 줄까?
그렇지 않다. - @ComponentScan 어노테이션이 어떤 클래스에 있어야지만 컴포넌트를 스캐닝할 수 있다.
- (@Bean Vs. @Component)
그런데 @ComponentScan은 찾아 볼 수 없다. 어디에 있는 것일까?
@ComponentScan을 프로젝트 내부에서 사용하지는 않았지만 @SpringBootApplication이 이미 @ComponentScan을 포함하고 있어서 굳이 추가하지 않아도 된다.
스프링을 이용해 관리하고 싶은 빈의 클래스 상단에 @Component를 추가해 주면 자동으로 이 오브젝트를 스프링에 빈으로 등록할 수 있다. 그리고 @Autowired와 함께 이용하면 스프링이 필요할 때 알아서 이 오브젝트를 생성해 준다.
만약 스프링이 자동으로 오브젝트를 찾아 생성하게 하고 싶지 않으면 어떻게 해야 할까?
다시 말해 @Component를 추가하지 않고 스프링을 통해 빈을 관리하고 싶은 경우 어떻게 해야 할까?
@Bean
@Configuration
public class ConfigClass {
@Bean
public Controller getController() {
if (env == 'local') {
return new LocalController(...);
}
return new Controller(...);
}
}
위 의문을 다시 말하면 언제 @Component를 사용하고 싶지 않을까? 또 언제 @Component를 사용하지 못할까?
- 엔터프라이즈 애플리케이션의 경우 @Autowired를 사용하지 않는 경향이 있다.
=> 엔지니어가 오브젝트를 어떻게 생성하고 어느 클래스에서 사용하는지 정확히 알아야 하는 경우가 많기 때문이다. - 다른예로는 로컬 환경에서 애플리케이션을 실행하는 경우 자동으로 연결될 빈이 아닌 다른 빈을 사용하고 싶은 경우가 있다. 위 코드가 그런 경우다.
- 또 우리가 라이브러리를 사용할 때 이 라이브러리 클래스가 스프링 기반이 아니라서 @Component를 추가하지 못하는 경우도 있다.
이런 경우 스프링으로 빈을 관리하려면 직접적으로 "이 빈은 이렇게 생성해라"라고 말해줄 필요가 있다.
그 작업을 위한 어노테이션이 바로 @Bean이다.
@Bean을 이용해 스프링에게 이 오브젝트를 정확히 어떻게 생성해야 하는지, 매개변수를 어떻게 넣어줘야 하는지 알려줄 수 있다.
1. 스프링 부트 애플리케이션 시작
2. @ComponentScan 어노테이션이 있는 경우 베이스 패키지와 그 하위 패키지에서 @Component가 달린 클래스를 찾는다.
3. 필요한 경우 @Component가 달린 클래스의 오브젝트를 생성한다. 이때 생성하려는 오브젝트가 다른 오브젝트에 의존한다면, 즉 멤버 변수로 다른 클래스를 갖고 있다면 그 멤버 변수 오브젝트를 찾아 넣어줘야 한다. @Autowired를 사용하는 경우 스프링이 그 오브젝트를 찾아 생성해 넣어준다.
a. 이때 @Autowired에 연결된 변수의 클래스가 @Component가 달린 클래스인 경우 스프링이 오브젝트를 생성해 넘겨준다.
b. 만약 @Bean 어노테이션으로 생성하는 오브젝트인 경우 @Bean이 달린 메서드를 불러 생성해 넘겨준다.
2.1.7 빌드 자동화 툴: Gradle과 라이브러리
빌드 자동화 툴을 이용하면 컴파일, 라이브러리 다운로드, 패키징, 테스팅 등을 자동화 할 수 있다.
그렇다면 빌드 자동화는 왜 사용할까?
빌드 자동화 툴이 없다면 라이브러리의 사용을 위해 라이브러리 사이트에서 jar 파일을 다운로드받아야 한다.
라이브러리를 추가하면 코드에서 이 라이브러리를 사용할 수 있다.빌드 자동화 툴이 없다면 라이브러리의 개수만큼 jar 파일을 다운받아 설치하는 작업을 하고 있을 것이다.
게다가 프로젝트가 커지면 여러 가지 빌드를 나눠 작업해야 할 수도 있다. 이 과정에서 Dependency가 있다면 빌드 순서를 고려해야 한다.
예를 들어 많은 프로젝트들이 프로덕션 릴리스 빌드 과정에서 빌드 => 유닛 테스트 실행 작업을 거친다.빌드 자동화 툴이 없다면 오퍼레이터 또는 개발자가 모든 라이브러리를 컴파일해 빌드를 하고 유닛 테스트를 실행시키는 작업을 해야 한다. 또한 배포가 있을 때마다 오퍼레이터는 이 작업을 해야 한다.
• 빌드 자동화 툴을 사용하면 이런 과정을 자동화할 수 있다.
• 다운로드받는 대신 원하는 라이브러리와 버전을 코드로 작성한다. 오퍼레이터가 직접 컴파일, 빌드, 유닛 테스트를 실행하는 대신 이 과정을 일련의 코드로 적는다.
• 그러면 빌드 자동화 툴이 이 코드를 해석해 프로젝트 빌드에 필요한 작업을 실행해 준다.
자동화 툴 중 하나인 Gradle은 자바, 그루비, 스칼라 등 JVM에서 실행되는 언어의 빌드 자동화를 위해 사용된다.
build.gradle
plugins {
id 'org.springframework.boot' version '2.7.1'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
Gradle은 의도적으로 많은 기능을 제공하지 않는다. 대신 플러그인을 통해 Gradle을 확장해 사용할 수 있다.예를 들어 자바를 컴파일하려면 Gradle 자바 플러그인이 필요하다. 플러그인의 id 'java'는 자바이고 이는 빌드를 위해 자바 플러그인을 사용함을 명시한다.
group, version, sourceCompatibility
group, version, sourceCompatibility는 프로젝트의 Meta-Data다.
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'
- group은 artifact(애플리케이션)를 배포하는 데 사용된다.
- version은 이 프로젝트의 버전이다.
- sourceCompatibility에 명시된 자바 버전을 이용해 소스를 컴파일한다.
Lombok
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
dependencies {
// 다른 디펜던시
annotationProcessor 'org.projectlombok:lombok'
// 다른 디펜던시
}
- Lombok은 어노테이션을 추가하면 컴파일 시 그에 상응하는 코드를 만들어 주는 라이브러리다.
- Lombok이 코드를 작성하려면 annotationProcessor라는 것이 필요하다.
- 그래서 configurations 부분에서 annotationProcessor를 컴파일 당시 사용하라고 Gradle에게 알려준다.
- 'AnnotationProcessor'로 org.project.lombok:lombok을 사용하도록 디펜던시에 명시한다.
Repository
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
maven { url 'https://repo.spring.io/snapshot' }
}
- Gradle이 라이브러리를 다운로드하는 곳을 Repository라고 부른다.
- 그중 MavenCentral을 주로 사용하도록 mavenCentral()을 작성하였다.
이후 Lombok annotationProcessor 설치를 위해 mavenCentral을 이용하면서 더 자세히 살명하도록 한다.
Dependency
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
- dependency 섹션에서 이 프로젝트에서 사용할 라이브러리를 명시하면
gradle이 repository에서 라이브러리를 다운로드 및 설치한다.
Test
tasks.named('test') {
useJUnitPlatform()
}
gradle을 사용하면 빌드뿐만 아니라 유닛 테스트도 실행시킬 수 있다.
여기선 JUnitPlatform을 사용해 유닛 테스트를 하도록 명시했다.
2.1.8 디펜덴시 라이브러리 추가
스프링 부트 프로젝트를 하다 보면 라이브러리를 추가해야 하는 상황이 있을 수 있다.
maven repository를 이용해 라이브러리를 추가할 수 있다.
mavenCentral을 사용하므로 maven repository를 이용해 라이브러리를 추가하면 된다.
https://mvnrepository.com/ 에서 원하는 라이브러리를 검색하여 추가한다.
예로 들어 google guava 라이브러리를 추가해보자.
- https://mvnrespository.com 에서 google guava를 검색한다.
- 원하는 버전을 선택한다. 웬만하면 최근 버전 중 Usage가 많은 버전을 선택한다.
- 버전을 선택하면 Maven, Gradle, SBT, Ivy등 각 빌드 자동화 툴마다 어떤 코드를 추가해야 할지 알려준다.
- Gradle을 사용한다면 위 코드를 build.gradle의 dependency 부분에 추가한다.
2.1.9 Lombok
Lombok 라이브러리를 이용하면 getter, setter, builder, constructor를 작성하는 데 시간을 소모할 필요가 없다.
Lombok이 제공하는 annotationProcessor가 getter, setter, builder, constructor 프로젝트 컴파일 시 관련 코드를 자동으로 작성해준다.
- Lombok을 사용하면 코드의 양을 줄이고 개발 시간을 단축할 수 있다.
2.1.10 포스트맨 API 테스트
REST API는 크게 URI, HTTP 메서드, 요청 매개변수 또는 요청 바디로 구분되는데 이를 브라우저에서 테스트하는 데는 한계가 있다. 또 REST API를 테스팅한다고 임시로 프론트엔드 UI를 만드는 것은 지속적인 방법이 아니다.
사용이 간편하고 직관적인 GUI를 제공하는 포스트맨 프로그램을 사용한다.
포스트맨을 이용하면 간단히 RESTful API를 테스트할 수 있다.
API 테스트를 위해 API 요청을 작성한다. +버튼을 클릭하면 새 요청을작성할 수 있다.
GET 부분에 www.google.com 을 입력하고 Send를 클릭한다.
구글 메인 페이지의 HTML이 결과 부분에 출력된다.