공부/MSA

[MSA] Spring Cloud로 개발하는 마이크로서비스 애플리케이션(MSA) - Catalogs and Orders Microservice 1(상품 목록 조회)

sesam 2024. 1. 20. 01:07
728x90

 

Catalogs and Orders Microservice 개요

APIs

기능 URI(API Gateway 사용 시) 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 Microservice

catalog-service

프로젝트 생성

버전은 이전에 user-service했던 것 처럼 다 낮추자..

그래야 강의처럼 돌아간다..

 

 

build.gradle

ModelMapper dependancy 추가

  • maven
<dependency>
    <groupId>org.modelmapper</groupId>
    <artifactId>modelmapper</artifactId>
    <version>3.1.1</version>
</dependency>

 

  • gradle
implementation 'org.modelmapper:modelmapper:2.4.2'

 

 

application.yml

server:
  port: 0

spring:
  application:
    name: catalog-service
  h2:
    console:
      enabled: true
      settings:
        web-allow-others: true
      path: /h2-console
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true
    generate-ddl: true # ✔여기 추가(이건 사용안해도 ㄱㅊ)
  datasource:
    driver-class-name: org.h2.Driver
    url: jdbc:h2:mem:testdb
#    username: sa
#    password: 1234

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

# ✔여기 추가
logging:
  level:
    com.example.catalogservice: DEBUG

 

 

💡 hibernate.ddl-auto VS. generate-ddl: true

💡 hibernate.ddl-auto VS. generate-ddl: true
참고링크

  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true
    generate-ddl: true​

 

hibernate.ddl-auto
- 해당 설정은 보다 상세하게 DDL 생성 전략을 설정할 수 있다.
- 둘다 기능은 비슷하다.(테이블 생성)
📌 hibernate.ddl-auto는 spring.jpa.generate-ddl보다 우선순위를 가진다.
즉, 두 설정이 같이 쓰인다면 spring.jpa.generate-ddl은 무시된다.


generate-ddl: true
- 상세한 설정이 어렵다.

 

💡 @Slf4j - Log Level

💡 @Slf4j - Log Level

참고링크 - @Slf4j - Log Level

ex. info 레벨이 설정되면 info 이상의 레벨인 info, warn, error에 대한 로그가 기록됨
Level이 높을수록 심각한 오류를 의미

1. trace : debug보다 세분화된 정보
2. debug : 디버깅하는데 유용한 세분화된 정보
3. info : 진행상황 같은 일반 정보
4. warn : 오류는 아니지만 잠재적인 오류 원인이 될 수 있는 경고성 정보
5. error : 요청을 처리하는 중 문제가 발생한 오류 정보

 

 

 

 

sql 파일 생성

data.sql

상품 db에 넣어놓자

insert into catalog(product_id, product_name, stock, unit_price)
    values('CATALOG-001', 'Berlin', 100, 1500);
insert into catalog(product_id, product_name, stock, unit_price)
    values('CATALOG-002', 'Tokyo', 110, 1000);
insert into catalog(product_id, product_name, stock, unit_price)
    values('CATALOG-003', 'Stockholm', 120, 2000);

 

 

 

CatalogEntity.java

@Data
@Entity
@Table(name = "catalog")
public class CatalogEntity implements Serializable {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

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

    @Column(nullable = false)
    private String productName;

    @Column(nullable = false)
    private Integer stock;

    @Column(nullable = false)
    private Integer unitPrice;

    @Column(nullable = false, updatable = false, insertable = false)
    @ColumnDefault(value = "CURRENT_TIMESTAMP") // h2에서 현재시간을 호출하는 함수를 가져오기
    private LocalDateTime createAt;
}

 

💡 @Column

💡 @Column
어노테이션의 속성을 사용해서 필드의 DDL을 자동으로 생성할 수 있다.
자세한건 아래 링크를 보자!
참고링크 - [Spring] [JPA] How to Generate DDL

 

💡 implements Serializable

💡 implements Serializable
- 자바에서 사용되는 인터페이스 마커
- 이것은 해당 클래스가 직렬화될 수 있다는 것을 나타낸다.
- 즉, 해당 클래스의 상태를 바이트 스트림으로 변환하고 나중에 다시 복원할 수 있다는 것을 의미
- Serializable 인터페이스 자체에는 구현해야 할 메서드가 없으며, 단순히 해당 클래스가 직렬화 가능하다는 표시 역할

 

 

💡 @ColumnDefault(value = "CURRENT_TIMESTAMP")

💡 @ColumnDefault(value = "CURRENT_TIMESTAMP")
-  @ColumnDefault 어노테이션 :  JPA(Java Persistence API)에서 사용되며, 엔터티의 필드에 기본 값을 설정할 때 사용

- value = "CURRENT_TIMESTAMP":  해당 필드가 데이터베이스에 삽입될 때 기본값으로 현재 시간을 사용한다는 것을 의미한다.
따라서 데이터베이스에 레코드가 삽입될 때 해당 필드의 값은 현재 시간으로 자동 설정된다.


