Home [Spring] Apache Kafka + Spring Boot
Post
Cancel

[Spring] Apache Kafka + Spring Boot

Kafka

Structure

kafka

img (5)

img (4)

img (3)

img (2)

Record < Partition < Topic < Broker < Cluster

Topic
๋ฉ”์‹œ์ง€๋ฅผ ๊ตฌ๋ถ„ํ•˜๋Š” ๋…ผ๋ฆฌ์ ์ธ ๋‹จ์œ„
Partition
๋ชจ๋“  ํ† ํ”ฝ์€ ๊ฐ๊ฐ ๋Œ€์‘ํ•˜๋Š” ํ•˜๋‚˜ ์ด์ƒ์˜ ํŒŒํ‹ฐ์…˜์ด ๋ธŒ๋กœ์ปค์— ๊ตฌ์„ฑ๋˜๊ณ , ๋ฐœํ–‰๋˜๋Š” ํ† ํ”ฝ ๋ฉ”์‹œ์ง€๋“ค์€ ํŒŒํ‹ฐ์…˜๋“ค์— ๋‚˜๋‰˜์–ด ์ €์žฅ๋จ.
  • ํ•˜๋‚˜์˜ ํ† ํ”ฝ์— ๋Œ€ํ•˜์—ฌ ์—ฌ๋Ÿฌ ํŒŒํ‹ฐ์…˜์„ ๊ตฌ์„ฑํ•˜๋Š” ๊ฐ€์žฅ ํฐ ์ด์œ  : ๋ถ„์‚ฐ ์ฒ˜๋ฆฌ๋ฅผ ํ†ตํ•œ ์„ฑ๋Šฅ ํ–ฅ์ƒ
  • ํ•˜๋‚˜์˜ ํŒŒํ‹ฐ์…˜ ๋‚ด์—์„œ๋Š” ๋ฉ”์‹œ์ง€ ์ˆœ์„œ๊ฐ€ ๋ณด์žฅ
Broker
์นดํ”„์นด ๋ธŒ๋กœ์ปค๋Š” ํ”„๋กœ๋“€์„œ์™€ ์ปจ์Šˆ๋จธ ์‚ฌ์ด์—์„œ ๋ฉ”์‹œ์ง€๋ฅผ ์ค‘๊ณ„ ์นดํ”„์นด ๋ธŒ๋กœ์ปค๊ฐ€ ์ผ๋ฐ˜์ ์œผ๋กœ โ€˜์นดํ”„์นดโ€™๋ผ๊ณ  ๋ถˆ๋ฆฌ๋Š” ์‹œ์Šคํ…œ์ž„. ํ”„๋กœ๋“€์„œ์™€ ์ปจ์Šˆ๋จธ๋Š” ๋ณ„๋„์˜ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์œผ๋กœ ๊ตฌ์„ฑ๋˜๋Š” ๋ฐ˜๋ฉด, ๋ธŒ๋กœ์ปค๋Š” ์นดํ”„์นด ์ž์ฒด์ด๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค. ๋”ฐ๋ผ์„œ โ€˜์นดํ”„์นด๋ฅผ ๊ตฌ์„ฑํ•œ๋‹คโ€™ ํ˜น์€ โ€˜์นดํ”„์นด๋ฅผ ํ†ตํ•ด ๋ฉ”์‹œ์ง€๋ฅผ ์ „๋‹ฌํ•œ๋‹คโ€™์—์„œ ์นดํ”„์นด๋Š” ๋ธŒ๋กœ์ปค๋ฅผ ์˜๋ฏธ.
imgg
Producer, Consumer
Producer๋Š” ์˜ค์ง ๋์—๋งŒ ์“ฐ๋ฉฐ, Consumer๋Š” ์˜คํ”„์…‹์„ ๊ธฐ์ค€์œผ๋กœ ์ฐจ๋ก€์ฐจ๋ก€ ์ฝ์–ด๋‚˜๊ฐ.
Topics
a particular stream of data
  • you can have as many topics as you want
  • a topic is identified by its name
  • Topics are split in partitions
    • Each partition is ordered
    • Each message within a paritition gets an incremental id, called offset
  • ๋™์ผํ•œ ํ† ํ”ฝ์˜ ๋ฉ”์‹œ์ง€๋“ค์€ ๋…ผ๋ฆฌ์ ์œผ๋กœ ๊ฐ™์€ ๋ฌธ๋งฅ(context)์„ ๊ฐ€์ง‘
