product 밈ㅊ recommendation -> MongoDB
review -> JPA 사용해 MySQL 데이터베이스에 접속

기술 요구사항

여전히 이 책은 mac 나는 윈도우~
6장에서는 새 도구 설치는 X
데이터베이스에 수동으로 접근하고자 데이터베이스 실행에 사용한 도커 이미지에 포함된
CLI 도구를 사용한다. ( port : mysql- 3306 , mongoDB - 27017)

마이크로서비스의 내부 계층 (6장)

  • 프로토콜 계층(Protocol layer) :공통 클래스인 GlobalControllerExceptionHandler와 RestController 애노테이션으로 구성되고 매우 얇음.
  • 서비스 계층 (service layer) : 각 마이크로서비스의 주요 기능
    product-composite 서비스에는 세 가지 핵심 마이크로서비스와 통신하는 통합 계층(integration layer)이 있고, 모든 핵심 마이크로서비스에는 자체 데이터베이스와 통신 하는 영속성 계층(Persistence layer) 있음

(책의 그림이 살짝 잘못된거같음 내 생각엔 product, recommendation, review 밑에 통합계층이 아니라 영속성 계층이 있어야할거같은 느낌 )

MongoDB에 저장된 데이터를 확인
docker-compose exec mongodb mongo product-db --quiet --eval "db.products.find()"

핵심 마이크로서비스에 persistence layer 추가

스프링 데이터 외에 자바 빈 매핑 도구 MapStruct도 사용
MapStruct == 스프링 데이터 엔티티 객체와 API 모델 클래스를 쉽게 상호 변환

각 핵심 마이크로서비스 빌드파일에 의존성을 추가해주고 (mongoDB, mysql, h2 등)

엔티티 클래스를 사용해 데이터 저장

id 필드와 version 필드는 엔티티 클래스에만 추가, api 모델 클래스에는 추가X
version 필드는 낙관적 잠금(optimistic locking)을 구현하고자 사용.
낙관적 잠금 : 스프링 데이터가 동시 업데이트에 의한 겹쳐 쓰기를 확인하고엔티티를 업데이트하고자 사용한다.
==
스테일 데이터인지 확인** (업데이트 요청version값 < version필드 값 이면 다른 사용자에 의해 업데이트된 데이터)

ProductEntity 클래스 (MongoDB)

  • @Document(collection="products") : 이 클래스가 MongoDB 엔티티 클래스며, products라는 이름의 MongoDB 컬렉션에 매핑된다는 뜻.
  • @Indexed (unique= true) : 애노테이션은 비즈니스 키, productId에 생성된 고유색인을 가져옴

RecommendationEntity 클래스 (MongoDB)

  • @CompoundIndex : productId와 recommendationId 필드로 구성된 복합 비즈니스 키를 위한 고유 복합 인덱스를 생성

ReviewEntity 클래스 (jpq-mysql)
여기는 몇번 써봐서 패스

스프링 데이터 리포지토리 정의

스프링 데이터는 리포지토리를 정의하기 위한 기본 클래스 집합을 제공하는데, 여기서 기본 클래스인 CrudRepository와 PagingAndSortingRepository를 사용.

  • CrudRepository : db에 데이터 생성(c) 저장된 데이터 읽고(r) 업데이트하고(u) 삭제하기(d)위한 표준 메서드 제공

  • PagingAndSortingRepository :Repository는 CrudRepository 클래스에 페이징 및 정렬 기능을 추가한 클래스.

영속성 중심의 자동 테스트 작성

  • @DataMongoTest: 테스트를 시작할 때 내장형 MongoDB 데이터베이스를 시작
  • @DataJpaTest: 테스트를 시작할 때 내장형 SQL 데이터베이스를 시작
    ** 테스트 부작용 최소화 위해서 springboot가 rollback 하도록 테스트 구성하는데 이 이유로 테스트실패할 수 있어서 @Transactional (propagation NOT_SUPPORTED)로 자동 롤백 활성화함
  1. create테스트
    findByProductId() 이용해서 검색되는지 확인
  1. update 테스트
    setupDb 메서드에서 생성 업데이트 -> findById()로 db 에서 엔티티 가져와서 확인
    version체크

  2. delete 테스트
    setupDb 메서드에서 생성-> 삭제 -> db에 남아있는지 existsById로 체크

  3. read 테스트
    setupDb 메서드가 생성한 엔티티 -> findByProductId()로 존재여부 확인 -> assertEqualsProduct() (=로컬 헬퍼 메서드) 이용해 반환된 엔티티가 setupDb 생성 에티티와 같은지 확인

