Skip to content
@FootballManagementMSA

FootballManagementMSA

✨ FootBall-Friends 풋볼프렌즈

image image image image

👉🏻 프로젝트 소개

기존 Monolithic한 프로젝트를 진행하며, 종속적인 서비스 계층과 협업 과정에서 Branch가 엉키는 경험을 통해 독립적인 아키텍처 환경에 대한 관심으로 Microservice Architecture 환경의 프로젝트를 진행하였습니다.

  • OO대학교 재학생 대상 축구 동아리를 만들고 싶을 때 🚲
  • 동아리간 경기 일정을 잡고 FIFA처럼 스쿼드를 짜고 싶을 때 🔌
  • 동아리 일정 관리를 수월하게 하고 싶을 때 👾

✨ 축구 동아리 관리 서비스, 풋볼프렌즈 입니다! 🥳

🛠 프로젝트 아키텍쳐

image


⚙ 기술 스택

✔ Frond-end

✔ Back-end

✔ Infra

✔ DevOps

✔ Monitoring



💡 주요 기능

  1. 메인 홈(본인 일정 및 정보 확인) ♾
  2. 구단 가입 및 검색 🆙
  3. 구단장의 가입 신청(Role 부여) 💬
  4. 일정 생성 및 조회 🔍
  5. 스쿼드 생성 및 조회 🗓
  6. 회원 정보 조회 🚦

메인 홈(본인 일정 및 정보 확인) 구단 가입 및 검색 구단장의 가입 신청(Role 부여)
일정 생성 및 조회 스쿼드 생성 및 조회 회원 정보 조회

🔆 트러블슈팅

Jwt Token Storage (Cookie? Session? Redis? Memcached?)

Refresh Token이란?

Access Token의 유효기간을 짧게하여 보안도 높이고, 편의성도 챙기는 방법이다. 로그인을 완료하면, 유효기간이 짧은 Access Token유효기간이 긴 Refresh Token을 발급해준다.

Access Token은 기존에 사용하던 JWT 토큰이라고 생각하면 되고, Refresh Token은 Access Token이 만료되었을 때, 새로 발급해주는 토큰이라고 생각하면 된다.

Refresh Token의 필요성

Access Token 만료시간을 짧게 하면 보안성은 좋아집니다. 그러나, Access Token의 만료시간을 짧게 가져가면 사이트를 이용하는 회원은 자주 로그인 해야되는 불편함이 있습니다.

따라서, Refresh Token을 이용하여 Access Token을 재발급할 수 있고 Access Token의 유효 기간을 짧고 자주 재발급 하도록 만들어 보안을 강화하면서 사용자는 로그아웃 되어 다시 로그인해야 되는 상황을 주지 않도록 하기 위함입니다.

Refresh Token을 어디에 저장해야 할까?

Refresh Token은 Access Token을 재발급하기 위한 용도입니다.

Refresh Token을 쿠키에 저장하면 오히려 보안성만 떨어뜨리는 행위가 됩니다. 쿠키는 CSRF 공격에 취약하다는 점을 가지고 있어 좋지 않은 방법이라고 결론을 내렸습니다.
마찬가지로 Refresh Token을 세션 스토리지에 저장하는 것도 XSS 공격의 취약성을 가지고 있습니다.

따라서 Refresh Token을 Redis에 저장하는 방식을 채택했습니다. 그 이유는

  1. Key - Value 방식, 인메모리 DB 방식으로 빠르게 접근할 수 있습니다.
  2. 브라우저에 비해 탈취 가능성이 낮다고 생각하는 redis 서버에 저장하는 방식입니다.
  3. Refresh Token은 영구적으로 저장되는 데이터가 아닙니다.

Redis(In-Memory DB) VS Memcached

레디스는 key-value 쌍으로 데이터를 관리할 수 있는 데이터 스토리지입니다. 모든 데이터를 메모리에(메인 메모리인 RAM) 저장하고 조회하는 in-memory 데이터베이스입니다.

Memcached 라는 인메모리 데이터 스토리지도 있지만, 성능차이가 크게 없고, Memcached는 문자열만 지원하기 때문에 Redis를 선택했습니다.

2. Gateway-Server JwtToken (Pre)Filter 적용

MSA 환경에서 JwtTokenFilter를 적용하는 과정은 Gateway-Server에서 시작합니다. 이 과정에서는 **Filter를 적용하여 모든 요청이 유효한 JWT 토큰을 가지고 있는지 검증**합니다. 검증을 통과한 요청만이 내부 서비스로 전달되며, 이는 보안을 강화하고 서비스 간의 안전한 통신을 보장합니다. 또한, Gateway-Se! rver는 로드 밸런싱도 담당하여, 요청을 여러 인스턴스에 균등하게 분배합니다. 이러한 과정을 통해 시스템의 안정성과 처리 능력을 높이며, MSA 환경에서의 서비스 운영을 최적화합니다.



