분류 전체보기 23

[Spring] 한 명의 오류가 전체 배치를 롤백시킨 이유: 트랜잭션 경계 재설계 기록

들어가면서정기 결제, 포인트 적립, 쿠폰 지급과 같이 금액이 직접적으로 연관된 시스템에서가장 중요한 것은 처리 속도가 아니라 데이터 정합성과 안정성입니다.정기 결제 배치 시스템을 운영하던 중단 한 명의 결제 실패가 이미 성공한 여러 건의 결제까지 모두 롤백되는 문제를 경험했습니다.더 큰 문제는로그에는 실제 예외가 발생한 위치가 아닌 UnexpectedRollbackException만 남아 있어서원인을 파악하는 데 생각보다 많은 시간이 소요되었다는 점이었습니다.이 글은 당시 문제를 해결하는 과정에서 제가 무엇을 놓치고 있었고트랜잭션을 어떻게 다시 바라보게 되었는지를 정리하기 위해 작성하게 되었습니다.본 글에서는 다음 세 가지를 중심으로 이야기를 풀어보려 합니다.UnexpectedRollbackExcepti..

Spring 2025.12.24

[Spring] @Transactional이 동작하지 않는 이유: Proxy와 내부 호출(Self-Invocation)의 함정

들어가면서스프링 환경에서 트랜잭션을 관리할 때, 우리는 마법처럼 편리한 @Transactional 어노테이션에 익숙해져 있습니다.하지만 이 '마법'이 때로는 우리를 배신하기도 합니다.분명 어노테이션을 붙였는데도 롤백이 안 되거나, 아예 트랜잭션이 적용되지 않는 상황을 마주하게 됩니다.데이터 정합성과 직결되는 트랜잭션 처리에서 이러한 문제는 치명적인 장애로 이어질 수 있습니다.따라서 "왜 안 되는지"에 대한 명확한 이해는 선택이 아닌 필수입니다.이 글에서는 스프링 트랜잭션의 '마법'이 벗겨지는 순간, 즉 AOP Proxy 패턴의 구조적 한계를 직접 코드로 파헤치고 그 해결책을 알아봅니다.AOP와 Proxy@Transactional이나 @Cacheable 같은 어노테이션이 마법처럼 동작할 수 있는 이유는 스..

Spring 2025.12.09

프롬프트의 중요성 (Feat. System Prompt)

들어가면서AI를 활용한 번역 기능을 개발하면서 경험했던 문제를 공유하려 합니다.당시 저는 GPT-4o-mini를 이용해 번역 기능을 구현하고 있었습니다.영어 번역은 자연스럽게 잘 되었지만, 중국어(번체·간체) 번역에서는문장이 한글로 그대로 반환되는 현상이 자주 발생했습니다.처음에는 “너무 경량형 모델이라서 생긴 문제 아닐까?”라고 생각했습니다.그래서 더 높은 모델인 GPT-4o로 변경해보았습니다.GPT-4o에서는 한글 반환 비율이 줄긴 했지만, 여전히 원하는 품질에는 미치지 못했습니다.특히 비용이 약 16배 이상 증가했는데도 품질 개선 폭은 크지 않았습니다.이 시점에서 “모델의 성능이 아니라 프롬프트 설계가 문제일 수 있다”는 생각이 들었습니다.이후 프롬프트 내용을 개선하면서 여러 가지 시도를 진행했습니..

기타 2025.11.08

if-else 구조에서 전략 패턴으로(Feat: 고민의 기록)

들어가면서PDF에 텍스트, 이미지 등 다양한 형태의 입력 기능을 개발하면서 처음에는 단순하게 if-else 구조로 진행했습니다.예시 코드 if (type == TEXT) { ... }else if (type == IMAGE) { ... }예시에는 분기문을 2개로 표현했지만 실제로는 5~6개가 있었고각 조건에 수행하는 코드가 점점 길어지고 새로운 기능 추가나 일부 기능을 수정하는 것도 부담이 되기 시작했습니다.무엇보다 가독성이 떨어져 코드 흐름을 파악하기 어려웠고,결국 유지보수성까지 크게 낮아졌습니다.이 시점에서 “이 구조는 이제 리팩터링이 필요하다”고 판단했습니다.그러면서 전략 패턴, 커맨드 패턴, 함수 추출 등 다양한 방법등이 생각났고전략 패턴을 도입하기까지의 고민과 선택과정을 공유해보려고 합니다.1...

OOP 2025.11.04

Redis 분산락 정리(직접 구현 vs Redisson 비교)

들어가면서어플리케이션에서 락(Lock)은 멀티쓰레드 환경에서 동시에 같은 자원에 접근하는 것을 방지하기 위해 사용됩니다.단일 서버 환경에서는 synchronized같은 어플리케이션 레벨 락으로 충분하지만,여러 서버가 동시에 하나의 자원(예: 주문, 결제, 재고관리)을 접근하는 분산 환경에서는 이 방식만으로는 제어가 어렵습니다.예를 들어 사용자가 주문 버튼을 2번 연속으로 클릭하여 서버에 요청했다는 가정을 하겠습니다.단일 서버 환경에서는 synchronized같은 어플리케이션 레벨의 락으로 중복실행을 막을 수 있지만,서버가 여러개면 1번째 요청은 1번 서버, 2번째 요청은 2번 서버로 요청이 갔을 경우1번 서버와 2번 서버가 서로의 서로의 상태를 모르기 때문에 2번 실행되어 문제가 발생합니다.이 문제를 해..

