요구사항 분석

기능 목록:

 회원 기능 - 회원 등록, 회원 조회

  상품 기능 - 상품 등록, 상품 수정, 상품 조회

  주문 기능 - 상품 주문, 주문 내역 조회, 주문 취소

  

기타 요구사항:

  상품은 재고 관리 필요

  상품의 종류는 도서, 음반, 영화로 한정

  상품을 카테고리로 구분 할 수 있음

  상품 주문시 배송 정보 입력 가능

 

도메인 모델, 테이블 설계

 

 

회원(Member): 이름과 임베디드 타입은 주소(Address), 그리고 주문(orders) 리스트를 가진다.

 

주문(Order): 한번 주문시 여러 상품을 주문 할 수 있으므로, 주문과 주문상품(OrderItem)은 일대다 관계, 주문은 상품을 주문한 회원과 배송정보, 주문 날짜, 주문 상태(status)를 가지고 있다. 주문 상태는 열거형을 사용했는데 주문(ORDER), 취소(CANCLE)을 표현 할 수 있다.

 

주문 상품(OrderItem): 주문한 상품 정보와 주문금액(orderPrice), 주문수량(count), 정보를 가지고 있다.(보통 OrderLine, LineItem으로 많이 표현한다.)

 

상품(Item): 이름, 가격, 재고수량(stockQuantity)을 가지고 있다. 상품을 주문하면 재고 수량이 줄어든다.

상품의 종류로는 도서, 음반, 영화가있는데 각각은 사용하는 속성이 조금씩 다르다.

 

배송(Delivery): 주문시 하나의 배송 정보를 생산한다. 주문과 배송은 일대일 관계.

 

카테고리(Category): 상품과 다대다 관계. parent, child로 부모, 자식 카테고리를 연결한다.

 

주소(Address): 값 타입(임베디드 타입). 회원과 배송에서 사용된다

 

 

회원 테이블 분석

 

MEMBER: 회원 엔티티의 Address 엠베디드 타입 정보가 회원 테이블에 그대로 들어갔다. Delivery 테이블도 마찬가지

ITEM: 앨범, 도서, 영화 타입을 통합해서 하나의 테이블로 만들었다. DTYPE 컬럼으로 타입을 구분한다.

 

참고: 테이블명이 실제 코드에서는 DB에 소문자 + _(언더스코어)스타일을 사용

데이터베이스 테이블명, 컬럼명에 대한 관례는 회사마다 다른데 보통은 대문자 + _(언더스코어)나 소문자 + _(언더 스코어)방식중에 하나를 지정해서 일관성 있게 사용한다. '

 

연관관계 매핑 분석

 

회원과 주문: 일대다, 다대일 양방향 관계. 따라서 연관관계의 주인을 정해야하는데, 외래 키가 있는 주문을 연관관계의 주인으로 정하는 것이 좋다. 그러므로 Order.member를 ORDERS.MEMBER_ID 외래키와 매핑한다.

(보통 외래키가 있는게 주인이 됨 대부분 다수)

 

주문 상품과 주문: 다대일 양방향 관계이다. 외래키가 주문 상품에 있으므로 주문 상품이 연관관계의 주인이다.

그러므로 OrderItem.order를 ORDER_ITEM.ORDER_ID외래 키와 매핑한다.

 

주문 상품과 상품: 다대일 단방향 관계이다. OrderItem.item을 ORDER_ITEm.ITEM_ID 외래키와 매핑한다.

 

주문과 배송: 일대일 단방향 관계. Order.delivery를 ORDERS.DELEVERY_ID 외래키와 매핑한다.

 

카테고리와 상품: @ManytoMany를 사용해서 매핑(실무에선 사용x)

 

 

엔티티 클래스 개발

Member 엔티티를 만들어준다.

그다음 address 클래스를 만들어준다.

address 클래스는 값이 변하지않게 만들어주고

다른 클래스들도 만들어준다.

실행시켜본 후 h2 데이터 베이스에 들어가서 제대로 다 구현이 됐는지 확인 해본다.

 

엔티티 설계시 주의점

 

엔티티에는 가급적 setter을 사용하지 말자

setter가 모두 열려있으면 변경 포인트가 너무 많아서 유지 보수가 어렵다.

 

모든 연관관계는 지연로딩(LAZY)으로 설정해야한다.

즉시 로딩(EAGER)은 예측이 어려우며, 어떤 SQL이 실행될지 추적하기 어렵다.

@XToOne(OneToOne, ManyToOne) 관계는 기본이 즉시로딩이므로 직접 위 사진처럼 지연로딩으로 설정을 해줘야한다.

 

컬렉션은 필드에서 초기화 하자

 

테이블,컬럼명 생성 전략

스프링 부트에서 기본적으로 SpringPhysicalNamingStrategy를 통하여 지정됨

카멜 케이스 > 언더스코어(memberPoint -> member_point)

.(점) > _(언더스코어)

대문자 > 소문자

 

 

추가 공부

서비스를 보고 직접 해본 erd 설계

아직 미완, 미흡

 

ERD는 프로젝트 시작과 동시에 설계하는 것이 좋습니다.

기획과 디자인이 어느정도 나와 개발자가 작업을 시작할 수 있는 시점을 말하는 것입니다.

 

ERD의 세세한 내용은 언제든지 바뀔 수 있기에 처음부터 완벽하게 만들 필요는 없습니다.

큰 틀을 정해두는 것이 1차 목표, 그 후 실제 기능 구현을 진행하며 필요한 내용을 수정합니다.

 

명심해야할것은

모든 팀원이 인지하는 데이터베이스는 동일해야 한다는 것입니다.

각자 자기 마음대로 DB를 설계하고, 작업 후 나중에 합치는 행위는 매우 좋지 않습니다.

처음에 빠르게 ERD를 설계하여, 모두가 공통된 데이터베이스에 대해 인지한 후 작업을 하는 것이 좋습니다.

 

DATABASE 설계

도서 대여 관리 앱에 대한 간단한 요구사항을 살펴보면

사용자 관련 요구 사항

  1. 카카오 소셜 로그인을 구현 할 예정이다.
  2. 회원 탈퇴 기능이 필요하다.
  3. 이름, 닉네임, 전화번호, 성별이 필요하다.

책 관련 요구 사항

  1. 사용자가 책 여러 권을 대여할 수 있다.
  2. 책은 하나의 카테고리가 있다.
  3. 책은 제목, 설명에 대한 정보가 필요하다.
  4. 책 소개 페이지에 해시태그가 붙을 수 있고, 책 한 권에 해시태그 여러 개가, 해시태그 하나가 여러 책에 붙을 수 있다.
  5. 사용자가 책 설명 페이지에서 책에 좋아요를 누를 수 있다.
  6. 카테고리 별로 현재 몇 개의 책이 있는지 집계가 필요하다.

알림 관련 요구 사항

  1. 알림은 공지 관련 알림, 책 반납 시간 임박 알림, 마케팅 알림이 있을 수 있다.

 

먼저 테이블 이름과 칼럼 이름은 모두 소문자, 그리고 단어 구분은 대소문자가 아닌, 밑줄로 구분해주는 것이 좋습니다.

기본 키를 위해 각 엔티티 정보 중 유일한 값을 기본 키로 설정하기 보다는 index를 따로 두는 것이 편합니다, 또한 각 index를 위한 id를 book_id, member_id 이렇게 보다는 그냥 id로 이름을 짓는 것이 좋습니다.

기본 키 타입은 int가 아닌 추후 서비스 확장을 고려하여 bigint로 하는 것이 좋습니다.

그러면 위 그림처럼 됩니다.

 

각각의 타입은 PM에게 물어봐서 확실하게 결정이 가능하게 하면 됩니다.(일단 위 사진은 임의로 타입을 저렇게 지정)

 

 

description의 경우 text타입은 string과 같이 길이 제한이 없는 타입인데 이것또한 pm과 상의를 통하여 글자수를 결정하는 것이 좋습니다.

gender의 경우 0이면 남자, 1이면 여자 이렇게 하는 경우도있고 varchar로 설정하여 문자로 두고 enum으로 관리를 해도 됩니다.

또한 아래처럼 create_at, update_at을 테이블마다 추가 해주는 것이 좋으며, datetime(6)는 밀리초 소수점 6자리까지 구분한다는 의미입니다.

왜 밀리초까지 구분할까요?

a: 나중 기능 구현을 염두한 설계입니다. 최신순 정렬 기능은 정말 흔한 기능입니다. 근데 초 단위까지만 저장을 하게 된다면 동시에 책을 등록하여 created_at이 초 단위까지 같은 상황이 일어나면 최신순 정렬이 잘 안되기에, 밀리초 까지 구분을 하는 것입니다.

(Mysql은 기준 6자리가 최대입니다.)

 

또한 멤버테이블은 status와 inactive_date를 두는 것이 좋습니다.

왜일까요? 

기능 구현시 회원 탈퇴, 게시글 삭제 등의 연산을 HTTP Method중 delete로 바로 삭제를 해버리는 방법도 있습니다(이를 Hard delete라고 합니다)

하지만 Hard delete의 방법은 지양하는 것이 좋습니다. 게시글은 요구 사항에 따라 다르겠지만, 유저의 경우 예시를 들어보자면 인스타그램같은 경우 , 회원 탈퇴를 철회하고 다시 돌아오는 회원들이 존재합니다.

그리고 아래와같은 요구사항이 있다고 해보면,

"매일 인기가 있는 사용자 상위 5명을 집계해서 보여줘야한다. 근데 이때 join 연산으로 가져와서 보여주도록 되어있는 상태"

