스파르타 Java 단기 심화 과정


코드카타


프로그래머스 118666

https://school.programmers.co.kr/learn/courses/30/lessons/118666

— 문제 설명

나만의 카카오 성격 유형 검사지를 만들려고 합니다.

성격 유형 검사는 다음과 같은 4개 지표로 성격 유형을 구분합니다. 성격은 각 지표에서 두 유형 중 하나로 결정됩니다.

지표 번호성격 유형
1번 지표라이언형(R), 튜브형(T)
2번 지표콘형(C), 프로도형(F)
3번 지표제이지형(J), 무지형(M)
4번 지표어피치형(A), 네오형(N)

4개의 지표가 있으므로 성격 유형은 총 16(=2 x 2 x 2 x 2)가지가 나올 수 있습니다. 예를 들어, "RFMN"이나 "TCMA"와 같은 성격 유형이 있습니다.

검사지에는 총 n개의 질문이 있고, 각 질문에는 아래와 같은 7개의 선택지가 있습니다.

  • 매우 비동의
  • 비동의
  • 약간 비동의
  • 모르겠음
  • 약간 동의
  • 동의
  • 매우 동의

각 질문은 1가지 지표로 성격 유형 점수를 판단합니다.

예를 들어, 어떤 한 질문에서 4번 지표로 아래 표처럼 점수를 매길 수 있습니다.

선택지성격 유형 점수
매우 비동의네오형 3점
비동의네오형 2점
약간 비동의네오형 1점
모르겠음어떤 성격 유형도 점수를 얻지 않습니다
약간 동의어피치형 1점
동의어피치형 2점
매우 동의어피치형 3점

이때 검사자가 질문에서 약간 동의 선택지를 선택할 경우 어피치형(A) 성격 유형 1점을 받게 됩니다. 만약 검사자가 매우 비동의 선택지를 선택할 경우 네오형(N) 성격 유형 3점을 받게 됩니다.

위 예시처럼 네오형이 비동의, 어피치형이 동의인 경우만 주어지지 않고, 질문에 따라 네오형이 동의, 어피치형이 비동의인 경우도 주어질 수 있습니다.

하지만 각 선택지는 고정적인 크기의 점수를 가지고 있습니다.

  • 매우 동의나 매우 비동의 선택지를 선택하면 3점을 얻습니다.
  • 동의나 비동의 선택지를 선택하면 2점을 얻습니다.
  • 약간 동의나 약간 비동의 선택지를 선택하면 1점을 얻습니다.
  • 모르겠음 선택지를 선택하면 점수를 얻지 않습니다.

검사 결과는 모든 질문의 성격 유형 점수를 더하여 각 지표에서 더 높은 점수를 받은 성격 유형이 검사자의 성격 유형이라고 판단합니다. 단, 하나의 지표에서 각 성격 유형 점수가 같으면, 두 성격 유형 중 사전 순으로 빠른 성격 유형을 검사자의 성격 유형이라고 판단합니다.

질문마다 판단하는 지표를 담은 1차원 문자열 배열 survey와 검사자가 각 질문마다 선택한 선택지를 담은 1차원 정수 배열 choices가 매개변수로 주어집니다. 이때, 검사자의 성격 유형 검사 결과를 지표 번호 순서대로 return 하도록 solution 함수를 완성해주세요.

— 제한 조건

  • 1 ≤ survey의 길이 ( = n) ≤ 1,000
    • survey의 원소는 "RT", "TR", "FC", "CF", "MJ", "JM", "AN", "NA" 중 하나입니다.
    • survey[i]의 첫 번째 캐릭터는 i+1번 질문의 비동의 관련 선택지를 선택하면 받는 성격 유형을 의미합니다.
    • survey[i]의 두 번째 캐릭터는 i+1번 질문의 동의 관련 선택지를 선택하면 받는 성격 유형을 의미합니다.
  • choices의 길이 = survey의 길이
    • choices[i]는 검사자가 선택한 i+1번째 질문의 선택지를 의미합니다.

    • 1 ≤ choices의 원소 ≤ 7

      choices
      1매우 비동의
      2비동의
      3약간 비동의
      4모르겠음
      5약간 동의
      6동의
      7매우 동의

— 입출력 예

surveychoicesresult
["AN", "CF", "MJ", "RT", "NA"][5, 3, 2, 7, 5]"TCMA"
["TR", "RT", "TR"][7, 1, 3]"RCJA"

입출력 예 #1

1번 질문의 점수 배치는 아래 표와 같습니다.

선택지성격 유형 점수
매우 비동의어피치형 3점
비동의어피치형 2점
약간 비동의어피치형 1점
모르겠음어떤 성격 유형도 점수를 얻지 않습니다
약간 동의네오형 1점
동의네오형 2점
매우 동의네오형 3점

1번 질문에서는 지문의 예시와 다르게 비동의 관련 선택지를 선택하면 어피치형(A) 성격 유형의 점수를 얻고, 동의 관련 선택지를 선택하면 네오형(N) 성격 유형의 점수를 얻습니다.

1번 질문에서 검사자는 약간 동의 선택지를 선택했으므로 네오형(N) 성격 유형 점수 1점을 얻게 됩니다.

2번 질문의 점수 배치는 아래 표와 같습니다.

선택지성격 유형 점수
매우 비동의콘형 3점
비동의콘형 2점
약간 비동의콘형 1점
모르겠음어떤 성격 유형도 점수를 얻지 않습니다
약간 동의프로도형 1점
동의프로도형 2점
매우 동의프로도형 3점

2번 질문에서 검사자는 약간 비동의 선택지를 선택했으므로 콘형(C) 성격 유형 점수 1점을 얻게 됩니다.

3번 질문의 점수 배치는 아래 표와 같습니다.

선택지성격 유형 점수
매우 비동의무지형 3점
비동의무지형 2점
약간 비동의무지형 1점
모르겠음어떤 성격 유형도 점수를 얻지 않습니다
약간 동의제이지형 1점
동의제이지형 2점
매우 동의제이지형 3점

3번 질문에서 검사자는 비동의 선택지를 선택했으므로 무지형(M) 성격 유형 점수 2점을 얻게 됩니다.

4번 질문의 점수 배치는 아래 표와 같습니다.

선택지성격 유형 점수
매우 비동의라이언형 3점
비동의라이언형 2점
약간 비동의라이언형 1점
모르겠음어떤 성격 유형도 점수를 얻지 않습니다
약간 동의튜브형 1점
동의튜브형 2점
매우 동의튜브형 3점

4번 질문에서 검사자는 매우 동의 선택지를 선택했으므로 튜브형(T) 성격 유형 점수 3점을 얻게 됩니다.

5번 질문의 점수 배치는 아래 표와 같습니다.

선택지성격 유형 점수
매우 비동의네오형 3점
비동의네오형 2점
약간 비동의네오형 1점
모르겠음어떤 성격 유형도 점수를 얻지 않습니다
약간 동의어피치형 1점
동의어피치형 2점
매우 동의어피치형 3점

5번 질문에서 검사자는 약간 동의 선택지를 선택했으므로 어피치형(A) 성격 유형 점수 1점을 얻게 됩니다.

1번부터 5번까지 질문의 성격 유형 점수를 합치면 아래 표와 같습니다.

지표 번호성격 유형점수성격 유형점수
1번 지표라이언형(R)0튜브형(T)3
2번 지표콘형(C)1프로도형(F)0
3번 지표제이지형(J)0무지형(M)2
4번 지표어피치형(A)1네오형(N)1

각 지표에서 더 점수가 높은 T,C,M이 성격 유형입니다.

하지만, 4번 지표는 1점으로 동일한 점수입니다. 따라서, 4번 지표의 성격 유형은 사전순으로 빠른 A입니다.

따라서 "TCMA"를 return 해야 합니다.

입출력 예 #2

1번부터 3번까지 질문의 성격 유형 점수를 합치면 아래 표와 같습니다.

지표 번호성격 유형점수성격 유형점수
1번 지표라이언형(R)6튜브형(T)1
2번 지표콘형(C)0프로도형(F)0
3번 지표제이지형(J)0무지형(M)0
4번 지표어피치형(A)0네오형(N)0

1번 지표는 튜브형(T)보다 라이언형(R)의 점수가 더 높습니다. 따라서 첫 번째 지표의 성격 유형은 R입니다.

하지만, 2, 3, 4번 지표는 모두 0점으로 동일한 점수입니다. 따라서 2, 3, 4번 지표의 성격 유형은 사전순으로 빠른 CJA입니다.

따라서 "RCJA"를 return 해야 합니다.

— 문제 풀이

