728x90

Auto Scaling 적용 후 아키텍처 도식도

 

Auto Scaling이 필요한 이유

오토스케일링(Auto Scaling)은 클라우드의 유연성을 돋보이게 하는 핵심기술로 CPU, 메모리, 디스크, 네트워크 트래픽과 같은 시스템 자원들의 메트릭(Metric) 값을 모니터링하여 서버 사이즈를 자동으로 조절한다. 이를 통해 사용자는 예상치 못한 서비스 부하에 효과적으로 대응하고 비용 절감 효과를 볼 수 있다.

 

Auto Scaling 적용

Auto Scaling 서비스는 Launch Configuration을 생성하고, Auto Scaling Group을 생성한 후에 이용할 수 있다.

 

Launch Configuration 생성

1. Launch Configuration 생성

image

2. 서버 이미지 설정

image


나는 centos-7.8-64로 선택하였다.

 

3. 서버 설정
서버 이미지를 설정 하고 서버를 설정한다.

image


서버 타입을 설정하고, 서버에 init Script가 설정 되어 있는 서버에서만 SourceDeploy 서비스를 이용할 수 있다.

init Script는 다음과 같다.

image


참고: 링크

 

4. 서버 이름 설정

image


서버의 이름을 설정한다.

 

5. 서버 인증키 설정

image


인증키를 설정한다. NCP에서 발급 받은 .pem 파일로 인증을 한다. Auto Scaling이 생성 해내는 인스턴스에 접근할 때 사용된다.

이 설정이 마치면 Auto Scaling Launcher가 생성된다.

 

Auto Scaling Group 생성

1. Auto Scaling Group 생성

image

2. Launch Configuration 선택
Launch Configuration을 선택한다.

image

3. Auto Scaling Group 설정

image


오토스케일링을 생성하기 전에 오토스케일링 전용 Subnet을 만들어서 적용시켜준다.

입력사항 설명
최소 용량 그룹의 최소 서버 수
최대 용량 그룹에서 생성 가능한 최대 서버 수
기대 용량 서버의 수는 기대 용량값에 따라서 조정된다. 이 값은 최소 용량 이상, 최대 용량 이하여야 한다.
쿨다운 기본값(초) 실제 Scaling이 수행 중이거나 수행 완료된 이후에 모니터링 이벤트 알람이 발생하더라도 반응하지 않고 무시하도록 설정한 기간이다. 기본값은 300초이다.
헬스 체크 보류 기간 서버 인스턴스가 생성되어 상태가 ‘운영 중’으로 변했더라도, 서버의 업데이트 설치 등 작업에 의해서 헬스 체크에 정상 응답하지 못하는 경우가 생길 수 있다. 이런 경우 헬스 체크 보류 기간을 지정하면 해당 기간 동안에는 헬스 체크에 실패하더라도 서버 헬스에 이상이 있다고 판단하지 않는다.
헬스 체크 유형 헬스체크 유형이다. 서버, 로드벨런서가 있다.

 

4. 네트워크 접근 설정
ACG를 생성하여 적용시킨다.

5. 정책 설정

image

Scaling 정책은 3가지 방식으로 설정할 수 있다.

(1) 증감변경: 현재 그룹 크기와 상관없이 지정한 서버 대수를 직접 추가 또는 삭제하는 방법

(2) 비율변경: 현재 그룹 크기 대비 일정한 비율(%)로 서버를 증감시키는 방법.

(3) 고정값: 그룹 크기를 지정한 값으로 고정시키는 방법.

쿨다운(cooldown)이란 일단 Scaling이 수행되면 추가적인 알람이 발생하더라도 이에 반응하지 않도록 설정된 시간이다. 쿨다운 시간 동안은 Scaling의 수행이나 완료 여부에 상관없이 추가로 발생한 이벤트에 대해서 아무 동작을 하지 않게 된다.

그 다음에는 오토스케일링 작동 시 문자, 이메일 발송하는 설정을 하고 생성이 완료된다.

 

Auto Scaling 적용 결과

image

 

728x90

CI/CD 적용 후 아키텍처 도식도

 

CI/CD가 필요한 이유

  • 새로운 코드 변경사항이 정기적으로 테스트 빌드 되므로, 여러 명의 개발자가 동시에 어플리케이션 개발을 할 때 충돌 문제를 해결할 수 있다. 그리고 개발자의 변경 사항을 리포지토리에서 고객이 사용 가능한 프로덕션 환경까지 자동으로 릴리스하기 위함이다.
  • CI/CD 프로세스를 적용함으로써 수작업으로 진행되었던 많은 부수적인 일들을 줄일 수 있다. 그리하여 개발자는 개발에 집중할 수 있다.

 

CI/CD 적용

NBP(네이버 클라우드 플랫폼)에는 CI/CD 구축을 돕기 위해 SourceCommit, SourceBuild, SourceDeploy, SourcePipeline을 제공하고 있다. 나열한 순서대로 CI 부터 CD까지 적용을 돕는다. 그리고 SourcePipeline으로 CI/CD 모든 과정을 이어준다.

SourceCommit

1. 외부 레포지토리 복사를 클릭

image

"레포지토리 생성"을 누르면 빈 레포지토리가 생성되고 "외부 레포지토리 복사"를 누르면 외부의 레포지토리를 복사할 수 있다. 나는 깃헙에 있는 레포지토리를 복사하는 것을 선택했다.

 

2. 레포지토리 생성

image

