백엔드 서비스 아키텍처란?
레이어드 아키텍처 패턴은 스프링 프로젝트를 분리해서 사용자와 서버 프로그램이 유기적인 소통이 가능하도록 한다. 프로젝트 내부에서 어떻게 코드를 관리할 것인가에 대한 지침을 마련해준다. 먼저 REST 아키텍쳐 스타일에 따라 구현된 서비스를 RESTful 서비스라고 할 수 있다.
레이어드 아키텍쳐 패턴은 애플리케이션을 구성하는 요소들을 수평으로 나눠서 관리하는 것이다. 애플리케이션을 레이어드로 구분하고 한개의 레이어 안에는 하나나의 클래스, 메소드를 구현해놓는 것이다. 애플리케이션을 만드는 개발자의 역량은 얼마나 코드를 잘게 쪼개느냐에 있다.
레이어드 아키텍처 패턴은 층을 가지고 있다. 한개의 레이어는 자기보다 하위의 레이어만 사용하게 된다. 스프링부트 애플리케이션의 레이어는 컨트롤러, 서비스, 퍼시스턴스, DB 4개의 레이어로 구성되어 있다. 먼저 사용자가 보낸 요청을 컨트롤러가 받는다. 컨트롤러는 서비스, 퍼시스턴스로 요청을 보내고 최종적으로 DB에 접근해서 데이터를 가져와 사용자에게 응당합게 된다.
자바로 된 비즈니스 애플리케이션은 기능을 수행하는 클래스와 데이터를 담는 클래스로 구분된다. 컨트롤러, 서비스, 퍼시스턴스 처럼 기능을 수행하게 된다.
데이터를 담는 클래스 Dto, Model, Entity
모델은 비즈니스 로직의 데이터를 담는 역할을 담당한다. 엔티티 Entity는 데이터베이스의 테이블과 스키마를 표현하는 역할을 담당한다. 데이터를 전달하는데 사용하는 오브젝트인 Dto(Data Transfer Object)는 클라이언트와 통신할 때 사용하는 데이터 오브젝트다. DTO를 사용하면 비즈니스 로직을 캡슐화 할 수 있고, 클라이언트가 필요한 정보를 모두 담을 수 있게 된다.
REST API 아키텍처
REST 클라이언트 서버 상에서 레이어 시스템으로 구성된 아키텍처 스타일이다. 아키텍처 패턴이 반복되는 문제를 해결하기 위한 도구인 반면, 아키텍처 스타일은 아키텍처 디자인을 말한다. REST(Representational State Transfer)은 아키텍처 스타일은 6가지 제약조건으로 구성된다.
- 클라이언트-서버(Client - Server)
- 상태가 없음(stateless)
- 캐시되는 데이터(Cashable Data)
- 일관적인 인터페이스(Uniform Interface)
- 레이어 시스템(Layered System)
- 코드-온-디맨드(Code-On-Demand)
클라이언트 서버는 리소스를 소비하는 클라이언트와 리소스를 공급하는 서버로 구성된 구조를 의미한다. 상태가 없다는 것은 클라이언트가 서버에 요청을 보낼 때 이전 요청의 영향을 받지 않는다는 것을 말한다. 클라이언트는 서버에 요청을 할 때마다 적절한 리소스를 받기 위해 모든 정보를 포함해서 요청을 해야 한다.
캐시되는 데이터는 서버에서 리소스를 응답할 때 캐시가 가능한지 아닌지 명시해야 한다. HTTP 프로토콜에서는 cache-control을 헤더에 태워서 캐시 여부를 명시한다. 일관적인 인터페이스란 서버가 응답하는 리소스의 형식이나, 요청하는 URI가 일정하게 유지되어야 한다는 것이다.
레이어 시스템은 클라이언트가 서버에 요청한 request를 여러개의 레이어로 구성된 서버를 거치게 되는 것을 의미한다. 클라이언트의 요청은 서버의 인증서버, 캐싱 서버, 로드밸런서를 거쳐 최종적으로 애플리케이션에 도착하는 구조를 말한다. 레이어들은 요청과 응답에 영향을 미치지 않으며 요청한 클라이언트는 서버의 구체적인 레이어에 대해 알지 못한다.
HTTP 프로토콜은 REST 아키텍처를 구현하기 쉬운 프로토콜이다.
스프링 REST API 컨트롤러 레이어
컨트롤러 레이어
HTTP에서는 GET, POST, PUT, DELETE, OPTIONS 메소드로 URI를 서버에 HTTP request를 보내게 된다. 서버는 이 요청을 받은 후 해당 메소드에 연결된 서버 메소드를 실행하게 된다. 이 때 요청과 서버의 연결을 도와주는 dependency가 spring-boot-starter-web 이다.
package com.example.damo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
// 이 컨트롤러가 RestController임을 명시함
@RestController
// URI 경로 매핑
@RequestMapping("test")
public class TestController {
// HTTP 메소드 매핑
@GetMapping
public String testController(){
return "hello world";
}
}
@RestController 어노테이션은 해당 컨트롤러가 RestController임을 명시한다. 이는 요청/응답 매핑을 스프링에서 자동으로 처리하게 된다. 스프링은 @RequestMapping("RESOURCE")는 URI에 매핑하고, @GetMapping은 메소드에 매핑한다. 이 뿐만 아니라, @PostMapping, @PutMapping, @DeleteMapping 등 HTTP 메소드와 연결된 어노테이션이 존재한다. 이제 컨트롤러를 postman에서 실행시켜 보면 정상적으로 "hello world" 리턴값을 얻을 수 있다.
매개변수를 활용하는 방법
클라이언트가 요청하는 URI에 담긴 매개변수를 서버에서 받기 위해서 사용하는 어노테이션이 @PathVariable이다. /{id}등의 URI에 포함된 매개변수를 얻을 수 있다.
package com.example.damo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("test")
public class TestController {
// URI에 포함된 /{id}를 매개변수로 받는다.
@GetMapping("/{id}")
// @PathVariable로 매개변수를 받는 메소드를 구현한다.
// (required = false)는 매개변수를 입력하는 게 필수값이 아니라는 말이다.
// 매개변수를 입력하지 않더라도 에러를 반환하지 않는다.
public String testController(@PathVariable(required = false) int id){
return "hello world"+id;
}
}
서버에 보내는 HTTP 요청 URI에 "/777" 매개변수를 보냈고, 정상적으로 리턴되는 것을 확인할 수 있다.
이와 유사하게 @RequestParam은 "localhost:8080/test?id=777" 형식의 요청을 매개변수로 받을 수 있다.
클라이언트의 요청 매개변수를 받는 다른 방법으로는 @RequestBody를 사용하는 것이다. 반환하는 리소스가 Integer이나 String 처럼 단순한 정보가 아닌 오브젝트 형태인 상황에서 사용한다. 컨트롤러 클래스에서 사용하는 데이터 클래스인 DTO 클래스를 만들고 테스트를 진행해보자.
@RequestController 어노테이션을 자세히 들여다 보면 @Controller와 @ResponseBody 두개의 어노테이션이 붙어있다. @Controller는 @Component의 일종으로 스프링에서 오브젝트를 알아서 생성하고 의존성을 주입한다. @ResponseBody는 이 클래스의 메소드가 반환하는 값이 웹 서비스의 ResponseBody라는 뜻을 가지고 있다.
@RestController 코드
@Controller
@ResponseBody
public @interface RestController{ ... }
컨트롤러 클래스
package com.example.damo.controller;
import com.example.damo.dto.TestRequestBodyDto;
import org.springframework.web.bind.annotation.*;
@RestController // 이 클래스는 REST Controller 임
@RequestMapping("test") // "test" URI에 Mapping
public class TestController {
// 해당 URI로 Mapping
@GetMapping("/testRequestBody")
public String testControllerRequestBody(@RequestBody TestRequestBodyDto testRequestBodyDto){
return "hello world ID : "+ testRequestBodyDto.getId() + "Messages :" +testRequestBodyDto.getMessage();
}
}
위 예제에서 매개변수로 DTO 클래스의 오브젝트를 매개변수로 받아 오브젝트의 id와 message를 리턴하고 있다. postman에서 localhost:8080/test/testRequestBody로 id와 message를 BODY에 태워서 요청하면 정상적으로 DTO 오브젝트의 멤버를 반환한다.
서버가 리턴하는 값이 문자열이 아니라 오브젝트라면 @ResponseBody 어노테이션을 사용한다. @RestController는 @Controller와 @ResponseBody로 구성되어 있다. @Controller는 @Component로 스프링이 해당 클래스의 오브젝트를 자동으로 생성하고 다른 클래스와 의존성을 연결하게 된다. @ResponseBody는 해당 클래스가 반환하는 값이 웹 서비스의 ResponseBody라는 것을 명시한다. 즉, 메서드가 반환한 오브젝트를 스프링이 JSON으로 바꾸고 HTTP Response에 담아서 클라이언트에게 반환한다는 것이다.
스프링이 오브젝트를 JSON으로 변환하는 작업을 직렬화(Serialization)이라고 하고, JSON을 오브젝트로 변환하는 작업을 역직렬화(Deserialization)이라고 한다.
ResponseDTO 클래스
package com.example.damo.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;
}
ResponseDTO를 반환하는 컨트롤러
package com.example.damo.controller;
import com.example.damo.dto.ResponseDTO;
import com.example.damo.dto.TestRequestBodyDto;
import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("test")
public class TestController {
@GetMapping("/testResponseBody")
public ResponseDTO<String> testControllerResponseBody(){
List<String> list = new ArrayList<>();
list.add("Hello World! Response DTO is here ! ");
// list를 매개변수로 넘겨주면서 ResponseDTO<T> 오브젝트를 생성한다.
ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
return response;
}
}
postman 결과값
서버가 HTTP 응답의 바디뿐만 아니라 'status'나 'header'를 조작해야 하는 경우 @ResponseEntity를 사용한다.
응답 status를 조작할 수 있는 @ResponseEntity
package com.example.damo.controller;
import com.example.damo.dto.ResponseDTO;
import com.example.damo.dto.TestRequestBodyDto;
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("/testResponseEntity")
public ResponseEntity<?> testControllerResponseEntity(){
List<String> list = new ArrayList<>();
list.add("Hello World, This is ResponseEntity Testing! ");
ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
// HTTP 200 Code를 반환한다.
return ResponseEntity.ok().body(response);
// HTTP 400 Code를 반환한다.
return ResponseEntity.badRequest().body(response);
}
}
postman에서 localhost:8080/test/testResponseEntity로 GET 요청을 보내면 BODY는 동일한 값을 얻지만 반환코드에 따라서 반환 상태값이 달라지는 것을 볼 수 있다. @ResponseEntity는 Body 뿐만 아니라 Status와 Header를 조작할 때 사용한다.
스프링 REST API 서비스 레이어
서비스 레이어는 컨트롤러와 퍼시스턴스 사이에서 비즈니스 로직을 수행하는 레이어다. HTTP와 긴밀히 연결되어 있는 컨트롤러와 분리되어 있고, 데이터베이스와 긴밀히 연결된 퍼시스턴스 레이어와도 분리되어 있다. 서버에서 구현하고자 하는 로직에 집중할 수 있는 레이어다.
@Service 어노테이션은 스프링 프레임워크의 스테레오타입의 어노테이션이다. 이 클래스가 스프링 컴포넌트이자, 기능적으로 비즈니스 로직을 담당하는 클래스임을 명시한다. @Service 내부에는 @Component를 가지고 있지만 기능상의 큰 차이점은 없다. @Service, @RestController 모두 자바 빈이면서 스프링이 관리하고 있다.
TodoService.java
package com.example.damo.service;
import com.example.damo.model.TodoEntity;
import com.example.damo.persistence.TodoRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class TodoService {
public String testService(){
return "Test Service";
}
}
TodoController.java
package com.example.damo.controller;
import com.example.damo.dto.ResponseDTO;
import com.example.damo.service.TodoService;
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 java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("todo")
public class TodoController {
/*
스프링이 todoController 오브젝트를 생성하는 시점에서 todoController
내부에 선언된 TodoService에 붙은 @Autowired 어노테이션을 확인한다.
@Autowired 어노테이션이 알아서 빈을 찾아서 인스턴스 멤버 변수에 연결하게
된다.
즉, TodoContoller를 초기화 할 때 스프링이 알아서 TodoService를 초기화하고,
의존성을 주입해준다. 개발자는 추가로 할 일이 없어진다.
*/
@Autowired
private TodoService service;
@GetMapping
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);
}
}
스프링이 알아서 TodoService와 TodoController의 멤버변수 초기화 및 의존성 주입을 완료해주게 된다. localhost:8080/todo URI로 요청을 보내면 정상적으로 응답이 오는 것을 확인할 수 있다.
스프링 REST API 퍼시스턴스 레이어
ORM(Object Relation Mapping)은 데이터베이스 커넥션을 통해 자바에서 사용할 수 있는 클래스 오브젝트로 변환하는 일련의 작업이다. 데이터베이스 테이블을 자바에서 사용하기 위해서는 Entity마다 ORM 작업을 일일이 해줘야 한다. 이런 반복 작업을 줄이기 위해 Hibernate 같은 ORM 프레임워크가 출시되었고, JPA나 스프링 데이터 JPA 등의 도구가 등장한다.
JPA는 스펙(구현을 위해 이런저런 기능을 해라는 지침을 담은 문서)이며 자바에서 데이터베이스 접근 / 저장 / 관리에 사용된다. 스펙을 구현하는 주체를 JPA Provider라고 하며 대표적인 JPA Provider는 Hibernate다.
스프링 데이터 JPA는 JPA를 추상화해서 사용하기 쉽게 도와주는 인터페이스를 제공한다. 대표적인 JPA 인터페이스가 바로 JpaRepository다. 프로젝트를 생성할 때 Dependency에 추가한 H2는 In-Memory 데이터베이스다. 로컬 환경에서 데이터베이스를 로컬에 구축한다.
TodoEntity.java
package com.example.damo.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
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에 이름을 지정할 수도 있고, @Table에 이름을 지정할 수도 있다. 만약 Table 이름을 지정하지 않으면 @Entity 이름을 테이블 이름으로 간주하고, @Entity 이름을 지정하지 않으면 해당 클래스 명을 테이블 이름으로 간주하게 된다.
기본키가 되는 필드에는 @Id 어노테이션을 추가해줘야한다. @GeneratedValue는 ID를 자동으로 생성하게 된다. 매개변수로 어떤 방식의 ID를 생성할지 지정할 수 있다. Hibernate가 제공하는 기본 generator로는 INCREMENTAL, SEQUENCE, IDENTITY등이 있다. 만약 커스텀 generator를 만들고자 한다면 @GenericGenerator에 정의된 generator이름으로 "system-uuid" 같은 generator 를 만들어서 사용하면 된다.
Entity 클래스를 완성하고 나면 Repository 클래스를 작성한다. TodoRepository가 상속하는 JpaRepository는 인터페이스다. 엔티티 클래스와 ID를 받기 위한 String 데이터 타입 제네릭을 사용한다. 인터페이스임에도 구현 클래스가 없는 이유는 스프링에서 MethodInterceptor(AOP 인터페이스)를 자동으로 생성하기 때문이다. 즉, 사용자가 JpaRepository 메소드를 호출할 때 마다 이 콜을 가로채어 AOP 인터페이스가 생성되고 있는 것이다.
TodoRepository.java
package com.example.damo.persistence;
import com.example.damo.model.TodoEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface TodoRepository extends JpaRepository<TodoEntity, String> {
}
@Repository 어노테이션은 @Component를 내장하고 있으며, 스프링이 관리하는 자바 빈에 추가하도록 하는 기능을 가진다. repository 구현을 하고 간단한 비즈니스 로직을 구성해본다. TodoEntity를 생성하고 repository에 저장 후 repository에서 title값을 가져와서 컨트롤러 레이어로 출력하게 된다.
TodoService.java
package com.example.damo.service;
import com.example.damo.model.TodoEntity;
import com.example.damo.persistence.TodoRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class TodoService {
// 의존성 주입
@Autowired
private TodoRepository repository;
public String testService(){
// TodoEntity 생성
TodoEntity entity = TodoEntity.builder().title("First Project item").build();
// TodoEntity 저장
repository.save(entity);
// TodoEntity 검색
TodoEntity savedEntity = repository.findById(entity.getId()).get();
// 검색한 TodoEntity에서 title을 출력함
return savedEntity.getTitle();
}
}
이제 postman에서 localhost:8080/todo로 GET 요청을 보내면 "First Project Item" 문자열이 반환되는 것을 확인할 수 있다. 컨트롤러에서 TodoService로직을 작동시키고, 서비스 로직에서 엔티티를 생성/저장/검색의 과정을 거쳐서 ArrayList로 반환된 데이터를 담게 된다. 이 데이터를 ResponseDTO로 담아서 출력하게 되는 과정이다.
// TodoController.java
package com.example.damo.controller;
import com.example.damo.dto.ResponseDTO;
import com.example.damo.service.TodoService;
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 java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("todo")
public class TodoController {
@Autowired
private TodoService service;
@GetMapping
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);
}
}
'Programming' 카테고리의 다른 글
스프링 부트 SpringBoot 웹 애플리케이션 개발 #4 프론트엔드 구현하기 (0) | 2022.04.01 |
---|---|
스프링 부트 SpringBoot 웹 애플리케이션 개발 #3 CRUD 구현하기 (1) | 2022.03.31 |
자바 JAVA 제네릭 Generic이란? (0) | 2022.03.31 |
줌 프로그래밍 만드는 방법 클론 코딩 (0) | 2022.03.29 |
SpringBoot 웹 애플리케이션 개발 #1 프로젝트 시작 (0) | 2022.03.28 |
자바 배열 선언 2차 배열 0으로 초기화 하는 방법 java.lang.NullPointerException (0) | 2022.03.22 |
댓글