커피숍 주문 시스템 개발 중 발생한 문제와 해결 방법을 정리한 문서입니다.
Execution failed for task ':compileJava'.
> Could not resolve all files for configuration ':annotationProcessor'.
IntelliJ에서 Gradle 빌드 시 QueryDSL 관련 오류 발생.
com.ewerk.gradle.plugins.querydsl 플러그인이 Gradle 8.x와 호환되지 않음build.gradle 수정:
// ❌ 제거: 호환되지 않는 플러그인
// id 'com.ewerk.gradle.plugins.querydsl' version '1.0.10'
dependencies {
// ✅ 추가: Annotation Processor 방식
implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta'
annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta'
annotationProcessor 'jakarta.annotation:jakarta.annotation-api'
annotationProcessor 'jakarta.persistence:jakarta.persistence-api'
}
// ✅ 추가: Q클래스 생성 경로 설정
def querydslDir = "$buildDir/generated/querydsl"
sourceSets {
main.java.srcDirs += [querydslDir]
}
tasks.withType(JavaCompile) {
options.generatedSourceOutputDirectory = file(querydslDir)
}
clean {
delete file(querydslDir)
}
Q클래스 생성:
./gradlew clean compileJava
생성 위치: build/generated/querydsl/
Communications link failure
The last packet sent successfully to the server was 0 milliseconds ago.
애플리케이션 실행 시 MySQL 연결 실패.
application.yaml의 DB 설정이 .env 파일과 불일치1단계: Docker 컨테이너 확인
# 컨테이너 상태 확인
docker-compose ps
# 컨테이너가 없으면 실행
docker-compose up -d
# 로그 확인
docker-compose logs mysql
2단계: application.yaml 수정
spring:
datasource:
url: jdbc:mysql://localhost:3307/coffee_order?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8
username: root
password: 1234
driver-class-name: com.mysql.cj.jdbc.Driver
3단계: .env 파일 확인
DB_PORT=3307
DB_USERNAME=root
DB_PASSWORD=1234
DB_NAME=coffee_order
참고:
coffee_user가 아닌 root 사용 (간단한 로컬 개발 환경)이메일 또는 비밀번호가 일치하지 않습니다
Postman에서 로그인 시도 시 계속 실패. DB에 저장된 비밀번호와 입력한 비밀번호가 일치하는데도 실패.
1단계: 올바른 BCrypt 해시 생성
온라인 BCrypt 생성기 또는 Java 코드로 생성:
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String hash = encoder.encode("admin123");
System.out.println(hash);
2단계: SQL 스크립트 수정
-- ❌ 잘못된 해시
INSERT INTO users (email, password, name, role) VALUES
('admin@coffee.com', '$2a$10$wronghash...', '관리자', 'ADMIN');
-- ✅ 올바른 해시
INSERT INTO users (email, password, name, role) VALUES
('admin@coffee.com', '$2a$10$YourCorrectHashHere...', '관리자', 'ADMIN');
3단계: DB 재생성
# Docker 컨테이너 중지 및 볼륨 삭제
docker-compose down -v
# 다시 시작 (초기화 스크립트 재실행)
docker-compose up -d
검증된 비밀번호:
admin123 → $2a$10$... (관리자)user123 → $2a$10$... (일반 사용자){
"menuId": 8,
"name": "ì•„ì´ìŠ¤í‹°",
"description": "ìƒí¼í•œ ì•„ì´ìŠ¤í‹°"
}
Postman에서 메뉴 조회 시 한글이 깨져서 표시됨. DB에는 정상적으로 저장되어 있음.
1단계: SQL 파일 인코딩 확인
# IntelliJ에서 파일 인코딩 확인
# 우측 하단 인코딩 표시 → UTF-8로 변경
# "Reload" 선택 시 기존 내용 손상 가능
# "Convert" 선택 권장
2단계: SQL 스크립트에 인코딩 설정 추가
-- 파일 최상단에 추가
SET NAMES utf8mb4;
SET CHARACTER SET utf8mb4;
CREATE DATABASE IF NOT EXISTS coffee_order
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
3단계: application.yaml 수정
spring:
datasource:
url: jdbc:mysql://localhost:3307/coffee_order?useSSL=false&serverTimezone=Asia/Seoul&characterEncoding=UTF-8
4단계: WebConfig 추가 (선택사항)
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.stream()
.filter(converter -> converter instanceof MappingJackson2HttpMessageConverter)
.forEach(converter -> {
MappingJackson2HttpMessageConverter jsonConverter =
(MappingJackson2HttpMessageConverter) converter;
jsonConverter.setDefaultCharset(StandardCharsets.UTF_8);
});
}
}
5단계: DB 재생성
docker-compose down -v
docker-compose up -d
참고:
import static kr.spartaclub.coffeeorder.domain.user.entity.QUser.user;
// Cannot resolve symbol 'QUser'
QueryDSL 사용 시 Q클래스를 찾을 수 없음.
1단계: Q클래스 생성
./gradlew clean compileJava
2단계: IDE 새로고침
IntelliJ IDEA:
1. File → Invalidate Caches / Restart
2. 또는 Gradle 탭 → Reload All Gradle Projects
3단계: 생성 확인
build/generated/querydsl/
└── kr/spartaclub/coffeeorder/domain/
├── user/entity/QUser.java
├── menu/entity/QMenu.java
└── ...
4단계: import 수정
// ✅ 올바른 import
import static kr.spartaclub.coffeeorder.domain.user.entity.QUser.user;
// QueryDSL 사용
JPAQueryFactory queryFactory;
List<User> users = queryFactory
.selectFrom(user)
.where(user.email.eq("test@example.com"))
.fetch();
java.lang.UnsupportedOperationException
at java.base/java.util.Collections$UnmodifiableMap.put
JWT 토큰 생성 시 Claims 객체에 값을 추가할 수 없음.
Jwts.claims() 메서드가 불변(Immutable) Map을 반환❌ 잘못된 코드:
Claims claims = Jwts.claims();
claims.put("userId", userId); // UnsupportedOperationException
claims.put("email", email);
claims.put("role", role);
✅ 올바른 코드:
// Builder 패턴 사용
String token = Jwts.builder()
.claim("userId", userId)
.claim("email", email)
.claim("role", role)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration))
.signWith(key, SignatureAlgorithm.HS256)
.compact();
java.lang.ClassCastException: class java.lang.String cannot be cast to class java.lang.Long
@CurrentUser Long userId 사용 시 String을 Long으로 캐스팅할 수 없다는 오류.
@CurrentUser 어노테이션이 email(String)을 반환하도록 구현됨1단계: UserPrincipal 클래스 생성
@Getter
@AllArgsConstructor
public class UserPrincipal {
private Long userId;
private String email;
private String role;
}
2단계: JwtTokenProvider 수정
public Authentication getAuthentication(String token) {
Claims claims = parseClaims(token);
UserPrincipal principal = new UserPrincipal(
claims.get("userId", Long.class),
claims.get("email", String.class),
claims.get("role", String.class)
);
return new UsernamePasswordAuthenticationToken(
principal,
"",
getAuthorities(claims)
);
}
3단계: CurrentUserArgumentResolver 수정
@Override
public Object resolveArgument(
MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory
) {
Authentication authentication = SecurityContextHolder
.getContext()
.getAuthentication();
if (authentication == null ||
!(authentication.getPrincipal() instanceof UserPrincipal)) {
return null;
}
return authentication.getPrincipal(); // UserPrincipal 반환
}
4단계: 컨트롤러 수정
// ❌ 이전
@GetMapping
public ResponseEntity<?> getOrders(@CurrentUser Long userId) {
// ...
}
// ✅ 현재
@GetMapping
public ResponseEntity<?> getOrders(@CurrentUser UserPrincipal principal) {
Long userId = principal.getUserId();
// ...
}
org.springframework.data.repository.query.QueryCreationException:
Could not create query for public abstract long
kr.spartaclub.coffeeorder.domain.order.repository.OrderRepository.countByMenuId(java.lang.Long);
No property 'menuId' found for type 'Order'
애플리케이션 실행 시 OrderRepository의 메서드를 찾을 수 없음.
menuId 필드를 제거했지만countByMenuId() 메서드가 남아있음1단계: 불필요한 메서드 제거
// OrderRepository.java
// ❌ 제거: Order 엔티티에 menuId가 없음
// long countByMenuId(Long menuId);
// List<Order> findByMenuIdOrderByOrderTimeDesc(Long menuId, Pageable pageable);
// ✅ 유지: 필요한 메서드만
List<Order> findByUserIdOrderByOrderTimeDesc(Long userId, Pageable pageable);
Page<Order> findByUserIdAndOrderTimeBetween(
Long userId,
LocalDateTime startDate,
LocalDateTime endDate,
Pageable pageable
);
2단계: 메뉴별 통계는 OrderItem 사용
// OrderItemRepository.java
List<OrderItem> findByMenuId(Long menuId);
long countByMenuId(Long menuId);
3단계: 애플리케이션 재시작
./gradlew clean build
./gradlew bootRun
docker-compose restart
# 재시작 후 테이블은 있지만 데이터가 없음
Docker 컨테이너 재시작 후 초기 데이터가 사라짐.
docker-compose restart는 초기화 스크립트를 재실행하지 않음데이터 완전 초기화가 필요한 경우:
# 1. 컨테이너와 볼륨 삭제
docker-compose down -v
# 2. 다시 시작 (초기화 스크립트 실행됨)
docker-compose up -d
# 3. 로그 확인
docker-compose logs mysql
데이터 유지하면서 재시작:
# 볼륨 유지하고 재시작
docker-compose restart mysql
수동으로 데이터 재삽입:
# MySQL 컨테이너 접속
docker exec -it coffee-order-mysql mysql -uroot -p1234 coffee_order
# SQL 파일 실행
source /docker-entrypoint-initdb.d/01-init.sql;
참고:
-v 옵션은 볼륨(데이터)을 삭제함down -v 후 up -d로 깨끗하게 초기화하는 것을 권장-v 옵션 사용 금지단일 메뉴만 주문 가능한 시스템을 여러 메뉴를 한 번에 주문할 수 있도록 변경 필요.
1단계: 데이터베이스 스키마 변경
orders 테이블:
-- ❌ 이전
CREATE TABLE orders (
order_id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
menu_id BIGINT NOT NULL, -- 제거
price INT NOT NULL, -- 제거
order_time DATETIME NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- ✅ 현재
CREATE TABLE orders (
order_id BIGINT PRIMARY KEY AUTO_INCREMENT,
user_id BIGINT NOT NULL,
total_price INT NOT NULL, -- 추가
order_time DATETIME NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
order_items 테이블 추가:
CREATE TABLE order_items (
order_item_id BIGINT PRIMARY KEY AUTO_INCREMENT,
order_id BIGINT NOT NULL,
menu_id BIGINT NOT NULL,
quantity INT NOT NULL,
price INT NOT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (order_id) REFERENCES orders(order_id) ON DELETE CASCADE,
FOREIGN KEY (menu_id) REFERENCES menus(menu_id) ON DELETE RESTRICT
);
2단계: 엔티티 수정
Order 엔티티:
@Entity
@Table(name = "orders")
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "order_id")
private Long id;
@Column(name = "user_id", nullable = false)
private Long userId;
// ❌ 제거
// private Long menuId;
// private Integer price;
// ✅ 추가
@Column(name = "total_price", nullable = false)
private Integer totalPrice;
@Column(name = "order_time", nullable = false)
private LocalDateTime orderTime;
}
OrderItem 엔티티 추가:
@Entity
@Table(name = "order_items")
public class OrderItem extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "order_item_id")
private Long id;
@Column(name = "order_id", nullable = false)
private Long orderId;
@Column(name = "menu_id", nullable = false)
private Long menuId;
@Column(nullable = false)
private Integer quantity;
@Column(nullable = false)
private Integer price;
public int getSubtotal() {
return price * quantity;
}
}
3단계: DTO 수정
CreateOrderRequest:
// ❌ 이전
public class CreateOrderRequest {
private Long menuId;
}
// ✅ 현재
public class CreateOrderRequest {
@NotEmpty(message = "주문 항목은 최소 1개 이상이어야 합니다")
private List<OrderItemRequest> items;
@Getter
@AllArgsConstructor
public static class OrderItemRequest {
@NotNull(message = "메뉴 ID는 필수입니다")
private Long menuId;
@NotNull(message = "수량은 필수입니다")
@Positive(message = "수량은 1개 이상이어야 합니다")
private Integer quantity;
}
}
OrderResponse:
// ✅ 현재
@Getter
@Builder
public class OrderResponse {
private Long orderId;
private Integer totalPrice;
private Integer remainingBalance;
private LocalDateTime orderTime;
private List<OrderItemResponse> items;
@Getter
@Builder
public static class OrderItemResponse {
private Long menuId;
private String menuName;
private Integer quantity;
private Integer price;
private Integer subtotal; // price * quantity
}
}
4단계: 서비스 로직 수정
@Transactional
public OrderResponse createOrder(Long userId, CreateOrderRequest request) {
// 1. 모든 메뉴 조회 및 검증
List<Long> menuIds = request.getItems().stream()
.map(item -> item.getMenuId())
.collect(Collectors.toList());
Map<Long, Menu> menuMap = menuRepository.findAllById(menuIds)
.stream()
.collect(Collectors.toMap(Menu::getId, menu -> menu));
// 2. 총 금액 계산
int totalPrice = 0;
List<OrderItemResponse> itemResponses = new ArrayList<>();
for (OrderItemRequest itemRequest : request.getItems()) {
Menu menu = menuMap.get(itemRequest.getMenuId());
if (menu == null) {
throw new MenuNotFoundException(itemRequest.getMenuId());
}
if (!menu.isAvailable()) {
throw new OrderException(ErrorCode.ORDER_002);
}
int subtotal = menu.getPrice() * itemRequest.getQuantity();
totalPrice += subtotal;
itemResponses.add(OrderItemResponse.of(
menu.getId(),
menu.getName(),
itemRequest.getQuantity(),
menu.getPrice()
));
}
// 3. 포인트 차감
int remainingBalance = userPointService.usePoint(userId, totalPrice, "주문 결제");
// 4. 주문 생성
Order order = Order.create(userId, totalPrice);
Order savedOrder = orderRepository.save(order);
// 5. 주문 상세 생성
for (OrderItemRequest itemRequest : request.getItems()) {
Menu menu = menuMap.get(itemRequest.getMenuId());
OrderItem orderItem = OrderItem.create(
savedOrder.getId(),
menu.getId(),
itemRequest.getQuantity(),
menu.getPrice()
);
orderItemRepository.save(orderItem);
// 6. 메뉴 통계 업데이트 (수량만큼)
updateMenuStatistics(menu.getId(), itemRequest.getQuantity());
}
return OrderResponse.of(savedOrder, itemResponses, remainingBalance);
}
5단계: Repository 정리
// OrderRepository.java
// ❌ 제거: Order에 menuId가 없음
// long countByMenuId(Long menuId);
// List<Order> findByMenuIdOrderByOrderTimeDesc(Long menuId, Pageable pageable);
// OrderItemRepository.java 추가
public interface OrderItemRepository extends JpaRepository<OrderItem, Long> {
List<OrderItem> findByOrderId(Long orderId);
List<OrderItem> findByOrderIdIn(List<Long> orderIds);
List<OrderItem> findByMenuId(Long menuId);
}
6단계: 데이터베이스 마이그레이션
# 1. 기존 데이터 백업 (필요시)
docker exec coffee-order-mysql mysqldump -uroot -p1234 coffee_order > backup.sql
# 2. 컨테이너와 볼륨 삭제
docker-compose down -v
# 3. 새 스키마로 재시작
docker-compose up -d
# 4. 확인
docker exec -it coffee-order-mysql mysql -uroot -p1234 coffee_order
SHOW TABLES;
DESC orders;
DESC order_items;
7단계: 문서 업데이트
# 애플리케이션 로그
./gradlew bootRun
# Docker 로그
docker-compose logs -f mysql
docker-compose logs -f redis
docker-compose logs -f kafka
# MySQL 접속
docker exec -it coffee-order-mysql mysql -uroot -p1234 coffee_order
# 테이블 확인
SHOW TABLES;
DESC users;
SELECT * FROM users;
# Redis CLI 접속
docker exec -it coffee-order-redis redis-cli -a redis_password
# 키 확인
KEYS *
GET lock:user:1:point
# Kafka UI 접속
http://localhost:8989
# 또는 CLI
docker exec -it coffee-order-kafka bash
kafka-topics --bootstrap-server localhost:9092 --list
# Windows
netstat -ano | findstr :8080
netstat -ano | findstr :3307
# 프로세스 종료
taskkill /PID <PID> /F