728x90

NATS란?

NATS는 클라우드 기반 애플리케이션, IoT 메시징 및 마이크로서비스 아키텍처를 위해 만들어진 가벼운 메시징 서비스입니다.

NATS Server는 Go언어로 구축되어있으나 NATS에서 제공하는 툴이나 라이브러리들을 많이 제공하고 있어 Cloud native Application, IoT Messaging, MSA등에서 많이 활용할 수 있습니다. 확인하러가기

NATS는 왜 사용하는 것인가??

  • 빠르다
  • 가볍다
  • 간편하다
  • 어디에든 배포가 가능하다
  • 최신 기술들 호환이 좋다

와 같은 여러가지 강점들을 가지고 있으며, 이를 토대로 kafka와 같은 Messaging 서비스들과 경쟁을 하고 있는 것 같습니다.

우선 NATS가 어떤 개념으로 작동하고, 어떤 기능들이 존재하는지에 대해 알아보도록 하겠습니다.


Subject-Based Messaging

NATS는 주제 기반 메시징을 지원하고 있습니다.
여기서 말하는 *주제는 무엇일까요?

Subject는 publisher와 subscriber가 서로를 알기 위한 문자열입니다.

이 Subject에는 몇가지 규칙이 따릅니다.

  • 권장 문자: a to b, A to Z 및 0 to 9(이름은 대소문자를 구분하며 공백을 포함할 수 없다.)
  • 특수문자: 마침표.(제목에서 토큰을 구분하는데 사용), *, > (각각이 와일드 카드로 사용됩니다)
  • 예약된 이름: $로 시작하는 이름은 시스템에 예약되어 있어 사용하면 안됩니다.

.은 이름의 그룹을 분간하기 위해 사용이 됩니다.

time.kr.soul
time.kr.busan

또한 *>는 이름이 매칭 부분을 조정할 수 있습니다.

*는 단일 토큰 매칭으로 *부분을 제외한 나머지 그룹군이 맞다면 매칭을 시켜줍니다.

>는 여러가지 토큰을 일치시켜줄 수 있는 것으로 이는 몇가지 조건을 맞춰야 사용할 수 있습니다.

  • 하나 이상의 토큰과 일치
  • 제목 끝에만 명시가능

위 조건을 만족할 경우 사용하여 토큰을 매칭시킬 수 있습니다.

와일드 카드는 혼합이 가능하며 *는 한 제목에 여러번 작성할 수 있습니다.
ex) *.*.east.>


Core NATS

NATS를 구성하는 모델들은 다음과 같습니다.

  • Publish-Subscribe
  • Request-Reply
  • Queue Groups

1. Publish-Subscribe

Publish와 Subscribe는 각각 메시지를 발행하는 송신자와 메시지를 수신하는 수신자로 봐도 좋을 것 같습니다.

Publish가 message를 발행할경우 Publisher와 매핑되는 토큰을 가진 Subscriber들이 message들을 읽어 처리하는 식입니다.

주고 받을 수 있는 Message는 아래의 필드들로 구성되어있습니다.

  • 과목
  • 패이로드(byte Array)
  • 헤더 필드의 수
  • reply(optional)

Message사이즈는 max_payload설정으로 설정이 가능하고, 기본적으로 1MB가 할당되어있습니다.
최대 사이즈 64MB까지 설정할 수 있지만 무작정 메모리 크기를 늘리기 보다는 꼭 필요한 만큼만 사이즈를 늘려 관리하는 것을 권장하고 있습니다.

2. Request-Reply


요청과 응답 부분입니다.
사용자 즉 Client가 서버에 요청을 보낼경우 서버는 보인이 처리해야하는 로직들을 처리 후 Message를 Publish합니다.

이때, 동기방식으로 Message처리 후 응답을 기다렸다 Client에게 응답을 하는 것이 아닌
서버에서 처리할 부분이 완료될경우 Message는 Publish한 후 Client에게 응답을 하고,
나머지 Message를 받아 처리하는 부분은 Background로 처리합니다.

3. Queue Groups


NATS의 로드밸런싱 기능입니다. 이는 분산 queue기능을 통해 처리하며 이것은 동일한 그룹 내에서 한번만 처리되도록 보장하기 위한 기능입니다.

Queue Groups는 다음과 같은 특징을 가지고 있습니다.

  1. Load Balancing
  2. Message Delivery Guaratees
  3. Scalability
  4. Fallover