레포지토리 이름과 복사할 Git URL을 입력한다.

그 다음 File Safer 상품과 연동할 것인지 묻는다. (File Safer는 고객이 업로드/다운로드 하는 파일의 악성코드 여부를 검사할 수 있는 서비스이다.)

처음에 연동을 하고 하루가 지나고 지불된 금액을 확인했는데 바로 10만원 가까이 나가있었다.. 그래서 나는 연동을 하지 않았다.

 

3. 생성

image

확인을 누르면 생성이 된다.

 

4. 생성된 레포지토리를 클릭하고 코드 이동을 누르면

imageimage

깃헙에 있었던 코드들이 그대로 옮겨져 있는 것을 볼 수 있다. 여기서 오해할 수 있는 점이 깃에 푸시를 해도 NCP의 SourceCommit 레포지토리에 반영은 되지 않는다. 그 의미는 깃헙의 깃액션 CI를 사용하지 못한다는 의미이다.

해당 SourceCommit 레포지토리에 반영을 하려면

image

따로 git clone을 해줘야한다. SourceCommit은 git 명령어를 지원해서 기존 깃헙 사용자들도 사용하기 편리할 것이다.

 

SourceBuild

1. 빌드 프로젝트 생성

image

2. Object Storage 연결

image

빌드한 결과물을 저장해두는 저장소 같은 개념이다. AWS의 S3랑 비슷한 개념을 가진 저장소이다.

 

3. 빌드 프로젝트 생성 - 기본 설정

image

여기서 중요한 곳은 빌드 대상인데, SourceCommit, 깃헙, Bitbucket에 있는 레포지토리 세 가지를 선택할 수 있다. 나는 SourceCommit을 선택하였다.

깃헙을 선택해서 깃헙에 있는 레포지토리에 push를 하면 그게 트리거가 되어서 배포 파이프라인이 작동되게끔 하고 싶었는데 NCP에서는 깃헙의 트리거를 못받는 것 같았다. 그래서 빌드 대상을 깃헙 대신 SourceCommit을 선택 하였다.

이 부분은 추후 젠킨스로 구현 해봐도 좋을 것 같다.

 

4. 빌드 프로젝트 생성 - 빌드 환경

빌드 환경 설정이다.

image

ubuntu 16.04 (x64) 환경에서 java로 빌드하게 하였다. 비용 문제로 컴퓨팅 유형은 제일 작은걸로 하였다.

그리고 나의 프로젝트에서 docker-compose를 사용하기 때문에 도커 환경이 필요했다. 그래서 도커 이미지 빌드 를 체크 해주었다. 도커 환경이 필요하지만 체크를 하지 않으면 명령어로 도커를 다운로드하고 설정하는 귀찮은 작업을 따로 해주어야한다.

 

5. 빌드 프로젝트 생성 - 빌드 명령어 설정

image

SourceCommit에 있는 프로젝트를 빌드 하고 필요한 명령어를 입력하는 곳이다.

나는 빌드 후 프로젝트 전체를 jar파일로 말아올려 서버에서 실행시킬 명령어인 ./gradlew bootjar 명령어를 입력 해주었고 깃헙을 거쳐 깃헙 액션을 실행시키지 못하는 환경을 대신해서 ./gradlew test 명령어를 실행시켜 빌드 후 테스트 과정도 거치게 하였다.

 

6. 빌드 프로젝트 생성 - 결과물 업로드 설정

image

빌드 후 결과물을 Object Storage에 저장할 때 경로를 설정하는 곳이다. 빌드 결과물은 .zip 파일로 저장이 된다. 빌드 결과물을 저장할 Storage를 설정하고 경로, 폴더/파일명을 설정한다. 그리고 백업파일 여부도 설정한다.

백업 사용을 설정하면 Object Storage에 따로 백업 폴더가 생성된다.

 

7. 빌드 프로젝트 생성 - 추가 상품 연동

image

추가 상품은 비용 문제로 연동하지 않았다.

 

SourceDeploy

1. 배포 프로젝트 생성

image

2. 프로젝트 이름 설정

image

3. 배포 환경 설정

image

배포 환경 설정에서 stage를 설정하고 배포 타겟을 설정한다. 나는 Auto Scaling 되고있는 서버 전체에 모두 배포를 하기 위해서 배포 타겟을 Auto Scaling으로 했다. 사실 Server를 체크 해서 해도 되지만 만약 Auto Scaling에서 서버를 자동적으로 늘릴 때 그 서버를 수동으로 포함 시켜줘야하기 때문에 Auto Scaling으로 체크 하였다.

 

4. 배포 시나리오 설정

설정이 끝나면 배포 시나리오를 설정 해야한다.

image

아래처럼 배포 시나리오를 설정하면 된다.

image

배포 전략에 지금은 기본으로 되어있지만 추후 블루/그린 배포 전략을 사용 해볼 예정이다.

배포 과정은 순차 배포, 동시 배포가 있는데 순차 배포가 더 낫다고 생각하여 순차배포를 선택하였다. 그 이유는 배포과정에 놓여진 서버는 로드벨런서가 트래픽을 보낼 수 없기 때문에 순차적으로 배포 될 때 배포 되고 있지 않는 서버들에게는 트래픽을 보낼 수 있기 때문에 무중단 배포가 된다고 생각하여 순차 배포가 낫다고 생각하였다.

배포 파일은 Object Storage에 있는 빌드된 zip 파일을 배포하는 것으로 설정 해두었다.

그 다음은 배포 명령어를 설정한다.

