반응형

배운것 

1. stream 연습~~

- stream은  util에서 퉁치면 안되고 따로 import 해줘야한다.

import java.util.*;
import java.util.stream.*;

 

- distinct() 사용

요소의 중복을 제거.

리턴 타입 : Stream, IntStream, LongStream, DoubleStream 

public class DistinctExample {
    public static void main(String[] args) {
        int[] nums = {1, 2, 3, 3}; // int[] 배열

        // int[]에서 중복을 제거하고 List<Integer>로 변환
        List<Integer> distinctList = Arrays.stream(nums) // int[]에서 IntStream으로 변환
            .distinct() // 중복 제거
            .boxed() // int를 Integer로 변환
            .collect(Collectors.toList()); // List<Integer>로 수집

        System.out.println(distinctList); // [1, 2, 3] 출력
    }
}

 

- filter() 사용

매개값으로 주어진 Predicate가 true를 리턴하는 요소만 필터링

List<String> nameList = reportList
                                .stream()
                                .filter(rep -> rep.startsWith(id+" "))
                                .map(repp -> repp.split(" ")[1])
                                .collect(Collectors.toList());

 

- count() 사용

요소 개수 반환 

리턴타입 : long

long cnt = nameList
                .stream()
                .filter(reportee -> reporteeCount.getOrDefault(reportee,0) >= k)
                .count();

 

 

2. startsWith(), endsWith()  사용해봄

써본적이 없는데 써봤다.

boolean startsWith(String str)
boolean endsWith(String str)

 

 

두번째 다른방법으로 풀기

다른사람 답안을 보면서 따라해봤다.

제일 오래걸린 시간 : 테스트3 (6000.64ms, 555MB)

뭐야 더오래걸림

report 길이가  1 ~ 200,000 인데 아무리 중복을 제거했어도 두번돌아서 그런가

import java.util.*;
import java.util.stream.*;

class Solution {
    public int[] solution(String[] id_list, String[] report, int k) {
        //1. report에서 중복을 제거
        List<String> reportList = Arrays.stream(report)
            .distinct()
            .collect(Collectors.toList());
        
        //2. id별로 신고당한 횟수를 누적한다.
        HashMap<String,Integer> reporteeCount = new HashMap<>();
        for(String rep : reportList){
            String reportee = rep.split(" ")[1];
            int cnt = reporteeCount.getOrDefault(reportee,0);
            reporteeCount.put(reportee,cnt+1);
        }
        
        //3. id 돌면서 메일 횟수 카운트
        int[] answer = new int[id_list.length];
        answer = Arrays.stream(id_list)
            .mapToInt(id ->{ //id별 처리
                //사용자가 신고한 리스트
                List<String> nameList = reportList
                                .stream()
                                .filter(rep -> rep.startsWith(id+" ")) //비슷한 이름 방지
                                .map(repp -> repp.split(" ")[1])
                                .collect(Collectors.toList());
                
                //사용자가 신고한 리스트에서 k 가 넘는 요소의 개수 산출
                long cnt = nameList.stream()
                                    .filter(reportee -> reporteeCount.getOrDefault(reportee,0) >= k)
                                    .count();
                return (int)cnt;
                }).toArray();

        return answer;  
    }  
}

 

 

 

 

첫번째 풀었던 것

제일 오래걸린시간 : 테스트21 (311.44ms, 164MB)

import java.util.*;

class Solution {
    public int[] solution(String[] id_list, String[] report, int k) {
        HashMap<String,Set<String>> reporterList = new HashMap<>();
        for(String rep : report){
            String reporter = rep.split(" ")[0]; //신고한사람 muzi
            String reportee = rep.split(" ")[1]; //신고당한사람 frodo
            
            //ID당 유저가 신고한 ID들
            Set<String> temp = reporterList.getOrDefault(reporter,new HashSet<String>());
            temp.add(reportee);
            reporterList.put(reporter,temp);        
        }
        
        //신고당한 id별로 횟수 정리
        HashMap<String,Integer> reporteeCount = new HashMap<>();
        Collection<Set<String>> reportees = reporterList.values();
        //ArrayList<String> bannedIds = new ArrayList<>(); //정지된 id
        for(Set<String> list : reportees){
            for(String name : list){
                int cnt = reporteeCount.getOrDefault(name,0);
                reporteeCount.put(name,cnt+1);
                //if(cnt >= k) bannedIds.add(name);
            }
        }
        
        int[] answer = new int[id_list.length];
        //정지된 신고 리스트 id가 정지되었나 검사
        for(int i=0; i<id_list.length;i++){
            String id = id_list[i];
            Set<String> reportList = reporterList.getOrDefault(id,new HashSet<String>());
            int mailCnt = 0;
            //신고 리스트에 있는 아이디가 정지되었나 검사
            for(String reportee : reportList){
                if(reporteeCount.getOrDefault(reportee,0) >= k ){
                    mailCnt += 1;
                }
            }
            answer[i] = mailCnt;
        }
        
      
        return answer;
    }
    

    

}