이때 상위 1등이 갑자기 탈퇴를 하고 delete로 그 자리에서 삭제를 하게 된다면 갑자기 1등은 없고 2 3 4 5 등만 존재하게 되버리는 것입니다. (이부분도 요구 사항에 따라 다를 수 있습니다.)

 

따라서 사용자같이 곧바로 지워버리게되면 위험한 엔티티는 바로 delete를 하는 것이 아니라 

일단 비활성화 상태로 두고, 일정 기간동안 비활성인 경우 자동 삭제가 되도록 설계하는 것이 좋습니다.

 

이때 status를 active, inactive등 enum으로 관리하기 위해 varchar(15)로 둔것이고 경우에 따라 0이면 비활성화, 1이면 활성화 이런식으로 구현을 해도 됩니다. 그리고 얼마동안 비활성된 상태인지 알아내기 위해 inactive_date를 따로 뒀습니다.

 

그렇다면 어떻게 자동으로 지울까요?

batch란 정해진 시간에 자동으로 실행되는 프로세스입니다. 예시로 매일 새벽 2시에 자동으로 member테이블을 검사하여 inactive된 이후 7일이 지난 경우 삭제하도록 할 수 있습니다.

이를 soft delete라고 부릅니다.

꼭 회원 뿐만 아니라 요구사항에 휴지통 기능(삭제했다가 다시 복구가 필요한 기능)이 있는 경우도 바로 delete보다는 soft delete를 해야합니다. (soft delete는 HTTP Method중 patch입니다)

 

연관 관계에 대한 고민

Mysql은 RDB기반이고, RDB에서는 외래 키로 연관 관계를 표시합니다.

그렇다면 사용자가 책을 대여할때 연관 관계를 어떻게 해야할까요?

사용자랑 책이랑 1:N을 하면 될까요?

테이블을 보면 테이블에서 book이 의미하는 것은 책 종류를 말하는 것이지 실제 책 한권을 얘기하는건 아닙니다.

한 종류의 책을 여러 사용자가 대여하고, 한 사용자가 여러 종류의 책을 대여할 수 있으니 이때 사용자와 책은 N:M관계입니다.

 

이렇게 N:M 관계일때는 가운데에 매핑 테이블을 따로 둬야합니다.

가운데 매핑 테이블은 양쪽의 기본 키를 외래키로 가지고 각각 1:N 관계를 가집니다.

 

책과 책 카테고리의 경우 어떻게 매핑을 할까요?

현재 요구 사항을 보면 책에 카테고리 한 종류만 붙습니다. 카테고리 하나당 그저 여러 종류의 책이 관계를 가지기에, 카테고리와 책이 1:N 관계입니다.

그러나 책 한종류에 여러 카테고리가 붙을수있다면 N:M관계이므로 당연히 가운데 매핑 테이블을 둬야 합니다.

 

책에 붙는 해시태그, 사용자가 책에 누르는 좋아요는

해시태그도 여러개가 한 책에 붙고, 책에 여러 해시태그가 붙기에 N:M 관계 입니다.

마찬가지로 한 종류의 책에 사용자 여러명이 좋아요를 누르고, 한 사용자가 여러 책에 좋아요를 누르기에 이 또한 N:M 관계입니다.

 

따라서 위와 같이 다대다 매핑 같은 경우 가운데에 매핑 테이블을 두어서 설계가 가능합니다.

 

그렇다면 알림의 경우는 어떻게 설계할까요?

알림의 까다로운 점은 

"공지사항에 대한 알림은 알림 터치시 해당 공지사항으로 이동이 되고, 마케팅 알림의 경우 터치시 해당 마케팅으로 이동이된다"

라는 요구사항이 붙으면 좀 힘들게됩니다.

 

여기서 설계를 할때 어려운 부분이 어떤 것에 대한 알림인지 어떻게 알아내지? 라는 것입니다.

이에 대해서 3가지 설계 방법이 있습니다.

 

1. 슈퍼 타입과 서브 타입의 구성

이런 모양으로 설계할 수 있습니다.

 

2, 하나의 테이블에 두고 dtype으로 구분

간단하게 모든 내용을 다 한 테이블에 두고 dtype으로 구분을 하는 방법이 있습니다.

이때 dtype을 테이블로 따로 관리를 하거나 enum으로 관리하는 것은 선택하면 됩니다.

 

3. 테이블 다 나누기

 

 

데이터 베이스는 처음부터 완벽하게 설계할 수 없습니다. 그러나 경험이 쌓임에 따라 미래를 예측하여 이렇게 했다가 나중에 바꾸겠지? 하는 부분이 있습니다.

요구 사항중 사용자가 책에 좋아요를 누를 수 있고 이를 집계를 한다고 했는데 이럴때 책 테이블에 likes 칼럼을 두고 좋아요를 집계하는 것이 좋을까요? 좋아요를 누르면 +1 취소하면 -1 이렇게 하는것은 큰 무리는 없지만 

"사용자 간 차단 기능이 생기게 되어 차단 한 사용자가 누른 좋아요는 집계를 하지않는다"

라는 요구 사항이 생기게되는 경우들을 대비하여서 좋아요 개수를 집계하는 것은

순수 DML 연산으로, book_likes에서 해당 책 아이디를 가진 것이 몇개인지 직접 세는 것이 좋습니다.

 

 

RDS의 설정

데이터 베이스를 로컬 컴퓨터(서버 개발자가 작업하는 컴퓨터)에 두는 것이 좋을까요?

좋지 않습니다.

컴퓨터를 끄면 데이터 베이스 접속이 안되기도하고, 로컬 컴퓨터에 접속을 한다는 것은 해당 컴퓨터의 데이터베이스로 접속이 되도록 포트포워딩을 해야합니다.

따라서 데이터베이스도 EC2처럼 외부 컴퓨터를 빌려서 사용하는 것이 좋습니다.

EC2에 데이터 베이스를 설치해서 사용해도 좋으나, RDS를 사용하게 될 경우 더 유연하게 데이터 베이스를 사용할 수 있기에 RDS를 설정해서 사용해보는것도 좋습니다.

미리 정해진 (정적인) 콘텐츠를 준비해두고 요청이 오면 응답으로 주는 것이 아닌 요청이 올때마다 해당 요청에 적절한 컨텐츠를 만들기(동적인) 위해서는 WAS가 필요합니다.

클라이언트 요청에 대한 적절한 데이터를 만들어 주는 서버를 Web Application Server(WAS = nodejs , Spring Boot)라고 합니다.

 

포트번호 , IP주소를 통해 식별된 컴퓨터의 프로세스를 찾아서 데이터를 송신합니다.

aws설정시 보안그룹에서 TCP 80번 포트를 열어준 이유는 EC2의 아이피 주소를 통해 EC2를 식별 후 80번 포트로 열린 Web Server(nginx)로 요청을 보내기 위함이였습니다.

웹 브라우저에서 ip주소를 입력하면 자동으로 80번 포트가 붙어서 nginx에 요청이 간 후 응답이 온 것입니다.

(웹 브라우저는 43.201.245.2 → 43.201.245.2:80 으로 요청을 보냄)

 

그렇다면 ip주소로 연결했을때 바로 nginx화면이 나오는걸 볼 수 있는데

개발자 도구를 열어 확인해보면 html 문서라는 걸 알 수 있습니다.

nginx가 요청에 대해서 정적인 컨텐츠(미리 준비해둔 html)를 준 걸 볼 수 있습니다.

 

그럼 어떤 경우에는 a 컨텐츠를 주고 어떤 경우에는 b 컨텐츠를 주는지 알아보도록 하겠습니다.

 

NGINX에서의 정적컨텐츠 호스팅

 

nginx에는 설정 파일이 있고 웹 서버가 실행될때 이 설정 파일을 읽으면서 실행이 됩니다.

##
# You should look at the following URL's in order to grasp a solid understanding
# of Nginx configuration files in order to fully unleash the power of Nginx.
# https://www.nginx.com/resources/wiki/start/
# https://www.nginx.com/resources/wiki/start/topics/tutorials/config_pitfalls/
# https://wiki.debian.org/Nginx/DirectoryStructure
#
# In most cases, administrators will remove this file from sites-enabled/ and
# leave it as reference inside of sites-available where it will continue to be
# updated by the nginx packaging team.
#
# This file will automatically load configuration files provided by other
# applications, such as Drupal or Wordpress. These applications will be made
# available underneath a path with that package name, such as /drupal8.
#
# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples.
##

# Default server configuration
#
server {
        listen 80 default_server;
        listen [::]:80 default_server;

        # SSL configuration
        #
        # listen 443 ssl default_server;
        # listen [::]:443 ssl default_server;
        #
        # Note: You should disable gzip for SSL traffic.
        # See: https://bugs.debian.org/773332
        #
        # Read up on ssl_ciphers to ensure a secure configuration.
        # See: https://bugs.debian.org/765782
        #
        # Self signed certs generated by the ssl-cert package
        # Don't use them in a production server!
        #
        # include snippets/snakeoil.conf;

        root /var/www/html;

        # Add index.php to the list if you are using PHP
        index index.html index.htm index.nginx-debian.html;

        server_name _;

        location / {
                # First attempt to serve request as file, then
                # as directory, then fall back to displaying a 404.
                try_files $uri $uri/ =404;
        }

        # pass PHP scripts to FastCGI server
        #
        #location ~ \.php$ {
        #       include snippets/fastcgi-php.conf;
        #
        #       # With php-fpm (or other unix sockets):
        #       fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
        #       # With php-cgi (or other tcp sockets):
        #       fastcgi_pass 127.0.0.1:9000;
        #}

        # deny access to .htaccess files, if Apache's document root
        # concurs with nginx's one
        #
        #location ~ /\.ht {
        #       deny all;
        #}
}


