Skip to content
Open
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
db45fad
fix: 의존성 충돌 해결
DWL21 Apr 11, 2025
d5566d2
feat: 1단계 - 로그인
yueunfive Apr 15, 2025
cdffb4f
feat: 2단계 - 로그인 리팩터링
yueunfive Apr 16, 2025
11b44d1
feat: 3단계 - 관리자 기능
yueunfive Apr 16, 2025
c6b8613
refactor: resolveArgument 반환 타입 변경
yueunfive Apr 16, 2025
c9d4f78
Merge branch 'logan' of https://github.com/yueunfive/java-sprout into…
yueunfive Apr 16, 2025
96b1a20
refactor: 쿠키 유효기간 설정 제거
yueunfive Apr 18, 2025
1ae7379
refactor: RowMapper 변수 분리
yueunfive Apr 18, 2025
1215822
refactor: GlobalExceptionHandler 호출을 위해 상위 @ControllerAdvice 제거
yueunfive Apr 18, 2025
5de5b63
refactor: 예약 생성 로직 수정
yueunfive Apr 18, 2025
15a68e8
refactor: @ExceptionHandler(Exception.class) 추가
yueunfive Apr 18, 2025
9b8ad45
refactor: 예약 생성 로직 수정
yueunfive Apr 18, 2025
3ba6966
refactor: DTO class to record
yueunfive Apr 18, 2025
3dbf00f
feat: 4단계: JPA 전환
yueunfive Apr 19, 2025
901bd5e
feat: 5단계 - 내 예약 목록 조회
yueunfive Apr 19, 2025
9409618
feat: 6단계 - 예약 대기 기능
yueunfive Apr 20, 2025
da5e586
refactor: 예약 상태 관리 구조 개선
yueunfive Apr 25, 2025
6ee806a
refactor: dao to repository
yueunfive Apr 25, 2025
b058fd2
refactor: merge conflict resolve
yueunfive Apr 25, 2025
bddf51d
refactor: remove .DS_Store
yueunfive Apr 25, 2025
4dc79da
refactor: API 변경사항에 맞게 타임리프 JS 수정
yueunfive Apr 28, 2025
8fc5bc8
refactor: Spring Validation 적용
yueunfive Apr 28, 2025
25e7439
refactor: dto 내 정적 팩토리 메서드 추가(+a)
yueunfive Apr 28, 2025
e25c043
refactor: 엔티티 기본 생성자 - public to protected
yueunfive Apr 28, 2025
44e36f0
refactor: schema.sql - CREATE문 제거
yueunfive Apr 29, 2025
8ab2938
feat: 예약 취소 기능 구현
yueunfive Apr 30, 2025
b487f68
feat: admin 예약 추가 기능 구현
yueunfive May 1, 2025
e65be6d
feat: admin 예약 목록 조회 기능 구현
yueunfive May 1, 2025
d8dd999
refactor: admin 페이지 접근 관련 에외 메시지 처리
yueunfive May 1, 2025
ad56b75
refactor: 예약 삭제 권한 수정
yueunfive May 1, 2025
d0b4b25
refactor: N+1 해결 with fetch join
yueunfive May 1, 2025
05739f8
refactor: 예약 생성시 중복체크
yueunfive May 1, 2025
df7a4d0
refactor: 이메일 중복 방지 및 형식 검증 기능 추가
yueunfive May 3, 2025
335cc99
fix: Time 및 Theme 삭제 시 예외 처리 추가
yueunfive May 3, 2025
e064a8d
refactor: Member 역할 enum 적용 및 isAdmin 메서드 도입
yueunfive May 3, 2025
ea79c7f
fix: SQL 초기 데이터와 충돌하지 않도록 테스트 코드 수정
yueunfive May 3, 2025
4c3e11e
fix: 예약 없는 슬롯(날짜/시간/테마)에 대한 대기 신청 방지 로직 추가
yueunfive May 3, 2025
75b32e6
test: ReservationServiceTest 추가 및 테스트용 DB 분리
yueunfive May 3, 2025
b4c2c18
test: MemberServiceTest 추가
yueunfive May 3, 2025
18dd7a8
test: AuthServiceTest 추가
yueunfive May 4, 2025
f2e6f8b
test: TimeServiceTest 추가
yueunfive May 4, 2025
b1e72b8
fix: 시간 중복 저장 방지 로직 추가
yueunfive May 4, 2025
f3b1434
test: ThemeServiceTest 추가
yueunfive May 4, 2025
ed6767a
fix: 테마 중복 저장 방지 로직 추가
yueunfive May 4, 2025
01ea224
test: 중복 time_value('10:00') 회피를 위해 테스트 데이터 수정
yueunfive May 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
.DS_Store