//사용자 누적 신고 횟수 k번 이상시 
//동일 유저에 대한 신고 횟수 중복제거 어떻게
//신고자에게 메일

//1. report 돌면서 id별로 누구를 신고했는지 정리(중복x) HashMap<String,Set<>>;
    //예 "muzi" <frodo, apeach>
//2. HashMap 돌면서 id별 신고당한 누적횟수 정리 HashMap, 3회 이상인경우 arraylist에 추가
    //예 "muzi", 3   ["muzi","frodo"]
//3. id list 돌면서 신고list꺼내서 배열에 있는지 확인하여 메일 횟수 ++

 

 

 

반응형
반응형

조건에 맞는걸 중간부터 찾으려고 했음 -> 넘 어려움

배열이 주어지면 배열을 앞에나 뒤부터 순회하는게 젤 나은듯

import java.util.*;

class Solution {
    public int solution(int[] citations) {
        int h = citations.length;
        Arrays.sort(citations);
        
        /*배열 순회하기
        for(int c : citations){
            if(c >= h){
                return h;
            }else{
                h -= 1;
            }
        }
        return h;
        */
        
        //길이기준 순회
        int index = 0;
        for(int i=h;i>0;i--){
            if(citations[index] >= i){
                return i;
            }else{
                index +=1;
            }
        }
        return 0;

    }
}
//,0,0]
//h=3 index=0;
//h=2 index 1;
//h=1

//길이 7 
//[1,2,3,5,6,7,8]
//뒤에부터 
//h = 7 일때 index[0] 1 의 값이 7이상이냐? 
//h = 6 일때 index[1] 2 의 값이 6이상이냐?
//h = 5 일때 index[2] 3 의 값이 5 이상이냐?
//h= 4 일때 index[3] 5의 값이 4 이상이냐? -> h반환

//index[0] 일때 h=len index[]의 값이 h 이상이냐?
//index[1]일때 h= len-1

//배열 -> 배열기준순회
반응형
반응형

개선할점

- 반복문 쓸 때 길이조건 변수 헷갈리지 말고 제대로 좀 넣어야한다.

- char 타입데이터에 int 더하면 int 나오니까 (char) 형변환 

class Solution {
    public String solution(String s, String skip, int index) {
        StringBuilder result = new StringBuilder();
        for(int i=0;i<s.length();i++){
           result.append(getAlphabet(s.charAt(i),skip,index));
        }
        return result.toString();
    }
    
    //index만큼 뒤에 있는 알파벳 찾기. skip에 있는거 빼고.
    public String getAlphabet(char c, String skip, int index){ 
        int count = 0 ;
        char standardChar = c; //standardChar = 'x' , count 4 
        while(true){
            char nextChar = standardChar == 'z' ?  'a' : (char)(standardChar + 1);//y
            standardChar = nextChar; //y
            //skip에 없으면
            if(skip.indexOf(Character.toString(nextChar)) == -1){
                count += 1;
            }
            
            if(count == index){
                break;
            }
        }
        
        return Character.toString(standardChar);
    }
}



//charAt(i) 문자 +1반복.. count index만큼 채울때까지..
//기준문자는 계속 바꾼다 
//문자가 z라면.. a로바꾸고..
반응형
반응형

개선할점

HashMap 초기화 하지 않고 put 메서드 호출하려 하면 nullPointException 나므로 조심.

메서드 나누니까 편하긴 하다

import java.util.*;
class Solution {
    String mainHand;
    int[] leftPosition = {1,4};
    int[] rightPosition = {3,4};
    HashMap<Integer,int[]> phone = new HashMap<>();
    