# Virtual Host configuration for example.com
#
# You can move that to a different file under sites-available/ and symlink that
# to sites-enabled/ to enable it.
#
#server {
#       listen 80;
#       listen [::]:80;
#
#       server_name example.com;
#
#       root /var/www/example.com;
#       index index.html;
#
#       location / {
#               try_files $uri $uri/ =404;
#       }
#}

 

root /var/www/html; -> root는 정적인 콘텐츠를 찾는 시작 디렉토리를 의미합니다.

index index.html index.htm index.nginx-debian.html; -> index는 기본적인 요청에 대해서 index뒤에 파일들을 찾아서 웹상으로 보여 준다는 것이며 실제로 파일경로로 이동하여서 있는 파일들을 보면 index.nginx-debian.html 파일이 있는것이 확인이 가능합니다.

파일의 내부를 살펴보면

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

 

아까 봤던 정적인 콘텐츠인 html파일인 것을 확인 할 수 있습니다.

(처음 80번 포트로 요청을 보냈을 때 응답으로 오는 html 문서임을 확인 가능합니다)

 

정리해서 root /var/www/html를 통해 정적 컨텐츠를 찾아낼 시작 디렉토리를 설정하고 index를 통해 기본 요청이 올 경우 어떤 파일을 줄지 설정하여 요청에 대해서 /var/www/html/index.nginx-debian.thml를 응답값으로 준 것을 알 수 있습니다.

 

nginx 설정 파일에서의 location 블록

/etc/nginx/sites-available/default에 아래와 같은 블록을 추가해봅시다.

location /temp{
                root /var/www;
                index temp.html
                try_files $uri $uri/ =404;
        }

 

인텔리제이에서 브라우저 리모트 호스트를 통하여 원래 만들어뒀던 탄력적 ip주소와 연결 후 nginx를 깔고 난후 저 경로로 이동하게된다면 위에 나와있는 코드처럼 파일이 보입니다(명령어: cat default)

만약 보인다면 그 후 default파일을 수정해주어야하는데 이때 리눅스 편집기를 사용할때의 문법을 사용하셔야합니다.

sudo vi default라는 명령어를 실행 후 i키를 눌러 파일편집을 해줍니다 그 후 :wq 명령어를 입력하여 저장 후 나와줍니다.

저장이 잘되었는지 확인을 하기 위해 cat default를 통하여 다시한번 확인해줍니다.

그 후 /var/www로 이동하여 nginx 설정이 변경되었으니 sudo systemctl restart nginx를 통하여 최신화 해줍니다.

다음으로 sudo mkdir temp 명령어를 통하여 temp 디렉토리를 생성 후 sudo vi temp.html를 통하여 html파일을 만듬과 동시에 열어줍니다.

아까와 같은 방법으로 파일편집을 통하여

<h1> hi im kimtaeyoung! <h1>

를 넣어준후 저장해줍니다.

그 후 탄력적 ip에/temp를 붙여서 경로를 이동해주면 작성된 html 파일이 나오는걸 볼 수 있습니다.

 

동작 원리

location /temp{
                root /var/www;
                index temp.html
                try_files $uri $uri/ =404;
        }

위 설정은 /temp요청에 대해서 아래 중괄호 속 내용과 같이 하라는 뜻입니다.

root /var/www라는 말은 /temp 앞에 /var/www을 붙여서 /var/www/temp 디렉토리에서 정적 파일을 찾아라라는 뜻입니다.

 

쉽게 설명하자면

location /y{
	root /x
}

/y로 오면 /x/y에서 파일을 찾으라는 것입니다.

추가로 붙혀져있는 index temp.html은 기본적으로 /temp요청이 올 경우 index뒤에 파일을 찾아서 응답을 리턴하라는 말입니다.

 

그럼 temp파일에 sudo vi test.html파일을 생성하고 아무 html 언어로 쓴후 저장하면 어떻게될까요?

ip/temp/test.html이라고 하면 그 파일이 나오게됩니다.

 

WAS(Web Server Application)

 

어떻게 was는 웹서버와 같이 동작이될까?

노드, 스프링 모두 was입니다. 

그런데 우리가 원하는 모습은 아래와 같습니다.

냅다 바로 노드 혹은 스프링을 실행시켜버리면 웹서버가 없는 모습이 됩니다.

 

Reverse Proxy

리버스 프록시는 컴퓨터 네트워크에서 사용되는 개념으로, 클라이언트와 서버간 통신을 중계하고 보안, 성능 개선등의 목적을위해 중간에 위치하는 서버를 말합니다.

프록시 서버 자체는 대리자로써 클라이언트의 요청을 받고 본 서버로 보내줍니다. 

여기서 서버는 한번 서버로써 요청을 받기만 하는건 아닙니다.

서버 프로세스가 내부적으로 connect() 시스템 콜과 같은 요청을 보내는 시스템 콜을 통하여 다른 서버 프로세스에게 다시 요청을 보낼 수 있습니다.

 

그렇다면 기존 nginx 설정파일을 아래와 같이 변경해봅시다.

location / {
                proxy_pass http://localhost:3000; <- 프록시 설정
                # First attempt to serve request as file, then
                # as directory, then fall back to displaying a 404.
                try_files $uri $uri/ =404;
        }

이 설정은 /요청에 대해서 우선적으로 프록시로써 요청을 건내주도록 했습니다.

이때 localhost:3000, 즉 내부 컴퓨터의 3000번 포트의 프로세스로 요청을 보내도록 되어있기에 이는 리버스 프록시라고 볼 수 있습니다.

nginx의 설정을 바꿨으니 다시 업데이트 명령어를 실행시켜준후 

ip주소로 이동한다면 502 badgateway가 뜨게됩니다.

현재 3000번 포트를 가진 프로세스가 없기때문에 당연합니다. 

 

회원 등록 API

api는 따로 디렉토리로 빼서 작업을 해주는게 좋다고 합니다.

 

우선 api를 만드는 2가지 방법중 첫번째 방법으로 해보겠습니다.

api를 개발하기전 rescontroller 어노테이션을 추가해줍니다.

@RestController // =  @Controller + @ResponseBody

 

 

 

회원 등록이니 api의 post url을 지정해주고 파라미터로 멤버 객체를 받아서 회원가입을 시켜주는것을 구현하였습니다.

@requestbody는 json으로 온 데이터를 member에 매핑해줍니다.

(Member 클래스에서는 name의 값을 필수적으로 받기위해서 NotEmpty 어노테이션을 사용)

 

응답값은 CreateMemberResponse를 만들어주어서 회원등록이 되면 id값을 반환할 수 있도록 만들었습니다.

 

포스트맨으로 확인결과 응답값과 통신 모두 잘 되었습니다.

 

하지만 이렇게 구현할시에 심각한 문제가 많습니다.

단순 등록인데 아까 멤버 객체에 이름값을 필수적으로 받기 위해서 NotEmpty 어노테이션을 사용하였다고 말씀드렸습니다.

그 경우 사용자가 직접 값을 입력하는 presentation 계층을 위한 검증 로직이 객체에 구현이 되어있는 것입니다.

즉 다른말로 어떤 api에서는 NotEmpty가 필요할 수도 있겠지만 어떤 api에서는 필요가 없다는 것을 의미합니다.

다음으로는 객체의 스펙을 name에서 username으로 변경할시에 api 스펙 자체가 변경이 되어버립니다.

문제는 객체를 손대서 api 스펙 자체가 변하는 것이 문제입니다.

객체는 굉장히 여러곳에서 쓰이는 것입니다. (바뀔 확률이 매우 높음)

(객체랑 api 스펙이 1대1로 매핑이 딱 되어있다는 뜻입니다)

따라서 이런 경우를 방지하기 위해서 api 스펙을 위한 별도의 데이터 트렌스퍼 오브젝트(DTO)를 만들어야합니다.(별도의 DTO를 파라미터로 받는게 좋습니다)

엔티티를 외부에서 json오는것을 바인딩 받는데 사용하는것은 안됩니다.(장애가 발생 할 수 있음)

api를 만들때는 항상 파라미터로 객체를 받지않기!(객체를 외부에 노출해서도 안됨)

 

이걸 적용한 2번째 방법으로 해보겠습니다.

바로 객체를 파라미터로 받지않고 CreateMemberRequest DTO를 생성하여서 DTO를 파라미터로 넣어줌으로써 구현하였습니다.

반환값이 잘 나오는게 확인됩니다.

이렇게 구현할시에 장점은 api의 스펙이 변하지 않게되고, 서비스를 안정적으로 운용 할 수 있게됩니다.

또한 지금 멤버는 파라미터가 어떤것이 넘어올지 모르는데 (id,name,address,orders등) DTO를 만들어놓으면 api 스펙자체에서 어떤 값을 받는지 바로 알 수 있습니다.

validation을 하고싶으면

NotEmpty 어노테이션을 DTO에 추가하여서 구현 가능합니다.

 

회원 수정 API

회원 수정을 위해서는 putmapping을 사용해주어서 경로변수로 id값을 받아서 수정해주도록 구현하였습니다.

구현시 아까와 같이 객체를 파라미터로 넘겨주는것이 아닌 수정 DTO를 생성하여서 파라미터로 넘겨주었습니다.

@AllArgsConstructor 어노테이션을 사용해주었기에 따로 생성자는 만들지 않았습니다.

회원 이름 수정시에 update 함수를 사용하여 변경감지를 통하여 영속상태인걸 이용하여서 트렌젝션이 커밋시에 JPA가 데이터를 추적해서 변경감지를 할 수 있게 함수를 구현 하였습니다.

MemberService class

다음으로 멤버를 id로 찾아준뒤 변경된 멤버의 id,name값을 리턴해주었습니다.

 