### STS ###
.apt_generated
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter:3.4.4'
implementation 'org.springframework.boot:spring-boot-starter-web:3.4.4'
implementation 'org.springframework.boot:spring-boot-starter-jdbc:3.4.4'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf:3.4.4'

implementation 'dev.akkinoc.spring.boot:logback-access-spring-boot-starter:4.5.1'
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/com/yourssu/roomescape/PageController.java
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public String reservation() {
return "reservation";
}

@GetMapping("/reservation-mine")
@GetMapping("/reservation/mine")
public String myReservation() {
return "reservation-mine";
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/com/yourssu/roomescape/auth/AuthController.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.yourssu.roomescape.auth;

import com.yourssu.roomescape.auth.dto.CheckLoginResponse;
import com.yourssu.roomescape.auth.dto.LoginRequest;
import com.yourssu.roomescape.jwt.TokenExtractor;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
Expand Down
17 changes: 11 additions & 6 deletions src/main/java/com/yourssu/roomescape/auth/AuthService.java
Original file line number Diff line number Diff line change
@@ -1,28 +1,33 @@
package com.yourssu.roomescape.auth;

import com.yourssu.roomescape.auth.dto.LoginRequest;
import com.yourssu.roomescape.exception.CustomException;
import com.yourssu.roomescape.exception.ErrorCode;
import com.yourssu.roomescape.jwt.TokenProvider;
import com.yourssu.roomescape.member.Member;
import com.yourssu.roomescape.member.MemberDao;
import com.yourssu.roomescape.member.MemberRepository;
import org.springframework.stereotype.Service;

@Service
public class AuthService {

private final MemberDao memberDao;
private final MemberRepository memberRepository;
private final TokenProvider tokenProvider;

public AuthService(TokenProvider tokenProvider, MemberDao memberDao) {
public AuthService(TokenProvider tokenProvider, MemberRepository memberRepository) {
this.tokenProvider = tokenProvider;
this.memberDao = memberDao;
this.memberRepository = memberRepository;
}

public String login(LoginRequest loginRequest) {
Member member = memberDao.findByEmailAndPassword(loginRequest.email(), loginRequest.password());
Member member = memberRepository.findByEmailAndPassword(loginRequest.email(), loginRequest.password())
.orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));
return tokenProvider.createToken(member.getEmail());
Comment on lines +23 to 25
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

토큰을 만들 때 멤버의 email 정보로 만들고 있는데 , 지금 유은님 코드 상으로는 동일한 이메일로 두 개 이상의 Member가 생길 수 있어요. 이러면 어떻게 될까요?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public Member checkLogin(String token) {
        String payload = tokenProvider.getPayload(token);
        return memberRepository.findByEmail(payload)
                .orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));
    }

해당 부분에서 token에서 추출한 payload(email)가 DB에 중복으로 존재할 경우,
findByEmail()이 2개 이상의 결과를 반환하면서 IncorrectResultSizeDataAccessException 예외가 발생합니다.
👉 이메일 중복 문제가 발생하지 않도록 Member 엔티티의 email 필드에 @column(unique = true)를 적용하여 DB 레벨에서 중복을 방지하고, 회원가입 로직에서는 existsByEmail을 통해 사전 중복 검사를 수행한 뒤, 중복 시 CustomException을 발생시키도록 보완했습니다.

}