Message
message
Key(ํ‚ค)์™€ Value(๊ฐ’)๋กœ ๊ตฌ์„ฑ
  • ๋ธŒ๋กœ์ปค๋ฅผ ํ†ตํ•ด ๋ฉ”์‹œ์ง€๊ฐ€ ๋ฐœํ–‰๋˜๊ฑฐ๋‚˜ ์†Œ๋น„๋  ๋•Œ, ๋ฉ”์‹œ์ง€ ์ „์ฒด๊ฐ€ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”๋จ
  • ํŠน์ •ํ•œ ๊ตฌ์กฐ์ธ ์Šคํ‚ค๋งˆ(schema)๋ฅผ ๊ฐ€์ง > ํ”„๋กœ๋“€์„œ๊ฐ€ ๋ฐœํ–‰ํ•˜๊ณ  ์ปจ์Šˆ๋จธ๊ฐ€ ์†Œ๋น„ํ•  ๋•Œ ๋ฉ”์‹œ์ง€๋ฅผ ์ ์ ˆํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•ด ํ•„์š” (๋งŒ์•ฝ ํ”„๋กœ๋“€์„œ์™€ ์ปจ์Šˆ๋จธ๊ฐ€ ๋ฉ”์‹œ์ง€์— ๋Œ€ํ•œ ์„œ๋กœ ๋‹ค๋ฅธ ์Šคํ‚ค๋งˆ๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ๋‹ค๋ฉด, ์ •์ƒ์ ์ธ ์ฒ˜๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์—†์Œ)
Replica
์„œ๋น„์Šค ์•ˆ์ •์„ฑ๊ณผ ์žฅ์•  ์ˆ˜์šฉ(Fault-Tolerance)์— ๊ด€ํ•œ ์š”์†Œ
  • ํ•˜๋‚˜์˜ ํŒŒํ‹ฐ์…˜์€ 1๊ฐœ์˜ ๋ฆฌ๋” ๋ ˆํ”Œ๋ฆฌ์นด์™€ ๊ทธ ์™ธ 0๊ฐœ ์ด์ƒ์˜ ํŒ”๋กœ์–ด ๋ ˆํ”Œ๋ฆฌ์นด๋กœ ๊ตฌ์„ฑ๋จ. ๋ฆฌ๋” ๋ ˆํ”Œ๋ฆฌ์นด๋Š” ํŒŒํ‹ฐ์…˜์˜ ๋ชจ๋“  ์“ฐ๊ธฐ, ์ฝ๊ธฐ ์ž‘์—…์„ ๋‹ด๋‹น. ๋ฐ˜๋Œ€๋กœ ํŒ”๋กœ์–ด ๋ ˆํ”Œ๋ฆฌ์นด๋Š” ๋ฆฌ๋” ๋ ˆํ”Œ๋ฆฌ์นด๋กœ ์“ฐ์ธ ๋ฉ”์‹œ์ง€๋“ค์„ ๊ทธ๋Œ€๋กœ ๋ณต์ œํ•˜๊ณ , ๋งŒ์•ฝ ๋ฆฌ๋” ๋ ˆํ”Œ๋ฆฌ์นด์— ์žฅ์• ๊ฐ€ ๋ฐœ์ƒํ•˜๋Š” ๊ฒฝ์šฐ, ๋ฆฌ๋” ์ž๋ฆฌ๋ฅผ ์Šน๊ณ„๋ฐ›์„ ์ค€๋น„๋ฅผ ํ•จ. ์ฐธ๊ณ ๋กœ ์Šน๊ณ„๋ฐ›์„ ์ค€๋น„๊ฐ€ ๋œ ์ฆ‰, ๋ฆฌ๋” ๋ ˆํ”Œ๋ฆฌ์นด์˜ ๋ฉ”์‹œ์ง€๋ฅผ ์ ์ ˆํ•˜๊ฒŒ ๋ณต์ œํ•˜์—ฌ ๋ฆฌ๋” ๋ ˆํ”Œ๋ฆฌ์นด์™€ ๋™๊ธฐํ™”๋œ ๋ ˆํ”Œ๋ฆฌ์นด๋“ค์˜ ๊ทธ๋ฃน์„ ISR(In-Sync Replica)๋ผ๊ณ  ํ•จ.
  • Replication-factor
Offset
only have ameaning for a specific partition
  • Ex. offset3 in partition0 doesnโ€™t represent the same data as offset3 in partition1
  • Order is guaranteed only within a partition (not across paritions)

Data (Message / Record)


Features

  • good solution for large scale message processing applications
  • better throughput, built-in partitioning, replication, and fault-tolerance, horizontal scalabiltiy
    • messaging uses are often comparatively low-throughput
    • but may require low end-to-end latency and often depend on the strong durability guarantees (Kafka can provide)
    • kafka can scale to 100s of brokers
    • kafka can scale to milions of messages per second
  • high performance (latency of less than 10ms) - real time
  • for the log history
  • for decoupling of data streams & systems