오류 상황의 대체 흐름 확인 테스트 2개

  1. duplicateError -> 주석 처리 해줄 것
    키 중복이 제대로 처리됐는지 확인
    setupDb 메서드가 생성한 엔티티와 같은 비즈니스 키를 가진 엔티티를 저장하려고 시도
    (테스트 실패가 의도상 성공임 )

😋6. optimisticlockError
낙관적 잠금 메커니즘이 잘 작동하는지 확인하고자 stale 데이터를 업데이트하는 경우에 오류 처리가 제대로 되는지 확인
동일한 엔티티 2개 가져와서 -> 다른 변수에 저장 -> 그중 하나를 업데이트 (version도 자동으로 증가된다) -> 다른변수 업데이트 시도 OptimisticLockingFailureException 예외가 발생할 것이라고 예상

@Test
   	public void optimisticLockError() {

        // Store the saved entity in two separate entity objects
        ReviewEntity entity1 = repository.findById(savedEntity.getId()).get();
        ReviewEntity entity2 = repository.findById(savedEntity.getId()).get();

        // Update the entity using the first entity object
        entity1.setAuthor("a1");
        repository.save(entity1);

        //  Update the entity using the second entity object.
        // This should fail since the second entity now holds a old version number, i.e. a Optimistic Lock Error
        try {
            entity2.setAuthor("a2");
            repository.save(entity2);

            fail("Expected an OptimisticLockingFailureException");
        } catch (OptimisticLockingFailureException e) {}

        // Get the updated entity from the database and verify its new sate
        ReviewEntity updatedEntity = repository.findById(savedEntity.getId()).get();
        assertEquals(1, (int)updatedEntity.getVersion());
        assertEquals("a1", updatedEntity.getAuthor());
    }
  1. paging 테스트
    스프링 데이터가 제공하는 정렬 및 페이징 기능의 사용법 보여주기 위한 테스트
    id가 1001~1010d인 엔터티 10개 생성 -> PageRequest 만들어서 페이지당 엔티티 4개, id기준오름차순 페이지 요청 -> 헬퍼 메서드인 testNextPage 메서드를 사용해 세 페이지 읽고, 각 페이지 id가 예상과 맞는지 확인 -> 세번째 페이지에서는 남은 페이지 없는 지 확인 (expectsNextPage)
@Test
    public void paging() {

        repository.deleteAll();

        List<ProductEntity> newProducts = rangeClosed(1001, 1010)
            .mapToObj(i -> new ProductEntity(i, "name " + i, i))
            .collect(Collectors.toList());
        repository.saveAll(newProducts);

        Pageable nextPage = PageRequest.of(0, 4, ASC, "productId");
        nextPage = testNextPage(nextPage, "[1001, 1002, 1003, 1004]", true);
        nextPage = testNextPage(nextPage, "[1005, 1006, 1007, 1008]", true);
        nextPage = testNextPage(nextPage, "[1009, 1010]", false);
    }

테스트 관련 에러가 너무 떠서 원인을 분석하려고 뒤져보는데
각각 프로젝트를 하나씩열어서 하나씩 빌드하는데


이런식으로 root project를 못알아먹어서

https://projobs.tistory.com/111 처럼 setting.gradle에서

include ':api'
include ':util'

를 준 후 해봤지만
더 난리났다.

3장에서도 비슷한 에러를 겪었어서
마찬가지로 root project 인 VECTOR 폴더의 settings.gradle에
include를 이용해 api와 util을 인식시켜줘봤다.

그리고 설정에서 invalidate chache를 통해서 다시 빌드시켜줬다.

여전히 안된다..
(이거는 각각 microservice 기능이 아니라 root project인 vector에서 빌드만 잘 되면 되는거라 일단 넘어감)

