개발

[React.js, 스프링부트, AWS로 배우는 웹 개발] 2장 - 레이어드 아키텍처 패턴, REST API 스타일

jih0ssang 2024. 7. 20. 10:39

 

레이어드 아키텍처 패턴은 스프링 프로젝트 내부에서 어떻게 코드를 적절히 분리하고 관리할 것이냐에 대한 것이다.

REST 아키텍처 패턴은 클라이언트(브라우저)가 우리 서비스를 이용하려면 어떤 형식으로 요청을 보내고 응답을 받는지에 대한 것이다.
클라이언트는 정해진 메서드로 우리 서비스를 이용할 것이다.
이렇게 REST 아키텍처 패턴을 따라 설계 및 구현된 서비스를 RESTful 서비스라고 한다.

 

 

레이어드 아키텍처 (Layered Architecture)

레이어드 아키텍처

  • 레이어드 아키텍처는 애플리케이션을 구성하는 요소들을 수평으로 나눠 관리하는 것이다.

 

[레이어가 없는 웹 서비스]

public String getTodo(Request request) {
   // request validation
   if(request.userId == null) {
     JSONObject json = new JSONObject();
     json.put("error", "missing user id");
     return json.toString();
   }
   List<Todo> todos = this.getTodoFromPersistence(request);
   
   return this.getResponse(todos);
}

private List<Todo> getTodoFromPersistence(Request request) {
 List<Todo> todos = new ArrayList<>();
 
 // 데이터베이스 콜
 String sqlSelectAllPersons = "SELECT * FROM Todo where USER_ID = " + request.getUserId();
 String connectionUrl = "jdbc:mysql://mydb:3306/todo";
 
 try (Connection conn = DriverManager.getConnection(connectionUrl, "username", "password");
     PreparedStatement ps = conn.prepareStatement(sqlSelectAllPersons);
     ResultSet rs = ps.executeQuery()) {
       while (rs.next()) {
        long id = rs.getLong("ID");
        String title = rs.getString("TITLE");
        Boolean isDone = rs.getBoolean("IS_DONE");
        
        todos.add(new Todo(id, title, isDone));
       }
    } catch (SQLException e) {
      // handle the exception
     }
     return todos;
  }
  
  private String getResponse(List<Todo> todos) {
   // 응답 생성
    JSONObject json = new JSONObject();
    JSONArray array = new JSONArray();
    
    for(Todo todo : todos) {
     JSONObject todoJson = new JSONObject();
     jsonObj.put("id", todo.getId());
     jsonObj.put("title", todo.getTitle());
     json.put("isdone", todo.isDone());
     array.put(json);
     }
     json.put("data", array);
     return json.toString();
  }
  • 위의 코드처럼 레이어가 없는 웹 서비스는 하나의 클래스, 하나의 메서드 안에 모두 구현이 되어있다.
  • 이는 코드가 서로에게 의존되어 있어 확장이 어렵다.

 

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

public class TodoService {
 public List<Todo> getTodos(String userId) {
  List<Todo> todos = new ArrayList<>();
  
  // ...비즈니스 로직
  return todos;
  }
 }
 
 public class WebController {
 
  private TodoService todoService;
  
  public String getTodo(Request request) {
   // request validation
   if(request.userId == null) {
    JSONObject json = new JSONObject();
    json.put("error", "missing user id");
    return json.toString();
   }
   
   // 서비스 레이어
   List<Todo> todos = service.getTodos(request.userId);
   
   return this.getResponse(todos);
  }
}

 

  • 레이어 사이에 계층이 있다. 레이어는 자기보다 한 단계 하위의 레이어만 사용한다.
    1. 컨트롤러가 요청을 받는다.
    2. 컨트롤러는 서비스를 쪼고
    3. 서비스는 퍼시스턴스를 쫀다.
    4. 퍼시스턴스는 요청한 데이터를 반환한다.
    5. 서비스는 퍼시스턴트로 받은 데이터를 검토 및 가공한 후 컨트롤러에게 반환한다.
    6. 컨트롤러는 데이터를 검토 및 가공한 후 응답을 반환한다.

 

모델, 엔티티, DTO

레이어드 아키텍처와 데이터 클래스

 

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

  1. 기능을 수행하는 클래스  (컨트롤러, 서비스, 퍼시스턴스와 같은 로직 수행)
  2. 데이터를 담는 클래스  (Entity, Model, DTO(Data Transfer Object) 와 같은 기능 없이 데이터만 담김 )

 

모델, 엔티티

  • 이 프로젝트에서는 모델과 엔티티를 한 클래스에서 구현한다. 하지만 큰 프로젝트는 분리한다.
  • 모델비즈니스 데이터를 담는 역할과 데이터베이스의 테이블과 스키마를 표현하는 두 역할을 한다.

 

[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;
}
  • TodoEntity를 작성하기 앞서 model 패키지 com.example.demo.model을 생성한다.
  • package 아래에 Todoentity.java를 생성한다.

 

DTO

  • 서비스가 요청을 처리하고 클라이언트로 반환할 때 모델 자체를 그대로 리턴하느 ㄴ경우는 없다.
  • 데이터를 전달하는 데 사용하는 오브젝트인 DTO로 변환해 리턴한다.

