Home > Backend Development > πŸ“š[Backend Development] 🚨 Spring Boot + Lombok νŠΈλŸ¬λΈ”μŠˆνŒ… κ°€μ΄λ“œ

πŸ“š[Backend Development] 🚨 Spring Boot + Lombok νŠΈλŸ¬λΈ”μŠˆνŒ… κ°€μ΄λ“œ
Backend Development Lombok Spring Boot Trouble Shooting

🚨 Spring Boot + Lombok νŠΈλŸ¬λΈ”μŠˆνŒ… κ°€μ΄λ“œ

Spring Boot와 Lombok을 μ‚¬μš©ν•˜λŠ” κ³Όμ •μ—μ„œ 직접 맞λ‹₯뜨린 μ—λŸ¬λ₯Ό ν•΄κ²°ν•˜λŠ” 과정을 기둝해 λ³΄μ•˜μŠ΅λ‹ˆλ‹€.


πŸ” 문제 1: Lombok Getter λ©”μ„œλ“œλ₯Ό 찾을 수 μ—†μŒ

πŸ“‹ μ—λŸ¬ 상황

ProductManagementApplication μ‹€ν–‰ μ‹œ λ‹€μŒκ³Ό 같은 컴파일 μ—λŸ¬κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.

/Users/kobe/Desktop/ProductManagement/src/main/java/com/kobe/productmanagement/dto/response/StockResponse.java:35: 
error: cannot find symbol

stock.getStockId(),
     ^
  symbol:   method getStockId()
  location: variable stock of type Stock

🎯 원인 뢄석

cannot find symbol μ—λŸ¬λŠ” Java μ»΄νŒŒμΌλŸ¬κ°€ μ½”λ“œμ—μ„œ μ°Έμ‘°ν•˜λŠ” λ©”μ„œλ“œλ‚˜ λ³€μˆ˜λ₯Ό μ°Ύμ§€ λͺ»ν•  λ•Œ λ°œμƒν•©λ‹ˆλ‹€.

  1. 컴파일 μ‹œμ  문제: Stock ν΄λž˜μŠ€μ—μ„œ getStockId() λ©”μ„œλ“œλ₯Ό 찾을 수 μ—†μŒ
  2. Lombok λ™μž‘ μ‹€νŒ¨: @Getter μ–΄λ…Έν…Œμ΄μ…˜μ΄ μ œλŒ€λ‘œ μ²˜λ¦¬λ˜μ§€ μ•Šμ•„ getter λ©”μ„œλ“œκ°€ μƒμ„±λ˜μ§€ μ•ŠμŒ
  3. IDE vs μ‹€μ œ λΉŒλ“œ: IDEμ—μ„œλŠ” 정상 μž‘λ™ν•˜μ§€λ§Œ μ‹€μ œ λΉŒλ“œ μ‹œ μ‹€νŒ¨

πŸ”§ ν•΄κ²° 방법

1단계: Stock μ—”ν‹°ν‹° 클래슀 확인

@Entity
@Table(name = "stocks")
@Getter  // ⭐ 이 μ–΄λ…Έν…Œμ΄μ…˜μ΄ μžˆλŠ”μ§€ λ°˜λ“œμ‹œ 확인!
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Stock extends BaseTimeEntity {
    
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "stock_id")
    private Long stockId;
    
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "product_id", nullable = false)
    private Product product;
    
    @Column(name = "barcode_number", unique = true)
    private String barcodeNumber;
    
    // Lombok이 μžλ™μœΌλ‘œ 생성할 λ©”μ„œλ“œλ“€:
    // - getStockId()
    // - getProduct()
    // - getBarcodeNumber()
}

2단계: 클린 λΉŒλ“œ μ‹€ν–‰

# Gradle μ‚¬μš©μž
./gradlew clean build

# Maven μ‚¬μš©μž
./mvnw clean install