이전 장까지 잘 되던 vector 프로젝트 빌드도 안된다 따흐흐흑

에러를 하나씩 수정해가고있다
진짜 많다 에러 ... 🤯😩

도대체 어디서 부터 잘못된건지 모르겠다..^^

일단 시간이 너무 소요돼서 내용확인부터 먼저 했다.

결국
저자의 github 기반의 코드로 깃허브 레포(VECTOR1)를 따로 파서 복사하면서 해봤다.
이 역시도 에러가 많이 났지만 구글링 하면서 몇개몇개 고쳐서 진행

와 드디어 build 성공이 떴다 진짜 너무 감격이다

내용을 이어서 진행한다.. 😅

서비스 계층에서 영속성 계층 사용

데이터베이스 연결 URL 기록

각각 마이크로서비스하나에 db 연결된걸 확장하면 각 ms가 실제 사용하는게 어떤 db인지 헷갈린다.
해결 : 마이크로서비 스가 시작된 직후에 접속한 데이터베이스의 URL을 기록하는 로그 문을 추가.

아래처럼 상위 서비스 클래스에서 main 메소드에 추가해서 log 출력

String mongodDbHost = ctx.getEnvironment().getProperty("spring.data.mongodb.host");
		String mongodDbPort = ctx.getEnvironment().getProperty("spring.data.mongodb.port");
		LOG.info("Connected to MongoDb: " + mongodDbHost + ":" + mongodDbPort);

새 API 추가

영속성 계층 통해서 db 정보 생성/ 삭제하려면 핵심 서비스 api에 api operation 추가해야함
아래는 create 와 delete 삭제 오퍼레이션의 예시

삭제 오퍼레이션의 구현의 멱등성 유지
즉, 삭제를 여러 번 호출하더라도 같은 결과를 반환해야한다는 것

영속성 계층 사용

product 마이크로서비스의 서비스계층인 ProductServiceImpl에 영속성 계층의 리포지토리 클래스와 자바 bean mapper 클래스를 생성자에 주입한다.

그리고 createProduct()구현에서는
save 통해 새 엔티티저장, 예외처리는 테스트에서 만든 DuplicateKeyException만 하고 나머지는 InvalidInputException예외로 변환

( 매퍼 메서드apiToEntity()와 entityToApi()를 사용해 API 모델 클래스와 엔티티 클래스를 상호 변환 한다)

자바 빈 매퍼 선언

MapStruct로 매퍼 클래스를 선언
product에서 서비스의 매퍼 클래스 ProductMapper

1 :entityToApi() 메서드는 엔티티 객체를 -> API 모델 객체에 매핑
2 : 엔티티 클래스에서는 serviceAddress 필드가 없으므로 ignore
3 : apiToEntity() 메서드는 API 모델 객체를 -> 엔티티 객체에 매핑
4 : API 모델 클래스에 없는 id, version 필드가 필요없으므로 ignore

MapStruct는 비단 이름이 같은 필드의 매핑만 지원하는 것 아니라 이름이 다른 필드의 매핑도 지원

ex. 추천 서비스의 rating, rate 필드 매핑

서비스 테스트 업데이트

생성 및 삭제 API 오퍼레이션에 대한 테스트 추가
setupDb()를 @Before 로 테스트에 우선되게 시행 시키고 (deleteAll)

duplicateError() 확인해 예상 오류가 맞는지 확인

delete도 멱등성 확인

헬퍼 메서드들 만들어서 api 전송하고 응답 상태 확인하고자 함

복합 서비스 API 확장

composite 엔티티의 생성 및 삭제 오퍼레이션을 위해 복합 API를 확장하는 방법

복합 서비스 API에 새 오퍼레이션 추가

기존 엔티티 생성, 삭제, 집계는 비슷하나 스웨거 기반 문서 어노테이션 추가해서 진행한다.
product-composite 엔티티 생성 api operation 추가하고
삭제하는거 추가하고
API 문서를 위한 설명문 application.yml 파일에 yaml 추가한다.

swagger 확인해보면 추가된것 확인 가능

통합 계층에 메서드 추가

