본문 바로가기
개발 일기

FastAPI로 보는 API 인터페이스 레이어 설계

by 김개발자 2023. 3. 23.

API의 인터페이스는 어떻게 설계하는 것이 좋을까?

 우선 API의 인터페이스 레이어는 사용자(Front)가 사용하게 되는 부분을 의미한다. 즉, 사용자가 사용하는 입장에서의 API를 말한다. 사용자는 API로 데이터를 요청할 수 있다. 이때 사용자는 데이터를 몇 개를 요청할지, 어떻게 정렬할지를 결정할 수 있다. 예시 데이터는 아래와 같다.

 

과일이름 | 갯수 | 들어온 날짜 | 가격
------|------|----------|------
바나나  | 12  | 03.12     | 1200
토마토  | 23  | 03.13     | 2200
오랜지  | 21  | 03.13     | 3200

 

 사용자는 바나나에 대한 정보만 얻을 수도 있고 가격이 2000원 이상인 과일의 이름만 얻을 수도 있다. 그렇다면 이를 API로 요청해야 하는데 이때 방법은 2가지로 분류할 수 있다.

상황 : 2000원 이상의 과일의 이름과 가격 정보를 얻는다.

  1. 자주 쓰이는 정보를 묶어서 하나의 경로(Path)로 만든다
        ex)/the_fruits

  2. 사용자가 API로 해당 조건에 맞춰서 쿼리를 날린다.
        ex) /fruit?minPrice=2000&data=name&data=price

  방법 1의 경우에는 인터페이스가 간단해진다. 마법의 버튼을 하나 만들어 버리니까 사용자는 그 버튼만 누르면 된다. 반면에 방법 2의 경우에는 인터페이스 조작이 조금은 복잡해진다. 인터페이스 제공자의 규칙에 맞춰서 사용해야 한다. 방법 1과 같은 인터페이스는 더 이상 업데이트가 필요 없는 제품에 어울린다. 전자레인지나 계산기 같이 해당 버튼을 누르면 예정된 기능을 분명히 제공하는 제품에 특화되어 있다. 전자레인지에 +30초 버튼이 그런 예인데, 사용자가 30초 말고 20초를 추가하고 싶어!라고 해도 전자레인지는 +30초 버튼만 제공할 뿐이다. 마음에 안 들면 전자레인지를 바꾸는 수밖에.

  방법 2의 경우에는 사용자의 요구가 다양할 것이 예상될 때 다음과 같은 인터페이스를 사용하게 된다. 쿠팡의 화면이 그러한데 낮은 가격순, 높은 가격순, 낮은 가격인데 별점은 높은 순, 등 다양한 사용자의 니즈를 충족시키기 위해서는 방법 2와 같이 많은 책임과 사용성을 사용자에게 전가하면 된다. 따라서 API는 방법 2와 같이 설계하는 게 좋다. 그렇다면 인터페이스 레이어와 로직 레이어는 어떻게 연결하는 게 좋을까? 우선 로직 레이어는 인터페이스 레이어의 변경에 영향을 받지 않아야 한다. 그럼 minPrice를 min_price로 바꾼다 하면 어떻게 변경이 이루어질까? FastAPI로 Query를 쏴주는 코드를 보자

 

# 인터페이스 레이어

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):
		...

 

위와 같이 작성되어 있었다면 minPrice에 min_price 만 넘겨주면 된다. 그럼 로직 레이어에서의 코드 수정은 없어도 된다. 그런데 인터페이스에서 새로운 인자를 받는다고 한다면 어떻게 변경될까?

 

상황 : 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 만 보면 어떻게 동작하는지 명확하지가 않다. 어떤 Reader 가 들어오냐에 따라서 동작이 달라진다. 추상화된 인터페이스는 아래와 같이 사용된다.

 

# 인터페이스 레이어

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)
	  ...

 

그렇다면 다음 변경이 생겨도 수정이 아니라 확장의 개념으로 변경을 받아낼 수 있다. 의존성 역전도 지키고 인터페이스 분리와 개방폐쇄도 지켜내게 된다.

'개발 일기' 카테고리의 다른 글

ChatGPT가 갖추지 못한 능력 : 추론능력  (0) 2023.03.25

댓글