Spring 7기 프로젝트/아웃소싱 팀 프로젝트
[트러블슈팅] 과도한 엔지니어링 : 이벤트 기반 아키텍처 삽질기
JuNo_12
2025. 6. 15. 21:37
"name 컬럼 하나 때문에 이벤트 기반 아키텍처를 도입한 뉴비의 솔직한 후기"
들어가며
팀 프로젝트에서 "새로운 아키텍처를 써보고 싶다"는 마음으로 시작한 이벤트 기반 아키텍처 도입기입니다. 하지만 기능구현을 마무리하며 깨달은 충격적인 사실...
"Auth 테이블에 name 컬럼 하나만 추가하면 되는 상황이었다"
이 글은 제가 저지른 과도한 엔지니어링의 전 과정과, 그 과정에서 배운 값진 교훈들을 솔직하게 기록한 트러블슈팅 후기입니다.
문제 상황: 팀원 간 블로킹 이슈
초기 상황
// Task 도메인에서 User 이름이 필요한 상황
@Service
public class TaskService {
private final UserRepository userRepository; // 👈 다른 팀원이 구현해야 함
public void createTask(CreateTaskRequest request) {
// User 이름이 필요하지만... 기다려야 함
User user = userRepository.findById(request.getUserId());
Task task = new Task(request.getTitle(), user.getName());
}
}
당시 생각: "도메인 간 의존성이 문제네! 이벤트 기반으로 해결해야지!"
지금 생각: "Auth 테이블에 name 컬럼만 추가하면 되는데..."
해결 과정: 삽질의 시작
1단계: 이벤트 기반 아키텍처 설계
복잡한 테이블 구조 설계
-- Auth 테이블 (인증 정보)
CREATE TABLE auth (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(20) NOT NULL,
email VARCHAR(100) NOT NULL,
password VARCHAR(255) NOT NULL,
role VARCHAR(20) NOT NULL
-- name이 없어서 User 테이블 필요하다고 생각 🤔
);
-- User 테이블 (프로필 정보)
CREATE TABLE users (
id BIGINT PRIMARY KEY, -- Auth.id와 같은 값
name VARCHAR(50) NOT NULL, -- 👈 이거 하나 때문에...
email VARCHAR(100) NOT NULL -- Auth와 중복 데이터
);
이벤트 클래스 구현
@Getter
public class UserRegisteredEvent {
private final Long userId;
private final String name;
private final String email;
// 생성자, 검증 로직 등... 50줄의 코드
}
Auth 서비스에서 이벤트 발행
@Transactional
public SignupResponseDto signup(SignupRequestDto request) {
Auth savedAuth = authRepository.save(new Auth(...));
// 복잡한 이벤트 발행
eventPublisher.publishEvent(new UserRegisteredEvent(
savedAuth.getId(),
request.getName(),
savedAuth.getEmail()
));
return response;
}
User 도메인에서 이벤트 처리
@EventListener
@Transactional
public void handleUserRegistered(UserRegisteredEvent event) {
User user = new User(event.getUserId(), event.getName(), event.getEmail());
userRepository.save(user);
}
당시 만족감: "와! 도메인이 완전히 분리됐어! 멋지다!"
트러블 1: 데이터 일관성 문제 발견
문제 상황
@Transactional
public SignupResponseDto signup(SignupRequestDto request) {
Auth savedAuth = authRepository.save(auth); // Auth 저장 ✅
eventPublisher.publishEvent(new UserRegisteredEvent(...)); // 이벤트 발행
// 만약 여기서 예외 발생하면?
throw new RuntimeException("뭔가 잘못됨!");
// Auth는 롤백되지만 User는 이미 생성됨 😱
}
해결 시도 1: @TransactionalEventListener 도입
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleUserRegistered(UserRegisteredEvent event) {
// Auth 커밋 후에만 실행되도록 변경
}
결과: Spring 6 호환성 문제로 에러 발생
당시 고민: "왜 이렇게 복잡하지? 뭔가 잘못된 건 아닐까?"
트러블 2: User 생성 실패 시 Auth 롤백 불가
새로운 문제 발견
- Auth는 성공적으로 커밋됨 ✅
- User 생성이 실패함 ❌
- Auth 롤백이 불가능함 😱
해결 시도 2: 비동기 + Saga 패턴 도입
비동기 처리 설정
@Configuration
@EnableAsync
public class AsyncConfig {
@Bean(name = "eventTaskExecutor")
public Executor eventTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(3);
executor.setMaxPoolSize(10);
// 복잡한 설정들...
return executor;
}
}
보상 트랜잭션 구현
@EventListener
@Async("eventTaskExecutor")
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleUserRegistered(UserRegisteredEvent event) {
try {
userRepository.save(new User(...));
} catch (Exception e) {
// 실패 시 보상 트랜잭션 발행
eventPublisher.publishEvent(new UserCreationFailedEvent(event.getUserId()));
}
}
@EventListener
@Transactional
public void handleUserCreationFailed(UserCreationFailedEvent event) {
// Auth 롤백 처리
authRepository.deleteById(event.getUserId());
}
당시 생각: "드디어 완벽한 아키텍처다! Saga 패턴까지 적용했어!"
지금 생각: "이게 정말 필요했나..."
진짜 해결책: 단순함의 발견
현재 복잡한 구조
// 이벤트 기반 회원가입 (200줄+ 코드)
Auth 생성 → UserRegisteredEvent 발행 → 비동기 User 생성 → 실패 시 보상 트랜잭션
진짜 필요했던 것
-- 이것만 하면 끝이었음 🤦♂️
CREATE TABLE auth (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
username VARCHAR(20) NOT NULL,
email VARCHAR(100) NOT NULL,
password VARCHAR(255) NOT NULL,
role VARCHAR(20) NOT NULL,
name VARCHAR(50) NOT NULL -- 👈 이 한 줄만 추가!
);
-- User 테이블은 아예 불필요했음
// 단순한 회원가입 (10줄 코드)
@Transactional
public SignupResponseDto signup(SignupRequestDto request) {
Auth auth = new Auth(
request.getUsername(),
request.getEmail(),
encodedPassword,
UserRole.USER,
request.getName() // 👈 name도 Auth에 저장
);
Auth savedAuth = authRepository.save(auth);
return new SignupResponseDto(savedAuth.getUsername(), savedAuth.getEmail());
}
왜 이런 삽질을 했을까?
삽질의 원인들
1. 기술에 대한 욕망
❌ "이벤트 기반 아키텍처를 써보고 싶다"
✅ "문제를 해결하기 위해 적절한 기술을 선택한다"
2. 도메인 분리 강박
❌ "인증과 사용자는 반드시 분리되어야 한다"
✅ "비즈니스 복잡도에 따라 분리 여부를 결정한다"
3. MSA를 고려한다는 핑계
❌ "나중에 MSA로 분리할 거니까 미리 준비해야지"
✅ "현재 요구사항에 맞는 가장 단순한 해결책부터"
4. 프로젝트 규모 오판
// 내가 생각한 User 도메인
class User {
private String name;
private String profileImage;
private String bio;
private List<SocialLink> socialLinks;
private UserPreferences preferences;
// 복잡한 프로필 정보들...
}
// 실제 필요했던 것
class Auth {
private String name; // 👈 이것만...
}
그래도 배운 것들
기술적 역량 향상
1. 이벤트 기반 아키텡처 이해
// 언제 사용해야 하는지 이제 안다
복잡한 비즈니스 로직이 여러 도메인에 걸쳐 있을 때
도메인 간 결합도를 낮춰야 할 때
비동기 처리가 성능상 필수일 때
❌ name 컬럼 하나 때문에 (내 경우)
2. 트랜잭션 이벤트 처리 이해
// @EventListener vs @TransactionalEventListener 완벽 이해
@EventListener // 즉시 실행
@TransactionalEventListener // 트랜잭션 생명주기 연동
3. 비동기 처리와 Saga 패턴 경험
// 실무에서 정말 필요할 때 바로 적용 가능
@Async + 보상 트랜잭션 패턴
아키텍처 설계 철학 정립
Before (기술 중심)
"이 기술 멋있어! 써보자!"
"MSA 대비해서 미리 분리하자!"
"복잡한 게 전문가답다!"
After (문제 중심)
"이 문제를 해결하는 가장 단순한 방법은?"
"현재 요구사항에 맞는 최소 구현은?"
"복잡도 대비 얻는 가치가 있나?"
협업 관점의 깨달음
1. 팀원 배려
// Before: 혼자만 아는 복잡한 구조
"이벤트 기반이니까 UserRegisteredEvent 보세요!"
// After: 모두가 이해하는 단순한 구조
"Auth 테이블에 name 컬럼 있어요!"
2. 유지보수성
// Before: 디버깅 지옥
Auth 생성 → 이벤트 발행 → 비동기 처리 → 보상 트랜잭션 (어디서 실패?)
// After: 디버깅 천국
Auth 생성 (끝!)
교훈과 가이드라인
기술 선택 가이드라인
1. YAGNI 원칙 (You Aren't Gonna Need It)
❌ "나중에 필요할 수도 있으니까 미리 구현해두자"
✅ "지금 당장 필요한가? 아니면 나중에 추가하자"
2. 복잡도 vs 가치 평가
// 질문해야 할 것들
1. 이 기술이 해결하는 진짜 문제가 있는가?
2. 단순한 대안은 정말 없는가?
3. 추가되는 복잡도 대비 얻는 가치가 있는가?
4. 팀원들이 이해하고 유지보수할 수 있는가?
3. 단계적 발전
// 올바른 진화 순서
1. 가장 단순한 해결책으로 시작
2. 문제가 실제로 발생하면 개선
3. 복잡도가 정당화될 때 고도화
// 잘못된 진화 순서 (내가 한 것)
1. 복잡한 해결책으로 시작
2. 문제를 찾아서 해결
3. 나중에 단순화 고려
팀 프로젝트에서의 적용
1. 기술 도입 시 팀 합의
# 기술 도입 제안서 (예시)
## 문제 상황
- 구체적인 문제 설명
- 현재 해결책의 한계
## 제안 기술
- 기술 설명 및 장단점
- 복잡도 증가 정도
- 학습 곡선
## 대안 검토
- 단순한 대안들과 비교
- 각 대안의 장단점
## 팀 의견
- 모든 팀원의 의견 수렴
- 합의된 결정과 이유
2. 점진적 도입
// ✅ 올바른 접근
1. 단순한 구조로 MVP 완성
2. 실제 문제 발생 시 개선 검토
3. 팀과 합의 후 단계적 도입
// ❌ 잘못된 접근 (내가 한 것)
1. 처음부터 복잡한 구조 도입
2. 팀원들에게 학습 강요
3. 문제 발생 시 더 복잡한 해결책 추가
결론: 삽질도 값진 경험
- 기술 역량: 이벤트 기반, 비동기, Saga 패턴 마스터
- 아키텍처 사고: 복잡도 vs 가치 평가 능력
- 협업 인사이트: 팀 친화적 기술 선택의 중요성
- 블로그 소재: 솔직한 삽질 경험담 (이것도 가치!)
미래를 위한 다짐
1. 문제 우선 사고
// Before: 기술이 먼저
"이벤트 기반 아키텍처를 써보자. 어떤 문제에 적용할까?"
// After: 문제가 먼저
"이 문제를 해결하려면 어떤 기술이 적합할까?"
2. 단순함의 가치 인정
// 복잡한 != 전문적인
// 단순한 != 실력 부족
// 진짜 전문가는
"가장 단순한 해결책을 찾는 사람"
3. 팀과 함께 성장
// 혼자만 아는 고급 기술 < 모두가 이해하는 적절한 기술
// 개인의 성장 < 팀 전체의 성장
에필로그: 삽질의 연대기
타임라인
Week 1: "이벤트 기반으로 해보자!"
Week 2: "데이터 일관성 문제 발견!"
Week 3: "비동기 + Saga로 해결!"
Week 4: "팀원: 그냥 name 컬럼 추가하면 안 돼요?"
Week 5: "...맞네"
만약 시간을 되돌릴 수 있다면?
그래도 똑같이 했을 것 같습니다.
왜냐하면:
- 이론으로만 알던 것을 직접 경험해봤기 때문
- 실패를 통해 더 깊이 이해하게 되었기 때문
- 앞으로는 절대 같은 실수 안 할 자신이 있기 때문
- 무엇보다 정말 재미있었기 때문!