멤버를 생성해주고 해당 id 값으로 이름 수정을 하니 리턴값에 이름 변경이 제대로된걸 알 수 있습니다.

 

회원 조회 API

지금까지는 단순 조회이기때문에 yml 파일에있는 auto-ddl을 none으로 설정해줍니다.(데이터를 계속 쓸수있게됨)

회원 조회시에는 그냥 회원 정보들을 다 list로 뽑아주면됩니다.

그 결과 회원들의 정보들이 list로 나오게되는데 이때 회원들의 정보만 추출하려했지 order은 추출하려하지않음

이때 @JsonIgnore 어노테이션을 사용하여서 order값을 추출값에서 빼줄수있습니다.

하지만 이렇게 구현할시 다른 api 개발을 함에 있어서 어떤 api는 order도 필요하고 어떤 api에서는 회원 정보 + order이 다 필요한 경우가 있을수 있기때문에 이런식의 개발은 좋지않습니다.

 

응답값으로 엔티티를 위처럼 직접 외부에 노출하게된다면

1. 엔티티(객체)에 프레젠테이션 계층의 로직이 추가된다.

2. 기본적으로 엔티티의 모든 값들이 외부에 노출된다.

3. 응답 스펙을 맞추기 위하여 로직들이 추가된다.(@JsonIgnore 등등)

4. 실무에서는 같은 엔티티에 대하여 api가 용도에 따라 다양하게 만들어지는데 한 엔티티에 각각의 api를 위한 프레젠테이션 응답 로직을 담기는 어렵다.

5.  엔티티가 변경되면 api 스펙이 변한다.

 

파훼법

1. api 응답 스펙에 맞추어 별도의 DTO를 구현한다.

 

단점들을 보완하여 다시 구현해보겠습니다.

 

회원정보중 간단하게 이름만 추출할수있도록 DTO를 구현하였으며 []:array의 형태로 감싸진 데이터(추후 추가 데이터를 넣으려면 json 형식이 파괴되어서 매우 복잡해짐)가 아닌 형태로 추출하기 위하여 구현해주었습니다.

다음으로 리스트를 추출하는건 동일하지만 아까 구현해놨던 멤버DTO로 추출한 리스트를 매핑 시켜주어서 구현하였습니다.

또한 추가로 count(추출된 데이터의 갯수)라는 정보를 바로 추가할수있게끔([]형태로 안해서 가능한 구현)확인하기 위하여 추출값을 넣어주었습니다.

 

준영속 엔티티는 영속성 컨텍스트가 더이상 관리하지 않는 엔티티를 말함.

Book객체는 이미 DB에 한번 저장되어서 식별자가 존재하는데 이렇게 임의로 만들어낸 엔티티도 기존 식별자를 가지고 있으면 준영속 엔티티라고 볼 수 있다. 

 

예시로 테스트를 하나 작성해보면 여기서 JPA에서 book 클래스를 찾아서 이름은 저렇게 바꿔주고 트렌젝션이 커밋이되면 이름이 바뀌게되는데 이게 변경 감지(더티체킹)이다. (내가 원하는 걸로 업데이트를 칠 수 있음)

 

주문 취소 로직에서도 동일하게 값을 cancle로 바꿔주는걸 해줬음.(알아서 JPA가 커밋 시점을 찾아서 DB업데이트를 해줌)

 

하지만 준영속 엔티티란 JPA의 영속성 컨텐츠가 더이상 관리를 해주지않는 상태를 말함.(영속성 엔티티는 JPA가 다 보고있어서 변경감지가 트렌젝션 커밋시점에 일어남)

 

준영속 엔티티를 변경할 수 있는 두가지 방법

1. 변경감지 기능 사용

2. 병합(merge) 사용

 

변경 감지 기능 사용

findItem으로 찾아온것은 영속 상태임. 

그럼 파라미터로 넘어온 book으로 값을 세팅을 다 했으면 스프링의 트렌젝셔널에 의해서 트렌젝션 커밋이 됨.

커밋이 되면 JPA는 플러시(영속성 컨텍스트중에 변경된 것이 뭔지 찾는것)를 날림 >  바뀐 값을 업데이트 쿼리를 날려서 DB에 업데이트를 시킴. 

 

 

병합 사용

준영속 상태의 엔티티를 영속 상태로 변경할때 사용

그냥 위에있는 코드를 한줄로 요약한것임

 

병합 동작 로직

1. merge() 실행

2. 파라미터로 넘어온 준영속 엔티티의 식별자 값으로 1차 캐시에서 엔티티 조회

2-1. 만약 1차 캐시에 엔티티가 없으면 데이터 베이스에서 엔티티를 조회하고 1차캐시에 저장.

3. 조회한 영속성 엔티티에 엔티티의 값을 채워 넣는다.

4. 영속 상태인 엔티티를 반환한다. 

 

간단히 하자면 

1. 준영속 엔티티의 식별자 값으로 영속 엔티티 조회

2. 영속 엔티티의 값을 준영속 엔티티의 값으로 모두 변경(병합)

3. 트렌젝션 커밋 시점에 변경 감지 기능이 동작해서 데이터베이스에 업데이트 sql이 실행

 

주의할점 : 변경감지 기능을 사용하면 원하는 속성만 선택해서 값을 변경 할 수 있지만 병합 기능으로 사용한다면 모든 속성이 변경됨.

병합시 값이 없으면 null로 업데이트 할 위험도 있음(병합은 모든 필드를 교체)

실무에서는 null값이 들어갈 위험도 있으니 병합보다는 변경 감지 기능을 통해 준영속 엔티티를 업데이트 하는게 좋음

(컨트롤러에서 어설프게 엔티티 생성하지 않기, 트렌젝션이 있는 서비스 계층에 식별자(id)와 변경할 데이터 명확하게 전달하기,

트렌젝션이 있는 서비스 계층에서 영속 상태의 엔티티 조회하고, 엔티티의 데이터 직접 변경하기 > 트렌젝션 커밋 시점에 변경 감지 실행)

 

컨트롤러에서 어설프게 엔티티를 생성하지 않기 위해서\

이렇게 아까 만든 업데이트item 를 사용하여서 값을 파라미터로 넘겨주었고,

업데이트 item도 값을 받아와서 바로 변경하게끔 값을 넣어주었다.

(파라미터가 많으면 새로운 DTO 클래스를 파서 값을 넣어주기)

AWS 리전, 가용영역

리전이란? 

aws에서 수많은 컴퓨팅 서비스를 하려면 당연히 대규모의 서버 컴퓨터를 모아놓을 곳이 필요하는데 이때 한곳에 몰아넣게 되면 2가지 문제가 생김

(자연재해 발생시 모든 서비스 마비, 모든 자원이 북미에 있다면 아시아지역에서의 서비스가 느려짐)

따라서 위 문제점 해결하기 위하여 자원을 여러곳에 분산시켜 놓은것

 

가용영역은 리전을 한번 더 분산시켜서 놓은것

 

AWS VPC

서브넷

흔히 사용되는 IPv4의 주소 체계는 클래스를 나누어 IP를 할당한다. 하지만 이 방식은 매우 비효율적이다. 

예를들어서 어떤 기관에 A클래스를 할당한다고 하면 16,777,214개의 호스트를 할당 할 수 있는데 이 기관이 100개의 호스트를 할당한다고 하더라도 16,777,114개의 호스트를 낭비하게 된다. 

이러한 비효율성을 해결하기 위해서 네트워크 장치들의 수에 따라서 효율적으로 사용할 수 있는 서브넷이 등장하게 되었다.

 

IP 주소란?

Internet Protocol의 약자로 네트워크 통신을 할때 사용되는 프로토콜.

IP 주소를 사용하는 이유는 각각의 host들을 구분하여 데이터를 정확하게 송수신하기 위함.

 

IP 주소는 IPv4와 IPv6체계로 나뉨

 

IPv4

3자리 숫자가 4마디로 표기되는 방식.

각 마디는 옥텟이라고 명칭. 

위 주소는 내부적으로 32비트로 처리됨(각 마디당 8비트)

예를들어서 192.168.123.123은 11000000.10101000.1111011.1111011로 표시됨.

IPv4는 한 옥텟당 256개(2의 8승)를 할당 할 수 있어서 4,294,967,296개의 주소를 만들수 있음(256의 4승) = 약 42억개

하지만 인터넷 환경이 발달함에 따라서 어마어마하게 많은 IP주소가 필요해져서 IPv4 주소 체계로는 IP주소를 할당하기에 힘들어졌음.

이때 새로운 주소 체계인 IPv6가 나오게됨.

 

IPv4 클래스

IP 주소는 대역에 따라서 A,B,C,D,E 클래스로 나뉘는데 클래스를 구분함으로써 클래스내에서 네트워크 ID와 호스트 ID를 구분함.

A class : 대규모 네트워크 환경에 쓰이며, 첫번째 마디 숫자가 0~127 까지 사용됨

B class : 중규모 네트워크 환경에 쓰이며, 첫번째 마디 숫자가 128~191 까지 사용됨

C class : 소규모 네트워크 환경에 쓰이며, 첫번째 마디 숫자가 192~223까지 사용됨

D class : 멀티캐스팅용으로 잘 쓰이지 않는다.

E class : 연구 / 개발용 혹은 미래에 사용하기 위해 남겨놓은 클래스로 일반적인 용도로 사용되지않음.

 

A class는 하나의 네트워크가 가질수있는 호스트수가 가장 많은 클래스.

네트워크 영역은 앞 8비트가 차지하고 나머지 24비트는 호스트 영역

예를들어  18.123.123.123이라는 IP주소가 있다면 18은 네트워크 ID를 나타내고 123.123.123은 호스트 ID