    public String solution(int[] numbers, String hand) {
        mainHand = hand.equals("right")? "R" : "L";
        phone.put(1,new int[]{1,1});
        phone.put(2,new int[]{2,1});
        phone.put(3,new int[]{3,1});
        phone.put(4,new int[]{1,2});
        phone.put(5,new int[]{2,2});
        phone.put(6,new int[]{3,2});
        phone.put(7,new int[]{1,3});
        phone.put(8,new int[]{2,3});
        phone.put(9,new int[]{3,3});
        phone.put(0,new int[]{2,4});
        
        StringBuilder answer = new StringBuilder();
        for(int i=0;i<numbers.length;i++){
            answer.append(getHand(numbers[i]));
        }
       
        return answer.toString();
    }
    
    
    //어느 손으로 누를지 계산 -> 현재위치 이동하기
    public String getHand(int num){
        String leftOrRight = switch(num){
                case 1,4,7 -> "L";
                case 3,6,9 -> "R";
                case 2,5,8,0 -> whenMiddle(num);
                default -> mainHand;
        };
        //손 이동하기 
        makePosition(num,leftOrRight);
        
        return leftOrRight;
    }
        
    //가운데일때 어느손으로 누를지 계산
    public String whenMiddle(int num){
        int[] numPosition = phone.get(num);
        int leftHandDistance = Math.abs(leftPosition[0]-numPosition[0])
                            + Math.abs(leftPosition[1]-numPosition[1]);
        int rightHandDistance = Math.abs(rightPosition[0]-numPosition[0])
                            + Math.abs(rightPosition[1]-numPosition[1]);
    
        if(leftHandDistance == rightHandDistance){
            return mainHand;
        }else if(leftHandDistance  < rightHandDistance){
            return "L";
        }else{
            return "R";
        }
    }
    
    //현재위치 이동하기    //누를 숫자   //누를 손
    public void makePosition(int num, String whatHand){
        if(whatHand.equals("L")){
            leftPosition = phone.get(num);
        }else{
            rightPosition = phone.get(num);
        }
    }
}

//현재위치 [1,3],[2,4]이렇게
//현재위치 lefthand=4   1->1, 4->2, 7->3, *->4
//현재위치 righthand=4  3->1, 6->2, 9->3, #->4
//숫자가 1,4,7이면 -> L 
//숫자가 3,6,9이면 -> R 
//숫자가 2,5,8,0이면 2->1, 5->2, 8->3, 0->4 로 해서 가까운걸로 같으면 hand 참고
//가운데 누르고 난뒤
반응형
반응형

틀린 이유

1. substring(start, end) 인덱스 헷갈림

//i부터 맨뒤까지 모두 자르기
str.substring(i);
str.substring(i,str.length);

//end보다 하나 전까지 가져옴

2. 오류파악 String index out of range: 0 

-> index0 도 없다는건 길이가 0이라는것. substring 결과의 길이가 0일때의 케이스 고려를 안 함

반응형
반응형

배열 정렬

Arrays.sort();

배열 비교

Arrays.mismatch();

import java.util.*;

class Solution {
    public String solution(String[] participant, String[] completion) {
        Arrays.sort(participant);
        Arrays.sort(completion);
        int mismatchIndex = Arrays.mismatch(participant,completion);
        return participant[mismatchIndex];
        
    }
}

 

다른방법

맵의 key-value에 접근하는법

Set<Map.Entry<String, Integer>> entrySet = map.entrySet();

 

맵의 모든 key 가져오기

 Set<String> keys = map.keySet();

 

맵의 모든 value 가져오기

Collection<String> values = map.values();

 

 

import java.util.*;

class Solution {
    public String solution(String[] participant, String[] completion) {
        HashMap<String,Integer> particimap = new HashMap<>();
        for(String name : participant){
            int cnt = particimap.getOrDefault(name,0);
            particimap.put(name,++cnt);
        }
        
        for(String name : completion){
            int cnt = particimap.get(name);
            particimap.put(name,--cnt);
        }
        
        return particimap.entrySet()
            .stream()
            .filter(entry -> entry.getValue() >0)
            .map(Map.Entry::getKey)
            .findFirst()
            .orElse("없음");
    }
}

 

 

반응형

'코딩 관련 > 코딩문제풀기' 카테고리의 다른 글

