GROWTH LOG STORIES

함께 만든 성장을,
기록으로 연결합니다.

AI와 함께 배우고, 직접 만들고, 서로 나눈
그로스로그 멤버들의 생생한 성장 기록을 만나보세요.

성장 기록
5기+이어진 커뮤니티
HEALTHIO
PROJECT

사진 한 장으로 시작하는 건강 루틴, Health IO

대표 이야기 읽기

ALL STORIES

성장의 순간을
한곳에서 만나보세요.

프로젝트의 시작부터 모임에서 나눈 인사이트까지,
그로스로그가 함께 만든 기록을 모았습니다.
개의 성장 기록

성장일지

[GL 2기] 0000-00-00이라는 날짜가 남긴 것, 그리고 VR을 위한 언리얼 공부 노트 (성장일지 12회차 큐레이션) 🌱

GROWTH LOG🌱 2026. 8. 7. 04:34
GL 2기 성장일지 12회차 큐레이션

안녕하세요! 그로스로그 입니다!😊 🌱

DB에 잘못 들어간 데이터 하나가 API를 멈추게 한 이야기, 그리고 VR 프로젝트를 위해 쌓아둔 언리얼 공부 노트. 2기의 마지막 회차입니다.

성장일지는 멤버들이 2주에 한 번씩 자신의 성장을 기록하는 활동이에요. 매 회차 운영진이 모든 글을 읽고, 그중 특히 마음에 남은 글을 함께 골라 큐레이션합니다.

 

01

📅 JpaSystemException — 0000-00-00이라는 날짜

오연수 님

이런 내용이에요
기존 프로젝트에 기능을 추가하며 DB 스키마를 변경합니다. 그런데 그 뒤로 멀쩡히 돌던 API 몇 개가 400 에러를 내기 시작해요.
원인은 코드가 아니라 데이터였습니다. birthdate 컬럼에 0000-00-00, 2000-00-00 같은 값이 들어 있었고, 이걸 LocalDate로 바꾸려다 터진 거예요.
AttributeConverter로 해결하고, 마지막에 "애초에 이런 데이터가 없었다면 필요 없었을 대응"이라고 스스로 정리합니다.

에러 메시지가 무섭게 생겼습니다. Could not extract column [3] from JDBC ResultSet [MONTH]. 그런데 원인은 아주 단순한 곳에 있었어요.

조금 더 들어가 보면

먼저 이 에러의 정체부터 보겠습니다.

{
  "code": 400,
  "message": "Bad Request / Could not extract column [3] from JDBC ResultSet [MONTH] [n/a] / org.hibernate.exception.GenericJDBCException ...",
  "path": "/v1/user",
  "method": "GET"
}

[MONTH]라는 단어가 힌트입니다. 월(月)을 꺼내다가 실패했다는 뜻이에요.

원인은 이렇습니다. birthdate 컬럼에 0000-00-00이나 2000-00-00 같은 값이 저장되어 있었습니다. 자바의 LocalDate실제로 존재하는 날짜만 담을 수 있어요. 0월이나 0일은 세상에 없으니 변환이 실패합니다.

여기서 궁금해지는 게 있습니다. 애초에 그런 값이 어떻게 DB에 들어갔을까요?

MySQL의 오래된 기본 설정에서는 0000-00-00허용되는 값이었습니다. 날짜를 모를 때 쓰는 일종의 빈칸 표시였어요. 표준에 맞지 않지만 관행적으로 쓰이던 값입니다. 그러다 자바 쪽에서 이 값을 읽으려는 순간 충돌이 나는 거죠.

DB는 받아줬는데 언어는 못 받는 상황입니다. 두 시스템의 "유효한 날짜" 기준이 달랐던 거예요.

📝 원문에서는 이렇게 해결합니다

AttributeConverter를 씁니다. DB 값과 객체 값 사이를 오갈 때 끼어드는 변환기예요.

@Converter(autoApply = false)
class LocalDateConverter : AttributeConverter<LocalDate?, String?> {
    override fun convertToDatabaseColumn(attribute: LocalDate?): String? {
        return attribute?.toString()
    }