첫번째 옥텟이 가질수 있는 범위는 0~127이고 1개의 네트워크 영역이 각각 가질수 있는 호스트 ID는( 2의 24승 )-2이다.

-2를 해준 이유는 시작주소는 x.0.0.0은 네트워크 주소로 사용하고 마지막 주소인 x.255.255.255는 브로드캐스트 주소로 사용되기 때문

 

예를들어서 13.x.x.x 네트워크 주소에서 호스트 영역을 할당한다고 하면 13은 네트워크 영역이고 x.x.x부분은 호스트 영역으로 0.0.0과 255.255.255를 제외한 13.0.0.1 ~ 13.255.255.254 까지 (2의 24승)-2 개를 할당할 수 있는 것이다.

 

B class는 중규모 네트워크에서 사용되는데 네트워크 영역은 앞의 16비트가 차지하고, 호스트 영역은 뒤 16비트가 차지한다.

예를들어 151.123.123.123이라는 IP 주소가 있다면 151.123은 네트워크 ID를 나타내고 123.123은 호스트 ID를 나타낸다.

첫번째 옥텟의 범위는 128~191이고 1개의 네트워크 영역이 각각 가질수있는 호스트 ID는 (2의 16승) -2 개이다.

 

C class 는 소규모 네트워크에서 사용되며 네트워크 영역은 앞의 24비트가 차지하고 호스트 영역은 뒤 6비트가 차지한다.

예를들어 201.123.123.123이라는 IP주소가 있다면 201.123.123은 네트워크 ID를 나타내고 121은 호스트 ID를 나타낸다.

첫번째 옥텟의 범위는 192~223이고 1개의 네트워크 영역이 각각 가질수 있는 호스트 ID는 (2의 8승) -2 개이다.

 

클래스 구분을 예시를 들어보면

 

132.12.11.4

클래스 : B

네트워크 영역 : 132.12

호스트 영역 : 11.4

 

10.3.4.1

클래스 : A

네트워크 영역 : 10

호스트 영역 : 3.4.1

 

203.10.1.1

클래스 : C

네트워크 영역 : 203.10.1

호스트 영역 : 1

 

 

IPv6

위에 IPv4 주소 체계만으로는 주소가 부족하여서 IPv6 가 나왔다고 말했는데, IPv6는 32비트 체계가 아닌 128비트 체계의 인터넷 프로토콜을 의미한다.

즉, 2^128 = 340,282,366,920,938,463,463,374,607,431,768,211,456 개의 주소를 할당할 수 있다.

16비트씩 8자리로 각 자리를 ':'로 구분한다.

여기서 문제는 아직까지도 IPv4에서 IPv6로의 전환이 완료되지않았다는 점이다.

아직 많은 라우터들이 IPv4를 사용한다. 

IPv6로의 전환은 많은 시간과 비용이 들기 때문에 완료되기까지 적지않은 시간이 걸릴 것이다.

현재는 IPv4와 IPv6를 혼용해서 사용하는데 IPv4 라우터에서 tunneling이라는 방식을 사용해서 IPv6 데이터그램을 전송한다.

 

다시 서브넷으로 돌아와보자.

 

서브넷은 IP주소에서 네트워크 영역을 부분적으로 나눈 부분 네트워크를 뜻한다. 이러한 서브넷을 만들때 사용되는것이 서브넷 마스크이다.

즉, 서브넷 마스크는 IP 주소 체계의 네트워크 ID와 호스트 ID를 분리하는 역할을한다. 

 

예를들어 C class는 기본적으로 앞의 24비트가 네트워크 영역, 뒤에 8비트가 호스트 영역으로 되어있는데, 이때 서브넷 마스크를 이용하면 원본 네트워크를 여러개의 네트워크로 분리할 수 있다. 이 과정을 서브넷팅이라고 한다.

기본 서브넷 마스크

각 클래스마다의 서브넷 마스크이다(D,E class는 사용하지않음)

이런 기본 서브넷 마스크를 사용하게되면 IP주소의 네트워크 id와 호스트 id를 구분할 수 있다.

IP 주소에 서브넷 마스크를 AND 연산하면 네트워크 ID가 된다.

예를들어 위에 사진처럼 저 C class의 IP주소가 있다고하면 C class의 기본 서브넷 마스크는 255.255.255.0이므로 and 연산을 해주면

네트워크 ID가 나온다. 이때 서브넷 마스크의 네트워크 ID 부분은 연속적으로 1이있어야하고 호스트ID부분은 0이 연속적으로 나와야한다.

 

예시의 IP주소를 보면 /24라는것이 붙어있는걸 볼 수 있을텐데 이것은 서브넷 마스크의 비트수(왼쪽에서부터 1의 갯수)를 나타낸다.

즉 /24는 해당 IP의 서브넷 마스크의 왼쪽에서부터 24개가 1이라는 것을 말해준다.

 

하지만 애초에 IP클래스들은 네트워크 영역이랑 호스트 영역이랑 나뉘어져있는데 굳이 서브넷 마스크가 왜 필요할까?

-> 서브넷팅을 하여 효율적인 네트워크 사용을 위해서

 

서브넷팅

서브넷팅은 IP주소 낭비를 방지 하기 위해서 원본 네트워크를 여러개의 서브넷으로 분리하는 과정을 뜻한다.

서브넷팅은 서브넷 마스크의 비트수를 증가시키는 것이라고 보면 된다. 서브넷 마스크의 비트수를 1씩 증가시키면 할당할 수 있는 네트워크가 2배수로 증가하고 호스트수는 2배로 감소한다.

예를들어서 C class 인 192.168.32.0/24를 서브넷 마스크의 비트수를 1 증가시켜서 192.168.32.0/25로 변경한다고 하면 위에 사진처럼

192.168.32.0/24는 원래 하나의 네트워크였지만 이때 할당 가능한 호스트의 수는 2의8승-2인 254개이다. 이때 서브넷 마스크의 비트수를 1 증가시켜서(서브넷팅) 192.168.32.0/25로 변경하게 되면 네트워크 id 부분을 나타내는 부분이 24비트에서 25비트로 증가하고 호스트 id를 나타내는 부분이다 8개비트에서 7개 비트로 줄어든다. 즉 할당 가능한 네트워크 수가 2개로 증가하고 각 네트워크당 할당가능한 호스트 수는 2의7승-2인 126개로 줄어든다 또한 서브넷 마스크가  255.255.255.128로 변한것을 확인할 수 있다.

 

서브넷의 구성

 

211.100.10.0/24 네트워크를 각 서브넷당 55개의 Host를 할당할 수 있도록 서브넷팅 한다고 하자.

a) 서브넷 마스크를 구하시오.

호스트 id의 비트가 6개라면 2의 6승은 62개의 호스트id를 할당할 수 있으므로 충분하다.

그렇다면 32개의 비트중 26개가 네트워크 id(서브넷 마스크의 비트 갯수)이므로  1111111.11111111.11111111.11000000가 서브넷 마스크가 될것이다.

즉 255.255.255.192이다.

 

b) 서브넷의 개수를 구하시오.

 

--- 개념 추후 작성 ----

 

 

라우팅(Routing)

네트워크 세계에서 라우팅이란, 패킷에 포함된 주소등의 상세 정보를 이용하여 목적지까지 데이터 또는 메세지를 체계적으로 다른 네트워크에 전달하는 경로 선택 및 스위칭하는 과정을 이야기한다.

(= 라우팅이란 데이터가 전달되는 과정에서 여러 네트워크들을 통과해야하는 경우가 생기는데, 여러 네트워크들의 연결을 담당하고 있는 라우터 장비가 데이터의 목적지가 어디인지 확인하여 빠르고 정확한 길을 찾아 전달해주는것)

 

라우팅을 위해서 필요한 정보

 

- 출발지와 목적지의 네트워크 정보

- 목적지로 가는 모든 경로 : 출발지와 목적지 사이의 어떤 경로들이 있는지 알아야 최적경로 선택 가능

- 최적 경로 : 데이터 전달 위해서 모든 경로를 사용할 필요가 없기때문에 학습한 경로중 최적의 경로 하나 선택(이 경로 저장하는 곳은 routing table이라고 부르며 L3 장비는 이 table 정보를 사용하여 패킷 전달)

- 지속적인 네트워크 상태 확인 : 데이터 전달해주려는 경로 알지만 만약 그 경로가 다운된 상태라 사용하지 못하면 routing table에 저장된 경로로 전달이 가능한 상태인지 지속적으로 네트워킹 상태를 확인하여서 네트워크 정보를 항상 올바른 최신정보로 유지해야함.

 

 

라우팅 테이블

 

목적지까지 갈수있는 모든 가능성이 있는 경로들중에서 가장 효율이라고 판단되는 경로 정보는 패킷을 전달할때 바로 참고해서 사용할 수 있도록 따로 모아두는데 이 공간을 라우팅 테이블 이라고 한다.

라우터는 패킷의 목적지와 목적지를 가려면 어느 인터페이스로 가야하는지 자신의 라우팅 테이블에 가지고 있고, 패킷의 목적지 주소를 라우팅 테이블과 비교하여 어느 라우터에게 넘겨줄지 판단하게된다. 

따라서 라우팅 프로토콜의 가장 중요한 목적이 바로 라우팅 테이블 구성이다.

 

 

정적 라우팅과 동적 라우팅

정적 라우팅

수동으로 라우팅 테이블을 만드는것

입력된 라우팅 정보가 수정하기 전에는 이전의 값이 변하지않고 고정된 값을 유지하며 라우팅 정보는 관리자가 수동으로 입력함.

장점: 관리자에 의한 라우팅 정보만 참조하기에 부담이 없어 빠르고, 안정적, 메모리 소모도 적음, 라우터간에 데이터 교환이 없으므로 대역폭을 절약할 수 있음. 외부에 정보 노출 안하기에 보안에도 좋음