[프로그래머스] 키패드 누르기  (0) 2024.10.09
[프로그래머스] 문자열 나누기  (0) 2024.10.06
[프로그래머스] 숫자 짝꿍  (0) 2024.10.04
[JAVA] 9012번 괄호  (0) 2023.03.13
[JAVA] 너의 평점은  (0) 2023.03.12
반응형

개선사항

1. 두 정수 비교시 for문 보다는 Math.min() 사용하기

2. String 에서 문자 하나 추출시 substring(i,i-1) 보다는 charAt() 사용하기

3. char 을 정수형으로 변환

//1.'0'빼기. '0'의 ASCII값 : 48
int text1 = '1';
int num1 = text1 - '0';

//2. Character.getNumericValue() 
int text2 = '1';
int num2 = Character.getNumericValue(text2);
반응형

'코딩 관련 > 코딩문제풀기' 카테고리의 다른 글

[프로그래머스] 문자열 나누기  (0) 2024.10.06
[프로그래머스] 완주하지 못한 선수  (0) 2024.10.04
[JAVA] 9012번 괄호  (0) 2023.03.13
[JAVA] 너의 평점은  (0) 2023.03.12
[JAVA] 그룹 단어 체커  (0) 2023.03.12
반응형

스프링부트에서 STOMP를 이용한 메세지 pub/sub 예시

build.gradle에서 라이브러리를 추가해주십쇼 

implementation 'org.springframework.boot:spring-boot-starter-websocket'

 

웹소켓 설정파일 WebsocketConfig.java 을 아래와 같은 내용으로 생성

import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import lombok.RequiredArgsConstructor;

@Configuration
@EnableWebSocketMessageBroker
@RequiredArgsConstructor
public class WebsocketConfig implements WebSocketMessageBrokerConfigurer {

@Override
    public void configureMessageBroker(MessageBrokerRegistry registry){
        // /pub으로 시작하는 요청은 @Controller의 @MessageMapping 메소드로 라우트됨
        // 클라이언트가 서버로 메세지를 보낼 때 붙여야하는 prefix
        registry.setApplicationDestinationPrefixes("/pub"); 

         //해당 문자열로 시작하는 message 주소값을 받아서 처리하는 Broker를 활성화한다.
        registry.enableSimpleBroker("/sub"); //메세지 구독 주소값
    }

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry){
        registry.addEndpoint("/websocket")  //클라이언트가 연결할 url
        .setAllowedOriginPatterns("*")
        .withSockJS(); 
        //withSockJS : 웹소켓을 지원하지 않는 환경에서도 웹소켓 사용을 가능하게 해주는 옵션
        //이 옵션을 달면 클라이언트에서 SockJS 라이브러리를 사용해야 함
        //http 아니면 https 로 연결가능(sockJS가 http를 ws로 변환해줌)
    }
}

 

서버에서 STOMP 메세지 보내기

- convertAndSend 메소드를 호출하면 클라이언트가 구독하고 있는 주소로 메세지가 발행됨.

- /sub/** 주소를 구독하고 있는 클라이언트에게 메세지를 전달하는 예시 

import java.util.HashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Component;
import lombok.RequiredArgsConstructor;


@Component
@RequiredArgsConstructor
public class sendService{
    private final SimpMessagingTemplate messagingTemplate;

    public void sendMessageToSubOrderTopic(String message,String id) {
        messagingTemplate.convertAndSend("/sub/"+id, message);
    }
}

 

 

 

서버에서 STOMP 메세지 받아서 전달하기

- 서버가 메세지 브로커 역할 수행

- 특정 클라이언트가 /sub/test 주소로 메세지를 보내면 @MessageMapping에서 받아 @SendTo에 설정된 쪽으로 전달

- /sub/** 을 구독하는 클라이언트가 메세지를 받아볼 수 있는 예시

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.DestinationVariable;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Controller
public class StompMessageController {
/* 클라이언트에서 웹소켓을 연결한 뒤 /pub/test 로 메세지를 보내면
 여기서 받아 SendTo에 명시된 구독 링크로 메세지를 발행
 prefix는 생략
*/
    @MessageMapping("/test")
    @SendTo("/sub")
    public String processMessage(String message){
        System.out.println("message : " + message);
        return message;

    }

    //STOMP를 쓰면 좋은점
   	/* @Controller 적용된 객체를 이용해 조직적으로 관리할 수 있다.
    	STOMP의 Destination 경로를 기반으로 Spring Security를 적용할 수 있다
    	외부 브로커를 이용해 여러 서버를 관리할 수 있다.
    */


}

 


