안녕하세요! 그로스로그 입니다!😊 🌱
논리는 맞는데 시간이 초과되는 코드, 관계는 제대로 맺었는데 JSON으로 못 바꾸는 객체, 그리고 웹에서 잘 쓰던 방식이 모바일에서 무거워지는 이유. 3기 8회차 세 편입니다.
3기 8회차, 2025년 7월 중순입니다. 이번 회차에는 맞는데 안 되는 경우를 다룬 세 편이 모였어요.
01
⏱️ 백준 10845 큐 — 시간초과 에러 해결
이혜원 님
백준 큐 문제를 풀다 시간초과에 계속 막힌 기록입니다.
형식이 단순해요. 실패 코드 전문 → 성공 코드 전문 → 수정사항 세 가지요.
두 코드의 로직은 같습니다. 바뀐 건 자료구조와 입력 방법뿐이에요.
마지막 한 줄이 정직합니다. "
input()이 오래 걸리는 줄 몰랐었네요.."3기 3회차에서 이분의 쿠버네티스 노트를 소개해드렸습니다. 이번엔 코딩 테스트 준비 기록이에요.
조금 더 들어가 보면
🧾 코드 두 벌을 통째로
이 기록의 형식이 특이합니다. 실패한 코드를 전부, 성공한 코드도 전부 올렸어요.
두 코드를 나란히 보면 함수 여섯 개(push·pop·size·empty·front·back)가 거의 같습니다. 다른 게 세 군데뿐이에요.
# 실패 queue = [] print(queue.pop(0)) N = int(input()) # 성공 queue = deque() print(queue.popleft()) N = int(sys.stdin.readline())
로직이 틀린 게 아니었다는 걸 두 코드를 다 올려야 보여줄 수 있습니다. 성공 코드만 올렸다면 "이렇게 푸는구나"로 끝났을 거예요.
🐌 리스트 맨 앞을 빼면
첫 번째 수정이 list에서 deque로 바꾼 것입니다.
list → deque 사용2.
queue.pop(0) → queue.popleft() 사용문제는 pop(0)입니다.
파이썬 리스트는 요소가 메모리에 나란히 붙어 있습니다. 맨 앞을 빼면 뒤에 있는 걸 전부 한 칸씩 당겨야 해요. 요소가 만 개면 만 번을 옮깁니다.
그러니 pop(0) 한 번이 O(n)이고, 그걸 만 번 하면 O(n²)이 됩니다. 3기 2회차 큐레이션에서 소개해드린 조민재 님의 시간복잡도 이야기와 같은 자리예요. 보이지 않는 루프가 메서드 안에 있는 것요.
deque는 양쪽 끝에서 넣고 빼도록 만들어진 자료구조입니다. 내부가 조각 단위로 이어져 있어서 앞을 빼도 뒤를 당길 필요가 없어요. popleft()가 O(1)입니다.
여기서 재미있는 점은 문제 이름이 「큐」라는 겁니다. 큐를 구현하라는 문제인데, 큐에 맞는 자료구조를 쓰지 않으면 시간초과가 나는 거죠. 자료구조를 고르는 것 자체가 문제의 일부였던 셈입니다.
⌨️ input()이 느린 이유
세 번째 수정이 이 기록에서 가장 널리 쓰이는 정보입니다.
input() → sys.stdin.readline() 사용input()이 오래 걸리는 줄 몰랐었네요..왜 느릴까요.
input()은 한 줄을 읽을 때마다 몇 가지 일을 더 합니다. 프롬프트를 확인하고, 줄 끝의 개행을 떼고, 입력 버퍼를 매번 비웁니다. 한 줄이면 아무것도 아닌데 십만 줄이면 그게 전부가 돼요.
sys.stdin.readline()은 그 과정을 건너뜁니다. 대신 개행 문자가 붙어 오니 숫자로 바꿀 때는 상관없고, 문자열로 쓸 땐 .strip()을 붙여야 하죠. 이 코드에서는 int()와 .split()을 쓰니 문제가 없습니다.
언어의 편의 기능이 성능의 발목을 잡는 경우입니다. 알고리즘을 아무리 잘 짜도 입력에서 걸리면 소용이 없어요.
📝 원문에서는 이렇게 씁니다
시작이 감정입니다.
"계속"이라는 단어가 있습니다. 한 번에 안 풀렸다는 뜻이죠.
그리고 원인을 두 개로 나눠 적었어요.
input()을 통해 입력을 받아왔었는데 이걸 수정해줬더니 잘 되었다.둘 다 고쳐서 통과했다는 것. 어느 쪽이 결정적이었는지는 밝히지 않았습니다. 그게 정직한 기록이기도 해요. 하나씩 바꿔가며 재보지 않았으니까요.
이분의 코테공부 카테고리를 보면 이 글 다음이 「11723 집합 / 비트마스크, 메모리 초과」입니다. 시간초과 다음이 메모리 초과예요. 같은 종류의 벽을 계속 만나면서 하나씩 넘고 있습니다.
큐레이터 노트
틀리지 않았는데 안 된 경우라 골랐습니다.
코딩 테스트를 처음 준비하면 이 벽에서 한 번 당황합니다. 답은 맞는데 통과가 안 되니까요. 로직을 다시 봐도 틀린 데가 없고요.
그때 봐야 하는 게 자료구조와 입출력입니다. 이 기록이 정확히 그 둘을 짚었어요. 그리고 두 코드를 통째로 올려서 로직은 그대로였다는 걸 보여줬습니다.
sys.stdin.readline()은 파이썬으로 코테를 준비하는 사람이라면 반드시 만나는 이야기인데, "몰랐었네요"라는 발견의 순간과 함께 적혀 있는 게 이 기록의 성격입니다.
02
🔁 UserDetails와 무한 순환 참조
이민경 님
두 주제를 한 글에 담았습니다. Spring Security의
UserDetails 인증 흐름과 무한 순환 참조요.뒷부분이 이 글의 중심입니다. 정의 → 발생 시점 세 가지 → 원인 세 가지 → 해결 방안 다섯 가지로 갑니다.
그리고 자기가 겪은 게 어느 경우인지 밝혔어요. "내가 이번에 겪었던 문제는 세 번째 경우였다."
도입이 재미있습니다. 몸이 세 개였으면 좋겠다는 이야기로 시작해요.
3기 6회차에서 이분의 JWT 정리를 소개해드렸습니다. 그때가 토큰이었다면 이번은 인증 이후의 사용자 정보예요.
조금 더 들어가 보면
🔗 로그인이 지나가는 여섯 단계
앞부분이 UserDetails의 사용 흐름입니다. 여섯 단계로 나눴어요.
AuthenticationFilter → AuthenticationManager → DaoAuthenticationProvider → UserDetailsService.loadUserByUsername(username) 호출 → Authentication 객체 생성·저장 → 보안 컨텍스트 유지3기 6회차에서 소개해드린 최태형 님의 정리와 같은 부품들이 나옵니다. 그쪽이 구성 요소가 각각 무엇인지를 표로 정리했다면, 이 글은 그것들이 순서대로 어떻게 이어지는지를 적었어요.
그리고 우리가 직접 만드는 자리가 어디인지 짚었습니다.
loadUserByUsername — DB에서 UserEntity 조회 → CustomUserDetails로 변환 후 반환여기 하나만 구현하면 됩니다. 나머지는 프레임워크가 부르고요. 그래서 이 메서드가 "DB에 사용자가 있는지 확인하는" 자리인 게 중요합니다. 없으면 예외를 던지고, 있으면 UserDetails 모양으로 바꿔 돌려주는 것요.
마지막 단계도 짚었습니다.
SecurityContextHolder.getContext().getAuthentication() 통해 현재 로그인 사용자 정보 사용 가능6회차에서 본 SecurityContextHolder가 여기서 다시 나옵니다. 인증이 끝나면 그 결과가 거기 담기고, 이후 코드는 거기서 꺼내 쓰는 거예요.
♾️ 세 군데서 터진다
무한 순환 참조의 정의는 간단합니다.
그런데 문제가 터지는 시점을 셋으로 나눈 게 이 정리의 핵심입니다.
로깅(
toString 호출): Lombok의 @Data 또는 IDE가 자동 생성한 toString()으로 출력할 때스프링 빈 의존 주입: A 빈이 B 빈을, B 빈이 다시 A 빈을 주입받을 때
셋의 성격이 다릅니다.
앞의 둘은 객체를 펼치려다 터집니다. A를 펼치려니 B가 있고, B를 펼치려니 A가 있고, 끝이 없어요. 스택이 넘칠 때까지 갑니다.
세 번째는 다릅니다. 만드는 단계에서 막혀요. A를 만들려면 B가 필요하고 B를 만들려면 A가 필요하니, 둘 다 못 만듭니다. 애플리케이션이 아예 안 뜹니다.
그리고 원인 쪽에서 중요한 지적이 나옵니다.
@OneToMany ↔ @ManyToOne자동 생성된
toString/equals/hashCode — Lombok @Data, IDE의 자동 생성 메서드@Data 하나가 문제를 만듭니다. 양방향 매핑 자체는 정상적인 설계인데, 거기에 @Data를 붙이면 toString()이 자동으로 생기고 그게 상대를 펼치려 들거든요.
설계가 틀린 게 아니라 편의 기능이 겹쳐서 생긴 문제입니다.
🧯 다섯 가지 해법이 층위가 다르다
해결 방안이 다섯 개인데, 성격이 갈립니다.
Jackson 어노테이션은 직렬화 방향을 정해줍니다.
@JsonManagedReference가 붙은 쪽만 직렬화반대쪽(
@JsonBackReference)은 역직렬화 시점에만 사용한쪽만 펼치게 만드는 것이죠. 부모에서 자식은 보이고, 자식에서 부모는 안 보입니다.
@JsonIdentityInfo는 다르게 풉니다.
두 번째부터는 ID만 적는 방식입니다. 순환을 끊는 게 아니라 반복을 끊어요.
세 번째가 가장 근본적입니다.
순환 참조 없이, 구조를 평탄화(flatten)하여 전송
public class ChildDto {
private Long id;
// ParentDto 대신 parentId만 노출
private Long parentId;
}
엔티티를 밖으로 내보내지 않는 것입니다. 순환을 다루는 기술을 쓰는 게 아니라, 순환이 있는 객체를 애초에 안 보냅니다.
이 방식이 나은 이유가 순환 말고도 있습니다. 엔티티를 그대로 반환하면 DB 컬럼이 곧 API 응답이 되거든요. 컬럼 하나 추가했을 뿐인데 API가 바뀌고, 감춰야 할 필드가 새어 나갑니다.
그리고 스프링 빈 순환에는 별도 해법이 붙었습니다.
@Lazy구조 재설계
@Lazy를 먼저 적고 재설계를 뒤에 적었지만, 실무에서는 순서가 반대일 때가 많습니다. 빈이 서로를 필요로 한다는 건 책임 분리가 덜 됐다는 신호이기도 하거든요.
3기 6회차에서 소개해드린 최준성 님의 OCP·DI 글과 이어지는 지점입니다. 의존성을 밖에서 주입받는 구조가 좋은데, 그 의존이 원을 그리면 또 다른 문제가 되는 것요.
📝 원문에서는 이렇게 씁니다
도입이 개발 이야기가 아닙니다.
그러나 몸이 정말 세 개였다면...필시 한 명은 굶어죽었을 것이다. 아무래도 주민번호 하나로 세 명이 살기란 쉽지 않은 일이니까...
그러니까 성장일지도 내가 써야겠지...?
바쁘다는 말을 이렇게 했습니다.
그리고 무한 순환 참조를 다시 적는 이유도 밝혔어요.
같은 에러를 두 번째 만난 기록입니다.
처음엔 검색해서 어노테이션 하나 붙이고 넘어갔을 거예요. 이번엔 발생 시점을 셋으로 나누고 해법을 다섯 가지로 정리했고요. 같은 에러인데 남는 게 다릅니다.
6회차에서 이분이 "작년까지만 해도 그냥 복붙하고 실행되면 일단 되는구나 했는데"라고 적었던 것과 같은 흐름이에요.
큐레이터 노트
두 번째 만난 에러를 다시 쓴 것이 이 글을 고른 이유입니다.
기록의 값어치는 새로운 걸 적을 때만 생기는 게 아닙니다. 이미 겪은 걸 다시 적을 때 그 사이의 성장이 드러나거든요.
그리고 "내가 이번에 겪었던 문제는 세 번째 경우였다"는 한 줄. 세 가지를 다 정리해두고 자기 경우를 표시했어요. 다음에 다른 경우를 만나면 나머지 두 개가 그대로 쓰입니다.
UserDetails 부분과 무한 순환 참조를 한 글에 담은 것도, 실은 같은 프로젝트에서 나온 두 가지라 자연스럽습니다. 인증을 붙이다 보면 사용자와 권한이 양방향으로 얽히기 마련이니까요.
03
📱 React Native에서 효과적인 UI 그리기
김주현 님
React Native의 스타일링 방법을 비교한 글입니다. 이번 회차에서 가장 깁니다.
시작이 웹과의 차이예요. 그리고 웹에서 인기 있는 Styled Components가 왜 React Native에서는 잘 안 쓰이는지를 파고듭니다.
이유를 성능과 제약 두 갈래로 나눠 적었어요.
그다음 대안 세 가지를 놓고 비교합니다. StyleSheet · NativeWind · Tamagui요.
조금 더 들어가 보면
🌉 브릿지를 건너야 한다
시작이 근본적인 차이입니다.
React Native: 네이티브 컴포넌트 사용 / JavaScript 객체로 스타일링 / 실제 네이티브 UI 컴포넌트로 변환
"변환"이 여기서 중요한 단어입니다.
웹에서는 CSS가 브라우저에 바로 먹힙니다. React Native에서는 JavaScript에서 만든 스타일이 네이티브 쪽으로 건너가야 해요. 그 건너가는 과정에 비용이 있습니다.
그래서 Styled Components의 문제가 이렇게 정리됩니다.
메모리 사용량 증가 : 스타일 객체가 계속 생성됨
네이티브 브릿지 비용 : JavaScript에서 네이티브로의 변환 비용 증가
웹에서는 문제가 안 되던 게 여기서는 문제가 됩니다.
Styled Components는 props에 따라 스타일을 그때그때 만듭니다. 웹에서는 CSS 클래스를 만들어 붙이면 끝인데, React Native에서는 매번 새 객체를 만들어 브릿지 너머로 보내야 해요.
3기 7회차 큐레이션에서 소개해드린 박상훈 님의 Flutter 최적화와 같은 이야기입니다. 불변인 것을 불변이라고 알려주면 건너갈 일이 줄어드는 것요.
🚫 아예 안 되는 것들
성능 다음이 제약입니다. 그리고 이쪽이 더 직접적이에요.
box-shadow: 0 4px 6px rgba(0,0,0,0.1); // ❌ 지원 안함 background: linear-gradient(45deg, red, blue); // ❌ 지원 안함 transform: skew(10deg); // ❌ 지원 안함
성능이 아니라 그냥 없는 기능입니다. 그림자도 그라디언트도 웹에서는 한 줄인데, 여기서는 다른 방법을 찾아야 해요.
그리고 플랫폼 차이가 이어집니다.
${Platform.OS === 'ios' ? `
shadow-color: #000;
shadow-offset: 0px 2px;
shadow-opacity: 0.1;
shadow-radius: 4px;
` : `
elevation: 4;
`}
같은 그림자인데 iOS와 Android가 완전히 다릅니다. iOS는 네 가지 속성으로 세밀하게 지정하고, Android는 elevation 숫자 하나예요.
이건 두 운영체제가 그림자를 다루는 방식 자체가 다르기 때문입니다. Android의 elevation은 높이를 주면 시스템이 그림자를 계산하는 방식이라, 방향이나 색을 따로 정할 수가 없어요.
"한 번 쓰면 양쪽에서 도는" 것이 크로스 플랫폼의 약속인데, 실제로는 이런 자리가 남습니다.
📐 미리 만들어두는 방식
권장하는 대안이 StyleSheet입니다.
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#f5f5f5', padding: 20 },
title: { fontSize: 24, fontWeight: 'bold', color: '#333', marginBottom: 20 }
});
메모리 효율성 : 스타일 객체 재사용
"미리 등록"이 핵심입니다.
StyleSheet.create로 만든 스타일은 앱이 시작할 때 한 번 네이티브 쪽에 등록됩니다. 그다음부터는 숫자 ID만 넘겨요. 매번 객체를 통째로 보내는 것과 비교가 안 되죠.
그리고 이 선택의 배경도 밝혔습니다.
제약이 큰 환경에서 나온 판단이라는 것. 여유 있는 최신 기기라면 다른 선택도 가능하겠지만, 자원이 빠듯하면 브릿지 비용 하나가 크게 다가옵니다.
대안 두 가지도 함께 소개했습니다.
Tamagui — 성능에 특화된 React Native UI 라이브러리
NativeWind가 성능 문제를 피하는 방식이 눈에 띕니다. 문법은 Tailwind인데 빌드할 때 스타일을 만들어두는 거예요. 결국 StyleSheet와 비슷한 자리로 가는 셈입니다.
런타임에 만드느냐 미리 만드느냐가 갈림길이라는 게 세 대안을 나란히 놓으면 보입니다.
📝 원문에서는 이렇게 씁니다
이 글의 구성이 비교 → 이유 → 대안 순서입니다.
그리고 "안 쓰는 이유"에 가장 많은 분량을 썼어요. 성능 세 가지, 제약 두 가지로 나누고 각각에 코드를 붙였습니다.
안 쓰는 이유를 자세히 적는 게 이런 글에서 중요합니다. "React Native에서는 StyleSheet를 쓰세요"만 있으면 웹에서 넘어온 사람이 납득을 못 하거든요. 익숙한 도구를 버려야 하는 이유가 필요합니다.
웹 기준을 먼저 놓은 것도 같은 배려입니다.
아는 것에서 출발해 모르는 것으로 가는 순서예요.
큐레이터 노트
익숙한 도구를 안 쓰는 이유를 파고든 글이라 골랐습니다.
웹에서 모바일로 넘어오면 도구를 그대로 가져가고 싶어집니다. 문법이 같으니 될 것 같거든요. 실제로 styled-components/native가 있기도 하고요.
그런데 되는 것과 맞는 것은 다릅니다. 이 글은 그 차이를 성능과 제약 두 축으로 설명했어요.
그리고 마지막의 실무 맥락. 자원이 빠듯한 기기로 옮기는 중이라 성능을 먼저 봤다는 것요. 선택에 조건이 붙어 있으면 읽는 사람이 자기 상황과 대볼 수 있습니다.
3기 7회차 큐레이션에서 소개해드린 박상훈 님의 Flutter 최적화와 나란히 읽으면 좋습니다. 다른 프레임워크인데 같은 결론에 닿아요. 런타임에 만드는 것을 줄이라는 것요.
💡 세 편을 겹쳐 읽으며
세 편이 다루는 게 다릅니다. 코딩 테스트, 스프링 엔티티, 모바일 스타일링이요.
그런데 셋 다 논리는 맞는데 안 되는 상황을 다룹니다.
틀린 게 아니라 안 맞는 것
이혜원 님의 큐 코드는 알고리즘이 맞습니다. 결과도 정확하고요. 그런데 자료구조가 안 맞아서 시간이 초과됐어요.
이민경 님의 양방향 매핑도 설계가 맞습니다. 부모가 자식을 알고 자식이 부모를 아는 건 정상적인 구조예요. 그런데 JSON으로 바꾸려는 순간 끝이 없어집니다.
김주현 님의 Styled Components도 문법이 맞습니다. 실제로 동작하고요. 그런데 브릿지를 건너야 하는 환경에서는 비용이 큽니다.
셋 다 "틀렸다"가 아니라 "이 자리에는 안 맞는다"입니다. 그래서 코드만 다시 봐서는 안 풀려요. 환경과 제약을 봐야 합니다.
편의 기능이 발목을 잡는다
세 경우 모두 편하려고 만든 것이 문제가 됐다는 점도 같습니다.
input()은 프롬프트를 처리해주고 개행을 떼어주는 친절한 함수입니다. 그 친절함이 십만 번 쌓이면 시간초과가 되고요.
Lombok의 @Data는 toString을 자동으로 만들어주는 편한 기능입니다. 그게 양방향 관계를 만나면 무한 루프가 되죠.
Styled Components는 props에 따라 스타일을 만들어주는 유연한 도구입니다. 그 유연함이 매번 객체를 새로 만들게 만들고요.
공짜로 편해지는 건 없다는 게 셋에서 반복됩니다. 대개는 그 비용을 안 느끼는데, 규모가 커지거나 환경이 빠듯해지면 드러납니다.
그래서 미리 만들어둔다
해법의 모양도 닮았습니다. 런타임에 하던 일을 앞으로 당기는 것요.
deque는 앞뒤로 빼도록 미리 만들어진 자료구조입니다. 쓸 때마다 옮기지 않아요.
DTO는 내보낼 모양을 미리 정해둔 것입니다. 엔티티를 그때그때 펼치지 않고요.
StyleSheet.create는 앱 시작할 때 한 번 등록합니다. 렌더링마다 만들지 않죠.
3기 7회차의 const와 GitOps도 같은 이야기였습니다. 판단을 실행 시점에서 설정 시점으로 옮기는 것이 성능 문제의 흔한 답입니다.
이번 회차에서 가져갈 것
1. 파이썬 큐는 list 말고 deque.
list.pop(0)은 뒤를 전부 당깁니다. deque.popleft()가 O(1)이에요.
2. 시간제한 있는 문제에서는 sys.stdin.readline().
input()은 한 줄마다 부가 작업을 합니다. 십만 줄이면 그게 전부가 됩니다.
3. 양방향 매핑에 @Data를 붙이지 않기.
toString()이 자동 생성되면서 무한 루프가 됩니다. @ToString(exclude=...)로 빼거나 @Data를 피하세요.
4. 엔티티를 API로 그대로 내보내지 않기.
DTO로 평탄화하면 순환도 사라지고 DB 구조도 안 새어 나갑니다. 어노테이션으로 막는 것보다 근본적입니다.
5. React Native에서는 StyleSheet.create.
스타일이 미리 네이티브에 등록되어 ID만 오갑니다. Styled Components는 매 렌더링마다 객체를 만들어 브릿지를 건넙니다.
3기 8회차는 2025년 7월 중순이었습니다. 1학기가 끝나고 방학으로 넘어가던 무렵이에요.
코딩 테스트와 스프링 프로젝트와 모바일 마이그레이션이라는 서로 다른 자리에서, 세 분이 나란히 맞는데 안 되는 순간을 기록했습니다.
성장일지는 멤버들이 2주에 한 번씩 자신의 성장을 기록하는 활동이에요. 지난 기수의 성장일지를 다시 읽으면서, 그때 소개해드리지 못한 글을 하나씩 꺼내고 있습니다.
좋은 글 남겨주신 이혜원 님, 이민경 님, 김주현 님께 감사드립니다. 그리고 3기를 함께 채워주신 모든 멤버분들께도요! 😊
다음 큐레이션으로 다시 찾아오겠습니다! 🌱
- 에디터 · 성장일지 큐레이터 -
※ 본 큐레이션은 각 저자가 공개한 글을 소개하는 것이며, 모든 원문의 저작권은 저자에게 있습니다. 저자 본인의 요청이 있을 경우 즉시 수정 또는 삭제합니다.