단점: 각 경로를 수동으로 추가해주어야하기에 번거롭고, 정해진 경로에 장애가 발생할 경우 네트워크 전체에 장애 발생

 

동적 라우팅

접하는 라우터들이 라우팅 정보를 서로 교환하여 라우팅 테이블을 자동으로 만드는 것.

장점: 라우터가 서로 라우팅 정보를 주고받아 자동으로 라우팅 테이블을 작성하기 때문에 관리자는 초기 설정만해주면되어서 간단함, 네트워크의 규모가 커져도 자동으로 라우팅 테이블을 갱신하기 때문에 규모가 큰 네트워크에서 사용이 가능함.

단점: 다른 라우터들과도 계속 통신하기때문에 많은 대역폭 소비

 

 

CIDR

CIDR은 클래스없는 라우팅간 도메인 기법이다. 

즉, 도메인간의 라우팅에 사용되는 인터넷 주소를 원래 IP 주소 클래스 체계를 쓰는거보다 더욱 능동의적으로 할 수 있도록 할당하여 지정하는 방식 중 하나.

IP주소 클래스를 배우면서 같이 배우는게 서브넷 마스크, 서브넷팅인데 서브넷팅과의 차이가 애매한데 서브넷팅 자체는 IP클래스에 국한되지않고 더욱 더 IP주소를 쪼개는 방식인데 이게 바로 클래스간 도메인 없는 라우팅 기법이다.

결론적으로는 서브네팅 ⊂ CIDR 이런 관계가 성립.

서브넷팅 뿐만 아니라 서브넷을 합치는 슈퍼네팅 역시 CIDR이다.

정리하자면 IP를 나누고 합치는 기술들을 다 CIDR이라고 보면 된다.

 

 

CIDR 표기법

cidr은 네트워크 정보를 여러개로 나누어진 sub-network들을 모두 나타낼 수 있는 하나의 network로 통합해서 보여주는 방법이라고 한다.

 

 

VPC

vpc는 기본적으로 가상의 네트워크 영역이기에 사설 아이피 주소를 가지게된다.

사설 아이피 대역은 10.0.0.0/8  172.16.0.0/12  192.168.0.0/24 이렇게 3개의 대역을 가지며, 하나의 vpc에는 위의 네트워크 대역, 혹은 서브넷 대역이 할당 가능하다.

 

10.0.0.0/8의 서브넷인 10.0.0.0/16도 vpc에 할당이 가능하다.

vpc는 실제로 사용시 vpc 자체에서도 서브넷을 나눠서 사용하게 된다.

예시로 10.0.0.0/16의 아이피 주소를 VPC에 할당한 상황에서,

VPC를 원하면 다시 서브넷으로 나눠서 각각 서브넷을 원하는 가용영역에 배치하여 사용하게 됨.

 

이때 vpc의 서브넷 아이피 대역에서는 실제 네트워크와 달리 총 5개의 아이피 주소를 호스트에 할당 할 수 없음.

  1. 첫 주소 : 서브넷의 네트워크 대역
  2. 두번째 : VPC 라우터에 할당
  3. 세번째 : Amazon이 제공하는 DNS에 할당
  4. 미래를 위해 예약
  5. 브로드 캐스트 주소

이 5가지 항목을보고 VPC내부적으로 라우터가 있고 그렇다면 VPC 내부 서브넷끼리 통신이 된다는 것을 알아야함

 

VPC와 외부 통신

VPC가 어떻게 외부와 통신을 할까?

사설 아이피 대역은 공용 아이피 대역과 통신이 불가능하다 그런데 AWS로 인프라를 구축하면 통신이 되는 이유는 무엇일까?

Public subnet은 VPC 서브넷 중 외부와 통신이 월활하게 되는 서브넷 대역이고, Private Subnet은 외부와 통신이 되지않는 서브넷 대역이다.

 

Public subnet

AWS의 internet gateway 를 통해 해당 서브넷을 퍼블릭 서브넷이 되게 할 수 있다.

서브넷이 외부와 통신 할때, internet gateway를 거치게 하면 외부와 통신이 가능하게 한다.

이때 네트워크 패킷이 이동할때 특정 방향으로 가게한다 = 라우팅 인걸 알수 있다.

퍼블릭 서브넷으로 만들고 싶은 서브넷을 인터넷 게이트웨이를 통해 밖으로 나가도록 라우팅 테이블을 설정해주어야한다.

위 그림처럼 인터넷 게이트웨이를 만들고 라우팅 테이블에서 

Destination : 0.0.0.0/0(모든 IP주소를 의미 = 외부 모든 아이피 = 밖으로 나갈때)

Target : 만들어둔 인터넷 게이트 웨이 식별자(해당 그림에서는 igw-1234567901234567)

이렇게 만들어진 라우팅 테이블을 내가 원하는 서브넷에 연결하여 퍼블릭 서브넷을 만들게 된다.

 

Destination: 10.0.0.0/16 = 목적지: 10.0.0.0/16(VPC 주소 = VPC로 들어올때)

Target: Local = (내부 VPC 라우터가 알아서 잘 보내줌)

 

Private Subnet

 

퍼블릭 서브넷과 달리, 아무런 조치를 취하지않아 (VPC가 기본적으로 사설 아이피) 외부와 단절된 서브넷

 

그럼 private subnet은 무슨 의미가 있을까?

 

사설 아이피 대역의 역할

1. 부족한 아이피 주소 문제를 완화

한국은 NAT이라는 서비스를 사용할 수 있는데 사실 공용 아이피 주소는 공유기에만 할당이되고, 공유기에 연결한 디바이스들은 사설 아이피 대역을 받게된다.

외부 인터넷은 공유기로 먼저 데이터를 보내고, 공유기는 포트를 통해 각 디바이스들을 구분하여 데이터를 보내주게 된다.

공유기의 80번 포트는 192.168.0.1 이 할당된 노트북

공유기의 8080번 포트는 192.168.0.2가 할당된 PC가 연결된 것

 

포트포 워딩이란?

하나의 공용 아이피 주소를 가진 공유기가 자신의 포트를 통해 올바른 사설 아이피 주소를 가진 디바이스에게 데이터를 주는 것.

 

 

2. 높은 보안성

외부 네트워크의 디바이스는 공유기 뒤에 사설 아이피를 가지고 있음.

이 숨어있는 디바이스로 직접 데이터를 절대 전송할 수 없고, 무조건 공유기를 거쳐야한다.

이때 공유기가 이상한 데이터는 버려준다면 이는 보안성 측면에선 좋다.

 

Private subnet을 사용하는 이유는 소중한 자원의 보호 때문입니다.

우리가 프로젝트를 진행하며 인프라를 구축할 때는 사실 Private subnet을 사용하지 않아도된다.

하지만 릴리즈 서버의 경우는 실제 고객의 데이터가 저장되는 데이터베이스를 보호해야하므로,

데이터베이스를 private subnet에 배치하는 것이 필요하다.

 

그럼 2가지 의문이 생기게되는데

1. private subnet에 두면 외부와 통신이 안되는데 그럼 데이터 베이스를 못쓰는거 아닌가?

2. 데이터베이스에 원격으로 접속하고싶은데 안되나?

이다.

 

1번은 데이터 베이스를 사용하는 (DB에 데이터를 저장하려는) EC2등과 같은 컴퓨팅 자원을 같은 VPC에 배치하면된다.(같은 VPC의 서브넷은 통신이 가능하다)

2번은 private subnet의 bastion host 개념을 이해하면되는데

릴리즈 인프라에서 사용될 데이터베이스를 보호하기 위해서 private subnet에 배치하였고, 실제로 데이터가 잘 저장이 되었는지 mysql workbench 혹은 datagrip으로 원격 접속하여 직접 눈으로 편하게 확인하고싶은데 database는 private subnet에 위치하여 데이터그립으로는 절대 원격접속이 안되는 상황이다. 이럴 경우 private subnet과 같은 VPC에 존재하는 public subnet의 도움을 받으면 된다.

이때 private subnet의 자원에 접속이 되도록 도와주는 호스트를 bastion host라고 한다.

 

실제로 데이터그립으로 private subnet의 데이터 베이스에 원격 접속을 하기 위해서는 SSH 터널링이라는 기술이 필요하나, 데이터그립에서 아주 쉽게 설정이 가능하다.

'Server' 카테고리의 다른 글

Server  (0) 2024.03.21

주문 + 배송정보 + 회원을 조회하는 API를 만들것이다.
지연 로딩 때문에 발생하는 성능 문제를 단계적으로 해결해볼 것이다
.

 

 
  * Order -> Member
 * Order -> Delivery
 *
 */
