공휴일 과제 진행 중 정리

ElementCollection

Jpa에서 값 타입 사용하기

선택의 이유

공휴일 데이터를 정리할 때 Holiday 테이블을 기준으로 typescountries가 배열로 들어오고 있었다. 이 데이터를 효과적으로 저장하기 위해, jsonb 타입을 사용하는 대신 1:n 관계로 별도의 테이블로 분리하는 방식을 선택하였다.

고려 사항

  1. 일반적인 1:n 방식으로 @ManyToOne@OneToMany 어노테이션을 활용하여 테이블을 연결하는 방법.
  2. 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를 찾지 못함

원인 분석

  • 응답 타입과 콘텐츠 타입 불일치
    • 서버에서 반환하는 콘텐츠 타입(Content-Type)이 클라이언트에서 기대하는 타입과 다를 수 있음
    • 서버가 text/json;charset=utf-8을 반환하지만, 클라이언트는 application/json만 처리 가능.
  • HttpMessageConverter 미설정
    • Spring의 기본 HttpMessageConverter가 특정 콘텐츠 타입을 처리하지 못할 수 있음

해결 방법

  • 커스텀 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는 컬렉션 페치 조인을 사용할 때, 데이터베이스에서 페이징(LIMITOFFSET)을 적용하지 못하고, 모든 데이터를 메모리로 가져온 뒤 페이징을 처리합니다.
    • 이는 데이터가 많을 경우 메모리 사용량이 급증하고 성능 문제가 발생할 수 있습니다.
  • SQL 제약
    • SQL 표준에서는 페이징(LIMITOFFSET)과 조인을 함께 사용할 때, 조인된 컬렉션의 중복 데이터를 제거하는 방식이 명확하지 않기 때문에 데이터베이스에서 이를 처리하지 못합니다.

해결 방법

  • 컬렉션 페치 조인 제거
    • 페이징이 필요한 경우, 컬렉션 페치 조인을 제거하고 필요한 데이터를 별도로 조회합니다.
    • 예를 들어 엔티티와 컬렉션 데이터를 각각 조회하거나, DTO를 사용하여 필요한 데이터만 가져옵니다
  • BatchSize 활용
    • 컬렉션 페치 조인 대신, Hibernate의 @BatchSize를 사용하여 N+1 문제를 완화합니다.
    • 코드 예제
@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);