    override fun convertToEntityAttribute(dbData: String?): LocalDate? {
        return if (dbData == "0000-00-00" || dbData == "2000-00-00") {
            null
        } else {
            dbData?.let { LocalDate.parse(it) }
        }
    }
}

핵심은 convertToEntityAttribute입니다. DB에서 객체로 올 때 호출되는 쪽이에요. 여기서 문제가 되는 값을 미리 걸러 null로 바꿉니다.

그리고 엔티티에 이 변환기를 지정합니다.

@NotNull
@Convert(converter = LocalDateConverter::class) // db 0000-00-00 값 등 대응 위해 설정
@Column(name = "birthdate", nullable = false)
var birthdate: LocalDate? = null

주석까지 달아둔 게 좋습니다. 이런 변환기는 나중에 보면 "왜 있는 거지?" 싶거든요. 이유가 적혀 있으면, 데이터를 정리한 뒤 이 코드를 지워도 되는지 판단할 수 있습니다.

그리고 @Converter(autoApply = false)도 눈여겨볼 만합니다. true로 두면 프로젝트의 모든 LocalDate에 이 변환기가 자동 적용돼요. 문제는 특정 컬럼 하나인데 전체에 손대는 건 위험하죠. 필요한 곳에만 명시적으로 붙이는 선택입니다.

🧭 그리고 진짜 결론

이 글의 값어치는 마지막 문단에 있습니다.

"개발 환경에서도 데이터베이스 정합성과 도메인 무결성은 반드시 지켜야 한다. (…) 위 내용은 사실 유효한 데이터만 데이터베이스에 저장되어 있었다면, 굳이 하지 않아도 될 대응이었다."

고쳐놓고 나서 "이건 원래 안 해도 될 일이었다"고 적은 겁니다.

이 시선이 중요합니다. 개발하다 보면 이런 방어 코드가 계속 쌓여요. 이상한 값을 걸러주는 변환기, 예외 상황을 처리하는 분기, "혹시 모르니" 넣어둔 널 체크. 하나하나는 합리적인데, 모이면 코드가 데이터의 지저분함을 떠받치는 구조가 됩니다.

그래서 원인을 어디서 막을지가 갈립니다. 들어올 때 막을 것인가, 나갈 때 걸러낼 것인가. 걸러내는 쪽은 빠르지만 영원히 유지해야 하고, 막는 쪽은 힘들지만 한 번으로 끝납니다.

개발 DB에 대충 데이터를 넣는 게 왜 위험한지도 여기서 드러나요. "어차피 개발용이니까"로 들어간 값이 코드를 바꿔놓습니다. 그리고 그 코드는 운영에도 그대로 나가죠.

큐레이터 노트

고치고 나서 원인을 되짚었기 때문에 뽑았습니다.

에러를 해결한 글은 많습니다. 그런데 대부분 "이렇게 하면 됩니다"에서 끝나요. 이 글은 한 발 더 가서 "애초에 이 대응이 필요했던 이유"를 봤습니다. 그러면 다음번에는 같은 문제를 안 만들 수 있죠.

스키마를 바꾸자 기존 API가 깨졌다는 상황도 값집니다. 새 기능을 만들 때 우리는 새 코드만 봅니다. 그런데 데이터베이스는 모두가 공유하는 자산이라, 한쪽을 건드리면 전혀 상관없어 보이는 곳이 함께 흔들려요. 이걸 겪어보는 게 중요합니다.

그리고 이 저자의 2기 기록 전체를 돌아보면 흐름이 뚜렷합니다. 5회차 시큐리티 무한 루프, 6회차 클린 코드 강연 정리, 9회차 코틀린 바이트코드, 11회차 패스워드리스 구현, 그리고 12회차 데이터 정합성. 한 기수 동안 여섯 편. 2기 최다 기록이고, 그 여섯 편이 전부 다른 층위를 다룹니다. 꾸준함이 무엇을 만드는지 보여주는 기록이에요.

원문 읽으러 가기  ↗devlemon.tistory.com/22
 

02

🎮 언리얼 공부 노트

허해수 님