요청이 들어오면, 매핑을 통해 프레디케이트에서 해당 요청이 처리될 조건을 판단합니다. 이후, 작업 실행 전에 **사전 필터(Pre Filter)를 통과**해야 하며, 이는 요청에 대한 초기 처리나 검증을 담당합니다. 조건에 부합하는 서비스가 실행되어, 요청에 대한 실제 로직이 처리됩니다. 작업이 종료된 후에는 후속 필터(Post Filter)를 통과하게 되는데, 이는 응답을 클라이언트로 보내기 전에 필요한 처리를 수행합니다. 필터는 프로퍼티 파일이나 자바 코드를 통해 정의할 수 있으며, 이를 통해 요청과 응답의 흐름을 유연하게 관리할 수 있습니다. 마지막으로, 처리된 응답은 매핑을 거쳐 클라이언트에게 전달됩니다. 이 과정을 통해, Spring Cloud Gateway는 다양한 요청에 대해 조건부 로직 실행, 사전 및 사후 처리를 통한 세밀한 요청/응답 관리를 가능하게 합니다.
3. Update 로직 수정(JPQL to JPA Dirty Checking)

문제 상황: 회원 정보 수정 로직을 구현할 때 @Modifying 어노테이션을 활용하여 Update 쿼리를 직접 작성하여 수정하도록 Repository에서 코드를 구현

JPA를 사용할 때 더티 체킹(Dirty Checking)을 활용하는 것은 매우 JPA스러운 접근 방식입니다. 더티 체킹은 엔터티의 상태가 변경될 때 이를 자동으로 감지하고 변경 사항을 데이터베이스에 반영하는 JPA의 핵심 기능 중 하나입니다. 이 과정은 트랜잭션이 커밋되는 시점에 실행되며, 변경된 엔터티의 스냅샷과 원본 엔터티를 비교하여 자동으로 UPDATE 쿼리를 생성하고 실행합니다.

더티 체킹을 이용하면, 개발자는 엔터티의 상태를 직접 관리하고 적절한 시점에 데이터베이스에 반영할 쿼리를 작성할 필요가 없습니다. 이는 코드의 복잡성을 줄이고, 오류 발생 가능성을 낮추며, 개발자가 비즈니스 로직에 더 집중할 수 있게 해줍니다. 또한, 트랜잭션 커밋 시점에 쓰기 지연 SQL 저장소에 쌓인 쿼리들이 일괄적으로 데이터베이스로 전송되기 때문에 성능 측면에서도 이점이 있습니다.

Modifying 방식

Spring Data JPA에서는 @Query 애노테이션을 사용하여 직접 정의한 쿼리를 실행할 수 있습니다. 특히, 데이터의 변경을 수반하는 INSERT, DELETE, UPDATE 같은 쿼리를 실행할 때는 @Modifying 애노테이션을 함께 사용해야 합니다. 이 조합을 사용하면 JPA의 변경 감지 기능을 건너뛰고, 쿼리 실행을 더 효율적으로 할 수 있습니다. 예를 들어, 특정 사용자의 이름을 업데이트하는 경우 다음과 같이 작성할 수 있습니다:

@Transactional
@Modifying
@Query("UPDATE User u SET u.name = :name WHERE u.id = :id")
int updateUserName(@Param("id") Long id, @Param("name") String name);

여기서 @Transactional은 해당 메서드의 실행을 트랜잭션 범위 내에서 처리하겠다는 것을 나타내며, @Modifying은 변경 쿼리를 실행할 것임을 명시합니다. @Query는 실행할 JPQL 쿼리를 정의하고, @Param은 쿼리에 전달될 파라미터를 지정합니다.

또한, JPA에서는 벌크 연산을 지원합니다. 벌크 연산이란, 단일 데이터가 아닌 대량의 데이터에 대한 UPDATE, DELETE 작업을 한 번에 처리하는 것을 의미합니다. 이를 통해 대량의 데이터를 효율적으로 관리할 수 있으며, 성능 개선에도 크게 기여합니다. 예를 들어, 모든 사용자의 나이를 한 살씩 증가시키고자 할 때 다음과 같이 할 수 있습니다:

@Transactional
@Modifying
@Query("UPDATE User u SET u.age = u.age + 1")
int incrementAllUserAges();

