[Jersey] 4. Response 전용 VO 이용하기

2022. 5. 12. 17:42Spring/Jersey

반응형
  1. 서론
  2. 사전 작업
    1. 각 API별 response 정리하기
    2. 상수 클래스 정의
  3. 성공된 request에 활용될 Response 클래스 정의
  4. 데이터 클래스 정의
  5. 적용

1. 서론

이전시간에 만들었던 api들의 response를 보면, user 엔티티를 그대로 response에 담아서 user 전체 조회나, user 단건 조회의 user 형태가 동일합니다.

02-1

02-2


dateTime 의 표현식을 yyyy-MM-dd HH:mm:ss 로 변경하고 싶다면 Entity 클래스에 @JsonFormat을 설정하여 적용할 수도 있겠지만

@JsonFormat(pattern="yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul")
@Column(updatable = false)
@ColumnDefault("CURRENT_TIMESTAMP()")
private LocalDateTime createdAt;

위와 같이 설정할 경우 이 엔티티를 이용하는 모든 Response에서 모두 동일한 포맷만 이용할 수 있다는 단점이 있습니다.

그 밖에도 Entity클래스를 그대로 내보내는 것에는 몇가지 단점이 더 있는데

  1. api 기능에 필요하지 않은 정보까지 포함해서 보내진다.
  2. 연관관계를 설정할 경우, api별로 response를 구성하는데에 있어서 하나의 Entity로 모든 경우를 표현하기 어려워진다.

물론 동일한 Table을 바라보는 Entity 클래스를 여러개 만들어 Response에 맞춰서 Entity클래스를 달리 적용하는 방법도 있겠지만, 이 경우에는 Entity에 맞춰서 Repository 구현 인터페이스도 각각 생성해줘야하기 때문에 번거롭습니다.

Entity 클래스는 아래와 같은 방식으로 정의하길 권장하고

  • Entity 클래스는 테이블당 하나씩
  • Entity 클래스 내에는 프로젝트 내에서 활용될 모든 컬럼을 설정
  • 연관관계는 Lazy 방식으로 fetch하고, 혹시모를 에러 방지를 위한 @JsonIgnore 설정

추가적으로 api의 Response 규격에 맞춰 별도의 클래스를 정의해주는 것을 권장합니다.


2. 사전 작업

자주 이용되는 문자열이나 datetime format 관련 객체를 별도의 클래스로 빼서, 프로젝트 전역에서 활용합니다.

2.1. 각 API별 response 정리하기

UserEndpoint 에 정의된 API 2가지의 Response 형식을 변경해봅니다.

2.1.1. 사용자 전체 조회 response

사용자 전체 조회 api의 response는 사용자의 간략정보만 포함하도록 response를 변경해보았습니다.
여기에는 id, type, email, name, sex 정보만 가지고 있는 것으로 변경할 것입니다.

에러가 발생되었을 때 result와 reason이 담긴 responseBody를 응답으로 보냈던것과 유사하게, result, reason, data 라는 key를 갖는 body 형식으로 내보낼 예정입니다.

GET /v1/users

{
  "result": "SUCCESS",
  "reason": "",
  "data": [
    {
      "id": 1,
      "name": "지니",
      "email": "jini@jiniworld.me",
      "type": "BASIC",
      "sex": "F"
    },
    {
      "id": 2,
      "name": "이한",
      "email": "2han@appleboxy.xyz",
      "type": "OWNER",
      "sex": "M"
    },
    {
      "id": 3,
      "name": "코코",
      "email": "coco@jiniworld.me",
      "type": "OWNER",
      "sex": "F"
    }
  ]
}

2.1.2. 사용자 단건 조회 response

사용자 단건 조회 api의 response는 사용자의 정보를 모두 포함하도록 변경합니다.
단, Json에 표현하는 만큼, DateTime이나 DateTime으로 정의된 컬럼을 String형식으로 변환할 것입니다.

날짜는 yyyy-MM-dd, 날짜시간은 yyyy-MM-dd HH:mm:ss 형식으로 표현할 것입니다.

단건 조회에서도 마찬가지로 result, reason, data 라는 key를 갖는 body 형식으로 내보내도록 할 것입니다.

GET /v1/users/{userId}

{
  "result": "SUCCESS",
  "reason": "",
  "data": {
    "id": 1,
    "name": "지니",
    "email": "jini@jiniworld.me",
    "type": "BASIC",
    "sex": "F",
    "birthDate": "1992-02-01",
    "phoneNumber": "01011112222",
    "createdAt": "2022-04-11 16:59:07",
    "updatedAt": null
  }
}

###
2.2. 상수 클래스 정의

2.2.1. String 상수 클래스

package xyz.applebox.jersey.util;

