문제 요약 A팀의 출전 순서는 이미 정해져 있고, B팀은 출전 순서를 자유롭게 정할 수 있다. B팀 선수가 A팀 선수보다 큰 숫자를 낼 때만 1점을 얻는다. B팀이 얻을 수 있는 최대 승점을 구한다. 핵심 아이디어 실제 출전 순서보다 어떤 숫자가 어떤 숫자를 이기는지가 중요하다. 따라서 A와 B를 모두 오름차순 정렬한다. B의 작은 숫자부터 확인하면서, 아직 이기지 않은 A 숫자 중 가장 작은 숫자를 이길 수 있는지 확인한다. B의 숫자 > A의 가장 작은 미매칭 숫자 : 이 A 선수를 이기고 승점을 얻는다. 그렇지 않다: 이 B 선수는 어떤 남은 A 선수도 이길 수 없으므로 승점 없이 넘긴다. 이길 수 있는 B 선수를 더 큰 A 숫자에 쓰면 더 작은 A 숫자를 이길 기회를 잃을 수 있다. 따라서 가능한 가장 작은 A 숫자와 매칭하는 선택이 항상 최적이다. 풀이 과정 A와 B를 오름차순 정렬한다. A에서 아직 이기지 않은 가장 작은 숫자의 인덱스를 a_index 로 둔다. 정렬된 B를 앞에서부터 확인한다. 현재 B 숫자가 A[a_index] 보다 크면 승점을 1 늘리고 a_index 를 다음 A 선수로 옮긴다. 모든 B 선수를 확인한 뒤 승점을 반환한다. Python 코드 def solution(A, B): A.sort() B.sort() score = 0 a_index = 0 for b_number in B: # 현재 B 선수가 가장 작은 미매칭 A 선수를 이길 수 있다. if a_index < len(A) and b_number > A[a_index]: score += 1 a_index += 1 return score 예시 A = [5, 1, 3, 7] -> [1, 3, 5, 7] B = [2, 2, 6, 8] -> [2, 2, 6, 8] B 숫자 이길 A 숫자 승점 2 1 1 2 이길 수 없음 1 6 3 2 8 5 3 따라서 B팀은 최대 3 점을 얻는다. 시간 복잡도 N 을 팀 인원 수라고 하자. A, B 정렬: O(N log N) B 배열 순회: O(N) 시간 복잡도: O(N log N) 공간 복잡도: 정렬 구현에 따라 O(N) 정리 이길 수 있는 B 숫자는 남은 A 숫자 중 가장 작은 수를 이기는 데 사용한다. 이 단순한 그리디 기준을 정렬된 두 배열에 적용하면 B팀의 최대 승점을 구할 수 있다.
유니티에서 그림자 처리를 직접 진행했던 기록을 위한 글 입니다. 커스텀 2D Shadow 커스텀 2D Light 에서 생성한 빛 객체는 그림자를 적용할 수 없던 반쪽자리 빛으로, 여기에 그림자 또한 동작하도록 구현해보았다. 이 그림자 구현에는 기존 커스텀 빛 관련 스크립트와 셰이더를 일부 이용해서 구현해주었다. 왼쪽의 노란 빛과 사과의 그림자가 커스텀 라이트와 섀도우, 오른쪽의 오렌지가 유니티 자체적인 빛과 그림자이다. 관련 코드 ScriptableRendererFeature 이전 빛 처리에 사용했던 Custom2DLightFeature에 그림자 관련 코드들을 넣어서 업데이트 해주었다. //셰이더에서 사용할 패스별 이름 설정 private readonly ShaderTagId m_OccluderShaderTagId = new ShaderTagId("Custom2DOccluder"); private readonly ShaderTagId m_ShadowExtrusionShaderTagId = new ShaderTagId("Custom2DShadowCaster"); //그림자 텍스처를 연결할 프로퍼티 ID 생성 private static readonly int CustomShadowTextureID = Shader.PropertyToID("_CustomShadowTexture"); 그림자 처리에 사용할 두 종류의 셰이더 패스 태그를 준비하고, 이후 완성된 그림자 텍스쳐를 전역으로 등록하기 위한 프로퍼티 ID를 생성해주었다. 이번 그림자 처리에는 빛과 다르게 한번이 아닌 두번의 렌더링이 필요하며, 그를 위한 두 패스 태그와 최종 그림자 텍스쳐를 외부에서 사용할 수 있도록 프로퍼티 ID를 선언해둔 것이다. private class ShadowPassData { public RendererListHandle occluderRendererList; public RendererListHandle shadowExtrusionRendererList; } 같은 이유로, 두개의 렌더러 리스트가 묶여있는 ShadowPassData를 생성하여, 뒤쪽의 실행 단계에서 두번의 렌더링을 진행할 수 있게 준비한다. //기존 커스텀 빛 생성 때의 RecordRenderGraph 메소드 public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) { //기존 빛 관련 내용들.. //그림자가 그려질 그림자 텍스쳐 생성, 기존 빛의 설정(RGB 색상용의 desc) 사용 TextureHandle shadowTexture = UniversalRenderer.CreateRenderGraphTexture( renderGraph, desc, "_CustomShadowTexture", clear: false, filterMode: FilterMode.Bilinear ); //현재 카메라 설정을 가져옴 RenderTextureDescriptor depthDesc = cameraData.cameraTargetDescriptor; //생성할 텍스쳐를 색상(RGB)기반 대신, Depth/Stencil 기반 텍스쳐로 설정 depthDesc.colorFormat = RenderTextureFormat.Depth; depthDesc.depthBufferBits = 24; depthDesc.msaaSamples = 1; //방금 생성한 depthDesc를 사용하는 스텐실 버퍼용 텍스쳐 TextureHandle shadowDepthTexture = UniversalRenderer.CreateRenderGraphTexture( renderGraph, depthDesc, "_CustomShadowDepthTexture", clear: false, filterMode: FilterMode.Point ); //그림자용 래스터 패스 등록, 다음 파트에서 추가 설명 예정 using (var builder = renderGraph.AddRasterRenderPass<ShadowPassData>("Custom 2D Shadow Pass", out var passData)) } 텍스쳐를 생성하고, 패스를 등록하는 RecordRenderGraph 메소드 내부에 그림자 관련 코드들이 추가되었다. 그림자 처리를 위해 텍스쳐가 두개 생성되었는데, 첫번째로는 기존 빛 텍스쳐 생성에 사용됐던 desc 설정값을 이용한, 색상 처리용 그림자 텍스쳐이다. 이 그림자 텍스쳐에는 최종적으로 실제 그림자들이 그려지게 될 예정이다. 그 다음으로는 새롭게 카메라 설정을 가져와서, 색상 기반 대신 Depth/Stencil 기반의 텍스쳐가 될 수 있도록 설정해주었는데, 실제로는 뎁스는 사용하지 않고, 스텐실만 사용해줄 계획이다. 이렇게 설정된 depthDesc를 이용하여, 두번째 텍스쳐인 스텐실 버퍼용 텍스쳐를 생성해주는데, 이 두번째 텍스쳐는 그림자에서 현재 물체 범위를 제외해주기 위해 사용된다. 빛과 빛을 받는 객체에 대해 그림자 생성을 판단할 때, 가장 간단한 방법은 객체의 영역에서 빛을 받았을 때, 그 영역부터 빛의 방향에 따라서 쭉 그림자를 늘어뜨려 주는것이다. 다만 이렇게 되면 빛을 받은 객체 자신 또한 그림자에 휩싸여서 그림자의 색인 검은 계열의 색으로 물들게 된다. 이러한 현상을 방지하기 위해, 두번째 텍스쳐를 이용해서 현 객체에 맞도록 스텐실 버퍼를 생성해주고, 그림자 텍스쳐를 작업하면서 이 스텐실 버퍼 텍스쳐를 참고해서 현재 객체의 범위에는 그림자가 생성되지 않도록 예외처리를 진행해준다. 렌더패스 등록 //렌더패스 등록 using (var builder = renderGraph.AddRasterRenderPass<ShadowPassData>("Custom 2D Shadow Pass", out var passData)) { //작업이 진행될 각 텍스쳐들을 연결해줌 builder.SetRenderAttachment(shadowTexture, 0, AccessFlags.Write); builder.SetRenderAttachmentDepth(shadowDepthTexture, AccessFlags.Write); //렌더러들의 작업 순서 지정(카메라 기본 정렬 기준을 그대로 사용) SortingCriteria sortingCriteria = cameraData.defaultOpaqueSortFlags; //그림자를 생성할 렌더러 필터 설정 FilteringSettings filteringSettings = new FilteringSettings(RenderQueueRange.all, m_Settings.shadowLayerMask); //Custom2DOccluder 패스 태그를 이용한 DrawingSettings 생성 DrawingSettings occluderDrawSettings = RenderingUtils.CreateDrawingSettings( m_OccluderShaderTagId, renderingData, cameraData, lightData, sortingCriteria ); //설정들을 이용해서 첫번째 렌더러 리스트 생성 RendererListParams occluderParams = new RendererListParams(renderingData.cullResults, occluderDrawSettings, filteringSettings); passData.occluderRendererList = renderGraph.CreateRendererList(occluderParams); //Custom2DShadowCaster 패스 태그를 이용한 DrawingSettings 생성 DrawingSettings extrusionDrawSettings = RenderingUtils.CreateDrawingSettings( m_ShadowExtrusionShaderTagId, renderingData, cameraData, lightData, sortingCriteria ); //설정들을 이용해서 두번째 렌더러 리스트 생성 RendererListParams extrusionParams = new RendererListParams(renderingData.cullResults, extrusionDrawSettings, filteringSettings); passData.shadowExtrusionRendererList = renderGraph.CreateRendererList(extrusionParams); //렌더러 리스트 등록 builder.UseRendererList(passData.occluderRendererList); builder.UseRendererList(passData.shadowExtrusionRendererList); //그림자 텍스쳐 전역 등록 builder.SetGlobalTextureAfterPass(shadowTexture, CustomShadowTextureID); builder.AllowPassCulling(false); //실제 동작 등록, 다음 파트에서 추가 설명 예정 builder.SetRenderFunc((ShadowPassData data, RasterGraphContext context) } 렌더 패스는 커스텀 라이트 때의 흐름과 비슷하게 구성되어있다. 먼저 각 작업별 텍스쳐를 등록해주는데, SetRenderAttachment와 SetRenderAttachmentDepth로 등록 작업에 차이가 있는 것을 확인할 수 있다. 이는 렌더 패스에서 작업된 컬러 텍스쳐와 뎁스 텍스쳐를 의미하는것으로, 그에 맞춰 그림자 텍스쳐와 스텐실 버퍼용 텍스쳐를 각각 등록해주었다. 다음으로는 그림자 생성 필터링 설정과 DrawingSettings을 생성하고, 이런 설정들을 통해 각각 렌더러 리스트를 생성해주었다. 각 텍스쳐와 목표에 맞춰서, 그림자용 렌더러 리스트는 Custom2DShadowCaster 패스 태그를 의미하는 m_ShadowExtrusionShaderTagId를, 스텐실용 렌더러 리스트는 Custom2DOccluder 패스 태그를 의미하는 m_OccluderShaderTagId를 지정해서 생성해주었다. 이렇게 생성된 두 리스트를 각각 패스에 등록해주고, 그림자 텍스쳐를 다른 위치에서도 호출할 수 있게 전역으로 등록해준 다음, 작업이 자동으로 제외되지 않게 AllowPassCulling 설정을 해주며, 실제 동작 파트로 넘어간다. builder.SetRenderFunc((ShadowPassData data, RasterGraphContext context) => { //렌더 타겟 전부 초기화(색상은 블랙, 깊이 버퍼는 1, 스텐실 버퍼는 0) context.cmd.ClearRenderTarget(RTClearFlags.All, Color.black, 1.0f, 0); //스텐실 버퍼 기록 context.cmd.DrawRendererList(data.occluderRendererList); //스텐실 버퍼 영역 확인 후, 영역 외에 그림자 영역 지정 context.cmd.DrawRendererList(data.shadowExtrusionRendererList); }); 작업은 총 3단계로, 렌더 타겟을 전부 깔끔하게 초기화 하며 시작한다. 초기화 후, 두번째 작업은 먼저 스텐실 버퍼와 관련된 작업으로, 간단하게 요약하면 오브젝트 범위에 맞춰서 스텐실 버퍼에 데이터를 등록해준다. 마지막인 세번째 작업 또한 간단하게 요약하면, 스텐실 버퍼를 확인해서 그림자 텍스쳐에 그림자 영역을 그려주도록 되어있다. 각 동작들은 셰이더쪽 코드를 통해 동작하기 때문에 자세한건 각 파트에서 설명할 예정이다. 커스텀(그림자) 셰이더 이번 셰이더는 기존 빛 셰이더와 다르게, 두개의 패스로 구성되어있다. 셰이더 공용 코드 Shader "Universal Render Pipeline/2D/Custom2DShadow" { //입력받는 설정값 Properties { //그림자로 이용할 텍스쳐, 그림자의 길이, 컷오프 알파값 기준 _MainTex ("Sprite Texture", 2D) = "white" {} _ShadowLength ("Shadow Extrusion Length", Float) = 5.0 _Cutoff ("Alpha Cutoff", Range(0, 1)) = 0.5 } SubShader { Tags { "Queue" = "Transparent" "RenderType" = "Transparent" "RenderPipeline" = "UniversalPipeline" } //블렌딩, 컬링, 깊이처리 전부 Off Blend Off Cull Off ZWrite Off } //계속해서 설명할 두 패스 Pass {...} Pass {...} } } 이번 셰이더의 공용 코드는 위와 같다. 머티리얼을 통해 전달받는 값은 그림자로 이용할 텍스쳐와, 만들어질 그림자의 길이, 컷 오프 될 알파값 기준이 존재한다. 텍스쳐에 애초에 알파값이 낮아 반투명, 혹은 투명한 부분에 대해 그림자가 생성되면 어색하기 때문에 그에 대한 예외처리를 위해 컷오프 기준값 설정을 전달받는다. 첫번째 패스(스텐실 마스킹) Pass { Name "Custom2DOccluder" //Custom2DOccluder를 통한 호출인 경우 실행되는 패스 Tags { "LightMode" = "Custom2DOccluder" } //렌더 타겟에 색상을 그리지 않음을 의미 ColorMask 0 //스텐실 버퍼 설정 Stencil { Ref 0 Comp Always //항상 통과 Pass IncrSat //스텐실 값 1 증가 } HLSLPROGRAM #include "Packages/com.unity.render-pipelines.universal/Shaders/2D/Include/Core2D.hlsl" #pragma vertex LitVertex #pragma fragment LitFragment #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" //버텍스 셰이더에서 사용 struct Attributes { //오브젝트의 버텍스 위치 float3 positionOS : POSITION; }; //픽셀 셰이더에서 사용 struct Varyings { //버텍스 셰이더에서 처리한 좌표 전달용 float4 positionCS : SV_POSITION; }; //버텍스 셰이더 Varyings LitVertex(Attributes input) { Varyings output; //오브젝트 기반 정점의 좌표를 클립 기반 좌표로 변경해서 전달 output.positionCS = TransformObjectToHClip(input.positionOS); return output; } //픽셀 셰이더는 ColorMask 0으로 인해 의미가 없는 상태 half4 LitFragment(Varyings input) : SV_Target { return 0; } ENDHLSL } 첫번째 패스의 목표는 스텐실 버퍼에 현재 오브젝트의 모양을 기록해두는 것이 목표이기 때문에 별 다른 작업은 진행되지 않는다. 가장 중요한건 스텐실 영역으로, 현재 스텐실 버퍼는 렌더 타겟 초기화로 인해 전부 0으로만 채워져있는 상태이다. 이 셰이더 기반 오브젝트를 렌더링하면서, 이번 패스가 동작할 때, 스텐실 버퍼에 오브젝트 범위에 맞게 1이 채워지게 된다. 광원 좌표 전달 public class CustomLightBinder : MonoBehaviour { private static readonly int CustomLightPosWSID = Shader.PropertyToID("_CustomLightPosWS"); void Update() { // 현재 광원 오브젝트의 월드 좌표를 전역 셰이더 변수로 업데이트 Vector4 lightPosWS = transform.position; Shader.SetGlobalVector(CustomLightPosWSID, lightPosWS); } } 두번째 패스로 넘어가기 전에 짧은 스크립트를 하나 봐야한다. 이 스크립트는 광원에 붙게 되는 스크립트인데, 단순하게 매 프레임 광원의 위치를 계속해서 업데이트 해주는 역할을 담당한다. 광원의 위치는 다음에 설명할 두번째 패스에서 사용된다. 두번째 패스(그림자 마스킹) Pass { Name "Custom2DShadowCaster" //Custom2DShadowCaster를 통한 호출인 경우 실행되는 패스 Tags { "LightMode" = "Custom2DShadowCaster" } //스텐실 버퍼, 현재 픽셀의 스텐실이 1이 아닌 경우에만 작업을 적용한다는 의미 Stencil { Ref 1 Comp NotEqual } HLSLPROGRAM #include "Packages/com.unity.render-pipelines.universal/Shaders/2D/Include/Core2D.hlsl" #pragma vertex LitVertex #pragma fragment LitFragment #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" //메인 텍스쳐와 샘플러 선언 TEXTURE2D(_MainTex); SAMPLER(sampler_MainTex); //CustomLightBinder 스크립트에서 공유중인 실시간 광원 위치 float4 _CustomLightPosWS; //텍스쳐(타일링 / 오프셋), 그림자 길이, 컷오프 기준값 CBUFFER_START(UnityPerMaterial) float4 _MainTex_ST; float _ShadowLength; float _Cutoff; CBUFFER_END //버텍스 셰이더에서 사용 struct Attributes { float3 positionOS : POSITION; float2 uv : TEXCOORD0; //이 버텍스가 어느 인스턴스(객체)에 속해있는지 구별하기 위한 값 UNITY_VERTEX_INPUT_INSTANCE_ID }; //픽셀 셰이더에서 사용 struct Varyings { float4 positionCS : SV_POSITION; float2 uv : TEXCOORD0; UNITY_VERTEX_INPUT_INSTANCE_ID }; Varyings LitVertex(Attributes input) { Varyings output; //현 버텍스의 인스턴스(객체) 정보 세팅 UNITY_SETUP_INSTANCE_ID(input); UNITY_TRANSFER_INSTANCE_ID(input, output); //광원 위치를 오브젝트 공간(Object Space)으로 변환 float3 lightPosOS = TransformWorldToObject(_CustomLightPosWS.xyz); //광원에서 오브젝트 중심(0,0,0)으로 향하는 방향 벡터 //(0, 0, 0) - lightPosOS.xy = -lightPosOS.xy float2 lightToCenterDirOS = -lightPosOS.xy; //광원과 오브젝트 중심 위치가 완전히 겹쳐 0이 될 때의 예외 처리 float dirLen = length(lightToCenterDirOS); if (dirLen < 0.0001) { lightToCenterDirOS = float2(0, 1); //기본 방향 지정 } //두 벡터에 대한 내적 진행(오브젝트 중심 -> 현재 정점 위치, 광원 -> 오브젝트 중심 float facing = dot(input.positionOS.xy, lightToCenterDirOS); //빛을 받는 방향을 구함 float3 positionWS = TransformObjectToWorld(input.positionOS); float2 lightToVertDirWS = positionWS.xy - _CustomLightPosWS.xy; float dist = length(lightToVertDirWS); float2 lightToVertNormWS = lightToVertDirWS / max(dist, 0.0001); //광원 반대편 정점(facing > 0)만 Extrusion 수행 if (facing > 0.0) { //빛을 받는 방향에 맞춰서 그림자 거리 설정만큼 밀어내기 positionWS.xy
문제 요약 중복 없는 학습 단어들이 주어졌을 때, 각 단어를 다른 단어와 구분해 자동완성하려면 몇 글자를 입력해야 하는지 구한다. 모든 단어에 필요한 입력 글자 수의 합을 반환한다. 핵심 아이디어 단어를 사전순으로 정렬하면, 어떤 단어와 가장 긴 접두사를 공유할 수 있는 단어는 정렬된 목록에서 바로 앞 또는 바로 뒤에 있다. 따라서 현재 단어가 필요한 입력 글자 수는 다음처럼 구할 수 있다. max(앞 단어와의 공통 접두사 길이, 뒤 단어와의 공통 접두사 길이) + 1 단, 현재 단어 자체가 다른 단어의 접두사라면 더 입력할 문자가 없으므로 단어 전체를 입력해야 한다. required = min(len(word), longest_common_prefix + 1) 왜 인접한 단어만 비교할까? 사전순 정렬에서 같은 접두사를 가진 단어들은 항상 연속해서 모인다. 예를 들어 word 와 wor... 로 시작하는 단어들은 모두 한 구간에 모인다. 현재 단어와 가장 긴 접두사를 공유하는 단어는 그 구간에서 바로 앞이나 바로 뒤에 있으므로, 두 이웃만 비교하면 충분하다. 풀이 과정 단어 목록을 사전순으로 정렬한다. 각 단어와 앞 단어의 최장 공통 접두사 길이를 구한다. 각 단어와 뒤 단어의 최장 공통 접두사 길이를 구한다. 둘 중 큰 값에 1을 더한다. 현재 단어 길이를 넘지 않도록 제한한 값을 답에 더한다. Python 코드 def common_prefix_length(first, second): limit = min(len(first), len(second)) index = 0 while index < limit and first[index] == second[index]: index += 1 return index def solution(words): words.sort() total = 0 for index, word in enumerate(words): longest_common_prefix = 0 if index > 0: longest_common_prefix = max( longest_common_prefix, common_prefix_length(word, words[index - 1]) ) if index + 1 < len(words): longest_common_prefix = max( longest_common_prefix, common_prefix_length(word, words[index + 1]) ) # word가 다른 단어의 접두사인 경우에는 단어 전체를 입력해야 한다. total += min(len(word), longest_common_prefix + 1) return total 예시 words = ["go", "gone", "guild"] 는 이미 사전순으로 정렬되어 있다. 단어 이웃 단어와의 가장 긴 공통 접두사 필요한 입력 go go (gone과 공유) 2 gone go 3 guild g 2 go 는 gone 의 접두사다. 공통 접두사 길이는 2이지만 go 뒤에 입력할 글자가 없으므로, 단어 전체인 2글자를 입력해야 한다. 총 입력 글자 수는 2 + 3 + 2 = 7 이다. 시간 복잡도 N 을 단어 개수, L 을 모든 단어 길이의 합, M 을 가장 긴 단어 길이라고 하자. 단어 정렬: 최대 O(N log N * M) 인접 단어 공통 접두사 계산: 각 단어는 앞뒤 단어와만 비교하므로 O(L) 시간 복잡도: O(N log N * M + L) 공간 복잡도: 정렬 구현을 포함해 O(N) 정리 자동완성에 필요한 글자 수는 해당 단어와 가장 비슷한 다른 단어를 구분할 수 있는 위치로 결정된다. 사전순 정렬 후 앞뒤 단어의 공통 접두사만 비교하면, 트라이 없이도 필요한 총 입력 횟수를 구할 수 있다.
📋 STEP1. AI 활용 자가진단 나는 ChatGPT를 내 상황에 맞는 결과물을 만드는 ‘도구형’ 으로 활용하는 편이다. 짧게 질문을 시작하더라도 배경과 조건을 추가하고 답변을 수정하면서, 지원서 작성이나 계획 수립 등에 활용한다. 앞으로는 처음부터 목표와 원하는 출력 형식을 명확히 전달하고, 답변의 사실 여부와 근거를 확인하는 데도 더 신경 쓰려고 한다. 🤔 STEP2. "지식인형 vs 도구형" 프롬프트 비교 실습 차이: 일반적인 공부 방향을 묻는 질문에서, 실제 수강할 과목을 비교하고 선택하는 요청으로 구체화 하였으며, SQL프로그래밍 과목을 추가로 알려주면서, 추천이 웹서비스와애플리케이션과 SQL프로그래밍 중심으로 조정됨. 🙋🏻 STEP3. 내 전공 맥락으로 도구형 질문 직접 만들기 작곡을 전공한 경험을 바탕으로, 영상디자인과 BGM을 결합한 포트폴리오를 주제로 질문을 만들었다. AI에게 포트폴리오를 지도하는 멘토 역할을 맡기고, 혼자 제작할 수 있는 1분 영상 기획안 3가지를 요청했다. 답변은 주제, 장면 구성, BGM 방향, 필요한 작업, 보여줄 수 있는 역량을 표로 정리하도록 했다. 마지막으로 첫 작품으로 만들기 적합한 기획안과 추천 이유도 함께 물었다. 💻 STEP 4. 결과 정리 1️⃣ 자가진단에서 체크된 항목 수 총 1개로, ‘한 줄로 물어보는 편이다’에 체크했다. 2️⃣ 가장 차이가 컸던 예시 번호와 이유 실제 사례 1 ‘백엔드 선수학습을 위한 수강과목 선택’이다. 미션 안내의 예시 3에 해당한다. 학습 목표와 선택 가능한 과목을 구체적으로 알려주자, 추천이 ‘웹서비스와애플리케이션’과 ‘SQL프로그래밍’ 중심으로 정리됐다. 내 상황에 맞게 과목을 선택하는 데 활용할 수 있다는 점에서 차이가 컸다. 3️⃣ 내가 만든 도구형 질문 너는 영상디자인 포트폴리오를 지도하는 멘토야. 나는 작곡을 전공한 경험이 있고, 영상디자인과 음악을 결합한 포트폴리오를 만들고 싶어. 영상디자인과 BGM 작곡 역량을 함께 보여줄 수 있는 1분 길이의 포트폴리오 영상 기획안 3가지를 제안해줘. 혼자 제작할 수 있는 규모로 구성하고, 영상과 음악이 어떻게 어우러지면 좋을지도 설명해줘. 각 기획안은 ‘주제 / 장면 구성 / BGM 방향 / 필요한 작업 / 보여줄 수 있는 역량’ 순서의 표로 정리해줘. 마지막에는 첫 작품으로 만들기 적합한 기획안 하나를 골라 이유도 알려줘. 4️⃣ GPT에게 받은 답변 중 가장 유용했던 부분 먼저 60초 BGM을 만들고, 음악의 변화 지점에 맞춰 화면을 구성하라는 조언이 가장 유용했다. 💪🏻 보너스 미션 총평! 이번 미션을 통해 내가 평소 ChatGPT를 어떻게 쓰고 있는지 돌아볼 수 있었다. 질문은 짧게 시작하는 편이지만, 대화하면서 조건을 추가하고 답변을 수정해 필요한 결과물을 만들어가고 있었다. 짧은 질문이라도 앞선 대화의 맥락과 목적이 담겨 있을 수 있다는 점도 생각해봤다. 작곡 경험을 바탕으로 영상 포트폴리오 기획안을 요청하는 과정에서는 역할, 배경, 구체적인 요청, 출력 형식을 정해주는 방법을 적용해봤다. 장면 구성과 BGM 방향, 필요한 작업까지 정리된 답변을 보니 무엇부터 시작하면 좋을지 이해하기 쉬웠다. 앞으로도 내 상황과 목표를 분명하게 전달하고, AI가 준 답변은 근거와 실현 가능성을 확인하면서 내 판단으로 선택하고 수정해가려고 한다.
문제 요약 주어진 단어 조각을 원하는 만큼 사용해 문자열 t 를 완성한다. 문자열을 완성하는 데 필요한 단어 조각 수의 최솟값을 구하고, 만들 수 없다면 -1 을 반환한다. 핵심 아이디어 dp[i] 를 t 의 앞에서부터 i 글자까지 완성하는 데 필요한 최소 조각 수라고 정의한다. dp[0] = 0 어떤 위치 i 까지 만들 수 있고, 그 위치부터 시작하는 조각 piece 가 t 와 일치한다면 다음 위치를 갱신한다. dp[i + len(piece)] = min( dp[i + len(piece)], dp[i] + 1 ) 각 조각을 무한히 사용할 수 있으므로, 같은 조각을 여러 위치에서 반복해서 사용해도 된다. 풀이 과정 길이가 len(t) + 1 인 DP 배열을 만들고, 도달할 수 없는 값은 큰 값으로 초기화한다. dp[0] = 0 으로 시작한다. 문자열의 각 위치에서, 해당 위치부터 일치하는 모든 단어 조각을 확인한다. 일치하는 조각의 끝 위치를 최소 조각 수로 갱신한다. dp[len(t)] 가 여전히 큰 값이면 -1 을 반환한다. Python 코드 def solution(strs, t): length = len(t) infinity = length + 1 # dp[i]: t의 앞 i글자를 만드는 데 필요한 최소 조각 수 dp = [infinity] * (length + 1) dp[0] = 0 for start in range(length): if dp[start] == infinity: continue for piece in strs: end = start + len(piece) if end <= length and t.startswith(piece, start): dp[end] = min(dp[end], dp[start] + 1) return -1 if dp[length] == infinity else dp[length] 예시 strs = ["ba", "na", "n", "a"] , t = "banana" 인 경우를 보자. dp[0] = 0 "ba" 사용 -> dp[2] = 1 "na" 사용 -> dp[4] = 2 "na" 사용 -> dp[6] = 3 따라서 "ba" + "na" + "na" 로 문자열을 만들 수 있고, 필요한 조각 수는 3 이다. 시간 복잡도 N 을 t 의 길이, S 를 단어 조각 개수, L 을 조각의 최대 길이라고 하자. 각 위치에서 모든 조각을 확인하고, 문자열 비교는 최대 L 글자를 확인한다. 시간 복잡도: O(N * S * L) 공간 복잡도: O(N) 이 문제에서는 N <= 20,000 , S <= 100 , L <= 5 이므로 충분히 빠르게 동작한다. 정리 문자열을 앞에서부터 완성하는 최소 비용 문제로 바꾸면 된다. dp[i] 가 도달 가능한 위치인지 확인하고, 그 위치에 이어 붙일 수 있는 단어 조각으로 다음 상태를 갱신한다.
부제: 나보다 훨씬 똑똑하다고 믿었던 모델이 전원 버튼 하나에 막혔다. 그 시행착오를 줄여 측정을 자동화하기까지 서두 windowinsets.info 를 만들고 있다. Android 기기별 상태 표시줄, 내비게이션 바, 제스처 영역, 화면 잘림과 폴더블 힌지 정보를 그림으로 보여 주는 사이트다. 개발자가 자기 화면에서 콘텐츠가 어디까지 안전하게 놓일 수 있는지 살펴볼 수 있게 하려는 프로젝트다. 이 값은 기종과 화면, 회전 방향, 내비게이션 방식에 따라 달라진다. 그래서 작은 Android 앱인 InsetsProbe 를 만들어 실기기와 Samsung Remote Test Lab(RTL)에서 값을 JSON으로 기록해 왔다. 기기가 많아질수록 측정 코드보다 기기를 조작하고 파일을 내 컴퓨터까지 옮기는 과정 이 더 큰 일이 됐다. 처음에는 당시 사용자에게 제공된 OpenAI의 최상위 GPT-6 모델, Astra에게 RTL 기기를 맡기면 이 반복 작업이 빨리 끝날 줄 알았다. 그런데 검은 화면을 깨울 사이드 버튼을 못 누르고, 설정을 한 번 스크롤하면 메뉴를 다시 찾느라 헤맸다. JSON을 저장한 뒤에는 RTL File Browser에서 내려받는 일도 오래 걸렸다. 어제까지만 해도 내가 직접 파일을 받아 Astra에게 넘기는 편이 빨랐다. 삼성 기기만 약 120종이다. 가로 화면을 추가하려면 이 기기들을 다시 예약해 같은 일을 반복해야 하는데, Pixel 지원까지 더하면서 측정 대상은 더 늘어났다. 수작업으로 파일을 모으는 방식은 도저히 감당할 수 없었다. 이 글은 그 병목을 겪고, Probe에서 GitHub PR까지 직접 업로드하는 흐름을 만든 기록이다. 마지막에는 가로 90°와 270°를 각각 측정해야 하는지 S23+와 Fold8·Flip8에서 먼저 측정한 결과로 판단했다. 문제 1. 검은 화면에서 사이드 버튼을 누르지 못했다 문제 발생 RTL WebClient에 Probe APK를 설치해도 기기 화면이 검게 보일 때가 있었다. 화면을 켜려면 앱 안의 버튼이 아니라 WebClient가 그린 휴대폰 프레임의 사이드 버튼 을 눌러야 했다. 브라우저 메뉴는 접근성 트리에서 이름을 찾을 수 있지만, 원격 Android 화면과 기기 프레임은 주로 픽셀로 보인다. Astra는 스크린샷에서 버튼처럼 보이는 돌출부를 찾아 좌표를 누르고, 다음 스크린샷에서 성공했는지 다시 확인해야 했다. Fold8의 검은 화면. 눌러야 할 사이드 버튼은 앱 화면 밖, RTL이 그린 기기 프레임에 있다. 문제는 버튼을 알고도 클릭이 실제 기기에 전달됐는지 확신하기 어려웠다는 점이다. Fold8과 Flip8을 시험할 때도 Astra가 버튼을 눌러 보았지만 화면은 계속 검었다. 내가 화면을 깨우고 잠금을 풀어 준 뒤에야 다음 단계로 갈 수 있었다. 혹시 Claude의 최상위 모델인 Opus 5.5 는 다를까 싶어 Fold7 RTL에서 APK 설치와 잠금 해제도 맡겨 봤다. 하지만 업로드와 클릭이 뜻대로 되지 않아 결국 내가 직접 두 단계를 끝냈다. 이번에는 모델을 바꿔도 원격 기기 조작의 어려움이 그대로였다. 그래도 응답은 Opus 쪽이 좀 더 빠릿빠릿하게 느껴져 마음에 들었다. Astra와 GPT도 이 부분은 분발했으면 싶다. Opus 5.5로도 업로드와 클릭이 되지 않아 내가 설치와 잠금 해제를 마친 뒤 이어서 작업했다. 문제 해결 이번 측정에서는 Probe APK 설치, 사이드 버튼, 잠금 해제, Flip8 커버 위젯 등록과 실행에 내 손이 필요했다. 기기가 측정할 화면에 앱을 띄운 뒤에야 Astra가 Probe의 측정과 업로드를 이어갈 수 있었다. Flip8 커버 화면은 특히 손이 많이 갔다. 내가 사이드 버튼을 눌러 화면을 켜 줘도, Astra가 다음 스크린샷을 확인하고 클릭할 좌표를 정하는 동안 화면이 다시 꺼졌다. 위젯을 등록한 뒤에는 커버 화면을 좌우로 스와이프해 해당 위젯을 찾아 실행해야 했는데, 이 조작도 Astra가 안정적으로 끝내지 못해 내가 직접 스와이프하고 위젯을 눌렀다. 문제 2. 설정을 스크롤하자 이전 스크린샷이 틀렸다 문제 발생 WindowInsets는 제스처와 3버튼 내비게이션에서 값이 달라진다. 두 모드를 측정하려면 Android 설정에서 내비게이션 방식을 바꿔야 한다. 그런데 Astra가 스크린샷에서 찾은 메뉴 위치는 한 번 스크롤한 뒤 더 이상 맞지 않았다. 사람은 목록이 움직이는 동안 같은 항목을 눈으로 따라간다. Astra의 조작은 화면 A를 보고 판단 → 스크롤 → 화면 B를 다시 관찰 하는 단계로 끊긴다. B를 확인하지 않고 A의 좌표로 누르면 엉뚱한 메뉴를 열게 된다. Flip8 설정 메뉴를 스크롤하기 전과 후. 다음 클릭은 반드시 바뀐 화면에서 다시 찾아야 했다. 처음에는 앱에서 Settings.Secure.putInt(navigation_mode, ...) 로 모드 변경도 시도했다. 호출 경로의 오류를 고쳐도 Samsung RTL 기기는 요청을 적용하지 않았다. 새 JSON은 계속 3버튼 모드였다. 문제 해결 Probe에 Display / navigation settings 버튼과 × 종료 버튼 을 넣어 설정 앱으로 이동하기 쉽게 했다. 설정을 스크롤한 뒤에는 반드시 새 화면에서 항목을 다시 찾도록 했다. 모드 변경 여부는 요청 코드가 아니라 Android 설정과 새 캡처의 navigation.mode 로 판단한다. 이번 시험 측정에서는 3버튼 모드의 Fold8·Flip8 데이터를 우선 모았다. 제스처로 바꾸지 않은 캡처를 제스처 데이터라고 부르지 않았다. 문제 3. 파일은 저장됐는데 내려받기가 오래 걸렸다 문제 발생 기존에는 RTL File Browser에서 Android/data/info.windowinsets.probe/files 로 들어가 JSON을 내려받았다. 파일 행에 마우스를 올려야 다운로드 아이콘이 나타났다. 호스트에 도착한 이름도 main-gesture.json 대신 content , content (1) 처럼 바뀌었다. Flip8 RTL File Browser. JSON 파일 행에 마우스를 올려야 오른쪽에 다운로드 아이콘이 나타났다. 다운로드를 눌렀다는 사실과 파일이 내 컴퓨터에 도착했다는 사실도 별개였다. Flip3에서는 기기에 저장된 제스처 JSON을 끝내 받지 못해 다음 예약에서 다시 측정했다. 한 파일씩 열어 모델, 화면, 모드와 크기를 확인하는 수작업도 계속됐다. 문제 해결 InsetsProbe가 JSON을 /api/captures 로 직접 보내게 했다. Vercel 함수가 요청을 받아 GitHub의 capture-inbox 브랜치에 원본을 커밋한다. 업로드마다 PR을 만들지 않고 여러 캡처를 하나의 PR 에 모은다. 병합 여부는 내가 결정하고, 병합할 때는 스쿼시 커밋으로 묶는다. 첫 프리뷰에서는 API가 404였고, 배포 뒤에는 ECMAScript Modules(ESM) import 오류로 500이 났다. 이를 고친 다음 GitHub PAT와 업로드 키를 Production에 넣었다. 기존 S24+ JSON 재전송에서 HTTP 201과 PR #30 의 커밋을 확인했다. 여기까지는 업로드 경로만 시험한 것이었다. 이후 Fold8과 Flip8을 실제 RTL에서 예약해 APK를 설치했다. Probe 버튼을 눌러 만든 새 JSON이 같은 PR에 도착하는 것까지 확인했다. Fold8 커버와 Flip8 메인은 세로·가로 양방향 캡처가 올라왔다. 이제 RTL File Browser를 거치지 않고, Probe가 측정한 JSON을 직접 전송한다. 문제 4. 업로드가 성공해도 화면 라벨은 틀릴 수 있었다 문제 발생 Fold8의 과거 main JSON은 1248 × 1972px였다. 이 크기는 내측이 아니라 커버 화면과 맞는다. 예전 Measure All 의 Cover/Main 선택은 실제 디스플레이를 바꾸지 않고 파일 라벨만 바꿨다. 실제 기기를 다시 측정하면서도 비슷한 실수를 했다. Fold8을 펼친 직후 Cover 라벨을 그대로 둔 채 한 번 측정해, 실제 2448 × 1848px 내측 화면이 cover-threeButton.json 으로 올라갔다. API 성공은 전송 성공 이지, 화면 분류가 맞다는 뜻이 아니었다. 문제 해결 폴더블 화면은 RTL의 접힘 제어로 실제 전환하고, Probe가 보고한 창 크기와 디스플레이를 확인한다. Fold8 커버는 1248 × 1972px, 내측은 2448 × 1848px로 구분했다. 잘못 표기된 원본은 고치지 않고 증거로 남기되, 유효한 커버 측정으로 사용하지 않는다. Probe의 회전 스윕도 화면이 실제로 돌아갔을 때만 파일을 저장한다. Flip8 커버 위젯은 Display 1에서 948 × 1048px 세로 캡처를 업로드했지만, 가로 요청은 적용되지 않아 가로 파일을 만들지 않았다. Flip8 커버 화면에서 직접 측정하고 업로드했다. 기기가 허용하지 않은 가로 값은 채우지 않았다. 문제 5. 가로 90° 하나를 뒤집어 270°로 쓸 수 있을까? 문제 발생 가로 데이터를 다시 모을 때 가장 궁금했던 건 이거였다. 90°와 270°가 같은 값이라면 하나만 찍고 뒤집어 쓰면 된다. 다르다면 삼성 기기 약 120종에 더해 Pixel 기기까지 두 방향을 각각 측정해야 한다. S23+ 바형, Fold8 커버, Flip8 메인을 같은 3버튼 조건으로 비교했다. 세 기기 모두 화면 크기는 같았지만, 상태 표시줄은 두 방향 모두 위쪽 에 남았다. 내비게이션 바와 카메라 잘림 영역은 오른쪽에서 왼쪽으로 옮겨 갔다. 기기 90° 상태 표시줄 / 내비게이션 바 / 잘림 영역 270°에서 달라진 점 S23+ 위 84px / 오른쪽 135px / 왼쪽 74px 상태 표시줄은 위 84px, 나머지는 좌우 반대 Fold8 커버 위 79px / 오른쪽 126px / 왼쪽 104px 상태 표시줄은 위 79px, 나머지는 좌우 반대 Flip8 메인 위 90px / 오른쪽 144px / 왼쪽 108px 상태 표시줄은 위 90px, 나머지는 좌우 반대 90° 캡처를 180° 돌리면 상태 표시줄까지 아래로 가므로 270° 실측값과 틀린다. 이 세 사례에서는 좌우 반전이 맞았지만, 모두 3버튼 모드였다. 제스처 모드와 다른 기종까지 같은 규칙이 적용된다고 단정할 수는 없다. 문제 해결 90°와 270°는 각각 실측한다 고 결정했다. Probe의 회전 스윕은 한 번 누르면 두 방향을 연달아 시도한다. 별도 예약을 한 번 더 할 필요는 없다. 기기가 회전을 허용하지 않으면 그 방향은 저장하지 않고 미측정으로 남긴다. 이렇게 해야 사이트에서 보여 주는 값이 실제 기기에서 나온 측정인지 분명해진다. 좌우 반전으로 만든 그림이 필요하더라도 실측한 270° 데이터인 것처럼 표시하지 않는다. 결론 한 대만 보면 내가 직접 버튼을 누르고 파일을 받는 편이 빨랐다. 하지만 삼성 기기만 약 120종이고 Pixel까지 지원 범위를 넓히면서, 화면·방향·내비게이션 조합을 손으로 모으는 일은 감당하기 어려워졌다. 그래서 사람이 기기를 준비한 뒤 Probe가 가능한 회전을 측정하고 JSON을 전송하는 흐름 으로 바꿨다. Fold8·Flip8 실기기에서 Probe → Vercel → GitHub PR까지 확인했다. 90°와 270°는 단순 회전으로 같은 값이 되지 않아 둘 다 실측하기로 했다. APK 설치와 사이드 버튼·잠금 해제, Flip8 커버 위젯 등록·실행에는 여전히 사람의 도움이 필요했다. 잘못 고른 화면 라벨도 업로드 전에 자동으로 막지 못했다. 원본 파일은 PR #30에서 스쿼시 머지했다. 이 머지는 증거 보관 단계이며, 사이트에 방향별 실측값을 연결하는 작업은 별개다. Astra가 간단한 버튼에서 헤맨 이유도 조금은 알 것 같다. 웹 메뉴, 원격 화면, 기기 프레임은 조작 방식이 달랐다. 특히 스크린샷 한 장은 스크롤이나 화면 꺼짐 뒤의 상태를 보장하지 않는다. 다음 행동 전에 현재 화면을 다시 확인하는 절차가 필요했다. P.S. 인간 데이터 수집기를 졸업하며 내가 직접 파일을 받아 넘기는 편이 빠르다고 느낀 게 불과 어제였다. 오늘은 그 일을 없애려고 코드를 고쳤다. 친구와 얘기하다 보니, 내가 하던 자리를 내가 줄이고 있다는 생각도 들었다. 작업하면서 기기 자체에 대해서도 새로 알았다. Flip8은 APK를 설치했다고 커버 화면에서 앱이 바로 뜨지 않았다. 접은 채 RTL의 Applications에서 실행하면 숨겨진 메인 화면에 열리기도 했다. 결국 FlexWindow용 위젯 을 만들어 커버 디스플레이에서 앱을 열고, 실제 화면 크기까지 확인해야 했다. 위젯을 만드는 것과 커버 화면에서 위젯을 등록하고 찾아 실행하는 것은 또 다른 일이었다. 180° 회전도 뜻밖이었다. 내가 확인한 Galaxy 바형 폰과 Fold·Flip의 일반 화면은 자동 회전만으로 세로 화면을 거꾸로 돌리지 않았다. Fold를 펼쳐도 이 점은 태블릿과 달랐다. 반면 Galaxy Tab은 거꾸로 든 세로 방향도 지원했다. 여기서 180°는 힌지 각도가 아니라 화면 방향이다. 이런 예외를 모르고 파일만 쌓았다면, 숫자는 늘어도 믿을 수 있는 데이터는 늘지 않았을 것이다. 이 고민에 깔끔한 답은 없을 것 같다. 그래도 이 프로젝트를 시작한 이유는 분명하다. 내가 겪은 번거로움을 다른 사람은 덜 겪게 하는 제품을 만들고 싶었다. 이번에는 그 ‘다른 사람’에 미래의 나도 들어간다. 손으로 파일을 옮기는 시간 대신 화면 데이터를 더 잘 설명하는 데 시간을 쓰고 싶다. 참고 자료 windowinsets.info 프로젝트 저장소와 InsetsProbe 캡처 업로드 구현 PR #29 실제 RTL 업로드가 모인 PR #30 Android setRequestedOrientation()
2026년 9월 28일 ~ 10월 4일 코드잇 9주차 이번주 할일 미션6 제출(제출 마감 : 10월 2일) 스터디 - 이미지 EDA 스터디 자료 만들기 스터디 발표(9월 29일 3시) 멘토링(9월 28일) 질문 적어두기 위클리 페이퍼 #8 (9월 30일) 이번주 다짐 저번주는 추석 연휴라서 흐지부지 지나갔햄..😭 이번주는 제대로 계획 세워서 못하거나 안하는거 없게 해보자햄..!!🐹✨ 9월 28일 오늘 할일 미션6 진행 계획 세우기 데이터 불러오기 Baseline 전체적인 흐름 정리 진행 방향 잡기 < 제출 안내 내용 > 1. 분석 과정과 결과 데이터 로드, 전처리, 모델 학습, 예측, 성능 평가 등의 모든 과정을 포함해야 합니다. 사전 학습된 모델을 활용한 Transfer Learning을 적용해 보세요. Fine-Tuning을 적용해 보세요. Frozen 모델, Partial Fine-Tuning, Full Fine-Tuning을 비교하며 실험을 진행해 봅시다. 모델별 성능 비교, 분석 결과를 코드와 함께 정리해 주세요. 2. 마크다운을 활용한 설명 코드의 각 단계에서 어떤 작업을 수행하는지, 어떤 의도를 가지고 접근했는지 명확히 표현할 수 있도록 마크다운을 적극 활용해 주세요. 코드와 실행 결과를 설명하는 문구를 추가하여, 전체 코드의 흐름을 이해할 수 있도록 작성해 주세요. 보고서를 따로 작성하지 않으므로, 노트북 파일 내에 모든 설명이 잘 드러나야 합니다. 3. 모델 성능 평가 및 제출 평가 지표(Accuracy, Precision, Recall, F1-score 등)를 활용해 모델 성능을 분석하고 비교해 보세요. 제공된 데이터셋의 테스트 파일을 사용하여 모델을 테스트해 보세요. 모델별 성능 평가 결과를 포함한 노트북 파일을 제출하세요. 멘토링(7시~) 회고 적기 질문 적기 자료 적기 스터디 자료 만들기 오늘 회고 및 일기
JavaScript의 작업 관리 방식 자바 스크립트는 싱글 스레드 로 동작한다. 즉, 한 번에 한 가지 일만 할 수 있는 언어이다. 그래서 만약 오래 걸리는 작업을 동기적으로 실행하면 다른 작업이 블로킹되는 문제가 발생할 수 있다. 그러나 실제 웹 어플리케이션은 I/O 관리, 네트워크 요청, 타이머 등 여러 가지 동작을 동시에 처리해야 하는 경우가 많다. 한 번에 한 가지 일만 처리할 수 있다고 했지만, 실제 웹앱에서는 여러 가지 작업이 동시에 진행 된다. (ex) 타이머를 기다리면서 입출력 관리, api를 받아오면서 버튼 이벤트 진행 등) 자바 스크립트는 이러한 동시성(Concurrency) 이슈를 어떻게 해결하고 있을까? 그 답은 바로 ‘ 이벤트 루프(Event Loop) ’에 있다. 이벤트 루프의 구성 요소와 동작 과정에 대해 살펴보자. 이벤트 루프(Event Loop) 이벤트 루프는(Event Loop)는 브라우저 내부의 Call Stack, Web APIs, Callback Queue 등의 요소들을 모니터링하고 제어하는 런타임 루프이다.(like 관리자) 런타임 요소 1. Memory Heap 객체가 저장되는 메모리 공간 실행 컨텍스트는 이 메모리 힙에 저장된 객체를 참조하여 값을 가져오게 된다. 2. Call Stack 실행중인 작업의 실행 컨텍스트가 쌓이는 스택 LIFO(Last In Final Out) 구조이다. 3. Web API 네트워크 요청, 파일 입출력, 타이머 등 브라우저에서 제공하는 다양한 API를 말한다. 이벤트 루프에서의 Web API는 비동기 작업들을 전담하여 처리한다. Web API는 브라우저(Chrome)에서 멀티 쓰레드로 구현되어 있기 때문에, 비동기 작업을 처리할 수 있다. ❗모든 Web API가 비동기적으로 실행되는 것은 아니다! 동기적/비동기적으로 처리되는 것이 모두 있다. Event Loop에서 Web API의 역할이 그런 것이지, 모두 비동기로 동작하는 것이 아니라는 점을 잊지 말자~! 4. Callback Queue(콜백 큐) 완료된 비동기 작업의 콜백 함수가 대기하는 공간. 이 때 Callback Queue는 두 가지 종류가 있다. 여기서 알아둬야할 점은 마이크로태스크 큐의 우선순위가 더 높다는 것이다. +) ✅ 매크로테스크큐(태스크 큐) : setTimeout , fetch , addEventListener 등 비동기로 처리되는 함수들의 콜백 함수가 저장된다. +) ✅ 마이크로테스크 큐 : Promise.then , MutationObserver 등 우선적으로 처리되는 비동기로 처리되는 함수들의 콜백 함수가 저장된다. 이벤트 루프의 동작 과정 이제부터는 이벤트 루프가 어떤 과정으로 동작하는지 살펴보도록 하자. 가장 먼저 코드가 실행되어 함수가 호출되면 실행 컨텍스트가 Call stack 에 쌓인다. 이 때, Call stack 은 말 그대로 스택이기 때문에 하나씩 쌓이게 되고 위에서부터 하나씩 실행된다. 비동기 작업(setTimeout, promise 등)은** webAPI**를 통해 브라우저에게 넘겨진다. (넘겨진 작업은 Call stack에서 즉시 pop 된다.) 브라우저는 비동기 작업들에게 각각 별도의 쓰레드를 배정하고 실행한다. 작업이 완료된 후 callback 함수는 Callback Queue 에 넣어준다. 이 때 작업의 종류에 따라 MacroTask Queue / MicroTask Queue 로 각각 구분되어 들어간다. 이벤트 루프는 반복해서 콜 스택이 비어 있는지, Callback Queue 에 콜백 함수가 있는지 계속해서 확인한다. 이 때, 콜 스택이 비어있다면 콜백 큐에 쌓여있는 콜백 함수를 콜 스택으로 넘겨준다. 설명이 거의 간장공장공장장급이라 이해가 잘 안 될 거 같아 움직이는 이미지를 첨부한다. 아래 이미지를 참고하면 이해가 훨씬 수월할 것이다! 🤔: 콜 스택은 왜 queue가 아니라 stack인가요? 💁♀️: 함수 호출의 구조적인 특성과 실행 흐름 제어 방식을 고려했을 때, stack 구조가 적합하기 때문입니다. (LIFO 방식) function A() { B(); } function B() { C(); } A(); // A → B → C 순으로 호출됨 위 코드에서 A()가 먼저 호출되고, B()가 A 안에서 실행되었으며, C()는 B 안에서 실행됩니다. 이 흐름에서 중요한 것은 " C가 끝나야 B가 끝날 수 있고, B가 끝나야 A가 끝날 수 있다는 것 "이에요. 즉, 나중에 호출된 함수가 먼저 끝나야 앞선 함수로 돌아갈 수 있다는 것인데 이게 사실상 LIFO 구조 인 셈이지요. JavaScript 이벤트 루프 시각화 아무래도 흐름이 중요한 개념이다 보니 이미지만 봐서는 이해하기가 어려웠는데, 찾아보니 이벤트 루프 과정을 애니메이션으로 볼 수 있는 시각화 사이트가 있다. 해당 사이트에서 다양한 비동기 코드를 추가하여 비동기 동작이 실제로 어떤 과정으로 일어나는지 확인해볼 수 있다. JavaScript 이벤트 루프 시각화 사이트 코드로 이해하는 이벤트 루프 동작 과정 이번에는 실제 코드를 사용해 이벤트 루프 동작 과정을 이해해보자. (아래 예시 코드는 ‘매일메일’ 서비스를 구독해서 받은 메일의 일부이다. 매일메일 최고👍 ) setTimeout(() => { console.log('1') setTimeout(() => {console.log('2')} ) Promise.resolve().then(()=> console.log('3')) console.log('4') }) Promise.resolve().then(() => { console.log('5') setTimeout(() => {console.log('6')} ) Promise.resolve().then(()=> console.log('7')) console.log('8') }) console.log('9') 정답: 9 5 8 7 1 4 3 6 2 실행 과정 1️⃣ setTimeout() , Promise 객체는 비동기 코드이기 때문에 webAPI로 넘어가고, 가장 하단의 console.log(’9’) 부터 실행된다. →** 9 출력** 2️⃣ setTimeout() 은 매크로테스크 큐에, Promise.then() 은 마이크로테스크 큐에 저장된다. 콜 스택이 비어있고, 테스크 큐에 콜백 함수가 있기 때문에 event loop가 동작한다. 이 때 마이크로테스크 큐의 우선순위가 더 높기 때문에 Promise.then() 부터 실행된다. 3️⃣ 5 출력 후, Promise.then() 내부의 setTimeout() 함수와 Promise 객체 역시 비동기 코드로 webAPI 넘어간다. 그 후 console.log(’8’) 이 실행된다. → 순서대로 5, 8 출력 4️⃣ Promise.then() 내부의 Promise 가 먼저 실행된다. → 7 출력 5️⃣ 그 후 첫 번째 setTimeout() 의 콜백 함수가 실행된다. 이 때도 역시 동기 함수 먼저 실행된다. → 1, 4 출력 6️⃣ 다음으로 setTimeout() 내부의 Promise.then() 의 콜백 함수가 마이크로 테스크 큐로 들어간다. → 3 출력 7️⃣ 마지막으로 매크로테스크 큐에 있는 3과 4의 setTimeout() 의 콜백 함수가 들어온 순서대로 실행된다. → 순서대로 6, 2 출력 8️⃣ 콜 스택과 이벤트 콜백 큐가 모두 비었기 때문에 event loop의 동작도 끝이 난다. +) 추가 참고 영상 이벤트 루프 과정을 이해하는데 큰 도움을 받았던 영상을 참고하며 글을 마친다. 애니메이션 영상과 코드를 사용해서 과정 그대로를 시각적으로 보여줘서 개념을 이해하기가 정말 수월했다! https://www.youtube.com/watch?v=eiC58R16hb8 참고 https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Execution_model https://inpa.tistory.com/entry/🔄-자바스크립트-이벤트-루프-구조-동작-원리 https://velog.io/@leehyunho2001/자바스크립트-이론-부시기#자바스크립트-런타임
SOAP SOAP란? SOAP은 XML을 기반으로 메시지를 교환하는 프로토콜로, 기업 간 시스템 통합이나 복잡한 분산 시스템에서 널리 사용되었다. DTD를 지원하긴 하지만 스키마(XSD)의 사용을 강력하게 권장했다. SOAP의 단점 SOAP의 단점 중 많은 부분은 스키마의 단점에서 왔다. 강제된 스키마 : 명확한 스키마 구조를 설계해야 하기에 소규모 프로젝트에선 부담스럽다. 높은 파일 용량 : XML의 여러 태그와 구조는 저장 데이터를 무겁게 만든다. 복잡한 구조 : XSD의 엄격한 구조는 배우기 어렵고, 숙련자도 유지보수가 어렵다. REST REST란? REST는 HTTP의 기본 기능을 활용해 리소스를 url로 식별하고, HTTP 메서드를 통해 처리한다. 특정한 형식이나 프로토콜을 강제하지 않으며, JSON의 사용률이 높다. 스마트폰의 등장으로 간결한 통신에 대한 수요가 증가했고, REST가 이해 적합했다. REST의 장점 HTTP 메서드 : (GET,POST,등등)의 메서드를 사용하여 직관적이고, 예측 가능하다. 높은 개발 생산성: 일반적으로 JSON을 사용하며, 복잡한 메시지 규격이 없어 비교적 쉽게 구현할 수 있다. 효율적인 데이터 전송: JSON은 일반적으로 XML보다 데이터가 간결하여 용량을 줄일 수 있다. 무상태성: 서버가 클라이언트의 상태를 기억하지 않아, 수평 확장에 유리. 캐싱 지원: HTTP의 캐싱 기능을 활용하여 반복적인 요청에 대한 속도를 높이고, 서버의 부하를 주일 수 있다.(모든 메서드가 캐싱 되는 것은 아님) SOAP가 REST보다 유리한 점 표준화된 통신: 엄격한 규칙을 통해 서로 다른 시스템 간의 통신을 표준화함. 보안 및 신뢰성: 높은 수준의 보안(Web Services Security)과 신뢰성(WS-ReliableMessaging, WS-AtomicTransaction)을 제공. 다양한 전송 프로토콜: REST는 HTTP에 의존하지만 SOAP는 다른 전송프로토콜도 사용할 수 있음.
지난 글에서 Gradle Multi-Module을 이용해 모듈을 독립적인 빌드 단위로 나누고, 모듈 간 의존성을 컴파일 타임에 제한하는 방법을 정리했다. 그래서 다음과 같은 의문이 생겼다. Gradle로 이미 모듈을 나눴는데 Spring Modulith는 왜 필요하지? 이번 글에서는 Gradle Moduler과 Spring Modulith의 Application Module이 어떻게 다른지, 현재 참여중인 프로젝트에서는 Spring Modulith와 ArchUnit을 이용해 어떤 경계를 추가로 검증하고 있는지 정리해보려고 한다!! 현재 프로젝트에서는 모듈 경계 검증을 위한 용도로만 Spring Modulith를 사용하고 있다. Gradle이 막아주는 경계 이전 글에서 정리한 것처럼 Gradle은 서로 다른 Subproject 사이의 물리적인 의존성 경계를 만들어준다. 하지만 하나의 Gradle Module 내부라면? 하나의 Gradle Module 안에서의 경계 참여중인 프로젝트처럼 하나의 Gradle Module 내부에 여러 패키지가 존재하는 경우, Gradle의 관점에서는 내부 모든 코드가 하나의 Subproject에 속한다. 따라서 Gradle만으로는 service.impl은 외부에서 직접 사용하지 않는다 같은 패키지 수준의 세부 규칙을 표현하기가 어렵다. -> Gradle Module에 대한 dependency가 있다고 해서 그 모듈의 모든 패키지를 사용해도 되는 것인지는 별개의 문제 Application Module Gradle에서의 모듈 : settings.gradle.kts에 등록한 Subproject Spring Modultih는 Spring Application의 Java Package 구조를 기준으로 Application Module을 해석 base package ├── api ├── security ├── logging ├── db ├── common ├── 도메인1 ├── 도메인2 ├── 도메인3 └── internal 이라고 할때, Spring Modulith는 이 패키지 구조를 기준으로 도메인1/2/3, security 등을 Application Module로 해쇼ᅥᄀ한다. Gradle 경로와 Java Package가 다를수도 있는 이유 Gradle 경로를 그대로 따라가면 Spring Modulith의 Application Module 구조와 원하는 도메인 경계가 맞지 않게 될수도 있다. 현재 진행중인 프로젝트에서도 Gradle의 디렉터리 구조를 그대로 반영할 경우 원하는 도메인 패키지들을 각각의 독립적인 Application Module로 인식시키기 위해 Gradle의 디렉터리 구조를 그대로 반영하지 않았다. => Gradle Module은 빌드와 classpath 경계를 만들고, Java Pacakage는 Spring Modulith가 해석하는 Application Module 경계에도 영항ᄋ르 준다. verify() 진행중인 프로젝트에서는 다음과 같은 테스트를 통해 Application Module 구조를 검증한다. class ModularityTests { private final ApplicationModules modules = ApplicationModules.of(ServerApplication.class); @Test void verify() { modules.verify(); } } ServerApplication을 기준으로 APplication Module을 분석하고 verify()를 실행한다. 이 검증은 Application Module 사이의 Dependency를 분석하고, 경계 위반과 순환 의존 등을 검증한다. 실제로 경계가 깨진 사례 실제로 프로젝트를 진행하면서 이 경계가 깨진것이 검증된 적이 있었다. 한 클래스를 다른 모듈에서 사용할 필요가 생겼을 때, 해당 클래스는 public이며 Gradle 관점에서도 필요한 모듈 dependency가 선언되어 있었기에 컴파일 단계에서는 성공했다. 하지만 Spring Modulith의 verify()가 실패했는데, 해당 클래스의 위치가 Application Module의 내부 영역이었는데 외부 Application Module에서 이를 직접 참조 하고 있었기 때문이다. => 해당 클래스의 위치를 base package로 이동시킴으로써 패키지 구조 자체가 외부에 공개하는 APIdhk 내부 구현이라는 의도를 표현하게 되었다. public이라는 Java 개념과 Application Module이 외부에 공개하는 API는 같은 개념이 아니라는 것을 알게 되었다. CLOSED Module Spring Modulith의 기본적인 Application Module은 CLOSED 형태의 경계를 제공한다. event ├── EventService ├── EventRepository │ └── internal └── EventServiceImp 다음과 같은 구조를 생각할 수 있는데, 모듈의 base package는 외부에 공개하고, 하위 ᅦackage는 내부 구현으로 취급한다. 하지만 현재 프로젝트의 Domain Package 구조는 이 모델과 맞지 않았다. Domain Module을 OPEN으로 event └── domain ├── event │ ├── domain │ ├── repository │ └── service │ ├── event2 │ ├── domain │ ├── repository │ └── service │ └── event3 다음과 같이 공개해야 할 타입들이 모두 하위 패키지에 존재한다. 그래서 Domain Module에서는 OPEN Module을 사용했다. @ApplicationModule( type = ApplicationModule.Type.OPEN ) 이를 통해 하위 패키지에 있는 공개하는 타입을 외부 모듈에서 사용할 수 있다. 내부 구현도 열리는 문제 위에서 정한 OPEN으로 인해 service.impl까지 Modulith 수준에서 열리게 된다. 따라서 Java 접근 제어와 ArchUnit을 함께 사용했다. 구현체는 package-private Service의 공개 인터페이스는 다음과 같이 public으로 선언했다. public interface EventService { // ... } 구현체에는 public을 붙이지 않았다. @Service @RequiredArgsConstructor class EventServiceImpl implements EventService { // ... } private 클래스 이므로 다른 package에서 직접 사용할 수 없어, API는 공개 인터페이스만 의존하고 구현체에 직접 의존할 수 없다. ArchUnit 앞선 글에서도 다뤘던 것 처럼 private로 선언하자는 약속은 규칙일 뿐이다. 따라서 이를 검증하고 강제하기 위해 ArchUnit을 사용했다. ArchUnit은 Java코드의 구조적인 규칙을 테스트 코드로 작성할 수 있게 해준다. Controller가 Repository를 직접 참조하지 않나? 특정 package의 클래스가 public이 아닌가? Gradle + Modulith + ArchUnit 프로젝트에 도입한 다음 3가지는 서로 다른 경계를 담당하고 있다. 각각 Gradle Module 사이의 의존성, Application Module 사이의 공개 경계, service.impl과 같은 프로젝트 내부 규칙 Gradle 가장 큰 물리적인 경계를 담당한다. dependency를 통해 컴파일 단계 실패하도록 한다. Spring Modulith Spring Application을 Application Module로 바라본다. verify()를 통해 모듈 사이의 경계와 의존 관계를 검증한다. public이고 Dependency가 존재하더라도 Application Module의 공개 영역이 아니면 문제가 된다. ArchUnit 프로젝트가 정의한 세부적인 코드 구조를 검증한다. 의존성을 없애는 것이 목적이 아니다 모듈화를 공부하며 다른 모듈에 의존하지 않는 것이 좋은 구조인가? 라는 생각을 가졌다. 하지만 실제 코드에서는 모듈 사이의 의존성이 존재한다. 예로 controller에서 Domain Service를 사용한다. 여기서 중요한 것은 어떤 방향의 의존성을 허용할 것인지를 결정하는 것 이다. 외부 모듈이 구현체에 직접 의존하지 않고 공개된 계약을 사용하도록 설계했다. 따라서 모듈화의 목적 : 필요한 의존성을 명시적이고 안정적인 방향으로 만드는 것 이라고 이해하게 되었다. @ 실제 규칙을 강제하는 효과를 지니려면 CI까지 연결해 Merge를 막아야한다.(참고) CLOSED 구조를 사용하지 않은 이유 처음부터 CLOSED 기본 구조에 맞춰 패키지를 구성했다면 더 단순하지 않았을까? 하는 생각을 가지게 되었다. 하지만 현재 프로젝트는 하나의 Domain Module 안에서 다시 도메인과 계층을 표현하는 패키지 구조를 선택했다. Spring Modulith의 기본 모델에 프로젝트 전체 구조를 억지로 맞추기보다, 필요한 부분만 사용하고 부족한 세부 경계는 다른 도구로 보완한 것이다. 정리 지난 글에서 Gralde Multi-Module을 정리할 때는 의존하지 않은 모듈을 complie classpath에서 제거하면 아키텍처 경계를 강제할 수 있다 가 핵심이었다면 이번 글에서는 더 안쪽의 경계를 살펴봤다. Gradle은 어떤 모듈의 타입을 complie classpath에서 볼 수 있는지 결정 Spring Modulith는 Java Package를 Application Module로 해석하고 verify()를 통해 모듈 경계를 검증 ArchUnit은 프로젝트에서 정의한 세부 구조 규칙을 검증 프로젝트를 진행하며 경험한 실제 사례를 통해 컴파일 가능하다는 사실과 아키텍처상 올바른 의존은 다르다는 것을 확인할 수 있었다. Modular Monolith의 경계는 하나의 도구과 아니라 알맞은 도구를 이용해 코드 간의 공개 여부를 명시하고 검증하는 것이라고 이해하게 되었다. 다음 글에서는 이렇게 구성한 모듈 경계 안에서 비즈니스 로직을 어떻게 구성하는지 정리해보려고 한다. Port/Adapter 구조와 비즈니스 규칙을 Service가 아닌 Domain Object에 두면서 어떤 차이가 생겼는지 정리해보려고 한다!
0. 서론 추석동안 휴가를 잘 즐기고 왔어.. 최근에 조현병 진단이 임시로 떠서, 걱정도 많고 불안도 많은 나날이 계속되고있음.. 뭐 잡설은 이쯤 하고, 지난번엔 사실상 캐시 메모리의 동작 원리(?)라고 할 수 있는 지역성(Locality)을 공부했었지. 지역성은 시간적 지역성 한번 접근한 메모리는 다시 접근될 가능성이 높다. 공간적 지역성 한번 접근한 메모리의 근처 주소는 다시 접근될 가능성이 높다. 이런 두 가지의 성질이었고, 그 두 가지 성질을 잘 만족하는 배열 접근에서의 stride-1 참조 패턴까지 알아봤었지. 메모리에서 배열의 각 원소를 접근할 때 바로 옆에 있는 것을 먼저 접근(원소와 원소 사이의 stride(간격)이 1)하는 식의 참조 패턴이었지. 저번 글은 특히 짧았어. 사실상 방금 설명한게 끝이어서..ᄏᄏ 이번 글도 짧을 예정이야. The Memory Hierarchy(메모리 계층)거든.. 메모리 계층은, (1번이 최상위 계층, 7번이 최하위 계층) 레지스터 L1 캐시 L2 캐시 L3 캐시 메인 메모리 로컬 디스크 원격 디스크 이 순서로, 아랫쪽 계층일수록 가격은 싸지고 속도는 느려지고 용량이 커져서 하위 계층의 데이터 중 일부가 상위 계층으로 캐싱되게돼. 추후 캐시 메모리 파트에서는 원하는 정보가 상위 계층에 캐싱되지 않았을 때 더 밑의 계층에서 캐싱해오는 그런 것도 나올텐데, 그러면 서론이 너무 길어지겠네.. 이번에는, 6챕터 - The Memory Hierarchy의 The Memory Hierarchy - 메모리 계층 챕터를 공부했어. 캐싱 관점의 메모리 계층이지. 6.3.1 : Caching in the Memory Hierarchy - 메모리 계층에서의 캐싱 이게 끝이야..ᄏᄏ 1. Caching in the Memory Hierarchy - 메모리 계층에서의 캐싱 정리 메모리 계층 구조의 핵심 더 높은 계층의 저장 장치는 더 낮은 계층의 캐시의 역할을 한다. 예 : 로컬 디스크는 원격 디스크의 캐시 역할을 하며, 메모리는 로컬 디스크의 캐시 역할을 한다. 이와 같은 방식은 최상위 계층인 레지스터에 도달할 때까지 이어진다. 상위 계층의 저장 장치에는 하위 계층 저장 장치의 데이터 중 일부가 복사되어 저장되며, 이걸 캐싱이라고 한다. 하위 계층일 수록 한 번에 캐싱하는 크기가 점점 더 커진다. 접근 시간이 느리니, 한 번에 많은 데이터를 가져오기 위함이다. 예 : 레지스터 : 4 / 8바이트 워드 L1 캐시 ~ L3 캐시 : 64바이트 블록 가상 메모리 : 4KB 페이지 등등.. 캐시 히트 프로그램이 하위 계층의 데이터가 필요할 때, 우선적으로 상위 계층의 저장 장치에서 해당 데이터를 찾는다. 만약 해당 데이터가 상위 계층에 캐싱되어있다면, 그걸 캐시 히트(Cache Hit)라고 한다. 일반적으로 상위 계층에 접근하는 속도가 훨씬 빠르기 때문에, 캐시 히트가 많이 발생하는 프로그램이 빠르다. 캐시 미스 만약 상위 계층에 해당 데이터가 캐싱되어있지 않다면 그걸 캐시 미스(Cache Miss)라고 한다. 만약 캐시 미스가 발생하면, 프로그램은 더 하위 계층으로 내려가 해당 데이터를 가져오고, 상위 계층에 캐싱한다. 상위 계층이 가득차있는 경우 기존 블록을 덮어쓸 수도 있다. 어떤 블록을 덮어쓰는지는 캐시의 정책을 따른다. 예 : LRU(Least Recently Used) 방식을 사용하는 캐시는 가장 오래전에 사용된 블록을 덮어쓴다. 일반적으로 하위 계층에 접근하는 속도가 상위 계층에 접근하는 속도보다 훨씬 느리기 때문에, 캐시 미스가 많이 발생하는 프로그램은 느리다. 캐시 미스의 종류 캐시 미스는 여러 종류가 있고, 캐시 미스의 종류를 구분해두는 것이 때때로 도움이 된다. 콜드 미스(Cold Miss) : 아예 캐시가 비어있는 상태에서 나는 캐시 미스. 해당 캐시 미스는 점차 캐시가 예열되며 해소된다. 캐시의 데이터 배치에 의해 발생하는 캐시 미스 캐시에 데이터가 캐싱될 때의 데이터는 특정 정책에 의해 배치된다. 충돌 미스(Conflict Miss) : 캐시가 충분히 크지만, 특정 블록에만 데이터들이 몰려서 배치되어서 발생하는 캐시 미스. 용량 미스(Capacity Miss) : 캐시 자체가 작업이 필요로 하는 데이터 집합(작업 집합)의 데이터들을 모두 저장하기에는 부족해서 발생하는 캐시 미스. (캐시 미스라고 하면 가장 먼저 떠올릴만한 캐시 미스) 캐시 관리 앞서 설명했듯이, 메모리 계층 구조의 핵심은 각 계층의 저장 장치가 바로 다음 하위 계층을 위한 캐시 역할을 한다는 점이다. 그래서, 각 계층에서는 특정 형태의 로직으로 캐시를 관리해야 한다. 캐시 저장 공간을 분할하고, 서로 다른 계층 간에 블록을 전송하고, 캐시 히트와 캐시 미스를 판단하고, 이에 따라 적절한 조치를 취해야 한다. 캐시를 관리하는 로직은 하드웨어 적 / 소프트웨어 적 / 둘 다의 형태일 수 있다. 느낀 점 & 배운 점 솔직히 지금까지는 캐싱이 그냥 캐시 메모리에서만 일어난다는 생각이 꽤 있었는데, 사실상 메모리 계층 구조가 거대한 캐시 구조라는걸 체감한 것 같아. 하위 계층의 데이터가 점점 더 상위 계층으로 캐싱되고, 상위 계층에서 우선적으로 데이터를 찾고 못 찾으면 다음 하위 계층으로 내려가서 찾고.. 그냥 이거야. 그리고, 캐시 미스의 종류들은 꽤 흥미로웠어. 캐시 예열이 안되어서(캐시 자체가 비어있어서) 발생하는 콜드(Cold) 미스 캐시에 공간은 있지만 같은 블록에 여러 하위 계층의 데이터가 배치되어 발생하는 충돌(Conflict) 미스 작업에 필요한 데이터 집합(작업 집합) 자체가 캐시의 용량보다 커서 생기는 용량(Capacity) 미스 이렇게 있었고, 내가 평소에 생각했던 캐시 미스는 용량 미스인 것 같더라고. 그리고 사실상 특정 계층은 한 단계 하위 계층의 캐시 역할을 하니까, 각 계층은 각자의 방식으로 캐시를 관리하는게 참 신기했어. 방금 전에 말했듯이, 그 전에는 캐싱이 캐시 메모리에서만 일어난다고 생각하고 있었기 때문에..