Post

External Client class의 param/return 설계 - flatten VS DTO.of

방안 비교

방안paramreturn
1Domain ModelDomain Model
2flattenDTO
3DTODTO
  • 요청/응답의 추상화 수준은 맞춰주는 것이 좋다. 요청/응답 한쪽은 Domain Model, 한쪽은 DTO로 처리하는 것이 가장 나쁘다. 따라서 애초에 방안으로 고려하지 않는다.
  • 1안은 좋지 않다. Client는 external layer의 DTO를 받거나/반환하는게 낫다.
    • 생성자 인터페이스가 business layer에서 사용하기 적절치 않다면, DTO.of를 활용하면 해결된다.
    • 즉, 단순히 DTO 인터페이스, 생성자 파라미터가 맞지 않는다는 이유로 VO를 만드는 것은 타당성이 떨어진다.
  • 2안 3안은 각자 장단점이 있으니 상황에 맞게 선택한다.

2안 flatten 예시 코드

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Client(
    @Value("\${url.payment-detail}")
    private val detailUrl: String,
    @Value("\${url.payment-history}")
    private val historyUrl: String,
    ...
) {
    suspend fun callAPI(
        flattenParams...
    ): ResponseDTO {

        val linkUrl = when (type) {
            Type.type1 -> "$historyUrl?page=1"
            else -> "$detailUrl?id=$id"
        }

        val request = RequestDTO(linkUrl, ...)
        ...
    }
}
1
2
3
caller() {
    client.callAPI(...)
}

historyUrl, detailUrl프로파일에 따라 달라지는 값으로, Spring에서 반드시 DI 받아야 하는 값이다.
DTO는 DI를 받을 수가 없기 때문에, url은 Client에서 가지고 있다가 DTO 생성 시 넘겨 주고 있다.

3안 DTO.of 예시 코드

1
2
3
4
5
6
7
8
9
10
11
12
class Client(
    @Value("\${url.payment-detail}")
    private val detailUrl: String,
    @Value("\${url.payment-history}")
    private val historyUrl: String,
    ...
) {
    suspend fun callAPI(requestDTO: RequestDTO): ResponseDTO {
        val request = requestDTO
        ...
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 3-1 DTO에서 historyUrl에 접근하지 못하는 경우 (bad)
caller() {
    val linkUrl = when (type) {
        Type.type1 -> "$historyUrl?page=1"
        else -> "$detailUrl?id=$id"
    }
    client.callAPI(RequestDTO.of(linkUrl, ...))
}
class RequestDTO {
    companion object {
        fun of(linkUrl, ...) {
            return RequestDTO(linkUrl, ...)
        }
    }
}

DTO는 DI를 받을 수가 없기 때문에, url은 반드시 Bean에서 넘겨 받아야 한다.
DTO.of로 불변 DTO 생성을 Client 바깥에서 처리하는 경우, Client에서만 알고있어야 하는 DTO 생성에 필요한 정보를 외부(caller)에서도 알고 있어야만 한다.

하지만 이런 url들은 caller의 관심사는 아니기 때문에, external layer 바깥에서는 모르게끔 하는게 낫다.
(그렇다고 RequestDTO를 불변이 아니게끔 세팅하는 것도 원하는 해결 방향은 아니다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 3-2 DTO에서 historyUrl에 접근 할 수 있는 경우 (ok)
caller() {
    client.callAPI(RequestDTO.of(...))
}
class RequestDTO {
    companion object {
        private val HISTORY_URL = Props.historyUrl
        private val DETAIL_URL = ...

        fun of(...) {
            val linkUrl = when (type) {
                Type.type1 -> "$HISTORY_URL?page=1"
                else -> "$DETAIL_URL?id=$id"
            }
            return RequestDTO(linkUrl, ...)
        }
    }
}

위와 같이 DTO에서 직접 HISTORY_URL을 참조 할 수 있다면 3-1의 문제가 해결되나
Spring에서 관리하는 historyUrl을 Spring 바깥에서 참조 할 수 있게끔 별도 세팅이 필요하다.
(Delegates.initOnlyOnce 사용하여 싱글턴 초기화 등)

flatten과 DTO.of 기타 장단점 비교

  • flatten(2안) 단점
    • API 파라미터 목록이 method param에도 등장하므로 변경 있을 때 수정 포인트가 늘어난다.
  • DTO(3안) 단점
    • business layer의 다른 영역에서 DTO가 전달되거나 쓰일 수 있다는 단점이 있다 (가능성은 낮다)

결론

프로파일 별 properties를 Spring 바깥에서 참조 할 수 있게끔 준비가 되어 있는 경우 DTO.of(3-2안)가 괜찮은 것 같다.
그 외의 경우 flatten(2안)이 좋아보인다.

2안, 3-2안은 어느 것 하나가 압도적인 장점을 가지고 있는 선택지가 아니다. 상황에 따라 최적의 선택이 달라진다.
중요한 것은 DTO와 Client가 같은 layer에 있음을 이해하는 것이다. (따라서 상호 코드 이동이 자유롭다.)

This post is licensed under CC BY 4.0 by the author.