이런 내용이에요
언리얼 엔진으로 VR 프로젝트를 만들며 정리한 공부 노트입니다.
레퍼런스를 GameInstance에서 싱글톤으로 관리하는 방법, VR에서 후처리와 실시간 라이팅을 왜 피하는지, UAudioComponentUSoundBase의 차이, 스켈레탈 메시의 소켓까지.
정리된 완성 글이 아니라 작업하며 부딪힌 것들을 그때그때 적어둔 노트 형태예요. 그래서 오히려 실전의 결이 살아 있습니다.

성장일지에 게임 개발 기록이 올라오는 게 반갑습니다. 웹과는 고민의 종류가 아주 달라요.

조금 더 들어가 보면

이 노트에서 가장 흥미로운 부분은 VR의 성능 기준입니다.

일반 게임은 60FPS면 충분합니다. 그런데 VR은 최소 90FPS, 권장 120FPS예요. 기기에 따라 144Hz까지 갑니다.

왜 이렇게 다를까요. 노트에 정확히 적혀 있습니다. 낮은 FPS는 어지러움을 유발하기 때문입니다.

이게 VR 개발이 특수한 이유예요. 일반 게임에서 프레임이 떨어지면 사용자는 "좀 끊기네"라고 느끼고 맙니다. VR에서 프레임이 떨어지면 사람이 실제로 멀미를 합니다. 머리를 돌렸는데 화면이 따라오는 게 늦으면, 뇌가 받아들이는 정보와 몸의 감각이 어긋나거든요.

그래서 성능 문제가 곧 사용자 건강 문제가 됩니다. 최적화가 "있으면 좋은 것"이 아니라 최소 요건인 셈이에요.

🎨 후처리를 왜 걷어내는가

여기서 노트의 두 번째 주제가 나옵니다. Post Process(후처리)를 최소화하는 것.

후처리는 화면이 다 그려진 다음에 전체에 덧입히는 효과입니다. 블룸(빛 번짐), 필름 그레인(입자감), DOF(피사계 심도), SSAO(주변광 차폐) 같은 것들이요. 게임을 영화처럼 보이게 만들어주는 장치들입니다.

그런데 VR에서는 두 가지 문제가 생깁니다.

첫째, 무겁습니다. 화면 전체를 한 번 더 처리하는 일인데, VR은 그걸 양쪽 눈에 각각 해야 해요. 사실상 두 배입니다.

둘째, 이상하게 보입니다. 노트에 "이상한 시각적 효과를 유발할 수도 있음"이라고 적혀 있는데, 정확한 지적이에요. 예를 들어 DOF(초점 흐림)를 생각해보세요. 일반 게임에서는 카메라가 초점을 맞추는 곳이 정해져 있습니다. 그런데 VR에서는 사용자가 어디를 볼지 개발자가 모릅니다. 사용자가 보려는 곳이 흐려져 있으면 굉장히 부자연스럽죠.

그래서 노트는 VR에서 안전한 효과를 따로 정리해뒀습니다.

· 컬러 그레이딩 — 색감만 조정하니 안전

· 톤 매핑 — 밝기 범위 변환, 안전

· 비네트 — 화면 가장자리를 어둡게. 오히려 멀미 방지에 도움이 되기도 함

· FXAA — MSAA보다 가벼운 안티에일리어싱

비네트가 재미있습니다. 원래는 분위기를 내는 효과인데, VR에서는 빠르게 이동할 때 주변 시야를 가려 멀미를 줄이는 용도로 쓰입니다. 실제로 많은 VR 게임이 이동 중에 화면 가장자리를 어둡게 만들어요. 같은 기술이 다른 목적으로 쓰이는 사례입니다.

💡 그리고 라이팅 — 미리 구울 것인가, 매번 계산할 것인가

세 번째 주제는 라이팅입니다.

정적 라이팅(Static Lighting)은 빛과 그림자를 미리 계산해서 텍스처에 구워두는 방식입니다. 게임이 도는 동안에는 계산이 없어요. 그냥 그려진 그림을 보여주는 셈이라 아주 가볍습니다. 대신 움직이는 물체는 그 조명을 못 받습니다.

실시간 라이팅(Dynamic Lighting)은 매 프레임 빛을 계산합니다. 자유롭지만 무겁죠. 노트가 짚은 것처럼 Lumen 같은 고급 GI 시스템은 VR에 잘 안 맞습니다.