image

배포 전 실행, 파일 배포, 배포 후 실행으로 나뉜다.

설정할 때 주의할 점이 경로이다. 그리고 한 줄이 실행되면 원래의 경로로 돌아오기 때문에 &&로 구분하여 묶여있는 명령어는 한번에 처리해야한다.

 

배포 전 실행

구분 실행 명령어 설명
1 cd /usr/lib && wget https://download.java.net/java/GA/jdk16.0.2/d4a915d82b4c4fbb9bde534da945d746/7/GPL/openjdk-16.0.2_linux-x64_bin.tar.gz /usr/lib 경로로 가서 자바 다른 버전을 다운 받는다. (다른 버전을 받는 이유가 NCP가 지원하는 자바 버전이 최고 11버전이기 때문에 16버전을 다운 받기 위함)
2 tar -zxvf /usr/lib/openjdk-16.0.2_linux-x64_bin.tar.gz -C /usr/lib 위에서 받은 .tar 파일을 /usr/lib에 압축 풀기
3 yum install -y yum-utils yum-utils 를 최신 버전으로 업데이트를 한다.
4 yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo Docker Engine을 설치할 수 있도록 저장소를 추가한다.
5 yum install docker-ce docker-ce-cli http://containerd.io/ -y 도커 엔진 최신 버전을 설치한다.
6 curl -L "https://github.com/docker/compose/releases/download/1.27.4/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose 도커 컴포즈를 설치한다.
7 chmod +x /usr/local/bin/docker-compose 도커 컴포즈가 실행할 수 있게 권한을 부여 한다.

 

파일 배포

구분 소스 파일 경로 배포 경로 설명
1 skka.zip/ /root/deploy skka.zip 파일을 /root/deploy 경로에 배포한다.

 

배포 후 실행

구분 실행 명령어 설명
1 systemctl start docker 도커 엔진 수동으로 데몬 실행
2 systemctl enable docker 도커 서비스 등록하여 부팅시 실행 하도록 하기
3 /usr/local/bin/docker-compose -f /root/deploy/docker-compose.yml up -d /usr/local/bin/docker-compose 로 도커 컴포즈로docker-compose.yml 파일에 설정 되어 있는 도커 이미지 띄우기
4 nohup /usr/lib/jdk-16.0.2/bin/java -jar /root/deploy/build/libs/skka-0.0.1-SNAPSHOT.jar & bootjar로 말아 올린 jar 파일 백그라운드로 실행

 

SourcePipeline

1. 파이프라인 생성

image

2. 파이프라인 이름 설정

image

3. 파이프라인 구성

image

작업 추가를 눌러서 파이프라인을 구성해야 한다.

그리고 Trigger를 설정한다. 트리거가 설정되면 설정된 트리거가 발생하면 설정된 파이프라인이 실행된다. 나는 SourceCommit에 push가 되면 트리거가 발동되어 설정 되어 있던 파이프라인인 빌드, 배포가 순차적으로 실행된다.

다음은 설정된 화면이다.

image

 

728x90
깨달음의 발단: https://github.com/f-lab-edu/SSKA/pull/47#discussion_r1070973660

 

 

나는 지금까지 어떤 기능을 만들 때는 꼭 해당 기능 맞춤형 API 하나가 만들어져야 한다고 생각했다. 그런데 그게 아니었다.

기능이 있다고 그 기능의 API 하나를 설계 해야 하는 것이 아니라 그 로직을 설계 해야 하는 것이었다.

 

우리가 Restful 하게 API를 만드는 것은 리소스를 생성, 수정, 삭제, 조회하는 것이지 특정 기능만을 위한 api는 아니다

 

이번 SKKA 프로젝트를 하면서 "좌석 시간 변경" 기능을 만들고 API를 붙이려고 할 때, 해당 프로젝트에는 API가 총 4개가 있는데 ("좌석 예약", "좌석 이동", "좌석 이용 시간 변경", "좌석 예약 취소/퇴실") 기능별로 꼭 하나의 API를 설계 해야 한다고 생각하고 있었던 것이었다.

 

 

이렇게 하다 보니 코드의 반복이 계속 되었다. 그러다 보니 코드가 복잡해지며 서비스 로직에서 하나의 메소드에서 하나의 역할에 충실하지 못한 메소드가 나와야했다. 이것을 해소하기 위해서는 코드의 재사용이 필요했다. 예를 들어 "좌석 이동" 기능을 개발할 때 여러 로직들이 다른 API 에서 사용되는 서비스, 도메인 로직이 겹쳐, 이를 해결하기 위해  해당 기능("좌석 이동")을 개발할 때 "좌석 예약 기능"을 사용하면 풀리는 문제였다.

 

즉, 

 

이런식으로 "좌석 예약" 기능을 이용하여 다른 기능을 하는 API를 추가하면 코드의 반복을 줄일 수 있고 코드의 재사용이 가능해지며 하나의 역할에 충실할 수 있는 구조가 되었다.

728x90

REST API의 근간

과거에 다음, 한미르 같은 시절에는 WAS가 없었고 Nginx, apache 같은 Web Server 만 있었다. Web Server 는 정적인 파일을 serving 해준다. 사용자가 이미지 던져달라하면 던져주고 html 던져달라하면 던져주는 방식이다.

 