public Member checkLogin(String token) {
String payload = tokenProvider.getPayload(token);
return memberDao.findByEmail(payload);
return memberRepository.findByEmail(payload)
.orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.yourssu.roomescape.auth;
package com.yourssu.roomescape.auth.dto;

public record CheckLoginResponse(String name) {
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.yourssu.roomescape.auth;
package com.yourssu.roomescape.auth.dto;

public record LoginRequest(String email, String password) {
}
2 changes: 2 additions & 0 deletions src/main/java/com/yourssu/roomescape/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ public enum ErrorCode {
COOKIE_NOT_FOUND(HttpStatus.UNAUTHORIZED, "쿠키가 존재하지 않습니다."),
TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "쿠키에 토큰이 존재하지 않습니다."),
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자의 정보를 찾을 수 없습니다."),
TIME_NOT_FOUND(HttpStatus.NOT_FOUND, "시간 정보를 찾을 수 없습니다."),
THEME_NOT_FOUND(HttpStatus.NOT_FOUND, "테마 정보를 찾을 수 없습니다."),
INVALID_RESERVATION_REQUEST(HttpStatus.BAD_REQUEST, "예약 요청 값이 누락되었습니다."),
UNAUTHORIZED_ACCESS(HttpStatus.FORBIDDEN, "해당 접근에 대한 권한이 없습니다."),
DUPLICATE_MEMBER_NAME_EXISTS(HttpStatus.INTERNAL_SERVER_ERROR, "중복된 이름이 존재합니다. 관리자에게 문의하세요.");
Expand Down
14 changes: 14 additions & 0 deletions src/main/java/com/yourssu/roomescape/member/Member.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
package com.yourssu.roomescape.member;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Member {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
Expand All @@ -21,6 +30,11 @@ public Member(String name, String email, String password, String role) {
this.role = role;
}

public Member() {

}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 기본 생성자는 왜 필요할까요? (노션에 적어둔 거 답변해주시면 돼요!)

Copy link
Author

@yueunfive yueunfive Apr 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Entity 클래스를 관리하는 JPA는 데이터베이스에서 데이터를 조회하여 객체를 생성할 때, 일반적인 new 키워드를 사용하는 것이 아니라 리플렉션(Reflection) 기법을 이용하여 객체를 인스턴스화합니다.
이때 리플렉션은 매개변수가 없는 생성자가 존재해야만 정상적으로 객체를 생성할 수 있습니다. 만약 기본 생성자가 없다면, JPA는 객체를 생성할 수 없어 런타임 시 No default constructor for entity 예외가 발생합니다.
또한, 일부 IDE나 빌드 도구에서는 이 문제를 사전에 감지하여 컴파일 타임에 Class 'Member' should have [public, protected] no-arg constructor와 같은 에러를 발생시킬 수 있습니다.
기본 생성자는 반드시 public 또는 protected 접근 제어자를 가져야 하며, private 생성자는 리플렉션 접근이 제한되기 때문에 사용할 수 없습니다.

💡 리플렉션(Reflection)이란?

  • 자바 프로그램이 실행 중일 때, 클래스 정보를 읽어서 객체를 만들거나, 변수 값을 수정하거나, 메서드를 호출하는 기능
  • 쉽게 말하면, 자바 프로그램이 자기 자신을 들여다보고 조작할 수 있는 기능

❔ protected와 public, 둘 중 어떤 걸 사용하는게 좋을까?

  • 기본 생성자는 외부에서 직접 사용할 필요가 없기 때문에 protected로 제한하는 것이 안전하다.
  • 비즈니스 로직용 생성자(public Member(String name, String email, ...))는 public으로 열려 있으므로 Service나 다른 코드에서 객체 생성이 가능하다.



public Long getId() {
return id;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.yourssu.roomescape.member;

import com.yourssu.roomescape.member.dto.MemberRequest;
import com.yourssu.roomescape.member.dto.MemberResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.ResponseEntity;
Expand Down
80 changes: 0 additions & 80 deletions src/main/java/com/yourssu/roomescape/member/MemberDao.java

This file was deleted.

12 changes: 12 additions & 0 deletions src/main/java/com/yourssu/roomescape/member/MemberRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.yourssu.roomescape.member;

import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {

Optional<Member> findByEmailAndPassword(String email, String password);
Optional<Member> findByEmail(String email);
Optional<Member> findByName(String name);
}
10 changes: 6 additions & 4 deletions src/main/java/com/yourssu/roomescape/member/MemberService.java
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package com.yourssu.roomescape.member;

import com.yourssu.roomescape.member.dto.MemberRequest;
import com.yourssu.roomescape.member.dto.MemberResponse;
import org.springframework.stereotype.Service;

@Service
public class MemberService {
private MemberDao memberDao;
private final MemberRepository memberRepository;

public MemberService(MemberDao memberDao) {
this.memberDao = memberDao;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}

public MemberResponse createMember(MemberRequest memberRequest) {
Member member = memberDao.save(new Member(memberRequest.getName(), memberRequest.getEmail(), memberRequest.getPassword(), "USER"));
Member member = memberRepository.save(new Member(memberRequest.getName(), memberRequest.getEmail(), memberRequest.getPassword(), "USER"));
return new MemberResponse(member.getId(), member.getName(), member.getEmail());
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.yourssu.roomescape.member;
package com.yourssu.roomescape.member.dto;

public class MemberRequest {
private String name;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.yourssu.roomescape.member;
package com.yourssu.roomescape.member.dto;

public class MemberResponse {
private Long id;
Expand Down
38 changes: 31 additions & 7 deletions src/main/java/com/yourssu/roomescape/reservation/Reservation.java
Original file line number Diff line number Diff line change
@@ -1,28 +1,48 @@
package com.yourssu.roomescape.reservation;

import com.yourssu.roomescape.member.Member;
import com.yourssu.roomescape.theme.Theme;
import com.yourssu.roomescape.time.Time;
import jakarta.persistence.*;

@Entity
public class Reservation {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "member_id")
private Member member;
Comment on lines +15 to +17
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fetch 설정을 LAZY로 하신 이유가 궁금해요!

Copy link
Author

@yueunfive yueunfive Apr 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ManyToOne 관계는 기본이 EAGER로 설정되어 있지만, 항상 연관된 Member 객체를 즉시 사용할 필요는 없기 때문에 LAZY로 변경했습니다.
지연 로딩(LAZY)을 적용하면 실제로 Member가 필요한 시점까지 조회를 미루어 불필요한 조인과 데이터베이스 부하를 줄일 수 있습니다.
반면, 기본 설정인 즉시 로딩(EAGER)을 그대로 사용할 경우, 연관된 Member를 항상 즉시 조회하게 되어 불필요한 조인이 발생할 수 있습니다. 특히, 여러 Reservation을 조회할 때 EAGER 설정이 되어 있으면 각 Reservation마다 Member를 개별적으로 조회하게 되어 N+1 문제가 발생할 수 있습니다.
이를 방지하고 필요한 경우에만 연관 객체를 가져오기 위해 기본 fetch 전략을 LAZY로 변경했습니다. 다만 LAZY를 사용할 경우 실제로 Member 정보가 필요한 상황에서는 조회 시점마다 추가 쿼리가 발생할 수 있으므로, 이때는 fetch join이나 @EntityGraph를 사용하여 명시적으로 함께 가져오도록 처리할 수 있습니다.
이렇게 기본 fetch 전략을 LAZY로 설정하면 성능 최적화와 유연한 데이터 조회가 모두 가능해집니다.

📚 참고자료

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

지금 유은님 코드를 보면 응답 dto를 만들 때 결국 Time이나 Theme의 값이 필요하기 때문에 추가 쿼리가 발생하고 있어요

Copy link
Author

@yueunfive yueunfive May 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reservation 객체를 리스트로 조회할 때 발생하던 추가 쿼리를 방지하기 위해,
fetch join을 사용하여 해당 부분을 보완하였습니다.

1️⃣ fetch join vs @EntityGraph

  • 쿼리 작성 방식:
    • Fetch Join은 JPQL에서 join fetch를 명시적으로 작성해야 한다.
    • EntityGraph는 메서드 위에 @EntityGraph 애노테이션을 붙여 설정만으로 쿼리를 간접 지정한다.
  • 조인 방식:
    • Fetch Join은 INNER JOIN이 기본이며, 필요 시 LEFT JOIN으로 명시적으로 조인 방식을 선택할 수 있다.
    • EntityGraph는 기본적으로 LEFT OUTER JOIN을 사용한다.
  • 유연성 및 재사용성:
    • EntityGraph는 여러 메서드에서 재사용 가능하고 유지보수가 용이하다.
    • Fetch Join은 쿼리마다 명시해야 하므로 반복적일 수 있다.

2️⃣ ReservationRepository 내 findByStatus()를 통해 비교

기본(fetch join 적용 X)

Hibernate: 
    select 
        m1_0.id,
        m1_0.email,
        m1_0.name,
        m1_0.password,
        m1_0.role 
    from
        member m1_0 
    where
        m1_0.email=?
Hibernate: 
    select 
        t1_0.id,
        t1_0.time_value 
    from
        time t1_0
Hibernate: 
    select
        m1_0.id,
        m1_0.email,
        m1_0.name,
        m1_0.password,
        m1_0.role 
    from
        member m1_0 
    where
        m1_0.email=?
Hibernate: 
    select
        t1_0.id,
        t1_0.description,
        t1_0.name 
    from
        theme t1_0
Hibernate: 
    select
        m1_0.id,
        m1_0.email,
        m1_0.name,
        m1_0.password,
        m1_0.role 
    from
        member m1_0 
    where
        m1_0.email=?
Hibernate: 
    select
        r1_0.id,
        r1_0.date,
        r1_0.member_id,
        r1_0.status,
        r1_0.theme_id,
        r1_0.time_id 
    from
        reservation r1_0 
    where
        r1_0.status=?
Hibernate: 
    select
        t1_0.id,
        t1_0.description,
        t1_0.name 
    from
        theme t1_0 
    where
        t1_0.id=?
Hibernate: 
    select
        t1_0.id,
        t1_0.time_value 
    from
        time t1_0 
    where
        t1_0.id=?
Hibernate: 
    select
        t1_0.id,
        t1_0.description,
        t1_0.name 
    from
        theme t1_0 
    where
        t1_0.id=?
Hibernate: 
    select
        t1_0.id,
        t1_0.time_value 
    from
        time t1_0 
    where
        t1_0.id=?
Hibernate: 
    select
        t1_0.id,
        t1_0.description,
        t1_0.name 
    from
        theme t1_0 
    where
        t1_0.id=?
Hibernate: 
    select
        t1_0.id,
        t1_0.time_value 
    from
        time t1_0 
    where
        t1_0.id=?
Fetch Join 쿼리

Hibernate: 
    select
        m1_0.id,
        m1_0.email,
        m1_0.name,
        m1_0.password,
        m1_0.role 
    from
        member m1_0 
    where
        m1_0.email=?
Hibernate: 
    select
        m1_0.id,
        m1_0.email,
        m1_0.name,
        m1_0.password,
        m1_0.role 
    from
        member m1_0 
    where
        m1_0.email=?
Hibernate: 
    select
        m1_0.id,
        m1_0.email,
        m1_0.name,
        m1_0.password,
        m1_0.role 
    from
        member m1_0 
    where
        m1_0.email=?
Hibernate: 
    select
        t1_0.id,
        t1_0.time_value 
    from
        time t1_0
Hibernate: 
    select
        t1_0.id,
        t1_0.description,
        t1_0.name 
    from
        theme t1_0
Hibernate: 
    select
        r1_0.id,
        r1_0.date,
        m1_0.id,
        m1_0.email,
        m1_0.name,
        m1_0.password,
        m1_0.role,
        r1_0.status,
        t2_0.id,
        t2_0.description,
        t2_0.name,
        t1_0.id,
        t1_0.time_value 
    from
        reservation r1_0 
    join
        member m1_0 
            on m1_0.id=r1_0.member_id 
    join
        time t1_0 
            on t1_0.id=r1_0.time_id 
    join
        theme t2_0 
            on t2_0.id=r1_0.theme_id 
    where
        r1_0.status=?
EntityGraph 쿼리

Hibernate: 
    select
        m1_0.id,
        m1_0.email,
        m1_0.name,
        m1_0.password,
        m1_0.role 
    from
        member m1_0 
    where
        m1_0.email=?
Hibernate: 
    select
        t1_0.id,
        t1_0.time_value 
    from
        time t1_0
Hibernate: 
    select
        m1_0.id,
        m1_0.email,
        m1_0.name,
        m1_0.password,
        m1_0.role 
    from
        member m1_0 
    where
        m1_0.email=?
Hibernate: 
    select
        t1_0.id,
        t1_0.description,
        t1_0.name 
    from
        theme t1_0
Hibernate: 
    select
        m1_0.id,
        m1_0.email,
        m1_0.name,
        m1_0.password,
        m1_0.role 
    from
        member m1_0 
    where
        m1_0.email=?
Hibernate: 
    select
        r1_0.id,
        r1_0.date,
        m1_0.id,
        m1_0.email,
        m1_0.name,
        m1_0.password,
        m1_0.role,
        r1_0.status,
        t1_0.id,
        t1_0.description,
        t1_0.name,
        t2_0.id,
        t2_0.time_value 
    from
        reservation r1_0 
    left join
        member m1_0 
            on m1_0.id=r1_0.member_id 
    left join
        theme t1_0 
            on t1_0.id=r1_0.theme_id 
    left join
        time t2_0 
            on t2_0.id=r1_0.time_id 
    where
        r1_0.status=?

3️⃣ 개인 의견
위 개념에 대해 학습하며, 복잡한 쿼리를 제어하고 성능을 고려해야 할 경우에는 fetch join을, 간결하고 재사용 가능한 코드를 원할 경우에는 @EntityGraph를 사용하는 것이 적절하다고 생각했습니다.
이 두 가지 방법을 함께 활용하는 방안을 고민하던 중 여러 자료를 참고하게 되었고, 그 과정에서 findAll()과 같이 기본 메서드를 오버라이드할 때만 @EntityGraph를 사용하고, 그 외에는 fetch join을 사용하는 방식도 존재함을 알게 되었습니다.
어떤 방식이 가장 적절한지에 대해 아직은 명확한 결론을 내리지 못한 상태입니다. 의견 주시면 감사하겠습니다🙏
추가로 @NamedEntityGraph에 대해서도 함께 학습해볼 예정입니다.


private String date;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "time_id")
private Time time;

@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "theme_id")
private Theme theme;

public Reservation(Long id, String name, String date, Time time, Theme theme) {
@Enumerated(EnumType.STRING)
private ReservationStatus status;
Comment on lines +29 to +30
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EnumType에는 무엇이 있나요? 그 중 STRING으로 설정하신 이유가 궁금해요!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Enumerated에는 ORDINAL과 STRING 두 가지 타입이 있습니다.
ORDINAL은 enum 순서를 숫자(0, 1, 2...)로 저장하고, STRING은 enum 이름 자체를 문자열로 저장합니다.
ORDINAL은 숫자만 저장되어 공간은 절약되지만, enum 순서가 바뀌거나 중간에 값이 추가되면 데이터가 꼬일 위험이 있습니다.
반면 STRING은 enum 이름을 그대로 저장하기 때문에 코드가 수정되어도 데이터 안정성이 유지되어 더 안전합니다.
따라서 코드 변경에 안전한 STRING을 선택했습니다.


public Reservation(Long id, Member member, String date, Time time, Theme theme) {
this.id = id;
this.name = name;
this.member = member;
this.date = date;
this.time = time;
this.theme = theme;
}

public Reservation(String name, String date, Time time, Theme theme) {
this.name = name;
public Reservation(Member member, String date, Time time, Theme theme, ReservationStatus status) {
this.member = member;
this.date = date;
this.time = time;
this.theme = theme;
this.status = status;
}

public Reservation() {
Expand All @@ -33,8 +53,8 @@ public Long getId() {
return id;
}

public String getName() {
return name;
public Member getMember() {
return member;
}

public String getDate() {
Expand All @@ -48,4 +68,8 @@ public Time getTime() {
public Theme getTheme() {
return theme;
}

public ReservationStatus getStatus() {
return status;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@
import com.yourssu.roomescape.exception.CustomException;
import com.yourssu.roomescape.exception.ErrorCode;
import com.yourssu.roomescape.member.Member;
import com.yourssu.roomescape.theme.ThemeDao;
import com.yourssu.roomescape.reservation.dto.ReservationFindAllResponse;
import com.yourssu.roomescape.reservation.dto.ReservationSaveRequest;
import com.yourssu.roomescape.reservation.dto.ReservationSaveResponse;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.net.URI;
import java.util.List;

@RestController
@RequestMapping("/reservations")
public class ReservationController {

private final ReservationService reservationService;
Expand All @@ -20,23 +23,23 @@ public ReservationController(ReservationService reservationService) {
this.reservationService = reservationService;
}

@GetMapping("/reservations")
public List<ReservationResponse> list() {
return reservationService.findAll();
@GetMapping("/mine")
public List<ReservationFindAllResponse> getMyReservations(@LoginMember Member member) {
return reservationService.getMyReservations(member);
}

@PostMapping("/reservations")
public ResponseEntity<ReservationResponse> create(@RequestBody ReservationRequest reservationRequest, @LoginMember Member member) {
if (reservationRequest.getDate() == null || reservationRequest.getTheme() == null || reservationRequest.getTime() == null) {
@PostMapping
public ResponseEntity<ReservationSaveResponse> create(@RequestBody ReservationSaveRequest reservationSaveRequest, @LoginMember Member member) {
if (reservationSaveRequest.date() == null || reservationSaveRequest.theme() == null || reservationSaveRequest.time() == null || reservationSaveRequest.status() == null) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이런 검증을 Spring Validation을 사용하면 dto단에서 간단하게 할 수 있어요.

throw new CustomException(ErrorCode.INVALID_RESERVATION_REQUEST);
}

ReservationResponse reservation = reservationService.save(reservationRequest, member);
ReservationSaveResponse reservation = reservationService.save(reservationSaveRequest, member);

return ResponseEntity.created(URI.create("/reservations/" + reservation.getId())).body(reservation);
return ResponseEntity.created(URI.create("/reservations/" + reservation.id())).body(reservation);
}

@DeleteMapping("/reservations/{id}")
@DeleteMapping("/{id}")
public ResponseEntity delete(@PathVariable Long id) {
reservationService.deleteById(id);
return ResponseEntity.noContent().build();
Expand Down
Loading