위와 같은 특징들은 모두 Message를 동일한 Queue Group내에서 한번만 처리되도록 하기 위해 분산 처리, 전달 보장, 장애 복구의 기능을 통해 메세지가 처리되도록 보장하고 있습니다. 또한 위와 같이 분산처리를 할 경우 K8s 환경에서 개발을 할경우 Pod를 얼마나 늘리든지 상관없이 얼마든지 확장 강능하다는 강점을 가지고 있습니다.

JetStream

NATS server에는 NATS의 기능보다 확장된 기능들을 제공하는 built-in distributed persistence system이 존재하는데 그것이 JetStream입니다.

JetStream에서 제공하는 기능은 다음과 같습니다.

  • Streaming
  • Replay policies
  • Retention policies and limits
  • Limits
  • Retention policy
  • Subhect mapping transformations
  • Persistent and Consistent distributed storage
  • Stream replication factor
  • Mirroring and Sourcing between streams
  • De-coupled flow control
  • Exactly once semantics
  • Consumers
  • Fast push consumers
  • Horizontaliy scalable pull consumers with batching
  • Cosumer acknowledgments
  • Key Value Store

이러한 기능들은 Option이므로 사용하실 때 필요한 기능들을 찾아 적용하시는 것이 좋을 것 입니다.

또한 NATS는 가볍고 빨라야하기 때문에 Event의 Message 즉 Payload가 적정선 이상으로 커지는 것을 경계하는 것으로 보입니다. Message를 설계를 할 때 Message의 크기를 8MB로 제한하고 설계한다면 NATS의 성능을 제대로 활용할 것 같습니다.


참고자료: https://docs.nats.io

728x90
728x90

우리가 개발을 하다보면 Redis라는 용어를 한번쯤 들어보았을 것이고, RedisTemplate라는 것을 한번은 코드상에서 봤을 수 있습니다.

개발을 하면서 데이터를 저장할 때 쓴다고 한 Mysql, Mongodb등 기존 데이터 베이스를 두고 왜 Redis라는 Database를 또 사용하는지, 그리고 어떻게 사용하는지에 대해 다뤄보겠습니다.

Redis란?

Redis는 REmote Dictionary Server의 약자이며, Mysql과 같이 서버와는 별개로 저장이 되지 않고 메모리상에 저장되는 In memory구조로 구성되어있습니다.

데이터는 Key Value로 관리가 되고 있으며, 다양한 구조 집합을 제공함으로 다양한 구조를 Value로 저장할 수 있으며 최대 512MB의 데이터가 저장가능합니다.

Key로 데이터를 관리하기때문에 조회에 있어 가장 빠른 성능을 자랑하며, 빠른 조회 성능을 요구하는 캐싱, 세션 관리 등에 사용을 하고 있습니다.

Redis는 In memory 구조로 key value형태로 데이터를 저장 하고 있으며, key로 데이터를 관리하기때문에 조회에 있어 빠른 성능을 자랑하며, 현재 가장 인기 있는 key value store입니다.

Key Value로 처리하면서 Redis는 Row로 저장되는 것 보다는 Column을 저장한다는 것으로 보이고, 이때문에 많은 양의 데이터를 저장하고 지속적으로 관리하는 기존 데이터베이스와 사용용도는 다르다고 필자는 생각합니다.

또한 InMemory구조로인한 데이터를 지속적으로 백업해주어야하는 문제가 있으며, 이를 해결하여 사용하기보다는 기존의 다른 데이터베이스와 함께 사용하면서 용도에 맞춰 사용하는 것이 가장 현명한거 같다 생각합니다.

Redis는 요청에 의했을 때는 Single Thread 개념을 가져오지만, background에서는 Multi Thread를 지원하는 만큼 데이터를 조회하는데에 있어 빠른 성능을 강점으로 가져가는 것 같습니다.

이처럼 Redis는 java의 Map과 같이 Key Value로 데이터를 저장하며, inmemory구조지만 Redis를 사용하는 상황은 분산 시스템에 있습니다. 분산시스템에서 동일한 key value의 저장소를 공유해야만 하는 상황이라면 Redis를 사용하여 해당 Issue를 해결하는 방법도 좋은 방법이 될 수 있습니다.


사용법

설치

우선 필자는 Database관리를 Docker로 하고 있기 때문에 Container로 만들어 관리를 하도록 하겠습니다.

1. Redis Image Pull

docker pull redis

