공휴일 과제 진행 중 정리
ElementCollection
선택의 이유
공휴일 데이터를 정리할 때 Holiday 테이블을 기준으로 types와 countries가 배열로 들어오고 있었다. 이 데이터를 효과적으로 저장하기 위해, jsonb 타입을 사용하는 대신 1:n 관계로 별도의 테이블로 분리하는 방식을 선택하였다.
고려 사항
- 일반적인 1:n 방식으로
@ManyToOne과@OneToMany어노테이션을 활용하여 테이블을 연결하는 방법. ElementCollection을 사용하여 값 타입 컬렉션으로 처리하는 방법.
Builder.Default
문제 상황
Lombok의 @Builder를 사용할 때, 초기화된 필드 값이 무시되고 null로 설정되는 문제가 발생할 수 있습니다. 이는 @ElementCollection과 같은 컬렉션 필드에서 특히 문제가 됩니다. 예를 들어, new HashSet<>()으로 초기화된 필드가 빌더를 통해 생성될 때 null로 설정되어 NullPointerException이 발생할 수 있습니다.
해결 방법
@Builder.Default를 사용하면 초기화된 필드 값이 빌더에서도 유지됩니다. 이를 통해 컬렉션 필드가 null로 설정되는 문제를 방지할 수 있습니다.
예제
@Builder
public class Example {
@Builder.Default
private Set<String> items = new HashSet<>();
public static void main(String[] args) {
Example example = Example.builder().build();
System.out.println(example.getItems()); // 출력: []
}
}요약
@Builder.Default는 Lombok의@Builder를 사용할 때 기본값을 유지하기 위해 사용됩니다.- 이를 통해 초기화된 값이 무시되는 문제를 해결하고, 안정적인 객체 생성을 보장할 수 있습니다.
No suitable HttpMessageConverter 에러 분석 및 해결
문제 상황
- 에러 메시지
- Could not extract response: no suitable HttpMessageConverter found for response type […] and content type [text/json;charset=utf-8]
- 발생 원인
- Spring의
RestTemplate또는WebClient가 응답 데이터를 변환할 수 있는 적절한HttpMessageConverter를 찾지 못함
- Spring의
원인 분석
- 응답 타입과 콘텐츠 타입 불일치
- 서버에서 반환하는 콘텐츠 타입(
Content-Type)이 클라이언트에서 기대하는 타입과 다를 수 있음 - 서버가
text/json;charset=utf-8을 반환하지만, 클라이언트는application/json만 처리 가능.
- 서버에서 반환하는 콘텐츠 타입(
- HttpMessageConverter 미설정
- Spring의 기본
HttpMessageConverter가 특정 콘텐츠 타입을 처리하지 못할 수 있음
- Spring의 기본
해결 방법
- 커스텀 HttpMessageConverter 추가
MappingJackson2HttpMessageConverter를 사용하여 필요한 콘텐츠 타입을 지원하도록 설정.
- 예제 코드
// Holiday 갖고 오는 Api Client 생성
@Bean
public DateNagerApiClient dateNagerApiClient() {
// Jackson 변환기 생성 및 media type 설정
MappingJackson2HttpMessageConverter converter
= new MappingJackson2HttpMessageConverter();
converter.setSupportedMediaTypes(Arrays.asList(
MediaType.APPLICATION_JSON,
MediaType.valueOf("text/json;charset=utf-8"),
MediaType.valueOf("text/json")
));
RestClient restClient = RestClient.builder()
.baseUrl(dateNagerUrl)
// 빌더에 추가된 Jackson 변환기를 추가해준다.
.messageConverters(configurer -> configurer.add(converter))
.build();
...
return factory.createClient(DateNagerApiClient.class);
}- Spring 클라이언트에서
Accept헤더를 명시적으로 설정하여 서버가application/json으로 응답하도록 요청 수도 있다. - 예제 코드
RestClient restClient = RestClient.builder()
.baseUrl(dateNagerUrl)
.defaultHeader(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE)
.build();RequestDto 하위에 다양한 타입을 지원하는 방법
인터페이스 또는 추상 클래스 활용
ActionDto와TextInfoDto가 공통 인터페이스나 추상 클래스를 구현하도록 설계합니다.- 코드 예시
public interface Action {
// 공통 메서드 정의 (필요 시)
}
public record ActionDto(String actionType) implements Action {
}
public record TextInfoDto(String text) implements Action {
}
public record ContentDto(
String type,
String style,
Action action // Action 인터페이스를 사용
) {
}제네릭 활용
ContentDto를 제네릭 타입으로 만들어 다양한 타입을 지원하도록 설계합니다.
public record ContentDto<T>(
String type,
String style,
T action // 제네릭 타입 사용
) {
}
// 다음과 같이 사용할 수 있다.
ContentDto<ActionDto> contentWithAction = new ContentDto<>("type1", "style1", new ActionDto("click"));
ContentDto<TextInfoDto> contentWithText = new ContentDto<>("type2", "style2", new TextInfoDto("Hello World"));Jackson의 다형성 지원 활용 (JSON 직렬화/역직렬화)
- Spring 환경에서 JSON 직렬화/역직렬화를 지원하기 위해 Jackson의
@JsonTypeInfo와@JsonSubTypes를 활용합니다. - 코드 예제
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = ActionDto.class, name = "action"),
@JsonSubTypes.Type(value = TextInfoDto.class, name = "textInfo")
})
public interface Action {
}
public record ActionDto(String actionType) implements Action {
}
public record TextInfoDto(String text) implements Action {
}
public record ContentDto(
String type,
String style,
Action action // 다형성 지원
) {
}@EventListener(ApplicationReadyEvent.class) 테스트 미실행
문제 사항
@EventListener(ApplicationReadyEvent.class)를 사용하여 서버 시작 시 특정 로직(예: 외부 API 호출)을 실행하도록 설정했으나, 테스트 환경에서 해당 로직이 실행되지 않도록 하기 위해@Profile({"!test"})를 사용- 하지만
@Profile({"!test"})를 메서드에 붙였을 때, 테스트 환경에서 해당 로직이 비활성화되지 않는 문제가 발생.
원인
@Profile은 Spring Bean의 활성화 여부를 제어하는 어노테이션입니다.- 메서드에
@Profile을 붙이는 경우, 해당 메서드가 포함된 클래스가 이미 Bean으로 등록된 상태라면@Profile이 제대로 동작하지 않습니다. - 따라서,
@Profile은 클래스 레벨에 적용해야만 의도한 대로 동작합니다.
해결 방법
@Profile을 클래스 레벨에 적용@Profile({"!test"})를 클래스에 붙여 테스트 환경에서 전체 Bean이 로드되지 않도록 설정한다.
Hibernate 페이징과 컬렉션 페치 조인 문제 해결
- 이 경고 메시지
HHH90003004: firstResult/maxResults specified with collection fetch; applying in memory는 Hibernate에서 발생하는 것으로, 페이징 처리(firstResult와maxResults)와 컬렉션 페치 조인(fetch join)을 함께 사용할 때 나타납니다.
원인
- 페이징과 컬렉션 페치 조인의 비호환성
- Hibernate는 컬렉션 페치 조인을 사용할 때, 데이터베이스에서 페이징(
LIMIT,OFFSET)을 적용하지 못하고, 모든 데이터를 메모리로 가져온 뒤 페이징을 처리합니다. - 이는 데이터가 많을 경우 메모리 사용량이 급증하고 성능 문제가 발생할 수 있습니다.
- Hibernate는 컬렉션 페치 조인을 사용할 때, 데이터베이스에서 페이징(
- SQL 제약
- SQL 표준에서는 페이징(
LIMIT,OFFSET)과 조인을 함께 사용할 때, 조인된 컬렉션의 중복 데이터를 제거하는 방식이 명확하지 않기 때문에 데이터베이스에서 이를 처리하지 못합니다.
- SQL 표준에서는 페이징(
해결 방법
- 컬렉션 페치 조인 제거
- 페이징이 필요한 경우, 컬렉션 페치 조인을 제거하고 필요한 데이터를 별도로 조회합니다.
- 예를 들어 엔티티와 컬렉션 데이터를 각각 조회하거나, DTO를 사용하여 필요한 데이터만 가져옵니다
BatchSize 활용- 컬렉션 페치 조인 대신, Hibernate의
@BatchSize를 사용하여 N+1 문제를 완화합니다. - 코드 예제
- 컬렉션 페치 조인 대신, Hibernate의
@Entity
public class Holiday {
@BatchSize(size = 10)
@OneToMany(mappedBy = "holiday")
private List<Type> types;
}- DTO를 사용한 데이터 조회
- 페이징과 컬렉션 데이터를 함께 처리하려면 DTO를 사용하여 필요한 데이터만 가져옵니다
@Query("SELECT new com.example.HolidayDto(h.id, h.name, t.type) " +
"FROM Holiday h LEFT JOIN h.types t")
Page<HolidayDto> findHolidayDtos(Pageable pageable);