복합 서비스의 생성 및 삭제 API를 구현하기전에-> 핵심 ms api 생성/ 삭제 가능하도록 통합 계층 확장 필요
@Override 이용해서 createProduct() 메소드를 구현해 product ms 호출하는 식으로 코드 구현한다.

HTTP 요청을 보내는 책임은 RestTemplate 객체에 위임하고, 오류 처리는 헬퍼 메서드인 handleHttpClientException에 위임

새 복합 API 오퍼레이션 구현

그리고 나서 복합 서비스 생성 및 삭제를 구현하는데

  • 생성의 경우
    ProductAggregate 객체를 상품, 추천, 리뷰 객체로 각각 나눈 다음 유형별로 통합계층의 create 호출한다.

  • 삭제의 경우
    각각 유형별 호출이 아니라 통합 계층의 delete 메소드 3개 호출해 삭제한다.

도커 컴포즈 환경에 데이터베이스 추가

도커를 이용해서 하려면 db 설정을 줘야함

도커 컴포즈가 제어하는 시스템 환경에 MongoDB와 MySQL을 추가하고 도커 컨테이너로 실행될 때도 데이터베이스를 찾을 수 있도록 마이크로서비스에 구성을 추가

데이터베이스 연결가능하도록 product, recommendation, review 의 yml에도 db관련 설정코드를 넣어준다.

MongoDB 및 MySQL CLI 도구

도커 컴포즈의 exec 커맨드로 데이터베이스의 CLI 도구를 실행 가능

docker-compose exec mongodb mongo --quiet
exit은 종료

docker-compose exec mysql mysql -uuser -preview-db
으로 mysql cli 들어갈수있고

마찬가지로 exit으로 종료

새로운 API와 영속성 계층의 수동 테스트

하면 200 OK가 떠야하는데..? 500이 뜬다 😭😢😢

그래 .. 한방에 될리가 없지 ....

아 docker를 띄울 때 메세지 중에서 3306 포트가 이미 사용중이어서 그렇다고 한다.

Error response from daemon: Ports are not available: exposing port TCP 0.0.0.0:3306 -> 0.0.0.0:0: listen tcp 0.0.0.0:3306: bind: Only one usage of each socket address (protocol/network address/port) is normally permitted.
workbench나 이외의 내 컴퓨터에 있는 프로젝트 때문에 그런것 같다
netstat -ano | findstr :3306로 사용중인지 확인해주고

mysql을 kill 해주는게 좋을까...? 그래도 한 번 해보자..

관리자모드로 열어서 taskkill /f /pid [pid] 해주기

그리고 다시 docker 실행
docker-compose up -d

헬시한 mysql...
mysql아 ... 건강하게 ... 잘 살아야한다~~~

다시 시도해본다

꺄홋

굿이다..


정리 & 느낀점

6장 .. 와 내용 많았고 에러도 많았고 나는 쓰러진다 ...

6장에서는 마이크로서비스 데이터에 영속성을 부여하는 방법을 배웠다. 스프링 데이터 프로젝트를 사용하며, 이의 핵심인 entity, repository를 사용해서 MongoDB 및 MySQL 데이터베이스에 데이터를 저장했다. spring boot의 @DataMongoTest, @DataJpaTest로 영속성 검증 테스트가 가능하다는 것도 봤고, 기존의 데이터 읽기용 API를 업데이트해 데이터베이스에 데이터를 생성하거나 삭제하는 오퍼레이션을 RESTful API에 추가함. 이전과 마찬가지로 docker compose가 관리하는 docker container를 사용해 DB를 실행하고 swagger를 통한 테스트도 해봤다.

책의 마지막엔 동기 API를 사용해 여러 마이크로서비스 엔티티로 구성된 복합 엔티티를 업데이트(=생성/삭제) 할때 관련된 마이크로서비스가 함께 업데이트되지 않아 일관성이 깨지는 경우가 생겨 바람직 하지 못하다고 해서
아.. 그렇구나... 하는 중

-> 7장에서 리액티브 마이크로서비스 필요성과 구축방법으로 설명해준다고 한다

아 대골빡아 🥵
지금 단계에서는 어려운데 진짜 어려운데 뭔가 멋지고 그렇다 ..

profile
HelloWorld! 같은 실수를 반복하지 말기위해 적어두자..

0개의 댓글