본문 바로가기
Java/Spring

[Backend] API 패턴 - 레이어드 아키텍처 패턴

by 전재경 2022. 12. 1.

아키텍처

시스템 목적을 달성하기위해 시스템의 상호작용등의 시스템디자인에 대한 제약 및 설계

최적화를 목표로 두고 시스템 구성과 동작원리 그리고 시스템의 구성환경등을 설명 및 설계하는 청사진 또는 설계도

 

레이어드 아키텍처

Multi-tier 아키텍처 패턴이라고도 하는 레이어드 아키텍처는 코드를 논리적인 부분 혹은 역할에 따라 독립된 모듈로 나누어서 구성하는 패턴

 

백엔드 API 코드에 가장 널리 적용되는 패턴 중 하나

 

  • Spring, SpringBoot 프로젝트 진행 시, 코드 분리/관리에 대한 방법론
  • 애플리케이션 구성 요소들을 수평으로 나눠서 관리함
  • 프레젠테이션, 비즈니스, 퍼시스턴스, 데이터베이스로 나눠짐

수평적으로 나누었다는 것은 무엇일까 ?

아래의 그림처럼 레이어로 나눠 놓은 것들을 하나의 클래스, 하나의 메소드 안에 다 구현한다고 해보자.

 

// AllInOneController.java

package com.sparta.springcore;

import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.sql.*;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@RequiredArgsConstructor // final로 선언된 멤버 변수를 자동으로 생성합니다.
@RestController // JSON으로 데이터를 주고받음을 선언합니다.
public class AllInOneController {
    // 등록된 전체 상품 목록 조회
    @GetMapping("/api/products")
    public List<Product> getProducts() throws SQLException {
        ArrayList<Product> products = new ArrayList<>();
        // DB 연결
        Connection connection = DriverManager.getConnection("jdbc:h2:mem:springcoredb", "sa", "");
        // DB Query 작성 및 실행
        Statement stmt = connection.createStatement();
        ResultSet rs = stmt.executeQuery("select * from product");
        // DB Query 결과를 상품 객체 리스트로 변환
        while (rs.next()) {
            Product product = new Product();
            product.setId(rs.getLong("id"));
            product.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
            product.setModifiedAt(rs.getTimestamp("modified_at").toLocalDateTime());
            product.setImage(rs.getString("image"));
            product.setLink(rs.getString("link"));
            product.setLprice(rs.getInt("lprice"));
            product.setMyprice(rs.getInt("myprice"));
            product.setTitle(rs.getString("title"));
            products.add(product);
        }
        // DB 연결 해제
        rs.close();
        connection.close();
        // 응답 보내기
        return products;
    }

    // 신규 상품 등록
    @PostMapping("/api/products")
    public Product createProduct(@RequestBody ProductRequestDto requestDto) throws SQLException {
        // 요청받은 DTO 로 DB에 저장할 객체 만들기
        Product product = new Product(requestDto);
        LocalDateTime now = LocalDateTime.now();
        product.setCreatedAt(now);
        product.setModifiedAt(now);
        // DB 연결
        Connection connection = DriverManager.getConnection("jdbc:h2:mem:springcoredb", "sa", "");
        // DB Query 작성
        PreparedStatement ps = connection.prepareStatement("select max(id) as id from product");
        ResultSet rs = ps.executeQuery();
        if (rs.next()) {
            // product id 설정 = product 테이블의 마지막 id + 1
            product.setId(rs.getLong("id") + 1);
        } else {
            throw new SQLException("product 테이블의 마지막 id 값을 찾아오지 못했습니다.");
        }
        ps = connection.prepareStatement("insert into product(id, title, image, link, lprice, myprice, created_at, modified_at) values(?, ?, ?, ?, ?, ?, ?, ?)");
        ps.setLong(1, product.getId());
        ps.setString(2, product.getTitle());
        ps.setString(3, product.getImage());
        ps.setString(4, product.getLink());
        ps.setInt(5, product.getLprice());
        ps.setInt(6, product.getMyprice());
        ps.setString(7, product.getCreatedAt().toString());
        ps.setString(8, product.getModifiedAt().toString());
        // DB Query 실행
        ps.executeUpdate();
        // DB 연결 해제
        ps.close();
        connection.close();
        // 응답 보내기
        return product;
    }