옛날에는 “im.html”을 던져달라하면 나의 개인정보가 나오는 html파일을 던져주곤 했다. 그러다 보니 그 당시 서버는 html 파일 서버였던 것이었다. 다시 말해 그 당시에는 우리가 그 html에 접근하기 위해서는 url에 “user/im.html” 를 요청하면 파일을 달라고 요청하는 것이었다. 그러면 html 웹 서버가 디렉토리에서 뒤져서 파일을 찾아서 주는 것이었다.

 

만약 그 위치에 파일이 존재하지 않으면 not found를 던져주는 것이고 url에 잘못 쳐서 “user/em.html” 을 요청하면 양식이 맞지않다고 400에러를 준다. 여기 개념에서 부터 Rest API가 나온 것이다.

 

 

REST API 설계

html파일도 정적 Resource를 호출하는 것이기 때문에 Rest API 는 어떻게 보면 Resource를 호출하는 것이다. 그러니까 우리가 Rest API 를 만들 때도 가상의 디렉토리가 있다고 생각하고 Rest API를 설계한다고 생각하면 쉽게 된다. 밑에를 예를 들면 파일 서버 안에 shops라는 폴더가 있고, 그 안에는 1번 id를 가진 애가 있고 얘는 “싸다김밥”이고, 2번 id를 가진 애가 있고 얘는 “메가커피”이다. 그러면 디렉토리 구조라고 쳤을 때 다음과 같은 구조를 가지게 된다.

 

shops
├── 1
│   ├── 싸다 김밥
└───└── 메가 커피

이런 구조에서 1번 디렉토리에 있는 정보를 들고오고 싶으면

GET + /shops/1

를 해서 들고오면 되는 것이다.

 

그리고 shops라는 디렉토리 내에 새로운 무언가를 만들려고 하면

POST + /shops

를 요청하게끔 하면 되는 것이다.

 

업데이트와 삭제도 마찬가지이다.

PUT + /shop/1

DELETE /shops/1

이런식으로 하면 된다.

 

 

REST API 행위 구분

Rest API의 특징중에 “유니폼 인터페이스”라고 있다. 유니폼이란 같은 옷을 입고 있다는 뜻이다. 이것을 제어 하는 것은 Http method(GET, POST, PUT, DELETE) 로 행위를 제어해야한다. 즉 End point는 동일한데 Http method를 가지고 행위를 다르게 한다는 뜻이다.

 

 

응용을 해보면

teams
├── 1
│   └── 법무팀
├── 2
│   └── 홍보팀
├── 3
└───└── 영업팀

이 있고

teams
└── 1
    └── memebers
           └── 홍길동

트리구조로 이렇게 "법무팀에서 일하는 홍길동 사원을 찾고싶을 때"

GET + /teams/1/members/1

요청으로 찾으면 “홍길동”이 결과로 나오는 것이다.

 

결론은 “파일” 처럼 보면 된다. 그러면 Rest API 설계가 굉장히 쉬워진다.
728x90

📌 API 요청이 들어오면 처리하는 과정에 대한 고찰

고찰의 시작: BaseEntity를 어느 패키지에 놓을지에 대한 고찰에서 시작되었다. ( 관련 PR )

보통 API를 만들 때, API 요청을 받고 Controller를 지나서 Service를 지나 Repository를 거치고 또 다시 Service로 가고 Controller로 나가는 구조이다. 여기서 요청할 때 처리하는 과정을 어떻게 처리할지에 따라 모든 설계가 달라진다. 해당 이슈는 SKKA 프로젝트에서 BaseEntity를 어느 패키지에 놓을지에 대한 고찰에서 나온 이슈이다. 어떻게 요청들을 처리할지 파헤쳐보자.




image




보통 (1)로도 충분하고, 나 또한 계속 1번으로 해 온 것같다. 간단하게 말하면 (1)의 구조로 하면 확장에 용이하지 않다. 그러면 (2)와 같이 세부적으로 나누면 무엇이 좋을지 한번 알아보자.



(CustomerWebRequestV1, CustomerWebResponseV1)


커스터머를 생성할 수 있는 API를 만든다고 생각하자. 그러면 클라이언트가 커스터머를 생성할 수 있는 파라메터를 줘 보자.

고객 객체는 다음과 같다.

 

class Customer { name, age, address }

 

POST /customers { name: “홍길동”, age: 30, address: “서울시 성동구” }


이 정보가 있으면 API에서 받아서 Customer 객체를 받아서 저장을 할 것이다.

그러면 Response로 (밑을 보자)

CustomerWebResponseV1 { id: 1 }

을 줄것이다.

 

이 Response는 결과가 도출 된 값인데 이 값은 어떻게 이용될까?


→ 사용자가 파라메터를 채워서 POST /customers API를 날리고 Response를 받아가는 것인데, 나중에 Customer를 식별할 수 있는 “id: 1” 정보를 클라이언트가 알아야 하는 정보인 것이다.


Response를 설계하는 방법은 방법론이 많다.

 

CustomerWebRequestV1와 CustomerWebResponseV1는 고객과의 한 약속이기 때문에 불변이다. 그러나 고객이 API를 통해 서버로 데이터를 보내면 서버가 데이터를 받고 그 데이터를 어떻게 안에서 대처하든 고객은 상관쓰지 않기 때문에 서버 개발자 마음대로 내부 로직을 처리한다. 이렇게 고객과의 약속은 절대 바뀌면 안된다. 신뢰성의 문제다. 다시 말해 내부 로직이 바뀌어서 사이드 이팩트로 API 호출 파라메터를 바꾸는 고객간의 신뢰를 저버리는 일이 발생하지 않게 하기 위해 CustomerWebRequestV1와 CustomerWebResponseV1는 불변이어야 하고 위와 같이 요청 프로세스를 (2)과 같이 나눈다.

 