그래서 결론이 "정적 라이팅을 기본으로, 실시간은 최소한으로"가 됩니다. 중요한 캐릭터의 그림자 정도만 동적으로요.

그런데 노트에 이런 문장이 붙어 있어요.

"(실시간 라이팅도 감당 가능해 보이는데 정적 라이팅을 굳이 구현할지는 모르겠다)"

이 괄호가 좋았습니다. 정리한 내용을 그대로 받아들이지 않고 자기 상황에 비춰 의문을 남긴 겁니다. 실제로 요즘 기기 성능이면 프로젝트 규모에 따라 실시간으로도 충분할 수 있어요. 일반론과 내 프로젝트는 다르다는 걸 아는 상태입니다.

🔊 소리 — 저장하는 것과 재생하는 것

노트에서 가장 깔끔하게 정리된 부분입니다. USoundBaseUAudioComponent의 차이요.

USoundBase는 소리 파일 자체입니다. 데이터일 뿐이라 혼자서는 소리가 안 나요.

UAudioComponent는 재생기입니다. 틀고, 멈추고, 볼륨과 피치를 실시간으로 바꾸고, 액터에 붙여 3D 위치에서 소리가 나게 할 수 있습니다.

UAudioComponent* AudioComp = NewObject<UAudioComponent>(this);
AudioComp->SetSound(MySound);  // USoundBase를 설정
AudioComp->Play();

그리고 한 번만 틀 거면 컴포넌트도 필요 없다는 것까지 정리해뒀습니다.

UGameplayStatics::PlaySoundAtLocation(this, MySound, GetActorLocation());

이 부분의 결론이 실용적입니다.

"소리를 단순히 저장하려면 USoundBase, 한 번만 재생할 거면 PlaySoundAtLocation(), 실시간 조작이 필요하면 UAudioComponent, 랜덤한 사운드 믹싱이 필요하면 SoundCue."

선택 기준을 한 문장으로 만들어둔 것이 이 정리의 핵심입니다. 그리고 예시도 구체적이에요. "차량 엔진 소리처럼 볼륨과 피치가 계속 변하는 경우"요. 기준이 왜 그렇게 갈리는지가 예시 하나로 설명됩니다.

큐레이터 노트

"왜 그런지"까지 파고든 노트라서 뽑았습니다.

공부 노트는 대개 사실만 나열됩니다. "VR에서는 후처리를 피한다" 같은 식으로요. 그런데 이 노트는 "VR에서는 90FPS 이상이 필요하고, 낮으면 멀미가 나기 때문에"까지 갑니다. 이유를 알면 새로운 상황에서도 판단할 수 있어요.

의문을 그대로 남긴 것도 좋았습니다. 실시간 라이팅에 대해 "굳이 구현할지는 모르겠다"고 적어두셨는데, 이런 미해결 상태가 기록되어 있으면 나중에 답을 찾았을 때 그 지점으로 돌아올 수 있습니다.

그리고 웹 개발과 결이 완전히 다른 고민이라 반가웠습니다. 프레임 예산, 미리 굽는 라이팅, 사운드 컴포넌트의 생명주기 — 다른 영역의 개발자가 무엇을 고민하는지 엿볼 수 있는 기록이에요. 성장일지가 여러 분야의 사람들이 함께 쓰는 기록이라 생기는 장점입니다.

원문 읽으러 가기  ↗imresearcher.tistory.com/50

💡 두 편을 겹쳐 읽으며

한 편은 백엔드, 한 편은 게임 엔진입니다. 쓰는 언어도 다루는 문제도 완전히 다릅니다.

그런데 두 글이 같은 종류의 판단을 하고 있습니다. 둘 다 "미리 할 것인가, 그때그때 할 것인가"를 다룹니다.

오연수 님의 글에서 잘못된 날짜는 저장할 때 막을 수도 있고, 읽을 때 걸러낼 수도 있었습니다. 이번엔 읽을 때 걸러냈지만, 저자 스스로 "애초에 막았어야 했다"고 정리했죠.

허해수 님의 노트에서 라이팅은 미리 구워둘 수도 있고, 매 프레임 계산할 수도 있습니다. VR에서는 미리 굽는 쪽이 기본입니다. 실시간 계산이 너무 비싸니까요.