Docker에서 지원해주는 Redis Image를 다운받아줍니다.

2. Redis Container run

docker run --name test-redis -p 6379:6379 redis

redis이미지를 토대로 container를 생성하여 실행하여줍니다.
port는 redis기본 port인 6379로 설정하였습니다.

3. redis접속

docker exec -it test-redis /bin/bash

위 명령어를 통해 실행중인 container에 접속해주도록 합니다.

redis-cli

위 명령어 입력을 완료하면 container로 실행중인 redis안에 접속할 수 있습니다.

4. CRD

set {key} {value}
get {key}
del {key}

위 명령어와 같이 set을 통해 데이터를 등록할 수 있습니다.
key는 식별자 역할을 함으로써 수정할 때에도 동일하게 set명령어를 사용할 경우 해당 keyvalue를 변경할 수 있습니다.

이처럼 Redis자체를 사용하는 것은 쉽게 사용할 수 있습니다.
그럼 이제 서버에서 어떻게 사용하는지를 알아보겠습니다.

JAVA에서 Redis사용법

spring:
  data:
    redis:
      host: localhost
      port: 6379

Application.yml설정입니다.
Redis기본 port가 6379이기 때문에 따로 port가 겹치지 않아 6379Port로 설정하여 사용하였습니다.

@Configuration
public class RedisConfig {
    @Value("${spring.data.redis.host}")
    private String redisHost;
    @Value("${spring.data.redis.port}")
    private int redisPort;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(redisHost, redisPort);
    }

    @Bean
    public RedisTemplate<String, String> redisTemplate() {
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}

RedisConfig설정입니다.
연결을 위해 redisHost와 redisPort값을 받아 Redis저장소에 연결 즉 connect를 하고, 이후 RedisTemplate를 정의함으로써 보다 편하게 사용할 수 있게 정의하였습니다.

@Repository
@Transactional
@RequiredArgsConstructor
public class TestStore {
    //
    private final RedisTemplate<String, String> redisTemplate;

    public void create(String key, String data) {
        ValueOperations<String, String> opreation = redisTemplate.opsForValue();
        opreation.set(key, data);
    }

    public void update(String key, String data) {
        ValueOperations<String, String> opreation = redisTemplate.opsForValue();
        opreation.set(key, data);
    }

    public String load(String key) {
        ValueOperations<String, String> opreation = redisTemplate.opsForValue();
        return opreation.get(key);
    }

    public void remove(String key) {
        redisTemplate.delete(key);
    }
}

TestStore입니다.
RedisTemplate를 통해 데이터를 CRUD를 하는 작업을 담당하게 구현하였습니다.


참고자료: https://redis.io/docs/

728x90
728x90

Code::Blocks를 설치 하였으면 우리의 목적은 C or C++를 공부하기 위해서니 해당 언어를 사용할 수 있는 작업 공간이 필요하다.

 

이를 위해 많은 용어가 떠다니는 Code::Blocks에서 어떻게 프로젝트를 만들 수 있는지 알아보자.

 

우선 Code::Blocks에 들어오게 될 경우 아래와 같은 화면이 나올것이다.

이때 저 가장 클릭하고 싶게 생긴 Create a new project버튼을 클릭해 주도록 하자.

 

이후 어떤 프로젝트를 생성할 것인지와 관련하여 선택하라는 Modal창이 뜰 것이다.

 

잘 사용하는 분들은 원하는 것을 선택하면 될 것이고, 필자와 같이 C++언어 뉴비들은 정석 코스인 Console application을 선택하여 project를 생성하도록한다.

 

그러고 나면 해당 project를 진짜 만들거냐 물어보는데 쿨하게 스킵하도록 하자

다음 단계로 사용할 언어를 고르라고 나올 것이다.

C와 C++은 큰 차이가 있으니 해당 부분은 확실히 구분을 하고 난 이후 언어를 선택하도록 하자.

 

언어까지 다 고르셨으면 다음 step으로 가장 중요한 Project의 이름을 설정하는 step이 나올 것이다.

Project의 이름은 해당 코드의 얼굴이라 할 정도로 중요하다 생각한다.

보통 코드를 보고 어떤 기능을 하는지에 대해 고민하기 전에 Project의 이름을 보고 "어떤 기능을 할 것이다!"라고 짐작 할 수 있기 때문에 필자처럼 test목적이 아닐 경우 무작정 test라 이름을 정의 할 경우 다음 개발자로부터 살인예고장을 받을 수 있으니 조심 또 조심하길 바란다.

 

