1. 요구사항

  • 채팅방 단위로 메시지를 주고받을 수 있어야 한다.
  • 각 채팅방의 최근 메시지를 미리보기 형태로 채팅방 리스트에서 확인할 수 있어야 한다.
  • 메시지 저장 시 WebSocket 성능 저하를 방지하기 위해 비동기 저장 구조를 적용해야 한다.

2. API 명세서

📡 WebSocket 메시지 구조 (STOMP)

각 채팅방에 메시지를 보내고 받는 방법은 다음과 같다.

 

첫번째 `메시지 전송` 경로는 클라이언트에서 메시지를 서버에 Publish 할 때 사용하는 Application Destination 이자, 클라이언트 입장에서는 서버에게 전송하는 용도의 STOMP 엔드포인트이다.

 

두번째 `메시지 수신` 경로는 클라이언트에서 서버의 메시지를 Subscribe 할 때 사용하는 Subscribe Destination 이자, 메시지 브로커가 관리하는 Broker Destination 이다.

Function URI Description
메시지 전송 /app/chat.send.{roomId} 특정 채팅방에 메시지 전송
메시지 수신 /topic/chatroom/{roomId} 해당 채팅방의 메시지 구독

📄 REST API

Method URI Description
GET /api/chat/rooms 모든 채팅방 목록 (최근 메시지 포함)
GET /api/chat/rooms/{roomId} 채팅방 상세 조회 (최근 메시지 포함)
POST /api/chat/rooms 채팅방 생성
DELETE /api/chat/roos/{roomId} 채팅방 삭제

3. 문제 해결

WebSocket 처리 중 DB 저장이 직접 이루어지면 성능 저하 우려

  • 문제: chatMessageRepository.save() 호출이 실시간 메시지 송수신 흐름에 개입될 경우, DB I/O로 인해 WebSocket 이 지연될 가능성이 있었다. 그러나 채팅 기록을 저장하지 않을 수도 없는 상황.
  • 해결: BlockingQueue + Background Thread 사용

메시지를 수신하면 enqueue() 메서드를 통해 비동기 큐에 저장한다.

ChatMessageQueueProcessor에서 별도의 쓰레드가 큐를 지켜보다가, queue.take()로 메시지를 꺼내고 저장한다.

@PostConstruct
public void startWorker() {
    Executors.newSingleThreadExecutor().submit(() -> {
        try {
            while (!Thread.currentThread().isInterrupted()) {
                ChatMessage message = queue.take();
                chatMessageRepository.save(message);
            }
        } catch (Exception e) {
            log.error("메시지 저장 중 예외 발생", e);
        }
    });
}

 최근 메시지 조회

  • 문제: 채팅방을 불러올 때, 해당 채팅방에서 가장 최신 내용을 미리보기로 불러와야 했다.
  • 해결:

처음 채팅방을 조회하려고 채팅방 객체를 불러올 때는, 딱 불러올 당시에 가장 최근 메시지를 곁들인 ChatRoomPreviewDto 를 호출한다. 이 Dto 에는 채팅방 객체뿐만이 아니라, 해당 채팅방 객체에 딸려있는 가장 최신 메시지도 함께 담겨있다. 즉, 각 채팅방 객체를 불러올 때, findTopByRoomIdOrderBySentAtDesc() 쿼리를 수행하도록 하였다.

 

  • 문제: 그러나, 처음 불러올 때만 최신화되어 있고,이 상태에서는 채팅방 미리보기에 표시된 최근 메시지가 오래된 내용으로 남게 되는 문제가 있었다. 채팅방 목록이 떠 있는 상태에서도 계속해서 신규 메시지가 올라올 가능성이 다분했다.
  • 해결:

이 문제를 해결하기 위해, WebSocket을 통한 실시간 푸시 방식을 도입했다. 사용자가 메시지를 보낼 때, 단순히 해당 채팅방 구독자에게만 메시지를 전송하는 것이 아니라, 모든 사용자에게 브로드캐스트하는 /topic/chatroom 채널에도 메시지를 전송하여

각 클라이언트의 채팅방 목록에서도 최신 메시지가 자동으로 반영되도록 구현했다.

messagingTemplate.convertAndSend("/topic/chatroom", saved);

 

  • 채팅방이 새로 생성될 때도 해당 채널을 통해 브로드캐스트됨
  • 이후 메시지 전송 시에도 별도로 최신 메시지를 요약해 해당 채널로 함께 전송하는 구조로 확장할 수 있음

 


 

4. 결론 및 추후 개발 사항

결론