이 코드는 모든 사용자의 나이를 데이터베이스에서 한 번에 업데이트하고, 변경된 행의 수를 반환합니다. @Transactional과 @Modifying을 사용함으로써, JPA를 통해 효율적으로 벌크 연산을 수행할 수 있게 됩니다. 이 방식은 **데이터 처리 작업을 대규모로 진행할 때 특히 유용**하며, 애플리케이션의 성능 최적화에도 크게 기여할 수 있습니다.
4. SpringBoot + Kafka 연동

Kafka란

카프카(Kafka)는 웹사이트, 어플리케이션, 센서 등에서 수집된 데이터를 실시간으로 관리하고 전송하기 위해 설계된 분산 스트리밍 플랫폼입니다. 이 플랫폼은 데이터를 생성하는 어플리케이션과 데이터를 소비하는 어플리케이션 사이에서 중재자 역할을 하며, 데이터의 전송, 처리, 관리를 담당합니다. 카프카 시스템은 여러 요소(노드)로 구성될 수 있으며, 이를 '카프카 클러스터'라고 부릅니다.

이 시스템은 다른 메시징 시스템과 유사하게 어플리케이션과 서버 간의 비동기 데이터 교환을 용이하게 합니다. 또한, 카프카는 하루에 수조 개의 이벤트를 처리할 수 있는 능력을 가지고 있습니다. 간단히 말해, 카프카는 다양한 서비스로부터 나오는 데이터 흐름을 실시간으로 제어하고, 이를 통해 서비스 간 연결을 가능하게 하는 중추적인 역할을 하는 플랫폼입니다. 이를 통해 복잡한 데이터 환경에서도 효율적인 데이터 스트림 관리가 가능해집니다.

Kafka의 기본 구성 요소

image ▶ Cluster : 여러 대의 컴퓨터들이 연결되어 하나의 시스템처럼 동작하는 컴퓨터들의 집합
▶ Producer : 데이터를 만들어내어 전달하는 전달자의 역할
▶ Consumer : 프로듀서에서 전달한 데이터를 브로커에 요청하여 메시지(데이터)를 소비하는 역할
▶ Broker : 생산자와 소비자와의 중재자 역할을 하는 역할
▶ Topic : 보내는 메시지를 구분하기 위한 카테고리화

카프카(Kafka)는 기본적으로 listener를 통해 producer로부터의 요청을 받아 처리하는 구조를 가집니다. 이 시스템에서 'KafkaServer'는 broker 역할을 하며, producer와 consumer는 카프카가 제공하는 API를 통해 구현된 어플리케이션을 의미합니다.

카프카 클러스터는 하나 이상의 broker로 구성될 수 있습니다. 클러스터 내의 각 KafkaServer(broker)는 고유한 식별자인 'broker.id'를 부여받습니다. 또한, 이러한 broker는 producer로부터 생성된 메시지를 저장할 위치 정보와 클러스터의 메타정보를 저장 및 관리하기 위해 Zookeeper와 연결됩니다.

Kafka Cluster는 여러 브로커들의 정보를 관리하고 효과적인 리더 선출(leader election)을 위해 Zookeeper를 활용합니다. 특정 broker에 장애가 발생한 경우, 컨트롤러는 변경된 리더 파티션의 정보를 업데이트하기 전에 그 정보를 Zookeeper에 저장하는 방식으로 운영됩니다. 이러한 구조는 카프카 클러스터가 높은 가용성과 신뢰성을 유지할 수 있도록 도와줍니다.

Topic

카프카(Kafka)에서 이벤트 스트림은 '토픽(Topic)'이라는 이름으로 저장됩니다. 카프카의 토픽은 구체화된 이벤트 스트림을 의미하며, 연관된 이벤트들을 묶어 영속화하는 역할을 합니다. 이를 데이터베이스의 테이블이나 파일 시스템의 폴더에 비유할 수 있습니다.

토픽은 카프카에서 생산자(Producer)와 소비자(Consumer)를 분리하는 중요한 개념입니다. Producer는 카프카의 토픽에 메시지를 저장(push)하고, Consumer는 저장된 메시지를 읽어(pull)옵니다. 하나의 토픽에는 여러 Producer와 Consumer가 존재할 수 있습니다.

이러한 개념은 간단히 설명하면, 관련된 이벤트들이 모여 스트림을 형성하고, 이 스트림이 카프카에 저장될 때 토픽의 이름으로 저장됩니다. 이러한 과정을 통해 카프카는 대량의 데이터 스트림을 효율적으로 관리하고 처리할 수 있게 됩니다.
image

Partition

위에서 설명한 카프카의 토픽들은 여러 파티션으로 나눠집니다. 토픽이 카프카에서 일종의 논리적인 개념이라면, 파티션은 토픽에 속한 레코드를 실제 저장소에 저장하는 가장 작은 단위입니다. 각각의 파티션은 Append-Only 방식으로 기록되는 하나의 로그 파일입니다.

