개발/도서

백엔드 서비스 아키텍처

괴발자-K 2022. 8. 8. 15:53

React js 스프링 부트 AWS 로 배우는 웹 개발 101 책의 좋은 점

내가 놓쳤던 개념적 부분에 대해 깊게는 아니지만 알아야 할 최소한의 개념을 설명

 

  • 학습내용
    • 레이어드 아키텍처 패턴
    • REST 아키텍터 스타일

레이어드 아키텍처 패턴이 프로젝트 내부에서 어떻게 코드를 관리할 것인가에 대한 내용이고

REST 아키텍처 스타일은 클아이언트가 우리 서비스를 이용하려면 어떤 형식으로 요청을 보내고

응답을 받는지에 대한 것(RESTful 서비스)

 

1. 레이어드 아키텍처

레이어드 아키텍처 패턴은 애플리케이션을 구성하는 요소들을 수평으로 관리하는 것

 

수평으로 관리 한다는 것은 무엇인가?

레이어가 없는 웹 서비스를 예시로 들어 보자

복잡한 비즈니스 로직을 구현한다면 몇 백줄 ~ 몇 천줄이 넘을거고, 기능을 추가 해야 하거나 

서비스를 유지보수를 하게 된다면 몇 백줄, 몇 천줄이 넘는 코드를 보면서 유지보수를 해야 한다

 

레이어드 아키텍처를 적용해 클래스/ 인터페이스로 분리한 웹 서비스 

 

위에 예제 처럼 다른 클래스에서 getTodos를 사용하고 싶다면 서비스 클래스를 사용하면 되고,

비즈니스 로직이 변한다면 서비스 레이어의 클래스만 수정하면 됩니다. 

훨씬 간편해진 것을 알 수 있다.

 

2. 모델, 엔티티, DTO

보통 자바로 된 비즈니스 애플리케이션의 클래스는 두가지 종류로 나눌 수 있다.

첫 번째는 기능을 수행하는 클래스, 두 번째는 데이터를 담는 클래스다.

기능을 수행하는 클래스는 컨트롤러, 서비스,  퍼시스턴스 처럼 로직을 수행한다

데이터를 담는 클래스는 데이터는 갖고 있다.

 

모델과 엔티티

모델은 비즈니스 데이터를 담는 역할과 데이터베이스의 테이블과 스키마를 표현하는 역할을 한다.

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;
	private boolean done;
}

 

DTO

개인적으로 제일 헷갈리는 부분!!

 

DTO를 사용하는 이유는 비즈니스 로직을 캡슐화 하기 위함이다. 모델은 데이터베이스

테이블 구조와 매우 유사하가. 모델이 갖고 있는 필드들은 테이블의 스키마와 비슷할 확률이

높다. 그렇기 때문에 외부에 노출될 가능성이 있고, DTO처럼 다른 오브젝트로 변경하여 외부접근

으로 부터 서비스 내부의 로직, 데이터베이스의 구조 등을 숨길 수 있다.

 

두번 째는 모델이 클라이언트가 필요한 정보를 전부 갖고 있지는 않기 때문이다.

예를 들자면 에러메시지 같은 경우이다. 그럴 경우 DTO에 에러 메시지를 담을 수 있는 변수를 

선언하면 된다.

 

TodoDTO

package com.example.demo.dto;

import com.example.demo.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();
	}
}

TodoDTO에는 userId가 없다, 이 책에서는 스프링 시큐어리티를 이용해 인증을 구현. 따라서 사용자가 자기 아이디를

넘겨주지 않아도 인증이 가능, userId는 애플리케이션과 데이터베이스에서 사용자를 구별하는 고유 식별자로 사용하기

때문에 숨길 수 있다면 숨기는 것이 보안상 안전하다.

 

ResponseDTO

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도 ResponseDTO를 이용해 리턴 할 수 있도록 자바 제네릭을 이용

 

 

REST API

 

제약조건

  • 클라이언트 서버
  • 상태가 없는
  • 캐시되는 데이터
  • 일관적인 인터페이스
  • 레이어 시스템
  • 코드-온-디맨드

클라이언트 서버

리소스를 관리하는 서버가 존재하고 다수의 클라이언트가 리소스를 소비하려고 

네트워크를 통해 서버에 접하는 구조를 의미

 

상태가 없음

stateless(상태가 없는??) 클라이언트가 서버에 요청을 보낼 때 이전 요청의 영향을 받지 않는 것을 의미

 

Resquest로 /login 요청을 보내고 로그인 후 에 /page로 넘어 갔을 때 /page로 리소스를 불러오는 이전에 

요청에서 login한 사실을 서버가 알고 있다면 상태가 있는 아키텍처다. 그 반대가 상태가 없는 아키텍처다

HTTP는 기본적으로 상태가 없는 프로토콜이다

따라서 HTTP를 사용하는 웹 애플리케이션은 기본적으로 상태가 없는 구조를 따른다

 

캐시되는 데이터

HTTP에서는 cache-control이라는 헤더에 리소스의 캐시 여부를 명시 할 수 있음

 

일관적인 인터페이스

일관적인 인터페이스라는 것은 시스템 또는 애플리케이션의 리소스에 접근 할 때 인터페이스가 일관적

이어야 한다는 뜻. ex) URI의 일관성, 리턴 타입의 일관성

 

레이어 시스템

클라이언트가 서버에 요청을 할 때 여러 개의 레이어로 된 서버를 거칠 수 있음

 