이렇게 되면 프론트앤드와의 협업에도 좋아진다. Controller의 파라메터의 수정에 의해 갑자기 파라메터가 바뀔 일이 없어지기 때문에 한번 정해놓은 약속을 어길 일이 없어지기 때문이다.



(CustomerRequest, CustomerResponse)

 

이 단계에서는 고객 객체의 파라메터에서 age를 받지 않고 tel 을 받는다. 그러면 다음과 같은 파라메터를 가지면 되는 것이다.

class Customer { name, address, tel }

이 구조는 크게 다음과 같은 두 장점이 있다.

 

1. 유동성

결론적으로 (2)과 같은 구조로 하면, API에서 분리되어 Controller에서만 신경쓸 수 있는 부분이다. 그러니까 서버쪽으로 데이터를 받고 난 다음에는 개발자의 자유로 유동적으로 파라메터를 조정할 수 있는 것이다.

 

2. 통일성

CustomerWebRequestV1 → CustomerRequest 이런 식으로가 아닌 이 단계를 Controller로 통일한다고 가정할 때,

또 다른 API가 나오면 CustomerWebRequestV2, CustomerWebRequestV3, … 이런식으로 늘어날 것인데, 이러면

 

register(CustomerWebRequestV2 customer)
register(CustomerWebRequestV3 customer)

 

이런식으로 병렬적으로 늘어나는 단점이 있다.

 

반면, CustomerWebRequestV1 → CustomerRequest 이런 식으로 하면

 

register(CustomerRequest customer)
register(CustomerRequest customer)

 

이렇게 Service로 들어올 수 있는 데이터가 통일될 수 있고 로직도 통일될 수 있는 엄청난 장점이 있다.



(Customer, CustomerEntity)

 

Customer와 CustomerEntity의 차이는 무엇일까?

→ JPA를 사용하면 테이블과 그 테이블에 해당하는 객체를 매핑을 해야한다. CustomerEntity는 그 역할을 한다. 반면 Customer는 DB와 상관없는 객체의 역할을 한다.

이렇게 나누는 이유는 무엇일까?

(2) 구조는 전체적으로 다음과 같은 구조가 된다. (Customer 도메인에 한해서)

 

*이 프로젝트에서 헥사고날 아키텍처를 사용하기로 하였다.

adaptor
├── inbound (primary adapter)
│   ├── api
│   ├── CustomerController
│   ├── CustomerWebRequestV1   --> 기본적 spring validation @NotNull, @NotEmpty, @Digit, @JsonProperty("")
│   ├── CustomerWebResponseV1
├── outbound (secondary adapter)
│   ├── jpa
│   ├── BaseEntity (package-private)
│   ├── CustomerEntity (package-private)
│   ├── CustomerJpaReposity (implements JpaRepository<>) (package-private)
│   └── CustomerRepositoryAdapter (implements CustomerRepository) (package-private) @Bean
│
application
├── CustomerService
├── CustomerRequest
└── CustomerResponse
│
domain
├── Customer
└── CustomerRepository

이러한 구조는 어떠한 장점이 있을까?

이 구조의 장점을 알기 위해 코드를 살펴보자

class CustomerService {
    @Autowire
    private CustomerRepository repository; // DI (CustomerRepositoryAdapter)
    public CustomerResponse save(CustomerRequest request) {
       repository.save(request.toCustomer());
    }
}


// 저장소가 뭐가 될진몰라 근데 도메인을 저장해..
interface CustomerRepository {
    public Customer save(Customer customer);
}



import CustomerEntity;

class CustomerRepositoryAdapter implements CustomerRepository {

  private CustomerJpaReposity jpaRepository;

    @override
    public Customer save(Customer customer) {
       CustomerEntity entity = customer.toCustomerEntity();
       CustomerEntity entity = jpaRepository.save(entity);
       return entity.toCustomer();
    }
}

이와 같은 OCP에 위반되지 않는 코드가 탄생한다. CustomerService는 무엇으로 어디에 저장할지는 몰라도 그냥 일단 “저장”을 한다.

그리고 CustomerRepository가 PORT 역할을 하고 JPA, MyBatis, Redis 등등의 어뎁터들이 포트에 끼워지며 저장을 하는 OCP를 위반하지 않는 코드가 된다. 그리고 CustomerRepositoryAdapter에서 로직을 살펴보면 JPA와 테이블이 매핑되어있는 객체로 변환(Customer → CustomerEntity)하고 데이터베이스에 저장하는 방식이다. 그리고 다시 Customer 객체로 변환하여 반환하는 구조이다. CustomerRepositoryAdapter 와 통신하는 주체 자체는 Customer 클래스이다.

이렇게 하면 CustomerEntity 객체가 CustomerJpaReposity와 CustomerRepositoryAdapter 안에서만 사용하기 때문에 밖에 나가지 않아서 구분이 확실해진다. 즉 각 역할별로 모듈화가 잘된다. 모듈화가 잘 되면 Service에서는 Service에만 집중할 수 있게 되어 논리적인 코드를 짤 수 있게 된다. 역할의 분리가 잘 된다.

그리고 이 구조는 의존성 방향에 장점이 있다.

image

 

즉 의존성 방향이 이렇게 되면서 application은 adaptor의 상세 구현을 모르는 구조가 된다.

