티스토리 뷰

저번 시간에 이어서 이번에는 Orders 서비스를 구축할 예정입니다. 

현재까지 구축되어 있는 서비스는 User-Service 와 Catalog-Service 인데, 둘다 각각 회원 등록 이나 조회 그리고 상품 등록과 조회를 할 수 있는 기능들로 구현되어 있습니다. 이제 이 둘을 엮어줄 Order-Service를 통해서 상품을 구매하는 서비스를 구현할 예정입니다.

 

  • 본 포스팅은 IntelliJ 환경에서 구축 되어 있습니다. ( STS나 Eclipse 로도 제작이 가능합니다.)
  • Properties 대신 yml 파일로 어플리케이션의 환경 설정을 진행했습니다.
  • Gradle 대신 Pom.xml으로 Dependency를 추가합니다. 

 

자 아래의 표를 봐주세요.  

기능 마이크로 서비스 URI(API Gateway) HTTP Method
상품 목록 조회 Catalogs MicroService /catalog-service/catalogs GET
사용자 별 상품 주문 Orders MicroService /order-service/{user_id}/orders POST
사용자 별 주문 내역 조회 Orders MicroService /order-service/{user_id}/orders GET

해당 표는 Catalogs 서비스를 만들때 참초했던 표인데, 상품목록을 조회하는 기능은 저번 시간에 제작을 완료했습니다.

이제 만들어야할 기능은 사용자 별 상품 주문 기능과 사용자 별 주문 내역 조회 기능 입니다. 어떻게 보면 참 당연한 이야기 이지만 특정 상품을 주문할때 API방식을 이용할 것이라면 해당 URI에 접속한 유저의 id가 입력되는 것은 당연합니다. 그렇기 때문에 반드시 {user_id} 형태로 받을 수 있도록 설계를 해야합니다!

 

서론이 길었습니다! 바로 시작해볼게요.

제가 애용하는 사이트가 있죠 

https://start.spring.io/

해당 사이트를 사용해서 프로젝트를 생성해줄게요.

#Spring initializr

다음과 같이 Dependencies를 추가해주고 GENERATE를 통해서 프로젝트를 생성해보겠습니다. 

프로젝트 실행 후 pom.xml파일을 조금 수정 해줘야 합니다. 

 

#h2database version

		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<version>1.3.176</version>
			<scope>runtime</scope>
		</dependency>

h2 database에 version을 1.3.176으로 사용할게요. 이렇게 하는 이유는 1.4 버젼 이상 부터는 기능이 강화돼, 자동으로 DB를 생성하지 않습니다. 그래서 편의성을 위해서 1.3.176버젼을 사용합니다.

#Add model mapper

		<dependency>
			<groupId>org.modelmapper</groupId>
			<artifactId>modelmapper</artifactId>
			<version>2.3.8</version>
		</dependency>

해당 Dependency를 통해서 ModelMapper를 활성화 시켜줍니다. 

 

자 이제는 너무나도 익숙해졌지만... YML 파일을 수정해야 합니다. 

#application.yml 

server:
   port: 0

spring:
   application:
      name: order-service
   h2:
      console:
         enabled: true
         settings:
            web-allow-others: true
         path: /h2-console
   jpa:
      hibernate:
         ddl-auto: update
      database: h2
      defer-datasource-initialization: true
   datasource:
      driver-class-name: org.h2.Driver
      url: jdbc:h2:mem:testdb

eureka:
   instance:
      instance-id: ${spring.application.name}:${spring.application.instance_id:${random.value}}
   client:
      register-with-eureka: true
      fetch-registry: true
      service-url:
         defalutZone: http://127.0.0.1:8761/eureka

ddl-auto는 update로 되어있는데, 기존에는 생성에 개념이었다면 주문 시스템은 업데이트의 개념으로 다가서야 합니다.

그래도 구조자체는 계속 진행했던 User-service나 Catalog-Service에서 볼 수 있는 구조랑 유사하네요.

 

그리고 이번에는 미리 패키지를 만들어두고 작업을 할 예정입니다. 필요한 패키지는 크게 5가지에요

저는 이런식으로 만들었습니다. 

 

이번엔 순서를 지켜서 가장 먼저 DB 테이블을 정보를 가지고있는 OrderEntity 부터 만들게요

OrderEntity의 위치는 JPA 패키지 안에 작성하시면 됩니다. 

 

#OrderEntity 

package com.example.orderservice.jpa;

import lombok.Data;
import org.hibernate.annotations.ColumnDefault;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;

@Data
@Entity
@Table(name="orders")
public class OrderEntity implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 120, unique = true)
    private String productId;
    @Column(nullable = false)
    private Integer qty;
    @Column(nullable = false)
    private Integer unitPrice;
    @Column(nullable = false)
    private Integer totalPrice;

    @Column(nullable = false)
    private String userId;
    @Column(nullable = false, unique = true)
    private String orderId;

    @Column(nullable = false,updatable = false,insertable = false)
    @ColumnDefault(value ="CURRENT_TIMESTAMP")
    private Date createdAt;
}