그럼 메세지를 구독하고 보내는 클라이언트 페이지를 띄워 테스트를 해보자..

검색해보면 전부 다 APIC으로 테스트를 하던데 APIC자체를 못찾아서 javascript로 짜서 했음. 

html 페이지에 javascript 소스를 넣는다.

 

메세지 구독하는 클라이언트 페이지 소스 예시 (javascript)

서버에 withSockJS() 설정이 되어있다면 SockJS 를 이용하여 연결해야 하며 이 경우엔 http나 https 로 접속을 해야함.

서버에서 /sub 주소로 구독하고 있는 클라이언트에 메세지를 전달해주므로 

/sub/aaa이나  /sub/bbb 를 구독하는 쪽은 메세지를 다 받을 수 있음. 

<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.min.js" integrity="sha512-1QvjE7BtotQjkq8PxLeF6P46gEpBRXuskzIVgjFpekzFVF4yjRgrQvTG1MTOJ3yQgvTteKAcO7DSZI92+u/yZw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>

<script type="text/javascript">
var socket = new SockJS("http://localhost:8081/websocket");
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
    console.log("연결 완료");
    stompClient.subscribe("/sub/aaa",function(response){
        console.log(response.body);
  });	
});
</script>

 

메세지를 발행하는 클라이언트 페이지 소스 예시(javascript)

<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.min.js" integrity="sha512-1QvjE7BtotQjkq8PxLeF6P46gEpBRXuskzIVgjFpekzFVF4yjRgrQvTG1MTOJ3yQgvTteKAcO7DSZI92+u/yZw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>

<script type="text/javascript">
var socket = new SockJS("http://localhost:8081/websocket");
stompClient = Stomp.over(socket);
stompClient.connect({}, function (frame) {
    console.log("연결 완료");
    stompClient.send("/pub/test",{},"test메시지입니다.");
});
</script>

 

SockJS를 안 쓰고 싶다면

서버에서 해당 설정을 없애주고

클라이언트에서는 stomp 라이브러리로 바로 연결하면 댐

<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.min.js" integrity="sha512-1QvjE7BtotQjkq8PxLeF6P46gEpBRXuskzIVgjFpekzFVF4yjRgrQvTG1MTOJ3yQgvTteKAcO7DSZI92+u/yZw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js"></script>
<script type="text/javascript">
var client = Stomp.client("ws://localhost:8081/websocket");
client.connect({},function(){
    console.log("연결 완료");
    client.subscribe("/sub/aaa",function(message){
        console.log("받은 메세지 : " + mesasge);
    });
})
	
</script>
반응형
반응형

1. websocket 연결설정

import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;

import lombok.RequiredArgsConstructor;

@Configuration
@EnableWebSocket
@RequiredArgsConstructor
public class WebSocketConfig implements WebSocketConfigurer{

    //밑에서 만들 WebSocketHandler 클래스
    private final WebSocketHandler webSocketHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry){
        registry.addHandler(webSocketHandler,"/websocket").setAllowedOrigins("*");
        // /websocket : 연결url
        //setAllowedOrigins : 웹소켓 cors정책으로, 허용 도메인 지정
        
    }

}

 

2. websocket 핸들러 생성. web socket 연결 및 종료의 수행에 대한 내용

import java.util.concurrent.ConcurrentHashMap;

import org.springframework.stereotype.Component;
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

@Component
//TextWebSocketHandler 상속 시 3개의 메소드 오버라이딩
public class WebSocketHandler extends TextWebSocketHandler{
    
    //ConcurrentHashMap : 멀티 스레드 환경에서 사용. entry 아이템별로 락을 건다.
    private static final ConcurrentHashMap<String, WebSocketSession> CLIENTS = new ConcurrentHashMap<String, WebSocketSession>();
    
    public void afterConnectionEstablished(WebSocketSession session)throws Exception{
        CLIENTS.put(session.getId(), session);
        System.out.println("session Id(" + session.getId() + ") 연결");
        //출력예시 : session Id(84693265-e147-0b2c-5505-b2d3c62e62b4) 연결
    }

