├── .gitignore ├── LICENSE ├── README.md ├── app ├── Dockerfile ├── cloudbuild.yaml ├── kubernetes │ ├── deployment.yml │ └── service.yaml ├── pom.xml └── src │ └── main │ ├── java │ └── fooddelivery │ │ ├── AbstractEvent.java │ │ ├── Application.java │ │ ├── Order.java │ │ ├── PolicyHandler.java │ │ ├── config │ │ └── kafka │ │ │ └── KafkaProcessor.java │ │ ├── external │ │ ├── 결제이력.java │ │ ├── 결제이력Service.java │ │ └── 결제이력ServiceFallback.java │ │ ├── 배달시작됨.java │ │ ├── 주문Controller.java │ │ ├── 주문Repository.java │ │ ├── 주문됨.java │ │ └── 주문취소됨.java │ └── resources │ └── application.yml ├── customer ├── Dockerfile.command.handler ├── Dockerfile.policy.handler ├── LICENSE ├── README.md ├── command-handler.py ├── kubernetes │ ├── deployment.yml │ └── service.yaml ├── policy-handler.py └── requirements.txt ├── gateway ├── Dockerfile ├── cloudbuild.yaml ├── pom.xml └── src │ └── main │ ├── java │ └── com │ │ └── example │ │ └── Application.java │ └── resources │ └── application.yml ├── pay ├── Dockerfile ├── cloudbuild.yaml ├── kubernetes │ ├── deployment.yml │ └── service.yaml ├── pom.xml └── src │ └── main │ ├── java │ └── fooddelivery │ │ ├── AbstractEvent.java │ │ ├── Application.java │ │ ├── PolicyHandler.java │ │ ├── config │ │ └── kafka │ │ │ └── KafkaProcessor.java │ │ ├── 결제승인됨.java │ │ ├── 결제이력.java │ │ ├── 결제이력Controller.java │ │ ├── 결제이력Repository.java │ │ ├── 결제취소됨.java │ │ └── 주문취소됨.java │ └── resources │ └── application.yml └── store ├── Dockerfile ├── cloudbuild.yaml ├── kubernetes ├── deployment.yml └── service.yaml ├── pom.xml └── src └── main ├── java └── fooddelivery │ ├── AbstractEvent.java │ ├── Application.java │ ├── PolicyHandler.java │ ├── config │ └── kafka │ │ └── KafkaProcessor.java │ ├── 결제승인됨.java │ ├── 결제취소됨.java │ ├── 배달시작됨.java │ ├── 주문관리.java │ ├── 주문관리Controller.java │ ├── 주문관리Repository.java │ └── 쿠폰발행됨.java └── resources └── application.yml /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | 22 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 23 | hs_err_pid* 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 msaez-examples 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 주제 - 도서관 시스템 2 | 3 | 도서관의 책 대여 및 예약, 관리 시스템입니다. 4 | - 체크포인트 : https://workflowy.com/s/assessment-check-po/T5YrzcMewfo4J6LW 5 | 6 | # 구현 Repository 7 | 8 | 총 5개 9 | 1. https://github.com/Juyounglee95/bookRental 10 | 2. https://github.com/Juyounglee95/gateway 11 | 3. https://github.com/Juyounglee95/bookManagement 12 | 4. https://github.com/Juyounglee95/point 13 | 5. https://github.com/Juyounglee95/view 14 | 15 | 16 | # 서비스 시나리오 17 | 18 | ## 기능적 요구사항 19 | 1. 관리자는 도서를 등록한다. 20 | 2. 사용자는 도서를 예약한다. 21 | 3. 도서를 예약 시에는 포인트를 사용한다. 22 | 3-1. 예약 취소 시에는 포인트가 반납된다. 23 | 4. 사용자는 도서를 반납한다. 24 | 25 | ## 비기능적 요구사항 26 | 1. 트랜잭션 27 | 1. 결제가 되지 않은 경우 대여할 수 없다. Sync 호출 28 | 2. 장애격리 29 | 1. 도서관리 기능이 수행되지 않더라도 대여/예약은 365일 24시간 받을 수 있어야 한다 Async (event-driven), Eventual Consistency 30 | 2. 결제시스템이 과중되면 사용자를 잠시동안 받지 않고 잠시후에 결제하도록 유도한다 Circuit breaker, fallback 31 | 3. 성능 32 | 1. 사용자는 전체 도서 목록을 확인하여 전체 도서의 상태를 확인할 수 있어야한다. CQRS 33 | 34 | 35 | # 체크포인트 36 | 37 | - 분석 설계 38 | 39 | 40 | - 이벤트스토밍: 41 | - 스티커 색상별 객체의 의미를 제대로 이해하여 헥사고날 아키텍처와의 연계 설계에 적절히 반영하고 있는가? 42 | - 각 도메인 이벤트가 의미있는 수준으로 정의되었는가? 43 | - 어그리게잇: Command와 Event 들을 ACID 트랜잭션 단위의 Aggregate 로 제대로 묶었는가? 44 | - 기능적 요구사항과 비기능적 요구사항을 누락 없이 반영하였는가? 45 | 46 | - 서브 도메인, 바운디드 컨텍스트 분리 47 | - 팀별 KPI 와 관심사, 상이한 배포주기 등에 따른  Sub-domain 이나 Bounded Context 를 적절히 분리하였고 그 분리 기준의 합리성이 충분히 설명되는가? 48 | - 적어도 3개 이상 서비스 분리 49 | - 폴리글랏 설계: 각 마이크로 서비스들의 구현 목표와 기능 특성에 따른 각자의 기술 Stack 과 저장소 구조를 다양하게 채택하여 설계하였는가? 50 | - 서비스 시나리오 중 ACID 트랜잭션이 크리티컬한 Use 케이스에 대하여 무리하게 서비스가 과다하게 조밀히 분리되지 않았는가? 51 | - 컨텍스트 매핑 / 이벤트 드리븐 아키텍처 52 | - 업무 중요성과  도메인간 서열을 구분할 수 있는가? (Core, Supporting, General Domain) 53 | - Request-Response 방식과 이벤트 드리븐 방식을 구분하여 설계할 수 있는가? 54 | - 장애격리: 서포팅 서비스를 제거 하여도 기존 서비스에 영향이 없도록 설계하였는가? 55 | - 신규 서비스를 추가 하였을때 기존 서비스의 데이터베이스에 영향이 없도록 설계(열려있는 아키택처)할 수 있는가? 56 | - 이벤트와 폴리시를 연결하기 위한 Correlation-key 연결을 제대로 설계하였는가? 57 | 58 | - 헥사고날 아키텍처 59 | - 설계 결과에 따른 헥사고날 아키텍처 다이어그램을 제대로 그렸는가? 60 | 61 | - 구현 62 | - [DDD] 분석단계에서의 스티커별 색상과 헥사고날 아키텍처에 따라 구현체가 매핑되게 개발되었는가? 63 | - Entity Pattern 과 Repository Pattern 을 적용하여 JPA 를 통하여 데이터 접근 어댑터를 개발하였는가 64 | - [헥사고날 아키텍처] REST Inbound adaptor 이외에 gRPC 등의 Inbound Adaptor 를 추가함에 있어서 도메인 모델의 손상을 주지 않고 새로운 프로토콜에 기존 구현체를 적응시킬 수 있는가? 65 | - 분석단계에서의 유비쿼터스 랭귀지 (업무현장에서 쓰는 용어) 를 사용하여 소스코드가 서술되었는가? 66 | - Request-Response 방식의 서비스 중심 아키텍처 구현 67 | - 마이크로 서비스간 Request-Response 호출에 있어 대상 서비스를 어떠한 방식으로 찾아서 호출 하였는가? (Service Discovery, REST, FeignClient) 68 | - 서킷브레이커를 통하여  장애를 격리시킬 수 있는가? 69 | - 이벤트 드리븐 아키텍처의 구현 70 | - 카프카를 이용하여 PubSub 으로 하나 이상의 서비스가 연동되었는가? 71 | - Correlation-key: 각 이벤트 건 (메시지)가 어떠한 폴리시를 처리할때 어떤 건에 연결된 처리건인지를 구별하기 위한 Correlation-key 연결을 제대로 구현 하였는가? 72 | - Message Consumer 마이크로서비스가 장애상황에서 수신받지 못했던 기존 이벤트들을 다시 수신받아 처리하는가? 73 | - Scaling-out: Message Consumer 마이크로서비스의 Replica 를 추가했을때 중복없이 이벤트를 수신할 수 있는가 74 | - CQRS: Materialized View 를 구현하여, 타 마이크로서비스의 데이터 원본에 접근없이(Composite 서비스나 조인SQL 등 없이) 도 내 서비스의 화면 구성과 잦은 조회가 가능한가? 75 | 76 | - 폴리글랏 플로그래밍 77 | - 각 마이크로 서비스들이 하나이상의 각자의 기술 Stack 으로 구성되었는가? 78 | - 각 마이크로 서비스들이 각자의 저장소 구조를 자율적으로 채택하고 각자의 저장소 유형 (RDB, NoSQL, File System 등)을 선택하여 구현하였는가? 79 | - API 게이트웨이 80 | - API GW를 통하여 마이크로 서비스들의 집입점을 통일할 수 있는가? 81 | - 게이트웨이와 인증서버(OAuth), JWT 토큰 인증을 통하여 마이크로서비스들을 보호할 수 있는가? 82 | - 운영 83 | - SLA 준수 84 | - 셀프힐링: Liveness Probe 를 통하여 어떠한 서비스의 health 상태가 지속적으로 저하됨에 따라 어떠한 임계치에서 pod 가 재생되는 것을 증명할 수 있는가? 85 | - 서킷브레이커, 레이트리밋 등을 통한 장애격리와 성능효율을 높힐 수 있는가? 86 | - 오토스케일러 (HPA) 를 설정하여 확장적 운영이 가능한가? 87 | - 모니터링, 앨럿팅: 88 | - 무정지 운영 CI/CD (10) 89 | - Readiness Probe 의 설정과 Rolling update을 통하여 신규 버전이 완전히 서비스를 받을 수 있는 상태일때 신규버전의 서비스로 전환됨을 siege 등으로 증명 90 | - Contract Test : 자동화된 경계 테스트를 통하여 구현 오류나 API 계약위반를 미리 차단 가능한가? 91 | 92 | 93 | # 분석/설계 94 | 95 | 96 | * 이벤트스토밍 결과: http://msaez.io/#/storming/nZJ2QhwVc4NlVJPbtTkZ8x9jclF2/every/a77281d704710b0c2e6a823b6e6d973a/-M5AV2z--su_i4BfQfeF 97 | 98 | 99 | ## 이벤트 도출 100 | ![image](https://user-images.githubusercontent.com/18453570/79930892-9c3cc800-8484-11ea-9076-39259368f131.png) 101 | 102 | 103 | ## 액터, 커맨드 부착하여 읽기 좋게 104 | ![image](https://user-images.githubusercontent.com/18453570/79931004-de660980-8484-11ea-9573-8cf3d8509e9e.png) 105 | 106 | ## 어그리게잇으로 묶기 107 | ![image](https://user-images.githubusercontent.com/18453570/79931210-6ea44e80-8485-11ea-959b-2f500a9a7c1d.png) 108 | 109 | 110 | ## 바운디드 컨텍스트로 묶기 111 | 112 | ![image](https://user-images.githubusercontent.com/18453570/79931545-32bdb900-8486-11ea-8518-558b5cf02d77.png) 113 | 114 | - 도메인 서열 분리 115 | - Core Domain: bookRental, bookManagement : 핵심 서비스 116 | - Supporting Domain: marketing, customer : 경쟁력을 내기위한 서비스 117 | - General Domain: point : 결제서비스로 3rd Party 외부 서비스를 사용하는 것이 경쟁력이 높음 (핑크색으로 이후 전환할 예정) 118 | 119 | ## 폴리시 부착 120 | 121 | ![image](https://user-images.githubusercontent.com/18453570/79933209-584cc180-848a-11ea-8289-c59468228c67.png) 122 | 123 | 124 | ## 폴리시의 이동과 컨텍스트 매핑 (점선은 Pub/Sub, 실선은 Req/Resp) 125 | 126 | ![image](https://user-images.githubusercontent.com/18453570/79933604-76ff8800-848b-11ea-8092-bd7510bf5d0b.png) 127 | 128 | - View Model 추가 129 | 130 | ## 기능적/비기능적 요구사항을 커버하는지 검증 131 | 132 | ![image](https://user-images.githubusercontent.com/18453570/79933961-5f74cf00-848c-11ea-9870-cbd05b6348c5.png) 133 | 134 | <기능적 요구사항 검증> 135 | 136 | - 관리자는 도서를 등록한다. ok 137 | - 사용자는 도서를 예약한다. ok 138 | - 도서를 예약 시에는 포인트를 사용한다. ok 139 | - 예약 취소 시에는 포인트가 반납된다. ok 140 | - 사용자는 도서를 반납한다. ok 141 | - 사용자는 예약을 취소할 수 있다 (ok) 142 | - 예약이 취소되면 포인트가 반납되고, 도서의 상태가 예약 취소로 변경된다 (ok) 143 | - 사용자는 도서상태를 중간중간 조회한다 (View-green sticker 의 추가로 ok) 144 | - 도서가 등록/예약/예약취소/반납 시, 도서의 상태가 변경되어 전체 도서 리스트에 반영된다. 사용자와 관리자 모두 이를 확인할 수 있다. ok 145 | 146 | 147 | ## 비기능 요구사항에 대한 검증 148 | 149 | - 마이크로 서비스를 넘나드는 시나리오에 대한 트랜잭션 처리 150 | - 도서 예약시 결제처리: 예약완료시 포인트 결제처리에 대해서는 Request-Response 방식 처리 151 | - 결제 완료시 도서 상태 변경: Eventual Consistency 방식으로 트랜잭션 처리함. 152 | - 나머지 모든 inter-microservice 트랜잭션: 데이터 일관성의 시점이 크리티컬하지 않은 모든 경우가 대부분이라 판단, Eventual Consistency 를 기본으로 채택함. 153 | 154 | 155 | 156 | 157 | ## 헥사고날 아키텍처 다이어그램 도출 158 | 159 | ![image](https://user-images.githubusercontent.com/18453570/80059618-5f95cd00-8567-11ea-9855-6fdc2e51bfd0.png) 160 | 161 | - Chris Richardson, MSA Patterns 참고하여 Inbound adaptor와 Outbound adaptor를 구분함 162 | - 호출관계에서 PubSub 과 Req/Resp 를 구분함 163 | - 서브 도메인과 바운디드 컨텍스트의 분리: 각 팀의 KPI 별로 아래와 같이 관심 구현 스토리를 나눠가짐 164 | 165 | 166 | # 구현: 167 | 168 | 분석/설계 단계에서 도출된 헥사고날 아키텍처에 따라, 각 BC별로 대변되는 마이크로 서비스들을 스프링부트로 구현함. 구현한 각 서비스를 로컬에서 실행하는 방법은 아래와 같다 (각자의 포트넘버는 8081 ~ 808n 이다) 169 | bookManagement/ bookRental/ gateway/ point/ view/ 170 | 171 | ``` 172 | cd bookManagement 173 | mvn spring-boot:run 174 | 175 | cd bookRental 176 | mvn spring-boot:run 177 | 178 | cd gateway 179 | mvn spring-boot:run 180 | 181 | cd point 182 | mvn spring-boot:run 183 | 184 | cd view 185 | mvn spring-boot:run 186 | ``` 187 | 188 | ## DDD 의 적용 189 | 190 | - 각 서비스내에 도출된 핵심 Aggregate Root 객체를 Entity 로 선언. 이때 가능한 현업에서 사용하는 언어 (유비쿼터스 랭귀지)를 그대로 사용함. 191 | 192 | ``` 193 | package library; 194 | 195 | import javax.persistence.*; 196 | import org.springframework.beans.BeanUtils; 197 | import java.util.List; 198 | 199 | @Entity 200 | @Table(name="PointSystem_table") 201 | public class PointSystem { 202 | 203 | @Id 204 | @GeneratedValue(strategy=GenerationType.AUTO) 205 | private Long id; 206 | private Long bookId; 207 | private Long pointQty =(long)100; 208 | 209 | @PostPersist 210 | public void onPostPersist(){ 211 | PointUsed pointUsed = new PointUsed(this); 212 | BeanUtils.copyProperties(this, pointUsed); 213 | pointUsed.publish(); 214 | 215 | 216 | } 217 | 218 | public Long getBookId() { 219 | return bookId; 220 | } 221 | 222 | public void setBookId(Long bookId) { 223 | this.bookId = bookId; 224 | } 225 | 226 | public Long getId() { 227 | return id; 228 | } 229 | 230 | public void setId(Long id) { 231 | this.id = id; 232 | } 233 | public Long getPointQty() { 234 | return pointQty; 235 | } 236 | 237 | public void setPointQty(Long pointQty) { 238 | this.pointQty = pointQty; 239 | } 240 | 241 | 242 | 243 | 244 | } 245 | 246 | 247 | ``` 248 | - Entity Pattern 과 Repository Pattern 을 적용하여 JPA 를 통하여 다양한 데이터소스 유형 (RDB or NoSQL) 에 대한 별도의 처리가 없도록 데이터 접근 어댑터를 자동 생성하기 위하여 Spring Data REST 의 RestRepository 를 적용하였다 249 | ``` 250 | package library; 251 | 252 | import org.springframework.data.repository.PagingAndSortingRepository; 253 | 254 | public interface PointSystemRepository extends PagingAndSortingRepository{ 255 | 256 | 257 | } 258 | ``` 259 | - 적용 후 REST API 의 테스트 260 | ``` 261 | # bookManagement 서비스의 도서 등록처리 262 | http POST http://52.231.116.117:8080/bookManageSystems bookName="JPA" 263 | 264 | # bookRental 서비스의 예약처리 265 | http POST http://52.231.116.117:8080/bookRentalSystems/returned/1 266 | 267 | # bookRental 서비스의 반납처리 268 | http POST http://52.231.116.117:8080/bookRentalSystems/reserve/1 269 | 270 | # bookRental 서비스의 예약취소처리 271 | http POST http://52.231.116.117:8080/bookRentalSystems/reserveCanceled/1 272 | 273 | # 도서 상태 확인 274 | http://52.231.116.117:8080/bookLists 275 | 276 | ``` 277 | 278 | ## 동기식 호출 과 비동기식 279 | 280 | 분석단계에서의 조건 중 하나로 예약(bookRental)->결제(point) 간의 호출은 동기식 일관성을 유지하는 트랜잭션으로 처리하기로 하였다. 호출 프로토콜은 이미 앞서 Rest Repository 에 의해 노출되어있는 REST 서비스를 FeignClient 를 이용하여 호출하도록 한다. 281 | 282 | - 결제서비스를 호출하기 위하여 Stub과 (FeignClient) 를 이용하여 Service 대행 인터페이스 (Proxy) 를 구현 283 | 284 | ``` 285 | # (app) pointSystemService.java 286 | 287 | @FeignClient(name="point", url="http://52.231.116.117:8080") 288 | public interface PointSystemService { 289 | 290 | @RequestMapping(method= RequestMethod.POST, path="/pointSystems", consumes = "application/json") 291 | public void usePoints(@RequestBody PointSystem pointSystem); 292 | 293 | } 294 | 295 | ``` 296 | 297 | - 예약을 받은 직후(@PostPersist) 결제를 요청하도록 처리 -> BookRental의 생성은 BookManageSystem에서 도서를 등록한 직후 발생하기 때문에, Post요청으로 예약이 들어온 후 결제 요청하도록 처리함. 298 | 299 | ``` 300 | # BookRentalSystemController.java (Entity) 301 | 302 | @PostMapping("/bookRentalSystems/reserve/{id}") 303 | public void bookReserve(@PathVariable(value="id")Long id){ 304 | PointSystem pointSystem = new PointSystem(); 305 | pointSystem.setBookId(id); 306 | PointSystemService pointSystemService = Application.applicationContext. 307 | getBean(library.external.PointSystemService.class); 308 | pointSystemService.usePoints(pointSystem); 309 | } 310 | } 311 | ``` 312 | 결제가 이루어진 후에 도서대여시스템으로 이를 알려주는 행위는 동기식이 아니라 비 동기식으로 처리하여 도서대여시스템의 처리를 위하여 도서 상태 업데이트는 블로킹 되지 않도록 처리한다. 313 | 314 | - 이를 위하여 결제이력에 기록을 남긴 후에 곧바로 결제승인이 되었다는 도메인 이벤트를 카프카로 송출한다(Publish) 315 | 316 | ``` 317 | 318 | #PointSystem.Java (Entity) 319 | { 320 | @PostPersist 321 | public void onPostPersist(){ 322 | PointUsed pointUsed = new PointUsed(this); 323 | BeanUtils.copyProperties(this, pointUsed); 324 | pointUsed.publish(); 325 | 326 | 327 | } 328 | } 329 | ``` 330 | 결제 완료 이벤트를 도서대여시스템의 리스너가 받아, 도서의 상태를 예약완료로 변경한다. 331 | 332 | 333 | ``` 334 | (BookRentalSystem) PolicyHandler.JAVA 335 | { 336 | @StreamListener(KafkaProcessor.INPUT) //포인트 결제 완료시 337 | public void wheneverPointUsed_ChangeStatus(@Payload PointUsed pointUsed){ 338 | try { 339 | if (pointUsed.isMe()) { 340 | System.out.println("##### point use completed : " + pointUsed.toJson()); 341 | BookRentalSystem bookRentalSystem = bookRentalSystemRepository.findById(pointUsed.getBookId()).get(); 342 | bookRentalSystem.setBookStatus("Reserved Complete"); 343 | bookRentalSystemRepository.save(bookRentalSystem); 344 | } 345 | }catch (Exception e){ 346 | e.printStackTrace(); 347 | } 348 | } 349 | } 350 | ``` 351 | 352 | 353 | 도서상태가 변경되면, Reserved라는 이벤트를 발행한다. 354 | 355 | 356 | ``` 357 | BookRentalSystem.java (Entity) 358 | 359 | { 360 | @PostUpdate 361 | public void bookStatusUpdate(){ 362 | 363 | if(this.getBookStatus().equals("Returned")){ 364 | Returned returned = new Returned(this); 365 | BeanUtils.copyProperties(this, returned); 366 | returned.publish(); 367 | 368 | }else if(this.getBookStatus().equals("Canceled")){ 369 | 370 | ReservationCanceled reservationCanceled = new ReservationCanceled(this); 371 | BeanUtils.copyProperties(this, reservationCanceled); 372 | reservationCanceled.publish(); 373 | }else if(this.getBookStatus().equals("Reserved Complete")){ 374 | Reserved reserved = new Reserved(this); 375 | BeanUtils.copyProperties(this, reserved); 376 | reserved.publish(); 377 | } 378 | 379 | } 380 | 381 | } 382 | ``` 383 | 384 | 385 | 결과 : 포인트가 사용된 후에, 예약이 완료되는 것과 도서의 상태가 변경된 것을 BookListView확인 할 수 있다. 386 | 387 | ![image](https://user-images.githubusercontent.com/18453570/80061051-12b3f580-856b-11ea-989c-f4cf958613d5.png) 388 | 389 | 390 | 391 | 392 | # 운영 393 | 394 | ## CI/CD 설정 395 | 396 | 397 | 각 구현체들은 각자의 source repository 에 구성되었고, 사용한 CI/CD 플랫폼은 azure를 사용하였으며, pipeline build script 는 각 프로젝트 폴더 이하에 azure-pipeline.yml 에 포함되었다. 398 | 399 | ## pipeline 동작 결과 400 | 401 | 아래 이미지는 azure의 pipeline에 각각의 서비스들을 올려, 코드가 업데이트 될때마다 자동으로 빌드/배포 하도록 하였다. 402 | 403 | ![image](https://user-images.githubusercontent.com/18453570/79945720-6b22be80-84a9-11ea-8465-132806bc0f97.png) 404 | 405 | 그 결과 kubernetes cluster에 아래와 같이 서비스가 올라가있는 것을 확인할 수 있다. 406 | 407 | ![image](https://user-images.githubusercontent.com/18453570/79971771-c2d42080-84cf-11ea-9385-0896baf668a4.png) 408 | 409 | 또한, 기능들도 정상적으로 작동함을 알 수 있다. 410 | 411 | **<이벤트 날리기>** 412 | 413 | ![image](https://user-images.githubusercontent.com/18453570/80060143-cb2c6a00-8568-11ea-934a-111ccd8c21c9.png) 414 | ![image](https://user-images.githubusercontent.com/18453570/80060146-ce275a80-8568-11ea-993a-9f206ed4e7e8.png) 415 | ![image](https://user-images.githubusercontent.com/18453570/80060149-d089b480-8568-11ea-83ef-8a2496163806.png) 416 | ![image](https://user-images.githubusercontent.com/18453570/80060153-d2537800-8568-11ea-8c01-0a4740373c4a.png) 417 | ![image](https://user-images.githubusercontent.com/18453570/80060164-d5e6ff00-8568-11ea-8f75-b8e735ba7e18.png) 418 | 419 | **<동작 결과>** 420 | 421 | ![image](https://user-images.githubusercontent.com/18453570/80060261-15ade680-8569-11ea-8256-d28b1e7f1e67.png) 422 | 423 | 424 | ### 오토스케일 아웃 425 | 426 | 427 | - 포인트서비스에 대한 replica 를 동적으로 늘려주도록 HPA 를 설정한다. 설정은 CPU 사용량이 15프로를 넘어서면 replica 를 10개까지 늘려준다: 428 | - 오토스케일이 어떻게 되고 있는지 모니터링을 걸어둔다: 429 | 430 | ![image](https://user-images.githubusercontent.com/18453570/80059958-51947c00-8568-11ea-9567-1b7d69c7381f.png) 431 | 432 | - 워크로드를 2분 동안 걸어준 후 테스트 결과는 아래와 같다. 433 | 434 | ![image](https://user-images.githubusercontent.com/18453570/80060025-8274b100-8568-11ea-8f60-fa428c62168c.png) 435 | 436 | 437 | ## 무정지 재배포 438 | 439 | Autoscaler설정과 Readiness 제거를 한뒤, 부하를 넣었다. 440 | 441 | 이후 Readiness를 제거한 코드를 업데이트하여 새 버전으로 배포를 시작했다. 442 | 443 | 그 결과는 아래는 같다. 444 | 445 | ![image](https://user-images.githubusercontent.com/18453570/80060602-ec418a80-8569-11ea-87ea-34004c1ce5d3.png) 446 | ![image](https://user-images.githubusercontent.com/18453570/80060605-eea3e480-8569-11ea-9825-a375530f1953.png) 447 | 448 | 449 | 다시 Readiness 설정을 넣고 부하를 넣었다. 450 | 451 | 그리고 새버전으로 배포한 뒤 그 결과는 아래와 같다. 452 | 453 | 454 | ![image](https://user-images.githubusercontent.com/18453570/80060772-565a2f80-856a-11ea-9ee3-5d682099b899.png) 455 | ![image](https://user-images.githubusercontent.com/18453570/80060776-5823f300-856a-11ea-89a9-7c945ea05278.png) 456 | 457 | 배포기간 동안 Availability 가 변화없기 때문에 무정지 재배포가 성공한 것으로 확인됨. 458 | 459 | -------------------------------------------------------------------------------- /app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8u212-jdk-alpine 2 | COPY target/*SNAPSHOT.jar app.jar 3 | EXPOSE 8080 4 | ENTRYPOINT ["java","-Xmx400M","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar","--spring.profiles.active=docker"] 5 | -------------------------------------------------------------------------------- /app/cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | ### Test 3 | # - id: 'test' 4 | # name: 'gcr.io/cloud-builders/mvn' 5 | # args: [ 6 | # 'test', 7 | # '-Dspring.profiles.active=test' 8 | # ] 9 | ### Build 10 | - id: 'build' 11 | name: 'gcr.io/cloud-builders/mvn' 12 | args: [ 13 | 'clean', 14 | 'package' 15 | # '-Dmaven.test.skip=true' 16 | ] 17 | # waitFor: ['test'] 18 | ### docker Build 19 | - id: 'docker build' 20 | name: 'gcr.io/cloud-builders/docker' 21 | args: 22 | - 'build' 23 | - '--tag=gcr.io/$PROJECT_ID/$_PROJECT_NAME:latest' 24 | - '.' 25 | ### Publish 26 | - id: 'publish' 27 | name: 'gcr.io/cloud-builders/docker' 28 | entrypoint: 'bash' 29 | args: 30 | - '-c' 31 | - | 32 | docker push gcr.io/$PROJECT_ID/$_PROJECT_NAME:latest 33 | ### deploy 34 | - id: 'deploy' 35 | name: 'gcr.io/cloud-builders/gcloud' 36 | entrypoint: 'bash' 37 | args: 38 | - '-c' 39 | - | 40 | PROJECT=$$(gcloud config get-value core/project) 41 | gcloud container clusters get-credentials "$${CLOUDSDK_CONTAINER_CLUSTER}" \ 42 | --project "$${PROJECT}" \ 43 | --zone "$${CLOUDSDK_COMPUTE_ZONE}" 44 | cat < 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.1.9.RELEASE 9 | 10 | 11 | fooddelivery 12 | app 13 | 0.0.1-SNAPSHOT 14 | app 15 | Demo project for Spring Boot 16 | 17 | 18 | 1.8 19 | Greenwich.RELEASE 20 | Germantown.SR1 21 | 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-data-jpa 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-data-rest 31 | 32 | 33 | 34 | com.h2database 35 | h2 36 | runtime 37 | 38 | 39 | 40 | org.springframework.cloud 41 | spring-cloud-starter-openfeign 42 | 43 | 44 | 45 | 46 | 47 | org.springframework.cloud 48 | spring-cloud-starter-stream-kafka 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-actuator 53 | 54 | 55 | 56 | org.springframework.boot 57 | spring-boot-starter-test 58 | test 59 | 60 | 61 | 62 | 63 | 64 | 65 | org.springframework.cloud 66 | spring-cloud-dependencies 67 | ${spring-cloud.version} 68 | pom 69 | import 70 | 71 | 72 | org.springframework.cloud 73 | spring-cloud-stream-dependencies 74 | ${spring-cloud-stream.version} 75 | pom 76 | import 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | org.springframework.boot 85 | spring-boot-maven-plugin 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /app/src/main/java/fooddelivery/AbstractEvent.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | import fooddelivery.config.kafka.KafkaProcessor; 4 | import com.fasterxml.jackson.core.JsonProcessingException; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import org.springframework.messaging.MessageChannel; 7 | import org.springframework.messaging.MessageHeaders; 8 | import org.springframework.messaging.support.MessageBuilder; 9 | import org.springframework.util.MimeTypeUtils; 10 | 11 | import java.text.SimpleDateFormat; 12 | import java.util.Date; 13 | 14 | public class AbstractEvent { 15 | 16 | String eventType; 17 | String timestamp; 18 | 19 | public AbstractEvent(){ 20 | this.setEventType(this.getClass().getSimpleName()); 21 | SimpleDateFormat defaultSimpleDateFormat = new SimpleDateFormat("YYYYMMddHHmmss"); 22 | this.timestamp = defaultSimpleDateFormat.format(new Date()); 23 | } 24 | 25 | public String toJson(){ 26 | ObjectMapper objectMapper = new ObjectMapper(); 27 | String json = null; 28 | 29 | try { 30 | json = objectMapper.writeValueAsString(this); 31 | } catch (JsonProcessingException e) { 32 | throw new RuntimeException("JSON format exception", e); 33 | } 34 | 35 | return json; 36 | } 37 | 38 | public void publish(){ 39 | this.publish(this.toJson()); 40 | } 41 | public void publish(String json){ 42 | if( json != null ){ 43 | 44 | /** 45 | * spring streams 방식 46 | */ 47 | KafkaProcessor processor = Application.applicationContext.getBean(KafkaProcessor.class); 48 | MessageChannel outputChannel = processor.outboundTopic(); 49 | 50 | outputChannel.send(MessageBuilder 51 | .withPayload(json) 52 | .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON) 53 | .build()); 54 | 55 | } 56 | } 57 | 58 | 59 | public String getEventType() { 60 | return eventType; 61 | } 62 | 63 | public void setEventType(String eventType) { 64 | this.eventType = eventType; 65 | } 66 | 67 | public String getTimestamp() { 68 | return timestamp; 69 | } 70 | 71 | public void setTimestamp(String timestamp) { 72 | this.timestamp = timestamp; 73 | } 74 | 75 | public boolean isMe(){ 76 | return getEventType().equals(getClass().getSimpleName()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/fooddelivery/Application.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | import fooddelivery.config.kafka.KafkaProcessor; 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.ApplicationContext; 6 | import org.springframework.cloud.stream.annotation.EnableBinding; 7 | import org.springframework.cloud.openfeign.EnableFeignClients; 8 | 9 | 10 | @SpringBootApplication 11 | @EnableBinding(KafkaProcessor.class) 12 | @EnableFeignClients 13 | public class Application { 14 | protected static ApplicationContext applicationContext; 15 | public static void main(String[] args) { 16 | applicationContext = SpringApplication.run(Application.class, args); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/java/fooddelivery/Order.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | import javax.persistence.*; 4 | import org.springframework.beans.BeanUtils; 5 | 6 | @Entity 7 | @Table(name="주문_table") 8 | public class Order { 9 | 10 | @Id 11 | @GeneratedValue(strategy=GenerationType.AUTO) 12 | private Long id; 13 | private String item; 14 | private Integer 수량; 15 | 16 | @PostPersist 17 | public void onPostPersist(){ 18 | 19 | //Following code causes dependency to external APIs 20 | // it is NOT A GOOD PRACTICE. instead, Event-Policy mapping is recommended. 21 | 22 | fooddelivery.external.결제이력 결제이력 = new fooddelivery.external.결제이력(); 23 | // mappings goes here 24 | Application.applicationContext.getBean(fooddelivery.external.결제이력Service.class) 25 | .결제(결제이력); 26 | 27 | 28 | } 29 | 30 | 31 | 32 | public Long getId() { 33 | return id; 34 | } 35 | 36 | public void setId(Long id) { 37 | this.id = id; 38 | } 39 | public String getItem() { 40 | return item; 41 | } 42 | 43 | public void setItem(String item) { 44 | this.item = item; 45 | } 46 | public Integer get수량() { 47 | return 수량; 48 | } 49 | 50 | public void set수량(Integer 수량) { 51 | this.수량 = 수량; 52 | } 53 | 54 | 55 | 56 | 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/fooddelivery/PolicyHandler.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | import fooddelivery.config.kafka.KafkaProcessor; 4 | import com.fasterxml.jackson.databind.DeserializationFeature; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.cloud.stream.annotation.StreamListener; 8 | import org.springframework.messaging.handler.annotation.Payload; 9 | import org.springframework.stereotype.Service; 10 | 11 | @Service 12 | public class PolicyHandler{ 13 | 14 | @StreamListener(KafkaProcessor.INPUT) 15 | public void whenever배달시작됨_주문상태변경(@Payload 배달시작됨 배달시작됨){ 16 | 17 | if(배달시작됨.isMe()){ 18 | System.out.println("##### listener 주문상태변경 : " + 배달시작됨.toJson()); 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /app/src/main/java/fooddelivery/config/kafka/KafkaProcessor.java: -------------------------------------------------------------------------------- 1 | package fooddelivery.config.kafka; 2 | 3 | import org.springframework.cloud.stream.annotation.Input; 4 | import org.springframework.cloud.stream.annotation.Output; 5 | import org.springframework.messaging.MessageChannel; 6 | import org.springframework.messaging.SubscribableChannel; 7 | 8 | public interface KafkaProcessor { 9 | 10 | String INPUT = "event-in"; 11 | String OUTPUT = "event-out"; 12 | 13 | @Input(INPUT) 14 | SubscribableChannel inboundTopic(); 15 | 16 | @Output(OUTPUT) 17 | MessageChannel outboundTopic(); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/fooddelivery/external/결제이력.java: -------------------------------------------------------------------------------- 1 | package fooddelivery.external; 2 | 3 | public class 결제이력 { 4 | 5 | private Long id; 6 | private String orderId; 7 | private Double 금액; 8 | 9 | 10 | public Long getId() { 11 | return id; 12 | } 13 | 14 | public void setId(Long id) { 15 | this.id = id; 16 | } 17 | public String getOrderId() { 18 | return orderId; 19 | } 20 | 21 | public void setOrderId(String orderId) { 22 | this.orderId = orderId; 23 | } 24 | public Double get금액() { 25 | return 금액; 26 | } 27 | 28 | public void set금액(Double 금액) { 29 | this.금액 = 금액; 30 | } 31 | 32 | } 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/java/fooddelivery/external/결제이력Service.java: -------------------------------------------------------------------------------- 1 | 2 | package fooddelivery.external; 3 | 4 | import org.springframework.cloud.openfeign.FeignClient; 5 | import org.springframework.web.bind.annotation.RequestBody; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestMethod; 8 | 9 | import java.util.Date; 10 | 11 | /** 12 | * Created by uengine on 2018. 11. 21.. 13 | */ 14 | 15 | @FeignClient(name="pay", url="http://localhost:8082")//, fallback = 결제이력ServiceFallback.class) 16 | public interface 결제이력Service { 17 | 18 | @RequestMapping(method= RequestMethod.POST, path="/결제이력s") 19 | public void 결제(@RequestBody 결제이력 결제이력); 20 | 21 | } -------------------------------------------------------------------------------- /app/src/main/java/fooddelivery/external/결제이력ServiceFallback.java: -------------------------------------------------------------------------------- 1 | package fooddelivery.external; 2 | 3 | /** 4 | * Created by uengine on 2020. 4. 18.. 5 | */ 6 | public class 결제이력ServiceFallback implements 결제이력Service { 7 | @Override 8 | public void 결제(결제이력 주문) { 9 | //do nothing if you want to forgive it 10 | 11 | System.out.println("Circuit breaker has been opened. Fallback returned instead."); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/fooddelivery/배달시작됨.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | 4 | public class 배달시작됨 extends AbstractEvent { 5 | 6 | private Long id; 7 | private String 요리종류; 8 | private String 배달지주소; 9 | private String orderId; 10 | 11 | public Long getId() { 12 | return id; 13 | } 14 | 15 | public void setId(Long id) { 16 | this.id = id; 17 | } 18 | public String get요리종류() { 19 | return 요리종류; 20 | } 21 | 22 | public void set요리종류(String 요리종류) { 23 | this.요리종류 = 요리종류; 24 | } 25 | public String get배달지주소() { 26 | return 배달지주소; 27 | } 28 | 29 | public void set배달지주소(String 배달지주소) { 30 | this.배달지주소 = 배달지주소; 31 | } 32 | public String getOrderId() { 33 | return orderId; 34 | } 35 | 36 | public void setOrderId(String orderId) { 37 | this.orderId = orderId; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/src/main/java/fooddelivery/주문Controller.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.web.bind.annotation.PathVariable; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | import org.springframework.web.bind.annotation.RequestMethod; 7 | import org.springframework.web.bind.annotation.RestController; 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.servlet.http.HttpServletResponse; 10 | import java.util.List; 11 | 12 | @RestController 13 | public class 주문Controller { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/fooddelivery/주문Repository.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | import org.springframework.data.repository.PagingAndSortingRepository; 4 | 5 | public interface 주문Repository extends PagingAndSortingRepository{ 6 | 7 | 8 | } -------------------------------------------------------------------------------- /app/src/main/java/fooddelivery/주문됨.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | public class 주문됨 extends AbstractEvent { 4 | 5 | private Long id; 6 | private String 품목; 7 | private Integer 수량; 8 | 9 | public 주문됨(){ 10 | super(); 11 | } 12 | 13 | public Long getId() { 14 | return id; 15 | } 16 | 17 | public void setId(Long id) { 18 | this.id = id; 19 | } 20 | public String get품목() { 21 | return 품목; 22 | } 23 | 24 | public void set품목(String 품목) { 25 | this.품목 = 품목; 26 | } 27 | public Integer get수량() { 28 | return 수량; 29 | } 30 | 31 | public void set수량(Integer 수량) { 32 | this.수량 = 수량; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/fooddelivery/주문취소됨.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | public class 주문취소됨 extends AbstractEvent { 4 | 5 | private Long id; 6 | 7 | public 주문취소됨(){ 8 | super(); 9 | } 10 | 11 | public Long getId() { 12 | return id; 13 | } 14 | 15 | public void setId(Long id) { 16 | this.id = id; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /app/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | --- 4 | 5 | spring: 6 | profiles: default 7 | jpa: 8 | properties: 9 | hibernate: 10 | show_sql: true 11 | format_sql: true 12 | cloud: 13 | stream: 14 | kafka: 15 | binder: 16 | brokers: localhost:9092 17 | streams: 18 | binder: 19 | configuration: 20 | default: 21 | key: 22 | serde: org.apache.kafka.common.serialization.Serdes$StringSerde 23 | value: 24 | serde: org.apache.kafka.common.serialization.Serdes$StringSerde 25 | bindings: 26 | event-in: 27 | group: app 28 | destination: fooddelivery 29 | contentType: application/json 30 | event-out: 31 | destination: fooddelivery 32 | contentType: application/json 33 | 34 | logging: 35 | level: 36 | org.hibernate.type: trace 37 | org.springframework.cloud: debug 38 | server: 39 | port: 8081 40 | 41 | feign: 42 | hystrix: 43 | enabled: true 44 | 45 | # To set thread isolation to SEMAPHORE 46 | #hystrix: 47 | # command: 48 | # default: 49 | # execution: 50 | # isolation: 51 | # strategy: SEMAPHORE 52 | 53 | hystrix: 54 | command: 55 | # 전역설정 56 | default: 57 | execution.isolation.thread.timeoutInMilliseconds: 610 58 | --- 59 | 60 | spring: 61 | profiles: docker 62 | cloud: 63 | stream: 64 | kafka: 65 | binder: 66 | brokers: my-kafka.kafka.svc.cluster.local:9092 67 | streams: 68 | binder: 69 | configuration: 70 | default: 71 | key: 72 | serde: org.apache.kafka.common.serialization.Serdes$StringSerde 73 | value: 74 | serde: org.apache.kafka.common.serialization.Serdes$StringSerde 75 | bindings: 76 | event-in: 77 | group: app 78 | destination: fooddelivery 79 | contentType: application/json 80 | event-out: 81 | destination: fooddelivery 82 | contentType: application/json 83 | -------------------------------------------------------------------------------- /customer/Dockerfile.command.handler: -------------------------------------------------------------------------------- 1 | FROM python:2.7-slim 2 | WORKDIR /app 3 | ADD . /app 4 | RUN pip install --trusted-host pypi.python.org -r requirements.txt 5 | ENV NAME World 6 | EXPOSE 8090 7 | CMD ["python", "command-handler.py"] 8 | 9 | 10 | -------------------------------------------------------------------------------- /customer/Dockerfile.policy.handler: -------------------------------------------------------------------------------- 1 | FROM python:2.7-slim 2 | WORKDIR /app 3 | ADD . /app 4 | RUN pip install --trusted-host pypi.python.org -r requirements.txt 5 | ENV NAME World 6 | EXPOSE 8090 7 | CMD ["python", "policy-handler.py"] 8 | 9 | 10 | -------------------------------------------------------------------------------- /customer/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 msaez-template 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /customer/README.md: -------------------------------------------------------------------------------- 1 | # python -------------------------------------------------------------------------------- /customer/command-handler.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from redis import Redis, RedisError 3 | from kafka import KafkaConsumer 4 | import os 5 | import socket 6 | 7 | app = Flask(__name__) 8 | 9 | @app.route("/customer") 10 | def hello(): 11 | 12 | html = "