모델이 아닌 DTO로 변환하여 리턴하는 이유

  1. 비즈니스 로직을 캡슐화 하기 위함
    • 모델이 가진 필드는 테이블의 스키마와 비슷할 확률이 높음
    • 자사의 데이터베이스의 스키마를 유출하는 것은 보안적으로 문제가 됨 
    • 이때 DTO처럼 다른 오브젝트로 변환하면 외부 사용자에게 내부 로직 및 데이터베이스 구조를 숨길 수 있음
  2. 클라이언트가 필요한 정보를 모델이 모두 포함하지 않는 경우가 많음
    • 대표적인 예로 에러 메시지가 있음
    • 서비스 실행 도중 사용자 에러가 나면 이 에러 메시지를 어디에 포함해야 하는가?
    • 모델은 서비스 로직과 관련 없으므로 모델이 담기에는 애매하다.
    • 이런 경우 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();
  }
}
  • 위의 DTO 코드에는 userId가 없음. 이 프로젝트는 이후 스프링 시큐리티를 이용해 인증을 구현하기 때문이다. 따라서 사용자가 자기 아이디를 넘겨주지 않아도 인증이 가능하다. 
  • userId는 애플리케이션과 데이터베이스에서 사용자를 구별하는 고유 식별자로 사용하므로 숨기는 것이 보안적으로 맞다.

[ResponseDTO.java]

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;
}
  • HTTP 응답으로 사용할 DTO이다. DTO 패키지 아래에 ResponseDTO를 생성한다.
  • TodoDTO 뿐 아니라 다른 모델의 DTO도 ResponseDTO를 이용해 리턴할 수 있도록 자바 Generic <T> 을 이용했다.
  • 이 프로젝트의 경우 Todo를 하나만 반환하는 경우보다 리스트를 반환하는 경우가 많으므로 데이터를 리스트로 반환하도록 작성했다.

* 자바 Generic <T> 는 타입 매개변수를 나타내며, 필요에 따라 여러 타입 매개변수로 사용할 수 있음

 


REST API

아키텍처 스타일이다.

아키텍처 스타일은 아키텍처 패턴과는 조금 다른데 아키텍처 패턴은 어떤 반복되는 문제 상황을 해결하는 도구이고, 

아키텍처 스타일은 반복되는 아키텍처 디자인을 의미한다.

REST 아키텍처 스타일은 6가지 제약조건으로 구성된다. 이 가이드 라인을 따르는  API를 RESTful API 라고 한다.

 

REST 제약조건

  • 클라이언트-서버 (Client - Server)
  • 상태가 없는 (Stateless)
  • 캐시되는 데이터 (Cacheable)
  • 일관적인 인터페이스 (Uniform Interface)
  • 레이어 시스템 (Layered System)
  • 코드-온-디맨드    → 선택사항

 

 

클라이언트-서버 (Client - Server)

  • 리소스를 관리하는 서버가 존재하고, 다수의 클라이언트가 리소스를 소비하려고 네트워크를 통해 서버에 접근하는 구조를 의미한다.

* 리소스는 REST API가 리턴할 수 있는 모든 것을 의미한다. 예를 들어 HTML, JSON, 이미지 등이다.

 

 

 

상태가 없는 (Stateless)

  • 클라이언트가 서버에 요청을 보낼 때 이전 요청의 영향을 받지 않음을 의미한다.
  • 예를 들어, 클라이언트가 /login으로 로그인 요청을 보내고 로그인이 돼 다음 페이지인 /page로 넘어간다고 가정하자.
    다음 페이지에서 이전 요청에서 login한 사실을 서버가 알고 있어야 한다면 그것은 상태가 있는 Stateful 아키텍처이다.
  • 상태가 없으면 그러면 로그인한 상태를 어떻게 유지하다는 것인가?
    클라이언트는 서버에 요청할 때마다 요청에 리소스를 받기 위한 모든 정보를 포함해야 한다.
    클라이언트가 서버에게 다음 페이지 요청 때마다 로그인 정보를 항상 함께 보내야 한다.
  • 리소스를 수정한 후 수정한 상태를 유지해야 하는 경우 서버가 아닌 데이터베이스 같은 퍼시스턴스에 상태를 저장해야 한다.
  • HTTP는 기본적으로 상태가 없는 프로토콜이다. 따라서 HTTP를 사용하는 웹 애플리케이션은 기본적으로 상태가 없는 구조를 따른다.

 

 

 

캐시되는 데이터 (Cacheable)

  • 서버에서 리소스를 리턴할 때 캐시가 가능한지 아닌지 명시할 수 있어야 한다.
  • HTTP에서는 cache-control이라는 헤더에 리소스의 캐시 여부를 명시할 수 있다.

 

 

일관적인 인터페이스 (Uniform Interface)

  • 시스템 또는 애플리케이션의 리소스에 접근할 때 인터페이스가 일관적이어야 한다.
  • 리소스에 접근하는 방식, 요청 형식, 응답 형식 이 애플리케이션에 걸쳐 일관적이어야 한다.
  • 예를 들어 Todo 아이템을 가져올 때, http://fsoftwareengineer.com/todo 를 사용한다.
    Todo 아이템을 업데이트할 때, http://fsoftwareengineer2.com/todo를 사용하면 일관적인 인터페이스가 아니다 !
  • 또, http://fsoftwareengineer.com/todo 는 JSON 형식의 리소스를 리턴하는데,
    http://fsoftwareengineer.com/account 는 HTML 형식의 리소스를 리턴한다면 일관적인 인터페이스가 아니다 !

 

레이어 시스템 (Layered System)

  • 클라이언트가 서버에 요청할 때 여러 개의 레이어로 된 서버를 거칠 수 있다.
  • 예를 들어 서버가 인증 서버, 캐싱 서버, 로드밸런서를 거쳐서 최종적으로 애플리케이션에 도착한다고 가정하자.
  • 이 사이의 레이어들은 요청과 응답에 어떤 영향을 미치지 않으며 클라이언트는 서버의 레이어 존재 유무를 알지 못한다.