    @Override
    public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception{
        CLIENTS.remove(session.getId());
        System.out.println("session Id(" + session.getId() + ") 연결해제");
        //출력예시 : session Id(03017781-abf2-bc66-855e-f217bb99b275) 연결해제
    }

    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception{
        String id = session.getId();
        System.out.println(CLIENTS.toString());
        //출력예시 : {03017781-abf2-bc66-855e-f217bb99b275=StandardWebSocketSession[id=03017781-abf2-bc66-855e-f217bb99b275, uri=ws://localhost:8081/websocket]}
        CLIENTS.entrySet().forEach(arg ->{
            if(!arg.getKey().equals(id)){
                try{
                    arg.getValue().sendMessage(message);
                }catch(Exception e){
                    e.printStackTrace();
                }
            }
        });
    }
}

 

 

 

테스트 방법

1. chrome에서 제공하는 확장 프로그램 이용

https://chromewebstore.google.com/detail/websocket-test-client/fgponpodhbmadfljofbimhhlengambbn?pli=1

 

2. postman에서 제공하는 웹소켓 연결 기능 이용

반응형
반응형

Spring boot 기반의 웹 어플리케이션에서 Amazon MSK 을 IAM 인증방식으로 연동하려고 함

MSK 클러스터 접속에 사용할 IAM 계정 : test-user ( MSK 관련 권한이 부여되어 있어야 함)

 

사용한 라이브러리

implementation 'org.springframework.kafka:spring-kafka:3.0.12'
implementation 'software.amazon.msk:aws-msk-iam-auth:2.0.3'

 

메세지 전송용 ProducerFactory 생성

KafkaAdmin 클라이언트는 생성하지 않았음
 * ProducerFactory : Kafka Producer 인스턴스를 생성하는 팩토리 빈
* Kafka Producer : Kafaka 브로커에 메시지를 전송하는 역할을 담당

@Bean
public ProducerFactory<String, String> producerFactory() {
     
        Map<String, Object> configProps = new HashMap<>();
        
        //연결할 kafka 브로커설정(MSK 서버)
        configProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaBootstrapServers);
        
        //보내는 메시지 타입 설정     
        configProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
        configProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);  

        //IAM인증 설정
        configProps.put(AdminClientConfig.SECURITY_PROTOCOL_CONFIG, "SASL_SSL");
        configProps.put(SaslConfigs.SASL_MECHANISM, "AWS_MSK_IAM");
        //awsProfileName 으로 계정명 명시
        configProps.put(SaslConfigs.SASL_JAAS_CONFIG,"software.amazon.msk.auth.iam.IAMLoginModule required awsProfileName=\"test-user\";");
        configProps.put(SaslConfigs.SASL_CLIENT_CALLBACK_HANDLER_CLASS, "software.amazon.msk.auth.iam.IAMClientCallbackHandler");
                
        return new DefaultKafkaProducerFactory<>(configProps);
}

//위에서 생성한 ProducerFactory로 KafkaTemplate 생성
@Bean
 public KafkaTemplate<String, String> kafkaTemplate() {
        return new KafkaTemplate<>(producerFactory());
}

토픽 생성은 하지 않았음. 특정 토픽으로 메세지를 보낼 때 해당토픽이 존재하지 않으면 자동으로 토픽이 생성됨
auto.create.topics.enable 설정이 디폴트로 true 값을 가짐 

 

메세지 전송용 Service 생성

@Service
public class KafkaProduceService {
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;

	//이 메소드를 호출해서 메세지를 보내면 됨
    public void sendMessage(String topic, String message) {
        kafkaTemplate.send(topic, message);
    }  

}

 

 

 

반응형

 

 

메세지 수신용 ConsumerFactory 생성

 * ConsumerFactory : Kafka Consumer 인스턴스를 생성하기 위한 팩토리 빈

 @Bean
 public ConsumerFactory<String, String> consumerFactory() {

        Map<String, Object> configProps = new HashMap<>();
        //consumer Group Id 부여해주기
        configProps.put(ConsumerConfig.GROUP_ID_CONFIG, "testGroup1");
        
        //MSK 브로커 설정(MSK서버)
        configProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaBootstrapServers);

	//IAM 인증 
        configProps.put(AdminClientConfig.SECURITY_PROTOCOL_CONFIG, "SASL_SSL");
        configProps.put(SaslConfigs.SASL_MECHANISM, "AWS_MSK_IAM");
        configProps.put(SaslConfigs.SASL_JAAS_CONFIG,"software.amazon.msk.auth.iam.IAMLoginModule required awsProfileName=\"test-user\";");
        configProps.put(SaslConfigs.SASL_CLIENT_CALLBACK_HANDLER_CLASS, "software.amazon.msk.auth.iam.IAMClientCallbackHandler");

        configProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        configProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class.getName());
        
        return new DefaultKafkaConsumerFactory<>(configProps);
 }

   
