[Spring Boot] 매장 예약 시스템 개발 중 발생한 오류 해결기

D3F D3V J30N·2025년 2월 3일
0

Error

목록 보기
4/4
post-thumbnail

1. 첫 번째 오류: 데이터베이스 연결 및 설정 오류

PersistenceException: Unable to build Hibernate SessionFactory
DataSourceBeanCreationException: Failed to create database
SQLGrammarException: Table doesn't exist
  • 원인
    MySQL 연결 설정 오류, 데이터베이스 권한 부족, Hibernate DDL 설정 오류가 겹쳐 발생함.

  • 문제 해결 과정

    • application.yml 설정을 재확인하고, MySQL 서버 상태와 권한 설정을 점검함.
    • createDatabaseIfNotExist=true 옵션을 추가하여 자동으로 데이터베이스가 생성되도록 수정함.
    • 권한 문제를 해결하기 위해 사용자 계정을 다시 설정하고 필요한 권한을 부여함.
  • 해결 방법
    application.yml 설정을 다음과 같이 개선하고 데이터베이스 권한을 수정함:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/zb-payment-study-test?createDatabaseIfNotExist=true&useSSL=false
    username: root
    password: root1234
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      maximum-pool-size: 10
      minimum-idle: 5
      idle-timeout: 300000
      connection-timeout: 20000
      connection-test-query: SELECT 1

  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MySQL8Dialect
        format_sql: true
        show_sql: true
CREATE DATABASE IF NOT EXISTS zb_payment_study_test;
GRANT ALL PRIVILEGES ON zb_payment_study_test.* TO 'root'@'localhost';
FLUSH PRIVILEGES;
  • 배운 점
    • 데이터베이스 연결 시 설정과 권한 관리가 중요하며, 커넥션 풀 설정이 성능에 영향을 줄 수 있음을 알게 됨.

2. 두 번째 오류: 엔티티 관계 매핑 오류

org.hibernate.LazyInitializationException: failed to lazily initialize a collection
DataIntegrityViolationException: Cannot delete or update a parent row
  • 원인

    • Lazy 로딩 설정으로 인한 세션 종료 문제
    • 잘못된 연관 관계 매핑으로 인해 N+1 문제 발생 및 참조 무결성 제약조건 위반
  • 문제 해결 과정

    • Lazy 로딩의 위험성을 줄이기 위해 fetch join 또는 @EntityGraph를 활용하기로 결정
    • 자식 엔티티 삭제 순서를 고려하여 트랜잭션 내에서 올바른 순서로 처리
  • 해결 방법

@Entity
public class Store {
    @OneToMany(mappedBy = "store", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
    private List<Reservation> reservations = new ArrayList<>();

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "owner_id", nullable = false)
    private Member owner;
}
@Repository
public interface StoreRepository extends JpaRepository<Store, Long> {
    @EntityGraph(attributePaths = {"owner", "reservations"})
    Optional<Store> findWithOwnerById(Long id);
}
@Transactional
public void deleteStore(Long storeId) {
    Store store = storeRepository.findById(storeId)
        .orElseThrow(() -> new BusinessException(ErrorCode.STORE_NOT_FOUND));
    
    reservationRepository.deleteAllByStoreId(storeId);
    reviewRepository.deleteAllByStoreId(storeId);
    storeRepository.delete(store);
}
  • 배운 점
    • Lazy 로딩의 위험성을 이해하고 fetch join 및 @EntityGraph 활용법을 익혔으며, 참조 무결성을 유지하는 트랜잭션 설계의 중요성을 체득함.

3. 세 번째 오류: QueryDSL 설정 및 동적 쿼리 오류

  • 원인

    • Gradle 설정 누락으로 Q클래스가 생성되지 않음
    • 잘못된 동적 조건 결합 및 null 체크 누락으로 쿼리 오류 발생
  • 문제 해결 과정

    • build.gradle에 annotationProcessor 설정을 추가하고 Q클래스가 정상적으로 생성되도록 함.
    • 동적 조건 결합 시 null 체크를 철저히 하여 쿼리 생성 실패를 방지함.
  • 해결 방법

dependencies {
    implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
    annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
}

