Spring
-
JPQL 실행 시 Flush와 영속성 컨텍스트 동작 확인2024.07.22
-
API 통신 시 null 값 처리: 포함 vs. 미포함의 장단점2024.07.22
-
[Spring] DynamicInsert 사용 이유2024.07.22
-
Optional 클래스의 orElseThrow2024.07.22
-
[Spring] Enum 타입으로 Exception 구현하기2024.07.22
JPQL 실행 시 Flush와 영속성 컨텍스트 동작 확인
JPQL 실행 시 Flush와 영속성 컨텍스트 동작 확인
이 테스트는 두 가지 주요 목적을 가지고 있습니다:
- JPQL 실행 전
flush
가 실행되는지 확인 - 영속성 컨텍스트에 동일한 ID를 가진 엔티티가 존재할 경우, JPQL로 가져온 값이 영속성 컨텍스트의 값으로 대체되지 않는지 확인
- JPQL 실행 전
@Test
@DisplayName("닉네임 변경 요청시 해당 회원의 닉네임을 변경한다.")
void updateMemberWithNickNameRequest() throws Exception {
//given
Member member = saveMemberAndMemberImage();
Long memberId = member.getId();
Jwt jwtCreatedBySavedMember = generateTokenWithMemberId(memberId);
MemberUpdateRequest memberUpdateRequest = MemberUpdateRequest.builder()
.nickname(CHANGED_NICK_NAME)
.build();
String jsonRequest = objectMapper.writeValueAsString(memberUpdateRequest);
//when & then
mockMvc.perform(
MockMvcRequestBuilders.patch("/api/members/{memberId}", memberId)
.header(AUTHORIZATION_STRING, JWT_TOKEN_PREFIX + jwtCreatedBySavedMember.getAccessToken())
.contentType(MediaType.APPLICATION_JSON)
.content(jsonRequest))
.andExpect(status().isOk());
Member updatedMember = memberProvider.findById(memberId);
assertThat(updatedMember.getNickname()).isEqualTo(CHANGED_NICK_NAME);
}
테스트 설명
Member 저장
Member member = saveMemberAndMemberImage(); Long memberId = member.getId();
이 메서드에 의해
member
와image
가 저장됩니다.@GeneratedValue(strategy = GenerationType.IDENTITY)
로 설정되어 있기 때문에member
가 저장될 때 바로 저장 쿼리가 실행됩니다. 이 상태에서 영속성 컨텍스트와 데이터베이스 모두에member
가 존재합니다. 이때Member
엔티티의isWithdrawal
필드는 데이터베이스에 의해 기본값으로 초기화되지만, 해당필드의 초기화 상태가 영속성 컨텍스트에는 반영되지 않습니다.
MockMvc 요청
mockMvc.perform( MockMvcRequestBuilders.patch("/api/members/{memberId}", memberId) .header(AUTHORIZATION_STRING, JWT_TOKEN_PREFIX + jwtCreatedBySavedMember.getAccessToken()) .contentType(MediaType.APPLICATION_JSON) .content(jsonRequest)) .andExpect(status().isOk());
이 요청은
member
의 닉네임을 변경합니다.updateMember
메서드는 JPQL을 통해member
를 조회합니다. 이때 JPQLmemberQueryService.findById(memberId)
이 실행되기 전에flush
가 자동으로 실행됩니다.@Transactional public void updateMember(Long memberIdFromJwt, Long memberId, MemberUpdateRequest memberUpdateRequest) { validateMemberId(memberIdFromJwt, memberId); Member member = memberQueryService.findById(memberId); // JPQL 실행 전 flush() 호출 Optional.ofNullable(memberUpdateRequest.getAlert()) .ifPresent(member::updateAlert); Optional.ofNullable(memberUpdateRequest.getNickname()) .ifPresent(member::updateNickname); }
entityManager.clear()
를 호출하지 않았으므로,entityManager
에는 여전히saveMemberAndMemberImage()
메서드에 의해 저장된member
가 존재합니다. 이 상태에서 JPQL로 가져온member
는 영속성 컨텍스트에 이미 존재하는member
로 대체되지 않습니다.(JPQL 을 통해 값을 가져오더라도, 영속성 컨텍스트안에 같은
ID
를 가진 객체가 있다면 JPQL 을 통해 가져온 값을 버리고 영속성 컨텍스트 안에 값을 유지하기 때문에 DB에 의해 기본값으로 초기화된 엔티티가 현재 영속성 컨텍스트 안의 엔티티로 대체되지 않습니다.)
Update 와 Flush
Member updatedMember = memberProvider.findById(memberId); assertThat(updatedMember.getNickname()).isEqualTo(CHANGED_NICK_NAME);
이 코드에서
memberProvider.findById(memberId)
는 JPQL 쿼리를 실행하며, 쿼리 실행 전에flush
가 호출됩니다.flush
가 호출될때,mockMvc.perform()
에 의해 실행된Member
엔티티 업데이트 쿼리가 나갈것입니다. 트랜잭션의 전파에 의해 test 코드에 걸어놓은 트랜잭션이 끝나지 않았으므로mockMvc.perform
에 의해 실행된 update 쿼리는mockMvc.perform
이 실행이 완료된 후에도 아직 실행되지 않기 때문입니다.
❗️이때 중요한것은 우린 아직 entityManager.clear()
를 한번도 호출하지 않았다는 점입니다. Member
엔티티 안에는 DB의 기본값에 의해 초기화되는 isWithdrawal
이라는 필드가 있는데, 아직 영속성 컨텍스트 안에 있는 entity
는 clear()
가 된 적이 없고, 따라서 DB에 의해 초기화된 필드는 영속성 컨텍스트 안에 적용이 되지 않았을 것입니다.
이를 검증하기 위해 아래와 같이 최종적으로 찾은 updatedMember
의 isWithdrawal
필드를 가져와보면 null
인것을 확인할수 있으며
만약 isWithdrawal
이 초기화된 상태를 보고싶다면 movkMvc.perform()
메서드 호출 이전 또는 이후에 entityManager
의 clear
메서드를 실행해주면 잘초기화된 isWithdrawal
값을 볼수있습니다.
참고로 mockMvc.perform
이후에 clear()
를 호출하는 로직에서 entityManger.flush()
가 추가된 이유는 만약 entityManger.clear()
만 호출시 mockMvc.perform()
에 의해 업데이트된 member
엔티티의 변경사항이 모두 detach
되면서 업데이트 쿼리가 나가지 않습니다. 따라서 mockMvc.perform
이후에는 entityManger.flush()
를 추가해 member
엔티티의 update
정보가 사라지지 않도록 합니다.
결론
JPQL이 실행되기 전에
flush
가 실행되는지 확인:mockMvc.perform()
이후,JPQL
로 인해update
쿼리가 실행되는 것을 확인했습니다. 이는JPQL
이 실행되기 전에flush
가 자동으로 호출된다는 것을 의미합니다.
영속성 컨텍스트에 동일한 ID의 엔티티가 존재할 때 JPQL로 가져온 값이 대체되지 않는지 확인:
entityManager.clear()
를 호출하지 않으면,JPQL
로 가져온 값이 영속성 컨텍스트에 이미 존재하는 값으로 대체되지 않는다는 것을 확인했습니다. 예를 들어,member.getIsWithdrawal()
이null
인 것을 확인했습니다.
사실, 엔티티 클래스 안에 초기화 로직을 넣거나 다른 방법으로 영속성 컨텍스트와 데이터베이스 간의 불일치를 줄이는 것이 가장 좋은 방법이지만, 이번 테스트는 이 불일치를 확인하기 위해 수행되었습니다.
'Spring' 카테고리의 다른 글
API 통신 시 null 값 처리: 포함 vs. 미포함의 장단점 (0) | 2024.07.22 |
---|---|
JPA에서 단방향 및 양방향 일대일 관계의 외래키 처리와 지연 로딩 문제 (1) | 2024.07.22 |
[Spring] DynamicInsert 사용 이유 (0) | 2024.07.22 |
Optional 클래스의 orElseThrow (0) | 2024.07.22 |
[Spring] Enum 타입으로 Exception 구현하기 (0) | 2024.07.22 |
API 통신 시 null 값 처리: 포함 vs. 미포함의 장단점
API 통신 시 null 값 처리: 포함 vs. 미포함의 장단점
JSON 기반의 API 통신을 할 때, 값이 null일 경우 해당 값을 요청이나 응답에 포함시켜야 하는지에 대해 고민해볼 필요가 있습니다. 이 글에서는 이 주제에 대해 StackOverflow
글을 기반하여 정리해보겠습니다.
StackOverflow
에서 같은 고민을 하는 사람들의 의견을 참고한 결과, Twitter와 같은 대형 플랫폼의 경우 "someGenericProperty":null
과 같은 26바이트를 차지하는 필드를 없애는 것만으로도 300GB 이상의 트래픽을 줄일 수 있다고 합니다. 하루에 수십억 건의 API 요청이 오가는 플랫폼에서는 이는 상당한 절감 효과를 가져올 수 있습니다.
그러나, null 값을 포함시키지 않는 것이 항상 최선의 선택은 아니라고 합니다. 일관성을 유지하는 것이 API 사용자의 혼란을 줄이고 예측 가능한 동작을 보장하며 디버깅을 용이하게 할 수 있습니다.
위 글을 기반으로 대략적으로 정리를 해보자면 내용은 아래와 같습니다.
일관성 유지:
- API 사용자는 모든 응답에서 예상 가능한 구조를 기대합니다. 필드가 항상 존재하면 API의 사용성과 신뢰성이 높아집니다.
잠재적 문제점:
- 특정 데이터 요소가 부재할 경우, 그 원인을 파악하기 어려울 수 있습니다. 예를 들어, 필드가 누락된 이유가 데이터가 없는 것인지, 아니면 데이터 전달 중 문제가 발생한 것인지 알기 어려울 수 있습니다.
- 필드가 예상대로 존재하지 않으면 소비자 애플리케이션의 안정성이 저하될 수 있습니다.
구현 관련 고려사항:
- null 값을 제외하려면 서버 측에서 추가 로직이 필요합니다. 이는 코드 복잡성을 증가시키고 유지보수 부담을 가중시킬 수 있습니다.
성능 관련:
- 네트워크 대기 시간과 대역폭을 고려해야 합니다. 대부분의 경우, 약간의 대역폭을 희생하더라도 일관된 응답 구조가 선호됩니다.
예외 상황:
- 극도로 제한된 대역폭 환경에서는 필드를 제외하는 것이 더 나을 수 있습니다.
- 희소 데이터 구조를 처리할 때도 필드를 제외하는 것이 유리할 수 있습니다.
결론적으로 예외적인 상황이 아닌 이상, null 값을 포함하더라도 해당 필드를 응답에 추가하는 것이 API 사용의 일관성을 유지하는 데 도움이 된다고 합니다.
'Spring' 카테고리의 다른 글
JPQL 실행 시 Flush와 영속성 컨텍스트 동작 확인 (0) | 2024.07.22 |
---|---|
JPA에서 단방향 및 양방향 일대일 관계의 외래키 처리와 지연 로딩 문제 (1) | 2024.07.22 |
[Spring] DynamicInsert 사용 이유 (0) | 2024.07.22 |
Optional 클래스의 orElseThrow (0) | 2024.07.22 |
[Spring] Enum 타입으로 Exception 구현하기 (0) | 2024.07.22 |
JPA에서 단방향 및 양방향 일대일 관계의 외래키 처리와 지연 로딩 문제
JPA에서 단방향 및 양방향 일대일 관계의 외래키 처리와 지연 로딩 문제
❗️일대일 대상 테이블에 외래키 단방향 관계는 JPA
에서 지원하지 않으며, 양방향 관계만 지원합니다.
[인프런 김영한님 자료]
대상 테이블에 외래키가 존재하는 경우, 프록시 기능의 한계로 인해 지연 로딩으로 설정해도 항상 즉시 로딩됩니다.
예를 들어, Member
객체를 가져왔을 때 JPA
는 가져온 Member
객체에 Locker
가 있는지 여부를 알아야 합니다.
1. 외래키가 Locker
에 있을 때
외래키가 Locker
에 있다면, Member
엔티티를 가져올 때 Member
클래스에 정의된 Locker
가 존재하는지 확인해야 합니다. 이때 Member
테이블에는 외래키가 존재하지 않기 때문에, 지연 로딩 설정 여부와 상관없이 Locker
테이블을 조회하여 Member
와 연결된 Locker
가 있는지 확인하는 추가 쿼리가 발생합니다.
2. 외래키가 Member
에 있을 때
외래키가 Member
에 있다면, Member
테이블의 조회만으로도 Member
와 연결된 Locker
가 있는지 여부를 판단할 수 있습니다. 따라서 이 경우에는 지연 로딩(lazy loading) 또는 즉시 로딩(eager loading) 설정이 가능합니다.
요약
- 외래키가
Locker
에 있는 경우:Member
를 조회할 때 항상Locker
테이블을 추가로 조회해야 하므로, 지연 로딩 설정이 무시되고 즉시 로딩이 이루어집니다. - 외래키가
Member
에 있는 경우:Member
테이블만으로Locker
와의 연결 여부를 확인할 수 있으므로, 지연 로딩과 즉시 로딩 설정이 가능합니다.
'Spring' 카테고리의 다른 글
JPQL 실행 시 Flush와 영속성 컨텍스트 동작 확인 (0) | 2024.07.22 |
---|---|
API 통신 시 null 값 처리: 포함 vs. 미포함의 장단점 (0) | 2024.07.22 |
[Spring] DynamicInsert 사용 이유 (0) | 2024.07.22 |
Optional 클래스의 orElseThrow (0) | 2024.07.22 |
[Spring] Enum 타입으로 Exception 구현하기 (0) | 2024.07.22 |
[Spring] DynamicInsert 사용 이유
[Spring] @DynamicInsert 사용이유
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Knowledge {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String thumbnailImageUrl;
@Column(nullable = false)
private String contentImageUrl;
@Column(columnDefinition = "boolean default false")
private Boolean isDeleted;
@Column(updatable = false)
@CreationTimestamp
private LocalDateTime createdAt;
}
위와 같은 knowledge 객체를 생성 및 저장 할 때 isDeleted
필드 값을 설정하지 않으면, 데이터베이스에 null
값이 저장됩니다. 이는 데이터베이스에 기본값이 true
또는 false
로 설정되어 있더라도 발생할 수 있는 문제입니다.
예를 들어, Knowledge
Entity 가 위와 같을때 Knowledge 객체를 저장하게 되면(isDeleted 가 null 인 상태로), 아래와 같은 쿼리가 생성됩니다:
2024-01-03T16:21:44.759+09:00 DEBUG 32029 --- [ main] org.hibernate.SQL :
insert
into
knowledge
(thumbnail_image_url, contentImageUrl, isDeleted, createdAt)
values
(?, ?, ?, ?)
이 쿼리는 isDeleted
필드가 명시적으로 null
로 설정되었기 때문에, 데이터베이스에 false
가 아닌 null
이 저장되는 문제를 발생시킵니다.
이를 해결하기 위해 @DynamicInsert
어노테이션을 사용할 수 있습니다. @DynamicInsert
를 사용하면 isDeleted
필드가 null
일 경우 INSERT
문에서 제외됩니다. 따라서 데이터베이스는 isDeleted
필드에 대한 기본값을 적용하여 false
를 저장하게 됩니다.
2024-01-03T16:21:44.759+09:00 DEBUG 32029 --- [ main] org.hibernate.SQL :
insert
into
knowledge
(thumbnail_image_url, content_image_url, created_at)
values
(?, ?, ?)
@DynamicInsert
어노테이션을 사용하면, 엔티티 객체의 null
값을 가진 필드를 INSERT
문에서 제외하여 데이터베이스의 기본값이 적용되도록 할 수 있습니다. 이를 통해 Knowledge
엔티티 클래스의 isDeleted
필드가 올바르게 처리되며, 불필요한 null
값 저장을 방지할 수 있습니다.
'Spring' 카테고리의 다른 글
API 통신 시 null 값 처리: 포함 vs. 미포함의 장단점 (0) | 2024.07.22 |
---|---|
JPA에서 단방향 및 양방향 일대일 관계의 외래키 처리와 지연 로딩 문제 (1) | 2024.07.22 |
Optional 클래스의 orElseThrow (0) | 2024.07.22 |
[Spring] Enum 타입으로 Exception 구현하기 (0) | 2024.07.22 |
docker hub 사용하는 방법 (1) | 2023.11.27 |
Optional 클래스의 orElseThrow
Optional 클래스의 orElseThrow
orElseThrow
는 Optional
클래스의 메서드로, 값이 존재하면 그 값을 반환하고, 값이 존재하지 않으면 예외를 던지도록 설계되었습니다. 이 메서드는 Supplier
인터페이스를 통해 예외를 생성하는데, Supplier
는 인자를 받지 않고 결과를 반환하는 함수형 인터페이스입니다.
orElseThrow
메서드는 다음과 같이 정의되어 있습니다
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}
이 메서드는 값이 존재하면 그 값을 반환하고, 값이 없으면 Supplier
를 통해 예외를 생성하여 던집니다. Supplier
는 @FunctionalInterface
어노테이션이 붙은 인터페이스로, 단 하나의 추상 메서드 get
을 가지고 있습니다
@FunctionalInterface
public interface Supplier<T> {
/**
* Gets a result.
*
* @return a result
*/
T get();
}
`
orElseThrow
메서드를 사용할 때는 보통 람다 표현식을 사용하여 예외 객체를 생성합니다. 예를 들어, find
메서드에서 특정 ID에 해당하는 Knowledge
객체를 찾지 못했을 때 예외를 던지도록 다음과 같이 작성할 수 있습니다:
public Knowledge find(Long knowledgeId) {
return knowledgeRepository.findById(knowledgeId).orElseThrow(() -> new CustomRuntimeException(
KnowledgeException.KNOWLEDGE_NOT_FOUND));
}
이 코드는 knowledgeId
에 해당하는 Knowledge
객체가 존재하지 않으면 CustomRuntimeException
을 던집니다. 여기서 사용된 람다 표현식 () -> new CustomRuntimeException(KnowledgeException.KNOWLEDGE_NOT_FOUND)
는 Supplier<CustomRuntimeException>
타입의 인스턴스를 생성합니다. (CustomRuntimeException 구조)
최종적으로 위와같이
public Knowledge find(Long knowledgeId) {
return knowledgeRepository.findById(knowledgeId).orElseThrow(() -> new CustomRuntimeException(
KnowledgeException.KNOWLEDGE_NOT_FOUND));
}
예외가 던져진다하면 orElseThrow
안에 제네릭은 타입 추론에 의하여
public <CustomRuntimeException extends Throwable> T orElseThrow(Supplier<? extends CustomRuntimeException> exceptionSupplier) throws CustomRuntimeException {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}
이렇게 바뀌고 value
즉 위에선 Knowledge
가 없는경우 CustomRuntimeException
이 throw
되게 됩니다.
그후 ControllerAdvice
등의 예외 처리 로직에 의하여 예외가 처리 됩니다.
'Spring' 카테고리의 다른 글
JPA에서 단방향 및 양방향 일대일 관계의 외래키 처리와 지연 로딩 문제 (1) | 2024.07.22 |
---|---|
[Spring] DynamicInsert 사용 이유 (0) | 2024.07.22 |
[Spring] Enum 타입으로 Exception 구현하기 (0) | 2024.07.22 |
docker hub 사용하는 방법 (1) | 2023.11.27 |
redis를 통해 로그아웃 기능 구현 (0) | 2023.11.27 |
[Spring] Enum 타입으로 Exception 구현하기
[Spring] Enum 타입으로 Exception 구현하기
기존에 예외 처리를 다음과 같이 Enum
타입으로 구현했습니다.
public enum AdminException {
FAIL_TO_SIGN_IN(HttpStatus.BAD_REQUEST, "로그인에 실패했습니다.");
private final HttpStatus status;
private final String message;
}
Enum
타입은 기본적으로 java.lang.Enum
을 암시적으로 상속받기 때문에 extends
키워드를 사용할 수 없습니다. 따라서 상속 기능을 구현하기 위해 interface
를 사용했으며, interface
는 다중 상속을 지원하므로 아래 코드와 같이 enum
클래스들이 CustomException
인터페이스를 implements
하도록 했습니다.
public interface CustomException {
HttpStatus getHttpStatus();
String getErrorMessage();
String getName();
}
@RequiredArgsConstructor
public enum AdminException implements CustomException {
FAIL_TO_SIGN_IN(HttpStatus.BAD_REQUEST, "로그인에 실패했습니다.");
private final HttpStatus status;
private final String message;
@Override
public HttpStatus getHttpStatus() {
return status;
}
@Override
public String getErrorMessage() {
return message;
}
@Override
public String getName() {
return name();
}
}
Enum
타입이 특정 인터페이스를 implements
한 이유는 RuntimeException
을 상속받는 특정 클래스(아래에서는 CustomRumtimeException
)에서 모든 예외에 대해 동일한 응답을 제공하기 위해서였습니다. 해당 클래스는 다음과 같습니다.
@Getter
@RequiredArgsConstructor
public class CustomRuntimeException extends RuntimeException {
private final CustomException customException;
public String getMessage() {
return customException.getErrorMessage();
}
}
위 코드는 CustomRuntimeException
클래스가 CustomException
인터페이스를 구현하는 열거형을 사용하여 예외 정보를 처리하고, 예외 발생 시 일관된 응답을 반환하도록 설계되었습니다. 이를 통해 각 예외에 대한 HTTP 상태 코드와 메시지를 통일된 형식으로 관리할 수 있습니다.
최종적으로 CustomRuntimeException
을 ControllerAdvice
또는 RestControllerAdvice
에서 아래와 같이 예외를 잡고 응답을 보낼 수 있습니다.
@ExceptionHandler(CustomRuntimeException.class)
public ResponseEntity<Map<String, String>> customExceptionHandler(CustomRuntimeException e) {
// 응답 맵 생성
Map<String, String> responseMap = new LinkedHashMap<>();
responseMap.put("status", e.getCustomException().getHttpStatus().toString());
responseMap.put("message", e.getCustomException().getErrorMessage());
// 응답 반환
return ResponseEntity.status(e.getCustomException().getHttpStatus())
.body(responseMap);
}
이렇게 하면 예외 발생 시 CustomRuntimeException
을 던지고, GlobalExceptionHandler
가 해당 예외를 잡아 일관된 형식으로 응답을 반환하게 됩니다.
'Spring' 카테고리의 다른 글
[Spring] DynamicInsert 사용 이유 (0) | 2024.07.22 |
---|---|
Optional 클래스의 orElseThrow (0) | 2024.07.22 |
docker hub 사용하는 방법 (1) | 2023.11.27 |
redis를 통해 로그아웃 기능 구현 (0) | 2023.11.27 |
Jasypt 를 통한 암호화 (2) | 2023.11.27 |