βœ… 확인 포인트

  • @Getter μ–΄λ…Έν…Œμ΄μ…˜μ΄ ν΄λž˜μŠ€μ— μΆ”κ°€λ˜μ–΄ μžˆλŠ”κ°€?
  • import lombok.Getter; import ꡬ문이 μžˆλŠ”κ°€?
  • 클린 λΉŒλ“œλ₯Ό μ‹€ν–‰ν–ˆλŠ”κ°€?

πŸ” 문제 2: λŒ€λŸ‰μ˜ Lombok λ©”μ„œλ“œ λˆ„λ½ μ—λŸ¬

πŸ“‹ μ—λŸ¬ 상황

./gradlew clean build μ‹€ν–‰ μ‹œ 25개의 컴파일 μ—λŸ¬κ°€ ν•œλ²ˆμ— λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.

> Task :compileJava FAILED

error: cannot find symbol
  symbol:   method getStockId()
  location: variable stock of type Stock

error: cannot find symbol
  symbol:   method getProduct()
  location: variable stock of type Stock

error: cannot find symbol
  symbol:   method builder()
  location: class Product

error: cannot find symbol
  symbol:   method getName()
  location: variable request of type ProductCreateRequest

25 errors

🎯 원인 뢄석

IDEμ—μ„œλŠ” Lombok이 정상 μž‘λ™ν•˜μ§€λ§Œ, Gradle λΉŒλ“œ μ‹œμ—λŠ” Lombok μ–΄λ…Έν…Œμ΄μ…˜ ν”„λ‘œμ„Έμ„œκ°€ λ™μž‘ν•˜μ§€ μ•Šκ³  μžˆμŠ΅λ‹ˆλ‹€.

πŸ’‘ 핡심 κ°œλ… 이해

  • IDE의 Lombok ν”ŒλŸ¬κ·ΈμΈ: 개발 쀑 μ½”λ“œ ν•˜μ΄λΌμ΄νŒ…κ³Ό μžλ™μ™„μ„±μ„ μœ„ν•¨
  • λΉŒλ“œ λ„κ΅¬μ˜ μ–΄λ…Έν…Œμ΄μ…˜ ν”„λ‘œμ„Έμ„œ: μ‹€μ œ .class 파일 생성 μ‹œ λ©”μ„œλ“œλ₯Ό λ§Œλ“œλŠ” μ—­ν• 

πŸ”§ ν•΄κ²° 방법

build.gradle μ„€μ • μˆ˜μ •