Screen Shot 2022-08-01 at 3 07 56 PM Screen Shot 2022-08-02 at 10 20 35 AM Screen Shot 2022-08-02 at 10 20 51 AM Screen Shot 2022-08-02 at 9 58 37 AM


Use cases

  • messageing system
  • activity tracking
  • gather metrics from many different locations
  • application logs gathering
  • stream processing (with the kafka stream API or Spark for example)
  • De-coupling of system dependencies
  • Integration with Spark, Flink, Storm, Hadoop and many other Big Data technologies

  • key point : in real-time

Screen Shot 2022-08-02 at 9 57 48 AM


Kafka VS RabbitMQ

ย KafkaRabbitMQ
ย distributed event streaming platformopen source distributed message broker
ย pub/sub (์ƒ์‚ฐ์ž๊ฐ€ ์›ํ•˜๋Š” ๊ฐ ๋ฉ”์‹œ์ง€๋ฅผ ๊ฒŒ์‹œํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ๋ฉ”์‹œ์ง€ ๋ฐฐํฌ ํŒจํ„ด)message broker (์‘์šฉํ”„๋กœ๊ทธ๋žจ, ์„œ๋น„์Šค ๋ฐ ์‹œ์Šคํ…œ์ด ์ •๋ณด๋ฅผ ํ†ต์‹ ํ•˜๊ณ  ๊ตํ™˜ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•˜๋Š” ์†Œํ”„ํŠธ์›จ์–ด ๋ชจ๋“ˆ)
ย ๋ณต์žกํ•œ ๋ผ์šฐํŒ…์— ์˜์กดํ•˜์ง€ ์•Š๊ณ  ์ตœ๋Œ€ ์ฒ˜๋ฆฌ๋Ÿ‰์œผ๋กœ ์ŠคํŠธ๋ฆฌ๋ฐํ•˜๋Š” ๋ฐ ๊ฐ€์žฅ ์ ํ•ฉ, ๋‹ค๋‹จ๊ณ„ ํŒŒ์ดํ”„๋ผ์ธ์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฒ˜๋ฆฌ๋ณต์žกํ•œ ๋ผ์šฐํŒ…, ์‹ ์†ํ•œ ์š”์ฒญ-์‘๋‹ต์ด ํ•„์š”ํ•œ ์›น ์„œ๋ฒ„์— ์ ํ•ฉ

  • reference : Apache Kafka in 5 minutes

https://www.youtube.com/watch?v=PzPXRmVHMxI Apache Kafka in 6 minutes https://www.youtube.com/watch?v=Ch5VhJzaoaI
https://always-kimkim.tistory.com/entry/kafka101-broker


Internal/External Structure

Screen Shot 2022-08-02 at 10 04 14 AM Screen Shot 2022-08-02 at 10 05 11 AM Screen Shot 2022-08-02 at 10 08 27 AM Screen Shot 2022-08-02 at 10 10 38 AM Screen Shot 2022-08-02 at 10 14 03 AM


Example

build.gradle

1
2
3
dependencies {
  implementation: "spring-kafka"
}

application.yml

1
2
3
spring:
  kafka:
    bootstrap-servers: localhost:9092

KafkaTopicConfig.java

1
2
3
4
5
6
7
8
@Configuration
public class KafkaTopicConfig {

  @Bean
  public NewTopic amgifoscodeTopic() { // NewTopic : org.apache.kafka.clients.admin.NewTopic
    return TopicBuilder.name("amigoscode").build();
  }
}

Output Screen Shot 2022-08-02 at 10 54 37 AM Screen Shot 2022-08-02 at 11 10 13 AM

Terminal
https://kafka.apache.org/quickstart + additional command : https://sangchul.kr/144
1
2
3
4
5
6
7
8
9
10
11
cd kafka_2.12-3.2.1

bin/zookeeper-server-start.sh config/zookeeper.properties

bin/kafka-server-start.sh config/server.properties

// Create a topic
bin/kafka-topics.sh --create --topic quickstart-events --bootstrap-server localhost:9092

// Read the events (topic --> amigoscode)
bin/kafka-console-consumer.sh --topic amigoscode --from-beginning --bootstrap-server localhost:9092

