프로젝트

자바프로젝트-5 클라이언트 시작! ChatService, userService

aerimi-code 2025. 12. 6. 15:03

 

ChatService

public class ChatService {
	private final NetworkService netwrokService; //네트워크 통신담당
	private final ObjectMapper objectMapper;//json 객체 변환기
	private Long currentUserId;//현재 사용자 id
	
	//이벤트 리스너 (Controller가 등록)
	private Consumer<ChatMessageResponse> onMessageReceived;
	
	public ChatService(NetworkService netwrokService) {
		this.netwrokService=netwrokService;
		this.objectMapper= new ObjectMapper();
		this.objectMapper.registerModule(new JavaTimeModule());
		
	}

 

📌 Consumer<ChatMessageResponse>란?

  • Java의 함수형 인터페이스 중 하나입니다.
  • Consumer<T>는 입력값을 받아서 처리하지만 반환값은 없는 함수를 의미해요.
  • 여기서는 Consumer<ChatMessageResponse> → ChatMessageResponse 객체를 받아서 처리하는 콜백.
  • 즉, 메시지가 도착했을 때 실행할 이벤트 리스너를 담는 변수입니다.

예시:

chatService.setOnMessageReceived(msg -> {
    System.out.println("새 메시지 도착: " + msg.getContent());
});
  • 메시지가 오면 onMessageReceived.accept(msg)가 호출되고,
  • 위 람다식이 실행되어 콘솔에 메시지를 출력
// 연결 및 구독 초기화
    public void initialize(Long userId, Consumer<ChatMessageResponse> messageListener) {
        this.currentUserId = userId;
        this.onMessageReceived = messageListener;
        
        String serverUrl = "ws://localhost:8080/ws";
        
        networkService.connect(
            serverUrl,
            () -> onConnected(null),  // 연결 성공 시
            this::onError       // 에러 발생 시
        );
    }

 

외부에서

chatService.initialize(123L, msg -> {
    System.out.println("메시지 도착: " + msg.getContent());
});

이렇게 이벤트를 등록하고, 

 

onMessageReceived.accept(msg);

이렇게 실행하면 저장해둔 람다식이 실행된다. 

 

📌 ObjectMapper란?

  • Jackson 라이브러리에서 제공하는 JSON 변환기입니다.
  • 역할:
    • Java 객체 → JSON 문자열 변환
    • JSON 문자열 → Java 객체 변환
  • new ObjectMapper()로 생성하고, registerModule(new JavaTimeModule())을 추가하면 LocalDateTime 같은 Java 8 날짜/시간 타입도 변환 가능해집니다.

📌 전체 흐름

  1. NetworkService → STOMP/WebSocket으로 서버와 연결
  2. 서버에서 메시지 도착 → ChatMessageResponse 객체로 변환
  3. onMessageReceived.accept(response) 호출 → Controller에서 등록한 콜백 실행
  4. ObjectMapper는 JSON ↔ 객체 변환을 담당해서 네트워크 메시지를 쉽게 다룸

 

NetworkService는 인터페이스

=> 즉, ChatService는 "네트워크 통신을 한다"는 사실만 알면 되고, 구체적으로 STOMP인지, 다른 방식인지 몰라도 된다는 게 핵심입니다.

 

NetworkService networkService = new StompNetworkService(stompClient);
ChatService chatService = new ChatService(networkService);

ChatService는 "네트워크 서비스"만 알면 되니까, 내부에서는 send(), subscribe() 같은 메서드만 호출합니다.

 

    // 입장 처리
    private void requestJoin() {
        JoinRequest request = new JoinRequest(currentUserId);
        networkService.send("/app/chat.join", request);
    }
    
    //구독, client 입장 버튼 클릭시 실행 될 코드
    public void joinChatRoom() {
        // 구독 먼저
        networkService.subscribe("/topic/public", this::handlePublicMessage);
        networkService.subscribe("/user/queue/whisper", this::handleWhisperMessage);
        networkService.subscribe("/user/queue/history", this::handleHistoryMessage);

        // 그 다음 입장 요청
        requestJoin();
    }
    