dependencies {
    // κΈ°μ‘΄ μ˜μ‘΄μ„±λ“€
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    
    // ⭐ Lombok μ„€μ • μΆ”κ°€ (κ°€μž₯ μ€‘μš”!)
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    
    // ν…ŒμŠ€νŠΈμš© μ–΄λ…Έν…Œμ΄μ…˜ ν”„λ‘œμ„Έμ„œλ„ μΆ”κ°€
    testCompileOnly 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'
    
    // λ°μ΄ν„°λ² μ΄μŠ€ λ“œλΌμ΄λ²„
    runtimeOnly 'com.mysql:mysql-connector-j'
    testImplementation 'com.h2database:h2'  // ν…ŒμŠ€νŠΈμš© H2 DB
    
    // ν…ŒμŠ€νŠΈ μ˜μ‘΄μ„±
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

싀무 μ½”λ“œ μ˜ˆμ‹œ

μ„€μ • ν›„ λ‹€μŒκ³Ό 같은 μ½”λ“œλ“€μ΄ 정상 μž‘λ™ν•©λ‹ˆλ‹€:

// βœ… ProductService.java
@Service
@RequiredArgsConstructor
public class ProductService {
    
    private final ProductRepository productRepository;
    
    public Long createProduct(ProductCreateRequest request) {
        Product newProduct = Product.builder()  // @Builder μ–΄λ…Έν…Œμ΄μ…˜μœΌλ‘œ 생성
                .name(request.getName())         // @Getter둜 μƒμ„±λœ λ©”μ„œλ“œ
                .price(request.getPrice())
                .stockQuantity(request.getStockQuantity())
                .category(request.getCategory())
                .costPrice(request.getCostPrice())
                .productSupplier(request.getProductSupplier())
                .barcodeNumber(request.getBarcodeNumber())
                .build();
        
        Product savedProduct = productRepository.save(newProduct);
        return savedProduct.getProductId();  // @Getter둜 μƒμ„±λœ λ©”μ„œλ“œ
    }
}
// βœ… StockResponse.java
public record StockResponse(
    Long stockId,
    String productName,
    BigDecimal costPrice,
    BigDecimal sellingPrice,
    Integer stockQuantity,
    String category,
    LocalDateTime createdAt,
    LocalDateTime updatedAt,
    String productSupplier
) {
    public static StockResponse from(Stock stock) {
        return new StockResponse(
            stock.getStockId(),              // βœ… 정상 μž‘λ™
            stock.getProduct().getName(),    // βœ… 정상 μž‘λ™
            stock.getProduct().getCostPrice(),
            stock.getProduct().getCostPrice(),
            stock.getProduct().getStockQuantity(),
            stock.getProduct().getCategory(),
            stock.getCreatedAt(),            // BaseTimeEntityμ—μ„œ 상속
            stock.getUpdatedAt(),
            stock.getProduct().getProductSupplier()
        );
    }
}

πŸ“š μ–΄λ…Έν…Œμ΄μ…˜ ν”„λ‘œμ„Έμ„œ μ„€μ • μ„€λͺ…

μ„€μ • μ—­ν• 
compileOnly 컴파일 μ‹œμ μ—λ§Œ ν•„μš”ν•˜κ³ , λŸ°νƒ€μž„μ—λŠ” λΆˆν•„μš”ν•œ μ˜μ‘΄μ„±
annotationProcessor 핡심! 컴파일 μ‹œ μ–΄λ…Έν…Œμ΄μ…˜μ„ μ‹€μ œ μ½”λ“œλ‘œ λ³€ν™˜ν•˜λŠ” ν”„λ‘œμ„Έμ„œ
testCompileOnly ν…ŒμŠ€νŠΈ μ½”λ“œ 컴파일 μ‹œμ—λ§Œ ν•„μš”ν•œ μ˜μ‘΄μ„±
testAnnotationProcessor ν…ŒμŠ€νŠΈ μ½”λ“œμ—μ„œλ„ Lombok μ–΄λ…Έν…Œμ΄μ…˜ 처리

πŸ” 문제 3: ν…ŒμŠ€νŠΈ μ‹€ν–‰ μ‹œ λ°μ΄ν„°λ² μ΄μŠ€ μ—°κ²° μ‹€νŒ¨

πŸ“‹ μ—λŸ¬ 상황

λΉŒλ“œλŠ” μ„±κ³΅ν–ˆμ§€λ§Œ ν…ŒμŠ€νŠΈ μ‹€ν–‰ μ‹œ λ‹€μŒ μ—λŸ¬κ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.

> Task :test FAILED

ProductManagementApplicationTests > contextLoads() FAILED
    java.lang.IllegalStateException at DefaultCacheAwareContextLoaderDelegate.java:180
        Caused by: org.springframework.beans.factory.BeanCreationException
            Caused by: org.hibernate.service.spi.ServiceException
                Caused by: org.hibernate.HibernateException at DialectFactoryImpl.java:191

FAILURE: Build failed with an exception.

🎯 원인 뢄석

Hibernateκ°€ λ°μ΄ν„°λ² μ΄μŠ€ λ°©μ–Έ(Dialect)을 κ²°μ •ν•˜μ§€ λͺ»ν•΄ λ°œμƒν•˜λŠ” λ¬Έμ œμž…λ‹ˆλ‹€.

  1. ν…ŒμŠ€νŠΈ ν™˜κ²½μ˜ λ“œλΌμ΄λ²„ λΆ€μ‘±: runtimeOnly둜 μ„ μ–Έλœ MySQL λ“œλΌμ΄λ²„λŠ” ν…ŒμŠ€νŠΈμ—μ„œ μ‚¬μš©ν•  수 μ—†μŒ
  2. μ„€μ • 파일 쀑볡: ν…ŒμŠ€νŠΈλ„ 메인 application.propertiesλ₯Ό μ‚¬μš©ν•΄ MySQL μ—°κ²° μ‹œλ„

πŸ”§ ν•΄κ²° 방법

1단계: ν…ŒμŠ€νŠΈμš© μ„€μ • 파일 생성

πŸ“ src/test/resources/application.yml νŒŒμΌμ„ μƒˆλ‘œ μƒμ„±ν•©λ‹ˆλ‹€.

# ===============================
# πŸ§ͺ TEST DATABASE (H2 In-Memory)
# ===============================
# Spring Bootκ°€ H2 λ°μ΄ν„°λ² μ΄μŠ€λ₯Ό μžλ™μœΌλ‘œ μ„€μ •ν•˜λ„λ‘ ν•©λ‹ˆλ‹€.

spring:
  # JPA/Hibernate μ„€μ •
  jpa:
    hibernate:
      ddl-auto: create-drop
    show-sql: true
    properties:
      hibernate:
        format_sql: true
        
  # H2 Console ν™œμ„±ν™” (λ””λ²„κΉ…μš©)
  h2:
    console:
      enabled: true
      path: /h2-console

# λ‘œκΉ… μ„€μ •
logging:
  level:
    org.springframework.web: DEBUG
    com.kobe.productmanagement: DEBUG

2단계: μ‹€μ œ ν…ŒμŠ€νŠΈ μ½”λ“œ μ˜ˆμ‹œ

@SpringBootTest
@Transactional
class ProductServiceTest {
    
    @Autowired
    private ProductService productService;
    
    @Autowired
    private ProductRepository productRepository;
    
    @Test
    @DisplayName("μƒν’ˆ 생성 ν…ŒμŠ€νŠΈ - H2 λ°μ΄ν„°λ² μ΄μŠ€ μ‚¬μš©")
    void createProduct_Success() {
        // given
        ProductCreateRequest request = ProductCreateRequest.builder()
            .name("λ§₯뢁 ν”„λ‘œ 16인치")
            .price(new BigDecimal("2490000"))
            .stockQuantity(10)
            .category("μ „μžμ œν’ˆ")
            .costPrice(new BigDecimal("2000000"))
            .productSupplier("Apple Korea")
            .barcodeNumber("8801234567890")
            .build();
        
        // when
        Long productId = productService.createProduct(request);
        
        // then
        assertThat(productId).isNotNull();
        
        Optional<Product> savedProduct = productRepository.findById(productId);
        assertThat(savedProduct).isPresent();
        assertThat(savedProduct.get().getName()).isEqualTo("λ§₯뢁 ν”„λ‘œ 16인치");
    }
    
    @Test
    @DisplayName("재고 쑰회 ν…ŒμŠ€νŠΈ - 연관관계 포함")
    void findStockWithProduct_Success() {
        // given - ν…ŒμŠ€νŠΈ 데이터 μ€€λΉ„
        Product product = Product.builder()
            .name("아이폰 15 Pro")
            .price(new BigDecimal("1350000"))
            .stockQuantity(5)
            .category("슀마트폰")
            .costPrice(new BigDecimal("1100000"))
            .productSupplier("Apple Korea")
            .barcodeNumber("8801234567891")
            .build();
        
        Product savedProduct = productRepository.save(product);
        
        Stock stock = Stock.builder()
            .product(savedProduct)
            .barcodeNumber("STOCK_8801234567891")
            .build();
        
        // when & then - H2 λ°μ΄ν„°λ² μ΄μŠ€μ—μ„œ 정상 μž‘λ™
        assertThat(stock.getProduct().getName()).isEqualTo("아이폰 15 Pro");
    }
}

πŸ—οΈ ν™˜κ²½λ³„ μ„€μ • ꡬ쑰

src/
β”œβ”€β”€ main/
β”‚   └── resources/
β”‚       └── application.yml          # πŸš€ 운영/개발용 (MySQL)
└── test/
    └── resources/
        └── application.yml          # πŸ§ͺ ν…ŒμŠ€νŠΈμš© (H2)

운영 ν™˜κ²½ μ„€μ • (main/resources)

# MySQL λ°μ΄ν„°λ² μ΄μŠ€ μ—°κ²°
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/product_management
    username: root
    password: password
  jpa:
    hibernate:
      ddl-auto: validate

ν…ŒμŠ€νŠΈ ν™˜κ²½ μ„€μ • (test/resources)

# H2 인메λͺ¨λ¦¬ λ°μ΄ν„°λ² μ΄μŠ€ (μžλ™ μ„€μ •)
spring:
  jpa:
    hibernate:
      ddl-auto: create-drop
  h2:
    console:
      enabled: true

πŸ“Š 문제 ν•΄κ²° 체크리슀트

βœ… Lombok μ„€μ • 확인

  • @Getter, @Builder λ“± μ–΄λ…Έν…Œμ΄μ…˜μ΄ ν΄λž˜μŠ€μ— μžˆλŠ”κ°€?
  • build.gradle에 annotationProcessor 'org.projectlombok:lombok' 좔가됨?
  • IDE에 Lombok ν”ŒλŸ¬κ·ΈμΈμ΄ μ„€μΉ˜λ˜μ–΄ μžˆλŠ”κ°€?

βœ… λ°μ΄ν„°λ² μ΄μŠ€ μ„€μ • 확인

  • ν…ŒμŠ€νŠΈμš© application.yml 파일이 λ³„λ„λ‘œ μžˆλŠ”κ°€?
  • testImplementation 'com.h2database:h2' μ˜μ‘΄μ„±μ΄ 좔가됨?
  • 운영 ν™˜κ²½κ³Ό ν…ŒμŠ€νŠΈ ν™˜κ²½μ˜ λ°μ΄ν„°λ² μ΄μŠ€κ°€ λΆ„λ¦¬λ˜μ–΄ μžˆλŠ”κ°€?

βœ… λΉŒλ“œ 및 ν…ŒμŠ€νŠΈ

  • ./gradlew clean build λͺ…λ Ήμ–΄κ°€ μ„±κ³΅ν•˜λŠ”κ°€?
  • λͺ¨λ“  ν…ŒμŠ€νŠΈκ°€ ν†΅κ³Όν•˜λŠ”κ°€?
  • IDEμ—μ„œ κ°œλ³„ ν…ŒμŠ€νŠΈ 싀행이 κ°€λŠ₯ν•œκ°€?

πŸŽ‰ 마무리

이제 Spring Boot + Lombok ν”„λ‘œμ νŠΈμ—μ„œ λ°œμƒν•˜λŠ” μ£Όμš” λ¬Έμ œλ“€μ„ ν•΄κ²°ν•  수 μžˆμŠ΅λ‹ˆλ‹€!

πŸš€ λ‹€μŒ 단계 ꢌμž₯사항

  1. CI/CD νŒŒμ΄ν”„λΌμΈ ꡬ좕: GitHub Actionsλ‚˜ Jenkinsλ₯Ό ν™œμš©ν•œ μžλ™ λΉŒλ“œ
  2. ν…ŒμŠ€νŠΈ 컀버리지 μΈ‘μ •: JaCoCoλ₯Ό ν™œμš©ν•œ μ½”λ“œ 컀버리지 확인
  3. ν”„λ‘œνŒŒμΌλ³„ μ„€μ • 뢄리: application-dev.yml, application-prod.yml λ“±

πŸ“ž μΆ”κ°€ 도움이 ν•„μš”ν•˜λ‹€λ©΄?

  • Spring Boot 곡식 λ¬Έμ„œ: https://spring.io/projects/spring-boot
  • Lombok 곡식 λ¬Έμ„œ: https://projectlombok.org/