KafkaProducerConfig.java

  • Use KafkaTemplate for implemeting producer
    • KafkaProducer.send() in KafkaTemplate.send()
  • reference : https://jessyt.tistory.com/142
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
    @Configuration
    public class KafkaProducerConfig {
    
      @Value("${spring.kafka.bootstrap-servers}")
      private String bootstrapServers;
    
      public Map<String, Object> producerConfig() {
          Map<String, Object> properties = new HashMap<>();
          properties.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
          properties.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
          properties.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
          return properties;
      }
    
      @Bean
      public ProducerFactory<String, String> producerFactory() {
          return new DefaultKafkaProducerFactory<>(producerConfig());
      }
    
      @Bean
      public KafkaTemplate<String, String> kafkaTemplate(ProducerFactory<String, String> producerFactory) {
          return new KafkaTemplate<>(producerFactory);
      }
    
      // KafkaTemplate<String, Object> ~~~
    }
    

KafkaConsumerConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Configuration
public class KafkaConsumerConfig {

    @Value("localhost:9092")
    private String bootstrapServers;

    public Map<String, Object> consumerConfig() {
        Map<String, Object> properties = new HashMap<>();
        properties.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
        properties.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringSerializer.class);
        properties.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringSerializer.class);
        return properties;
    }

    @Bean
    public ConsumerFactory<String, String> consumerFactory() {
        return new DefaultKafkaConsumerFactory<>(consumerConfig());
    }

    @Bean
    public KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<String,String>> factory(ConsumerFactory<String, String> consumerFactory) {
        ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>();
        factory.setConsumerFactory(consumerFactory);
        return factory;
    }

    // ConcurrentKafkaListenerContainerFactory<String, Object> ~~~
}

KafkaApplication.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootApplication
public class KafkaexampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(KafkaexampleApplication.class, args);
    }

    @Bean
    CommandLineRunner commandLineRunner(KafkaTemplate<String, String> kafkaTemplate) {
        return args -> {
            kafkaTemplate.send("amigoscode", "hello kafka"); // topic, data(message)
        };
    }
}
KafkaTemplate.send()
it goes through different layers before the message is sent to Kafka.

KafkaListeners.java

1
2
3
4
5
6
7
@Componenet
public class KafkaListeners {
  @KafkaListener(topics = "amigoscode", groupId = "groupId")
  void listener(String data) {
    System.out.println("Listener received: " + data );
  }
}

MessageRequest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// record
public record MessageRequest(String message) {
}

=

// class
// Records provide a public constructor (with all arguments), read methods for each field (equivalent to getters) and the implementation of hashCode, equals and toString methods.
public class MessageRequest {
    private String message;

    public MessageRequest() {
    }

    public MessageRequest(String message) {
    }

    public String getMessage() {
        return message;
    }

    public boolean equals(Object o) {
        return true;
    }

    public int hashCode() {
        return 0;
    }

    public String toString() {
        return "";
    }
}

MessageController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
@RequestMapping("api/vi/messages")
public class MessageController {
  private KafkaTemplate<String, String> kafkaTemplate;

  public MessageController(KafkaTemplate<String, String> kafkaTemplate) {
    this.kafkaTemplate = kafkaTemplate;
  }

  @PostMapping
  public void publish(@RequestBody MessageReqeust request) {
    kafkaTemplate.send("amigoscode", request.getMessage());
  }
}

Output Screen Shot 2022-08-02 at 1 16 46 PM

API TEST

1
2
3
4
5
6
POST http://localhost:8080/api/vi/messages
Content-Type: application/json

{
  "message": "Api With Kafka"
}

Output Screen Shot 2022-08-02 at 1 28 05 PM


Kafka Stream

  • kafka streams api๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ, ์ง€์†์ ์œผ๋กœ ํ˜๋Ÿฌ๋“ค์–ด์˜ค๋Š” ๋ฐ์ดํ„ฐ์— ๋Œ€ํ•œ ๋ถ„์„, ์ฒ˜๋ฆฌ๋ฅผ ์œ„ํ•œ client library

  • ์–ด๋–ค Topic์œผ๋กœ ๋“ค์–ด์˜ค๋Š” ๋ฐ์ดํ„ฐ๋ฅผ Consumeํ•˜์—ฌ, streams api๋ฅผ ํ†ตํ•ด ์ฒ˜๋ฆฌ ํ›„ ๋‹ค๋ฅธ Topic์œผ๋กœ ์ „์†ก(Producing) ํ•˜๊ฑฐ๋‚˜ ๋๋‚ด๋Š” ํ–‰์œ„

  • Spring cloud stream์—์„œ ์ œ๊ณตํ•˜๋Š” Binder๋ผ๋Š” ๊ตฌํ˜„์ฒด๋ฅผ ์ค‘๊ฐ„์— ๋‘๊ณ  ํ†ต์‹ ํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ์–ด๋Š ํ•˜๋‚˜์˜ ๋ฏธ๋“ค์›จ์–ด์— ๊ฐ•๊ฒฐํ•ฉ ๋˜์–ด์žˆ์ง€ ์•Š์€ ์ƒํƒœ์—์„œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ฐœ๋ฐœ

