Entity 구조
현재 프로젝트의 entity 구조는 아래와 같다.
ChoiceContent
를 상속받아 현재는 ImageTextChoiceContent
만 있지만, 요구사항 상 후에 ChoiceContent
를 상속하는 다른 클래스들도 생길 가능성이 있어 그를 고려한 설계를 했다.
문제는, 이를 클라이언트의 요청을 어떻게 받을 지였다.
{
"choices": [
{ ??? }, { ??? }
],
}
위 choices의 값으로 여러 타입이 올 수 있기 때문이다. 그 전에는 항상 정해진 타입의 고정 형태로만 요청을 받았었기 때문에, 새롭게 찾아서 적용했다.
Request DTO 구조
요청을 받기 위한 DTO 클래스를 만들어야 한다. 따라서 기존 Entity에 대응되게 아래와 같이 만들었다. 기존 Entity에서 ChoiceContent
는 abstract class이다. 하지만 여기 DTO에서 ChoiceContent
를 위한 DTO는 interface로 만들었다. 어차피 DTO와 Entity는 별개이기 때문에 서로 abstract이든 interface든 뭐든 간에 당연히 상관이 없다.
코드를 보는 게 더 이해가 쉬울 수 있어서 필요한 부분만 포함하여 코드를 추가한다.
TopicCreateRequest
public record TopicCreateRequest(
List<ChoiceCreateRequest> choices,
) {
}
ChoiceCreateRequest
public record ChoiceCreateRequest (
ChoiceContentCreateRequest choiceContentRequest
){
}
ImageTextChoiceContentCreateRequest
public record ImageTextChoiceContentCreateRequest(
/* ... */
) implements ChoiceContentCreateRequest{
}
Interface/abstract class에 올 수 있는 타입들 표시하기
이제 인터페이스인 ChoiceContentCreateRequest
에 올 수 있는 하위 타입들을 표시하면 요청으로 다양한 하위 타입을 받을 수 있다!
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = ImageTextChoiceContentCreateRequest.class, name = "IMAGE_TEXT")
})
public interface ChoiceContentCreateRequest {
}
현재 프로젝트의 경우에는 아직 하위 타입이 한개밖에 없으므로 ImageTextChoiceContentCreateRequest.class
하나만 작성해줬다. 여기서 property = "type"
과 name="IMAGE_TEXT"
가 중요하다. 이 의미는 json에서 type이라는 property로 각 타입들을 구분할거고, 그 때 ImageText...Request
클래스의 경우엔 name = "IMAGE_TEXT"
가 type의 값으로 온다는 의미이다.
즉, json에선 아래와 같이 구분한다는 의미이다.
{
"choices": [
{
"choiceContentRequest": {
"type": "IMAGE_TEXT"
}
},
{
"choiceContentRequest": {
"type": "IMAGE_TEXT"
}
}
]
}
그리고 하위 타입에도 자신이 어떤 이름으로 구분되는지 표시해준다.
@JsonTypeName("IMAGE_TEXT")
public record ImageTextChoiceContentCreateRequest(
//...
) implements ChoiceContentCreateRequest{
}
이제 문제 없이 다양한 하위 타입을 읽어올 수 있다!
좀 더 내부 구현을 설명하자면, 각 ChoiceContentCreateRequest
들을 쉽게 Entity로 전환하기 위해 인터페이스에 ChoiceContent toEntity()
를 만들어놓았다.
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = ImageTextChoiceContentCreateRequest.class, name = "IMAGE_TEXT")
})
public interface ChoiceContentCreateRequest {
ChoiceContent toEntity();
}
그리고 각 구체 클래스에서 toEntity()
를 구현하여 타입에 따른 엔티티를 쉽게 만들 수 있게 했다.
public record ImageTextChoiceContentCreateRequest(
// ...
) implements ChoiceContentCreateRequest{
@Override
public ChoiceContent toEntity() {
return new ImageTextChoiceContent(imageUrl, text);
}
}
이 방법이 instanceOf
로 하나하나 값을 구분하여 엔티티를 만드는 것보다 객체지향적인 것 같기도 하고 무엇보다 간편하다.
내가 구현한 방법은 인터페이스를 이용할 때지만, 추상 클래스일 때도 문제 없이 가능하다.
참고
https://stackoverflow.com/questions/42965921/use-interface-in-spring-controller-method-argument