태그: , ,

카테고리:

업데이트:


마이크로서비스 프로젝트 만들기

다음과 같은 구조의 매우 단순한 마이크로서비스를 한번 만들어보곘습니다.

Spring Initializer로 Microservice Project 만들기

아래 사진과 같이 Spring Initializer를 통해 post-service, composite, comment-service, user-service 프로젝트를 만들어줍니다.

Dependency

  • Spring Web : 웹 서버를 만들기 위한 의존성

  • Actuator : 애플리케이션을 모니터링하기 위한 의존성

이때 프로젝트는 다음과 같이 위치시켜줍니다.

[Project Root]
 '-- microservices
     |-- composite
     |-- post-service
     |-- comment-service
     '-- user-serivce

통합 Build 설정하기

settings.gradle 작성하기

다음과 같이 settings.gradle에 작성해 빌드할 프로젝트들을 입력합니다.

include ':microservices:composite'
include ':microservices:comment-service'
include ':microservices:post-service'
include ':microservices:user-service'

gradle 실행 파일 이동하기

다음 명령어를 입력해 composite 프로젝트에 있는 gradle 파일들을 프로젝트 root 위치로 복사해줍니다.

cp -r microservices/composite/gradle .
cp microservices/composite/gradlew .
cp microservices/composite/gradlew.bat .
cp microservices/composite/.gitignore .

그리고 각 프로젝트에서는 gradle 파일이 필요없으므로 제거해줍니다.


find microservices -depth -name "gradle" -exec rm -rfv "{}" \;
find microservices -depth -name "gradlew*" -exec rm -rf "{}" \;

이때 프로젝트는 다음과 같은 구조를 가지고 있습니다.

[Project Root]
 |-- gradle
 |-- microservices
 |   |-- composite
 |   |-- post-service
 |   |-- comment-service
 |   '-- user-serivce
 |
 |-- .gitignore
 |-- gradlew
 |-- gradlew.bat
 '-- settings.gradle   

build 하기

이제 terminal에서 Project의 Root에 위치해 다음과 같이 명령어를 입력하면 정상적으로 빌드되는 것을 알 수 있습니다.

$ ./gradlew build

BUILD SUCCESSFUL in 6s

API와 Util Project 생성하기

이번에는 마이크로서비스의 API를 관리하는 api 프로젝트와 예외 및 유틸리티를 관리하는 util 프로젝트를 만들어줍니다.

API Project

이번에는 메인 어플리케이션이 없는 Gradle 프로젝트를 만들어주고 다음 코드들을 직성합니다.

  1. build.gradle

     plugins {
         id 'java'
         id 'org.springframework.boot' version '3.0.2'
         id 'io.spring.dependency-management' version '1.1.0'
     }
    
     group = 'com.microblog'
     version = '0.0.1-SNAPSHOT'
     sourceCompatibility = '17'
     bootJar.enabled = false
    
     repositories {
         mavenCentral()
     }
    
     dependencies {
         implementation 'org.springframework.boot:spring-boot-starter-web'
     }
    
     test {
         useJUnitPlatform()
     }
    
  2. 빌드 설정 및 의존성 추가

    우선 settings.gradle에 다음과 같이 build할 프로젝트를 추가해줍니다.

     include ':api' // 추가
     include ':microservices:composite'
     include ':microservices:comment-service'
     include ':microservices:post-service'
     include ':microservices:user-service'
    

    그리고 각 마이크로서비스 프로젝트마다 build.gradle에 다음과 같이 Project를 추가해줍니다.

     dependencies {
         implementation project(':api')
     }
    

Util Project

util또한 메인 어플리케이션이 없는 Gradle 프로젝트를 만들어주고 다음 코드들을 직성합니다.

  1. build.gradle

     plugins {
         id 'java'
         id 'org.springframework.boot' version '3.0.2'
         id 'io.spring.dependency-management' version '1.1.0'
     }
    
     group = 'com.mycloud'
     version = '0.0.1-SNAPSHOT'
     sourceCompatibility = '17'
     bootJar.enabled = false
    
     repositories {
         mavenCentral()
     }
    
     dependencies {
         implementation 'org.springframework.boot:spring-boot-starter-web'
     }
    
     test {
         useJUnitPlatform()
     }
    
  2. 빌드 설정 및 의존성 추가

    util 프로젝트도 api 프로젝트와 동일하게 settings.gradle에 다음과 같이 build할 프로젝트를 추가해줍니다.

     include ':api'
     include ':util' // 추가
     include ':microservices:composite'
     include ':microservices:comment-service'
     include ':microservices:post-service'
     include ':microservices:user-service'
    

    그리고 각 마이크로서비스 프로젝트마다 build.gradle에 다음과 같이 Project를 추가해줍니다.

     dependencies {
         implementation project(':util')
     }
    