- updatable = false, insertable = false: 해당 필드가 업데이트 및 삽입에 대한 대상이 아니라는 것을 나타낸다.
따라서 이 필드는 외부에서 변경되거나 데이터베이스에 직접 삽입되지 않을 것이다.
대신, 데이터베이스에서 자동으로 현재 시간이 기록될 것이다.


❓ 어차피 LocalDateTime으로 타입을 지정해뒀는데 왜
@ColumnDefault(value = "CURRENT_TIMESTAMP")를 지정해야하는지 궁금했다.
여러 이유 중 가독성이 좋다는 이유가 있었다.

LocalDateTime은 단순 시간이다라는 뜻이지만,
"CURRENT_TIMESTAMP"라고 적어두면 "현재시간"이라는 것을 알 수 있다.

 

 

 

f

CatalogRepository.interface

public interface CatalogRepository extends CrudRepository<CatalogEntity, Long> {
    CatalogEntity findByProductId(String productId);
}

 

 

ResponseCatalog.java

@Data
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ResponseCatalog {
    private String productId;
    private String productName;
    private Integer unitPrice;
    private Integer stock;
    private LocalDateTime createdAt;
}

 

💡 @JsonInclude(JsonInclude.Include.NON_NULL)

 

 

 

 

 

CatalogService.interface

public interface CatalogService {
    Iterable<CatalogEntity> getAllCatalogs();
}

 

 

CatalogServiceImpl.java

@Data
@Slf4j
@Service
public class CatalogServiceImpl implements CatalogService{
    CatalogRepository catalogRepository;
    @Autowired
    public CatalogServiceImpl(CatalogRepository catalogRepository) {
        this.catalogRepository = catalogRepository;
    }

    @Override
    public Iterable<CatalogEntity> getAllCatalogs() {
        return catalogRepository.findAll();
    }
}

 

 

CatalogController.java

@RestController
@RequestMapping("/catalog-service")
public class CatalogController {
    Environment env;
    CatalogService catalogService;
    @Autowired
    public CatalogController(Environment env, CatalogService catalogService) {
        this.env = env;
        this.catalogService = catalogService;
    }

    @GetMapping("/health_check")
    public String status() {
        return String.format("It's Working in Catalog Service on PORT %s",
                env.getProperty("local.server.port"));
    }