    // 설정 가격 변경
    @PutMapping("/api/products/{id}")
    public Long updateProduct(@PathVariable Long id, @RequestBody ProductMypriceRequestDto requestDto) throws SQLException {
        Product product = new Product();
        // DB 연결
        Connection connection = DriverManager.getConnection("jdbc:h2:mem:springcoredb", "sa", "");
        // DB Query 작성
        PreparedStatement ps = connection.prepareStatement("select * from product where id = ?");
        ps.setLong(1, id);
        // DB Query 실행
        ResultSet rs = ps.executeQuery();
        if (rs.next()) {
            product.setId(rs.getLong("id"));
            product.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
            product.setModifiedAt(rs.getTimestamp("modified_at").toLocalDateTime());
            product.setImage(rs.getString("image"));
            product.setLink(rs.getString("link"));
            product.setLprice(rs.getInt("lprice"));
            product.setMyprice(rs.getInt("myprice"));
            product.setTitle(rs.getString("title"));
        } else {
            throw new NullPointerException("해당 아이디가 존재하지 않습니다.");
        }
        // DB Query 작성
        ps = connection.prepareStatement("update product set myprice = ?, modified_at = ? where id = ?");
        ps.setInt(1, requestDto.getMyprice());
        ps.setString(2, LocalDateTime.now().toString());
        ps.setLong(3, product.getId());
        // DB Query 실행
        ps.executeUpdate();
        // DB 연결 해제
        rs.close();
        ps.close();
        connection.close();
        // 응답 보내기 (업데이트된 상품 id)
        return product.getId();
    }
}

 

controller, service, model 등 모두 한 곳에 구현되어도 서버는 동작한다.

하지만 이런 코드는 가독성도 떨어질 뿐 아니라 코드 중의 어떤 값을 수정해야 할 때에도 힘들어 진다.

객체지향 프로그래밍에 SOLID라는 원칙이 있다.

이 중 S는 단일 책임 원칙(Single responsibility principle), 하나의 파일이 너무 많은 역할을 맡고 있기 때문에

에러를 핸들링 하거나 수정하기가 상당히 힘들어 진다.

 

 

위의 코드에서 컨트롤러 역학을 하는 코드만 분리

@RestController // JSON으로 데이터를 주고받음을 선언합니다.
public class ProductController {
    // 등록된 전체 상품 목록 조회
    @GetMapping("/api/products")
    public List<Product> getProducts() throws SQLException {
        ProductService productService = new ProductService();
        List<Product> products = productService.getProducts();
        // 응답 보내기
        return products;
    }

    // 신규 상품 등록
    @PostMapping("/api/products")
    public Product createProduct(@RequestBody ProductRequestDto requestDto) throws SQLException {
        ProductService productService = new ProductService();
        Product product = productService.createProduct(requestDto);
        // 응답 보내기
        return product;
    }

    // 설정 가격 변경
    @PutMapping("/api/products/{id}")
    public Long updateProduct(@PathVariable Long id, @RequestBody ProductMypriceRequestDto requestDto) throws SQLException {
        ProductService productService = new ProductService();
        Product product = productService.updateProduct(id, requestDto);
        return product.getId();
    }
}

 

 

서비스를 맡은 부분을 분리

public class ProductService {

    public List<Product> getProducts() throws SQLException {
        ProductRepository productRepository = new ProductRepository();
        return productRepository.getProducts();
    }

    public Product createProduct(ProductRequestDto requestDto) throws SQLException {
        ProductRepository productRepository = new ProductRepository();
        // 요청받은 DTO 로 DB에 저장할 객체 만들기
        Product product = new Product(requestDto);
        productRepository.createProduct(product);
        return product;
    }

    public Product updateProduct(Long id, ProductMypriceRequestDto requestDto) throws SQLException {
        ProductRepository productRepository = new ProductRepository();
        Product product = productRepository.getProduct(id);
        if (product == null) {
            throw new NullPointerException("해당 아이디가 존재하지 않습니다.");
        }
        int myPrice = requestDto.getMyprice();
        productRepository.updateProductMyPrice(id, myPrice);
        return product;
    }
}

 

보통 애플리케이션의 클래스는 두 종류로 나뉜다.

하나는 기능을 수행하는 클래스, 하나는 데이터를 담는 클래스

기능을 맡은 클래스는 컨트롤러/서비스/퍼시스턴스 처럼 로직을 수행

데이터를 담는 클래스는 말 그대로 데이터만 담는다.

기능을 맡은 클래스에게 데이터를 요청했을 때, 데이터를 담는 클래스에 요청을 보내 응답을 받아온다.

그 결과는 아직까지 데이터 객체 그 자체이다. 아무런 기능도 없다.

이런 비즈니스 데이터를 담기 위한 클래스들이 있는데 컨트롤러 - Dto / 서비스 - 모델 / 퍼시스턴스 - Entity 이다.

 

 

이해하기 쉬운 블로그

 

[SpringBoot] 레이어드 아키텍쳐 패턴, Controller Service Repository 가 하는 일

스프링과 스프링부트는 레이어드 아키텍쳐 패턴이 REST 아키텍쳐 스타일을 이용하는데 도움이 되는 어노테이션을 제공한다. 😦 레이어드 아키텍쳐 패턴 - Spring, SpringBoot 프로젝트 진행 시, 코드

minnnmi.com

 

댓글