※ 주의 사항 ※
아마 필자처럼 Folder to create project in 을 지정안하고 프로젝트를 생성 할 경우 Code::blocks가 죽고 process만 살아 재실행이 안되는 경우가 생길 수 있다.

그럴때는 Command를 관리자 권한으로 실행 이후 "tasklist"로 process확인한 이후 "taskkill /f /im codeblocks.exe" 를 사용하여 process를 죽이면 정상적으로 재실행 할 수 있다.

결론 : project어디에 생성할지 지정해 주자.. (어디 만들지 지정을 안했을 경우 경고를 하나 띄워주면 얼마나 좋을까하는 아쉬움이 있다...)

 

 마지막 step인거 같다.

대강 읽어보니 해당 코드를 검사해줄 compiler를 선택하라는 것 같은데 이는 지금 알아보기엔 어서 코드를 짜고 싶기 때문에 우선 skip하도록 하겠다.

 

봐보니 기본적으로 GCC Compiler를 mapping해주는 것 같기 때문에 무지성 finish버튼을 클릭해 보았다.

 

이로써 코딩할 준비가 된거 같다! 

다음편부터 C or C++에 관해 다루도록 하겠다.

 

처음 시작에 애먹는 분들에게 도움이 되었기를 바라며...

728x90

'C언어 개발 > C++독학하기' 카테고리의 다른 글

[C언어 독학하기] Code::Blocks 설치하기  (2) 2022.10.09
728x90

좋은 아침입니다.

 

이번 post내용은 제목에서 보이듯이 ApplicationContext에서 지원해 주는 getBean이란 메소드를 사용하여 Bean들을 다루는 것에 대해 포스팅 할 예정이다.

 

우리가 업무를 진행할 때 가끔(?) 이런 요구 사항을 받을 수 있다.

 

1. 작업 내용이 몇개가 추가 될 지 모른다.

2. 업무의 종류는 같지만 하는 일은 다를 수도 있다.

 

즉 몇개의 항목이 있는지도 모르고, 추가되는 항목이 있을 때 그 항목이 하는 일도 다를 수 있다는 뜻이다...

 

만약 저런 요구조건이 들어오게 된다면 우리는 if 혹은 switch를 사용하여 코드를 더럽힐 수도 있다.( 필자가 그랬다... )

 

하지만 세상은 바보가 아니고 그를 좀더 합리적으로 해결 할 수 있는 방법은 이미 알게모르게 제공을 해주고 있다.

 

바로 추상화 즉 Interface를 사용하는 것이다.

 

읭...? 갑자기 Interface?? 라고 생각할 수도 있다.

하지만 그렇게 생각한다면 Interface 무엇인가 부터 고민을 해봐야한다 생각한다.

 

Interface가 무엇인가?? 바로 어떤 행위를 규칙으로 정해두고 구현체들에게 해당 행위를 정의하도록 하는 것이 Interface의 역할이다 라고 볼 수 있다고 생각한다.

 

그러니 위처럼 끝이 확실하지 않고 확장을 할 가능성이 있는 요구조건이 들어오면 행위들을 Interface로 설계를 하고 추가 생성을 해야한다면 해당 업무를 담당하는 Class를 만들어 구현하기만 하면 되어 기존 코드에 영향력을 끼치지 않을 수 있다는 장점도 추가로 가져갈 수 있다.

 

이제 Interface를 왜 사용하는지도 알았으니 본문 Bean들을 불러오는 것에 대해 말해보도록 하겠다.

 

고맙게도 Spring에서 지원하는 ApplicationContext에는 Class type을 가지고 있는 Bean들의 이름을 가져올 수 있는 메소드를 지원해준다.

 

우선 사용할 Interface를 만들어준다.

public interface WorkGroup {
    void work();
}

 

이후 해당 Interface를 구현한 Class들을 만들어주면 준비는 끝이다.

그럼 이제 Bean들의 이름을 통해 Bean들을 가져오는 코드를 만들면 된다.

@Component
public class BeanHandler {
    //
    private final List<WorkGroup> works;

    public BeanHandler(ApplicationContext applicationContext) {
        //
        this.works = new ArrayList<>();

        String[] beanNames = applicationContext.getBeanNamesForType(WorkGroup.class);
        for (String beanName: beanNames) {
            this.works.add((WorkGroup) applicationContext.getBean(beanName));
        }
    }
}