@RestController
@RequiredArgsConstructor
public class OrderSimpleApiController {
    private final OrderRepository orderRepository;
/**
* V1. 엔티티 직접 노출
* - Hibernate5Module 모듈 등록, LAZY=null 처리 * - 양방향 관계 문제 발생 -> @JsonIgnore
*/
    @GetMapping("/api/v1/simple-orders")
    public List<Order> ordersV1() {
        List<Order> all = orderRepository.findAllByString(new OrderSearch());
        for (Order order : all) {
order.getMember().getName(); //Lazy 강제 초기화
order.getDelivery().getAddress(); //Lazy 강제 초기환 }
return all; }
}

 

엔티티를 직접 노출하는 것은 좋지 않다. (앞장에서 이미 설명)
`order` `member` `order` `delivery` 는지연로딩이다.따라서실제엔티티대신에프록시존재

jackson 라이브러리는 기본적으로 이 프록시 객체를 json으로 어떻게 생성해야 하는지 모름 예외 발생

`Hibernate5Module` 을 스프링 빈으로 등록하면 해결

 

하이버네이트 모듈 등록

스프링 부트 버전에 따라서 모듈 등록 방법이 다르다. 스프링 부트 3.0 부터는 `javax -> jakarta` 로 변경되어서 지 원 모듈도 다른 모듈을 등록해야 한다.

 

다음으로는 JpashopApplication 에 다음 코드를 추가해줄것이다.

@Bean
 Hibernate5Module hibernate5Module() {
     return new Hibernate5Module();
 }

 

만약 스프링 부트 3.0 이상을 사용하면 다음을 참고해서 모듈을 변경해야 한다. 그렇지 않으면 다음과 같은 예외가 발생 한다.

java.lang.ClassNotFoundException: javax.persistence.Transient

 

스프링 부트 3.0 이상: Hibernate5JakartaModule 등록
build.gradle 에 다음 라이브러리를 추가해야한다.
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-hibernate5- jakarta'

 

 

다음과 같이 설정해준다면 강제로 지연 로딩이 가능해진다.

@Bean
 Hibernate5Module hibernate5Module() {
Hibernate5Module hibernate5Module = new Hibernate5Module();
//강제 지연 로딩 설정 hibernate5Module.configure(Hibernate5Module.Feature.FORCE_LAZY_LOADING,
 true);
     return hibernate5Module;
}

 

지연 로딩(LAZY)을 피하기 위해 즉시 로딩(EARGR)으로 설정하면 안된다! 즉시 로딩 때문에 연관관계가 필요 없는 경우에도 데이터를 항상 조회해서 성능 문제가 발생할 수 있다. 즉시 로딩으로 설정하면 성능 튜닝이 매 우 어려워 진다.
항상 지연 로딩을 기본으로 하고, 성능 최적화가 필요한 경우에는 페치 조인(fetch join)을 사용해라!

 

package jpabook.jpashop.repository.order.simplequery;
 import jpabook.jpashop.domain.Address;
 import jpabook.jpashop.domain.OrderStatus;
 import lombok.Data;
 import java.time.LocalDateTime;
 @Data
 public class OrderSimpleQueryDto {
private Long orderId;
private String name;
private LocalDateTime orderDate; //주문시간 private OrderStatus orderStatus;
private Address address;
     public OrderSimpleQueryDto(Long orderId, String name, LocalDateTime
 orderDate, OrderStatus orderStatus, Address address) {
         this.orderId = orderId;
         this.name = name;
         this.orderDate = orderDate;
         this.orderStatus = orderStatus;
 
         this.address = address;
    }
}

OrderSimpleQueryDto 리포지토리에서 DTO 직접 조회해주었다.

 

일반적인 SQL을 사용할 때 처럼 원하는 값을 선택해서 조회 `new` 명령어를 사용해서 JPQL의 결과를 DTO로 즉시 변환

SELECT 절에서 원하는 데이터를 직접 선택하므로 DB 애플리케이션 네트웍 용량 최적화(생각보다 미비) 리포지토리 재사용성 떨어짐, API 스펙에 맞춘 코드가 리포지토리에 들어가는 단점

 

package jpabook.jpashop.repository.order.simplequery;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Repository;
 import javax.persistence.EntityManager;
 import java.util.List;
  
 @Repository
