얼떨결에 마이크로서비스 아키텍처로 전환한 썰

이번 글은 MSA(MicroService Architecture) 의 필요성을 느끼지 못하다가 개발 진행 과정에서 의도치 않게 MSA를 도입하게 된 경험을 공유합니다. 개인 혹은 소규모 프로젝트에서 MSA는 때로 오버엔지니어링으로 여겨질 수 있지만, 최근 서비스 분리 트렌드를 고려할 때, 제 경험이 비슷한 고민을 하는 분들께 작은 참고가 되었으면 합니다.


초기 프로젝트 구조

architecture
현재 진행 중인 프로젝트는 특수동물을 키우는 사람들을 위한 커뮤니티입니다. 프로젝트 설계 당시에는 관리자, 사용자 서버가 각각 모놀리식(Monolithic Architecture) 으로 이루어져 있었고, 서버 간 WebClient를 이용해 통신하는 구조였습니다. 예전 팀 프로젝트도 이와 같은 구조로 진행했었고, 당시에는 다른 구조를 고려할 만큼 지식이 많지 않아 이대로 진행할 계획이었습니다.

발생한 문제

서버 간 JWT 인증 로직의 불일치로 인해 권한 문제가 발생했습니다.

요구사항 분석서 작성 시, 관리자는 사용자 리스트를 조회하고 사용자의 활동 내역을 관리하거나 탈퇴 처리가 가능해야 했습니다.

analysis
하지만 각 서버에 JWT 인증 로직이 따로 존재하고 접근 키가 달라서 WebClient를 이용해 관리자 서버에서 사용자 서버로 사용자 정보를 요청할 때 권한 문제가 발생했습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 개별 유저 조회
public Mono<UsersResponseDto> getUserByProviderId(String provider, String providerId) {

        return webClient.get()
                .uri("/api/users/{provider}/{providerId}", provider, providerId)
                .retrieve()
                .bodyToMono(UsersResponseDto.class);

    }

// 유저 리스트 조회
public Flux<UsersResponseDto> getUsers() {

    return webClient.get()
            .uri("/api/users")
            .retrieve()
            .bodyToFlux(UsersResponseDto.class);

}

문제를 해결하기 위해 관리자 액세스 키에 권한을 부여하고,

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// JWT 토큰 생성 메서드
public String generateAccessToken(String subject, Map<String, Object> claims) {
    Date now = new Date();
    Date expiryDate = new Date(now.getTime() + accessTokenValidityInMilliseconds);

    Map<String, Object> allClaims = new HashMap<>(claims != null ? claims : Map.of());
    allClaims.put("roles", List.of(ADMIN_ROLE)); // ⚠️ Here ⚠️

    return Jwts.builder()
            .setClaims(allClaims)
            .setSubject(subject)
            .setIssuedAt(now)
            .setExpiration(expiryDate)
            .signWith(accessKey, SignatureAlgorithm.HS256)
            .compact();
}

사용자 서버의 환경변수에 관리자 접근키를 설정해두었습니다.
이렇게 하면 문제가 해결될 것이라고 생각했지만, JWT는 발급한 서버 내에서만 유효하기 때문에 관리자 서버에서 발행한 토큰은 사용자 서버에서 생성한 값이 아니므로 아무런 의미가 없었습니다. 관리자 서버 로그인 시 사용자 서버로 동시에 로그인 처리가 되게끔 우회하는 방식도 있었지만,

login

이 방식은 사용자 서버에서 직접 데이터를 받아오는 것과 다를 바 없어 서버 간 통신을 구현한 의미가 퇴색되고 보안상 문제가 발생할 수 있다고 판단했습니다.

인증 서버 구축

결국 관리자와 사용자의 인증을 통합하는 별도의 인증 서버를 구축하여 문제를 해결했습니다.

기존에 각 서버에 적용했던 인증 로직을 모두 제거하고, 관리자와 사용자를 통합한 인증 서버를 따로 구현했습니다. 인증 서버에는 공통의 테이블을 두고 SecurityConfig나 JwtUtil 코드는 관리자와 사용자 로직을 각각 만들되, 한 서버 내에서 구현했습니다. 이렇게 구성하니 관리자로 로그인한 후 사용자 데이터를 관리자 권한으로 가져올 수 있게 되었습니다.

server

Gateway 설정

늘어난 서버들을 효율적으로 관리하고 클라이언트의 복잡도를 줄이기 위해 게이트웨이를 도입했습니다.

이렇게 서버를 한 대 더 증설하고 나니 포트 번호가 8080, 8082, 8083으로 늘어나고, 인증 로직이 한 서버에 몰리면서 엔드포인트도 변경되었습니다. 이는 프론트엔드 팀원이 개발 시 번거로울 것이라고 판단했습니다. 따라서 게이트웨이를 구축하여 클라이언트가 여러 서버와 직접 통신하지 않고, 게이트웨이를 통해 모든 요청을 라우팅하도록 설정했습니다. API 게이트웨이는 단일 진입점을 제공하여 클라이언트의 복잡도를 낮추고, 인증 및 인가와 같은 공통 기능을 처리하며, 로드 밸런싱과 라우팅을 담당하여 MSA 환경에서 필수적인 역할을 수행합니다. 저는 인증서버를 따로 구현해두었기 때문에 게이트웨이에서 인증 및 인가 처리는 제외하였습니다.

컨테이너 환경 도입

개발 및 배포 환경의 효율성을 높이기 위해 도커 컨테이너를 활용했습니다.

프론트엔드 팀원이 관리자 기능을 개발하려면 인증 서버, 게이트웨이, 관리자 서버 총 3대를 동시에 켜놓아야 했고, 이를 관리하는 것도 번거로울 것이라고 예상했습니다. 이러한 불편함을 해소하기 위해 각 서버에 도커파일을 만들고, GitHub Actions를 연동하여 dev 브랜치에 푸시하면 자동으로 프로젝트 빌드를 실행하고 빌드된 JAR 파일을 Docker Hub에 업로드하도록 자동화했습니다.

이제 다음 세 가지 명령어만으로 개발 환경을 쉽게 구성하고 관리할 수 있게 되었습니다:

docker compose pull (Github 코드 변경 시 이미지 업데이트)
docker compose up -d (컨테이너 실행)
docker compose down (컨테이너 종료)

이를 통해 개발 환경 구축의 복잡성을 크게 줄일 수 있었고, 나름 뿌듯함을 느꼈습니다.


마치며

이 작업을 통해 개발 및 배포 환경 전반에 대한 깊은 이해와 고민이 필요하다는 것을 깨달았습니다. 의도치 않게 시작된 변화였지만, 결과적으로 각 서비스의 독립성을 확보하고 개발 편의성을 높이는 중요한 경험이 되었습니다. 초기 설계의 한계를 마주하고 해결해 나가는 과정에서 얻은 지식과 경험은 앞으로 더 복잡한 시스템을 설계하고 구축하는 데 큰 자산이 될 것이라고 생각합니다.

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy