KT-A 7주차-2 회고록 / AIVLE_DAY 01

Mini·2026년 5월 26일

KT-A

목록 보기
11/12
post-thumbnail

들어가며

이번 글에서는 01에이블데이를 보냈던 기록을 남겨보려고 한다. (에이블데이 진행 직후, 기억나는 내용들을 급하게 임시 저장 형태로 작성하고 이후 내용을 다시 정리해 업로드해 늦게 업로드가 되었다.. 참고로 썸네일은 코딩테스트 후 나의 모습과 비슷해 보여서 선정했다...)

이번 01 에이블데이는 Step1이 끝난 시점에 진행되었다. 오전에는 코딩테스트를 치르고, 오후에는 자소서 특강과 함께 각 반별 랜선 회식 시간을 가졌다.
약 한 달이 넘는 시간 동안 세 번의 미니 프로젝트와 공모전도 제출하고 코딩테스트 문제를 풀며 달려왔고, 어느새 하나의 단계가 마무리되었다. 이번 회고록에서는 오전에 진행했던 코딩테스트문제에 대해 집중해 나의 기억을 더듬어 작성해보겠다.


코딩테스트


문제는 총 3문제로, 제한 시간은 2시간이었다. 문제 내용은 유출이 금지되어 있기 때문에 구체적으로 다루지는 못하지만, 이번 글에서는 문제를 풀며 느꼈던 점과 접근 방식, 그리고 아쉬웠던 부분들에 대해 이야기해보려고 한다. (이번 코딩테스트는 파이썬을 선택해 진행했다.)

1번 : 2차원 배열 시뮬레이션과 차원 축소

1번 문제에서는 주어진 조건에 따라 2차원 배열의 크기를 점진적으로 줄여나가는 전형적인 구현 및 시뮬레이션 문제였다.

Main : 배열 시뮬레이션과 풀링

이 문제의 핵심은 2차원 배열의 행과 열 길이를 비교하여 분기를 나누고, 인접한 원소들을 특정 기준(최댓값 또는 최솟값)으로 압축하는 것이다. 이는 컴퓨터 비전이나 딥러닝에서 이미지의 차원을 축소하고 주요 특징을 추출할 때 사용하는 풀링 연산, 그중에서도 특정 축을 기준으로 값을 줄여나가는 과정과 매우 유사하다.

가장 대표적인 예로 맥스 풀링이 있다. 지정된 윈도우 크기(예: 1x2 또는 2x2) 내에서 가장 큰 값만 남기고 나머지는 버리는 방식이다.

개념 / 파이썬을 활용한 1차원 풀링

## 예시 
# 특정 축(가로 또는 세로)을 기준으로 인접한 두 값을 압축(Sum Pooling)하는 예시

def axis_based_pooling(arr, axis):
    row_len = len(arr)
    col_len = len(arr[0])
    
    if axis == 'horizontal': 
        # row을 순회하며 인덱스를 2칸씩 건너뛰어 압축
        return [[row[j] + row[j+1] for j in range(0, col_len, 2)] for row in arr]
    elif axis == 'vertical': 
        # col을 고정하고 행 인덱스를 2칸씩 건너뛰어 압축
        return [[arr[i][j] + arr[i+1][j] for j in range(col_len)] for i in range(0, row_len, 2)]

sample_matrix = [
    [1, 2, 3, 4],
    [5, 6, 7, 8]
]

# 1. 가로축 기준 압축 결과 (열의 개수가 절반으로 줄어듦)
# [[1+2, 3+4], [5+6, 7+8]] => [[3, 7], [11, 15]]
print(axis_based_pooling(sample_matrix, 'horizontal'))

# 2. 세로축 기준 압축 결과 (행의 개수가 절반으로 줄어듦)
# [[1+5, 2+6, 3+7, 4+8]] => [[6, 8, 10, 12]]
print(axis_based_pooling(sample_matrix, 'vertical'))

이번 테스트 문제는 이러한 차원 축소의 개념을 2차원 배열의 가로, 세로 비율에 따라 유동적으로(가로 방향 축소 또는 세로 방향 축소) 적용하는 것이 핵심이였다고 생각한다.

나의 접근법

문제를 해결하기 위해 나는 파이썬의 기능인 List Comprehension을 적극 활용했다. 지정된 횟수만큼 반복문을 수행하되, 루프 시작 지점에서 현재 배열의 열과 행 길이를 측정했다. 만약 배열이 더 이상 줄어들 수 없는 1x1 크기가 되었다면, 불필요한 반복 연산을 막기 위해 즉시 탈출하도록 예외 처리를 두었다.

가로가 세로보다 크거나 같을 때 행(row) 단위로 순회하며, 인덱스를 2칸씩 건너뛰며 인접한 열의 원소들을 비교해 최댓값을 추출했다.
세로가 길 때 -> 열 단위로 인덱스를 고정한 채, 인접한 행의 원소들을 비교해 최솟값을 추출하도록 구현했다.

코드 분석

리스트 컴프리헨션을 사용하여 2중 배열의 구조 변경을 단 한 줄의 코드로 처리했고, 변수 상태가 꼬일 여지를 차단했다. 특히 range(0, length, 2) 방식으로 인덱스를 제어한 것은 배열의 길이가 항상 짝수(혹은 2의 거듭제곱 꼴)로 떨어지는 제약 조건 내에서 인덱스 초과(Index Out of Bounds) 오류를 방지하는 좋은 접근이였다고 생각한다. 다만, 최적화 관점에서 내 코드를 분석해 볼 필요는 있다.
현재 방식은 매 반복 단계마다 arr = [...] 형태로 완전히 새로운 2차원 리스트를 메모리에 반복해서 동적 할당하고 있다. 파이썬 내부적으로 이전 배열은 가비지 컬렉터가 처리해주겠지만, 만약 입력 배열의 초기 크기가 어마어마하게 크고 변환 횟수가 많았다면 잦은 메모리 할당과 해제로 인해 오버헤드가 발생했을 것이다.
만약 메모리 제한이 극단적으로 빡빡한 환경이었다면, 새로운 배열을 생성하는 대신 원본 배열은 그대로 둔 채 접근해야 하는 인덱스의 범위(투 포인터 등)만 절반씩 좁혀가는 In-place 덮어쓰기 방식을 고민해야 했을 것이다. 하지만 주어진 코딩테스트의 제한 사항과 시간 복잡도 내에서는, 개발 시간 단축과 버그 방지를 위해 직관적인 배열 재생성 로직을 선택한 것이 훌륭한 트레이드오프였다고 생각한다.


2번 : 다중 라운드 상태 추적과 엣지 케이스

2번 문제는 시간이 흐름에 따라 변화하는 객체들의 상태를 추적하고, 규칙 위반 여부를 검증하는 시뮬레이션 문제였다.

Main : 상태 머신과 이력 관리

이 문제의 핵심은 여러 주체가 매 라운드마다 특정 대상과 상호작용을 시도할 때, '현재의 선택'이 '과거의 이력(직전 상태)'에 의해 제약받는다는 점이다.
이와 유사한 로직을 자주 접할 수 있다. 예를 들어, 사용자가 비밀번호를 변경할 때 '최근 3번 이내에 사용한 비밀번호는 사용할 수 없다'는 제약을 걸거나, 스케줄링 시스템에서 '이틀 연속 동일한 근무자와 짝을 이룰 수 없다'는 조건을 검증하는 로직이 이에 해당한다.

개념 / 파이썬을 활용한 과거 이력 기반 유효성 검사

문제 유출을 피하기 위해, 이와 본질적으로 동일한 구조를 가진 연속 파트너 지목 제한 로직을 간단한 파이썬 코드로 구현했다.

## 예시

def check_rule_violations(current_choices, last_partners):
    violations = 0
    valid_matches = {}
    
    for person, target in current_choices.items():
        # 규칙 1: 자기 자신을 지목할 수 없음
        if person == target:
            violations += 1
            continue
        # 규칙 2: 직전 라운드에서 짝이었던 사람을 다시 지목할 수 없음
        if last_partners.get(person) == target:
            violations += 1
            continue
        # 서로 지목하여 매칭이 성사된 경우를 확인 
        if current_choices.get(target) == person:
            valid_matches[person] = target
            
    return violations, valid_matches

# 1라운드 결과 (A-B 매칭)
history = {'A': 'B', 'B': 'A', 'C': None}

# 2라운드 선택 (A가 직전 파트너인 B를 또 지목하여 위반 발생)
round_2_choices = {'A': 'B', 'B': 'C', 'C': 'B'}

errors, new_history = check_rule_violations(round_2_choices, history)
print(f"위반 횟수: {errors}") # 결과: 위반 횟수 1

이 문제는 위 예시처럼 각 라운드의 선택을 순회하며 조건을 확인하고, 다음 라운드 검증을 위해 history를 정확히 갱신해 주는 것이 목표였다.

나의 접근법

나는 이 문제를 해결하기 위해 파이썬의 Dictionary를 활용하여 각 주체의 상태를 관리했다.
last_partner라는 딕셔너리를 초기화하여 이전 라운드의 결과를 저장할 공간을 만들었다.
각 라운드마다 반복문을 돌며, 현재 지목한 target이 자기 자신이거나 직전 파트너(last_partner[me])인지 검사하여 위반 횟수를 누적했다.
서로 지목하여 조건이 맞는 경우에만 next_partner 딕셔너리에 매칭 결과를 임시 저장하고, 라운드가 끝날 때 last_partner를 next_partner로 덮어씌워 상태를 갱신했다.

코드 분석

기본적인 로직의 뼈대는 맞았지만, 결과적으로 100점 코드 결과가 나오지 못한 이유는 행위와 결과 상태의 분리가 완벽하지 않았고, 이로 인해 연쇄적인 엣지 케이스를 놓쳤기 때문이라고 생각한다. 복기해 보면 내 코드에는 문제가 있었다.
나는 서로를 올바르게 지목했을 때만(curr_choices.get(target) == me) next_partner에 기록을 남겼다. 그런데 만약 다대다 관계에서 누군가 한 명이라도 규칙을 어겼다면 어떻게 될까? A와 B가 서로를 지목했지만 A가 과거 이력 규칙을 어겨서 무효가 되었다면, A를 지목했던 B의 상태는 '매칭 실패'로 처리되어 다음 라운드에 반영되어야 한다. 즉, 특정 주체의 규칙 위반이 얽혀있는 다른 주체들에게 미치는 '연쇄 작용'을 제대로 끊어내지 못했다. 복잡한 엇갈림 속에서 매칭이 실패했음에도 불구하고, 과거의 엉뚱한 데이터를 참조하게 되어 특정 테스트 케이스에서 위반 횟수가 누락되거나 과다 측정된 것이다.

향후 개선 방향

  • 상태 전이도 분리:
    '현재 선택 확인' -> '위반자 색출 및 필터링' -> '유효한 선택자들 간의 매칭 성사 확인' -> '결과 상태 갱신'이라는 단계를 명확한 함수나 블록으로 쪼개어 결합도를 낮춰야 한다.
  • 테스트 주도 접근:
    코딩을 시작하기 전에, 문제 지문에 나오지 않은 최악의 엇갈림 상황을 종이에 손으로 그려가며 상태표가 어떻게 변해야 하는지 먼저 검증해야겠다.

공부합시다 --- 출처/참고
https://kr.linkedin.com/pulse/state-machine-design-pattern-concepts-examples-python-sajad-rahimi?tl=ko
https://cloudjini.tistory.com/entry/%F0%9F%93%8C-%EC%BD%94%EB%94%A9%ED%85%8C%EC%8A%A4%ED%8A%B8-%ED%85%8C%EC%8A%A4%ED%8A%B8%EC%BC%80%EC%9D%B4%EC%8A%A4-%EC%A0%9C%EB%8C%80%EB%A1%9C-%EC%9D%B4%ED%95%B4%ED%95%98%EA%B3%A0-%EA%B0%80%EC%9E%90
https://blog.naver.com/kmh03214/221685090465
https://wikidocs.net/blog/@jaehong/12397/


3번 : 이진 트리의 구조적 한계와 Bottom-Up 최적화

이진 트리의 성질을 다루는 문제였는데, 직관적인 탐색(Top-Down) 로직 함정에 낚였다... '우선순위(삭제 최소화 > 추가 최소화)'라는 제약 조건이 알고리즘의 방향성을 어떻게 완전히 뒤집어야 하는지 깨닫게 해준 좋은 문제였던거같다.

Main : 이진 트리의 종속성과 Bottom-Up 용량 전파

이 문제의 본질은 일반적인 그래프 탐색이 아니라, 이진 트리가 가지는 수학적 용량의 종속성을 해결하는 것이다.
이진 트리에서 부모 노드 1개는 최대 2개의 자식 노드만 가질 수 있다. 이를 역으로 생각하면, 특정 레벨(L)에 N개의 노드가 존재하려면, 그 바로 위 레벨(L-1)에는 최소한 N/2\lceil N/2 \rceil개의 부모 노드가 반드시 존재해야 한다는 뜻이다.

개념 / 파이썬을 활용한 하위 종속성 해결 (Bottom-Up)

문제 유출을 방지하기 위해, 이진 트리 구조에서 하위 레벨의 노드들을 유지하기 위해 각 상위 레벨에서 최소 몇 개의 노드가 필요한지 역산하는 핵심 개념을 파이썬 코드로 구현했다.

import math

# 각 레벨(깊이)별 최소 필요 노드 수를 바텀업으로 계산하는 예시
def calculate_minimum_required_nodes(node_counts):
    max_level = len(node_counts) - 1
    required = [0] * (max_level + 1)
    required[max_level] = node_counts[max_level]
    
    # 가장 아래 레벨부터 루트를 향해 Bottom-Up으로 순회
    for L in range(max_level - 1, 0, -1):
        # 하위 레벨(L+1)의 노드들을 모두 수용하기 위해 필요한 최소 부모 수
        min_needed = math.ceil(required[L+1] / 2)
        # 실제 존재하는 노드 수와 비교하여, 더 큰 값을 현재 레벨의 필요량으로 확정
        required[L] = max(node_counts[L], min_needed)
        
    return required

sample_levels = [0, 1, 0, 4] # 인덱스가 레벨, 값은 해당 레벨의 노드 수
# 레벨 3에 4개의 노드가 있다면, 레벨 2에는 최소 2개의 노드가 필요하다.
# 결과: [0, 1, 2, 4] -> 빈 레벨이었던 레벨 2에 최소 2개가 필요함이 전파됨
print(calculate_minimum_required_nodes(sample_levels))

이처럼 하위 레벨의 데이터가 상위 레벨의 최소 조건을 강제하는 로직이 이 문제의 핵심 키였다.

나의 접근법

나는 주어진 노드들의 레벨 분포를 카운팅한 뒤, 루트(레벨 1)부터 시작하여 아래로 내려가는 탑다운(Top-Down) 방식으로 접근했다.
빈 레벨이 나타나면 노드를 1개 추가(added += 1)하여 연결을 유지했다. 현재 레벨의 노드 수 곱하기 2(current_node * 2)를 다음 레벨의 최대 허용치로 설정했다.다음 레벨의 노드 수가 허용치를 초과하면, 그 초과분만큼을 삭제(removed += excess)했다.
언뜻 보면 문제의 조건(빈 레벨 금지, 부모 당 자식 최대 2개)을 모두 지킨 것 같지만, 여기서 1순위, 노드 삭제를 최소한으로 할 것'이라는 가장 중요한 조건을 위배하는 실수를 저질러 버렸다..

  • 무엇이 문제였을까?
    가정해보자. 레벨 2에는 노드가 0개이고, 레벨 3에는 노드가 4개 존재한다.

나의 탑다운 로직: 레벨 2가 비었으므로 노드를 1개만 추가한다. 그러면 레벨 3의 최대 허용치는 2개(1*2)가 된다. 결국 레벨 3에 있던 4개의 노드 중 2개를 강제로 삭제해야 한다. (결과: 추가 1, 삭제 2)

정답 로직: 레벨 3의 노드 4개를 '삭제 없이' 살리기 위해 역산해보면, 레벨 2에는 최소 2개의 노드가 필요하다. 따라서 레벨 2에 노드를 2개 추가하면 레벨 3에서는 아무것도 삭제하지 않아도 된다. (결과: 추가 2, 삭제 0)

문제의 1순위 조건은 삭제의 최소화로 기억한다. 나의 코드는 상위 레벨에서 노드를 최소한(1개)으로 추가하려다가 극심한 병목을 만들어버렸고, 그 결과 하위 레벨에서 대량의 노드가 삭제되는 문제가 발생했다.

향후 개선 방향

  • 데이터가 위에서 아래로 흐르는지, 아니면 밑바닥의 조건이 위를 강제하는지 코딩 전에 확실히 설계해야 한다. 최소 삭제가 목표라면, 지켜야 할 대상(가장 많은 노드가 포진된 하위 레벨)부터 거꾸로 올라가며 필요 인프라(부모 노드)를 구축하는 것이 맞다.
  • 그리디(Greedy) 알고리즘의, 특정 시점(빈 레벨)에서 당장 최적이라고 생각한 선택(1개만 추가)이 전체의 최적해를 망칠 수 있음을 항상 경계해야 한다.

공부합시다 --- 참고/출처
https://wing-beat.tistory.com/130
https://cdragon.tistory.com/entry/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0%EC%99%80-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-Greedy-Algorithms
https://as-ps.tistory.com/78
https://velog.io/@yoon_0/%EC%9E%90%EB%A3%8C%EA%B5%AC%EC%A1%B0-%EC%95%8C%EA%B3%A0%EB%A6%AC%EC%A6%98-%EC%9D%B8%EB%8D%B1%EC%8A%A4-%ED%8A%B8%EB%A6%AC-Top-Down-%EB%B0%A9%EC%8B%9D%EA%B3%BC-Bottom-Up-%EB%B0%A9%EC%8B%9D


자기소개서 특강

기업이 포트폴리오를 통해 확인하고자 하는 것은 지원자가 몇 개의 언어와 프레임워크를 아는지가 아니라, 실제 실무에 투입되었을 때 마주할 비즈니스 문제를 어떻게 해결할 것인가에 대한 능력이었다. 특강을 통해 배운 포트폴리오 작성의 핵심 원리와 흐름을 내 관점에서 정리해본다.

1. 포트폴리오의 진짜 목적은? -> 재사용 가능한 역량 증명

가장 크게 와닿았던 핵심 개념은 포트폴리오는 시험 점수처럼 나의 스펙을 나열하는 곳이 아니라는 점이다. 기업은 '적합하지 않은 인재를 채용할 두려움'을 강하게 가지고 있다. 따라서 우리는 포트폴리오를 통해 재사용 가능한 역량을 보여주어야 한다.
재사용 가능한 역량이란, 과거의 프로젝트에서 어떤 비즈니스 요구사항(Why)을 바탕으로 어떤 제약사항 속에서 문제를 정의(What)하고, 어떤 트레이드오프(How)를 거쳐 최종 구현을 해냈는지를 논리적으로 설명할 수 있는 능력이다. 이를 증명하면 채용 담당자는 "이 지원자는 우리 회사에 와서도 비슷한 방식으로 문제를 잘 해결하겠구나"라고 판단하게 된다.

2. 기업 문제 중심의 포트폴리오 설계

내 프로젝트를 무작정 포장하기 전에, 지원하고자 하는 기업이 현재 어떤 문제를 겪고 있는지부터 파악해야 한다. 이를 위한 가장 좋은 방법은 채용 공고(JD) 분석과 기업에서 제공하는 기술 블로그 분석이다.
적어도 10개 이상의 채용 공고를 분석하여 해당 직무의 핵심 과제, 성과 지표, 필수 경험, 그리고 요구되는 기본기를 도출해야 한다. 또한 타겟 기업의 기술 블로그를 읽으며 현업 개발자들이 어떤 제약 상황 속에서 어떤 아키텍처를 선택했고, 왜 그런 결정을 내렸는지(트레이드오프)에 대한 고민을 내 포트폴리오에 자연스럽게 녹여내야 한다.

3. 프로젝트 경험 구조화 (어떻게 작성할 것인가)

프로젝트를 소개할 때는 단순히 "무엇을 만들었다"가 아니라 철저히 문제 해결 중심으로 구조화해야 한다.

  • 기술 스택 선정의 이유
    가장 많이 하는 실수가 사용한 툴을 아이콘으로 줄줄이 나열만 하는 것이다. 면접관이 궁금한 것은 어떤 기술을 썼는지가 아니라 왜 하필 그 기술을 선택했는지다. 예를 들어 단순한 데이터 처리에 무거운 프레임워크를 썼다면 오버엔지니어링으로 보일 수 있다. 현재 프로젝트의 상황, 팀원의 숙련도, 성능상의 이점 등 명확한 도입 목적과 트레이드오프 판단 근거를 적어야 한다.

  • 핵심 기능과 문제 해결 과정
    기능을 설명할 때는 반드시 문제 → 영향 → 해결 결과의 흐름을 따라야 한다.
    특히 많은 사람들이 누락하는 것이 영향 부분이다. 내가 마주한 에러나 병목 현상이 '비즈니스나 사용자 경험에 어떤 치명적인 영향을 미칠 수 있었는지'를 명시해야 내가 비즈니스를 이해하고 코드를 짜는 개발자임을 어필할 수 있다.

  • 성능 개선과 회고
    개선 전후의 지표를 수치화(예: 응답시간 3초 → 1초로 66% 개선)하여 보여주는 것이 중요하다. 프로젝트가 끝난 후에는 한계를 분석하고, "운영 안정성 측면에서 이러한 부분이 부족했기에 향후에는 이 아키텍처를 도입해 실시간 안정성을 높이겠다"는 식의 향후 개선 방향을 적어야 지속적으로 성장하는 인재임을 증명할 수 있다.

4. 포트폴리오 작성 도구와 검토 전략

결국 내 포트폴리오를 읽는 사람은 실무 관리자이거나 최종 결정권자다. 이들은 단순한 코딩 스킬을 넘어 시스템을 확장하고 운영할 수 있는 안정성을 본다.

  • Notion 활용: 정보가 한눈에 들어오도록 3단 위계(표지 → 프로젝트 목록 DB → 개별 상세)로 구성하고, Toggle이나 Callout을 활용해 핵심만 노출시켜 가독성을 극대화해야 한다.
  • GitHub 활용: README는 한 편의 훌륭한 기술 보고서가 되어야 한다. 아키텍처 다이어그램 한 장이 코드 백 줄보다 낫다. 의미 단위의 커밋 컨벤션과 CI/CD 구축 흔적으로 코드 품질과 협업 능력을 증명해야 한다.

5. 포트폴리오 작성의 비교

특강에서 제시된 사례들을 비교해보면, 합격하는 포트폴리오와 탈락하는 포트폴리오의 차이가 명확하게 드러난다.

구분단순 기술 나열형문제 해결형
프로젝트명쇼핑몰 백엔드 API 개발동시성 이슈 해결 - 분산 락 기반 선착순 쿠폰 발급 API
문제 정의구현해야 할 API가 많았음쿠폰 발급 시 초당 3,000건 요청으로 DB Lock 충돌 및 중복 발급 0.4% 발생
해결 과정Java, Spring, Redis, MySQL 등 다양한 기술을 사용해 개발함Redis Lua 스크립트로 재고 차감, Redisson 분산 락 도입으로 DB 부하 분리
결과정상적으로 작동하며, 리뷰에서 좋은 평가를 받음중복 발급 0%, 최대 TPS 4배 향상(3,000→12,000) 및 응답시간 단축

정리해보자면

결국 좋은 포트폴리오란

"나는 주어진 스펙대로만 코딩하는 단순 구현자가 아니라, 비즈니스의 제약사항을 이해하고 최적의 기술적 타협점(트레이드오프)을 찾아내어 시스템을 개선할 수 있는 문제 해결사입니다." 라는 메시지를 던지는 매체다. 이번 특강을 바탕으로 내 과거 프로젝트들을 단순히 '무엇을 만들었나'가 아닌 '어떤 문제를 어떻게 해결했나'의 관점에서 전면 재작성해 이력서를 정리해보자!


랜선회식

반에서 간단하게 에이블기간동안 한 핵심단어 퀴즈 맞추기 및 단원 정리를 진행 (레크레이션 시간이랄까..)

그림보고 키워드 맞추기

정답 실패..

단체 컷

선물 감사합니다 111

선물 감사합니다 222

모든 9기 에이블러님들 고생하셨습니다 -_-

profile
무지(無知)

0개의 댓글