주문 테이블에 들어가야할 정보를 입력해줍니다. 

그리고 같은 위치에 해당 테이블 정보를 다뤄줄 OrderRepository 인터페이스도 제작 할게요

#OrderRepository ( Interface ) 

package com.example.orderservice.jpa;

import org.springframework.data.repository.CrudRepository;

public interface OrderRepository extends CrudRepository<OrderEntity, Long> {
    OrderEntity findByOrderId(String productId);
    Iterable<OrderEntity> findByUserId(String userId);
}

 

다음으로는 DB와 원활한 통신을 위해서 Dto를 만들어줍니다.  DTO의 역할을 간략히 설명하자면

DB로 연결되기 위한 중간 계층 관리자라고 생각하면 편하실거에요.

#OrderDto

package com.example.orderservice.dto;

import lombok.Data;

import java.io.Serializable;

@Data
public class OrderDto implements Serializable {
    private String productId;
    private Integer qty;
    private Integer unitPrice;
    private Integer totalPrice;

    private String orderId;
    private String userId;
}

 

진행하다보니 Serializable을 굉장히 많이 사용하는데, 그 이유가 문뜩 궁금해졌습니다. Serializable은 직렬화 입니다. 

가장 와닿는 예시로는 저희가 외국사람과 소통하기 위해 만국 공통어인 영어를 사용하는 이유와 같습니다. 

직렬화를 해주지 않으면 외부에서 접근하기가 어렵습니다. 일반적으로 데이터의 타입이 Reference Type(참조 타입) 이면 접근이 안되는 이슈가 발생해요.

그렇기 때문에 직렬화를 통해서 Value Type으로 공통어로 만들어 줍니다! 그렇기에 Serializable은 반드시는 거쳐야 하는 선행 과정이었어요.

 

그럼 계속해서 VO 패키지안에 ResponseOrder라는 클래스를 만들어줍니다. 

해당 클래스는 응답을 위한 내용이 담기게 될거에요.

#ResponseOrder.java

package com.example.orderservice.vo;

import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Data;

import java.util.Date;

@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseOrder {
    private String productId;
    private Integer qty;
    private Integer unitPrice;
    private Integer totalPrice;
    private Date createdAt;

    private String orderId;
}

 

다음으로는 RequestOrder 클래스를 만들어 줍니다. 

해당 클래스는 클라이언트의 주문요청 시 필요한 내용들이 담기게 될거에요.

#RequestOrder.java

package com.example.orderservice.vo;

import lombok.Data;

@Data
public class RequestOrder {
    private String productId;
    private Integer qty;
    private Integer unitPrice;
}

 

 

이제 실제 기능개발을 위해서 서비스를 작성해야합니다. 

먼저 Interface를 하나 만들어줄게요.  위치는 서비스 패키지 안에 작성하시면 됩니다. 

#OrderService(Interface)

package com.example.orderservice.service;

import com.example.orderservice.dto.OrderDto;
import com.example.orderservice.jpa.OrderEntity;

public interface OrderService {
    OrderDto createOrder(OrderDto orderDetails);
    OrderDto getOrderByOrderId(String orderId);
    Iterable<OrderEntity> getOrdersByUserId(String userId);
}

 

기능은 크게 3가지가 필요해요. 주문 생성을 위한 CreateOrder, 특정 주문을 조회하기 위한 getOrderByOrderId, 특정 유저의 주문정보를 모두 조회하기 위한 getOrderByUserId 이렇게 3가지 기능이 필요합니다. 

다음으로는 실제 기능들을 구현해봅시다.

#OrderServiceImpl(Interface)

package com.example.orderservice.service;

import com.example.orderservice.dto.OrderDto;
import com.example.orderservice.jpa.OrderEntity;
import com.example.orderservice.jpa.OrderRepository;
import com.netflix.discovery.converters.Auto;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.UUID;

@Data
@Slf4j
@Service
public class OrderServiceImpl implements OrderService{
    OrderRepository orderRepository;

    @Autowired
    public OrderServiceImpl(OrderRepository orderRepository){
        this.orderRepository = orderRepository;
    }

    @Override
    public OrderDto createOrder(OrderDto orderDto) {
        orderDto.setOrderId(UUID.randomUUID().toString());
        orderDto.setTotalPrice(orderDto.getQty() * orderDto.getUnitPrice());

        ModelMapper mapper = new ModelMapper();
        mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
        OrderEntity userEntity = mapper.map(orderDto,OrderEntity.class);

        orderRepository.save(userEntity);

        OrderDto returnUserDto = mapper.map(userEntity, OrderDto.class);

        return returnUserDto;
    }