def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile
sourceSets {
    main.java.srcDirs += querydslDir
}
private BooleanExpression createWhereCondition(StoreSearchCriteria criteria) {
    BooleanExpression condition = null;

    if (StringUtils.hasText(criteria.keyword())) {
        condition = store.name.containsIgnoreCase(criteria.keyword())
            .or(store.location.containsIgnoreCase(criteria.keyword()));
    }

    if (criteria.latitude() != null && criteria.longitude() != null) {
        BooleanExpression locationCondition = createLocationCondition(
            criteria.latitude(), criteria.longitude()
        );
        condition = condition == null ? locationCondition : condition.and(locationCondition);
    }

    return condition;
}
  • 배운 점
    • QueryDSL 설정의 중요성과 동적 조건 결합 시 null 체크 및 적절한 로직 설계의 필요성을 깊이 이해하게 됨.

4. 네 번째 오류: AOP 로깅 관련 무한 재귀 호출

StackOverflowError: Infinite recursion
  • 원인

    • AOP 포인트컷 설정 오류로 인해 재귀 호출 발생
  • 문제 해결 과정

    • 포인트컷 설정을 재확인하고 정확한 메서드만 처리되도록 개선
    • @Around 어드바이스에서 예외 처리와 로깅을 강화하여 디버깅이 용이하도록 함
  • 해결 방법

@Aspect
@Component
@Slf4j
public class LoggingAspect {
    
    @Pointcut("execution(* com.zerobase.zbpaymentstudy..*Service.*(..))")
    private void allService() {}
    
    @Around("allService()")
    public Object logging(ProceedingJoinPoint joinPoint) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        
        try {
            log.info("Start - {} args: {}", methodName, 
                Arrays.toString(joinPoint.getArgs()));
            Object result = joinPoint.proceed();
            log.info("End - {} result: {}", methodName, result);
            return result;
        } catch (Exception e) {
            log.error("Error - {} error: {}", methodName, e.getMessage());
            throw e;
        }
    }
}
  • 배운 점
    • AOP 설정 오류가 심각한 문제를 초래할 수 있음을 깨닫고 예외 처리 및 로깅의 중요성을 다시 확인함.

5. 다섯 번째 오류: 트랜잭션 관리 오류

TransactionRequiredException: No transaction aspect-managed TransactionStatus in scope
  • 원인

    • @Transactional 어노테이션이 제대로 적용되지 않아 트랜잭션 관리가 누락됨
  • 문제 해결 과정

    • 읽기 전용과 쓰기 트랜잭션을 명확히 구분하고 필요한 메서드에 정확히 @Transactional을 적용함.
  • 해결 방법

@Service
@Transactional
public class ReservationServiceImpl implements ReservationService {
    
    @Transactional(readOnly = true)
    public Page<ReservationDto> findReservations(...) {
        // 조회 로직
    }
    
    @Transactional
    public ApiResponse<ReservationDto> createReservation(...) {
        // 예약 생성 로직
    }
}
  • 배운 점
    • 트랜잭션 관리의 중요성을 다시금 느꼈으며, 특히 읽기 전용과 쓰기 트랜잭션의 구분이 필수적임을 알게 됨.

프로젝트 개선 사항

  1. 도메인 구조 개선

    • 각 엔티티의 역할을 명확히 정의하여 재사용성과 확장성을 높임.
  2. Redis 통합

    • Redis 클라이언트를 추상화하고 데이터 접근 로직을 모듈화하여 성능과 유지보수성을 향상시킴.
  3. 일관된 예외 처리

    • CustomException과 ErrorCode enum을 활용해 명확하고 일관된 예외 처리 체계를 구축함.
  4. 테스트 코드 개선

    • Redis와 통합된 기능 및 서비스 레이어에 대한 단위 테스트 작성.

인사이트

이번 프로젝트에서 다양한 오류를 경험하며 문제 해결 능력을 크게 향상시킬 수 있었다. 특히 오류를 해결하면서 시스템이 점점 더 견고해지는 것을 체감할 수 있었고, 앞으로 더 나은 설계와 최적화를 적용할 수 있는 자신감을 얻었다.

오늘의 명언

"나이가 60이다 70이다 하는 것으로 그 사람이 늙었다 젊었다 할 수 없다. 늙고 젊은 것은 그 사람의 신념이 늙었느냐 젊었느냐 하는데 있다."

-맥아더-

profile
Problem Solver

0개의 댓글