2.1 백엔드 개발 환경 설정
2.1.1 자바 버전
2.1.2 통합개발환경
2.1.3 스프링 프레임워크와 의존성 주입
스프링 이란 메모리 혹은 CPU 자원이 적게 들어가는 경량(?) 프레임워크이다.
프레임워크 제공하는 클래스나 라이브러리를 사용하거나 이를 상속 및 구현해 발자들이 확장하여 사용 가능한 코드 .
의존성 주입 이란 클래스가 의존하는 다른 클래스들을 외부에서 주입시키는 것을 의미한다.
생성자를 이용하여 주입 Setter를 이용하여 주입 유닛 테스트 시 Mock 오브젝트 주입
Copy public class TodoService {
private final ITodoPersistence persistence;
public TodoService ( ITodoPersistence persistence) {
this . persistence = persistence;
}
public void create (. . .) {
. . .
persistence . create ( . . . );
}
}
// Todo Service 오브젝트를 생성할 ITodoPersistence 구현부를 넘겨줌.
public static void main ( String [] args) {
ITodoPersistence persistence = new FileTodoPersistence() ;
TodoService service = new TodoService(persistence) ;
}
Copy public class TodoService {
private final ITodoPersistence persistence;
public void setITodoPersistence ( ITodoPersistence persistence) {
this . persistence = persistence;
}
}
public static void main( String [] args) {
ITodoPersistence persistence = new FileTodoPersistence() ;
TodoService = new TodoService() ;
service . setITodoPersistence (persistence);
}
Copy @ Test
public void test() {
ITodoPersistence persistence = new MockTodoPersistence() ;
TodoService service = new TodoService(persistence) ;
}
스프링의 의존성 주입 컨테이너로서의 기능
스프링은 베이스 패키지와 그 하위 패키지에서 자바 빈을 찾아 ApplicationContext (스프링의 의존성 주입 컨테이너 오브젝트)에 등록
애플리케이션 실행 중 어쩐 오브젝트가 필요한 경우 의존하는 다른 오브젝트를 찾아 연결해 줌
스프링 프레임워크에서 의존성을 명시하는 방법 (때에 따라 적합한 방법 사용)
의존성 주입을 직접 구현하지 않고 스프링 프레임워크를 사용하는 이유 ?
규모가 큰 프로그램의 경우 관리해야하는 오브젝트가 많아질 수록 비효율적
스프링 부트
스탠드얼론 프로덕션급의 스프링 기반 애플리케이션 구동을 가능하게 해줌
임베디드 톰캣이나 제티(jetty)같은 웹 서버를 애플리케이션 실행 시 함께 실행 및 설정
2.1.4 스프링 프레임워크와 디스패처 서블릿
자바 서블릿 (Java Servlet)
http 요청이 서버로 전달되면 웹 서버는 받은 요청을 해석하여 해당되는 서블릿 클래스를 실행
그러면 서블릿 컨테이너가 서블릿 서브 클래스를 실행
개발자는 javax.servlet.http.HttpServlet을 상속받는 서브 클래스를 작성해야 함
Copy package com . example . Demo ;
import java . io . * ;
import javax . servlet . * ;
import javax . servlet . http . * ;
public class Hello extends HttpServlet {
@ Override
public void doGet ( HttpServeltRequest 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에서 원하는 정보를 추출
반환할 정보를 HttpServletResponse에 담음
문제
매개변수 해석과 응답 부분을 항상 작성해 줘야함
API 하나 만들 때마다 위와같은 똑같은 작업을 반복
DispatcherServlet
스프링 부트는 DispatcherServlet이라는 서블릿 서브 클래스를 이미 구현하고 있음
개발자는 스프링 부트가 제공하는 어노테이션과 인터페이스를 이용하면 됨
Copy @ RestController //Json을 리턴하는 웹 서비스임을 명시
public class HelloController {
@ GetMapping ( "/test" )
public String process (@ RequestParam String name) {
// business logic
return "test" + name;
}
}
스프링 사용했을 경우
HttpServletRequest를 직접 파싱하지 않아도 됨
HttpServletResponse를 작성하지 않아도 됨
2.1.5 스프링부트 프로젝트 설정
2.1.6 메인 메서드와 @SpringBootApplication
@SpringBootApplication
해당 클래스가 스프링 부트를 설정하는 클래스임을 의미
이 어노테이션이 달린 클래스가 있는 패키지를 베이스 패키지로 간주
컴포넌트 스캐닝을 할 수 있는 @ComponentScan 을 포함
Copy // . . . 다른 어노테이션들
@ ComponentScan //매개변수 생략
public @ interface SpringBootAllication {
// . . .
}
@Component
스프링에게 이 클래스를 자바 빈으로 등록시키라고 알려주는 어노테이션
앞선 예제에서 사용하였던 @Service 내부에도 @Component 어노테이션을 달고있음 (이후 나올 @Controller 와 @Repository 등 다양한 스테레오 타입 어노테이션 내부에도 @Component이 달려있음)
Copy @ Component
public @ interface Service {
. . .
}
중간 정리
→ 관리하고 싶은 빈의 클래스 상단에 @Component 를 추가
→ 자동으로 이 오브젝트를 스프링에 빈으로 등록
→ @Autowired와 함께 스프링이 필요할 때 알아서 오브젝트를 생성
🙌🏻 What if 스프링이 자동으로 오브젝트를 찾아 생성하게 하고싶지 않다면 ? (= @Component를 추가하지 않고 스프링을 통해 빈을 관리하고 싶다면 ? )
어느 클래스에서 사용하는지 정확히 알아야 하는 경우
로컬 환경에서 애플리케이션을 실행할 때, 자동으로 연결되는 빈이 아닌 다른 빈을 사용하고 싶을 경우
@Bean
스프링에게 이 오브젝트를 어떻게 생성해야 하는지 알려줌
정리
@ComponentScan 어노테이션 있는 경우 베이스 패키지와 하위 패키지에서 @Component가 달린 클래스를 찾음
필요한 경우 @Component가 달린 클래스의 오브젝트를 생성
이때 생성하려는 오브젝트가 다른 오브젝트에 의존할 경우, 그 멤버 변수 오브젝트를 찾아 넣어주어야 함
@Autowired를 사용하는 경우 스프링이 오브젝트를 찾아 생성하여 넣어줌
@Autowired에 연결된 변수의 클래스가 @Component가 달린 클래스 → 스프링이 오브젝트를 생성해 넘겨줌
만약 @Bean 어노테이션으로 생성하는 오브젝트 → @Bean이 달린 메서드를 불러 생성하여 넘겨줌
2.1.7 빌드 자동화 툴 : Gradle과 라이브러리 추가
Gradle
컴파일, 라이브러리 다운로드, 패키징, 테스팅 등 자동화
빌드 자동화를 사용하는 이유 ?
빌드 자동화 툴이 없다면
라이브러리 사이트에서 jar 파일 다운, Project Build Path에 추가
오퍼레이터 또는 개발자가 모든 라이브러리를 컴파일해 빌드를 하고 유닛 테스트를 실행시켜야 함 (빌드 순서 고려)
빌드 자동화 툴 사용
라이브러리를 다운받는 대신 원하는 라이브러리와 버전을 코드로 작성
오퍼레이터가 직접 컴파일, 빌드, 유닛테스트를 실행하는 대신 이 과정을 일련의 코드로 적어, gradle이 이 코드를 해석해 프로젝트 빌드에 필요한 작업 실행
Copy plugins {
id 'org.springframework.boot' version '2.6.4'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'com.soobin'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
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'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named( 'test' ) {
useJUnitPlatform ()
}
2.1.8 디펜던시 라이브러리 추가
구글 구아바 라이브러리 추가
메이븐 센트럴에서 google guava를 검색
원하는 버전 선택 (주로 Usage가 많은 버전)
Copy // <https://mvnrepository.com/artifact/com.google.guava/guava>
implementation 'com.google.guava:guava:31.0.1-jre'
위 코드를 build.gradlw의 dependency 부분에 추가
2.1.9 롬복
롬복이 제공하는 어노테이션 프로세서가 getter, setter, builder, constructor 프로젝트 컴파일 시 관련 코드를 자동으로 작성해 줌
2.1.10 포스트맨 API 테스트
포스트맨은 REST API 테스팅 툴
REST API는 크게 URI, HTTP 메서드, 요청 매개변수 또는 요청 바디로 구분되는데 이를 브라우저에서 테스팅하는 데는 한계
사용이 간편하고 직관적인 GUI를 제공하는 포스트맨 사용
2.2 백엔드 서비스 아키텍처
2.2.1 레이어드 아키텍
레이어드 아키텍처 → 프로젝트 내부에서 어떻게 코드를 관리할 것인지
REST 아키텍처 → 클라이언트가 서비스를 이용할 때 어떤 형식으로 요청을 보내고 응답을 받을 것인지
레이어가 없는 웹서비스 메서드로 분리한 웹서비스 레이어드 아키텍처를 적용한 웹서비스
2.2.2 모델, 엔티티, DTO
모델과 엔티티 (이 프로젝트에서는 모델과 엔티티를 한 클래스에 구현)
Copy //TodoEntity.java
package com . soobin . todoWeb . model ;
import lombok . AllArgsConstructor ;
import lombok . Builder ;
import lombok . Data ;
import lombok . NoArgsConstructor ;
@ Builder
@ NoArgsConstructor
@ AllArgsConstructor
@ Data
public class TodoEntity {
private String id;
private String userId;
private String title;
private boolean done;
}
@Builder
Builder 클래스를 따로 개발하지 않고도 오브젝트 생성
Copy TodoEntity todo = TodoEntity . builder ()
. id ( "t-10328373" )
. userId ( "developer" )
. title ( "Implement Model" )
. build ();
생성자를 이용하는 방법과 비교하자면, 매개변수의 순서를 기억할 필요가 없음
@NoArgsConstructor
Copy public TodoEntity() {
}
@AllArgsConstructure
클래스의 모든 멤버 변수를 매개변수로 받는 생성자를 구현
Copy public TodoEntity( String id , String userId , String title , boolean done) {
super();
this . id = id;
this . userId = userId;
this . title = title;
this . done = done;
}
@Data
클래스 멤버 변수의 Getter/Setter 메서드를 구현
Copy public String getId() {
return id;
}
public void setId() {
this . id = id;
}
DTO (Data Transfer Object)
서비스가 요청을 처리하고 클라이언트로 반환하는 경우 사용
모델을 그래도 리턴하지 않고 DTO를 사용하는 이유 ?
클라이언트가 필요로하는 정보를 커스터마이징해서 전달
Copy //TodoDTO.java
package com . soobin . todoWeb . dto ;
import com . soobin . todoWeb . model . TodoEntity ;
import lombok . AllArgsConstructor ;
import lombok . Builder ;
import lombok . Data ;
import lombok . NoArgsConstructor ;
@ Builder
@ NoArgsConstructor
@ AllArgsConstructor
@ Data
public class TodoDTO {
private String id;
private String title;
private boolean done;
public TodoDTO ( final TodoEntity entity) {
this . id = entity . getId ();
this . title = entity . getTitle ();
this . done = entity . isDone ();
}
Copy //ResponseDTO.java
package com . soobin . todoWeb . dto ;
import lombok . AllArgsConstructor ;
import lombok . Builder ;
import lombok . Data ;
import lombok . NoArgsConstructor ;
import java . util . List ;
@ Builder
@ NoArgsConstructor
@ AllArgsConstructor
@ Data
public class ResponseDTO < T > {
private String error;
private List < T > data;
}
2.2.3 REST API
REST(Representational State Transfer) API
아키텍처 패턴 : 어떤 반복되는 문제 상황을 해결하는 도구
아키텍처 스타일 : 반복되는 아키텍처 디자인
REST 제약조건
일관적인 인터페이스 (Uniform Interface)
코드-온-디맨드 (Code-On-Demand)
클라이언트-서버
다수의 클라이언트가 리소스를 소비하려고 네트워크를 통해 접근
리소스란 REST API가 리턴할 수 있는 모든 것. (HTML, JSON, Image 등)
상태가 없는 (Stateless)
클라이언트가 서버에 요청을 보낼 때 이전 요청의 영향을 받지 않음
캐시되는 (Cacheable) 데이터
서버에서 리소스를 리턴할 때 캐시가 가능한지 아닌지 명시할 수 있어야 함
HTTP에서는 cache-control이라는 헤더에 리소스의 캐시 여부 명시
일관적인 인터페이스 (Uniform Interface)
시스템 또는 애플리케이션의 리소스에 접근할 때 인터페이스가 일관적이어야 함
레이어 시스템 (Layered System)
클라이언트가 서버에 요청할 때 여러 개의 레이어로 된 서비를 거칠 수 있음
(ex. 인증 서버, 캐싱 서버, 로드 밸런서)
레이어들은 요청과 응답에 어떤 영향을 미치지 않음
클라이언트는 서버의 레이어 존재 유무를 알지 못함
코드-온-디맨드 (Code-On-Demand) (선택사항)
클라이언트는 서버에 코드를 요청할 수 있고 서버가 리턴한 코드를 실행 할 수 있음
REST는 HTTP와 다르다. REST는 HTTP를 이용해 구현하기 쉽고 대부분 그렇게 구현하지만, 엄밀히 말하면 REST는 아키텍처이고, HTTP는 REST 아키텍처를 구현할 때 사용하면 쉬운 프로토콜이다.
2.2.4 컨트롤러 레이어 : 스프링 REST API 컨트롤러
HTTP 요청 처리
HTTP는 GET/POST/DELETE/OPTIONS 등과 같은 메서드와 URI를 이용해 서버에 요청을 보냄
Copy GET / test HTTP / 1.1
Host : localhost : 8080
Content - Type : application / json
Content - Length : 17
{
"id" : 123
}
localhost:8080에 http GET 메서드를 이용해 test라는 리소스를 요청
서버는 자기 주소를 제외한 /{리소스} 부분을 이해하고 또 이 요청이 어떤 HTTP 메서드를 이용했는지 알아야 함
그 후 해당 리소스의 HTTP 메서드에 연결된 메서드 실행
스프링 부트 스타터 웹 (spring-boot-starter-web)
@RestController
이 컨트롤러가 RestController임을 명시
http와 관려된 코드 및 요청/응답 매핑을 스프링이 알아서 해줌
@GetMapping
@GetMapping을 통해 이 메서드의 리소스와 HTTP 메서드를 지정
!!@RequestMapping은 URI 경로에, @GetMapping은 HTTP 메서드에 매핑
(하지만, @GetMapping에서도 URI 경로를 지정할 수 있음)
@PostMapping, @PutMapping, @DeleteMapping 등도 있음
@GetMapping, @PostMapping, @PutMapping, @DeleteMapping dms tmvmfld 4.3부터 지원되기 시작했고, 그 이전에는 아해와 같이 하나의 어노테이션에 HTTP 메서드를 매개변수로 주는 형태로 컨트롤러 메서드를 연결했다.
@RequestMapping(value=”/경로”, method=RequestMethod.GET)
@RequestMapping(value=”/경로”, method=RequestMethod.POST)
@RequestMapping(value=”/경로”, method=RequestMethod.PUT)
@RequestMapping(value=”/경로”, method=RequestMethod.DELETE)
매개변수를 넘겨받는 방법
/test가 아닌 /test/{id}로 PathVariable이나 /test?id=123처럼 요청 매개변수를 받아야 하는 경우
@PathVariable
/{id}와 같이 URI의 경로로 넘어오는 값을 변수로 받을 수 있음
@RequestParam
?id={id}와 같이 요청 매개변수로 넘어오는 값을 변수로 받을 수 있음
@RequestBody
보통 반환하고자 하는 리소스가 복잡할 때 사용
Copy {
"id" : 123 ,
"message" : "Hello ?"
}
문자열보다 복잡한 오브젝트를 리턴하려면 ?
@RestController의 내부에는 두 어노테이션 있음
Copy @ Controller
@ ResponseBody
public @ interface RestController {
. . .
}
@Controller는 @Component로 스프링이 이 클래스의 오브젝트를 생성하고 다른 오브젝트들과의 의존성을 연결한다는 뜻
@ResponseBody는 이 클래스의 메서드가 리턴하는 것은 웹 서비스의 ResponseBody라는 뜻
메서드가 리턴할 때 스프링은 리턴된 오브젝트를 JSON의 형태로 바꾸고 HttpResponse에 담아 반환한다는 뜻
스프링이 오브젝트를 JSON으로 바꾸는 것처럼, 오브젝트를 저장하거나 네트워크를 통해 전달할 수 있도록 변환하는 것을 직렬화라고 하고, 반대의 작업을 역직렬화라고 한다.
ResponseEntity
앞으로 우리가 작성할 컨트롤러는 모두 ResponseEntity를 반환할 예정
HTTP 응답의 바디뿐만 아니라 여러 다른 매개변수들, 이를테면 status나 header를 조작하고 싶을 때 사용
TestController TestRequestDTO ResponseDTO
Copy package com . soobin . todoWeb . controller ;
import com . soobin . todoWeb . dto . ResponseDTO ;
import com . soobin . todoWeb . dto . TestRequestBodyDTO ;
import org . springframework . beans . factory . annotation . Autowired ;
import org . springframework . http . ResponseEntity ;
import org . springframework . web . bind . annotation . * ;
import java . util . ArrayList ;
import java . util . List ;
@ RestController
@ RequestMapping ( "test" )
public class TestController {
@ GetMapping
public String testController () {
return "Hello World !" ;
}
@ GetMapping ( "/testGetMapping" )
public String testControllerWithPath () {
return "Hello World! testGetMapping !!" ;
}
@ GetMapping ( "/{id}" )
public String testControllerWithPathVariables (@ PathVariable (required = false ) int id) {
return "Hello world ! ID :" + id;
}
@ GetMapping ( "/testRequestParam" )
public String testControllerRequestParam (@ RequestParam (required = false ) int id) {
return "Hello world ! ID " + id;
}
@ GetMapping ( "/testRequestBody" )
public String testControllerRequestBody (@ RequestBody TestRequestBodyDTO testRequestBodyDTO) {
return "Hello World ! ID : " + testRequestBodyDTO . getId () + " Message : " + testRequestBodyDTO . getMessage ();
}
@ GetMapping ( "/testResponseBody" )
public ResponseDTO < String > testControllerResponseBody () {
List < String > list = new ArrayList <>();
list . add ( "Hello World! I'm ResponseDTO" );
list . add ( "hello hello" );
ResponseDTO < String > response = ResponseDTO . < String > builder() . data (list) . build ();
return response;
}
@ GetMapping ( "/testResponseEntity" )
public ResponseEntity < ? > testControllerResponseEntity () {
List < String > list = new ArrayList <>();
list . add ( "Hello World ! I'm ResponseEntity." );
ResponseDTO < String > response = ResponseDTO . < String > builder() . data (list) . build ();
return ResponseEntity . ok () . body (response);
}
@ GetMapping ( "/todo" )
public ResponseEntity < ? > testTodo () {
String str = service . testService ();
List < String > list = new ArrayList <>();
list . add (str);
ResponseDTO < String > response = ResponseDTO . < String > builder() . data (list) . build ();
return ResponseEntity . ok () . body (response);
};
Copy package com . soobin . todoWeb . dto ;
import lombok . Data ;
@ Data
public class TestRequestBodyDTO {
private int id;
private String message;
Copy package com . soobin . todoWeb . dto ;
import lombok . AllArgsConstructor ;
import lombok . Builder ;
import lombok . Data ;
import lombok . NoArgsConstructor ;
import java . util . List ;
@ Builder
@ NoArgsConstructor
@ AllArgsConstructor
@ Data
public class ResponseDTO < T > {
private String error;
private List < T > data;
}
2.2.5 서비스 레이어 : 비즈니스 로직
서비스레이어
컨트롤러와 퍼시스턴스 사이에서 비즈니스 로직을 수행
HTTP와 심닐히 연관된 컨트롤러에서 분리돼 있고 또 데이터베이스와 긴밀히 연관된 퍼시스턴스와도 분리돼 있어서, 개발하고자하는 로직에 집중할 수 있음
Copy package com . soobin . todoWeb . service ;
import org . springframework . stereotype . Service ;
@ Service
public class TodoService {
public String testService () {
return "Test Service" ;
}
}
Copy package com . soobin . todoWeb . controller ;
import org . springframework . beans . factory . annotation . Autowired ;
// 여러 다른 패키지들
@ RestController
@ RequestMapping ( "test" )
public class TestController {
@ Autowired
private TodoService service;
@ GetMapping ( "/test" )
public ResponseEntity < ? > testTodo () {
String str = service . testService ();
List < String > list = new ArrayList <>();
list . add (str);
ResponseDTO < String > response = ResponseDTO . < String > builder() . data (list) . build ();
return ResponseEntity . ok () . body (response);
}
}
@Service
내부에 @Component 어노테이션을 갖고 있음
기능적으로 비즈니스 로직을 수행하는 서비스 레이어임을 알려줌
@RestController도 내부에 @Component 어노테이션을 갖고있고, @Service, @RestController 모두 자바 빈이고 스프링이 관리한다.
스프링은 TodoController 오브젝트를 생성할 때 TodoController 내부에 선언된 TodoService에 @Autowired 어노테이션이 붙어 있다는 것을 확인하고, 알아서 빈을 찾은 다음 그 빈을 인스턴스 멤버 변수에 연결한다.
2.2.6 퍼시스턴스 레이어 : 스프링 데이터 JPA
시스턴스 레이어 : 스프링 데이터 JPA
JDBC 드라이버
자바에서 데이터베이스에 연결할 수 있도록 도와주는 라이브러리
MySQL 클라이언트도 내부에서 JDBC/ODBC 등의 드라이버를 사용한다
Copy //데이터베이스 콜 스니팻
String sqlSlectAlltodo = "SELECT * FROM Todo where USER_ID = " + request . getUserId ();
String connectionUrl = "jdbc:mysql://mydb:3306/todo" ;
try {
/* (1) 데이터베이스에 연결 */
Connection conn = DriverManager . getConnection (connectionUrl , "username" , "password" );
/* (2) SQL 쿼리 준비 */
PreparedStatement ps = conn . prepareStatement (sqlSelectAllTodos);
/* (3) 쿼리 실행 */
RequltSet rs = ps . excuteQuery ();
/* (4) 결과를 오브젝트로 파싱 */
while ( rs . next ()) {
long id = rs . getString ( "id" );
String title = rs . getString ( "title" );
Boolean isDone = rs . getBeoolean ( "done" );
todos . add ( new Todo(id , title , idsdDone) );
}
} catch ( SQLException e) {
// handle the exception
}
JDBC 커넥션인 Connectio을 이용해 데이터베이스에 연결
sqlSelectAllTodos에 작성된 SQL을 실행
ResultSet이라는 클래스에 결과를 담아옴
while문 내부에서 ResultSet을 Todo 오브젝트로 바꿔줌
이런 작업을 ORM 이라고 함
데이터베이스 테이블을 자바 내에서 사용하려면 위와같은 작업을 엔티티마다 해줘야 함
이런 ORM 작업을 집중적으로 해주는 DAO(Data Access Object) 클래스를 작성
보통 생성, 검색, 수정, 삭제 같은 기본적인 오퍼레이션들을 엔티티마다 작성해줌
시간이 흐르며 이런 반복 작업을 줄일 수 있는 Hibernate같은 ORM 프레임워크가 등장했고
더 나아가 JPA나 스프링 데이터 JPA 같은 도구들이 개발됐다.
JPA
반복해서 데이터베이스 쿼리를 보내 ResultSet을 파싱해야 하는 작업의 노고를 덜어줌
JPA는 스펙 (스펙은 ‘JPA의 구현을 위해서 이런 이런 기능을 작성해라’라고 말해주는 지침 문서)
JPA란 자바에서 데이터베이스 접근, 저장, 관리에 필요한 스펙
이 스펙을 구현하는 구현자를 JPA Provider라고 하는데, 그 중 대표적인 것이 Hibernate 임
스프링 데이터 JPA vs JPA
JPA를 더 사용하기 쉽게 더와주는 스프링의 프로젝트
기술적으로는 추상화 했다고 하며, 이는 사용하기 쉬운 인터페이스를 제공한다는 뜻
JpaRepository는 그런 인텉페이스 중 하나
스프링 데이터 JPA를 사용하려면 spring-boot-starter-jpa 라이브러리가 필요
H2
로컬 환경에서 메모리상에 데이터베이스를 구축해 줌
build.gradle에 h2를 디펜던시로 설정하면 @SpringBootApplication 어노테이션의 일부로 스프링이 알아서 애플리케이션을 H2 데이터베이스에 연결
엔티티 클래스
보통 데이터베이스 테이블마다 그에 상응하는 엔티티 클래스가 존재
그 자체가 테이블을 정의해야 함
ORM이 엔티티를 보고 어떤 테이블의 어떤 필드에 매핑해야 하는지 알 수 있어야 함
어떤 필드가 기본 키인지, 외래 키인지도 구분할 수 있어야 함
이런 데이터베이스 테이블 스키마에 관한 정보는 javax.persistence가 제공하는 JPA 관련 어노테이션을 이용해 정의함
자바 클래스를 엔티티로 정의할 때 주의할 점
클래스에는 매개변수가 없는 생성자, NoArgsConstructor가 필요
Copy package com . soobin . todoWeb . model ;
import lombok . AllArgsConstructor ;
import lombok . Builder ;
import lombok . Data ;
import lombok . NoArgsConstructor ;
import org . hibernate . annotations . GenericGenerator ;
import javax . persistence . Entity ;
import javax . persistence . GeneratedValue ;
import javax . persistence . Id ;
import javax . persistence . Table ;
@ Builder
@ NoArgsConstructor
@ AllArgsConstructor
@ Data
@ Entity
@ Table (name = "Todo" )
public class TodoEntity {
@ Id
@ GeneratedValue (generator = "system-uuid" )
@ GenericGenerator (name = "system-uuid" , strategy = "uuid" )
private String id;
private String userId;
private String title;
private boolean done;
}
@Entity
@Entity(”TodoEntity”)처럼 매개변수를 넣어 엔티티에 이름을 부여할 수 있음
@Table
해당 엔티티가 데이터베이스의 Todo 테이블에 매핑된다는 뜻
@Table을 추가하지 않거나 추가해도name을 명시하지 않는다면 @Entity의 이름을 테이블 이름으로 간주
@Entity에 이름을 지정하지 않은 경우 클래스의 이름을 테이블 이름으로 간주
@GeneraredValue
매개변수인 generator로 어떤 발식으로 ID를 생성할지 지정 가능
위에 사용한 system-uuid는 @GenericGenerator에 정의된 generator의 이름
@GenericGenerator
Hibernate가 제공하는 기본 Generator가 아니라 나만의 Generator를 사용하고 싶은 경우 이용
기본 Generator로는 INCREMENTAL, SEQUENCE, IDENTITY 등이 있음
이렇게 uuid를 사용하는 “system-uuid”라는 이름의 GenericGenerator를 만들어, 이 Generator는 @GeneratedValue가 참조해 사용한다.
Copy package com . soobin . todoWeb . persistence ;
import com . soobin . todoWeb . model . TodoEntity ;
import org . springframework . data . jpa . repository . JpaRepository ;
import org . springframework . stereotype . Repository ;
@ Repository
public class TodoRepository extends JpaRepository < TodoEntity , String > {
}
JpaRepository
이 인터페이스를 사용하려면 새 인터페이스를 작성해 JpaRepository를 확장해야 함
JpaRepository<T, ID>가 Genetic Type을 받음
Copy //JpaRepositoryBean
public interface JpaRepository < T , ID > extends PagingAndSortingRepository < T , ID > , QueryByExampleExcutor < T > {
List < T > findAll ();
List < T > findAll ( Sort var1);
List < T > findAllById ( Iteravle < ID > var1);
void flush ();
< S extends T > S savedAndFlush ( S var1);
void deleteInBatch ( Iterabel < T > Vvar1);
void deleteAllBatch ();
T getOne ( ID var1);
< S extends T > List < S > findAll ( Example < S > var1);
< S extends T > List < S > findAll ( Example < S > var1 , Sort var2);
}
기본적인 쿼리가 아닌 경우 ?
Copy @ Repository
public interface TodoRepository extends JpaRepository < TodoEntity , Long > {
List < TodoEntity > findByUserId ( String userId);
}
스프링 데이터 JPA가 메서드 이름을 파싱해서 SELECT * FROM TodoRepository WHERE userId = {userId}
와 같은 쿼리를 작성해 실행
메서드 이름은 쿼리, 매개변수는 쿼리의 where문에 들어갈 값을 의미
더 복잡한 쿼리의 경우
Copy @ Repository
public interface TodoRepository extends JpaRepository < TodoEntity , Long > {
// ?1은 메서드의 매개변수의 순서 위치
@ Query ( "select * from Todo t where t.userId = ?1" )
List < TodoEntity > findByUserId ( String userId);
}
2.3 서비스 개발 및 실습
2.3.1 Create Todo
구현 과정 : 퍼시스턴스 → 서비스 → 컨트롤러
퍼시스턴스 구현
Copy package com . soobin . todoWeb . persistence ;
import com . soobin . todoWeb . model . TodoEntity ;
import org . springframework . data . jpa . repository . JpaRepository ;
import org . springframework . stereotype . Repository ;
import java . util . List ;
@ Repository
public interface TodoRepository extends JpaRepository < TodoEntity , String > {
List < TodoEntity > findByUserId ( String userId);
}
서비스 구현
Copy package com . soobin . todoWeb . service ;
import com . soobin . todoWeb . dto . TodoDTO ;
import com . soobin . todoWeb . model . TodoEntity ;
import com . soobin . todoWeb . persistence . TodoRepository ;
import lombok . extern . slf4j . Slf4j ;
import org . springframework . beans . factory . annotation . Autowired ;
import org . springframework . stereotype . Service ;
import java . util . List ;
import java . util . Optional ;
@ Slf4j
@ Service
public class TodoService {
@ Autowired
private TodoRepository repository;
public List < TodoEntity > create ( final TodoEntity entity) {
validate(entity) ;
repository . save (entity);
return repository . findByUserId ( entity . getUserId ());
}
private void validate ( final TodoEntity entity) {
if (entity == null ) {
log . warn ( "Entity cannot be null" );
throw new RuntimeException( "Entity cannot be null" ) ;
}
if ( entity . getUserId () == null ) {
log . warn ( "Unknown user." );
throw new RuntimeException( "Unknown user." ) ;
}
}
}
컨트롤러 구현
HTTP 응답을 반환할 때 DTO를 사용하는 이유
컨트롤러는
사용자에게서 TodoDTO를 요청 바디로 넘겨받고
TodoService의 create()가 리턴하는 TodoEntity를
Copy package com . soobin . todoWeb . controller ;
import com . soobin . todoWeb . dto . ResponseDTO ;
import com . soobin . todoWeb . dto . TodoDTO ;
import com . soobin . todoWeb . model . TodoEntity ;
import com . soobin . todoWeb . service . TodoService ;
import org . apache . coyote . Response ;
import org . springframework . beans . factory . annotation . Autowired ;
import org . springframework . http . ResponseEntity ;
import org . springframework . web . bind . annotation . * ;
import java . util . List ;
import java . util . stream . Collectors ;
@ RestController
@ RequestMapping ( "todo" )
public class TodoController {
@ Autowired
private TodoService service;
@ PostMapping
public ResponseEntity < ? > createTodo (@ RequestBody TodoDTO dto) {
try {
String temporaryUserId = "temp-user" ;
TodoEntity entity = TodoDTO . toEntity (dto);
entity . setId ( null );
//임시 사용자 아이디 설정
entity . setUserId (temporaryUserId);
List < TodoEntity > entities = service . create (entity);
List < TodoDTO > dtos = entities . stream () . map (TodoDTO ::new ) . collect ( Collectors . toList ());
ResponseDTO < TodoDTO > response = ResponseDTO . < TodoDTO > builder() . data (dtos) . build ();
return ResponseEntity . ok () . body (response);
} catch ( Exception e) {
String error = e . getMessage ();
ResponseDTO < TodoDTO > response = ResponseDTO . < TodoDTO > builder() . error (error) . build ();
return ResponseEntity . badRequest () . body (response);
}
}
}
dto/TodoDTO dto/ResponseDTO model/TodoEntity
Copy package com . soobin . todoWeb . dto ;
import com . soobin . todoWeb . model . TodoEntity ;
import lombok . AllArgsConstructor ;
import lombok . Data ;
import lombok . NoArgsConstructor ;
@ NoArgsConstructor
@ AllArgsConstructor
@ Data
public class TodoDTO {
private String id;
private String title;
private boolean done;
public TodoDTO ( final TodoEntity entity) {
this . id = entity . getId ();
this . title = entity . getTitle ();
this . done = entity . isDone ();
}
public static TodoEntity toEntity ( final TodoDTO dto) {
return TodoEntity . builder ()
. id ( dto . getId ())
. title ( dto . getTitle ())
. done ( dto . isDone ())
. build ();
}
}
Copy package com . soobin . todoWeb . dto ;
import lombok . AllArgsConstructor ;
import lombok . Builder ;
import lombok . Data ;
import lombok . NoArgsConstructor ;
import java . util . List ;
@ Builder
@ NoArgsConstructor
@ AllArgsConstructor
@ Data
public class ResponseDTO < T > {
private String error;
private List < T > data;
}
Copy package com . soobin . todoWeb . model ;
import lombok . AllArgsConstructor ;
import lombok . Builder ;
import lombok . Data ;
import lombok . NoArgsConstructor ;
import org . hibernate . annotations . GenericGenerator ;
import javax . persistence . Entity ;
import javax . persistence . GeneratedValue ;
import javax . persistence . Id ;
import javax . persistence . Table ;
@ Builder
@ NoArgsConstructor
@ AllArgsConstructor
@ Data
@ Entity
@ Table (name = "Todo" )
public class TodoEntity {
@ Id
@ GeneratedValue (generator = "system-uuid" )
@ GenericGenerator (name = "system-uuid" , strategy = "uuid" )
private String id;
private String userId;
private String title;
private boolean done;
}
2.3.2 Retrieve Todo
서비스 구현
Copy package com . soobin . todoWeb . service ;
import com . soobin . todoWeb . dto . TodoDTO ;
import com . soobin . todoWeb . model . TodoEntity ;
import com . soobin . todoWeb . persistence . TodoRepository ;
import lombok . extern . slf4j . Slf4j ;
import org . springframework . beans . factory . annotation . Autowired ;
import org . springframework . stereotype . Service ;
import java . util . List ;
import java . util . Optional ;
@ Slf4j
@ Service
public class TodoService {
@ Autowired
private TodoRepository repository;
public List < TodoEntity > findByUserId ( final String userId) {
return repository . findByUserId (userId);
}
}
컨트롤러 구현
Copy package com . soobin . todoWeb . controller ;
import com . soobin . todoWeb . dto . ResponseDTO ;
import com . soobin . todoWeb . dto . TodoDTO ;
import com . soobin . todoWeb . model . TodoEntity ;
import com . soobin . todoWeb . service . TodoService ;
import org . springframework . beans . factory . annotation . Autowired ;
import org . springframework . http . ResponseEntity ;
import org . springframework . web . bind . annotation . * ;
import java . util . List ;
import java . util . stream . Collectors ;
@ RestController
@ RequestMapping ( "todo" )
public class TodoController {
@ Autowired
private TodoService service;
@ GetMapping ( "/findById" )
public ResponseEntity < ? > findById () {
String temporaryUserId = "temp-user" ;
List < TodoEntity > entities = service . findByUserId (temporaryUserId);
List < TodoDTO > dtos = entities . stream () . map (TodoDTO ::new ) . collect ( Collectors . toList ());
ResponseDTO < TodoDTO > response = ResponseDTO . < TodoDTO > builder() . data (dtos) . build ();
return ResponseEntity . ok () . body (response);
}
}
2.3.3 Update Todo
서비스 구현
Copy package com . soobin . todoWeb . service ;
import com . soobin . todoWeb . dto . TodoDTO ;
import com . soobin . todoWeb . model . TodoEntity ;
import com . soobin . todoWeb . persistence . TodoRepository ;
import lombok . extern . slf4j . Slf4j ;
import org . springframework . beans . factory . annotation . Autowired ;
import org . springframework . stereotype . Service ;
import java . util . List ;
import java . util . Optional ;
@ Slf4j
@ Service
public class TodoService {
@ Autowired
private TodoRepository repository;
public List < TodoEntity > update ( final TodoEntity entity) {
validate(entity) ;
final Optional < TodoEntity > original = repository . findById ( entity . getId ());
original . ifPresent (todo -> {
todo . setTitle ( entity . getTitle ());
todo . setDone ( entity . isDone ());
repository . save (todo);
});
return findByUserId( entity . getUserId()) ;
}
private void validate ( final TodoEntity entity) {
if (entity == null ) {
log . warn ( "Entity cannot be null" );
throw new RuntimeException( "Entity cannot be null" ) ;
}
if ( entity . getUserId () == null ) {
log . warn ( "Unknown user." );
throw new RuntimeException( "Unknown user." ) ;
}
}
}
컨트롤러 구현
Copy package com . soobin . todoWeb . controller ;
import com . soobin . todoWeb . dto . ResponseDTO ;
import com . soobin . todoWeb . dto . TodoDTO ;
import com . soobin . todoWeb . model . TodoEntity ;
import com . soobin . todoWeb . service . TodoService ;
import org . springframework . beans . factory . annotation . Autowired ;
import org . springframework . http . ResponseEntity ;
import org . springframework . web . bind . annotation . * ;
import java . util . List ;
import java . util . stream . Collectors ;
@ RestController
@ RequestMapping ( "todo" )
public class TodoController {
@ Autowired
private TodoService service;
@ PutMapping
public ResponseEntity < ? > updateTodo (@ RequestBody TodoDTO dto) {
String temporaryUserId = "temp-user" ;
TodoEntity entity = TodoDTO . toEntity (dto);
entity . setUserId (temporaryUserId);
List < TodoEntity > entities = service . update (entity);
List < TodoDTO > dtos = entities . stream () . map (TodoDTO ::new ) . collect ( Collectors . toList ());
ResponseDTO < TodoDTO > response = ResponseDTO . < TodoDTO > builder() . data (dtos) . build ();
return ResponseEntity . ok () . body (response);
}
}
2.3.4 Delete Todo
서비스 구현
Copy package com . soobin . todoWeb . service ;
import com . soobin . todoWeb . dto . TodoDTO ;
import com . soobin . todoWeb . model . TodoEntity ;
import com . soobin . todoWeb . persistence . TodoRepository ;
import lombok . extern . slf4j . Slf4j ;
import org . springframework . beans . factory . annotation . Autowired ;
import org . springframework . stereotype . Service ;
import java . util . List ;
import java . util . Optional ;
@ Slf4j
@ Service
public class TodoService {
@ Autowired
private TodoRepository repository;
public List < TodoEntity > deleteById ( final TodoEntity entity) {
validate(entity) ;
try {
repository . delete (entity);
} catch ( Exception e) {
log . error ( "error deleting entity " , entity . getId () , e);
throw new RuntimeException( "error deleting entity " + entity . getId()) ;
}
return findByUserId( entity . getUserId()) ;
}
private void validate ( final TodoEntity entity) {
if (entity == null ) {
log . warn ( "Entity cannot be null" );
throw new RuntimeException( "Entity cannot be null" ) ;
}
if ( entity . getUserId () == null ) {
log . warn ( "Unknown user." );
throw new RuntimeException( "Unknown user." ) ;
}
}
}
컨트롤러
Copy package com . soobin . todoWeb . controller ;
import com . soobin . todoWeb . dto . ResponseDTO ;
import com . soobin . todoWeb . dto . TodoDTO ;
import com . soobin . todoWeb . model . TodoEntity ;
import com . soobin . todoWeb . service . TodoService ;
import org . springframework . beans . factory . annotation . Autowired ;
import org . springframework . http . ResponseEntity ;
import org . springframework . web . bind . annotation . * ;
import java . util . List ;
import java . util . stream . Collectors ;
@ RestController
@ RequestMapping ( "todo" )
public class TodoController {
@ Autowired
private TodoService service;
@ DeleteMapping
public ResponseEntity < ? > deleteTodo (@ RequestBody TodoDTO dto) {
try {
String temporaryId = "temp-user" ;
TodoEntity entity = TodoDTO . toEntity (dto);
entity . setUserId (temporaryId);
List < TodoEntity > entities = service . deleteById (entity);
List < TodoDTO > dtos = entities . stream () . map (TodoDTO ::new ) . collect ( Collectors . toList ());
ResponseDTO < TodoDTO > response = ResponseDTO . < TodoDTO > builder() . data (dtos) . build ();
return ResponseEntity . ok () . body (response);
} catch ( Exception e) {
String error = e . getMessage ();
ResponseDTO < TodoDTO > response = ResponseDTO . < TodoDTO > builder() . error (error) . build ();
return ResponseEntity . badRequest () . body (response);
}
}
}