ย binderbindings(input/output)
ย ๋ฉ”์‹œ์ง€ broker ์ •๋ณด๋ฉ”์„ธ์ง€๋ฅผ ์ „์†กํ•  ์ฑ„๋„์ •๋ณด
ย ๋ฏธ๋“ค์›จ์–ด์™€์˜ ํ†ต์‹ ์„ ๋‹ด๋‹นํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ๋ฏธ๋“ค์›จ์–ด์™€ ํ†ต์‹ ์„ ์œ„ํ•œ ๋ธŒ๋ฆฟ์ง€
ย ๋ฏธ๋“ค์›จ์–ด(kafka)์™€ producer ๋ฐ consumer์˜ ์—ฐ๊ฒฐ, ์œ„์ž„ ๋ฐ ๋ผ์šฐํŒ… ๋“ฑ์„ ๋‹ด๋‹น๋ฐ”์ธ๋”์˜ ์ž…/์ถœ๋ ฅ์„ ๋ฏธ๋“ค์›จ์–ด(kafka)์— ์—ฐ๊ฒฐํ•˜๊ธฐ ์œ„ํ•œ Bridge

Example

build.gradle

1
2
3
dependencies {
  implementation 'org.springframework.cloud:spring-cloud-starter-stream-kafka'
}

Default interface

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface Sink {
    String INPUT = "process-input"; // INPUT : consumer ์ž…์žฅ์—์„œ subscribe ๋ฐ›์„ TOPIC๋ช…

    @Input(INPUT)
    SubscribableChannel input();
}

public interface Source {
    String OUTPUT = "process-output"; // OUTPUT : producer ์ž…์žฅ์—์„œ publishํ•  TOPIC๋ช…

    @Output(OUTPUT)
    MessageChannel output();
}

public interface Processor extends Source, Sink {
}

Custom interface

1
2
3
4
5
6
7
8
9
10
public interface ProcessMessage {
    String SEND_MESSAGE = "send-message";
    String RECEIVE_MESSAGE = "receive-message";

    @Output(SEND_MESSAGE)
    MessageChannel sendMessage();

    @Input(RECEIVE_MESSAGE)
    SubscribableChannel getMessage();
}
1
2
3
4
5
6
7
8
9
@ConditionalOnProperty(name = "spring.cloud.stream.enabled", havingValue = "true", matchIfMissing = true)
@EnableBinding(EventSource.class)
public class MessageConsumer {

  @StreamListener(target = ProcessMessage.RECEIVE_MESSAGE, condition = "headers['eventType'] == 'MessageEvent'")
    public void pushMessage(MessageEvent event) throws JsonProcessingException {
      ...
    }
}
1
2
3
public class MessageEvent {
  // props
}

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spring:
  cloud:
    stream:
      enable:
      kafka:
        binder:
          brokers: localhost // broker ip
          # replication-factor: 2 // minimum = 2
          # auto-create-topics: false
        bindings: // input/output
          receive-message: // channel name
            destination: "${spring.cloud.stream.topic:}process-input" // topic name
            group: "${spring.cloud.stream.consumer.group:}" // consumer group id
          send-message: // output
            destination: "${spring.cloud.stream.topic:}process-output"

@EnableBinding

1
@EnableBinding(EventSource.class)

@KafkaListener

1
@KafkaListener(topics = "amigoscode", groupId = "groupId")

@ConditionalOnProperty(โ€ฆ)

enables bean registration only if an environment property is present and has a specific value.

1
@ConditionalOnProperty(name = "spring.cloud.stream.enabled", havingValue = "true", matchIfMissing = true)
  • condition : spring.cloud.stream.enabled:true (in application.yml)
  • name (=value) : key in application.yml
  • havingValue : value of key in application.yml
  • matchIfMissing : whether create a bean even if not matching

@ConditionalOnMissingBean


https://piotrminkowski.com/2021/11/11/kafka-streams-with-spring-cloud-stream/
http://www.chidoo.me/index.php/2016/11/06/building-a-messaging-system-with-kafka/
https://jaehun2841.github.io/2019/12/23/2019-12-23-kafka-streams-binder-feature/#Version-Up
https://sangchul.kr/144

This post is licensed under CC BY 4.0 by the author.

[Spring] Microservices in Spring Boot

[Easy] Remove Duplicates from Sorted Array

Comments powered by Disqus.