패키지 검색 설정

이제 각 마이크로서비스에서 api 프로젝트의 패키지를 검색하도록 메인 Application 프로젝트에서 @ComponentScan 어노테이션을 추가해줍니다.

@SpringBootApplication
@ComponentScan("com.microblog")
public class CommentServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(CommentServiceApplication.class, args);
    }

}

이때 프로젝트 구조는 다음과 같습니다.

[Project Root]
 |-- gradle
 |
 |-- api
 |-- microservices
 |   |-- composite
 |   |-- post-service
 |   |-- comment-service
 |   '-- user-serivce
 |-- util
 |
 |-- .gitignore
 |-- gradlew
 |-- gradlew.bat
 '-- settings.gradle   



간단한 REST API 추가하기

이번에는 간단하게 Post의 ID를 입력하면 Post 정보 및 Comment를 가져오는 기능을 구현해보겠습니다.

Microservice application.yml 작성하기

postcomment의 application.yml은 다음과같이 port와 logging에 대한 설정을 해줍니다.

아직 서비스를 자동으로 등록하고 관리하는 기능을 구현하지 않았기 때문에 수동으로 port를 지정해줍니다.

저는 post의 경우 8001번 포트, comment의 경우 8002번 포트, user의 경우 8003번 포트를 이용하겠습니다.


server:
  port: 8001
  error:
    include-message: always

logging:
  level:
    root: info
    com:
      microblog: debug

그리고 복합 마이크로서비스인 composite의 경우는 다음과 같이 다른 microservice의 정보를 추가해 설정해줍니다.

server:
  port: 8000
  error:
    include-message: always

app:
  post:
    host: localhost
    port: 8001
  comment:
    host: localhost
    port: 8002
  user:
    host: localhost
    port: 8003

logging:
  level:
    root: info
    com:
      microblog: debug