class Solution {
    public String solution(String[] survey, int[] choices) {
        StringBuilder sb = new StringBuilder();
        int[] score = {-3,-2,-1,0,1,2,3};
        String[] type = {"RT","CF","JM","AN"};
        String[] reverseType = {"TR","FC","MJ","NA"};
        int[] result = new int[4];

        for(int i =0;i<survey.length;i++) {
            for(int j =0; j< 4 ; j++){
                if(survey[i].equals(type[j])) {
                    result[j] += score[choices[i]-1];
                    break;
                }
                if(survey[i].equals(reverseType[j])) {
                    result[j] -= score[choices[i]-1];
                    break;
                }
            }
        }
        for(int i =0; i<4; i++) {
            if(result[i] > 0) {
                sb.append(type[i].charAt(1));
            }else{
                sb.append(type[i].charAt(0));
            }
        }
        return sb.toString();
    }
}

DB 복제 지연

복제 지연이란?

  • 데이터가 쓰기DB에서 읽기 DB로 복제되는 과정에서 발생하는 시간 지연을 의미
  • 복제 지연으로 인해, 읽기 DB에서 최신 상태의 데이터를 읽지 못하고 이전 상태의 데이터를 읽게 되는 문제가 발생할 수 있음
  • 시스템에서는 일반적으로 다음과 같은 방식으로 DB를 운영함
    • 쓰기 DB : 모든 쓰기 작업을 처리하는 DB. 보통 마스터 DB라 부름
    • 읽기 DB : 읽기 작업만 처리하는 DB. 보통 슬레이브 혹은 레플리카 DB라고 부름. 쓰기 DB에서 복제된 데이터를 사용

문제 발생 예시

  • 쓰기와 읽기 DB가 분리된 시스템에서는 쓰기 작업이 일어난 후, 그 변경 내용이 읽기 DB로 복제되기까기 시간이 걸림. 이 지연으로 인해 다음과 같은 상황이 발생하 수 있음
    1. 쓰기 작업 : App이 Master DB에 새로운 데이터를 저장
    2. 지연 : 이 데이터는 복제 지연 때문에 즉시 Slave DB에 반영되지 않음
    3. 읽기 작업 : 쓰기 작업 직후에 App이 Slave DB에서 데이터를 조회하려 시도
    4. 이전 데이터 조회 : 이 시점에서 Slave DB는 아직 새로운 데이터를 반영하지 않은 상태기 때문에, 이전 데이터를 반환

해결 방법

  • 지연 적용 알고리즘 (Lag Compensating Logic)
    • 쓰기 후 즉시 읽기를 시도하는 경우, 잠시 지연을 두고 읽기를 재시도하는 방법
  • Master DB로의 직접 읽기 (Read-after-Write)
    • 중요한 데이터에대해 쓰기 직후에 즉시 읽어야 하는 경우, 해당 작업을 Master DB에서 직접 수행하도록 함
  • Master - Slave간 복제 지연 최소화
    • 복제 지연을 최소화하도록 DB 설정을 조정.
      • 예시
        • MySQL의 경우 복제 지연을 줄이기 위해 semi-synchronous replication을 사용하거나, 다른 복제 설정을 최적화할 수 있음
    • 물리적인 네트워크 지연이나 시스템 부하 등으로 지연을 완전히 제거할 수는 없음
  • CQRS 패턴
    • Command Query Responsibility Segregation (CQRS) 패턴을 적용하여, 쓰기 작업과 읽기 작업을 명확히 분리하고, 읽기 작업에 대해 일관성을 보장하는 별도의 메커니즘을 적용할 수 있음
      • ex) Master DB의 변경이 Slave에 완전히 반영된 후에 읽기 작업을 허용하는 방식 도입
  • 캐시 사용
    • 캐시 시스템을 도입하여, 쓰기 작업 후 바로 캐시를 갱신하고, 그 이후 읽기 작업은 캐시에서 제공하는 방식을 사용. 캐시는 일관성을 보장하기 위해 일정 시간 동안(ex) 1초) Master DB의 데이터를 캐시하는 방법을 사용할 수 있음

메모리 릭 (Memory Leak)

메모리 릭이란?

  • 프로세스가 더 이상 필요하지 않은 메모리를 할당한 후 이를 해제하지 않아서 해당 메모리가 지속적으로 점유된 상태로 남아있는 현상
  • 메모리 사용량을 지속적으로 증가시켜, 결국에는 시스템의 메모리를 고갈시켜 성능 저하 또는 App의 충돌을 유발할 수 있음