'난중(개발)일기' 카테고리의 다른 글

홈서버 만들기-2. 포트포워딩  (0) 2024.03.19
final 클래스 모킹  (0) 2023.07.12
728x90

문제

<choose>
    <when test = ”isLiked == ‘t’”>
        UPDATE t_book_like
        SET is_liked = 'f'
        WHERE identity_id = #{userId} AND book_id = #{bookId}
    </when>

<when test = ”isLiked == ‘t’”> 여기서 난 문제였다.

 

 

해결

<when test='isLiked == "t"'> 이렇게 ‘ ‘ 로 해주니까 해결 되었다.

728x90

헥사고날 아키텍처를 처음 접해보았을 때는 인턴때였습니다. 프로젝트 베이스가 깔려지고 사수님한테 설명을 듣고 "나도 저런 아키텍처를 내 코드에 사용해보고싶다.. 더 나아가서 아키텍처를 개발해보고 싶다" 라는 생각이 들었습니다. 마침F-lab을 하며 멘토님이 헥사고날 아키텍처를 사용하여 코드 작성하는 모습을 보여주었습니다. 인턴 때 예습이 되어있어서 그런지 정말정말 이해가 잘 되는 느낌이었습니다. 

 

 

 

헥사고날 아키텍처

  • 내부 영역 - 순수한 비즈니스 로직을 표현하며 캡슐화된 영역이고 기능적 요구사항에 따라 먼저 설계
  • 외부 영역 - 내부 영역에서 기술을 분리하여 구성한 영역이고 내부 영역 설계 이후 설계

이상 대략적인 설명이었고

자세한 설명은 https://netflixtechblog.com/ready-for-changes-with-hexagonal-architecture-b315ec967749 여기에 가서 보시길 바랍니다.. 괜히 잘못된 정보가 전해지면 안되니까요..

 

비즈니스 로직에 집중

내/외부를 분리하고 내부에는 비즈니스 로직들만 존재하고 외부에는 내부의 비즈니스 로직의 관심사가 아닌 것들(database, logging, etc...)이 존재하였습니다. 내/외부를 adaptor와 port를 이용하여 내부의 비즈니스 로직이 필요한 것들을 꽂아서 사용할 수 있는 구조였습니다. 다음 그림으로 설명이 될 것 같습니다.

포트(port)

 

어뎁터(adaptor)

USB를 옆의 그림처럼 USB 포트에 꼽아서 사용하는 것과 같다고 생각합니다. USB는 우리가 원하는 것들을 꼽아서 사용하지 않나요? 그런거와 같이 우리가 비즈니스로직에 MySql을 사용하고싶으면 MySql에 해당하는 어뎁터를 포트에 꼽고 H2를 사용하고 싶으면 그에 해당하는 어뎁터를 꼽아 사용할 수 있습니다. 

 

 

 

 

어뎁터는 포트에다가 꽂을 수 있는 것들을 골라서 꼽을 수 있습니다. 비즈니스 로직에 사용하고 싶은 어뎁터로 포트에 꼽으면 되는 구조였습니다.

 

 

 

 

 

 

 

 

 

어떻게?

인터페이스가 가능하게 해줍니다. 

 

원래 이런 구조였습니다. CommandHandler가 바로 MybatisRepositoryRepository를 의존하고 있었습니다. 그런데 사실상 CommandHandler는 MyBatis, JPA, JDBCtemplate 등 무슨 ORM을 사용하는지 비즈니스 로직의 관심사가 아닙니다. 그리고 바로 구현 객체를 의존하면 변경에 유연하지 않다고 생각합니다. 이런 상황을 풀어줄 친구는 인터페이스 입니다. 다음 사진으로 가보겠습니다.

 

이렇게 인터페이스인 IBookRepository를 의존하는 방법입니다. 여기서 인터페이스는 port 역할을 합니다. 그리고 BookMybatisRepository는 adaptor 역할을 합니다.

 확실하게 관심사가 분리될 수 있었습니다. 비즈니스 로직은 비즈니스로직이 중심이 되고 비즈니스 로직이 필요한 것들은 포트를 게이트로 삼아 끼워넣는 방식은 변경에 유연하다고 생각했습니다. 

 

 

 

 

프로젝트 구조 바뀌기 전 후

구조 바뀌기 전
구조 바뀐 후

 

 

훨씬 깔끔해졌습니다..!!!!

 

https://github.com/Taewoongjung/Doseoro_Java/pull/37

 

728x90

이와 같이 패키지 전체를 포인트 컷으로 잡아서 구현한 AOP를 Custom AOP로 바꾸어보았습니다.  

@Aspect
@Component
public class LogAspect {
    private static final Loggerlogger= LoggerFactory.getLogger(LogAspect.class);

    @Around("within(com.myproject.doseoro..*)")
    public Object logging(ProceedingJoinPoint pjp) throws Throwable {

        String params = getRequestParams();

        long startAt = System.currentTimeMillis();

logger.info("----------> REQUEST : {}({}) = {}", pjp.getSignature().getDeclaringTypeName(),
                pjp.getSignature().getName(), params);

        Object result = pjp.proceed();

        long endAt = System.currentTimeMillis();

logger.info("----------> RESPONSE : {}({}) = {} ({}ms)", pjp.getSignature().getDeclaringTypeName(),
                pjp.getSignature().getName(), result, endAt-startAt);

        return result;
    }