image

회원 탈퇴 API 구현

회원탈퇴 API 같은 경우, User-Server에서 회원을 탈퇴하면 Team-Server에 존재하는 UserSquad Table에서 삭제해야합니다. 이 상황에서 Kafka를 사용하여 회원 탈퇴 이벤트를 처리하는 경우, User-Server가 Kafka의 Producer 역할을 하고, Team-Server가 Consumer 역할을 합니다.

USER-SERVER

  1. Kafka Config (USER-SERVER)
@Bean
public Map<String, Object> UserProducerConfig() {
    return CommonJsonSerializer.getStringObjectMap(BOOTSTRAP_SERVERS_CONFIG);
}

@Bean
public ProducerFactory<String, Long> deleteUserProducerFactory() {
    return new DefaultKafkaProducerFactory<>(UserProducerConfig());
}

@Bean
public KafkaTemplate<String, Long> deleteUserKafkaTemplate(){
    return new KafkaTemplate<>(deleteUserProducerFactory());
}
  1. UserKafkaProducer
@Component
@RequiredArgsConstructor
public class UserKafkaProducer {
    private final KafkaTemplate<String, Long> deleteUserKafkaTemplate;

    public void deleteUser(Long userId) {
        deleteUserKafkaTemplate.send("user", userId);
    }

  1. USER-SERVICE
@Transactional
public void deleteUser(String studentId) {
    User user = userRepository.findByStudentId(studentId)
            .orElseThrow(() -> new NotFoundException(NOT_FOUND_STATUS_CODE, NOT_REGISTER_USER_EXCEPTION_MESSAGE));
    userRepository.delete(user);
    userKafkaProducer.deleteUser(user.getId());
}

TEAM-SERVER

  1. Kafka Config
@Bean
public Map<String, Object> ConsumerConfigs() {
    return CommonJsonDeserializer.getStringObjectMap(BOOTSTRAP_SERVERS_CONFIG, GROUP_ID_CONFIG);
}

@Bean
public ConsumerFactory<String, Long> deleteUserSquadConsumerFactory() {
    return new DefaultKafkaConsumerFactory<>(ConsumerConfigs());
}

@Bean
public ConcurrentKafkaListenerContainerFactory<String, Long> deleteUserSquadListener() {
    ConcurrentKafkaListenerContainerFactory<String, Long> factory =
            new ConcurrentKafkaListenerContainerFactory<>();
    factory.setConsumerFactory(deleteUserSquadConsumerFactory());
    return factory;
}

  1. TEAM-SERVICE
@Transactional
@KafkaListener(topics = "user", groupId = "group_2")
public void deleteUserSqaud(Long userId) {
    userSquadRepository.deleteAllByUserIds(userId);
}


👻 풋볼프렌즈 팀원들!

Role Name Github
BE 박종훈 https://github.com/euics
BE 변은성 https://github.com/bes99
BE 이재표 https://github.com/jaepyo-Lee
Android 임성우 https://github.com/imseongwoo
Android 이윤호 https://github.com/lyh990517
Android 김찬휘 https://github.com/1chanhue1
Design 박재원

Popular repositories Loading

  1. Football_Club_Manager Football_Club_Manager Public

    Matching schedule management app for college soccer clubs

    Kotlin 3 3

  2. User-Server User-Server Public

    Java 1

  3. Gateway-Server Gateway-Server Public

    Java

  4. Team-Server Team-Server Public

    Java

  5. Eureka Eureka Public

    Java

  6. .github .github Public

Repositories

Showing 6 of 6 repositories
  • Football_Club_Manager Public

    Matching schedule management app for college soccer clubs

    FootballManagementMSA/Football_Club_Manager’s past year of commit activity
    Kotlin 3 3 3 0 Updated May 27, 2024
  • .github Public
    FootballManagementMSA/.github’s past year of commit activity
    0 0 0 0 Updated May 13, 2024
  • FootballManagementMSA/Team-Server’s past year of commit activity
    Java 0 0 2 0 Updated Apr 11, 2024
  • FootballManagementMSA/User-Server’s past year of commit activity
    Java 1 0 1 0 Updated Mar 29, 2024
  • Eureka Public
    FootballManagementMSA/Eureka’s past year of commit activity
    Java 0 0 0 0 Updated Mar 21, 2024
  • FootballManagementMSA/Gateway-Server’s past year of commit activity
    Java 0 0 0 0 Updated Mar 21, 2024

Top languages

Loading…

Most used topics

Loading…