    @Override
    public OrderDto getOrderByOrderId(String orderId) {
        OrderEntity orderEntity = orderRepository.findByOrderId(orderId);
        OrderDto orderDto = new ModelMapper().map(orderEntity, OrderDto.class);

        return orderDto;
    }

    @Override
    public Iterable<OrderEntity> getOrdersByUserId(String userId) {
        return orderRepository.findByUserId(userId);
    }
}

 

한가지 핵심이 있다면 Entity는 자체로 읽을 수 없습니다. 그래서 return 시에 Dto타입으로 정제를 해야 비로소

확인 할 수 있으니 해당 선행작업은 항상 진행해줘야합니다.  이제 기능적인 작업은 끝났으니 Controller를 작성해줘야 합니다.

#OrderController

package com.example.orderservice.controller;

import com.example.orderservice.dto.OrderDto;
import com.example.orderservice.jpa.OrderEntity;
import com.example.orderservice.service.OrderService;
import com.example.orderservice.vo.RequestOrder;
import com.example.orderservice.vo.ResponseOrder;
import org.modelmapper.ModelMapper;
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.List;

@RestController
@RequestMapping("/order-service")
public class OrderController {
    Environment env;
    OrderService orderService;

    @Autowired
    public OrderController(Environment env,OrderService orderService){
        this.env = env;
        this.orderService = orderService;
    }
    @PostMapping("/{userId}/orders")
    public ResponseEntity<ResponseOrder> createUser(@PathVariable("userId") String userId,
                                                    @RequestBody RequestOrder orderDetails){
        ModelMapper mapper = new ModelMapper();
        mapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);

        OrderDto orderDto = mapper.map(orderDetails, OrderDto.class);
        orderDto.setUserId(userId);
        OrderDto createdOrder = orderService.createOrder(orderDto);

        ResponseOrder responseOrder = mapper.map(createdOrder, ResponseOrder.class);

        return ResponseEntity.status(HttpStatus.CREATED).body(responseOrder);

    }
    @GetMapping("/{userId}/orders")
    public ResponseEntity<List<ResponseOrder>> getOrder(@PathVariable("userId") String userId){
        Iterable<OrderEntity> orderList = orderService.getOrdersByUserId(userId);

        List<ResponseOrder> result = new ArrayList<>();
        orderList.forEach(v->{
            result.add(new ModelMapper().map(v,ResponseOrder.class));
        });

        return ResponseEntity.status(HttpStatus.OK).body(result);
    }

}

 

이제 끝이 보이네요... 마지막으로 게이트웨이에 등록해주면 서비스 작성이 완료 될 예정 입니다. 

최 하단에 다음과 같이 Route정보를 추가해줍니다. 

server:
   port: 8000

eureka:
   client:
      register-with-eureka: true
      fetch-registry: true
      service-url:
         defaultZone: http://localhost:8761/eureka

spring:
   application:
      name: apigateway-service
   cloud:
      gateway:
         default-filters:
            - name: GlobalFilter
              args:
                 baseMessage: Glabal Filter의 BaseMessage 에요!!
                 preLogger: true
                 postLogger : true
         routes:
            - id: user-service
              uri: lb://USER-SERVICE
              predicates:
                 - Path=/user-service/**
            - id: catalog-service
              uri: lb://CATALOG-SERVICE
              predicates:
                  - Path=/catalog-service/**
            - id: order-service
              uri: lb://ORDER-SERVICE
              predicates:
                 - Path=/order-service/**

그럼 이제, 저희가 지금까지 제작한 모든 서비스를 시작해볼게요.

ApiGataway-Service 와 Catalog-Service, Order-Service 그리고 User-Service까지 시작해보겠습니다. 

다음과 같이 서비스가 모두 구동된 것이 확인이 되나요?

그럼 일단 User-Service를 활용해 회원등록을 진행해볼게요

#회원등록

http://localhost:8000/user-service/users를 활용해서 

Body > raw 에 Json 타입으로 다음과 같이 RequestBody를 채워주고 등록을 진행합니다.

이때 userId는 복사해둡시다.

#상품정보 조회

그 뒤에 Catalog-Service에서 조회를 통해서 productId 하나 고릅시다. 

저는 CATALOG-001를 주문해볼게요.

 

#주문 등록

마지막으로 /{userId}/orders 형태였던거 기억나시나요?

해당 형식으로 POST 요청을 보낼게요.

그리고 마찬가지로 RequestBody에 필요한 정보들을 기입 후 결과를 확인해봅시다. 

결과가 잘 확인 되시나요? 그렇다면 따따봉입니다!

#H2-Database 조회

마지막으로 H2-Console에 접속해 결과를 확인했을때 다음과 같이 주문정보가 기입이 잘 되어있다면

성공입니다! 

 

이제 기본적으로 어플리케이션의 구색은 갖춰졌으니 다음 포스팅 부터는 고도화작업을 진행하겠습니다. 

감사합니다.

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/11   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
글 보관함