    private String getRequestParams() {

        String params = "";

        RequestAttributes requestAttribute = RequestContextHolder.getRequestAttributes();

        if (requestAttribute != null) {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
                    .getRequestAttributes()).getRequest();

            Map<String, String[]> paramMap = request.getParameterMap();

            if (!paramMap.isEmpty()) {
                params = " [" + paramMapToString(paramMap) + "]";
            }
        }
        return params;
    }

    private String paramMapToString(Map<String, String[]> paramMap) {
        StringJoiner sj = new StringJoiner(",", "[", "]");
        return paramMap.entrySet().stream()
                .map(entry -> String.format("$s -> ($s)",
                        entry.getKey(), sj,join(entry.getValue())))
                .collect(Collectors.joining(", "));
    }
}

 

바꾼 이유는 공부 목적도 있었고 멘토님이 말하시길 "코드상 눈에 보이는 표식을 하지 않았기 때문에 모르는 사람이 볼때는 Aspect 로직이 왜 실행되는지 이해를 하지 못하는 경우도 많다"고 하셨다. 듣고 보니 정말 그럴 것 같았다.

 

어떻게 바뀌었냐면 

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Logging {}
@Aspect
@Component
public class LogAspect {
    private static final Logger logger = LoggerFactory.getLogger(LogAspect.class);

