[React.js, 스트링부트, AWS로 배우는 웹개발 101] 2장 백엔드 개발-2
웹개발 101 - 2장 백엔드 개발
[React.js, 스트링부트, AWS로 배우는 웹개발 101] 2장 백엔드 개발-1
[React.js, 스트링부트, AWS로 배우는 웹개발 101] 2장 백엔드 개발-2
2.2 백엔드 서비스 아키텍처
레이어드 아키텍처, Layered Architecture
레이어드 아키텍처(Layered Architecture) 패턴은 애플리케이션을 구성하는 요소들을 수평으로 나눠 관리하는 것이다.
하나의 클래스에 하나의 메서드 안에 전부 구현하는 것이 아닌, 메서드를 쪼개 작은 메서드로 나눈다.
기본적인 레이어드 아키텍처에서는 상위 레이어가 자신의 바로 하위 레이어를 사용한다. 하지만 반드시 하위 레이어만 사용해야 하는 것은 아니다.
서비스를 따로 레이어의 클래스를 분리하면, 수정이 필요한 경우 서비스 레이어만 고치면 된다.
자바로 된 비즈니스 애플리케이션의 클래스는 두 가지 종류로 나눌 수 있다. 첫번째는 기능을 수행하는 클래스이고, 이는 Controller Service, Persistence처럼 로직을 수행하는 클래스이다. 두번째는 데이터를 담는 클래스이고, 이는 아무 기능 없이 데이터베이스에서 반환된 비즈니스 데이터를 담기 위한 클래스이다. 그러한 클래스들을 기능에 따라 Entity, Model, DTO(Data Transfer Obejct)로 부른다.
모델(Model)은 비즈니스 데이터를 담는 역할과 데이터베이스의 테이블과 스키마를 표현하는 두 역할을 한다.
TodoEntity.java
// TodoEntity.java
package com.example.demo.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; // Todo 타이틀 예) 운동하
private boolean done; // true - todo를 완료한 경우(checked)
}
TodoEntity.java에서 사용된 어노테이션
어노테이션 | 설명 |
@Builder | 오브젝트 생성을 위한 디자인 패턴 중 하나이다, 생성자 매개변수의 순서를 기억할 필요가 없다. |
@NoArgsConstructor | 매개변수가 없는 생성자를 구현해준다. |
@AllArgsConstructor | 클래스의 모든 멤버변수를 매개변수로 받는 생성자를 구현해준다. |
@Data | 클래스 멤버 변수의 Getter/Setter 메서드를 구현해준다. |
DTO, Data Transition Object
데이터를 전달하기 위해 사용하는 오브젝트인 DTO로 변환해 리턴한다.
사용자는 이 클래스를 이용해 Todo 아이템을 생성/수정/삭제할 예정이다.
DTO로 변환하는 이유는?
- 비즈니스 로직을 캡슐화하기 위함이다.
- DTO 처럼 다른 오브젝트로 바꿔 반환하면, 외부 사용자에게 서비스 내부의 로직, 데이터베이스 구조 등을 숨길 수 있다.
- 클라이언트가 필요한 정보를 모델이 전부 포함하지 않는 경우가 많기 때문이다.
- 서비스에 에러가 발생했을 때, 메시지를 모델에 담는 것은 애매하다. 그렇기에 DTO에 에러 메시지 필드를 선언하고 DTO에 포함하면 된다.
// TodoDTO.java
package com.example.demo.dto;
import com.example.demo.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();
}
}
위 TodoDTO.java에는 사용자를 구별하기 위한 userId가 존재하지 않는다. 이는 고유한 식별자로, 숨길 수 있다면 숨기는 것이 보안상 알맞기 때문에 DTO에는 포함하지 않는다.
// ResponseDTO.java
// 이는 HTTP 응답으로 사용할 DTO이다.
package com.example.demo.dto;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class ResponseDTO<T> {
private String error;
private List<T> data;
}
- TodoDTO 뿐만 아니라 이후 다른 모델의 DTO도 이 java를 통해 리턴할 수 있도록 자바 Generic을 이용함
- List<T>를 통해 데이터를 리스트로 반환할 수 있음
REST API, Representational State Transfer의 약자로 아키텍처 스타일이다.
패턴은 어떤 반복되는 문제 상황을 해결하기 위한 도구이고, 아키텍처 스타일은 반복되는 아키텍처 디자인을 의미한다.
이는 6가지 제약 조건으로 구성된다.
- 클라이언트-서버, Client - Server
- 다수의 클라이언트가 리소스를 소비하기 위해 네트워크를 통해 서버에 접근하는 구조를 의미한다.
- 이때, 리소스는 Rest API가 리턴할 수 있는 모든 것을 의미한다. ex) HTML, JSON, 이미지
- 상태가 없는, Stateless
- 클라이언트가 서버에 요청을 보낼 때, 이전 요청의 영향을 받지 않음을 의미
- /login으로 로그인 요청 후, /page로 넘어가서 리소스를 불러올 때, 이전 요청에서 login한 사실을 서버가 알고 있어야 하는 것은 상태가 있는(Stateful) 아키텍처이다. 서버가 그 사실을 알지 못한다면 상태가 없는(Stateless) 아키텍처이다.
- HTTP는 기본적을 상태가 없는 프로토콜이다. 따라서 HTTP를 사용하는 웹 애플리케이션은 기본적으로 상태가 없는 구조를 따른다.
- 캐시 가능한 데이터, Cacheable
- 서버에서 리소스를 리턴할 때 캐시가 가능한지 아닌지에 대해 명시할 수 있어야 한다. HTTP에서는 cache-control이라는 헤더에 리소스의 캐시 여부를 명시할 수 있다.
- 일관적인 인터페이스, Uniform Interface
- 시스템 또는 애플리케이션의 리소스에 접근하기 위한 인터페이스가 일관적이어야 한다.
- 리소스에 접근하는 방식, 요청의 형식, 응답의 형식이 애플리케이션 전반에 걸쳐 URI, 요청의 형태와 응답의 형태가 일관적이야 한다는 것이 일관적인 인터페이스 방침이다.
- 서버가 리턴하는 응답에는 해당 리소스를 수정하기 위한 충분한 정보가 있어야 한다.
- 레이어 시스템, Layered System
- 클라이언트가 서버에 요청을 날릴 때, 여러 개의 레이어로 된 서버를 거칠 수 있다.
- 이때 사이사이에 있는 레이어들은 요청과 응답에 어떤 영향을 미치지 않으며 클라이언트는 서버의 레이어 존재 유무를 알지 못한다.
- 코드-온-디맨드(선택사항), Code-On-Demand
- 클라이언트는 서버에 코드를 요청할 수 있고, 서버가 리턴한 코드를 실행할 수 있다.
- REST는 HTTP와 다르다. REST는 아키텍처이고, HTTP는 REST 아키텍처를 구현할 때 사용하기 쉬운 프로토콜이다.
컨트롤러 레이어: 스프링 REST API 컨트롤러
HTTP는 GET/POST/PUT/DELETE/OPTIONS 등과 같은 메서드와 URI를 이용해 서버에 HTTP 요청을 보낼 수 있다.
package com.example.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
// 이 어노테이션을 이용해 이 컨트롤러가 RestController임을 명시한다.
@RestController // 이를 이용하면 http 관련된 코드 및 요청/응답 매핑을 스프링이 알아서 해준다.
@RequestMapping("test") // 리소스
public class TestController {
// @RequestMapping에서 URI 경로 지정
@GetMapping
public String testController() {
return "Hello World!";
}
// @GetMapping에서 URI 경로 지정
@GetMapping("/testGetMapping")
public String testControllerWithPath() {
return "Hello World! testGetMapping";
}
}
@GetMapping 어노테이션을 이용해 이 메서드의 리소스와 HTTP 메서드를 지정한다. 클라이언트가 이 리소스에 대해 GET 메서드로 요청하면, @GetMapping에 연결된 컨트롤러가 실행된다.
URI 경로 지정(@RequestMapping이 있는 경우)
- @RequestMapping에서 지정 ex)@RequestMapping("/test") 경우에는, http://localhost:8080/test에 연결된다.
- @GetMappingd에서 지정 ex)@GetMapping("/testGetMapping") 경우에는, http://localhost:8080/test/testGetMapping에 연결된다.
- @RequestMapping이 없는 경우에는, http://localhost:8080/testGetMapping에 연결된다.
@RestController
public class TestController {
@GetMapping("/testGetMapping")
public String testControllerWithPath() {
return "Hello World! testGetMapping";
}
}
매개변수를 넘겨받는 방법
/test/{id}처럼 PathVariable이나 /test?id=123처럼 요청 매개변수를 받아야 한다면 어떻게 해야할까?
@PathVariable
이를 이용하면 /{id}와 같이 URI의 경로로 넘어오는 값을 변수로 받아올 수 있다.
import org.springframework.web.bind.annotation.PathVariable;
@GetMapping("/{id}")
public String testControllerWithPathVariables(@PathVariable(required = false) int id){
return "Hello World! ID" + id;
}
@GetMapping("/{id}")의 매개변수 /{id}는 경로로 들어오는 임의의 숫자 또는 문자를 변수 id에 매핑하라는 뜻이다.
(required = false)은 이 매개변수가 꼭 필요한 건 아니라는 뜻이다. 따라서 꼭 id = 123을 명시하지 않아도 에러가 발생하지 않는다.
@RequestBody
반환하고자 하는 리소스가 복잡할 때 사용한다.
@ResponseBody
메시지가 리턴할 때 스프링은 리턴된 오브젝트를 JSON의 형태로 바꾸고 HttpResponse에 담아 반환한다.
@RestController는 안을 들여다보면 크게 두 어노테이션의 조합으로 이루여져 있다. 하나는 @Controller이고, 다른 하나는 @ResponseBody이다.
TodoController.java
package com.example.demo.controller;
import java.util.ArrayList;
import java.util.List;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("todo")
public class TodoController {
// testTodo 메서드 작성하기
public ResponseEntity<?> testTodo(){
List<String> list = new ArrayList<>();
}
}
2.2.5 서비스 레이어 : 비즈니스 로직
서비스 레이어는 Controller와 Persistence 사이에서 비즈니스 로직을 수행한다.
package com.example.demo.controller;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.demo.dto.ResponseDTO;
import com.example.demo.service.TodoService;
@RestController
@RequestMapping("todo")
public class TodoController {
@Autowired
private TodoService service;
// testTodo 메서드 작성하기
@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);
}
}
퍼시스턴스 레이어 : 스프링 데이터 JPA
JDBC 드라이버는 자바에서 데이터베이스에 연결할 수 있도록 도와주는 라이브러리이다.
데이터베이스와 스프링 데이터 JPA 설정
스프링 데이터 JPA를 사용하기 위해선 spring-boot-starter-jpa 라이브러리가 필요하다.
build.gradle에서 추가되어 있어야 하는 것들(하지만, Spring Initializr에서 프로젝트 생성할때 추가했었다.) -> 혹시 모르니 확인
h2 디펜던시
- H2는 In-Memory 데이터베이스로, 로컬 환경에서 메모리 상에 데이터베이스를 구축해준다. 그래서 개발자들이 따로 데이터베이스 서버를 구축하는데 시간을 들일 필요가 없어, 초기 개발 시 많이 사용한다.
- runtimeOnly 'com.h2database:h2'
spring-boot-starter dependency 디펜던시
- 스프링 데이터 JPA를 사용하기 위해선 spring-boot-starter-jpa 라이브러리가 필요하다.
- h2가 동작하는지, 스프링 데이터 JPA가 동작하는지는 애플리케이션 실행 시 출력되는 로그를 보면 확인할 수 있다.
implementation 'org.springframework.boot:spring-boot-startre-data-jpa'
TodoEntity.java
보통 데이터베이스 테이블마다 그에 상응하는 엔티티 클래스가 존재한다. 이 책에서의 프로젝트에서는 Todo 테이블에 상응하는 TodoEntity가 존재한다. 데이터베이스 테이블 스키마에 관한 정보는 Javax.persistence가 제공하는 JPA 관련 어노테이션을 이용해 정의한다.
자바 클래스를 엔티티로 정의할 때 주의해야 하는 점
- 클래스에는 매개변수가 없는 생성자, NoArgsConstructor가 필요함
- Getter/Setter가 필요함
- 기본키(Primary Key)를 지정해줘야 함
TodoRepository.java
JpaRepository는 기본적인 데이터베이스 오퍼레이션 인터페이스를 제공한다. save, findById, findAll 등이 기본적으로 제공되는 인터페이스에 해당한다. 구현은 스프링 데이터 JPA가 실행 시에 알아서 해준다. 따라서 save 메서드를 구현하기 위해 "Insert into ..."와 같은 sql 쿼리를 짤 필요가 없다.
메서드 이름을 작성하는 법은 아래 링크의 레퍼런스를 통해 확인할 수 있다.
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation
Spring Data JPA - Reference Documentation
Example 119. Using @Transactional at query methods @Transactional(readOnly = true) interface UserRepository extends JpaRepository { List findByLastname(String lastname); @Modifying @Transactional @Query("delete from User u where u.active = false") void del
docs.spring.io