메모리 릭의 일반적인 원인

  • 이벤트 리스너나 콜백의 등록 후 해제되지 않음
  • 전역 변수 사용 : 전역 변수로 객체를 참조하게 되면, 해당 객체가 프로그램의 종료 시점까지 해제되지 않을 수 있음
  • 캐시 관리 실패
  • 잘못된 데이터 구조 관리 : 객체가 필요없어졌을 때 제거하지 않으면 메모리 릭 발생

스프링에서의 메모리 릭

  • 스프링 빈의 라이프사이클 관리 문제
    • 스프링 빈은 Application context가 살아있는 동안 메모리에 유지됨. 만약 빈이 내부적으로 많은 메모리를 점유하는 객체를 가지고 있다면, 객체들이 계속 메모리에 남아 메모리 릭을 유발할 수 있음
  • @Autowired 주입된 객체의 잘못된 관리
    • 예상치 못한 참조 순환을 일으키거나, 의도적으로 제거되지 않으면 메모리 릭을 일으킬 수 있음
  • ThreadLocal의 잘못된 사용
    • 스레드별로 데이터를 저장하는 경우, 스레드가 종료하면 해당 스레드와 관련된 모든 데이터를 제거해야함. 명시적으로 제거하지 않으면 메모리 릭 발생
    • 요청 스코프의 빈을 ThreadLocal로 처리하는 경우가 있는데, 요청이 종료될 때 이 데이터를 적절히 정리해야함
  • 이벤트 리스너의 잘못된 관리
    • 이벤트 리스너 등록 후, 리스너가 더 이상 필요하지 않은 경우 제거해야함

캐시 압력(Cache Pressure)

캐시 압력이란?

  • 캐시 메모리가 부족해지면서 발생하는 문제
  • 캐시가 가득 차서 더 이상 새로운 데이터를 저장할 수 없고, 이로 인해 기존 데이터를 삭제해야하는 상황이 발생할 수 있음
  • 캐시 압력이 증가하면, 캐시 항목을 삭제하거나 새 데이터를 캐시에 저장하는 동안 대기 상태가 발생할 수 있음

캐시 압력 예시

  • 스프링에서 사용자별 페이징 데이터를 캐시에 저장하는 경우, 특히 대규모 사용자와 많은 데이터를 처리하는 시스템에서는 캐시 메모리가 빠르게 소진될 수 있으며, 이는 “캐시 쓰로틀링(Cache Throttling)” 또는 “캐시 압력(Cache Pressure)” 상황을 초래할 수 있음
  • 이러한 상황은 캐시가 가득 차서 새로운 데이터를 저장할 수 없게 되어 요청이 지연되거나, 대기 상태로 빠지는 문제 일으킬 수 있음
  • 문제상황
    1. 데이터량 증가
      • 각 사용자가 고유한 페이징 데이터를 요청할 경우, 캐시에 저장되는 데이터의 양이 빠르게 증가
      • 다양한 페이징 요청(ex) 페이지 번호, 페이지 크기, 필터링 조건 등)에 따라 각기 다른 캐시 항목이 생성되어, 캐시 공간을 많이 차지
    2. 캐시 용량 초과:
      • 캐시는 일반적으로 제한된 메모리 공간을 사용함. 사용자별로 많은 페이징 데이터를 캐시에 저장하면 캐시 용량이 초과될 수 있음
      • 용량 초과로 인해 캐시가 새로운 항목을 수용할 수 없게 되면, 기존 항목을 제거해야 하거나 요청이 대기 상태로 전환될 수 있음
    3. 캐시 히트율 저하:
      • 페이징 데이터를 캐시하는 경우, 사용자가 특정 페이지를 반복적으로 요청하지 않는 한 캐시 히트율이 낮아질 수 있음
      • 낮은 캐시 히트율은 캐시의 효율성을 떨어뜨리며, 메모리 낭비로 이어질 수 있음
    4. 사용자별 캐시 공간 경쟁:
      • 다수의 사용자가 동일한 캐시 리소스를 공유할 때, 사용자별로 많은 데이터를 캐시에 저장하면 캐시 공간이 경쟁 상태가 되어, 중요한 데이터가 자주 제거될 수 있음
      • 이로 인해 필요한 데이터가 캐시에 존재하지 않아, 다시 데이터베이스에서 가져오는 상황이 빈번해질 수 있음
  • 해결방법
    1. 캐시 제한 설정
      • TTL(Time-To-Live) : TTL을 설정하여 캐시 데이터가 일정 시간 후에 자동으로 만료되도록 설정
      • 최대 캐시 크기 설정 : 사용자별 페이징 데이터에 대해 캐시의 최대 크기를 설정하여, 용량이 초과되면 오래된 데이터를 자동으로 제거하도록 할 수 있음
    2. 페이징 전략 최적화
      • 부분적 캐싱 : 모든 페이지를 캐싱하기보다는 자주 요청되는 특정 페이지만을 캐싱하여, 캐시 공간을 효율적으로 사용
      • 데이터베이스 인덱스 최적화 : 캐시를 사용하는 대신, 데이터베이스 인덱스를 최적화하여 페이징 성능을 향상
    3. 분산 캐시 사용
      • Redis와 같은 분산 캐시 시스템을 사용하여, 여러 서버에 캐시 데이터를 분산시킴. 이를 통해 한 서버의 메모리 용량 한계를 극복하고, 캐시 성능을 높일 수 있음
    4. 캐시 우회 전략
      • 특정 조건에서 캐시를 우회하고 직접 데이터베이스에서 데이터를 가져오는 전략을 사용.
        • 예시
          • 페이징 요청이 매우 세분화된 경우 캐시를 우회하도록 설정

