대용량 주문 처리 API 서버를 만들면서 Java 백엔드 기술을 정리합니다.
기술 스택
주문 시스템에 필요한 테이블 5개를 설계했습니다.
Member ─ 1:N ─ Order ─ 1:N ─ OrderItem ─ N:1 ─ Product
│
1:1
│
Delivery
| 테이블 | 설명 |
|---|---|
| members | 회원 정보 |
| orders | 주문 정보 |
| order_items | 주문 상품 목록 |
| products | 상품 정보 |
| deliveries | 배송 정보 |
주문 당시 가격을 스냅샷으로 저장합니다.
상품 가격이 나중에 변경되더라도 주문 이력은 결제 시점 금액을 유지해야 하기 때문입니다.
@Builder
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password;
private String userName;
@OneToMany(mappedBy = "member")
private List<Order> orderList;
@CreationTimestamp
private LocalDateTime createdAt;
}
@Getter
@Table(name = "orders")
@Entity
public class Order {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ORDER_ID")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
@OneToMany(mappedBy = "order")
private List<OrderItem> orderItemList = new ArrayList<>();
@OneToOne(mappedBy = "order", fetch = FetchType.LAZY)
private Delivery delivery;
@Enumerated(EnumType.STRING)
private OrderStatus orderStatus;
private Long orderPrice;
@Enumerated(EnumType.STRING)
private PaymentStatus paymentStatus;
@Enumerated(EnumType.STRING)
private OrderType orderType;
private LocalDateTime orderedAt;
private LocalDateTime paidAt;
}
@Getter
@Table(name = "ORDER_ITEM")
@Entity
public class OrderItem {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "ORDER_ITEM_ID")
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "product_id")
private Product product;
private int quantity;
private int orderPrice;
}
@Getter
@Table(name = "product")
@Entity
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "PRODUCT_ID")
private Long id;
private String name;
private int price;
private int stockQuantity;
private LocalDateTime createdAt;
}
@Getter
@Table(name = "deliveries")
@Entity
public class Delivery {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "order_id")
private Order order;
private String address;
@Enumerated(EnumType.STRING)
private DeliveryEnum status;
private LocalDateTime createdAt;
}
양방향 연관관계에서 연관관계의 주인을 지정합니다.
주인이 아닌 쪽은 외래키를 관리하지 않고 조회만 가능합니다.
// Order가 주인 — member_id 외래키 관리
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
// Member는 주인 아님 — 읽기만 가능
@OneToMany(mappedBy = "member")
private List<Order> orderList;
연관된 Entity를 실제로 사용할 때 쿼리를 날립니다.
EAGER는 연관 Entity를 무조건 즉시 로딩해서 불필요한 쿼리가 발생할 수 있습니다.
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
boolean existsMemberByEmail(String email);
}
public Long signUp(SignUpRequest signUpRequest) {
// 이메일 중복 체크
boolean isAlreadyMember = memberRepository.existsMemberByEmail(signUpRequest.getEmail());
if (isAlreadyMember) {
throw new IllegalArgumentException("이미 가입된 회원 이메일입니다.");
}
// 비밀번호 암호화
String encodePassword = passwordEncoder.encode(signUpRequest.getPassword());
// 빌더 패턴으로 객체 생성
Member member = Member.builder()
.email(signUpRequest.getEmail())
.password(encodePassword)
.userName(signUpRequest.getUserName())
.build();
memberRepository.save(member);
return member.getId();
}
@RequiredArgsConstructor
@RestController
@RequestMapping("/member")
public class MemberController {
private final MemberService memberService;
@PostMapping("/signUp")
public ResponseEntity<Long> signUp(@RequestBody SignUpRequest signUpRequest) {
memberService.signUp(signUpRequest);
return ResponseEntity.ok().build();
}
}
POST http://localhost:8080/member/signUp
Content-Type: application/json
{
"email": "test@test.com",
"password": "1234",
"userName": "홍길동"
}