Redis 2025.11.02

Redis 데이터 만료 전략 개념 및 예제 (Feat: TTL, TTI)

들어가면서원래는 분산락까지 함께 다루려 했지만 TTI, TTL의 내용이 많아서 다음에 다루겠습니다.서비스에서 데이터를 일정 시간 후 만료시키는 정책은 성능과 보안을 모두 고려할 때 매우 중요합니다.대표적인 시간 기반 만료 정책에는 다음 두 가지가 있습니다.TTL (Time To Live) → 저장 시점부터 고정된 시간 후 자동 만료TTI (Time To Idle) → 마지막 접근 이후 일정 시간이 지나면 만료Redis는 기본적으로 TTL 기반 만료를 제공했었습니다.그러나 Redis 6.2 이상 버전부터는 GETEX 명령어를 통해 TTI(유휴 만료)를 직접 구현할 수 있습니다.만약 6.2 미만 버전이라면, GET과 EXPIRE 명령어를 조합하여 동일한 효과를 낼 수 있습니다.이번 글에서는 TTL과 TTI의..

Redis 2025.10.12

Spring RedisTemplate 자료구조별 사용법과 CLI 정리

들어가면서Redis는 처음 접할 때는 단순히 캐시 서버나 세션 관리 도구 정도로만 생각하기 쉽습니다.하지만 Redis가 제공하는 다양한 자료구조를 활용하면 단순 캐시를 넘어 다양한 기능을 구현할 수 있습니다.세션 만료 제어 (TTL vs TTI)분산 환경에서의 락 처리작업 큐와 예약 작업랭킹 시스템UV/통계 집계이번 글에서는 Redis의 기본 개념과 함께, Spring에서 RedisTemplate을 설정하고 각 자료구조별 사용하는 방법, CLI 명령어를 정리해보겠습니다.👉 다음 편에서는 여기서 다룬 기본기를 바탕으로 실습 예제를 직접 만들어 보겠습니다.1. Redis란?Redis는 Remote Dictionary Server의 약자로, 인메모리 기반 Key-Value 저장소입니다.이름처럼 원격에서 접근..

Redis 2025.09.29

캡슐화는 왜 중요한가? 변경 최소화와 응집도를 위한 경계 설계

들어가면서처음 웹 개발을 시작했을 때 OOP의 캡슐화에 대해 들은 설명은 단순했습니다.“필드를 private으로 두고 getter/setter를 쓰면 캡슐화된다.”당시에는 “getter로 내부 상태를 그대로 꺼내고 setter로 외부 값을 주입하는 게, 결국 public으로 열어두는 것과 뭐가 다른 걸까?” 라는 의문만 남았고 그런가 보다 하고 넘어갔습니다.그러다 최근에 「객체지향의 사실과 오해」, 「오브젝트」 같은 책을 읽으면서 알게되었습니다.캡슐화는 단순히 private 키워드를 붙이는 문법이 아니라, OOP에서 소프트웨어를 지속 가능하게 만드는 핵심 원칙이라는 점을 느끼게 되었습니다.제가 느낀 캡슐화는 getter/setter 남발이 아닌, 변경 전파를 줄이고 응집도를 높이는 경계 설계였습니다.즉,..

OOP 2025.09.14

[생각정리] Enum vs 공통코드, 나는 왜 Enum을 더 선호하는가

들어가면서개발을 하다 보면 자주 마주치는 고민 중 하나가 코드 값 관리입니다.회원 상태, 주문 상태, 결제 수단처럼 시스템 전반에서 쓰이는 값들을 어떻게 관리할지는작은 서비스든 큰 서비스든 중요하며, 특히 서비스 유지보수 관점에서 더욱 중요한 주제입니다.제가 처음 개발을 시작했을 때는 C++ 기반의 장비 개발이었습니다.그 당시에는 DB를 거의 사용하지 않았고,주로 모터와 같이 장비마다 다른 환경 세팅과 디바이스 정보를 저장하는 용도로만 활용했습니다.그래서 코드 내부의 상태 값은 주로 enum이나 struct를 이용해 관리했고,부득이하게 매직넘버나 매직스트링을 써야 할 때는 const 상수로 정의하여 사용했습니다.이유는 간단합니다.매직넘버나 매직스트링은 오타나 잘못된 사용으로 인해 쉽게 버그를 만들 수 있..

기타 2025.09.06

JPA Converter처럼, MyBatis TypeHandler로 변환 책임 한 곳에 모으기

들어가며Oracle의 DATE 타입은 사실 년-월-일 + 시:분:초까지 저장할 수 있지만, MyBatis는 기본적으로 LocalDateTime ↔ DATE 매핑을 지원하지 않습니다.처음에는 DB 컬럼 타입을 VARCHAR로 바꿀까, 혹은 Java에서 String으로 관리하고 SQL에서 TO_DATE로 변환할까 생각했습니다.하지만 이런 방식은 DB와 Java의 타입을 억지로 다르게 가져가는 것이고, 결국 일관성이 깨지고 유지보수성이 떨어질 수 있습니다.반대로, DB의 컬럼 타입과 Java의 타입을 최대한 동일하게 가져가면 코드의 가독성과 예측 가능성이 높아집니다.또한 변환 책임을 한 곳에 모으면 관심사 분리 원칙을 지킬 수 있어 관리도 편리해집니다.그래서 이 글에서는 JPA의 @Converter와 유사한 ..

DB 2025.09.02