getBeanNamesForType()메소드를 사용할 경우 Interface의 class type을 명시해주면 해당 interface를 구현한 구현체들의 Bean이름을 가져올 수 있다.

 

이름을 가져왔다면 applicationContext.getBean() 메소드를 사용하여 해당 이름을 가진 Bean을 가져와 주면 된다.

 

이제 모든 준비가 끝났다 안에서 각자 다른 업무를 진행하는 Class들이지만 우리는 해당 업무를 work()라 정의하여 interface를 만들었고 각각 Class들은 interface를 구현한 구현체다 보니 어떤 Class든 work()메소드를 호출하면 각자 필요에 맞는 업무를 진행할 것이다.

@Component
public class BeanHandler {
    //
    private final List<WorkGroup> works;

    public BeanHandler(ApplicationContext applicationContext) {
        //
        this.works = new ArrayList<>();

        String[] beanNames = applicationContext.getBeanNamesForType(WorkGroup.class);
        for (String beanName: beanNames) {
            this.works.add((WorkGroup) applicationContext.getBean(beanName));
        }
    }

    public void process() {
        //
        log.info("Process Start");
        for (WorkGroup workGroup: works) {
            workGroup.work();
        }
        log.info("Process End");
    }
}

이로써 각자 업무 내용이 다르더라도 process가 시작되면 각각의 그룹들이 일을 하는 것을 지금은 log를 통해 확인해 볼 수 있다.


요구사항은 우리 개발자들이 원하듯이 딱 정해져서 오는 것 보다 유동적인 변화를 가진 요구사항이 오는 경우가 제 짧은 개발 인생에는 더 많은거 같다.

 

그랬을 때 당황하지 않고 이전의 해결방법들을 참고하여 더 좋은 해결방안을 내는게 좋을거 같다.

 

이글 이 도움이 되길 바라며 모두 화이팅 하길 바란다. 이상!

728x90

'Server' 카테고리의 다른 글

REST API란?  (0) 2024.04.14
[Docker] 도커로 데이터 베이스 편하게 사용하자~  (1) 2024.04.08
[Docker] Docker 설치하기  (0) 2024.04.08
[Docker] Docker란 무엇인가?  (2) 2024.04.08
NATS란?  (0) 2024.04.08
728x90

최근 관심을 가지게 된 C언어를 독학하기로 했다.

우선 기존 사용하던 JAVA나 javaScript와 다른 언어인 만큼 초심으로 돌아가 차근차근 시작해 보기로 하였다.

모든 개발의 첫번 째 환경 구축 하는 것부터 시작하겠다.

개발 환경 구축하기

Code::Blocks 설치하기

구글링을 통해 C언어를 개발할 수 있는 개발툴을 찾아보았는데,

고심한 결과 Code::Blocks를 설치해보기로 하였다.(특별한 이유는 없다...)

 

https://www.codeblocks.org

 

Code::Blocks

The IDE with all the features you need, having a consistent look, feel and operation across platforms.

www.codeblocks.org

우선 해당 사이트로 들어가게 되면 Downlods가 있는데 해당 링크로 들어가도록한다.

 

이후 위와 같이 Download the binary release를 클릭하여 Download하러 들어간다.

 

들어가면 Binary release가 나오게 되는데 사용하고 있는 운영체제에 맞는 곳으로 들어가면 된다.

 

여러가지 설치 파일들이 존재하는데 필자의 경우 codeblocks-20.03mingw-setup.exe파일을 설치하였다.

해당 파일을 설치한 이유는 mingw-setup으로 설치를 안할 경우 직접 컴파일러를 설치하고 설정해야 하는것으로 알고있어서 해당 파일로 설치를 하였다.(구글이 알려주더라고..)

 

이후 진행사항은 아무런 할것없이 그냥 next로 넘겨 다운을 완료하면된다.

 

이상으로 설치하는 법을 작성하였고 다음편에서 마저 독학해보도록하겠다...

화이팅...

728x90

'C언어 개발 > C++독학하기' 카테고리의 다른 글

[C언어 독학하기] Code::Blocks 시작하기  (0) 2023.05.02
728x90

이번에 소개할 문제는 문자열 압축이라는 알고리즘 문제이다.

 

해당 문제는 데이터를 처리할 때 최소한의 메모리를 사용하여 문자열을 저장하고 싶은 게 핵심인 문제인 거 같다.

 

예를 들어보면 "가나다라마바사아"라는 문자열이 존재한다고 가정해보자.

