본문 바로가기
Java

레이어드 아키텍처와 각 계층의 역할

by 돈민찌 2021. 11. 8.
반응형

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

수평적으로 나눴다는 것은 무슨 뜻일까? 간단히 말하면 이렇다. 위의 그림처럼 레이어로 나눠 놓은 것들을 하나의 클래스, 하나의 메소드 안에 전부 구현한다고 생각해 보자.

// 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();
    }
}

이런 식으로 컨트롤러, 서비스, 모델 등이 모두 한 곳에 구현되어 있는 파일로도 서버는 동작한다. 하지만 이런 코드는 보기에도 좋지 않은 것은 물론이고, 코드 중의 어떤 값을 수정해야 할 때에도 어디부터 손을 대야 할 지 난감하다. 잘못해서 한 두개를 빠뜨리면 프로그램이 어그러지는 수도 있다.

조만간 한번 다루려고 했던 개념이지만 객체 지향 프로그래밍에는 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, 서비스-모델, 퍼시스턴스-엔티티 가 있다.

엔티티란, 실제 데이터베이스의 테이블과 1:1로 매핑되는, 각 데이터를 실제로 구현한 클래스로, 데이터베이스의 테이블 내에 존재하는 컬럼만을 속성(필드)로 가져야 한다. 테이블 내에 존재하지 않는 컬럼을 가지거나, 외부 클래스를 상속받지 않아야 한다. 엔티티의 모든 값은 테이블의 행을 나타낸다. RDB(Relational DataBase, 관계형 데이터베이스)에서의 Entity(개체)란, 현실세계에서의 개체를 표현하기 위한 유형, 무형의 실체로써, Entity를 표현하기 위해서 테이블을 생성합니다.

DTO가 뭐예요? 꼭 써야 하나요?

@Getter
@Setter
@ToString
@Table(name = "user")
@Entity
public class User {

    @Id
    @GeneratedValue
    private int id;

    @Column(name = "name", nullable = false)
    private String name;

    @Column(name = "password", nullable = false)
    private String password;

    @Column(name = "email", nullable = false, unique = true)
    private String email;

    @Column(name = "phone", nullable = false, unique = true)
    private String phone;

    @Column(nullable = true)
    private LocalDateTime create_date;

    private LocalDateTime modify_date;

}

 

 

 

 

 

 

 

반응형

댓글