이번 개발의 핵심은 초기 채팅방 목록이나 특정 채팅방을 불러올 때, 채팅방 객체 정보뿐만이 아니라, 해당 채팅방 객체의 가장 최근 메시지까지 불러오도록 설계하였다.

  • 채팅방 최초 진입 시:
    REST API를 통해 각 채팅방별 최신 메시지를 포함한 ChatRoomPreviewDto를 제공하여,
    사용자에게 빠른 미리보기 제공이 가능하도록 하였다.
  • 실시간 메시지 수신 시:
    메시지 본문은 채팅방별로 분리된 경로(/topic/chatroom/{roomId})를 통해 전송하고,
    이 경로를 통해서 채팅방 목록 API 를 이미 가져온 이후라도, 최신 메시지를 갱신할 수 있다.
다만 개선점: 메시지 전송 시 ChatRoomPreviewDto를 별도로 브로드캐스트하는 구조

채팅방의 메시지는 /topic/chatroom/{roomId} 를 통해 전송하더라도,
채팅방 미리보기용 메시지는 별도의 브로드캐스트(ex. /topic/chatroom/preview) 하여 채팅방 목록에서도 실시간 최신 메시지 갱신이 가능하도록 구성할 수 있다.

추후 개발 계획

  • JWT 기반 인증 방식 도입하여 무상태 Stateless 구조 실험
  • Redis Pub/Sub 기반 큐 구조로 확장 → 서버 다중화(스케일아웃) 대응
  • 채팅 메시지에 파일 전송, 이미지, 이모지 기능 추가
  • 메시지 국제화 (현재는 메시지 하드코딩)
  • 채팅방별 참여자 목록 관리, 인증 기반 메시징 보한 강화(SendToUser)
  • WebSocketSessionDesconnectEvent 활용한 입장/퇴장 알림 추가
  • 관리자 전용 시스템 메시지 messagingTemplate.convertAndSend()로 구현

 

1. 요구사항

  • 사용자는 별도의 회원가입 없이 이름만 입력해서 로그인할 수 있어야 한다.
  • 로그인된 사용자는 채팅방을 생성할 수 있어야 한다.
  • 채팅방 목록은 실시간으로 동기화되어야 한다.
  • 초기 진입 시에는 기존 채팅방 목록을 REST API로 불러와야 한다.

2. API 명세서

🔐 POST /api/login

  • 사용자 이름으로 로그인 (회원가입 없음)

Request

  • Content-Type: application/x-www-form-urlencoded
  • Parameter:
    • name: string (required)

Response (200 OK)

{
  "success": true,
  "code": 200,
  "message": "로그인 성공",
  "data": "mj"
}

인증 방식

  • 세션 쿠키(JSESSIONID) 기반
  • 서버는 HttpSessionSPRING_SECURITY_CONTEXT 저장하여 인증 유지

📋 GET /api/chat/rooms

  • 전체 채팅방 목록 조회

Request

  • Headers: Cookie: JSESSIONID=...

Response (200 OK)

{
  "success": true,
  "code": 200,
  "message": "요청 성공",
  "data": [
    {
      "id": 1,
      "name": "테스트 채팅룸",
      "createdAt": "2025-04-06 19:31:51"
    },
    {
      "id": 2,
      "name": "백엔드 공부방",
      "createdAt": "2025-04-06 19:31:54"
    }
  ]
}

인증 필요

  • 로그인 후에만 호출 가능

🔐 POST /api/chat/rooms

  • 새로운 채팅방 생성

Request

  • Parameter:
    • name: string (required)
  • Headers: Cookie: JSESSIONID=...

Response (200 OK)

{
  "success": true,
  "code": 200,
  "message": "요청 성공",
  "data": {
    "id": 2,
    "name": "testChatRoom",
    "createdAt": "2025-04-06 19:31:54"
  }
}

실시간 처리

  • 생성 성공 시 STOMP로 /sub/chat/rooms 채널에 broadcast 됨

3. 문제 해결 (Troubleshooting)

① MvcRequestMatcher permitAll 이슈

  • 문제: .requestMatchers(mvc.pattern("/login")).permitAll()이 동작하지 않음
  • 원인: MvcMatcher가 DispatcherServlet과 서블릿 경로(/)를 정확히 판단하지 못함
  • 해결: AntPathRequestMatcher/login을 허용하자 즉시 해결됨

관련 포스팅: https://jinn-o.tistory.com/386

 

[SpringSecurity6.1] requestMatchers에서 오류가 나기 시작했다