이때 해당 문자열을 그대로 저장하게 되면 8글자를 Byte로 저장하게 된다.

위와 같이 중복된 글자가 없으면 그대로 저장하는 것이 데이터의 정확도상 당연한 것이라 생각한다.

 

하지만 "가가가나나나나나나다다다다다라라라마마바"라는 문자열이 있다고 생각해보자.

위와 같은 문자열이 있을 때 해당 문자열을 그대로 저장하는 것은 20개의 글자를 Byte로 저장하는 것이다.

이럴 때 동일한 문자가 연달아 존재할 때 몇 개인지를 앞에 명시하여 보다 짧은 문자열로 저장하는 것이 훨씬 적은 데이터를 가지고 저장을 할 수 있다.

 

또한 문자열을 줄일 때 1 단어 단위로 줄이면 "3가6나5다3라2마바"와 같은 형태로 저장되는 것이고,

2 단위로 줄이면 "가가가나2나나나다2다다라라라마마바"라는 문자열로 저장되는 식으로 몇 개 단위로 잘라 저장하느냐에 따라 저장되는 데이터 양에 차이를 주게 된다.

 

다시 문제로 돌아가서 해당 문제는 데이터를 최소한 짧은 문자열로 저장하고 싶은데 n단위로 잘라 저장했을 때 가장 적을 경우 가장 짧은 문자열일 경우 해당 문자열의 length를 구하여라로 생각하면 될 거 같다.

 

우선 default를 지정하도록 하자.

int answer = s.length();

해당 default로 지정한 경우는 어떤 방법으로 문자열을 압축한다 하더라도 기본 문자열보다 길어질 수 없기 때문에 기본 문자열의 length를 default로 지정하였다.

 

이후 먼저 중요하게 생각한 것은 문자열을 자를 건데 몇 개 단위로 자를지에 관해 먼저 생각하기로 하였다.

for(int i = 1; i< s.length() /2 +1; i++) {

}

문자열을 자를 거지만 절반을 넘어갈 경우 어차피 default와 길이는 같을 테니 적절하게 max를 지정해 주었다.

 

이후 잘라 만들어진 문자열과 이전 값을 비교하기 위한 변수와 몇 번째 압축인지를 알기 위한 변수를 선언해 주었다.

// i 는 자르는 갯수
for(int i = 1; i< s.length() /2 +1; i++) {
    StringBuffer sb = new StringBuffer();

    //이전값
    String bef = "";

    // 압축 count default 1
    int zipNum = 1;
}

이후부터는 subString으로 문자열을 자르기 위해 반복문을 돌려 문자열을 자르는 startPoint와 endPoint를 지정해 주도록 하자.

for (int j = 0; j < s.length(); j++) {

    //마지막 index
    int endNum = i+j > s.length() ? s.length() : i+j;

    //현재 
    String sub = s.substring(j, endNum);

}

위에서 나온 sub의 값은 현재 잘려 나온 문자열의 값이다.

 

현재 잘려나온 값을 알았으니 이제 해당 값이 압축이 가능한 문자열인지 check를 해볼 차례이다.

for(int i = 1; i< s.length() /2 +1; i++) {

    StringBuffer sb = new StringBuffer();
    String bef = "";
    int zipNum = 1;

	for (int j = 0; j < s.length(); j++) {
				
        //마지막 index
        int endNum = i+j > s.length() ? s.length() : i+j;

        //현재 
        String sub = s.substring(j, endNum);
    
        if (!sub.equals(bef)) {
        
            bef = sub;
            zipNum = 1;
        }
        else {
            zipNum++;
        }
    
    }
}

bef = 이전 값. 

sub = 현재 값.

위 두 값을 비교하여 같을 경우 압축 가능한 문자열이니 zipNum을 올려주며 끝나고,

문자열이 같지 않을 경우 압축할 수 없는 값이니 다음 단계를 진행해 주었다.

 

if (!sub.equals(bef)) {

    if(zipNum > 1) {
        sb.append(zipNum);
    }

    if (!bef.equals("")) {

        if (endNum != s.length()) {
            sb.append(bef);
        }
        else {
            sb.append(bef).append(sub);
        }	
    }

    bef = sub;
    zipNum = 1;
}
else {
    zipNum++;

    if (endNum == s.length()) {
        sb.append(zipNum).append(bef);
    }
}