@RequiredArgsConstructor
public class OrderSimpleQueryRepository {
    private final EntityManager em;
    public List<OrderSimpleQueryDto> findOrderDtos() {
        return em.createQuery(
                "select new
jpabook.jpashop.repository.order.simplequery.OrderSimpleQueryDto(o.id, m.name,
o.orderDate, o.status, d.address)" +
} }

OrderSimpleQueryRepository 조회 전용 리포지토리

 

 

OrderRepository에 다음 추가 코드를 적어준다.

public List<Order> findAllWithMemberDelivery() {
     return em.createQuery(
}
```
"select o from Order o" +
        " join fetch o.member m" +
        " join fetch o.delivery d", Order.class)
.getResultList();

 

엔티티를 DTO로 변환하거나, DTO로 바로 조회하는 두가지 방법은 각각 장단점이 있다. 둘중 상황에 따라서 더 나은 방법을 선택하면 된다. 엔티티로 조회하면 리포지토리 재사용성도 좋고, 개발도 단순해진다. 따라서 권장하는 방법은 다음과 같다.

 

쿼리 방식 선택 권장 순서

  1. 우선 엔티티를 DTO로 변환하는 방법을 선택한다.
  2. 필요하면 페치 조인으로 성능을 최적화 한다. 대부분의 성능 이슈가 해결된다.
  3. 그래도 안되면 DTO로 직접 조회하는 방법을 사용한다.
  4. 최후의 방법은 JPA가 제공하는 네이티브 SQL이나 스프링 JDBC Template을 사용해서 SQL을 직접 사용한다.

 

주문내역에서 추가로 주문한 상품 정보를 추가로 조회하자. Order 기준으로 컬렉션인 OrderItem와 Item이 필요하다.

 

package jpabook.jpashop.api;
 import jpabook.jpashop.domain.Address;
 import jpabook.jpashop.domain.Order;
 import jpabook.jpashop.domain.OrderItem;
 import jpabook.jpashop.domain.OrderStatus;
 import jpabook.jpashop.repository.*;
 import lombok.Data;
 import lombok.RequiredArgsConstructor;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RestController;
 import java.time.LocalDateTime;
 import java.util.List;
    
 import static java.util.stream.Collectors.*;
/**
* V1. 엔티티 직접 노출
* - 엔티티가 변하면 API 스펙이 변한다.
* - 트랜잭션 안에서 지연 로딩 필요
* - 양방향 연관관계 문제 *
* V2. 엔티티를 조회해서 DTO로 변환(fetch join 사용X)
* - 트랜잭션 안에서 지연 로딩 필요
* V3. 엔티티를 조회해서 DTO로 변환(fetch join 사용O)
* - 페이징 시에는 N 부분을 포기해야함(대신에 batch fetch size? 옵션 주면 N -> 1 쿼리로 변경 가
능) *
* V4. JPA에서 DTO로 바로 조회, 컬렉션 N 조회 (1 + N Query)
* - 페이징 가능
* V5. JPA에서 DTO로 바로 조회, 컬렉션 1 조회 최적화 버전 (1 + 1 Query)
* - 페이징 가능
* V6. JPA에서 DTO로 바로 조회, 플랫 데이터(1Query) (1 Query)
* - 페이징 불가능... *
*/
@RestController
@RequiredArgsConstructor
public class OrderApiController {
    private final OrderRepository orderRepository;
/**
* V1. 엔티티 직접 노출
* - Hibernate5Module 모듈 등록, LAZY=null 처리 * - 양방향 관계 문제 발생 -> @JsonIgnore
*/
    @GetMapping("/api/v1/orders")
    public List<Order> ordersV1() {
        List<Order> all = orderRepository.findAllByString(new OrderSearch());
        for (Order order : all) {
order.getMember().getName(); //Lazy 강제 초기화 order.getDelivery().getAddress(); //Lazy 강제 초기환
List<OrderItem> orderItems = order.getOrderItems(); orderItems.stream().forEach(o -> o.getItem().getName()); //Lazy 강제
초기화

 }
return all; }
}

orderItem , item 관계를 직접 초기화하면 Hibernate5Module 설정에 의해 엔티티를 JSON으로 생성한 다.
양방향 연관관계면 무한 루프에 걸리지 않게 한곳에 @JsonIgnore를 추가해야 한다.
엔티티를 직접 노출하므로 좋은 방법은 아니다.

 

엔티티를 DTO로 변환

@GetMapping("/api/v2/orders")
 public List<OrderDto> ordersV2() {
     List<Order> orders = orderRepository.findAllByString(new OrderSearch());
     List<OrderDto> result = orders.stream()
             .map(o -> new OrderDto(o))
             .collect(toList());
     return result;
 }

 

다음으로 OrderApiController에 추가해준다.

 

@Data
 static class OrderDto {
private Long orderId;
private String name;
private LocalDateTime orderDate; //주문시간 private OrderStatus orderStatus;
private Address address;
private List<OrderItemDto> orderItems;
     public OrderDto(Order order) {
         orderId = order.getId();
         name = order.getMember().getName(); 
 } }
orderStatus = order.getStatus();
address = order.getDelivery().getAddress();
orderItems = order.getOrderItems().stream()
        .map(orderItem -> new OrderItemDto(orderItem))
        .collect(toList());
@Data
static class OrderItemDto {
private String itemName;//상품 명 private int orderPrice; //주문 가격 private int count; //주문 수량
    public OrderItemDto(OrderItem orderItem) {
        itemName = orderItem.getItem().getName();
        orderPrice = orderItem.getOrderPrice();
        count = orderItem.getCount();
} }

지연 로딩은 영속성 컨텍스트에 있으면 영속성 컨텍스트에 있는 엔티티를 사용하고 없으면 SQL을 실행한 다. 따라서 같은 영속성 컨텍스트에서 이미 로딩한 회원 엔티티를 추가로 조회하면 SQL을 실행하지 않는다.

 

생성자 추가해주기

 

 public OrderQueryDto(Long orderId, String name, LocalDateTime orderDate,
 OrderStatus orderStatus, Address address, List<OrderItemQueryDto> orderItems) {
     this.orderId = orderId;
     this.name = name;
     this.orderDate = orderDate;
     this.orderStatus = orderStatus;
     this.address = address;
     this.orderItems = orderItems;
}

 

OrderQueryRepository에 추가

public List<OrderFlatDto> findAllByDto_flat() {
     return em.createQuery(
             "select new
 jpabook.jpashop.repository.order.query.OrderFlatDto(o.id, m.name, o.orderDate,
 o.status, d.address, i.name, oi.orderPrice, oi.count)" +
}
```
**OrderFlatDto** ```java
        " from Order o" +
        " join o.member m" +
        " join o.delivery d" +
        " join o.orderItems oi" +
        " join oi.item i", OrderFlatDto.class)
.getResultList();
 package jpabook.jpashop.repository.order.query;
import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.OrderStatus;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class OrderFlatDto {
    private Long orderId;
 private String name;
private LocalDateTime orderDate; //주문시간 private Address address;
private OrderStatus orderStatus;
private String itemName;//상품 명 private int orderPrice; //주문 가격 private int count; //주문 수량
    public OrderFlatDto(Long orderId, String name, LocalDateTime orderDate,
OrderStatus orderStatus, Address address, String itemName, int orderPrice, int
count) {
        this.orderId = orderId;
        this.name = name;
        this.orderDate = orderDate;
        this.orderStatus = orderStatus;
        this.address = address;
        this.itemName = itemName;
        this.orderPrice = orderPrice;
        this.count = count;
} }

 

OrderFlatDto 만들어주기

 

 

정리

엔티티 조회
엔티티를 조회해서 그대로 반환
: V1

엔티티 조회 후 DTO로 변환: V2 페치 조인으로 쿼리 수 최적화: V3 컬렉션 페이징과 한계 돌파: V3.1

컬렉션은 페치 조인시 페이징이 불가능
ToOne 관계는 페치 조인으로 쿼리 수 최적화
컬렉션은 페치 조인 대신에 지연 로딩을 유지하고
, `hibernate.default_batch_fetch_size` ,

`@BatchSize` 로 최적화 DTO 직접 조회
JPA에서 DTO를 직접 조회: V4

컬렉션 조회 최적화 - 일대다 관계인 컬렉션은 IN 절을 활용해서 메모리에 미리 조회해서 최적화: V5 플랫 데이터 최적화 - JOIN 결과를 그대로 조회 후 애플리케이션에서 원하는 모양으로 직접 변환: V6

**권장 순서**
1. 엔티티 조회 방식으로 우선 접근

  1. 페치조인으로 쿼리 수를 최적화
  2. 컬렉션 최적화
    1. 페이징 필요 `hibernate.default_batch_fetch_size` , `@BatchSize` 로 최적화
    2. 페이징 필요X 페치 조인 사용

2. 엔티티조회방식으로해결이안되면DTO조회방식사용
3. DTO 조회 방식으로 해결이 안되면 NativeSQL or 스프링 JdbcTemplate

참고: 엔티티 조회 방식은 페치 조인이나, `hibernate.default_batch_fetch_size` , `@BatchSize` 같 이 코드를 거의 수정하지 않고, 옵션만 약간 변경해서, 다양한 성능 최적화를 시도할 수 있다. 반면에 DTO를 직접 조회하는 방식은 성능을 최적화 하거나 성능 최적화 방식을 변경할 때 많은 코드를 변경해야 한다.

DTO로 조회하는 방법도 각각 장단이 있다. V4, V5, V6에서 단순하게 쿼리가 1번 실행된다고 V6이 항상 좋은 방법인 것은 아니다.
V4
는 코드가 단순하다. 특정 주문 한건만 조회하면 이 방식을 사용해도 성능이 잘 나온다. 예를 들어서 조회한 Order 데이터가 1건이면 OrderItem을 찾기 위한 쿼리도 1번만 실행하면 된다.

V5는 코드가 복잡하다. 여러 주문을 한꺼번에 조회하는 경우에는 V4 대신에 이것을 최적화한 V5 방식을 사용해 야 한다. 예를 들어서 조회한 Order 데이터가 1000건인데, V4 방식을 그대로 사용하면, 쿼리가 총 1 + 1000번 실행된다. 여기서 1Order 를 조회한 쿼리고, 1000은 조회된 Orderrow 수다. V5 방식으로 최적화 하면 쿼리가 총 1 + 1번만 실행된다. 상황에 따라 다르겠지만 운영 환경에서 100배 이상의 성능 차이가 날 수 있다. V6는 완전히 다른 접근방식이다. 쿼리 한번으로 최적화 되어서 상당히 좋아보이지만, Order를 기준으로 페이징 이 불가능하다. 실무에서는 이정도 데이터면 수백이나, 수천건 단위로 페이징 처리가 꼭 필요하므로, 이 경우 선택 하기 어려운 방법이다. 그리고 데이터가 많으면 중복 전송이 증가해서 V5와 비교해서 성능 차이도 미비하다.

package jpabook.jpashop.api;
 import jpabook.jpashop.domain.Member;
 import jpabook.jpashop.service.MemberService;
 import lombok.AllArgsConstructor;
 import lombok.Data;
 import lombok.RequiredArgsConstructor;
 import org.springframework.web.bind.annotation.*;
 import javax.validation.Valid;
 import java.util.List;
 import java.util.stream.Collectors;
 @RestController
 @RequiredArgsConstructor
 public class MemberApiController {
     private final MemberService memberService;
/**
* 등록 V1: 요청 값으로 Member 엔티티를 직접 받는다.
* 문제점
* - 엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.
* - 엔티티에 API 검증을 위한 로직이 들어간다. (@NotEmpty 등등)
* - 실무에서는 회원 엔티티를 위한 API가 다양하게 만들어지는데, 한 엔티티에 각각의 API를 위한
모든 요청 요구사항을 담기는 어렵다.
* - 엔티티가 변경되면 API 스펙이 변한다.
* 결론
* - API 요청 스펙에 맞추어 별도의 DTO를 파라미터로 받는다. */
     @PostMapping("/api/v1/members")
     public CreateMemberResponse saveMemberV1(@RequestBody @Valid Member member)
 {
         Long id = memberService.join(member);
         return new CreateMemberResponse(id);
     }
     @Data
     static class CreateMemberRequest {
         private String name;
     }
      @Data
     static class CreateMemberResponse {
         private Long id;
         public CreateMemberResponse(Long id) {
             this.id = id;
} }
}

회원 등록 api

 

@PutMapping("/api/v2/members/{id}")
 public UpdateMemberResponse updateMemberV2(@PathVariable("id") Long id,
 @RequestBody @Valid UpdateMemberRequest request) {
     memberService.update(id, request.getName());
     Member findMember = memberService.findOne(id);
     return new UpdateMemberResponse(findMember.getId(), findMember.getName());
}
 @Data
 static class UpdateMemberRequest {
     private String name;
 }
 @Data
 @AllArgsConstructor
 static class UpdateMemberResponse {
     private Long id;
     private String name;
 }

회원 수정 api

 

응답 값으로 엔티티를 직접 외부에 노출한 문제점

엔티티에 프레젠테이션 계층을 위한 로직이 추가된다.
기본적으로 엔티티의 모든 값이 노출된다.
응답 스펙을 맞추기 위해 로직이 추가된다. (@JsonIgnore, 별도의 뷰 로직 등등)
실무에서는 같은 엔티티에 대해 API가 용도에 따라 다양하게 만들어지는데, 한 엔티티에 각각의 API를 위 한 프레젠테이션 응답 로직을 담기는 어렵다.
엔티티가 변경되면 API 스펙이 변한다.
추가로 컬렉션을 직접 반환하면 항후 API 스펙을 변경하기 어렵다.(별도의 Result 클래스 생성으로 해결)

결론
API 응답 스펙에 맞추어 별도의 DTO를 반환한다.

 

 

package jpabook.jpashop;
import jpabook.jpashop.domain.*;
import jpabook.jpashop.domain.item.Book;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.PostConstruct;
import javax.persistence.EntityManager;
@Component
@RequiredArgsConstructor
public class InitDb {
private final InitService initService;
     @PostConstruct
    public void init() {
        initService.dbInit1();
        initService.dbInit2();
    }
    @Component
    @Transactional
    @RequiredArgsConstructor
    static class InitService {
        private final EntityManager em;
public void dbInit1() {
Member member = createMember("userA", "서울", "1", "1111"); em.persist(member);
            Book book1 = createBook("JPA1 BOOK", 10000, 100);
            em.persist(book1);
            Book book2 = createBook("JPA2 BOOK", 20000, 100);
            em.persist(book2);
            OrderItem orderItem1 = OrderItem.createOrderItem(book1, 10000, 1);
            OrderItem orderItem2 = OrderItem.createOrderItem(book2, 20000, 2);
            Order order = Order.createOrder(member, createDelivery(member),
orderItem1, orderItem2);
            em.persist(order);
}
public void dbInit2() {
Member member = createMember("userB", "진주", "2", "2222"); em.persist(member);
            Book book1 = createBook("SPRING1 BOOK", 20000, 200);
            em.persist(book1);
            Book book2 = createBook("SPRING2 BOOK", 40000, 300);
            em.persist(book2);
            Delivery delivery = createDelivery(member);

             OrderItem orderItem1 = OrderItem.createOrderItem(book1, 20000, 3);
            OrderItem orderItem2 = OrderItem.createOrderItem(book2, 40000, 4);
            Order order = Order.createOrder(member, delivery, orderItem1,
orderItem2);
            em.persist(order);
}
        private Member createMember(String name, String city, String street,
String zipcode) {
            Member member = new Member();
            member.setName(name);
            member.setAddress(new Address(city, street, zipcode));
            return member;
}
        private Book createBook(String name, int price, int stockQuantity) {
            Book book = new Book();
            book.setName(name);
            book.setPrice(price);
            book.setStockQuantity(stockQuantity);
            return book;
        }
        private Delivery createDelivery(Member member) {
            Delivery delivery = new Delivery();
            delivery.setAddress(member.getAddress());
            return delivery;
}
} }

 

 

+ Recent posts