Hello {name}!

" \ 13 | "Hostname: {hostname}
" 14 | 15 | return html.format(name=os.getenv("NAME", "world"), hostname=socket.gethostname()) 16 | 17 | if __name__ == "__main__": 18 | app.run(host='0.0.0.0', port=8084) 19 | -------------------------------------------------------------------------------- /customer/kubernetes/deployment.yml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: customer 5 | labels: 6 | app: customer 7 | spec: 8 | replicas: 1 9 | selector: 10 | matchLabels: 11 | app: customer 12 | template: 13 | metadata: 14 | labels: 15 | app: customer 16 | spec: 17 | containers: 18 | - name: command-handler 19 | image: username/customer-command-handler:latest 20 | ports: 21 | - containerPort: 8084 22 | - name: policy-handler 23 | image: username/customer-policy-handler:latest 24 | -------------------------------------------------------------------------------- /customer/kubernetes/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: customer 5 | labels: 6 | app: customer 7 | spec: 8 | ports: 9 | - port: 8080 10 | targetPort: 8080 11 | selector: 12 | app: customer -------------------------------------------------------------------------------- /customer/policy-handler.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | from redis import Redis, RedisError 3 | from kafka import KafkaConsumer 4 | import os 5 | import socket 6 | 7 | 8 | # To consume latest messages and auto-commit offsets 9 | consumer = KafkaConsumer('fooddelivery', 10 | group_id='', 11 | bootstrap_servers=['localhost:9092']) 12 | for message in consumer: 13 | print ("%s:%d:%d: key=%s value=%s" % (message.topic, message.partition, 14 | message.offset, message.key, 15 | message.value)) 16 | 17 | 18 | -------------------------------------------------------------------------------- /customer/requirements.txt: -------------------------------------------------------------------------------- 1 | Flask 2 | Redis 3 | kafka-python 4 | 5 | -------------------------------------------------------------------------------- /gateway/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8u212-jdk-alpine 2 | COPY target/*SNAPSHOT.jar app.jar 3 | EXPOSE 8080 4 | ENTRYPOINT ["java","-Xmx400M","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar","--spring.profiles.active=docker"] 5 | -------------------------------------------------------------------------------- /gateway/cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | - id: 'build' 3 | name: 'gcr.io/cloud-builders/mvn' 4 | args: [ 5 | 'clean', 6 | 'package', 7 | '-Dmaven.test.skip=true' 8 | ] 9 | ### Build 10 | - id: 'docker build' 11 | name: 'gcr.io/cloud-builders/docker' 12 | entrypoint: 'bash' 13 | args: 14 | - '-c' 15 | - | 16 | echo '$COMMIT_SHA =' $COMMIT_SHA 17 | docker build -t gcr.io/$PROJECT_ID/$_PROJECT_NAME:$COMMIT_SHA . 18 | ### Test 19 | ### Publish 20 | - id: 'publish' 21 | name: 'gcr.io/cloud-builders/docker' 22 | entrypoint: 'bash' 23 | args: 24 | - '-c' 25 | - | 26 | docker push gcr.io/$PROJECT_ID/$_PROJECT_NAME:$COMMIT_SHA 27 | ### deploy 28 | - id: 'deploy' 29 | name: 'gcr.io/cloud-builders/gcloud' 30 | entrypoint: 'bash' 31 | args: 32 | - '-c' 33 | - | 34 | PROJECT=$$(gcloud config get-value core/project) 35 | gcloud container clusters get-credentials "$${CLOUDSDK_CONTAINER_CLUSTER}" \ 36 | --project "$${PROJECT}" \ 37 | --zone "$${CLOUDSDK_COMPUTE_ZONE}" 38 | 39 | cat < 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.1.1.RELEASE 9 | 10 | 11 | com.example 12 | boot-camp-gateway 13 | 0.0.1-SNAPSHOT 14 | boot-camp-gateway 15 | 16 | 17 | 1.8 18 | Greenwich.SR2 19 | 20 | 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-actuator 26 | 27 | 28 | org.springframework.cloud 29 | spring-cloud-starter-gateway 30 | 31 | 32 | 33 | 34 | 35 | 36 | org.springframework.cloud 37 | spring-cloud-dependencies 38 | ${spring-cloud.version} 39 | pom 40 | import 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | org.springframework.boot 49 | spring-boot-maven-plugin 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /gateway/src/main/java/com/example/Application.java: -------------------------------------------------------------------------------- 1 | package com.example; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.ApplicationContext; 6 | 7 | @SpringBootApplication 8 | public class Application { 9 | 10 | public static ApplicationContext applicationContext; 11 | public static void main(String[] args) { 12 | applicationContext = SpringApplication.run(Application.class, args); 13 | } 14 | 15 | 16 | } 17 | 18 | 19 | -------------------------------------------------------------------------------- /gateway/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8088 3 | 4 | --- 5 | 6 | spring: 7 | profiles: default 8 | cloud: 9 | gateway: 10 | routes: 11 | - id: app 12 | uri: http://localhost:8081 13 | predicates: 14 | - Path=/주문/** /메뉴판/**/통합주문상태/** 15 | - id: pay 16 | uri: http://localhost:8082 17 | predicates: 18 | - Path=/결제이력/** 19 | - id: store 20 | uri: http://localhost:8083 21 | predicates: 22 | - Path=/주문관리/** /주문상세보기/** 23 | - id: customer 24 | uri: http://localhost:8084 25 | predicates: 26 | - Path= 27 | globalcors: 28 | corsConfigurations: 29 | '[/**]': 30 | allowedOrigins: 31 | - "*" 32 | allowedMethods: 33 | - "*" 34 | allowedHeaders: 35 | - "*" 36 | allowCredentials: true 37 | 38 | 39 | --- 40 | 41 | spring: 42 | profiles: docker 43 | cloud: 44 | gateway: 45 | routes: 46 | - id: app 47 | uri: http://app:8080 48 | predicates: 49 | - Path=/주문/** /메뉴판/**/통합주문상태/** 50 | - id: pay 51 | uri: http://pay:8080 52 | predicates: 53 | - Path=/결제이력/** 54 | - id: store 55 | uri: http://store:8080 56 | predicates: 57 | - Path=/주문관리/** /주문상세보기/** 58 | - id: customer 59 | uri: http://customer:8080 60 | predicates: 61 | - Path= 62 | globalcors: 63 | corsConfigurations: 64 | '[/**]': 65 | allowedOrigins: 66 | - "*" 67 | allowedMethods: 68 | - "*" 69 | allowedHeaders: 70 | - "*" 71 | allowCredentials: true 72 | 73 | server: 74 | port: 8080 75 | -------------------------------------------------------------------------------- /pay/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8u212-jdk-alpine 2 | COPY target/*SNAPSHOT.jar app.jar 3 | EXPOSE 8080 4 | ENTRYPOINT ["java","-Xmx400M","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar","--spring.profiles.active=docker"] 5 | -------------------------------------------------------------------------------- /pay/cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | ### Test 3 | # - id: 'test' 4 | # name: 'gcr.io/cloud-builders/mvn' 5 | # args: [ 6 | # 'test', 7 | # '-Dspring.profiles.active=test' 8 | # ] 9 | ### Build 10 | - id: 'build' 11 | name: 'gcr.io/cloud-builders/mvn' 12 | args: [ 13 | 'clean', 14 | 'package' 15 | # '-Dmaven.test.skip=true' 16 | ] 17 | # waitFor: ['test'] 18 | ### docker Build 19 | - id: 'docker build' 20 | name: 'gcr.io/cloud-builders/docker' 21 | args: 22 | - 'build' 23 | - '--tag=gcr.io/$PROJECT_ID/$_PROJECT_NAME:latest' 24 | - '.' 25 | ### Publish 26 | - id: 'publish' 27 | name: 'gcr.io/cloud-builders/docker' 28 | entrypoint: 'bash' 29 | args: 30 | - '-c' 31 | - | 32 | docker push gcr.io/$PROJECT_ID/$_PROJECT_NAME:latest 33 | ### deploy 34 | - id: 'deploy' 35 | name: 'gcr.io/cloud-builders/gcloud' 36 | entrypoint: 'bash' 37 | args: 38 | - '-c' 39 | - | 40 | PROJECT=$$(gcloud config get-value core/project) 41 | gcloud container clusters get-credentials "$${CLOUDSDK_CONTAINER_CLUSTER}" \ 42 | --project "$${PROJECT}" \ 43 | --zone "$${CLOUDSDK_COMPUTE_ZONE}" 44 | cat < 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.1.9.RELEASE 9 | 10 | 11 | fooddelivery 12 | pay 13 | 0.0.1-SNAPSHOT 14 | pay 15 | Demo project for Spring Boot 16 | 17 | 18 | 1.8 19 | Greenwich.RELEASE 20 | Germantown.SR1 21 | 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-data-jpa 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-data-rest 31 | 32 | 33 | 34 | com.h2database 35 | h2 36 | runtime 37 | 38 | 39 | 40 | org.springframework.cloud 41 | spring-cloud-starter-openfeign 42 | 43 | 44 | 45 | 46 | 47 | org.springframework.cloud 48 | spring-cloud-starter-stream-kafka 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-actuator 53 | 54 | 55 | 56 | org.springframework.boot 57 | spring-boot-starter-test 58 | test 59 | 60 | 61 | 62 | 63 | 64 | 65 | org.springframework.cloud 66 | spring-cloud-dependencies 67 | ${spring-cloud.version} 68 | pom 69 | import 70 | 71 | 72 | org.springframework.cloud 73 | spring-cloud-stream-dependencies 74 | ${spring-cloud-stream.version} 75 | pom 76 | import 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | org.springframework.boot 85 | spring-boot-maven-plugin 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /pay/src/main/java/fooddelivery/AbstractEvent.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | import fooddelivery.config.kafka.KafkaProcessor; 4 | import com.fasterxml.jackson.core.JsonProcessingException; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import org.springframework.messaging.MessageChannel; 7 | import org.springframework.messaging.MessageHeaders; 8 | import org.springframework.messaging.support.MessageBuilder; 9 | import org.springframework.util.MimeTypeUtils; 10 | 11 | import java.text.SimpleDateFormat; 12 | import java.util.Date; 13 | 14 | public class AbstractEvent { 15 | 16 | String eventType; 17 | String timestamp; 18 | 19 | public AbstractEvent(){ 20 | this.setEventType(this.getClass().getSimpleName()); 21 | SimpleDateFormat defaultSimpleDateFormat = new SimpleDateFormat("YYYYMMddHHmmss"); 22 | this.timestamp = defaultSimpleDateFormat.format(new Date()); 23 | } 24 | 25 | public String toJson(){ 26 | ObjectMapper objectMapper = new ObjectMapper(); 27 | String json = null; 28 | 29 | try { 30 | json = objectMapper.writeValueAsString(this); 31 | } catch (JsonProcessingException e) { 32 | throw new RuntimeException("JSON format exception", e); 33 | } 34 | 35 | return json; 36 | } 37 | 38 | public void publish(){ 39 | this.publish(this.toJson()); 40 | } 41 | public void publish(String json){ 42 | if( json != null ){ 43 | 44 | /** 45 | * spring streams 방식 46 | */ 47 | KafkaProcessor processor = Application.applicationContext.getBean(KafkaProcessor.class); 48 | MessageChannel outputChannel = processor.outboundTopic(); 49 | 50 | outputChannel.send(MessageBuilder 51 | .withPayload(json) 52 | .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON) 53 | .build()); 54 | 55 | } 56 | } 57 | 58 | 59 | public String getEventType() { 60 | return eventType; 61 | } 62 | 63 | public void setEventType(String eventType) { 64 | this.eventType = eventType; 65 | } 66 | 67 | public String getTimestamp() { 68 | return timestamp; 69 | } 70 | 71 | public void setTimestamp(String timestamp) { 72 | this.timestamp = timestamp; 73 | } 74 | 75 | public boolean isMe(){ 76 | return getEventType().equals(getClass().getSimpleName()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /pay/src/main/java/fooddelivery/Application.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | import fooddelivery.config.kafka.KafkaProcessor; 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.ApplicationContext; 6 | import org.springframework.cloud.stream.annotation.EnableBinding; 7 | import org.springframework.cloud.openfeign.EnableFeignClients; 8 | 9 | 10 | @SpringBootApplication 11 | @EnableBinding(KafkaProcessor.class) 12 | @EnableFeignClients 13 | public class Application { 14 | protected static ApplicationContext applicationContext; 15 | public static void main(String[] args) { 16 | applicationContext = SpringApplication.run(Application.class, args); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pay/src/main/java/fooddelivery/PolicyHandler.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | import fooddelivery.config.kafka.KafkaProcessor; 4 | import com.fasterxml.jackson.databind.DeserializationFeature; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.cloud.stream.annotation.StreamListener; 8 | import org.springframework.messaging.handler.annotation.Payload; 9 | import org.springframework.stereotype.Service; 10 | 11 | @Service 12 | public class PolicyHandler{ 13 | 14 | @StreamListener(KafkaProcessor.INPUT) 15 | public void whenever주문취소됨_결재취소함(@Payload 주문취소됨 주문취소됨){ 16 | 17 | if(주문취소됨.isMe()){ 18 | System.out.println("##### listener 결재취소함 : " + 주문취소됨.toJson()); 19 | } 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /pay/src/main/java/fooddelivery/config/kafka/KafkaProcessor.java: -------------------------------------------------------------------------------- 1 | package fooddelivery.config.kafka; 2 | 3 | import org.springframework.cloud.stream.annotation.Input; 4 | import org.springframework.cloud.stream.annotation.Output; 5 | import org.springframework.messaging.MessageChannel; 6 | import org.springframework.messaging.SubscribableChannel; 7 | 8 | public interface KafkaProcessor { 9 | 10 | String INPUT = "event-in"; 11 | String OUTPUT = "event-out"; 12 | 13 | @Input(INPUT) 14 | SubscribableChannel inboundTopic(); 15 | 16 | @Output(OUTPUT) 17 | MessageChannel outboundTopic(); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /pay/src/main/java/fooddelivery/결제승인됨.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | public class 결제승인됨 extends AbstractEvent { 4 | 5 | private Long id; 6 | private String orderId; 7 | private Double 금액; 8 | 9 | public 결제승인됨(){ 10 | super(); 11 | } 12 | 13 | public Long getId() { 14 | return id; 15 | } 16 | 17 | public void setId(Long id) { 18 | this.id = id; 19 | } 20 | public String getOrderId() { 21 | return orderId; 22 | } 23 | 24 | public void setOrderId(String orderId) { 25 | this.orderId = orderId; 26 | } 27 | public Double get금액() { 28 | return 금액; 29 | } 30 | 31 | public void set금액(Double 금액) { 32 | this.금액 = 금액; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /pay/src/main/java/fooddelivery/결제이력.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | import javax.persistence.*; 4 | import org.springframework.beans.BeanUtils; 5 | import java.util.List; 6 | 7 | @Entity 8 | @Table(name="결제이력_table") 9 | public class 결제이력 { 10 | 11 | @Id 12 | @GeneratedValue(strategy=GenerationType.AUTO) 13 | private Long id; 14 | private String orderId; 15 | private Double 금액; 16 | 17 | @PrePersist 18 | public void onPrePersist(){ 19 | 결제승인됨 결제승인됨 = new 결제승인됨(); 20 | BeanUtils.copyProperties(this, 결제승인됨); 21 | 결제승인됨.publish(); 22 | 23 | 24 | try { 25 | Thread.currentThread().sleep((long) (400 + Math.random() * 220)); 26 | } catch (InterruptedException e) { 27 | e.printStackTrace(); 28 | } 29 | } 30 | 31 | 32 | public Long getId() { 33 | return id; 34 | } 35 | 36 | public void setId(Long id) { 37 | this.id = id; 38 | } 39 | public String getOrderId() { 40 | return orderId; 41 | } 42 | 43 | public void setOrderId(String orderId) { 44 | this.orderId = orderId; 45 | } 46 | public Double get금액() { 47 | return 금액; 48 | } 49 | 50 | public void set금액(Double 금액) { 51 | this.금액 = 금액; 52 | } 53 | 54 | 55 | 56 | 57 | } 58 | -------------------------------------------------------------------------------- /pay/src/main/java/fooddelivery/결제이력Controller.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.web.bind.annotation.PathVariable; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | import org.springframework.web.bind.annotation.RequestMethod; 7 | import org.springframework.web.bind.annotation.RestController; 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.servlet.http.HttpServletResponse; 10 | import java.util.List; 11 | 12 | @RestController 13 | public class 결제이력Controller { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /pay/src/main/java/fooddelivery/결제이력Repository.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | import org.springframework.data.repository.PagingAndSortingRepository; 4 | 5 | public interface 결제이력Repository extends PagingAndSortingRepository<결제이력, Long>{ 6 | 7 | 8 | } -------------------------------------------------------------------------------- /pay/src/main/java/fooddelivery/결제취소됨.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | public class 결제취소됨 extends AbstractEvent { 4 | 5 | private Long id; 6 | 7 | public 결제취소됨(){ 8 | super(); 9 | } 10 | 11 | public Long getId() { 12 | return id; 13 | } 14 | 15 | public void setId(Long id) { 16 | this.id = id; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /pay/src/main/java/fooddelivery/주문취소됨.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | 4 | public class 주문취소됨 extends AbstractEvent { 5 | 6 | private Long id; 7 | 8 | public Long getId() { 9 | return id; 10 | } 11 | 12 | public void setId(Long id) { 13 | this.id = id; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /pay/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | --- 4 | 5 | spring: 6 | profiles: default 7 | jpa: 8 | properties: 9 | hibernate: 10 | show_sql: true 11 | format_sql: true 12 | cloud: 13 | stream: 14 | kafka: 15 | binder: 16 | brokers: localhost:9092 17 | streams: 18 | binder: 19 | configuration: 20 | default: 21 | key: 22 | serde: org.apache.kafka.common.serialization.Serdes$StringSerde 23 | value: 24 | serde: org.apache.kafka.common.serialization.Serdes$StringSerde 25 | bindings: 26 | event-in: 27 | group: pay 28 | destination: fooddelivery 29 | contentType: application/json 30 | event-out: 31 | destination: fooddelivery 32 | contentType: application/json 33 | 34 | logging: 35 | level: 36 | org.hibernate.type: trace 37 | org.springframework.cloud: debug 38 | server: 39 | port: 8082 40 | --- 41 | 42 | spring: 43 | profiles: docker 44 | cloud: 45 | stream: 46 | kafka: 47 | binder: 48 | brokers: my-kafka.kafka.svc.cluster.local:9092 49 | streams: 50 | binder: 51 | configuration: 52 | default: 53 | key: 54 | serde: org.apache.kafka.common.serialization.Serdes$StringSerde 55 | value: 56 | serde: org.apache.kafka.common.serialization.Serdes$StringSerde 57 | bindings: 58 | event-in: 59 | group: pay 60 | destination: fooddelivery 61 | contentType: application/json 62 | event-out: 63 | destination: fooddelivery 64 | contentType: application/json 65 | -------------------------------------------------------------------------------- /store/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:8u212-jdk-alpine 2 | COPY target/*SNAPSHOT.jar app.jar 3 | EXPOSE 8080 4 | ENTRYPOINT ["java","-Xmx400M","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar","--spring.profiles.active=docker"] 5 | -------------------------------------------------------------------------------- /store/cloudbuild.yaml: -------------------------------------------------------------------------------- 1 | steps: 2 | ### Test 3 | # - id: 'test' 4 | # name: 'gcr.io/cloud-builders/mvn' 5 | # args: [ 6 | # 'test', 7 | # '-Dspring.profiles.active=test' 8 | # ] 9 | ### Build 10 | - id: 'build' 11 | name: 'gcr.io/cloud-builders/mvn' 12 | args: [ 13 | 'clean', 14 | 'package' 15 | # '-Dmaven.test.skip=true' 16 | ] 17 | # waitFor: ['test'] 18 | ### docker Build 19 | - id: 'docker build' 20 | name: 'gcr.io/cloud-builders/docker' 21 | args: 22 | - 'build' 23 | - '--tag=gcr.io/$PROJECT_ID/$_PROJECT_NAME:latest' 24 | - '.' 25 | ### Publish 26 | - id: 'publish' 27 | name: 'gcr.io/cloud-builders/docker' 28 | entrypoint: 'bash' 29 | args: 30 | - '-c' 31 | - | 32 | docker push gcr.io/$PROJECT_ID/$_PROJECT_NAME:latest 33 | ### deploy 34 | - id: 'deploy' 35 | name: 'gcr.io/cloud-builders/gcloud' 36 | entrypoint: 'bash' 37 | args: 38 | - '-c' 39 | - | 40 | PROJECT=$$(gcloud config get-value core/project) 41 | gcloud container clusters get-credentials "$${CLOUDSDK_CONTAINER_CLUSTER}" \ 42 | --project "$${PROJECT}" \ 43 | --zone "$${CLOUDSDK_COMPUTE_ZONE}" 44 | cat < 2 | 4 | 4.0.0 5 | 6 | org.springframework.boot 7 | spring-boot-starter-parent 8 | 2.1.9.RELEASE 9 | 10 | 11 | fooddelivery 12 | store 13 | 0.0.1-SNAPSHOT 14 | store 15 | Demo project for Spring Boot 16 | 17 | 18 | 1.8 19 | Greenwich.RELEASE 20 | Germantown.SR1 21 | 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-data-jpa 27 | 28 | 29 | org.springframework.boot 30 | spring-boot-starter-data-rest 31 | 32 | 33 | 34 | com.h2database 35 | h2 36 | runtime 37 | 38 | 39 | 40 | org.springframework.cloud 41 | spring-cloud-starter-openfeign 42 | 43 | 44 | 45 | 46 | 47 | org.springframework.cloud 48 | spring-cloud-starter-stream-kafka 49 | 50 | 51 | org.springframework.boot 52 | spring-boot-starter-actuator 53 | 54 | 55 | 56 | org.springframework.boot 57 | spring-boot-starter-test 58 | test 59 | 60 | 61 | 62 | 63 | 64 | 65 | org.springframework.cloud 66 | spring-cloud-dependencies 67 | ${spring-cloud.version} 68 | pom 69 | import 70 | 71 | 72 | org.springframework.cloud 73 | spring-cloud-stream-dependencies 74 | ${spring-cloud-stream.version} 75 | pom 76 | import 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | org.springframework.boot 85 | spring-boot-maven-plugin 86 | 87 | 88 | 89 | 90 | 91 | -------------------------------------------------------------------------------- /store/src/main/java/fooddelivery/AbstractEvent.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | import fooddelivery.config.kafka.KafkaProcessor; 4 | import com.fasterxml.jackson.core.JsonProcessingException; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import org.springframework.messaging.MessageChannel; 7 | import org.springframework.messaging.MessageHeaders; 8 | import org.springframework.messaging.support.MessageBuilder; 9 | import org.springframework.util.MimeTypeUtils; 10 | 11 | import java.text.SimpleDateFormat; 12 | import java.util.Date; 13 | 14 | public class AbstractEvent { 15 | 16 | String eventType; 17 | String timestamp; 18 | 19 | public AbstractEvent(){ 20 | this.setEventType(this.getClass().getSimpleName()); 21 | SimpleDateFormat defaultSimpleDateFormat = new SimpleDateFormat("YYYYMMddHHmmss"); 22 | this.timestamp = defaultSimpleDateFormat.format(new Date()); 23 | } 24 | 25 | public String toJson(){ 26 | ObjectMapper objectMapper = new ObjectMapper(); 27 | String json = null; 28 | 29 | try { 30 | json = objectMapper.writeValueAsString(this); 31 | } catch (JsonProcessingException e) { 32 | throw new RuntimeException("JSON format exception", e); 33 | } 34 | 35 | return json; 36 | } 37 | 38 | public void publish(){ 39 | this.publish(this.toJson()); 40 | } 41 | public void publish(String json){ 42 | if( json != null ){ 43 | 44 | /** 45 | * spring streams 방식 46 | */ 47 | KafkaProcessor processor = Application.applicationContext.getBean(KafkaProcessor.class); 48 | MessageChannel outputChannel = processor.outboundTopic(); 49 | 50 | outputChannel.send(MessageBuilder 51 | .withPayload(json) 52 | .setHeader(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.APPLICATION_JSON) 53 | .build()); 54 | 55 | } 56 | } 57 | 58 | 59 | public String getEventType() { 60 | return eventType; 61 | } 62 | 63 | public void setEventType(String eventType) { 64 | this.eventType = eventType; 65 | } 66 | 67 | public String getTimestamp() { 68 | return timestamp; 69 | } 70 | 71 | public void setTimestamp(String timestamp) { 72 | this.timestamp = timestamp; 73 | } 74 | 75 | public boolean isMe(){ 76 | return getEventType().equals(getClass().getSimpleName()); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /store/src/main/java/fooddelivery/Application.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | import fooddelivery.config.kafka.KafkaProcessor; 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.context.ApplicationContext; 6 | import org.springframework.cloud.stream.annotation.EnableBinding; 7 | import org.springframework.cloud.openfeign.EnableFeignClients; 8 | 9 | 10 | @SpringBootApplication 11 | @EnableBinding(KafkaProcessor.class) 12 | @EnableFeignClients 13 | public class Application { 14 | protected static ApplicationContext applicationContext; 15 | public static void main(String[] args) { 16 | applicationContext = SpringApplication.run(Application.class, args); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /store/src/main/java/fooddelivery/PolicyHandler.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | import fooddelivery.config.kafka.KafkaProcessor; 4 | import com.fasterxml.jackson.databind.DeserializationFeature; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.cloud.stream.annotation.StreamListener; 8 | import org.springframework.messaging.handler.annotation.Payload; 9 | import org.springframework.stereotype.Service; 10 | 11 | @Service 12 | public class PolicyHandler{ 13 | 14 | @Autowired 주문관리Repository 주문관리Repository; 15 | 16 | @StreamListener(KafkaProcessor.INPUT) 17 | public void whenever결제승인됨_주문정보받음(@Payload 결제승인됨 결제승인됨){ 18 | 19 | if(결제승인됨.isMe()){ 20 | System.out.println("##### listener 주문정보받음 : " + 결제승인됨.toJson()); 21 | 22 | 주문관리 주문 = new 주문관리(); 23 | 주문.setId(결제승인됨.getOrderId()) 24 | 주문관리Repository.save(주문); 25 | } 26 | } 27 | @StreamListener(KafkaProcessor.INPUT) 28 | public void whenever결제취소됨_주문취소처리(@Payload 결제취소됨 결제취소됨){ 29 | 30 | if(결제취소됨.isMe()){ 31 | System.out.println("##### listener 주문취소처리 : " + 결제취소됨.toJson()); 32 | } 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /store/src/main/java/fooddelivery/config/kafka/KafkaProcessor.java: -------------------------------------------------------------------------------- 1 | package fooddelivery.config.kafka; 2 | 3 | import org.springframework.cloud.stream.annotation.Input; 4 | import org.springframework.cloud.stream.annotation.Output; 5 | import org.springframework.messaging.MessageChannel; 6 | import org.springframework.messaging.SubscribableChannel; 7 | 8 | public interface KafkaProcessor { 9 | 10 | String INPUT = "event-in"; 11 | String OUTPUT = "event-out"; 12 | 13 | @Input(INPUT) 14 | SubscribableChannel inboundTopic(); 15 | 16 | @Output(OUTPUT) 17 | MessageChannel outboundTopic(); 18 | 19 | } 20 | -------------------------------------------------------------------------------- /store/src/main/java/fooddelivery/결제승인됨.java: -------------------------------------------------------------------------------- 1 | 2 | package fooddelivery; 3 | 4 | import javax.persistence.PrePersist; 5 | 6 | public class 결제승인됨 extends AbstractEvent { 7 | 8 | private Long id; 9 | private String orderId; 10 | private Double 금액; 11 | 12 | public Long getId() { 13 | return id; 14 | } 15 | 16 | public void setId(Long id) { 17 | this.id = id; 18 | } 19 | public String getOrderId() { 20 | return orderId; 21 | } 22 | 23 | public void setOrderId(String orderId) { 24 | this.orderId = orderId; 25 | } 26 | public Double get금액() { 27 | return 금액; 28 | } 29 | 30 | public void set금액(Double 금액) { 31 | this.금액 = 금액; 32 | } 33 | 34 | 35 | @PrePersist 36 | public void delay(){ 37 | try { 38 | Thread.currentThread().sleep(5000); 39 | } catch (InterruptedException e) { 40 | e.printStackTrace(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /store/src/main/java/fooddelivery/결제취소됨.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | 4 | public class 결제취소됨 extends AbstractEvent { 5 | 6 | private Long id; 7 | 8 | public Long getId() { 9 | return id; 10 | } 11 | 12 | public void setId(Long id) { 13 | this.id = id; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /store/src/main/java/fooddelivery/배달시작됨.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | public class 배달시작됨 extends AbstractEvent { 4 | 5 | private Long id; 6 | private String 요리종류; 7 | private String 배달지주소; 8 | private String orderId; 9 | 10 | public 배달시작됨(){ 11 | super(); 12 | } 13 | 14 | public Long getId() { 15 | return id; 16 | } 17 | 18 | public void setId(Long id) { 19 | this.id = id; 20 | } 21 | public String get요리종류() { 22 | return 요리종류; 23 | } 24 | 25 | public void set요리종류(String 요리종류) { 26 | this.요리종류 = 요리종류; 27 | } 28 | public String get배달지주소() { 29 | return 배달지주소; 30 | } 31 | 32 | public void set배달지주소(String 배달지주소) { 33 | this.배달지주소 = 배달지주소; 34 | } 35 | public String getOrderId() { 36 | return orderId; 37 | } 38 | 39 | public void setOrderId(String orderId) { 40 | this.orderId = orderId; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /store/src/main/java/fooddelivery/주문관리.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | import javax.persistence.*; 4 | import org.springframework.beans.BeanUtils; 5 | import java.util.List; 6 | 7 | @Entity 8 | @Table(name="주문관리_table") 9 | public class 주문관리 { 10 | 11 | @Id 12 | @GeneratedValue(strategy=GenerationType.AUTO) 13 | private Long id; 14 | 15 | @PostPersist 16 | public void onPostPersist(){ 17 | 배달시작됨 배달시작됨 = new 배달시작됨(); 18 | BeanUtils.copyProperties(this, 배달시작됨); 19 | 배달시작됨.publish(); 20 | 21 | 22 | } 23 | 24 | @PrePersist 25 | public void onPrePersist(){ 26 | 쿠폰발행됨 쿠폰발행됨 = new 쿠폰발행됨(); 27 | BeanUtils.copyProperties(this, 쿠폰발행됨); 28 | 쿠폰발행됨.publish(); 29 | 30 | 31 | } 32 | 33 | 34 | public Long getId() { 35 | return id; 36 | } 37 | 38 | public void setId(Long id) { 39 | this.id = id; 40 | } 41 | 42 | 43 | 44 | 45 | } 46 | -------------------------------------------------------------------------------- /store/src/main/java/fooddelivery/주문관리Controller.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.web.bind.annotation.PathVariable; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | import org.springframework.web.bind.annotation.RequestMethod; 7 | import org.springframework.web.bind.annotation.RestController; 8 | import javax.servlet.http.HttpServletRequest; 9 | import javax.servlet.http.HttpServletResponse; 10 | import java.util.List; 11 | 12 | @RestController 13 | public class 주문관리Controller { 14 | 15 | } 16 | -------------------------------------------------------------------------------- /store/src/main/java/fooddelivery/주문관리Repository.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | import org.springframework.data.repository.PagingAndSortingRepository; 4 | 5 | public interface 주문관리Repository extends PagingAndSortingRepository<주문관리, Long>{ 6 | 7 | 8 | } -------------------------------------------------------------------------------- /store/src/main/java/fooddelivery/쿠폰발행됨.java: -------------------------------------------------------------------------------- 1 | package fooddelivery; 2 | 3 | public class 쿠폰발행됨 extends AbstractEvent { 4 | 5 | private Long id; 6 | private String 요리종류; 7 | private String 배달지주소; 8 | private String orderId; 9 | 10 | public 쿠폰발행됨(){ 11 | super(); 12 | } 13 | 14 | public Long getId() { 15 | return id; 16 | } 17 | 18 | public void setId(Long id) { 19 | this.id = id; 20 | } 21 | public String get요리종류() { 22 | return 요리종류; 23 | } 24 | 25 | public void set요리종류(String 요리종류) { 26 | this.요리종류 = 요리종류; 27 | } 28 | public String get배달지주소() { 29 | return 배달지주소; 30 | } 31 | 32 | public void set배달지주소(String 배달지주소) { 33 | this.배달지주소 = 배달지주소; 34 | } 35 | public String getOrderId() { 36 | return orderId; 37 | } 38 | 39 | public void setOrderId(String orderId) { 40 | this.orderId = orderId; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /store/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | --- 4 | 5 | spring: 6 | profiles: default 7 | jpa: 8 | properties: 9 | hibernate: 10 | show_sql: true 11 | format_sql: true 12 | cloud: 13 | stream: 14 | kafka: 15 | binder: 16 | brokers: localhost:9092 17 | streams: 18 | binder: 19 | configuration: 20 | default: 21 | key: 22 | serde: org.apache.kafka.common.serialization.Serdes$StringSerde 23 | value: 24 | serde: org.apache.kafka.common.serialization.Serdes$StringSerde 25 | bindings: 26 | event-in: 27 | group: store 28 | destination: fooddelivery 29 | contentType: application/json 30 | event-out: 31 | destination: fooddelivery 32 | contentType: application/json 33 | 34 | logging: 35 | level: 36 | org.hibernate.type: trace 37 | org.springframework.cloud: debug 38 | server: 39 | port: 8083 40 | --- 41 | 42 | spring: 43 | profiles: docker 44 | cloud: 45 | stream: 46 | kafka: 47 | binder: 48 | brokers: my-kafka.kafka.svc.cluster.local:9092 49 | streams: 50 | binder: 51 | configuration: 52 | default: 53 | key: 54 | serde: org.apache.kafka.common.serialization.Serdes$StringSerde 55 | value: 56 | serde: org.apache.kafka.common.serialization.Serdes$StringSerde 57 | bindings: 58 | event-in: 59 | group: store 60 | destination: fooddelivery 61 | contentType: application/json 62 | event-out: 63 | destination: fooddelivery 64 | contentType: application/json 65 | --------------------------------------------------------------------------------