//kafka 메시지를 수신하는 리스너 컨테이너를 생성하는데 사용되는 인터페이스
@Bean
public ConcurrentKafkaListenerContainerFactory<String, String> kafkaListenerContainerFactory() {
        ConcurrentKafkaListenerContainerFactory<String, String> factory =
                new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(consumerFactory());
        return factory;
}

 

 

메세지 수신용 Service 생성

@Service
public class KafkaConsumeService {
    @Autowired
    private KafkaTemplate<String, String> kafkaTemplate;
    
    @KafkaListener(topics = "testTopic", groupId = "testGroup1")
    public void listen(ConsumerRecord<String, String> record) {
        System.out.println(record.value());
        System.out.println(record.toString());
    }

}

 


MSK 에 IAM 인증 시도시 오류 정리

1. IAM 계정의 access key와 secret key를 찾지 못하는 경우

com.amazonaws.SdkClientException: Unable to load AWS credentials from any provider in the chain: [software.amazon.msk.auth.iam.internals.EnhancedProfileCredentialsProvider@3bf917a2: Profile file contained no credentials for profile 'testUser': ProfileFile(profilesAndSectionsMap=[]), cohttp://m.amazonaws.auth.AWSCredentialsProviderChain@2dadd688: Unable to load AWS credentials from any provider in the chain: [EnvironmentVariableCredentialsProvider: Unable to load AWS credentials from environment variables (AWS_ACCESS_KEY_ID (or AWS_ACCESS_KEY) and AWS_SECRET_KEY (or AWS_SECRET_ACCESS_KEY)), SystemPropertiesCredentialsProvider: Unable to load AWS credentials from Java system properties (aws.accessKeyId and aws.secretKey), WebIdentityTokenCredentialsProvider: You must specify a value for roleArn and roleSessionName, software.amazon.msk.auth.iam.internals.EnhancedProfileCredentialsProvider@68aac71d: Profile file contained no credentials for profile 'default': ProfileFile(profilesAndSectionsMap=[]), cohttp://m.amazonaws.auth.EC2ContainerCredentialsProviderWrapper@69883287: Failed to connect to service endpoint: ]]

시스템 환경변수에 AWS_ACCESS_KEY_ID(혹은 AWS_ACCESS_KEY ) 와 AWS_SECRET_KEY (혹은 AWS_SECRET_ACCESS_KEY) 이름으로 access key와 secret key를 세팅해준다. 

혹은 producerFactory와 consumerFactory 내에 System.setProperty로 키 세팅해주면 됨.

System.setProperty("aws.accessKeyId", awsAccessKey);
System.setProperty("aws.secretKey", awsSecretKey);

 

 

2. IAM인증 설정이 바르지 않을때 난 에러 

Node -1 disconnected.
2024-01-31 10:16:24,725 [INFO  ] NetworkClient.cancelInFlightRequests(NetworkClient.java:344) - [Producer clientId=producer-1] Cancelled in-flight API_VERSIONS request with correlation id 1 due to node -1 being disconnected (elapsed time since creation: 98ms, elapsed time since send: 98ms, request timeout: 30000ms)
2024-01-31 10:16:24,726 [WARN  ] NetworkClient$DefaultMetadataUpdater.handleServerDisconnect(NetworkClient.java:1105) - [Producer clientId=producer-1] Bootstrap broker sdfssdfdf.kafka.ap-northeast-2.amazonaws.com:9098 (id: -1 rack: null) disconnected

Factory 생성 시에 sasl_jaas_config 값에 IAM로그인모듈 설정과 IAM계정의 이름을 적어주자.

configs.put(SaslConfigs.SASL_JAAS_CONFIG,"software.amazon.msk.auth.iam.IAMLoginModule required awsProfileName=\"testUser\";");

 

반응형

+ Recent posts