0. 서론최근 Spring Boot 3.x + Spring Security 6.x로 프로젝트를 시작하면서 기존에 잘 사용하던 `.requestMatchers("/login", "/api/**")` 구문이 갑자기 다음과 같은 에러를 발생시키기 시작했다!This method cannot decid

jinn-o.tistory.com

 

② 인증 상태 유지 실패

  • 문제: 로그인 후 인증이 유지되지 않아 /chat/rooms 호출 시 403 발생
  • 원인: 인증 정보를 SecurityContextHolder에는 저장했지만, HttpSession에 저장하지 않음
  • 해결: 로그인 시 HttpSessionSPRING_SECURITY_CONTEXT 수동 저장하여 해결

 

4. 결론 및 추후 개발 사항

결론

  • V1에서는 "비회원 이름 기반 로그인 + 실시간 채팅방 생성 + REST 조회"까지 구현 완료
  • WebSocket(STOMP) 기반 실시간 브로드캐스트 구조를 잡음
  • Spring Security + 수동 인증 구조도 직접 구성하며 인증 흐름에 대한 이해를 높임

추후 개발 계획

  • 실시간 채팅 메시지 송수신 기능 구현
  • JWT 기반 인증 방식 도입하여 무상태 Stateless 구조 실험
  • Redis Pub/Sub 연동 → 서버 다중화 대응
  • Swagger 또는 REST Docs 기반 자동 API 명세화

이 문서는 2025년 4월 기준 CloudTalk 프로젝트 V1 개발 현황을 기준으로 작성되었습니다.

0. 도입 : HTTP 에서의 양방향 통신의 한계

먼저 우리는 양방향 통신에 대해서 고민해보아야 합니다. http는 전형적인 단방향 통신을 거치는 프로토콜입니다. 통신이 필요하다면, 클라이언트가 요청하고 서버가 응답해야 합니다.

 

만약 http 에서 양방향 통신이 필요하다면, 다음과 같은 방식들을 사용해야합니다.

 

Polling : 클라이언트가 주기적으로 서버에 요청을 보내 응답을 기다리는 방식

Long Polling : 요청을 보내고, 서버에서 데이터가 생길 때까지 기다린 후 응답을 보내는 방식

Streaming : 응답 연결을 확인받지 않고 계속해서 데이터를 전송만 하는 방식. (UDP와 유사한 비신뢰적 느낌)

 

하지만 이러한 방식들은 한계가 있습니다.

누구와 어떤 메시지를 어떻게 실시간으로 통신할 것인가? 에 대한 체계가 잘 잡혀있지않죠.

 

그래서 나온 것이 웹소켓입니다.

 

1. WebSocket(웹 소켓)의 등장

WebSocket은 HTTP 위에서 동작하면서 양방향 통신을 가능하게 하는 프로토콜입니다. 처음 한 번의 핸드셰이크(handshake)를 통해 연결을 맺고 나면, 서버와 클라이언트는 끊김 없는 상태에서 자유롭게 메시지를 주고받을 수 있습니다. 즉, 웹 소켓을 사용하면 보내는 이와 메시지를 담아서 실시간으로 통신이 가능하죠. 이로 인해 실시간성이 필요한 서비스(채팅, 게임 등)에서 WebSocket은 필수 기술입니다.

 

그러나 WebSocke 자체도 완전한 해결책이 될 수는 없습니다.

 

메시지 전송 방식, 수신자 식별, 채널 분리, 메시지 라우팅 등에 대한 구체적인 규칙(컨벤션)이 부족합니다.

 

구체적인 규칙이 부족하면 발생하는 문제는 무엇일까요?

누가 누구에게 보낸건지 알 수 없습니다.

어떤 메시지 형식으로 데이터가 전송된 것인지 파악하고 파싱하기 어렵습니다.

 

비유하자면, 랜덤 채팅이라고 생각해봅시다. 어떤 언어를, 누가, 어떤 방식으로 보내올지 알수 없습니다. 그러나 우리는 교내/사내 혹은 친구들끼리만, 같은 언어로, 예상 가능한 형식의 메시지를 전송받고 싶은 겁니다. 누군가의 메시지에는 날짜가 포함되어 있고, 누군가의 메시지에는 채팅 제목이 없고, 식의 알수없는 프로토콜은 안됩니다.

 

2. 그래서 등장한 STOMP

STOMP(Simple Text Oriented Messaging Protocol)는 WebSocket 위에서 동작하는 경량 메시지 전송 프로토콜로, 구조화된 메시지 송수신 체계를 제공합니다. STOMP를 사용하면 다음과 같은 이점을 얻을 수 있습니다.

 