캐시 백오프 주의사항

  • 캐시 백오프전략을 사용할 경우, 캐시가 일시적으로 사용되지 않게 되면서 모든 요청이 DB로 직접 전달될 수 있음
  • 이로 인해 DB에 과부화가 발생하고, 성능 저하나 시스템 장애가 일어날 가능성이 있음
  • 이는 특히 캐시가 갑작스럽게 무효화되거나, 캐시가 의도치 않게 사용되지 않게 되는 상황에서 문제가 될 수 있음

설정 버전 관리 (Configuration Versioning)

설정 버전 관리란?

  • App의 설정 데이터를 시간이나 버전별로 관리하는 방법을 의미
  • 최신 설정 데이터를 항상 사용할 수 있도록 각 설정에 버전(혹은 날짜)을 부여하고, 이를 기반으로 최신 설정을 조회하고 적용하는 방식

설정 버전 관리의 장점

  • 빠른 롤백
    • 설정 버전 관리를 통해 이전 버전의 설정 데이터를 보관하면, 장애가 발생했을 때 문제가 있는 최신 설정을 신속하게 이전 버전으로 롤백할 수 있음. 이는 서비스의 가동 중단 시간을 최소화하고, 문제를 빠르게 해결하는 데 큰 도움이 됨
  • 변경 이력 추적
    • 각 설정이 버전별로 관리되기 때문에, 어떤 설정이 언제 변경되었는지 쉽게 추적할 수 있음
    • 문제의 원인을 분석할 때 유용하며, 특정 설정 변경이 문제를 유발했는지 확인할 수 있음
  • 테스트와 배포 용이성
    • 새로운 설정을 적용하기 전에 테스트 환경에서 해당 설정을 미리 적용해보고, 문제가 없다고 판단되면 실제 운영 환경에 적용할 수 있음. 이를 통해 설정 변경으로 인한 장애 발생 가능성을 줄일 수 있음
  • 신속한 복구
    • 장애 발생 시 최신 설정을 빠르게 적용하거나, 문제가 된 설정을 즉시 변경할 수 있음. 또한, 설정의 버전 관리 덕분에 복구 작업이 체계적으로 이루어질 수 있음
  • 일관된 설정 관리
    • 설정 데이터를 체계적으로 관리함으로써, 장애 발생 시에도 일관된 설정을 유지할 수 있음. 이로 인해 설정 오류로 인한 장애를 예방할 수 있음
  • 가시성 향상
    • 설정 변경 사항이 명확하게 기록되고 관리되므로, 모든 팀원이 설정 상태를 명확히 파악할 수 있음. 이는 장애 대응을 위한 의사소통과 협업을 개선함

설정 버전 관리의 단점

  • 복잡성 증가
    • 여러 버전의 설정을 관리하는 것이 복잡성을 초래할 수 있음. 특히 버전 수가 많아질수록 관리가 어려워질 수 있음
  • 저장 공간 부담
    • 여러 버전의 설정 데이터를 저장하는 데 필요한 저장 공간이 늘어나며, 시간이 지남에 따라 상당한 용량을 차지할 수 있음
  • 인적 오류 가능성
    • 잘못된 버전을 관리하거나 롤백할 때 실수가 발생할 수 있어 복구 과정을 복잡하게 만들 수 있음
profile
기록을 남겨보자

0개의 댓글