스터디카페 좌석 예약 시스템(backend-1st-studycafe)
본 프로젝트는 스터디카페에서 좌석을 예약하고 시간을 충전할 수 있는 간단한 Java 프로그램입니다. 사용자는 로그인 후 좌석 현황을 확인하고, 좌석을 예약하거나 시간을 충전할 수 있습니다. 모든 동작은 MySQL 데이터베이스와 연동되어 관리됩니다.
팀원이 진행한 JPA 리팩토링 코드를 리뷰해 보겠습니다.
- 개발 환경: Java, eclipse, MySQL
- 리팩토링 기간: 2024.08.30.
들어가며: JPA와 JPQL
- JPA에 대해 알아보기 전에 ORM에 대한 이해부터 필요합니다.
1) ORM: Object Relational Mapping
💡 Object, 즉 Java의 클래스(객체)와 RDB의 테이블을 연결한다는 의미 => ORM
- 객체(Object) - 관계형 데이터베이스의 테이블 간(DB 테이블의 Relation) 매핑합니다.
- 이 두 개념을 서로 연결(Mapping)하여 마치 하나인 것처럼 관리하기 위한 방법론입니다.
- ⇒ SQL문 작성 없이 Java 코드만으로 DB에 데이터 저장 가능합니다.
- DB에 데이터를 저장하기 위해서는 결국 JDBC API를 사용하지만, 보다 표면적인 코드를 통해 개발 생산성이 증가됩니다.
- ⇒ SQL 쿼리가 아닌, 직관적인 코드로 데이터를 조작할 수 있어 개발자가 객체지향 프로그래밍을 하는 데 집중할 수 있도록 도와줍니다.
- ORM 기술은 Application과 JDBC 인터페이스 사이에 위치합니다.
- DB 접근 계층(DAO)를 한 단계 추상화할 수 있습니다.
- 단점: 완벽한 ORM으로만 서비스를 구현하기 어렵습니다.
- 프로젝트의 복잡성이 커질경우 난이도도 올라갑니다.
- 잘못 구현하면 속도 저하 및 일관성이 무너지는 문제가 생길 수 있습니다.
2) JPA: Java Persistence API
💡 Java 진영에서 ORM 기술 표준으로 사용하는 인터페이스 모음
Java 어플리케이션에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스입니다.
인터페이스이기 때문에 Hibernate 등이 JPA를 구현합니다.
- Persistence, 지속성
- 애플리케이션을 여러 번 실행해도 데이터가 유지되는 성질입니다.
- 애플리케이션 내 Model 객체를 DB와 같은 저장 공간에 저장하는 것을 의미입니다.
- JPA: Java Persistence API
- 자바 진영에서 ORM 기술의 표준으로 자리잡은 인터페이스 집합입니다.
(ORM은 뭐라구? Java 객체 ↔ RDM 테이블 연결한다)
- 자바 진영에서 ORM 기술의 표준으로 자리잡은 인터페이스 집합입니다.
- JPA: 구현체 - Hibernate
- JavaEE 플랫폼에서 제공하는 규격입니다.
- 인터페이스로만 구성되어 있습니다.
- 실제 구현이 아닌 구현 클래스(implementation)는 따로 있습니다.
- 대표적인 JPA 구현체: Hibernate 하이버네이트 (그 외: eclipseLink, Kodo)
- JPA를 왜 사용해야 할까요?
- JPA는 반복적인 CRUD SQL을 처리해줍니다.
- SQL이 아닌 객체지향 중심으로 개발할 수 있다는 점이 가장 이점이라고 할 수 있습니다.
3) JPQL: Java Persistence Query Language
💡 엔티티 객체를 조회하는 객치지향 쿼리입니다.
- 테이블을 대상으로 쿼리하는 것이 아니라, 엔티티 객체를 대상으로 쿼리합니다.
- SQL과 비슷한 문법을 가지고, JPQL은 결국 SQL로 변환됩니다.
- SQL을 추상화했기 때문에 특정 벤더에 종속적이지 않습니다.
- JPA는 JPQL을 분석하여 SQL을 생성한 후 DB에서 조회합니다.
4) DTO: Data Transfer Object
- Soc: Separation of Concerns, 관심사 분리
- 소프트웨어 설계에서 서로 다른 기능이나 책임을 가진 부분을 명확히 구분하여 각 부분이 독립적으로 변경될 수 있도록 하는 설계 원칙입니다.
- Entity와 DTO의 역할
구분 | Entity | DTO |
역할 | 데이터베이스와 직접 연결된 객체입니다. 데이터베이스 테이블과 1:1로 매핑되어 있으며, 주로 비즈니스 로직과 데이터 관리에 사용됩니다. | 데이터를 전송(Transfer)하기 위한 객체입니다. 주로 애플리케이션의 다른 계층 간 데이터를 전송하는 데 사용됩니다. 데이터를 교환하는 목적을 갖는 객체이기에, 서비스 로직은 갖고 있지 않습니다. |
설계 | Entity는 데이터베이스 구조와 밀접하게 연관되어 있어 변경이 어려운 경우가 많습니다. 데이터베이스와 관련된 구조가 바뀌면 Entity도 변경해야 할 수 있습니다. | DTO는 클라이언트가 필요한 데이터만 포함하며, 개인정보 등 민간한 정보는 포함하지 않을 수 있습니다. |
예시 | User, Reservation 엔티티는 데이터베이스의 users, reservations 테이블과 직접적으로 매핑됩니다. | ReservationDTO는 예약 정보를 클라이언트에게 전달할 때 사용됩니다. 이들은 Entity와 다르게, 클라이언트가 필요한 형태로 데이터를 변환할 수 있습니다. |
- Entity와 DTO를 분리하는 이유
- 안정성: 데이터베이스 구조가 바뀌면 Entity도 함께 바뀔 수 있기에 Entity는 최대한 변경되지 않도록 설계하는 것이 좋습니다. DTO는 클라이언트 요구에 맞게 자주 변경될 수 있습니다. 즉, 클라이언트가 필요한 데이터만 포함하기에 Entity와 독립적으로 변동할 수 있습니다.
- 보안: Entity는 비밀번호, 이메일 등 민감함 정보가 포함될 수 있으나, DTO는 이를 제외한 채로 클라이언트에게 안전하게 데이터를 전달할 수 있습니다.
- 유연성: 클라이언트 요구사항에 따라 DTO의 구조는 변경될 수 있습니다. 서버 측 비즈니스 로직이나 데이터베이스 구조에 영향을 주지 않으면서 클라이언트와의 데이터 전송 형식을 조정할 수 있습니다.
JPA, JPQL 리팩토링 코드 리뷰
DTO
💡 Lombok 어노테이션, 필드
package com.reservation.model;
import lombok.*;
import java.time.LocalDate;
import java.time.LocalTime;
@Getter
@Setter
@ToString
@AllArgsConstructor
@NoArgsConstructor
public class ReservationDTO {
private int resId;
private LocalTime startTime;
private LocalTime endTime;
private LocalDate date;
private int seatId;
private int userUid;
}
- AS-IS
- 없습니다.
- TO-BE
- Lombok 어노테이션
- @Getter: 모든 필드에 대한 getter 메소드를 자동으로 생성합니다.
- @Setter: 모든 필드에 대한 setter 메소드를 자동으로 생성합니다.
- @ToString: toString() 메소드를 자동으로 생성합니다. 이 메소드는 객체의 문자열 표현을 반환합니다.
- @AllArgsConstructor: 모든 필드를 매개변수로 받는 생성자를 자동으로 생성합니다.
- @NoArgsConstructor: 매개변수가 없는 기본 생성자를 자동으로 생성합니다.
- 필드
- int resId: 예약의 고유 식별자로, 데이터베이스의 기본 키에 해당합니다.
- LocalTime startTiem, endTime: 예약 시작시작 및 종료시간입니다.
- LocalDate date: 예약 날짜입니다.
- int seatId: 예약한 좌석의 ID입니다.
- int userUid: 예약한 사용자의 ID입니다.
- Lombok 어노테이션
Model: Reservation 클래스
💡 @Entity, @Id, @GeneratedValue, @OneToOne, @JoinColumn
@Entity
@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class Reservation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
@Temporal(TemporalType.TIME)
private LocalTime startTime;
@Temporal(TemporalType.TIME)
private LocalTime endTime;
@Temporal(TemporalType.DATE)
private LocalDate date;
@OneToOne
@JoinColumn(name = "seat_id")
private Seat seat;
@OneToOne
@JoinColumn(name = "user_id")
private User user;
}
- AS-IS
- Reservation 클래스 내 필드값만 존재: private int resId, private LocalTime startTime ... private int userUid
- TO-BE
- @Entity: Reservation 클래스를 엔티티로 정의, JPA가 이 클래스를 DB 테이블과 매핑합니다.
- @Id: int id 필드를 Reservation 테이블의 기본키로 지정합니다.
- @GeneratedValue: DBMS별로 키 생성 전략(하기 참조)을 지정합니다.
- IDENTITY 전략을 사용할 경우, MySQL에서는 Auto_increment가 적용됩니다.
- IDENTITY: 주로 MySQL, PostgreSQL, DB2 SQL Server에서 사용합니다. ID를 명시적으로 설정하지 않아도 persist 시킨 엔티티들의 ID가 자동 증가합니다.
- => 먼저 엔티티를 DB에 저장한 후 식별자값을 획득하고, 영속성 컨텍스트에 저장합니다.
- SEQUENCE: DB의 시퀀스는 유일한 값을 반환합니다. 따라서 어플리케이션 구축 시 순번을 채번할 때 중복을 막기 위한 방법으로 종종 이용됩니다.
- => DB에 시퀀스 값을 획득한 후 영속성 컨텍스트에 저장합니다.
- TABLE: 키 생성 전용 테이블을 하나 만들고 DB의 시퀀스를 흉내내는 전략입니다. 테이블을 사용하는 것이므로 모든 DB에서 사용할 수 있습니다.
- => 시퀀스 생성용 테이블에서 조회한 후 영속성 컨텍스트에 저장합니다.
- AUTO: DB 종류에 따라 어떤 것을 지원하는지 따져서 전략을 설정하지 않아도 되는 AUTO 옵션도 있습니다. 방언에 따라서 전략을 자동 설정합니다. DB를 변경해도 코드를 수정하지 않아도 되고, 프로젝트 초반에 Id 생성전략이 정의되지 않았거나 프로토타입성 프로젝트를 만든다면 이 전략을 고려해 볼 수 있습니다.
- @Temporal
- JPA에서는 @Temporal을 사용해 날짜 및 시간 타입을 매핑합니다.
- TemporalType.TIME: LocalTime 필드를 데이터베이스의 시간 필드에 매핑합니다.
- TemporalType.DATE: LocalDate 필드를 데이터베이스의 날짜 필드에 매핑합니다.
- @OneToOne: 다른 엔티티와의 일대일 관계를 나타냅니다.
- 한 Reservation 엔티티가 하나의 Seat, 하나의 User 엔티티와 관련이 있음을 나타냅니다.
- => 하나의 예약이 하나의 좌석과 하나의 사용자와 관련이 있음을 의미합니다.
- 한 Reservation 엔티티가 하나의 Seat, 하나의 User 엔티티와 관련이 있음을 나타냅니다.
- @JoinColumn: 외래키 컬럼의 이름을 정의합니다.
- name = "seat_id": 이 컬럼은 Seat 엔티티와의 관계를 나타냅니다.
- name = "user_id": 이 컬럼은 User 엔티티와의 관계를 나타냅니다.
- Lombok 어노테이션
- @Getter: 모든 필드에 대한 getter 메소드를 자동으로 생성합니다.
- @ToString: toString() 메소드를 자동으로 생성합니다. 객체의 문자열 표현을 생성합니다.
- @AllArgsConstructor: 모든 필드를 매개변수로 가지는 생성자를 자동으로 생성합니다.
- @NoArgsConstructor: 매개변수가 없는 기본 생성자를 자동으로 생성합니다.
- @Builder: 빌더 패턴을 지원하는 생성자를 자동으로 생성합니다. 객체 생성 시 유연하게 필드를 설정할 수 잇습니다.
⚠️ @Builder를 사용하고 setter 메소드를 사용하지 않은 이유는?
- 가변성 vs. 불변성
- @Setter: 모든 필드에 대해 setter 메소드를 제공하기에, 객체가 생성된 후에도 필드 값을 변경할 수 있습니다. 즉, 객체가 가변적입니다.
- @Builder: 빌더 패턴을 사용하여 객체를 생성하므로, 필드를 설정한 후 객체를 불변 상태로 유지할 수 있습니다. 즉, 객체의 일관성을 보장할 수 있습니다. 이는 멀티스레드 환경에서의 안정성을 높이고, 객체의 상태가 변하지 않도록 보장합니다.
- @Builder를 사용하면 객체를 불변 상태로 유지할 수 있고, 필드를 선택적으로 설정할 수 있어 코드의 안정성과 가독성을 높일 수 있습니다. 따라서 @Builder를 사용하는 경우에는 일반적으로 @Setter를 추가하지 않습니다.
+) 생성자 vs. Setter vs. 빌더 패턴
구분 | 생성자(Constructor) | Setter 메서드 | Builder 패턴 |
비교 | 객체를 생성할 때 필요한 모든 값을 한 번에 설정해야 합니다. 필드가 많아질수록 매개변수의 순서가 헷갈리거나, 불필욯나 값까지 입력해야 하는 경우가 생길 수 있습니다. |
객체를 생성한 후에 필요한 값을 개별적으로 사용할 수 있습니다. 유연성은 높지만, 객체가 완전히 초기화되지 않았을 때 사용자가 실수로 해당 객체를 사용하게 될 위험이 있습니다. |
객체를 단계적으로 구성하면서 불필요한 필드를 건너뛸 수 있습니다. 빌더 패턴은 객체의 생성과 설정을 분리하여 더 직관적이고 안정한 객체 구성을 지원합니다. |
적용 케이스 | 하기 | 코드 | 참조 |
// 생성자
// 생성자 적용 케이스
public class User {
private String name;
private int age;
private int height;
private int iq;
// 전체 필드를 초기화하는 생성자
public User(String name, int age, int height, int iq) {
this.name = name;
this.age = age;
this.height = height;
this.iq = iq;
}
// 추가 메서드
public void displayInfo() {
System.out.println("Name: " + name);
System.out.println("Age: " + age);
System.out.println("Height: " + height);
System.out.println("IQ: " + iq);
}
}
// 생성자 사용의 문제점
User user = new User("테스트", 181);
user.displayInfo();
// Name: 테스트
// Age: 0
// Height: 181
// IQ: 0
// Setter
// Setter 메서드 적용 케이스
Uesr user = new User();
user.setName("테스트");
user.setHeight(181);
user.setIq(121);
// Setter 메서드 순서에 따른 문제
user.setHeight(181);
user.calculateBMI(); // height가 설정되었지만 weight가 설정되지 않으면 BMI 계산이 올바르지 않음
user.setWeight(75);
// Builder
// Builder 패턴 적용 케이스
User user = User.builder()
.name("테스트")
.height(181)
.iq(121)
.build();
Data
💡EntityManagerFactory, EntityManager, EntityTransaction
transaction.begin(), commit(), manager.find, manager.persist, manager.createQuery
public class ReservationDAO {
EntityManagerFactory factory = Persistence.createEntityManagerFactory("studycafe");
EntityManager manager = factory.createEntityManager();
EntityTransaction transaction = manager.getTransaction();
public void createReservation(ReservationDTO reservationDTO) {
transaction.begin();
Seat seat = manager.find(Seat.class, reservationDTO.getSeatId());
User user = manager.find(User.class, reservationDTO.getUserUid());
Reservation reservation = Reservation.builder()
.startTime(reservationDTO.getStartTime())
.endTime(reservationDTO.getEndTime())
.date(reservationDTO.getDate())
.seat(seat)
.user(user)
.build();
manager.persist(reservation);
transaction.commit();
}
public boolean isSeatAvailable(int seatId) {
List<Seat> resultList = manager.createQuery("select count(s) from Seat s where s.id = :seatId and s.id "
+ "not in (select s.id from Reservation r", Seat.class)
.setParameter(seatId, factory)
.getResultList();
if(resultList.size() >= 1) {
return true;
}
return false;
}
}
- AS-IS
- query문과 try~catch문을 통해 데이터를 조회했습니다.
- TO-BE
- EntityManagerFactory: JPA에서 EntityManager를 생성하기 위한 팩토리입니다.
- Persistence.createEntityManagerFactory("studycafe"): persistence.xml 파일에 정의된 studycafe 이름의 EntityManagerFactory를 생성합니다.
- EntityManager: DB와의 상호작용을 위한 JPA 주요 인터페이스입니다.
- EntityTransaction: 트랜잭션을 관리하는 객체입니다. DB 작업을 트랜잭션으로 묶어 원자성을 보장합니다.
- createReservation 메소드
- transaction.begin(), transaction.commit(): 트랜잭션을 시작, 커밋하여 모든 변경사항을 데이터베이스에 적용합니다.
- manager.find(Seat.class, reservationDTO.getSeatId()): setId를 사용하여 데이터베이스에서 Seat 엔티티를 조회합니다.
- manager.find(User.class, reservationDTO.getUserUid()): userUid를 사용하여 데이터베이스에서 User 엔티티를 조회합니다.
- Reservation.builder(): Reservation 객체를 생성하기 위해 빌더 패턴을 사용합니다. DTO에서 전달된 데이터를 사용하여 객체를 생성합니다.
- manager.persist(reservation): 빌더 패턴을 통해 생성된 Reservation 객체를 데이터베이스에 저장합니다.
- isSeatAvailable 메소드
- 쿼리 작성 및 실행
- manager.createQuery: JPQL 쿼리는 Seat의 개수를 카운트합니다. seatId에 해당하는 좌석이 Reservation에 없는 경우를 찾습니다.
- resultList.size() >= 1: 쿼리의 결과로 반환된 리스트의 크기를 검사합니다. 쿼리 결과가 1개 이상이면 좌석이 사용 가능하다고 판단합니다.
- 쿼리 작성 및 실행
- EntityManagerFactory: JPA에서 EntityManager를 생성하기 위한 팩토리입니다.
총평
JPA와 더불어 JPQL까지 공부할 수 있게 해 주신 선생님께 압도적 감사 인사를 드리며... JPQL 문법과 관련해서는 추후 더 공부하여 포스팅하도록 하겠습니다.
GitHub - woorifisa-service-dev-3rd/backend-1st-studycafe: 스터디카페 예약 관리 콘솔 프로젝트
스터디카페 예약 관리 콘솔 프로젝트. Contribute to woorifisa-service-dev-3rd/backend-1st-studycafe development by creating an account on GitHub.
github.com
반응형
'프로젝트 > 개인 개발일지' 카테고리의 다른 글
[FE/Next.js/Storybook] 25.08.04 - 스토리북 적용 및 PostCSS 오류 해결 (2) | 2025.08.04 |
---|---|
[Java/Toy Project] 팀원들과 함께 만든 교통수단 선택기 (0) | 2024.08.17 |
[Java/Toy Project] 하루만에 만든 나만의 교통수단 선택기 (0) | 2024.08.16 |