Post Service의 Rest Controller 구현

  1. Post Model 만들기

    우선 첫번째로 api 프로젝트에서 Post Model을 만들어줍니다.

     // api/src/main/java/.../core/post/Post.java
    
     @Getter
     @Setter
     @RequiredArgsConstructor
     public class Post {
         private final int postId;
         private final String title;
         private final String author;
         private final int contents;
    
         Post() { // RestTemplate에서 JSON을 파싱하기 위한 생성자
             postId = 0;
             title = null;
             author = null;
             contents = null;
         }
     }
    
  2. Post Controller Interface 구현

    PostController Interface도 api 프로젝트에서 다음과같이 구현해줍니다.

     // api/src/main/java/.../core/post/PostController.java
    
     public interface PostController {
         @GetMapping(value="/post/{postId}", produces="application/json")
         Post getPost(@PathVariable int postId);
     }
    
  3. Post Rest Controller 구현

    방금 만든 PostController를 implements하는 방식으로 post-service 프로젝트에서 세부 기능을 구현합니다.

     // microservices/post-service/src/main/java/.../controller/PostControllerImpl.java
    
     @RestController
     public class PostControllerImpl implements PostController {
         private static final Logger LOG = LoggerFactory.getLogger(PostControllerImpl.class);
    
         @Override
         public Post getPost(int postId) {
             LOG.debug("Post Service 요청 수신 후 응답 - post id : {}", postId);
             return new Post(postId, "post title " + postId, "post author " + postId, "contents " + postId);
         }
     }
    
  4. 테스트

    이제 정상적으로 요청을 처리하는지 테스트합니다.

     ./gradlew build
    
     java -jar microservices/post-service/build/libs/*.jar &
    

    그리고 http://localhost:8001/post/123에 GET 요청을 보내면 다음과 같은 결과가 나옵니다.

     {
         "postId": 123,
         "title": "post title 123",
         "author": "post author 123",
         "contents": "contents 123"
     }
    

Comment Controller 구현

  1. Comment Model 만들기

    Comment Model도 api 프로젝트에서 만들어줍니다.

     // api/src/main/java/.../core/comment/Comment.java
     @Getter
     @Setter
     @RequiredArgsConstructor
     public class Comment {
         private final int postId;
         private final int commentId;
         private final String author;
         private final String comment;
    
         Comment() {
             this.postId = 0;
             this.commentId = 0;
             this.author = null;
             this.comment = null;
         }
     }
    
  2. CommentController 만들기

    CommentController Interface도 api 프로젝트에서 다음과같이 구현해줍니다.

     // api/src/main/java/.../core/comment/CommentController.java
    
     public interface CommentController {
         @GetMapping(value = "/comment/{postId}", produces = "application/json")
         List<Comment> getComments(@PathVariable int postId);
     }
    
  3. Comment Controller 구현

    CommentControllerImpl도 PostController와 동일하게 CommentController 인터페이스를 implements하는 방식으로 comment 프로젝트에서 세부 기능을 구현합니다.

     // microservices/comment-service/src/main/java/.../controller/CommentControllerImpl.java
     @RestController
     public class CommentControllerImpl implements CommentController {
         private static final Logger LOG = LoggerFactory.getLogger(CommentControllerImpl.class);
    
         @Override
         public List<Comment> getComments(int postId) {
             LOG.debug("Comment Service 요청 수신 후 응답 - post id : {}", postId);
    
             List<Comment> list = new ArrayList<>();
             list.add(new Comment(postId, 1, "작성자1", "내용1"));
             list.add(new Comment(postId, 2, "작성자2", "내용2"));
    
             return list;
         }
     }
    

Composite Controller 구현

이제 위에서 구현한 두가지 마이크로서비스를 사용해 사용자에게 응답하는 복합 마이크로서비스를 구현해줍니다.

  1. PostCompositeModel 만들기

     // api/src/main/java/.../composite/PostComposite.java
     @Getter
     @Setter
     @RequiredArgsConstructor
     public class PostComposite {
         private final int postId;
         private final String title;
         private final String author;
         private final String contents;
         private final List<Comment> comments;
     }
    
  2. PostCompositeController 만들기

     // api/src/main/java/.../composite/PostCompositeController.java
     public interface PostCompositeController {
         @GetMapping(value = "/composite/{postId}", produces = "application/json")
         PostComposite getPost(@PathVariable int postId);
     }
    
    
  3. CompositeControllerImpl 만들기

     // /microservices/composite/src/main/java/.../controller/CompositeControllerImpl.java
     @RestController
     public class CompositeControllerImpl implements PostCompositeController {
         private static final Logger LOG = LoggerFactory.getLogger(CompositeControllerImpl.class);
    
         private final RestTemplate restTemplate;
         private final String POST_URL;
         private final String COMMENT_URL;
    
         public CompositeControllerImpl(
                 @Value("${app.post.host}") String postHost,
                 @Value("${app.post.port}") String postPort,
                 @Value("${app.comment.host}") String commentHost,
                 @Value("${app.comment.port}") String commentPort
         ) {
             POST_URL = String.format("http://%s:%s/post", postHost, postPort);
             COMMENT_URL = String.format("http://%s:%s/comment", commentHost, commentPort);
    
             restTemplate = new RestTemplate();
         }
    
         @Override
         public PostComposite getPost(int postId) {
             Post post = getPostFromPostService(postId);
             List<Comment> comments = getCommentFromCommentService(postId);
    
             if(post == null) {
                 return null;
             }
    
             return new PostComposite(postId, post.getTitle(), post.getAuthor(), post.getContents(), comments);
         }
    
         private Post getPostFromPostService(int postId) {
             String url = String.format("%s/%d", POST_URL, postId);
             LOG.debug("POST URL : {}", url);
    
             Post post = restTemplate.getForObject(url, Post.class);
             if (post == null) {
                 return null;
             }
    
             LOG.debug("Post 반환 - postId: {}", post.getPostId());
    
             return post;
         }
    
         public List<Comment> getCommentFromCommentService(int postId) {
             String url = String.format("%s/%d", COMMENT_URL, postId);
    
             LOG.debug("COMMENT URL : {}", url);
    
             return restTemplate.exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference<List<Comment>>() {}).getBody();
         }
     }
    
    

    RestTemplate란?

    간편하게 REST API를 호출할 수 있는 스프링 내장 클래스입니다.
    getForObject : GET 방식 요청으로 결과를 객체로 반환합니다.
    exchange : 헤더 생성이 가능하고 및 어떤 요청이든 사용이 가능합니다.

일괄 실행 설정하기

터미널에서 각 마이크로서비스를 실행하려면 다음 명령어를 입력해 실행해야 합니다. 하지만 실행할때마다 다음 명령어를 입력하는 것은 매우 불편하기 때문에 향후 일괄 배포를 알아보기 전에 개발하는 동안 실행하기위해 IntelliJ에서 실행버튼 하나로 일괄적으로 실행하는 방법을 알아보겠습니다.

Terminal로 실행

java -jar microservices/post-service/build/libs/post-service-0.0.1-SNAPSHOT.jar &
java -jar microservices/comment-service/build/libs/comment-service-0.0.1-SNAPSHOT.jar &
java -jar microservices/user-service/build/libs/user-service-0.0.1-SNAPSHOT.jar &
java -jar microservices/composite/build/libs/composite-0.0.1-SNAPSHOT.jar & 

IntelliJ 실행 설정

  1. Edit Configuration 선택하기

  2. Add Compound

  3. 실행 App 추가하기

  4. 실행하기

이제 실행하면 다음과 같이 모든 Application이 실행되는 것을 볼 수 있습니다.

예외 처리하기

예외 발생시키기

서비스 상황에서 예외가 발생하는 상황을 가정한 기능을 만들기 위해 다음과 같이 Post Controller에 예외를 발생시키는 코드를 추가해줍니다.

// microservices/post/src/main/java/.../controller/PostControllerImpl.java

@Override
public Post getPost(int postId) {
    if(postId >= 100) {
        throw new IllegalArgumentException("Post ID는 100이하여야 합니다.");
    }

    LOG.debug("Post Service 요청 수신 후 응답 - post id : {}", postId);
    return new Post(postId, "post title " + postId, "post author " + postId, "contents " + postId);
}

예외 핸들링

우선 예외를 핸들링하기 위한 모델인 ErrInfo를 util 프로젝트에 다음과 같이 만들어줍니다.

// util/src/main/java/.../exception/ErrInfo.java
public class ErrInfo {
    private final LocalDateTime timestamp;
    private final String path;
    private final Integer status;
    private final String error;
    private final String message;

    // Object Mapping에 사용
    public ErrInfo() {
        timestamp = null;
        status = null;
        error = null;
        path = null;
        message = null;
    }

    public ErrInfo(HttpStatus httpStatus, String path, String message) {
        timestamp = LocalDateTime.now();
        this.status = httpStatus.value();
        this.error = httpStatus.getReasonPhrase();
        this.path = path;
        this.message = message;
    }
}

그리고 다음과 같이 RestControllerAdvice Annotation을 이용해 예외를 처리하는 기능을 구현해줍니다.

이때, ResponseStatus Annotation으로 Status를 지정해주고, ExceptionHandler Annotation으로 핸들링할 예외를 선택합니다.

// util/src/main/java/.../exception/ControllerExceptionHandler.java
@RestControllerAdvice
class ControllerExceptionHandler {
    private static final Logger LOG = LoggerFactory.getLogger(ControllerExceptionHandler.class);

    @ResponseStatus(HttpStatus.BAD_REQUEST)
    @ExceptionHandler(IllegalArgumentException.class)
    public @ResponseBody ErrInfo handleIllegalArgumentException(ServerHttpRequest request, Exception ex) {
        String path = request.getPath().pathWithinApplication().value();
        return new ErrInfo(HttpStatus.BAD_REQUEST, path, ex.getMessage());
    }

    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler(IllegalAccessException.class)
    public @ResponseBody ErrInfo handleIllegalAccessException(ServerHttpRequest request, Exception ex) {
        String path = request.getPath().pathWithinApplication().value();
        return new ErrInfo(HttpStatus.NOT_FOUND, path, ex.getMessage());
    }

}

@RestControllerAdvice란?
여러 RestController에 대해 전역적인 Exception Handler를 적용해주는 Annotation입니다.
해당 클래스 내부에서 @ResponseStatus와 @ExceptionHandler를 붙인 메소드를 이용해 해당 예외가 발생했을 때 Rest 요청에 어떻게 응답할지 지정할 수 있습니다.

Composite 프로젝트의 예외 핸들링하기

이제 Composite 프로젝트에서 다른 마이크로서비스에 요청을 했을 때 예외가 발생한 경우를 핸들링하는 기능을 구현해줍니다.

근데 이때 다른 마이크로서비스의 예외정보를 ObjectMapper를 이용해 파싱을 하는데 LocalDateTime을 파싱하지 못합니다. 따라서 Composite 의존성에 다음 의존성을 추가해주고 진행합니다.

implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
// microservices/composite/src/main/.../controller/CompositeControllerImpl.java
@RestController
public class CompositeControllerImpl implements PostCompositeController {
    private static final Logger LOG = LoggerFactory.getLogger(CompositeControllerImpl.class);

    private final ObjectMapper mapper; // 추가
    private final RestTemplate restTemplate;
    private final String POST_URL;
    private final String COMMENT_URL;

    public CompositeControllerImpl(
            @Value("${app.post.host}") String postHost,
            @Value("${app.post.port}") String postPort,
            @Value("${app.comment.host}") String commentHost,
            @Value("${app.comment.port}") String commentPort
    ) {
        POST_URL = String.format("http://%s:%s/post", postHost, postPort);
        COMMENT_URL = String.format("http://%s:%s/comment", commentHost, commentPort);

        restTemplate = new RestTemplate();
        mapper = new ObjectMapper(); // 추가
        mapper.registerModule(new JavaTimeModule()); // 추가
    }

    @Override
    public PostComposite getPost(int postId) throws Exception {
        try {
            Post post = getPostFromPostService(postId);
            List<Comment> comments = getCommentFromCommentService(postId);

            return new PostComposite(postId, post.getTitle(), post.getAuthor(), post.getContents(), comments);
        } catch (HttpClientErrorException ex) {
            HttpStatusCode statusCode = ex.getStatusCode();
            if (HttpStatus.NOT_FOUND.equals(statusCode)) {
                throw new IllegalAccessException(getErrorMessage(ex));
            } else if (HttpStatus.BAD_REQUEST.equals(statusCode)) {
                throw new IllegalArgumentException(getErrorMessage(ex));
            }
            LOG.warn("Unexpected error: {}", ex.getStatusCode());
            LOG.warn("Error: {}", ex.getResponseBodyAsString());
            throw ex;
        }

    }

    // 추가 (다른 마이크로서비스에서 발생한 예외를 ErrInfo에 매핑해서 메시지를 가져오는 기능 구현)
    private String getErrorMessage(HttpClientErrorException ex) {
        try {
            return mapper.readValue(ex.getResponseBodyAsString(), ErrInfo.class).getMessage();
        } catch (IOException ioex) {
            return ex.getMessage();
        }
    }

    private Post getPostFromPostService(int postId) throws IllegalAccessException {
        String url = String.format("%s/%d", POST_URL, postId);
        LOG.debug("POST URL : {}", url);

        Post post = restTemplate.getForObject(url, Post.class);
        if (post == null) {
            throw new IllegalAccessException("해당 Post를 찾을 수 없습니다."); // 추가
        }

        LOG.debug("Post 반환 - postId: {}", post.getPostId());

        return post;
    }

    public List<Comment> getCommentFromCommentService(int postId) {
        String url = String.format("%s/%d", COMMENT_URL, postId);

        LOG.debug("COMMENT URL : {}", url);

        return restTemplate.exchange(url, HttpMethod.GET, null, new ParameterizedTypeReference<List<Comment>>() {
        }).getBody();
    }
}

이제 다음과 같이 잘못된 Post ID값을 입력하면 다음과 같이 Post 서비스의 Exception을 읽어와 새로운 ErrInfo를 만들고 반환하는 것을 볼 수 있습니다.

참고 및 출처

댓글남기기