자른 문자열이 이전과 이후가 같을 확률은 다를 경우보다 높아 if문 앞 조건에서 처리를 할 수 있도록 하였다.

 

zipNum이 1보다 클 경우 해당 문자열은 이미 앞에서 문자열 압축이 이뤄진 것이니 압축이 된 문자열 숫자를 붙여주고,

이전 문자를 붙여줌과 동시에 현재 잘린 문자열이 기존 문자열의 마지막 단계가 아닐 경우 bef에 값을 저장하여 이후 들어올 잘린 문자열이 현재 값과 비교를 할 수 있게 조절해준다.

 

문자열이 서로 같을 경우 zipNum을 1올려줌과 동시에 현재 문자열이 마지막 문자열인지 체크하여 값을 추가해주도록 하여 값이 빠지는 경우가 생기지 않게 해 주었다.

 

이대로 끝났으면 좋겠지만 아직 2가지를 더 처리를 해 주어야 한다.

 

우선 현재 문자열을 자를 때 j의 값을 사용하게 되는데 해당 값은 무조건 1씩 오르기 때문에 2개 이상의 단어를 자르게 될 시 값이 중복되어 나오는 경우가 생기게 된다.

 

설명을 하자면 본인이 원하는 경우는 1, 3, 5, 7, 9와 같이 2단계씩 건너뛰려 하는데 이 코드를 지금 상태로 사용하게 될 시 1, 2, 3, 4, 5, 6, 7, 8, 9와 같이 1단계씩 건너 2 단어씩 잘라 앞 문자열의 뒷 단어와 뒷 문자열의 앞 단어가 곂치는 현상이 발생하는 것이다.

 

이를 해결하기 위해선 

for(int i = 1; i< s.length() /2 +1; i++) {

    StringBuffer sb = new StringBuffer();
    String bef = "";
    int zipNum = 1;

	for (int j = 0; j < s.length(); j++) {
			
        int endNum = i+j > s.length() ? s.length() : i+j;

        if (!sub.equals(bef)) {
					
            if(zipNum > 1) {
                sb.append(zipNum);
            }

            if (!bef.equals("")) {

                if (endNum != s.length()) {
                    sb.append(bef);
                }
                else {
                    sb.append(bef).append(sub);
                }	
            }

            bef = sub;
            zipNum = 1;
        }
        else {
            zipNum++;

            if (endNum == s.length()) {
                sb.append(zipNum).append(bef);
            }
        }
    
    	//단계를 조절하기 위해 값을 현재 end값 -1로 명시해준다.
        j = endNum - 1;
    }
}

j = 문자열 자르는 단계 (index)

 

j를 마지막 잘랐던 index번호로 변경해주면 반복문이 작동하며 j++로 인해 의도했던 번호보다 1이 많아지게 되어 -1을 해주면 정상적으로 작동하게 된다.

 

subString의 용도상 startIndex부터 ~ endIndex전까지 잘라주게 되는데 위와 같이 해주면 이전 문자열의 index 다음 index부터 문자열을 잘라주게 되어 값의 중복 없이 정상적으로 작동하게 된다.

 

마지막으로

answer = answer > sb.toString().length() ? sb.toString().length() : answer;

잘랐던 문자열을 toString()으로 하나의 문자열로 뽑아 보았을 때 해당 문자열의 길이가 이전에 잘라서 합친 문자열보다 짧을 경우 해당 값을 정답으로 제출하기 위해 비교하며 저장한다.

 


제출 코드

class Solution {
    public int solution(String s) {
    
        int answer = s.length();
	
		for(int i = 1; i< s.length() /2 +1; i++) {
        
			StringBuffer sb = new StringBuffer();			
			String bef = "";
			int zipNum = 1;
            
			for (int j = 0; j < s.length(); j++) {
				
				int endNum = i+j > s.length() ? s.length() : i+j;				
				String sub = s.substring(j, endNum);
                
				if (!sub.equals(bef)) {
					
					if(zipNum > 1) {
						sb.append(zipNum);
					}
					
					if (!bef.equals("")) {

						if (endNum != s.length()) {
							sb.append(bef);
						}
						else {
							sb.append(bef).append(sub);
						}	
					}
					
					bef = sub;
					zipNum = 1;
				}
				else {
                
					zipNum++;
					
					if (endNum == s.length()) {
						sb.append(zipNum).append(bef);
					}
				}
				j = endNum - 1;
			}
			answer = answer > sb.toString().length() ? sb.toString().length() : answer;
		}
		
        return answer;
    }
}

 