 // 전체 채팅 전송
    public void sendPublicMessage(String content) {
        ChatSendRequest request = new ChatSendRequest(currentUserId, content);
        networkService.send("/app/chat.send", request);
    }
    
    // 귓속말 전송
    public void sendWhisper(Long recipientId, String content) {
        WhisperRequest request = new WhisperRequest(currentUserId, recipientId, content);
        networkService.send("/app/chat.whisper", request);
    }

버튼을 누르면 구독 후, 입장 처리가 진행된다. 
구독 후, 채팅과 귓속말은 서버의 경로로 보내면 된다. 

 

 

 

서버에서 받은 메시지는 JSON이다. 따라서 이를 파싱해서 컨트롤러로 전달해야한다. 

 

📌 1. handlePublicMessage(String json)

// 메시지 핸들러들 (JSON 파싱 후 Controller에 전달)
    private void handlePublicMessage(String json) {
        try {
            ChatMessageResponse msg = objectMapper.readValue(json, ChatMessageResponse.class);
            if (onMessageReceived != null) {
                onMessageReceived.accept(msg);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

역할: 서버에서 받은 공개 채팅 메시지(JSON 문자열)를 ChatMessageResponse 객체로 변환.

objectMapper.readValue(json, ChatMessageResponse.class) → JSON을 Java 객체로 변환.

변환된 객체(msg)를 onMessageReceived.accept(msg)로 전달 → 컨트롤러나 UI에서 메시지를 처리할 수 있게 함.

📌 2. handleWhisperMessage(String json)

private void handleWhisperMessage(String json) {
    // 동일 로직
    handlePublicMessage(json);
}

 

📌 3. handleHistoryMessage(String json)

private void handleHistoryMessage(String json) {
    try {
        List<ChatMessageResponse> history = objectMapper.readValue(
            json, 
            new TypeReference<List<ChatMessageResponse>>() {}
        );
        history.forEach(msg -> {
            if (onMessageReceived != null) {
                onMessageReceived.accept(msg);
            }
        });
    } catch (Exception e) {
        e.printStackTrace();
    }
}

역할: 서버에서 받은 채팅 히스토리(JSON 배열) 처리.

변환된 리스트를 forEach로 돌면서 하나씩 onMessageReceived.accept(msg) 호출 → UI에 과거 메시지들을 순차적으로 전달.

 

 

UserService

public class UserStateService {
	private final Set<String> activeUsers=new HashSet<>();
	private Consumer<Set<String>> onUserListUpdated;
	
	 public void setUserListListener(Consumer<Set<String>> listener) {
	        this.onUserListUpdated = listener;
	    }
	 
	 public void updateActiveUsers(Set<String> users) {
		 this.activeUsers.clear();
		 this.activeUsers.addAll(users);
		 
		 if(onUserListUpdated!=null) {
			 onUserListUpdated.accept(new HashSet<>(activeUsers));
		 }
	 }
	 
	 public Set<String> getActiveUsers() {
	        return new HashSet<>(activeUsers);
	    }
	 
	   public boolean isUserActive(String username) {
	        return activeUsers.contains(username);
	    }
}

 

activeUsers: 현재 접속 중인 사용자들의 이름(ID)을 저장하는 집합(Set). 중복된 사용자 이름 제거를 위해 Set 사용

onUserListUpdated: 이벤트 리스너, 사용자 목록이 갱신될 때 실행할 함수를 저장

 

📌 메서드 설명

1. setUserListListener(Consumer<Set<String>> listener)

    • 외부(UI나 Controller)가 "사용자 목록이 갱신되면 이 코드를 실행해라" 하고 리스너를 등록하는 메서드.

2. updateActiveUsers(Set<String> users)

새로운 사용자 목록을 받아서 activeUsers를 갱신.

  • clear() → 기존 목록 비움
  • addAll(users) → 새로운 목록 추가
  • onUserListUpdated.accept(new HashSet<>(activeUsers)) 실행 → 이벤트 발생.
  • 즉, 사용자 목록이 바뀌면 등록된 리스너가 자동으로 호출됨.