1. Topic(채널) 기반의 Pub/Sub

메시지는 destination 경로에 따라 전달되고, 해당 토픽(채널)을 구독(Sub: subscribe)한 클라이언트만 메시지를 수신할 수 있습니다! 즉 `누구와` 혹은 `누구의` 메시지를 주고받을지 지정할 수 있습니다.

 

2. 명령이 구조화되어 있다!

SEND, SUBSCRIBE, UNSUBSCRIBE, CONNECT, DISCONNECT 등의 명령이 존재해, 메시지 송수신 흐름이 명확하게 컨트롤됩니다.

 

3. url 경로를 통한 라우팅

예시를 들자면, /app/chat.sendMessage, /chat/public 등으로, 특정 채팅방, 시스템 메시지, 알림 등을 분류하기 쉽습니다. 즉, 목적별로 메시지를 분류할 수 있게 된겁니다!

 

4. 외부 라이브러리 연동 가능

보안(Spring Security) 적용이나 외부 메시지 브로커(Redis, RabbitMQ) 연동에도 적합합니다~

 

 

정리

WebSocket은 실시간 통신을 가능하게 하지만, 누가 어떤 메시지를 어떻게 주고받는지에 대한 구조가 부족합니다. 그래서 STOMP를 도입하면 topic 기반으로 메시지를 구분하고, 브로커를 통해 메시지를 전달해줄 수 있어 확장성과 메시지 라우팅이 수월해집니다.

 

특히 Spring에서 STOMP 구조를 사용하면 REST API처럼 각 메시지의 흐름을 URL 기반으로 정의하고, 컨트롤러 메서드로 처리할 수 있어 구조적입니다. 메시지 브로커를 통해 구독자에게 실시간 메시지를 푸시하는 방식은 REST의 GET 요청을 이벤트 기반으로 대체한 개념이라 볼 수 있고, REST의 경량 실시간 버전이라 할 수 있을 정도로, 구조, 처리 방식, 라우팅이 매우 유사하고 친숙합니다.

 

 

참고 자료

실시간 메시징 처리 서비스

HTTP 는 단방향 통신이죠. 우리가 보여지는 웹 페이지 말이에요.

사용자들은 일방적으로 미리 제공되는 페이지를 보게되고, 서버는 사용자의 반응에 따라서 응답을 할 때도 일방적으로 데이터를 보내게 돼요. 이런식으로 일방향을 왔다갔다 해도 뭐, 통신은 가능하긴 하죠!

 

그런데 메시지 처리는 속도, 그리고 동시성이 가장 중요한 포인트입니다.

 

우리는 양방향 처리가 가능한 메시징 서비스를 만드려고 해요

 

웹 소켓(Web Socket)과 Redis

HTTP 가 단방향 통신인 데에 반해, 웹 소켓은 양방향 통신이랍니다.

한 번 연결을 맺으면, 연결이 끊이지 않고 데이터를 실시간으로 주고받을 수 있죠.

 

이것이 바로 단방향 통신과의 차이점입니다.

단방향으로 통신하면 매 순간 연결을 새로 만들어야 하지요.

 

웹소켓의 STOMP 프로토콜 (Simple Text Oriented Messaging Protocol)

스트리밍 텍스트 지향 프로토콜이랍니다~ 그래서 보통 jackson-databind 라이브러리도 함께 추가해서 JSON 형식으로 데이터를 처리하곤 해요.

 

먼저 엔드포인트를 설정해서, 특정 엔드포인트에 여러 topic 을 등록할 수 있는데, 이때 topic 은 Spring 내부에서 path url 로 구분됩니다. 예를들어 /topic/chat 의 path 와 /user/online 의 path 는 다른 토픽으로 구분됩니다.

 

이렇게 path 를 구분해서 메시지를 전달하는 역할을 하는건 `메시지 브로커` 입니다. 클라이언트가 보낸 메시지를 라우팅하고, 구독한 클라이언트에게 메시지를 전달할 수 있어요.

 

네! 동일한 엔드포인트에서, 토픽 별로, 실시간 양방향으로 통신할 수 있게 해주는 프로토콜이랍니다!!!

 

 

 

그렇다면 양방향 통신에 Redis 가 필요한 이유는 뭘까요?

Redis 는 인메모리 데이터베이스(In-Memory Database)입니다.

그래요. 데이터를 디스크가 아닌 메모리에 저장한다는 의미지요.

그렇기 때문에 초고속 성능을 자랑하게 됩니다.