앞에서 처리하면 나중이 가볍고, 뒤로 미루면 계속 비용을 냅니다. 데이터도 그렇고 그래픽도 그렇습니다.

그리고 두 글 모두 "그래서 지금 내 상황에는 뭐가 맞는가"를 묻습니다. 정답을 옮겨 적은 게 아니라, 각자의 조건에 비춰 판단하고 그 판단에 이유를 붙였어요.

이번 회차에서 가져갈 것

1. 데이터는 들어올 때 막는 게 싸다. 나중에 걸러내는 코드는 한 번 만들면 영원히 유지해야 합니다. 개발 DB라도 아무 값이나 넣지 마세요. 그 값이 결국 코드를 바꿉니다.

2. 방어 코드에는 이유를 적기. @Convert처럼 왜 있는지 모를 코드는 나중에 지울 수도, 둘 수도 없게 됩니다. 주석 한 줄이 나중의 판단을 가능하게 해요.

3. 성능 기준은 도메인마다 다르다. 웹에서 100ms와 VR에서 11ms는 무게가 다릅니다. "이 분야에서 느리다는 게 무슨 뜻인가"를 먼저 알아야 어디를 최적화할지 정해집니다.

4. 미리 계산할 수 있으면 미리. 정적 라이팅, 캐시, 인덱스, 빌드 타임 처리. 런타임에 하지 않아도 되는 일을 앞으로 옮기는 것이 최적화의 큰 축입니다.

5. 정리한 내용에 의문을 남겨두기. "굳이 이렇게 해야 하나?"라는 괄호가 나중에 다시 찾아올 표시가 됩니다. 완결된 노트보다 열려 있는 노트가 오래 쓰여요.


🌱 2기를 마치며

이렇게 그로스로그 2기 성장일지 큐레이션이 마무리됩니다. 2024년 9월부터 2025년 2월까지, 12회차 동안 28편의 글이 쌓였어요.

돌아보면 한 사람 한 사람의 궤적이 보입니다.

오연수 님은 12회차 중 여섯 번 큐레이션되며 2기 최다를 기록하셨습니다. 시큐리티 프레임워크 내부, 클린 코드 강연, 코틀린 바이트코드, 패스워드리스 구현, 데이터 정합성까지 — 매번 다른 층위를 파고들었어요.

이종경 님은 다섯 번. Shared Worker와 WeakRef, Bun 도입, 브라우저 렌더링까지 "웹이 빠르다는 게 무엇인가"를 여러 각도에서 다뤘습니다.

이태형 님은 동기화, 테스트, 클린 아키텍처로 이어지며 혼자서 팀의 기준을 만들어가는 과정을 남겼고, 이민경 님은 베타 출시부터 PMF 고민까지 만든 것을 살아남게 하는 여정을 기록했습니다.

그리고 이범수 님, 백종오 님, 마지혁 님, 허해수 님, 박예승 님, 김경보 님까지 — 각자의 자리에서 각자의 속도로 2주에 한 번씩 기록을 남겨주셨습니다.

2주에 한 번은 크지 않습니다. 그런데 그게 열두 번 쌓이면 반년이 되고, 반년치 기록은 사람의 방향을 보여줍니다. 이번 큐레이션을 정리하며 저희가 가장 크게 느낀 부분이에요.

2기 여러분, 정말 고생 많으셨습니다. 😊


좋은 글 남겨주신 오연수 님, 허해수 님께 감사드립니다! 😊

다음은 3기 큐레이션으로 찾아오겠습니다! 🌱

- 에디터 · 성장일지 큐레이터 -


※ 본 큐레이션은 각 저자가 공개한 글을 소개하는 것이며, 모든 원문의 저작권은 저자에게 있습니다. 저자 본인의 요청이 있을 경우 즉시 수정 또는 삭제합니다.

WRITE YOUR GROWTH

오늘의 배움을
내일의 성장으로.

기록은 나를 돌아보게 하고, 공유는 우리를 연결합니다.
당신의 다음 성장 이야기도 그로스로그에서 시작해보세요.

커뮤니티 둘러보기