-
Notifications
You must be signed in to change notification settings - Fork 7
[Logan] Spring Data JPA 4~6단계 미션 #12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: logan
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
예약 생성, 내 예약보기 기능 등 동작하지 않는 기능들이 있는데 확인해주세요!
public Member() { | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이 기본 생성자는 왜 필요할까요? (노션에 적어둔 거 답변해주시면 돼요!)
There was a problem hiding this comment.
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나 다른 코드에서 객체 생성이 가능하다.
@ManyToOne(fetch = FetchType.LAZY) | ||
@JoinColumn(name = "member_id") | ||
private Member member; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
fetch 설정을 LAZY로 하신 이유가 궁금해요!
There was a problem hiding this comment.
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로 설정하면 성능 최적화와 유연한 데이터 조회가 모두 가능해집니다.
📚 참고자료
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
지금 유은님 코드를 보면 응답 dto를 만들 때 결국 Time이나 Theme의 값이 필요하기 때문에 추가 쿼리가 발생하고 있어요
There was a problem hiding this comment.
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은 JPQL에서
- 조인 방식:
- Fetch Join은
INNER JOIN
이 기본이며, 필요 시LEFT JOIN
으로 명시적으로 조인 방식을 선택할 수 있다. - EntityGraph는 기본적으로
LEFT OUTER JOIN
을 사용한다.
- Fetch 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에 대해서도 함께 학습해볼 예정입니다.
@Enumerated(EnumType.STRING) | ||
private ReservationStatus status; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
EnumType에는 무엇이 있나요? 그 중 STRING으로 설정하신 이유가 궁금해요!
There was a problem hiding this comment.
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을 선택했습니다.
@PostMapping | ||
public ResponseEntity<ReservationSaveResponse> create(@RequestBody ReservationSaveRequest reservationSaveRequest, @LoginMember Member member) { | ||
if (reservationSaveRequest.date() == null || reservationSaveRequest.theme() == null || reservationSaveRequest.time() == null || reservationSaveRequest.status() == null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이런 검증을 Spring Validation을 사용하면 dto단에서 간단하게 할 수 있어요.
@Service | ||
@Transactional(readOnly = true) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Transactional의 readOnly 설정은 어떤 설정인가요?
service에는 readOnly를 설정해두고, save에는 또 별도로@Transactional
을 선언하고 있는데 이렇게 설계하신 이유가 무엇인가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Transactional(readOnly = true)
는 조회 전용 트랜잭션 설정으로 데이터 변경이 없는 메소드에서 성능을 최적화하기 위해 사용합니다.- JPA의 영속성 컨텍스트는 기본적으로 변경 감지를 수행하여 트랜잭션 종료 시점에 변경된 엔티티를 데이터베이스에 반영하려고 합니다.
- 하지만 readOnly = true를 설정하면 쓰기 지연(SQL 저장) 기능이 비활성화되고, 영속성 컨텍스트가 엔티티 변경을 추적할 필요가 없어져 메모리 사용과 처리 비용이 줄어 성능이 향상됩니다.
- 또한, 데이터 변경이 일어나지 않는 로직임을 코드 레벨에서 명확히 드러낼 수 있어 가독성과 유지보수성도 함께 높아집니다.
이에 따라 Service 클래스 전체에 @Transactional(readOnly = true)
를 설정하여 기본적으로 조회 성능을 최적화하고,
save, delete와 같이 데이터를 변경하는 메소드에는 별도로 @Transactional
을 선언해 readOnly를 해제하고 쓰기 작업이 가능하도록 설계했습니다. 이렇게 하면 조회 메소드는 가볍게 처리하고, 변경이 필요한 메소드만 명확하게 트랜잭션 속성을 관리할 수 있습니다.
다만 테스트를 진행해본 결과, @Transactional
없이도 save 메서드를 통해 DB에 데이터가 정상적으로 저장되는 것을 확인했습니다. 그 이유는 다음과 같습니다.
- Spring Data JPA의 JpaRepository 구현체(SimpleJpaRepository)는 save, findAll, findById 등 기본 메소드에 자체적으로
@Transactional
어노테이션이 적용되어 있다. - 현재 코드에서 save 메서드 내부 로직을 살펴보면 reservationRepository.save(reservation)에서만 상태 변경이 일어나며 나머지는 전부 조회 작업이다.
- 이 때문에, reservationRepository.save(reservation)처럼 Repository 메소드만 단독 호출할 경우, 별도로
@Transactional
을 선언하지 않아도 내부적으로 트랜잭션이 열리고, 저장 작업이 정상적으로 수행된다.
그럼에도 상태 변경 로직에 대한 확장 가능성을 고려하고, 트랜잭션 경계의 명확한 설정과 코드의 가독성 및 유지보수성 향상 등을 위해 save 메서드에 @Transactional
을 추가했습니다.
트랜잭션 관리를 어떻게 설계하는 것이 더 바람직할지에 대해 계속 고민하고 있으며, 동시에 트랜잭션에 대한 추가적인 학습의 필요성도 느끼고 있습니다.
) | ||
.collect(Collectors.toList()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
) | |
.collect(Collectors.toList()); | |
) | |
.toList(); |
이렇게 작성할 수도 있어요. 둘의 차이가 무엇일까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
핵심은 리턴되는 List의 수정 가능 여부와 null 허용 여부에 있습니다.
Java 8에서는 collect(Collectors.toList())를 사용하면 수정 가능한 List가 반환됩니다. null 값도 허용됩니다.
Java 10에서 도입된 toUnmodifiableList()는 수정 불가능한 List를 반환합니다. 그러나 이 방식은 null 값을 허용하지 않기 때문에, null을 삽입하려고 하면 NullPointerException이 발생할 수 있습니다.
Java 16에서는 Stream.toList()가 도입되었으며, 이는 내부적으로 toUnmodifiableList()와 같은 기능을 제공하면서, 수정 불가능한 List를 반환합니다. 또한, null 값도 허용합니다. 이름이 간단해져 코드 작성이 더 간결해졌습니다.
위 코드에서는 Stream.concat()으로 결합한 두 스트림을 하나의 리스트로 반환하며 수정이 없기 때문에 toList()가 적절해보이네요.
@@ -1,7 +1,15 @@ | |||
package com.yourssu.roomescape.time; | |||
|
|||
import jakarta.persistence.*; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import에 와일드카드를 사용하는 건 좋지 않다고 생각해요! 인텔리제이 설정 바꾸는 걸로 간단하게 해결 가능합니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.

100으로 설정했습니다! (참고)
private Long id; | ||
|
||
@Column(name = "time_value") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
필드명을 이렇게 바꿔주신 이유가 궁금해요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sql 파일에서 time_value라고 되어있으며 해당 사항에 맞춰서 필드명을 수정했습니다.(value는 H2 데이터베이스에서 기본적으로 예약어로 사용되고 있어 sql문 내에서 time_value를 value로 변경했을 때 에러가 발생)
또한 엔티티 이름이 이미 Time이므로, 필드명을 timeValue로 변경하기보다는 value로 유지하고, 컬럼명만 SQL에 맞춰 time_value로 설정하는 것이 더 적합하다고 판단하여 이렇게 수정했습니다.
src/main/resources/schema.sql
Outdated
CREATE TABLE time | ||
CREATE TABLE IF NOT EXISTS time |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ddl-auto를 create-drop으로 설정해두면 알아서 테이블이 만들어지는데, 여기서 테이블을 생성하는 쿼리는 없어도 되지 않을까요? 굳이 살려두신 이유가 궁금해요!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
schema.sql 파일에 CREATE 쿼리가 아예 없어도 되지 않느냐는 의미였어요! 어차피 JPA가 자동으로 만들어주니까요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
아.. 그러네요😅 create문은 제거해놓겠습니다
return new ReservationSaveResponse( | ||
newReservation.getId(), | ||
newReservation.getName(), | ||
newReservation.getMember().getName(), | ||
newReservation.getTheme().getName(), | ||
newReservation.getDate(), | ||
newReservation.getTime().getValue()); | ||
newReservation.getTime().getValue(), | ||
newReservation.getStatus() | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
이렇게 dto로 변환하는 경우 dto에 정적팩토리메서드를 추가해서 사용하는 것은 어떤가요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋은 것 같습니다👍
저는 기존에 DTO로 변환할 때 주로 빌더 패턴을 사용했고, 여러 번 변환이 필요한 경우에는 converter 파일(필요한 엔티티나 DTO 같은 클래스 객체를 생성하는 메서드를 모아놓은 별도의 클래스)를 만들어 사용하기도 했습니다. 그런데 DTO 내부에 처음부터 정적 메서드를 정의하는 방식도 좋은 것 같네요. 일반적으로 DTO 변환 시 정적 팩토리 메서드를 주로 사용하는지 궁금합니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
사람마다 다르기는 한데 저는 dto 변환 시 정적팩토리메서드를 사용하는 것을 선호하는 편이에요
- DB에 존재하지 않는 사용자 이름으로 예약 생성 시 예외 처리
- /reservations/admin API 추가 - ReservationFindAllForAdminResponse DTO 작성 - 관리자 권한 검사 후 RESERVED 상태 예약만 조회
관리자(admin)는 다른 사용자의 예약도 삭제 가능하도록 수정
- 학습 목적으로 JOIN FETCH와 `@EntityGraph` 2가지 방안 모두 활용
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
수고하셨습니다~
리뷰한 내용 반영 + 테스트코드 추가 해서 5월 4일 일요일 오후 2시까지 재리뷰요청 해주세요.
Member member = memberRepository.findByEmailAndPassword(loginRequest.email(), loginRequest.password()) | ||
.orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND)); | ||
return tokenProvider.createToken(member.getEmail()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
토큰을 만들 때 멤버의 email 정보로 만들고 있는데 , 지금 유은님 코드 상으로는 동일한 이메일로 두 개 이상의 Member가 생길 수 있어요. 이러면 어떻게 될까요?
There was a problem hiding this comment.
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 void deleteById(Long id) { | ||
timeDao.deleteById(id); | ||
timeRepository.deleteById(id); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
예약이 있는 시간을 삭제하게 되면 서버에 문제가 있다는 예외 메시지가 나오는데 왜 그럴까요? 또, 이 예외 메시지만으로는 사용자가 왜 오류가 났는지 알 수 없어요. 어떻게 하면 좋을까요? Theme의 경우에도 마찬가지에요.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
1️⃣ 원인
현재 Time이나 Theme을 삭제할 때, 해당 항목이 Reservation에서 참조되고 있는 경우 데이터베이스의 외래 키 제약 조건에 의해 삭제가 거부되며, Spring에서는 이를 DataIntegrityViolationException으로 처리합니다.
2️⃣ 해결
삭제 전에 Reservation에 의해 참조 중인지 여부를 사전 검사하고, 참조 중일 경우 CustomException을 통해 사용자에게 명확한 예외 메시지를 전달하도록 로직을 보완하였습니다.
return reservationDao.findAll().stream() | ||
.map(it -> new ReservationResponse(it.getId(), it.getName(), it.getTheme().getName(), it.getDate(), it.getTime().getValue())) | ||
.toList(); | ||
if(reservation.getMember() != member && !member.getRole().equals("ADMIN")) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
멤버 역할도 ReservationStatus처럼 enum 으로 관리해도 좋을 것 같아요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
member.isAdmin() 이런 식으로 하면 좀 더 객체지향적으로 작성할 수도 있겠네요! 객체에게 메시지를 보낸다
- 삭제 전 참조 여부를 확인하고, 존재 시 CustomException으로 예외 메시지 전달 - ThemeService 도입
- 문자열 기반 역할(Role) 관리를 enum(MemberRole)으로 변경 - Member 엔티티 내부에 isAdmin() 메서드를 정의하여 역할 판별 로직을 객체 내부로 캡슐화 - 불필요한 import문 제거
꼼꼼한 리뷰 감사합니다🙂 피드백 주신 부분들 보완했고, Service 테스트 코드도 함께 추가했습니다. |
주요 구현 내용
: Reservation 엔티티 내에 Enum 타입의 예약 상태 필드(예약, 예약 대기 등)를 추가하고, 이를 활용하여 예약 대기 기능을 구현했습니다.
학습 내용
@Enitity
설정시 no-args constructor 필요Class 'Member' should have [public, protected] no-arg constructor
👉
@Entity
클래스는 매개변수가 없는 생성자(no-arg constructor)가public
또는protected
로 꼭 있어야 함.이유
JPA는 데이터를 DB에서 읽어서 객체를 만들 때, 직접
new Member()
를 호출하지 않고, 자바가 제공하는 리플렉션(Reflection) 이라는 기능으로 객체를 만든다.그런데 이 리플렉션 방식은 기본 생성자가 없으면 객체를 만들 수 없다.
그래서 반드시 public 또는 protected 기본 생성자가 필요하다.
💡리플렉션이란?
자바 프로그램이 실행 중일 때, 클래스 정보를 읽어서 객체를 만들거나, 변수 값을 수정하거나, 메서드를 호출하는 기능
쉽게 말하면, 자바 프로그램이 자기 자신을 들여다보고 조작할 수 있는 기능
JPA 기본키 생성 전략
auto_increment
기능 사용IDENTITY
또는SEQUENCE
사용예) MySQL → PostgreSQL 변경 시
IDENTITY
에서SEQUENCE
로 바뀌며 데이터 무결성 문제나 성능 차이 발생 가능→ DB에 맞는 전략을 명확하게 설정하는 게 더 안전하다고 판단하여 auto를 제외.
참고
JPA 관련 설정
DTO 네이밍
조회 - 여러건 - 자원 : findAllUser()
참고