어머, 속도는 실시간 처리에서 필수적인 특성아닌가요?

 

네? 그러면 일반적인 RDBMS, 전통적인 관계형 데이터베이스냐고요?

아니요. Redis 는 NoSQL(Not Only SQL) 데이터베이스입니다.

데이터를 키(Key)-값(Value) 형태로 저장하지요. 그래서 스키마나 테이블로 고정된 형태로 저장되어 있다기보다는, 특정 자료구조(List, Set, Hash) 등의 형태로 저장합니다. 그래요. 테이블이나 데이터 간의 관계가 없어요!

 

유연하고 자유로운 데이터 관리가 가능하겠군요?

그럼으로 인해, NoSQL은 빠른 읽기/쓰기 성능단순한 데이터 모델을 지향했다는 것을 알 수 있습니다.

 

 

그러니까,

  • User(회원) 데이터나 친구 관계 데이터 등의 영구적 데이터 저장이 필요하면 RDBMS를 추가로 도입하여, 서비스 로직을 구성할 수 있을 것이다.
  • 그러나 지금 실시간으로, 채팅방에 누가 들어왔는지, 지금 온라인 상태인지 오프라인 상태인지, 메시지를 실시간으로 전송하고 수신받는 상황, 그러니까 상당히 일시적이고 휘발적이지만 신속함이 요구되는 데이터들은 Redis 로 처리하면 좋을 것이다.

 

 

Redis Client : Lettuce

네? 상추요?

 

Lettuce는 영어로 상추라는 의미긴 하지만, Redis 와 통신을 편리하게 추상화해놓은 도구입니다. RedisTemplate 같은 도구를 사용해서 손쉽게 Redis 를 구현할 수 있는 구현체 라이브러리라고 할 수 있지요~

 

Redis 와 애플리케이션(Spring) 간에 네트워크 통신을 추상화 해준다고 볼 수 있습니다. Spring Data Redis에서 RedisTemplate이나 Repository 방식으로 데이터를 다루게 해준다는 의미에요.

 

일반적으로 spring-data-redis 는 lettuce-core 가 만들어놓은 추상화 라이브러리 중, RedisTemplate을 Config(설정) 파일에서 Bean으로 등록해서 사용합니다. 그래요, Redis DB는 보통 따로 데이터 접근기술(JPA, MyBatis 등)을 두지 않는답니다! 바로 서비스 로직에 직접 호출해 사용합니다.

 

 

오.. 그러면 서비스 서버의 전원을 꺼버리면 Redis 데이터가 다 날아가겠네요?

오.. 네 아무래도........

.

.

.

일줄 알았겠지만, Redis 는 Persistence 설정을 하면 데이터를 디스크에 저장할 수도 있답니다. RDB 라는 Redis DataBase 를 이용해서 특정 시점의 메모리를 저장하여 스냅샷을 만들 수도 있고, AOF 라는 Append Only File 을 이용해서 모든 변경 사항을 로그로 기록할 수도 있어요! 데이터 백업도 가능하답니다?

 

오오 ~ 이건 나중에 더 알아봅시다

 

 

PUB / SUB, 그리고 TOPIC - Redis 의 구현 방식

Redis 는 메시지를 발행(Publish)하고 구독(Subscribe)하는 방식으로 동작합니다. 그리고 하나의 구독 채널은 Topic 이라고 불리죠.

 

어어..? pub/sub...... ? Java Design Pattern 에서 Observer 패턴이 생각난 당신! 백엔드 개발자시군요!

 

예.. 아무튼 신문을 구독하는 형식으로 Redis 는 구현됩니다. 이 설계 동작 방식에 대해서는 구현해보면서 차차 더 알아가보도록 해보자구요~ 왠지 메시징 큐가 무조건 쓰일거같은 느낌~

1. 자체 회원가입 API

API POST http://localhost:8080/api/auth/signup
요청 예시
{
  "email": "user@example.com",
  "password": "password123",
  "nickname": "John Doe"
}
응답 예시 회원가입 성공

 

자체 회원가입의 경우, 기존 Member 테이블에 비밀번호만 encode 하여 데이터를 저장했다.

 

2. 자체 로그인 API

API POST http://localhost:8080/api/auth/login
요청 예시
{
  "email": "user@example.com",
  "password": "password123"
}
응답 예시
{
    "grantType": "Bearer",
    "token": {accessToken 값}
}

 

이제 반환받은 accessToken 은 세션 스토리지에 저장 후 관리해야 할 것이다.

이 accessToken 값은 로그인 시마다 값이 바뀌게 된다.

 