public class MessageUtils {
    public static final String SUCCESS = "SUCCESS";
    public static final String FAIL = "FAIL";
}

2.2.2. DateTime 상수 클래스

package xyz.applebox.jersey.util;

import java.time.format.DateTimeFormatter;

public final class DateTimeUtils {
    public static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    public static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
}

 

3. 성공된 request에 활용될 Response 클래스 정의

기존의 BaseResponse에서 data라는 필드값을 추가적으로 갖는 클래스를 정의합니다.

package xyz.applebox.jersey.domain.value;

import lombok.Getter;

@Getter
public final class DataResponse<T> extends BaseResponse {
    private final T data;

    private DataResponse(T data) {
        super();
        this.data = data;
    }

    public static <T> DataResponse<T> of(T data) {
        return new DataResponse<T>(data);
    }
}

4. 데이터 클래스 정의

UserEndpoint에서 활용할 데이터 클래스를 정의합니다.

간략조회와 상세조회에 대한 class를 UserValue 내에 record 타입으로 정리하였습니다.

※ record키워드는 JDK14 이상 부터 사용가능합니다.
자세한 사용 방법을 알고 싶다면 아래의 웹페이지를 참고해주세요.
JEP 395: Records


package xyz.applebox.jersey.domain.value;

import xyz.applebox.jersey.domain.entity.User;
import xyz.applebox.jersey.util.DateTimeUtils;

import java.util.Objects;
import java.util.Optional;

public class UserValue {

    public record UserData(Long id, String name, String email, String type, String sex, String birthDate, String phoneNumber, String createdAt, String updatedAt) {
        public UserData {
            Objects.requireNonNull(id);
            Objects.requireNonNull(name);
            Objects.requireNonNull(email);
        }

        public static UserData of(User u) {
            return new UserData(u.getId(), u.getName(), u.getEmail(), u.getType(), u.getSex(),
                    Optional.ofNullable(u.getBirthDate()).map(m -> m.format(DateTimeUtils.DATE_FORMATTER)).orElse(null),
                    u.getPhoneNumber(),
                    Optional.ofNullable(u.getCreatedAt()).map(m -> m.format(DateTimeUtils.DATE_TIME_FORMATTER)).orElse(null),
                    Optional.ofNullable(u.getUpdatedAt()).map(m -> m.format(DateTimeUtils.DATE_TIME_FORMATTER)).orElse(null));
        }
    }

    public record UserSimpleData(Long id, String name, String email, String type, String sex) {
        public UserSimpleData {
            Objects.requireNonNull(id);
            Objects.requireNonNull(name);
            Objects.requireNonNull(email);
        }

        public static UserSimpleData of(User u) {
            return new UserSimpleData(u.getId(), u.getName(), u.getEmail(), u.getType(), u.getSex());
        }
    }

}

Optional.ofNullable 을 이용하여, Null이 들어있을 경우에 대한 예외처리도 설정하였고,
위에서 정의한 DateTimeUtils.DATE_FORMATTERDateTimeUtils.DATE_TIME_FORMATTER 를 활용하여 datetime 타입의 포맷을 설정하였습니다.


5. 적용

5.1. Service

User 엔티티를 map을 이용하여 UserSimpleData 나 UserData로 변환하여 return 합니다.

@Transactional(readOnly = true)
@RequiredArgsConstructor
@Service
public class UserService {

    private final UserRepository userRepository;

    public List<UserValue.UserSimpleData> findAll() {
        return userRepository.findAll().stream().map(UserValue.UserSimpleData::of).collect(Collectors.toList());
    }

    public UserValue.UserData findById(long id) {
        return userRepository.findById(id).map(UserValue.UserData::of)
                .orElseThrow(() -> new InvalidRequestException("조회되는 User가 없습니다."));
    }

}

5.2. Endpoint

UserEnpoint에서도 마찬가지로 response 를 변경합니다.

@Produces(MediaType.APPLICATION_JSON)
@RequiredArgsConstructor
@Component
@Path("/users")
public final class UserEndpoint {

    private final UserService userService;

    @GET
    public DataResponse<List<UserValue.UserSimpleData>> getAll() {
        return DataResponse.of(userService.findAll());
    }

    @GET
    @Path("/{userId}")
    public DataResponse<UserValue.UserData> getOne(@PathParam("userId") Long userId) {
        return DataResponse.of(userService.findById(userId));
    }

}

6. 결과

별도의 VO 객체로 변환하여 response에 설정한 응답 결과입니다.

GET /v1/users

02-3


GET /v1/users/1

02-4


※ GitHub에서 jersey 프로젝트(v1.0.3)를 다운받아 볼 수 있습니다.

728x90
반응형