    @GetMapping("/catalogs")
    public ResponseEntity<List<ResponseCatalog>> getCatalogs() {
        Iterable<CatalogEntity> catalogList = catalogService.getAllCatalogs();

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

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

 

 

 

서버 재실행 후 확인

h2 DB 확인

 

 

 

 

apigateway-service

apigateway-service에 Catalog-service 등록

 

application.yml

routes:
  - id: user-service
    uri: lb://USER-SERVICE
    predicates:
      - Path=/user-service/**
  - id: catalog-service
    uri: lb://CATALOG-SERVICE
    predicates:
      - Path=/catalog-service/**

 

 

서버 재실행 후 포스트맨 확인

등록한 상품이 잘 조회되고 있다.

 

 

💬 혼잣말

이렇게 sql파일을 만들어서 하는 방법이 있었다니!

전에 프로젝트할 때는 몰라서 duilder를 사용해서 saveAll()로 저장했다.

근데 sql파일을 만들어서 보관하면 최초db파일이 어디 있는지 찾기 편해서 좋은 것 같다.

builder보다는 insert into문을 사용해서 넣는게 더 편한 것 같다.

아래 처럼 db에 데이터를 넣으면 저 많은 개수만큼(100개는 넘었음..) 프로젝트를 시작하자마자 저 많은 객체를 다 생성한 다음에 builder로 넣는 것이니...좋지 않을 것 같다.

@Bean
    public ResponseEntity<String> saveBuildings() {
        // buildingRepository 비어 있으면 저장
        if(!buildingRepository.findAll().isEmpty()) {
            return ResponseEntity.status(HttpStatus.CONFLICT)
                    .body("Data already exists");
        }

        buildingRepository.saveAll(List.of(
                Building.builder().name("본부").grade(Grade.NORMAL).attackPower(0).defencePower(0).life(10000).woodRate(0).ironRate(0).foodRate(0).imagePath("CommandCenter.webp").buildingType(BuildingType.COMMANDCENTER).build(),
                Building.builder().name("일반 동물 훈련소").grade(Grade.NORMAL).attackPower(10).defencePower(10).life(100).woodRate(0).ironRate(0).foodRate(0).imagePath("NormalTrainingCenter.webp").buildingType(BuildingType.TRANINGCENTER).build(),
                Building.builder().name("일반 목공소").grade(Grade.NORMAL).attackPower(10).defencePower(10).life(100).woodRate(50).ironRate(0).foodRate(0).imagePath("NormalWoodFactory.webp").buildingType(BuildingType.WOODFACTORY).build(),
                Building.builder().name("일반 제철소").grade(Grade.NORMAL).attackPower(10).defencePower(10).life(100).woodRate(0).ironRate(50).foodRate(0).imagePath("NormalSteelMill.webp").buildingType(BuildingType.STEELMILL).build(),
                Building.builder().name("일반 식품 저장소").grade(Grade.NORMAL).attackPower(10).defencePower(10).life(100).woodRate(0).ironRate(0).foodRate(50).imagePath("NormalFoodTower.webp").buildingType(BuildingType.FOODSTORAGE).build(),
                Building.builder().name("일반 동물원").grade(Grade.NORMAL).attackPower(10).defencePower(10).life(100).woodRate(0).ironRate(0).foodRate(0).imagePath("NormalZoo.webp").buildingType(BuildingType.ZOO).build(),
//... 생략

 

 

그럼 data.sql파일 어떻게, 언제 실행돼서 db에 들어가는거지?

 

GPT왈: 
Spring Boot에서 data.sql 파일은 내장 데이터베이스(H2, HSQL, Derby 등)가 시작될 때 자동으로 실행되는 SQL 스크립트입니다. 이 파일은 클래스패스의 루트 디렉토리 또는 META-INF 디렉토리에 위치할 수 있습니다.

여기서 주의할 점은 data.sql은 내장 데이터베이스를 초기화하는 용도로 사용되므로 실제 운영 환경에서는 사용을 지양해야 합니다. 개발 환경에서 초기 데이터를 주입하거나, 테스트 시에 특정 데이터를 추가하는 용도로 많이 사용됩니다.

기본적으로 Spring Boot는 내장 데이터베이스가 클래스패스에 존재하면 애플리케이션을 시작할 때 data.sql을 실행합니다. application.properties 또는 application.yml 파일에서 내장 데이터베이스의 설정을 찾고, 그에 따라 초기화 작업을 수행합니다.

여기서 주의해야 할 것은 spring.datasource.initialization-mode 프로퍼티입니다. 이 프로퍼티의 기본값은 embedded로, 내장 데이터베이스가 클래스패스에 존재하면 초기화를 시도합니다. 만약 이 값을 never로 설정하면 내장 데이터베이스 초기화를 비활성화할 수 있습니다.

예를 들어, application.properties 파일에 다음과 같이 설정할 수 있습니다:

spring.datasource:
  initialization-mode: never

이렇게 하면 data.sql이 자동으로 실행되지 않습니다. 개발 환경에서는 자주 사용되지만, 실제 운영 환경에서는 데이터베이스 초기화를 다른 방식으로 수행하는 것이 권장됩니다.

 

어떤 다른 방법으로 데이터베이스를 초기화 하는데?


실제 운영 환경에서는 일반적으로 데이터베이스 초기화를 다른 방식으로 수행하는 것이 권장됩니다. 몇 가지 대안이 있을 수 있습니다:

Flyway 또는 Liquibase 사용:

Flyway나 Liquibase와 같은 마이그레이션 도구를 사용하여 데이터베이스 스키마를 버전 관리하고 업데이트합니다.
마이그레이션 도구는 변경된 스키마를 버전으로 관리하며, 애플리케이션을 업데이트할 때 스키마 변경 사항을 적용할 수 있습니다.
스크립트 수동 실행:

운영 환경에서는 초기 데이터베이스 스키마를 수동으로 관리하고, 필요한 경우에만 스크립트를 실행합니다.
스크립트를 직접 실행하면 데이터베이스 변경사항을 더 세밀하게 제어할 수 있습니다.
ORM 마이그레이션 도구 사용:

Hibernate나 Spring Data JPA와 같은 ORM 프레임워크를 사용하는 경우, 해당 프레임워크가 제공하는 마이그레이션 도구를 활용할 수 있습니다.
Hibernate의 hibernate.hbm2ddl.auto 속성을 사용하여 데이터베이스 스키마를 업데이트할 수 있습니다. 하지만 이것은 개발 또는 테스트 환경에서만 사용해야 하며, 운영 환경에서는 비활성화하는 것이 좋습니다.
Docker와 함께 초기화 스크립트 사용:

Docker를 사용하여 애플리케이션과 데이터베이스를 배포하는 경우 초기화 스크립트를 Dockerfile 또는 Docker Compose 파일에 포함시켜서 운영 환경에서 스크립트를 실행할 수 있습니다.
흔히 사용되는 방법은 Flyway나 Liquibase를 사용하는 것이며, 이들은 데이터베이스 마이그레이션에 특화되어 있어 변경 이력을 관리하고 업데이트를 쉽게 관리할 수 있게 해줍니다. 마이그레이션 도구를 사용하면 애플리케이션이 실행될 때 데이터베이스 변경을 자동으로 적용할 수 있습니다.