3. AccessToken 에 대한 Member 값 요청 API

API POST http://localhost:8080/api/member/getInfo
요청 예시 반드시 요청하는 HTTP Request에 Authorizaion = {AccessToken} 이 배정되어 있어야함!!!
응답 예시 Member 객체

 

예시

 

다른 모든 요청에도 기본적으로

HTTP Request 에 Authorization = {AccessToken} 을 깔고가야 한다.

안그러면 인증/인가가 안된다.

 

 

4. Google Login API

API GET http://localhost:8080/login
응답 예시
{
  "grantType": "Bearer",
  "token": {accessToken 값}
}
Google 로그인 IdP 로 리다이렉트하여 Google 로그인 진행 후, 반환한다.

 

 

5. Logout API

API GET http://localhost:8080/api/auth/logout
응답 예시
{
  "email": "user@example.com"
}

 

 

6. AccessToken 재발급

API POST http://localhost:8080/api/auth/refresh-token
요청 예시
{
  "acessToken": {accessToken 값}
}
응답 성공 기존 acceeToken 이 만료 안됐으면 그대로 반환, 만료가 되었으면 재발급후 반환.
응답 실패 만약 refreshToken 까지 만료 되었다면, BadRequest 반환 => "REISSUE" 반환

 

 

https://github.com/Prom-o/BFromAt

 

https://jinn-o.tistory.com/298

 

[요구사항 및 API 설계] 회원가입 기능 v0

.. 어쩐지 이후에 요구사항이 더 추가되고 변경될 것같아 버전 정보도 함께 붙였다.최초 생성이니 v0 로 주었고, 이 심플한 구조에서 앞으로 많이 변경될 것이라 생각한다. 24.11.03첫번째 설계 //

jinn-o.tistory.com

 

24. 12. 29

요구사항 및 주요 목표

 

  1. `Google 연동 로그인` 을 REST API 화 해야한다.
    - 우리의 서비스는 CSR 구조를 띄고 있으며, React 에서 요청을 쏘아서 회원가입을 하도록 유도해야 한다.
    - OAuth 2.0 규격에 맞는 Google API 를 우리 서비스화 해야 한다.

  2. 자체 회원가입 구현
    - 이메일과 비밀번호만으로 회원가입이 가능하도록 (닉네임은 랜덤 부여)
    - 기존 Google 로그인 사용자와 중복되지 않도록 관리해야 할 것이다.
  3. 통합 회원 관리
    - 동일 이메일(ID) 는 같은 사용자이다.
    - 로그인 방식을 구분하여 한 테이블에서 관리하도록 해야 한다.

 

 

// 자체 회원가입 API
POST /api/member/signup

// 자체 로그인 API
POST /api/member/login

// Google 연동 로그인 API
POST /api/member/google-login

 

Member 테이블 : 통합 회원 관리 테이블.

기존 설계에서 Member 정보와 소셜 정보를 분리해놓았었는데, 접근성과 효용성을 따져봤을 때 통합적으로 관리하는 것이 더욱 효율적으로 판단되었기 때문에 하나의 테이블에서 관리하기로 하였다.

 

그 중 가장 중요한 요소들은 아무래도,

email(PK), password 및 Token 관리 속성, auth-type (local or google), created_at 일 것이다.

 

서비스 로직에서 JWT 구현.

우리 서비스는 아직 작은 프로젝트이다. 현재 상황을 고려해서, Google 연동 로그인 방식과 비슷하게 JWT OAuth2.0 로직을 서비스에 구현하도록 한다.

 

 

 

1. 401 오류: invalid_client

 

  • 문제 인식: Client 를 인식하지 못하고 있는 듯 싶다.
  • 오류 세부정보를 참고해보자.

 

조사해본 결과,

flowName 은 현재 어떤 OAuth2.0 흐름을 타고 있는지 확인하기 위한 파라미터이다.

이때 GeneralOAuthFlow 라함은 가장 일반적인 Flow인 Authorization Code Flow 라는 의미겠지.

이는 서버 기반 어플리케이션에서 자주 사용되는 Flow 방식이다. 그리하여 우리 서비스에서도 적용했다.

흠 생각보다 도움이 되는 세부정보는 아니었나.

  • 해결 방안: 가장 먼저 떠오르는 것은 Client-ID 와 Secret-Key 의 유효성이었다. 올바른 값이 잘 전달 되었나?
  • 해결 성공: Client-ID 와 Secret-Key 가 제대로 지정이 안되어있었다. 제대로 지정해주니 성공.