    @Around("@annotation(Logging)") // Custom AOP
    public Object logging(ProceedingJoinPoint pjp) throws Throwable {
    
    	// ... 이하 위 로직과 같음

 

이렇게 바꿈으로써 전에 없었던 어노테이션이 생겼고, 그 어노테이션으로 무엇을 하려고 하는지 정확히 알 수 있었습니다.

 

 

이 과정에서 @Transactional, @Cacheable, @Async 에너테이션도 AOP가 적용된 사례라는 것을 알게되었습니다.

 

 

참고

https://velog.io/@ann0905/AOP%EC%99%80-Transactional%EC%9D%98-%EB%8F%99%EC%9E%91-%EC%9B%90%EB%A6%AC

 

AOP와 @Transactional의 동작 원리

오늘은 @Transactional의 동작 원리를 AOP와 함께 좀 더 자세하게 조사해보려고 한다.여기서 다루는 내용은 다음과 같다.AOP란 무엇이며 왜 사용하는가Spring AOP는 왜 프록시를 사용하는가@Transactional은

velog.io

https://tecoble.techcourse.co.kr/post/2021-06-25-aop-transaction/

 

AOP 입문자를 위한 개념 이해하기

이 글은 AOP 개념이 생소한 입문자들을 위한 포스팅입니다. 1. OOP의 한계 image…

tecoble.techcourse.co.kr

https://private-space.tistory.com/98

 

Spring에서 AOP를 구현하는 방법과 Transactional

Spring에서 AOP를 구현하는 방법과 Transactional AOP에 관한 간략한 개념이 필요하다면 다른 글을 참조한다. 이 내용은 공식 문서를 참조하여 작성하였다. Spring Framework Document AOP 구현 방식 Spring에서 A..

private-space.tistory.com

 

728x90

버스를 타고 집에 오는 길에 생각이 나서 적어본다...

앞으로 얘기할 주제는 seq를 1,2,3,... 으로 하고 PK를 주고 id를 uuid를 넣어 UK를 주는 방식을 말한다.  

 

이 방법의 핵심은 이벤트 드리븐 형식에 있다고 생각한다. 아직 카프카 같은 것을 써보진 않았지만... (얼핏 봤을 때) 이벤트가 발생하고 그 이벤트가 큐에 들어갔을 때 이벤트의 고유번호를 테이블의 컬럼의 id를 주면 되겠다는 생각이 들었다.

 

그러면 나중에 모니터링 할때도 쉬워진다고 생각했다. 왜냐하면 그 이벤트가 발생하고 생겨난 row를 추적할 때 그 id를 가진 컬럼만 찾으면 되기 때문이라고 생각한다.

 

단지 생각만 나서 기록을 해둔다... 나중에 카프카나 RabbitMQ같은 메시징 서비스를 사용해볼 예정이라 그때 드는 생각을 또 적어보겠다.

728x90

깃헙 액션 스크립트는 별로 특별한게 없다.. 별로 달라진것도 없고.. 

name: PR test CI

# 하기 내용에 해당하는 이벤트 발생 시 github action 동작
on:
  push: # feature/*와 pre-production와 main 브랜치에서 push가 일어났을 때 github action 동작
    branches:
      - 'feature/*'
      - 'main'
      - 'pre-production'
  pull_request: # feature/*와 pre-production와 main 브랜치에서 PR이 일어났을 때 github action 동작
    branches:
      - 'feature/*'
      - 'main'
      - 'pre-production'

# 참고사항
# push가 일어난 브랜치에 PR이 존재하면, push에 대한 이벤트와 PR에 대한 이벤트 모두 발생합니다.

jobs:
  build: 
    runs-on: ubuntu-latest # 실행 환경 지정

    steps:
      - uses: actions/checkout@v2 # github action 버전 지정(major version)

      - name: Set up JDK 11 # JAVA 버전 지정
        uses: actions/setup-java@v3
        with:
          java-version: 11
          distribution: 'temurin'

      - name: Grant execute permission for gradlew
        run: chmod +x gradlew

      - name: Build with Gradle # 실제 application build(-x 옵션을 통해 test는 제외)
        run: ./gradlew build
#         run: ./gradlew build -x test
        
#       - name: Test with Gradle # test application build
#         run: ./gradlew test

      - name: Publish Unit Test Results # test 후 result를 보기 위해 추가
        uses: EnricoMi/publish-unit-test-result-action@v2
        if: ${{ always() }} # test가 실패해도 report를 남기기 위해 설정
        with:
          files: build/test-results/**/*.xml

 

 

그런데....

 

 

 

이 화면의 연속이었다. 어제 오늘...순수 해결하려고 노력한 시간만 대략 20시간 동안 꼬박 이것만 한 것 같다.

 

DoseoroApplicationTests > contextLoads() FAILED
 java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:132
 

이 오류에 대한 원인을 살펴봤었다.

  • MySql의 문제
  • DB가 물리지 않았기 때문
    • 디비가 물렸어도 테이블이 없기 때문
      • 현재 MyBatis를 사용하고 있는데 JPA나 Node js에 Sequelize 같은 ORM은 자동으로 테이블을 만들어 주지만 MyBatis에는 그런게 없는데 어떡하지? 의 문제

등등 긴 시간동안 많은 고민이 있었던 것 같다...

 

 

1.  처음에 MySql의 문제로 다가갔다.

테스트 코드가 돌려면 어쨌든 DB에 갔다 와야하는데 깃헙 액션 스크립트를 봤는데 MySql을 세팅하는 로직이 없었다. 그래서 

 

 - name: Setup MySQL
        uses: samin/mysql-action@v1
        with:
          mysql user: 'root'
          mysql password: ${{ secrets.MYSQL_PASSWORD }}

 

이 코드를 추가했다. 그런데 예상 외로 실행되지  않았다... 그렇다면 무슨 문제가 있을까 하며 다른 문제를 찾았다. MySql을 세팅을 했지만 거기에 테이블이 없네??? 그러면 만들어야지. 라며 생각했다. 그런데 H2가 요즘 현업에서 테스트용도로만 쓴다고 했다.

 

2.  H2의 적용

그래서 찾아보니 H2는 정말정말 구미가 당길만한 능력이 있는 친구였다. In-Memory 형식 디비를 구축할 수 있었다. H2 DB를 메인 메모리(JVM) 위에서 돌아가며 애플리케이션이 종료되면 없어지는 휘발성 디비였다. 그러면 완전 좋은거네??? 이득!! 하면서 바로 적용해보기로 했다. 그리고 메인 메모리에서 돌아가는 DB라 데이터 캐싱 DB로도 사용할 수 있는 장점을 느꼈다. 다시 사용할거라는 기약을 했다.

 

테스팅 용으로 H2를 적용은 했는데 이제 어떻게해?? 라는 의문점만 남았다. 그냥 돌려봐야지. 라는 생각만 들었다. 빨리 결과를 도출하고싶은 급한 마음이었다... 당연히 안될게 뻔했다. 내 자신이 부끄러울정도로 멍청하였다. 테이블이 없는데 어떻게 돌아가겠니!!!... 바로 테이블을 생성하는 시도로 넘어가자.

 

3.  테이블 생성

지금까지 MyBatis를 사용하면서 코드상으로 DDL을 한번도 한적이 없었다. 어떻게 하지..? 라는 생각이 들고 계속 검색만 했다. 정말 여러 방법들이 있었다. 이번에 자바 스프링을 처음 배우면서 MyBatis만 사용해봤으니 다른 것도 사용해볼까? 라는 생각이 들었다. JdbcTemplate이 pure 한 쿼리문을 넣을 수 있고 예전부터 사용해보고 싶었는데 이번 기회에 사용해보면 좋겠다 싶어서 사용해봤다. 정말정말 편했다...

 

JdbcTemplate으로 테이블을 생성하고 테스트코드 로직에 맞게 잘 수정한 뒤 로컬에서 테스트를 돌려보았다. 잘 돌아갔다. 그러면 깃헙 액션에서도 잘 돌아가겠지?? 라는 안일한 생각으로 기쁜 마음으로 돌려보았다. 결과는 아직이었다.. 계속 위와 같은 오류를 뿜어냈다. 그래도 고지가 코앞이라는 생각이 들었다. 이 문제를 해결할 자신이 있었다. 

 

4. application.yml 의 분리

테스트 용도랑 로컬용도로 분리하였다. main과 test 폴더 각각 resource 폴더가 있고 그 안에는 application.yml 파일이 각각 있었다. 그런데 main에는 application-test.yml을 추가하고 

spring:
  profiles:
    active: test

을 추가하며 test 용도로 분리했다.  그리고 test 폴더의 application.yml은 

spring:
  profiles:
    active: test
  config:
    import: application-test.yml

main의 application.yml을 불러오는 식으로 처리했다. 이제 완벽히 세팅이 끝났다고 생각하고 깃헙 액션을 돌려보았다. 잘 안됐다. 다 됐다고 생각했는데 왜 안되지???

 

+ ./bin에 있는 H2 라이브러리를 추가할 때 바로 위와 같은 상황일 때 main에만 추가 해주면 된다.

 

5. 커밋 히스토리를 보고 .gitignore를 확인하는 순간 해결

커밋 히스토리를 유심히 보니 테스트 폴더쪽에 application.yml이 들어가있지 않았다 .gitignore에 application.yml이 추가가 되어있었다. 정말 바보같은 실수였다. 테스트 폴더쪽의 application.yml과 함께 푸시를 하면서 모든 문제는 해결이 되었다. 안도의 한숨을 쉰다.

+ Recent posts