API의 인터페이스는 어떻게 디자인해야 합니까?
먼저 API의 인터페이스 레이어는 사용자(전면)가 사용하는 부분을 의미합니다. 즉, 사용자 관점에서 API를 의미합니다. 사용자는 API를 통해 데이터를 요청할 수 있습니다. 이 시점에서 사용자는 요청할 데이터 항목 수와 정렬 방법을 결정할 수 있습니다. 샘플 데이터는 아래와 같습니다.
과일이름 | 갯수 | 들어온 날짜 | 가격
------|------|----------|------
바나나 | 12 | 03.12 | 1200
토마토 | 23 | 03.13 | 2200
오랜지 | 21 | 03.13 | 3200
사용자는 2,000원 이상의 바나나나 과일 이름에 대한 정보만 얻을 수 있습니다. 그렇다면 API를 통해 요청해야 하며 두 가지 방법이 있습니다.
상황: 2000원 이상 과일의 이름과 가격 정보를 가져옵니다.
- 일반적으로 사용되는 정보를 단일 경로로 결합
ex)/the_fruits - 사용자는 API를 통해 약관에 따라 요청을 보냅니다.
예) /fruit?minPrice=2000&data=name&data=price
방법 1은 인터페이스를 단순화합니다. 매직 버튼을 생성하여 사용자가 해당 버튼을 누르기만 하면 됩니다. 반면 방법 2의 경우 인터페이스 조작이 다소 복잡해진다. 인터페이스 제공자의 규칙에 따라 사용해야 합니다. 더 이상 업데이트가 필요하지 않은 제품에는 방법 1과 같은 인터페이스가 적합합니다. 전자레인지, 계산기 등 해당 버튼을 누르면 의도한 기능을 명확하게 제공하는 제품에 특화되어 있다. 예를 들어 전자렌지의 +30초 버튼인데 사용자가 30초가 아닌 20초를 더하고 싶어도! 전자레인지는 +30초 버튼만 제공한다. 그게 싫으시면 전자레인지를 교체할 수밖에 없습니다.
방법 2의 경우 사용자의 요구사항이 다양할 것으로 예상되는 경우 다음과 같은 인터페이스를 사용합니다. 쿠팡의 화면은 이렇습니다만, 다양한 사용자의 니즈를 충족시키기 위해 나. 저가, 고가, 저가이면서 별점은 높음 등 방법 2와 같이 많은 책임과 사용의 용이성을 사용자에게 전가하여야 한다. 따라서 방법 2와 같이 API를 설계하는 것이 좋습니다. 그렇다면 인터페이스 계층과 로직 계층은 어떻게 연결되어야 할까요? 우선 인터페이스 계층의 변화에 로직 계층이 영향을 받아서는 안 된다. 그런 다음 minPrice를 min_price로 변경하면 어떻게 변경됩니까? FastAPI를 사용하여 쿼리를 트리거하는 코드를 살펴보겠습니다.
# 인터페이스 레이어
from app.logic import ReadFruit
@app.get("/fruit/")
async def read_item(minPrice: int = 0):
data = ReadFruit().read(minPrice)
...
minPrice를 min_price로 변경하면 위의 코드는 다음 코드로 대체됩니다.
# 인터페이스 레이어
@app.get("/fruit/")
async def read_item(min_price: int = 0):
data = ReadFruit().read(min_price)
...
그러면 인터페이스 계층에서 로직 계층의 코드를 사용하게 되는데 인터페이스에 해당하는 부분도 로직 계층의 코드에서 필요하게 된다. 그렇다면 로직 레이어 인터페이스는 어떻게 작성해야 할까요?
# 로직 레이어
class ReadFruit: # 과일 정보를 가져오는 로직의 인터페이스
def read(minPrice):
...
위와 같이 작성했다면 min_price를 minPrice에 넘기기만 하면 됩니다. 그러면 논리 계층에서 코드를 변경할 필요가 없습니다. 그러나 인터페이스가 새 인수를 수신하면 어떻게 변경됩니까?
상황: max_price를 추가해야 합니다!
이 경우 다음과 같이 수정해야 합니다.
# 인터페이스 레이어
@app.get("/fruit/")
async def read_item(min_price: int = 0, max_price: int = -1):
...
# 로직 레이어
class ReadFruit: # 추상화 되지 않은 로직의 인터페이스
def read(minPrice, maxPrice): # <--- maxPrice가 추가되는 수정이 발생!!
...
즉, 인터페이스의 변화는 논리의 변화를 가져온다. 인터페이스 계층은 로직 계층에 단방향 종속적이었지만 인터페이스 변경으로 인해 로직이 변경되었습니다. 이 경우 자유롭게 인터페이스를 변경하는 것이 불가능해지고 서비스가 변경 사항에 대응하기 어려워집니다. 이것을 어떻게 해결할 수 있습니까? 논리 계층 인터페이스를 추상화하십시오. 종속성을 분리하면 느슨한 결합이 가능합니다.
# 로직 레이어
from abc import ABCMeta, abstractmethod
class Reader(metaclass=ABCMeta):
@abstractmethod
def read():
pass
class MinMaxPriceReader(Reader):
def read(min_price, max_price):
pass
class MinPriceReader(Reader):
def read(min_price):
pass
class ReadFruit: # 추상화 된 인터페이스
def init(self, reader: Reader):
self.reader = reader
이렇게 코드를 작성하면 기존 ReadFruit 클래스가 추상화가 됩니다. 즉, ReadFruit만 보면 어떻게 작동하는지 명확하지 않습니다. 어떤 리더가 들어오느냐에 따라 동작이 달라집니다. 추상화된 인터페이스는 다음과 같이 사용됩니다.
# 인터페이스 레이어
from logic import ReadFruit, MinPriceReader
@app.get("/fruit/")
async def read_item(min_price: int = 0):
data = ReadFruit(reader=MinPriceReader).read(min_price)
...
이 구성에서 max_price 인수가 read_item에 추가되면 코드가 어떻게 변경됩니까? 논리 계층에서 MinMaxPriceReader는 수정이 아닌 확장으로 추가됩니다.
class MinMaxPriceReader(Reader):
def read(min_price, max_price):
pass
그리고 중간 레이어는 다음과 같이 수정됩니다.
from logic import ReadFruit, MinPriceReader, MinMaxPriceReader
@app.get("/fruit/")
async def read_item(min_price: int = 0, max_price: int = -1):
data = ReadFruit(reader=MinMaxPriceReader).read(min_price, max_price)
...
그러면 다음 변경이 발생하더라도 수정이 아닌 확장의 개념으로 변경을 수용할 수 있습니다. 또한 종속성 반전을 유지하고 인터페이스 분리 및 개방 폐쇄성을 유지합니다.