코드-온-디멘드

옵션, REST는 아키텍처이고, HTTP는 REST 아키텍처를 구현 할 때 사용하면 쉬운 프로토콜

 

 

TestContorller

package com.example.demo.controller;

... import 생략

@RestController
@RequestMapping("test")
public class TestController {

	@GetMapping
	public String testController() {
		return "Hello World!";
	}
    
}

@RestController어노테이션 사용하여 RestController임을 명시

@RestController는  @Controller + @ReponseBody 

RestController를 이용하면 http와 관련된 코드 및 요청/응답 매핑을 스프링이 알아서 해준다

 

@GetMapping과 비슷한 어노테이션으로 @PostMapping, @PutMapping, @DeleteMapping이 

있는 각 HTTP 메서드가 POST, PUT, DELETE를 의미한다.

 

매개변수를 넘겨 받는 방법

/test/{id}로 PathVariable이나 /test?id=12345 처럼 매개변수 받는 경우

 

@PathVariable

URI 경로로 넘어오는 값을 변수로 받을 수 있음

@GetMapping("/{id}")
	public String testControllerWithPathVariables(@PathVariable(required = false) int id) {
		return "Hello World! ID " + id;
	}

요청으로 localhost:8080/test/123345 이런 URI 요청이 가능

  • @GetMapping("/{id}"}는 매개변수로 숫자 또는 문자를 변수 id에 매핑하라는 의미
  • @PathVarialble(required = false)는 매개 변수가 필수가 아니라는 의미

 

@RequestParam

URI에서 ?id={id}와 같은 요청에서 넘어오는 매개변수 값을 받을 수 있음

	@GetMapping("/testRequestParam")
	public String testControllerRequestParam(@RequestParam(required = false) int id) {
		return "Hello World! ID " + id;
	}

/testRequestParam?id=12345로 GET 요청 시 Hello World!  ID 12345로 결과로 리턴 된 것을 확인 할 수 있다.

 

@RequestBody

RequestBody는 보통 반환하고자 하는 리소스가 복잡할 때 사용한다. 오브젝트처럼 복잡한 자료형 

전체를 요청에 보내고 싶을 때 사용

 

TestRequestBodyDTO 

package com.example.demo.dto;

import lombok.Data;

@Data
public class TestRequestBodyDTO {
	private int id;
	private String message;
}


JSON 형태의 TestRequestBodyDTO
{
    "id" : 123,
    "message" : "Hello ? " 
}

TestController

@GetMapping("/testRequestBody")
	public String testControllerRequestBody(@RequestBody TestRequestBodyDTO testRequestBodyDTO) {
		return "Hello World! ID " + testRequestBodyDTO.getId() + " Message : " + testRequestBodyDTO.getMessage();
	}

id와 message 형태를 JSON형태로 보내면 @RequestBody는 받은 JSON 데이터를 TestRequestBodyDTO 오트젝트로

변환하여 받는다. => 클라이언트는 요청바디로 JSON형태의 문자열을 넘겨준다. (타입 주의)

 id는 정수, message 문자열

 

 

개인적으로 @RestController를 쓰면서 잘 안쓰는 @ResponseBody에 대해 알아 보도록 하겠다

@ResponseBody

이 어노테이션을 쓰는 이유는 오브젝트를 리턴하기 위해서 사용한다

책에서 @RestController는 @Controller, @ResponseBody의 조합으로 이루어져 있다고 한다

@Controller는 @Component로 스프링이 이 클래스의 오브젝트를 알아서 생성하고 다른 오브젝트들과

의 의존성을 연결한다는 뜻이다. @ResponseBody는 이 클래스의 메서드가 리턴하는 것은 웹 서비스의 

ResponseBody라는 뜻이다.  ????????????????????

 

이 것만 알아 두자. 메서드가 리턴할 때 스프링은 리턴된 오브젝트를 JSON 형태로 바꾸고 HttpResponse에 담아

반환한다는 뜻이다.

 

@GetMapping("/testResponseBody")
	public ResponseDTO<String> testControllerResponseBody() {
		List<String> list = new ArrayList<>();
		list.add("Hello World! I'm ResponseDTO");
		ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
		return response;
	}

 

RETURN 값

{
    "error" : null,
    "data" : [
            "Hello World! I'm ResponseDTO"
      ]
}

return response 라는 오브젝트 형태로 반환을 했다, 결과 같이 JSON 형태로 반환 되었고, data는 배열 형태로 

반환 되었다. 이유는 ResponseDTO를 확인하면 알 수 있다.

 

 

ResponseEntity

조금은 생소 할 수도 있지만 매우 유용하게 사용하게 되고 자주 보게 된다.

ResponseEntity는 HTTP 응답의 바디 뿐만 아니라 여러 다른 매개변수들, 예를 들어 status나 header를

지정하고 싶을 때 사용한다.

 

@GetMapping("/testResponseEntity")
	public ResponseEntity<?> testControllerResponseEntity() {
		List<String> list = new ArrayList<>();
		list.add("Hello World! I'm ResponseEntity. And you got 400!");
		// http status를 400로 설정.
		ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
		return ResponseEntity.badRequest().body(response);
	}

ResponseEntity를 리턴하는 것과 ResponseDTO를 리턴하는 값을 변한게 없다.

차이점이라고 하면 header와 Http Status를 조작할 수 있다는 점이다

 

ResponseEntity.ok()

ResponseEntity.ok().body(response);  // http status를 200으로 설정