기존에 지정했던 값. 오류가 날만 했다.

2. 400 오류: redirect_uri_mismatch

 

생각보다 오류가 깔끔하고 명확하게 나와 놀랐다.

 

  • 문제 인식: Redirection Url 을 지정안했다.

  • 해결

 

3. 해

0. 준비

구글 로그인 연동 - 아이디어 구상

https://jinn-o.tistory.com/323

 

[인증/인가] 구글 로그인 연동 - 아이디어 구상

구글 연동 로그인 ?우리가 여러 홈페이지에 로그인을 하려다보면, `간편 로그인` 서비스를 제공한다. kakao 로 로그인하기, Facebook 으로 로그인하기 ... 등등 그런데 이러한 연동 로그인에 있어서

jinn-o.tistory.com

 

1. Gradle 의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-security' // Spring Security
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' // OAuth 2.0 Client

 

Spring Security 는 OAuth2.0 규칙에 맞는 통신을 도와주는 라이브러리를 보유하고 있다. 그렇기 때문에 OAuth 2.0 통신을 위한 코드를 직접 구현하지 않아도 편리하게 안전한 구조를 생성할 수 있다.

 

2. Google Cloud Console 에서 Cloud-ID 와 Secret-Key 생성

Google 에서는 다양한 REST API 를 제공한다. 이 API 들을 이용해서 우리는 여러 정보들을 주고받을 수 있다. 이러한 REST API 를 관리하기 위한 도구가 바로 Cloud Console 이다.

 

0. Google Cloud Console 에 접속한다.

https://console.cloud.google.com/

 

Google 클라우드 플랫폼

로그인 Google 클라우드 플랫폼으로 이동

accounts.google.com

 

 

1. 우측 상단 버튼의 새 프로젝트로 프로젝트 생성 (나는 이미 FromAt 프로젝트를 생성해놓았다.)

 

2. 앱 생성 및 OAuth 동의 화면

이때, 사용자 유형은 `외부` 이다.

 

 

3. 범위 설정

 

4. 테스트 유저 설정

 

5. 배포

 

 

6. 사용자가 처음 로그인(구글을 통해 회원가입) 할 시에 일어나는 일 정리하고 다시 시작하기

 

 

7. application.yml 에 구글 연동 정보 작성하기

security:
  oauth2:
    client:
      registration:
        google:
          client-id: your-google-client-id
          client-secret: your-google-client-secret
          redirect-uri: "http://localhost:8080/login/oauth2/code/google"

 

왜 이것만 써도 되냐고요?

https://jinn-o.tistory.com/311

 

[OAuth2] 기본적인 로그인 연동은 이미 스프링에서 구현해놓았다?

글로벌 로그인 연동 Resource Server 에는 무엇이 있을까?바로 Google, Facebook 이다. 글로벌하게 자주 쓰이는 로그인 연동 방식이다. 자, 우리의 개발자들은 생각한다.뭐? 자주 쓰인다고 ??? 자동화해야

jinn-o.tistory.com

 

8. Spring Security 설정

@Slf4j
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

    @Value("${spring.security.oauth2.client.registration.google.client-id}")
    private String clientId;

    @Value("${spring.security.oauth2.client.registration.google.client-secret}")
    private String clientSecret;

    @Value("${spring.security.oauth2.client.registration.google.redirect-uri}")
    private String redirectUri;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeHttpRequests(auth -> auth
                            .requestMatchers("/", "/login").permitAll()
                            .anyRequest().authenticated() // 이 외의 모든 요청은 제한
                )
                .oauth2Login(Customizer.withDefaults())

                .build();
    }

    @Bean
    public ClientRegistrationRepository clientRegistrationRepository() {
        return new InMemoryClientRegistrationRepository(googleClientRegistration());
    }

    private ClientRegistration googleClientRegistration() {
        return CommonOAuth2Provider.GOOGLE
                .getBuilder("google")
                .clientId(clientId)
                .clientSecret(clientSecret)
                .redirectUri(redirectUri)
                .build();
    }


}

 

 

 

9. 이제 모든 준비가 완료 되었다. 실행시켜보자.

연동 로그인 ?

우리가 여러 홈페이지에 로그인을 하려다보면, `간편 로그인` 서비스를 제공한다. kakao 로 로그인하기, Facebook 으로 로그인하기 ... 등등 그런데 이러한 연동 로그인에 있어서 단연 전세계 1위로 가장 많이 사용되는 연동 로그인 제공자가 있다. 바로 구글이다.

 