해당 문제를 풀면서 stream을 사용하며 풀거나 재귀 함수를 사용할 시 훨씬 코드가 줄어들게 되지만 아직 실력이 부족하기도 하고 외부 라이브러리를 사용하지  않고 직접 돌아가는 상황을 작성해보고 싶어 위와 같이 작성하게 되었다.

 

다음에 stream이나 재귀함수를 공부하며 Refactoring 해 보아야겠다.

728x90
728x90

이번에 소개할 문제는 로또의 최고 순위와  최저 순위를 찾는 문제이다.

 

해당 문제는 로또 6/45는 1부터 45까지의 숫자 중 6개를 찍어 맞히는 복권이고,

1등은 6개, 2등은 5개, 3등은 4개, 4등은 3개, 5등은 2개 그 외에는 전부 6등이라는 규칙과, 일부 번호가 지워져 지워진 번호는 0으로 표기하고 0으로 표기된 수와 매칭 되는 숫자를 가지고 최저 순위와 최고 순위를 알아내라는 것이 이번 문제다.

 

우선 기본으로 주어지는 paramiter들을 확인해보자.

class Solution {
	public int[] solution(int[] lottos, int[] win_nums) {
    	int[] answer = {};
    }
}

라는 코드가 주워지게 된다. 

 

이때 lottos는 본인이 작성한 로또 번호 + 지워진 숫자 포함 6자리가 들어오고, win_nums는 당첨번호 6자리가 서로 무작위로 들어오게된다.

 

문제를 보면 우선 몇 개가 당첨번호인지 찾아보도록 하는 것이 중요하다 생각해서 찾아본다.

for(int myNum: lottos) {
	// myNum : 본인이 작성한 번호
}

해당 코드를 보면 본인이 작성한 로또 번호가 어떤 번호인지 알기 위해 반복문을 사용해 값을 알아내도록 하였다.

 

이제 반복문이 돌면서 값이 나올 텐데 이때 등수를 알기 위해선 맞춘 숫자 + 지워진 숫자가 몇 개인지가 필요하다 생각하여 해당 갯수를 체크 할 수 있는 변수를 선언하였다.

int win = 0;
int zero = 0;

for(int a : lottos) {

}

 

또한 반복문을 돌면서 지워진 숫자를 굳이 당첨번호와 비교를 할 필요는 없다 생각하여 제어문을 사용하여 걸러줌과 동시에 count집계를 시작한다.

int win = 0;
int zero = 0;

for(int a : lottos) {

    if (a == 0) {
        zero++;
    }
    else {
        //로또 번호와 matching을 진행할 번호들
    }
    
}

 

이제 당첨번호가 몇개인지 알아보자.

int win = 0;
int zero = 0;

for(int a : lottos) {

    if (a == 0) {
        zero++;
    }
    else {
        for (int b : win_nums) {
            if (b == a) {
                win++;
            }
        }
    }

}

번호를 일일이 비교하면서 집계를 늘려주어 당첨된 번호가 몇 개인지 알 수 있게 하였다.

 

이제 최고 순위와 최저 순위를 구해야 하는데 

순위는 아까 말했듯 등수의 조건은 정해져 있으니 맞춘 번호와 지워진 번호로 구해야 한다.

 

최저 순위부터 구하자면 지워진 번호가 모두 틀렸을 경우를 가장해보자 그러면 맞춘 번호의 개수가 최대 갯수가 돼버리게 된다.

 

이때 다맞으면 count가 6일테니 -6을 해주면 index순번으로 따졌을 때 0,1,2,3,4,5의 순위가 나오게 되고 1을 더해 실제 순위로 만들어준다. 

int first = Math.abs(win + zero - 6);
int second = Math.abs(win - 6);

//최고 순위
answer[0] = first < 5 ? first + 1 : 6;

//최저 순위
answer[1] = second < 5 ? second + 1 : 6;

 

이때 0개일때와 1개일때는 자동으로 6등이 되니 삼항연산자를 사용하여 계산을 하였다.

 


해당 문제를 풀면서 직접적으로 코드를 풀어보고 싶어 최대한 풀어서 작성해 보았다.

 

무엇보다 순위 계산을 할때 min과 max를 사용하여 연산하는 방법도 있는것을 확인하고 더 가독성이 좋은 방법을 고민해보아야겠다는 반성을 하였다.

728x90

+ Recent posts