구글은 단연 전 세계에 정말 많은 사용자를 보유하고 있을 것이며, Gmail 주소가 없는 사람을 찾아보기 힘들다. 이렇듯 대부분의 인터넷 사용자들은 구글 계정을 갖지 않기가 힘들 정도다.

 

이렇게 사용자가 많은 만큼 간편 연동 로그인을 구현하면, 작은 서비스들은 사용자를 유치하는데 애를 덜 먹을 것이다. 또한 구글은 자신의 인증/인가 체계를 적용한 사용자 연동 로그인에 대해서 무료로 풀어놓았다. 그 이유는 사용자와 애플리케이션 간의 인증을 간소화하고 자신의 서비스를 더 널리 활용할 수 있는 데에 큰 이유가 있을 것이다. 또한 이렇게 사용량이 증가하면 할수록, 특정 수치를 넘어서면 유료화하여 유료 서비스 창출에도 도움이 될 것이다.

 

우리가 이것을 FromAt 서비스에 구현하려고 하는 이유

프롬앳 서비스는 먼저 사용자 유치가 제 1순위라고 생각했다. 그 첫번째 방식이 `로그인 간소화` 이다. 그러기 위해서 우리 팀은 구글을 활용하기로 하였다. 몇 번의 클릭만으로도 사용자를 인식할 수 있다면, 귀찮은 일이 사라져 사용자 유치에 좀더 힘이 될 것이다.

 

구글에서 제공하는 로그인 연동 기반 개념, OAuth 2.0 방식

토큰 값으로 사용자를 인증/인가하는 규칙으로써, `인가`에 좀 더 강점을 둔 방식이다.

사용자의 귀중한 정보(아이디, 비밀번호)의 직접적인 노출 없이, 주기적으로 재발급되는 토큰을 이용하여 사용자를 인증하고 인가하는 방식이다.

 

다음은 OAuth2.0 프로토콜에 대한 설명이다.

https://jinn-o.tistory.com/309

 

[OAuth2] accessToken에 이어서 왜 refreshToken 까지 필요할까?

OAuth2.0 이 뭐에요 ?Open Authorization의 약자로, 웹 애플리케이션이나 모바일 앱 등에서 인증(Authentication)과 인가(Authorization)를 처리하기 위한 표준 프로토콜이다. 표준이기 때문에, 사용자가 직접 자

jinn-o.tistory.com

 

 

간단 로드맵

구글 사용자 유치,

더 나아가 로그인을 자체적으로 `간소화`시키는 그 순간까지.

(예를 들어, 연동없이 로그인하더라도 아이디나 닉네임도 랜덤 지정)

또 비회원에 대한 로직도 고민해야 한다.

.. 어쩐지 이후에 요구사항이 더 추가되고 변경될 것같아 버전 정보도 함께 붙였다.

최초 생성이니 v0 로 주었고, 이 심플한 구조에서 앞으로 많이 변경될 것이라 생각한다.

 

24.11.03

첫번째 설계 

// 회원가입 API
POST /api/member/signup

 

회원가입시 입력받을 정보

Name Description Required
email PK, 이메일을 아이디화한다. 이메일을 아이디로 사용하면 비밀번호 찾기와 소셜 로그인 연동이 용이하고, 고유한 식별자로 중복 계정을 방지할 수 있어 사용자 관리가 효율적이기에. O
authType 서비스에 직접 로그인한 사용자와 소셜 로그인 사용자를 구분하기 위한 컬럼 O
password 비밀번호 (영문/숫자/특수문자를 포함한 8~20자리) O
name 실명 X
phoneNumber 휴대전화번호 X
birthDate 생년월일 YYYY-MM-DD 형식 X
nickname 닉네임(별칭) X
address 주소. (추후에 중간에서 만나 -! 에서 사용되려나..?) X
gender 성별 X
profileImage 프로필 이미지 url X

 

소셜 로그인 사용자들에 대한 부가 정보들 (추후 테이블을 분리하여 저장할 컬럼들)

Name Description Requried
socialId 소셜 로그인 사용자가 제공한 사용자 고유 id O
provider 어떤 소셜로 로그인을 타고왔는지 구분. ex. kakao, facebook 등 O

 

Spring Security + JWT 관련 비밀키 정보들 (간단 설계, 추후 수정가능성 있음)

Name Description Requried
accessToken 액세스 토큰 O
refreshToken 리프레시 토큰 O
issuedAt 토큰 발급 시각 O
expiredAt 토큰 만료 시각 O
secretKey 비밀키 (암호화 필수) ?

 

+ Recent posts