Product / / 2026. 2. 20. 22:57

[토이프로젝트 배포기] JudgeYourPrompt의 백엔드 편 : Python(파이썬), FastAPI, DDD, Dependency Injector, SQLAlchemy, Alembic, DI, IoC 그외 등등

반응형

목차

1. 토이 프로젝트 고민하기

2. 스택 정하기

3. FastAPI 사용기

4. DDD 사용하기

5. 추후 개발 개선 방향

 

 


1. 토이 프로젝트 고민하기 : LLM이 범람하는 시대와 나의 번아웃

 

LLM이 범람하는 시대다. ChatGPT부터 Gemini, Claude, 그리고 Grok 등등. 다양한 AI LLM 서비스들이 범람하는 시대다. 누구는 OpenAI의 ChatGPT를 가장 좋다고 생각하고, 누군가는 Gemini의 상요성을 좋게 평가하고, 혹자는 Claude를 좋아한다. 이렇게 다양한 AI 서비스에 대한 취향이 넘쳐나는 시대에 각자의 취향에 맞는 서비스에 입력하는 LLM 프롬프트도 다양하다. 그리고 그 프롬프트의 결과도 시시각각 다르다. 나도 다양한 모델을 사용하며 입맛에 맞게 프롬프트를 입력하고 그 프롬프트를 사용하고는 했다. 그러다가 든 생각이 있었다. 이러한 프롬프트나 결과들을 누군가가 보면 어떤 생각을 할까?

 

처음에는 단순한 생각에서 출발했다. 내가 지금 이 모델에 맞게 제대로 된 성능을 뽑아내는 최적의 프롬프트를 작성하고 있는 것일까? 혹은 내가 쓴 프롬프트 의외로 괜찮은 거 같은데 다른 사람들은 어떻게 생각할까? 라는 궁금증이었다. 여기서 더 나아가서 든 생각은 그렇다면 이런 프롬프트를 모아두거나 혹은 프롬프트를 잘 쓰는 방법을 이야기하는 사이트가 있을까? 하는 생각이었다. 물론 여러 사이트에서 프롬프트를 어떻게 사용할지에 대해서는 이야기하고 있었다. 국내만 봐도 나무위키 같은 대중 정보 취합 사이트에서도 간단하게 적혀 있을 정도고 특정 주제에 대해서는 Medium같은 블로그 사이트에서 아예 전문적으로 글을 작성하는 사람도 있다. 해외만 봐도 레딧에서 활발하게 토론하는 스레드가 열릴 정도이다. 하지만 이것을 한 곳에 모아둔 사이트 혹은 전문적인 사이트는 내가 조사한 바로는 없었다.

 

그래서 토이프로젝트겸 한 번 만들어 보는 것도 좋지 않을까 하는 생각을 1년 정도 생각해 두었다. 

 

그리고 그 1~2년의 시간이 지난 올해 1월 예전부터 이 생각을 구현하기 위해 조금씩 틀을 잡아오긴 했지만 일에 치어서 시간 부족으로 크게 디벨롭하지 못하고 생각과 작은 코드 덩어리로 남겨두고 있었다. 그러다가 7월 말 퇴사후 8월 부터 쉬기 시작한 뒤 대략 5~6개월 정도 쉬고 난 뒤 회복이 된 날, 나는 무료함을 느꼈다. 사실 퇴사 직후 개발을 아예 손을 뗄 정도로 회사에서 일을 하다가 번아웃을 느꼈다. 그 결과 개발이 오히려 미워지는 순간이 오게 되었다. 그 시절에는 개발의 영어 한 글자 꼴도 보기 싫었다. - 물론 회사 동료들은 좋았다. 하지만 나는 못버틸 것 같다는 생각이 개발로 못 버티겠다는 생각으로도 이어진 것 같다 - 번아웃은 결국 내 정신을 갉아 먹었고 회사에서 한 이 개발이 나를 망친 것 같은 생각이 들었기 때문이다. 하지만, 이 시간이 지나가고 개발이 조금씩 내 손에서 다시 욕망의 꿈틀림으로 몸부림치고 있는 순간이 다가왔다. 그 때는 손가락에 점점 코드의 맛이 사라지고 있을 때 즈음이었다. 그러다가 생각해 둔 내 토이 프로젝트 주제와 작은 코드 덩어리가 생각났다. 이걸 계기로 다시 개발을 시작해 보면 어떨까 하는 생각이 들었다. 

 

그렇게 주제를 다시 고민했다. 큰 틀은 벗어나지 않았다. 하지만 큰 틀만으로는 부족했다. 글을 쓸 때도 그렇지만, 아무리 아이디어가 있어도 재료가 될 목차는 필요했다. 그래야 그 틀을 벗어나지 않는 결과물이 나온다고 생각하기 때문이다. 그래서 살짝 목표를 상세하게 잡았다. "프롬프트를 모아볼 수 있는 사이트"가 큰 틀이었다면 여기서 조금 상세하게 디벨롭하는 것이다. 그래서 고민했다. 뭐 그렇게 치열하게 고민하지는 않았고 적당한 열기가 들 정도로 고민했다. 너무 상세하게 고민하면 오히려 제품 구상만 하다가 그 열기에 취해서 결국 코드를 짜지 못하고 제품으로 빚어내지 못하는 경우가 많았다는 것을 느꼈기 때문이다. - 이 부분은 직전 회사에서 많이 배운 부분이다. 아직도 감사하게 생각한다. 어찌보면 내가 단순 개발자가 아닌 프로덕트 개발자가 되는 계기가 된 것 같다 - 아무튼 그렇게 고민하다가 큰 틀에서 벗어나지 않는 적당히 상세한 틀을 정했다.

 

  • 프롬프트와 프롬프트의 결과를 공유할 수 있어야 한다.
  • 프롬프트와 그 결과에 대한 의견을 공유할 수 있는 댓글 시스템이 필요하다.
  • 회원이 가입하고 직접 글을 써야한다. 
  • 자신이 어떤 글을 썼는지 혹은 댓글을 달았는지 알 수 있어야 한다.
  • 그리고 이 프로덕트는 결심한 뒤 1~2개월 안에 나와야 한다.

 

이렇게 5가지로 큰 틀을 잡았다. 처음에는 프롬프트를 단순하게 모아두는 사이트로 생각했다가, 이걸로는 깃허브 Awesome 시리즈 README.md와 뭐가 다를 것인가 라는 생각이 들어서 커뮤니티로 살짝 방향을 디벨롭했다. 그렇게 큰 틀과 그에 맞는 몇몇의 세세한 방향성 그리고 스스로에게 둔 기간까지 정해졌으니 이제 개발 스택을 정할 차례가 되었다.

 

2. 스택 정하기 : 백엔드는 FastAPI로

 

 

이전 회사에서는 주로 파이썬을 이용한 백엔드 툴을 사용했다. Flask(이하 플라스크)와 Django(이하 장고)가 메인이었다. 하지만 장고에는 손이 가지 않았다. 일단 장고를 너무 자주 사용해서 질리기도 한 부분이 있고 장고의 경우 흔히 말하는 풀스택 프레임워크다. 즉, 다양한 기능을 제공해 주지만, 그만큼 그 기능을 내가 만들 서비스에서 다 쓸 것인가?라는 생각이 들었다. 물론 장고와 django-rest-framework 혹은 django-ninja를 통해 간편하게 RestAPI를 설정할 수 있고, 데이터베이스 설계도 따로 라이브러리를 설치할 필요 없이 모델 폴더를 설정한 뒤 그 안에다가 모델을 코드로 적어서 "make migrations"를 CLI에 입력하면 금방 코드로 만들어 주고 그거를 "migrate"를 통해 간단하게 디비에다가 반영할 수도 있다. 이렇게 하면 손 쉽게 내가 하려는 프로젝트를 1달이라는 시간 내에 빨리 끝낼 수 있다는 생각도 했다. 진지하게 고민하다가 결국 장고는 쓰지 않기로 했다.

 

일단 "필요가 없다", 이게 가장 컸다. 너무 많은 것을 제공했다. 나는 장고의 불필요하게 많은 것을 이 프로젝트에 투입하고 싶지 않았다. User 기능부터가 그랬다. 물론 백오피스 기능이나 프론트 기능도 바로 구축이 가능하겠지만 - 추후에 쓸 글에서 언급하겠지만 - 리액트를 쓸 것이고, 백 오피스도 있어서 좋겠지만 지금 당장은 백오피스 등등 장고의 다양한 기능들을 사용하다 보면 오히려 이것을 세세하게 조정하거나 세팅하는 과정에서 오히려 시간을 더 뺏길 것 같았다. 두번 째로 "하고 싶지 않았다", 너무 장고를 자주 - 뭐 그래봤자 2~3년 밖에 실무에서 사용하지 않았지만, 그것도 나에게는 긴 시간이다 - 사용했다고 생각했다. 내가 재밌게 하고 스스로를 디벨롭 하자는 생각으로 하는 프로젝트인데, 이걸 굳이  장고를 쓰면서 고통받고 싶지 않았다. 마치 "끔찍한 시간을 보내고 싶어?"를 반복하고 싶지 않았다. 그래서 장고는 일단 기각했다.

 

그렇다면 여러 선택지가 남아있다. 일단 대표적으로 Java/Kotlin과 Spring같은 국민적인 스택이다. 사실 나에게 가장 부족한 부분이 이 스택이다. 이 설명을 하자면, 내가 지금까지 실무에서 써 온 스택은 기업에서 사용하는 '일반적인' 스택과는 많이 달랐다. 그래서 이 부분을 장고 다음으로 진짜 진지하게 고민했다. 내가 부족한 부분이 맞고, 이것을 계기로 어느정도 자바 혹은 코틀린과 함께 스프링 - 흔히들 말하는 자프링/코프링 스택 - 을 잘하면 체득할 수 있는 기회라고 생각했기 때문이다. 하지만 허들이 있다. 일단 스프링 프레임워크다. 언어는 문제가 되지 않는다. 언어는 거기서 거기다라고 생각한다 - 물론 이 말을 듣고서 굉장히 극대노할 사람도 있겠지만, 내 의견에서는 그냥 큰 틀에서 비슷하다는 의미다, 깊게 파고들면 다 다른 것은 맞다, 한중일 모두 동아시아인이어도 한 국가를 깊게 파고들면 다 다른 것이 맞는데 말이다 - 문제는 스프링이었다. 내가 이 스프링을 이용해서 프레임워크를 배워 프로젝트와 프로덕트를 디벨롭하고 제품으로 그럴듯하게 보일 수 있을까?라는 생각이었다. 큰 고민을 했다. 하지만 앞에서 세운 5가지 기준 중 마지막이 이 생각을 고이 접게 만들었다. 바로 "1~2달 내로 끝내는 것", 이 부분을 고려하니 이걸로는 힘들다는 생각을 했다. 이 프로젝트는 내 재미와 속도감 있는 프로덕트 빌딩을 위한 것이라고 생각했다. 그래서 자프링/코프링 스택은 생각을 접었다.

 

남은 것은 두 가지 생각이었다. Python에서 그대로 갈 것인지 아니면 아예 리액트와 맞춰서 JavaScript/TypeScript의 Nest.js 혹은 Express.js + GraphQL스택을 해볼 것인가?였다. 그러다가 생각이 난 것이 FastAPI다. 예전부터 애정을 가지고 있었는데, 어쩌다가 까먹은 나를 원망했다. 아니 이런 스택을 까먹다니! 내가 생각한 기간에 맞출 정도로 어느정도 익숙하기도 하고 큰 기능을 필요로 하지도 않는 내 프로젝트에 가볍게 - 근데 DDD나 Dependency Injector를 적용하면서 그런거 같지도 않다 - 사용할 수 있지 않을까?라는 생각을 하고 결국은 FastAPI로 결정햇다.

 

3. FastAPI 사용기 : FastAPI를 위한 라이브러리 정하기

 

 

그렇게 백엔드 스택을 정하고 난 뒤 세팅을 시작했다. 간단하게 pip을 사용하고 pylint를 사용할 수 있었지만, 무언가 최신 혹은 최근에 나온 파이썬에서 핫한 스택들을 사용하고 싶었다.

 

그래서 패키지 관리자의 경우에는 pip을 처음에는 사용하다가 그 다음에는 poetry로 넘어갔고 그 다음에는 poetry를 사용했다. 그러다가 uv의 존재를 알게 되었다. Rust로 작성되어서 굉장히 빠르고 설치나 관리도 편하다는 것이었다. 그래서 uv로 - 근데 지금 생각하면 그렇게 많은 패키지를 관리할 필요도 없었고, 오히려 poetry만으로도 충분하지 않았나 하는 오버 엔지니어링이 생각나기도 한다 - 넘어갔다.

 

린터의 경우에는 처음에는 pylint를 사용하다가, 타입 추론 및 정합을 위해 mypy를 추가했다. 워낙 FastAPI의 경우에 타입 힌팅(type hinting)과 type annotation(타입 어노테이션)을 잘 활용해서 궁합이 좋다고 생각했다. 실제로도 mypy와의 궁합은 좋았고, 내가 놓친 부분을 잡아 주기도 하는 등 여러 도움을 받았다. 그리고 린터를 black으로 넘어갔는데, 그때 다시 위의 uv처럼 ruff를 찾게 된다. uv의 경우 지금 생각하면 오버 엔지니어링이 아닌가 하는 생각이 들지만, ruff의 경우에는 린팅 속도가 굉장히 빠르다고 하고 실제로도 사용하면서 빠른 사용성을 보여줘서 오버 엔지니어링은 아니라고 생각한다. 

 

 

그렇게 자잘한 부분을 세팅한 뒤 백엔드인 FastAPI를 위한 ORM툴 과 디비 스키마 툴을 생각했다. DB설계의 경우에는 그냥 간단하게 SQLAlchemy를 써서 SQL문으로 하나하나 설정한 뒤 반영할 수 있기도 했다. 하지만 장고에서 들인 그 편한 손 맛을 잊지 못해서 alembic을 사용하게 되었다. 사실 트레이드 오프가 있는 듯 하다. alembic을 사용해서 편하게 데이터베이스 마이그레이션을 파이썬 코드로 관리하고 이를 형상화할 수 있겠지만, 그만큼 세세한 조정이 힘들다는 점도 있다. 물론 내 프로젝트를 생각하면 데이터베이스에서 그렇게 미세 조정이 필요할까 라는 생각을 했고, 추가적으로 나중에 필요해지면 SQLAlchemy에서 지원하는 SQL문 입력을 이용해서 디비 스키마를 설계하는 것이 나을 것이라고 생각했다.

 

그렇게 FastAPI를 위해 필요한 라이브러리들을 간단하게? 정한 뒤 설치하고 본격적인 개발을 시작했다. 일단 FastAPI의 장점이라고 한다면 파이썬의 Type Hinting 기능을 100% - 까지는 모르겠으나 그래도 90%는 진짜 잘 활용하고 있다고 생각한다 - 활용한 파이썬에서 지키기 힘든 타입 문제와 FastAPI에서 사용하는 Pydantic의 데이터 정합성 맞추기다. 

 

물론 개발을 진행하면서 문제에 직면하는 것은 당연지사다. 바로 Django라면 당연히 @atomic같은 데코레이터를 활용해 데이터베이스 트랜잭션 락 관리가 없다는 것이다. 물론 그냥 넘어갈 수도 있겠지만, 일단 이 부분 만큼은 놓치기 싫었다. 아집이라고 해야할지 아니면 내 스스로 레벨업을 위한 것인지는 모르겠지만, 이 부분은 놓고 가고 싶지 않았다. 그래서 인터넷을 뒤져서 이 트랜잭션을 관리할 데코레이터를 직접 만든 사례나 SQLAlchemy를 활용한 사람들, 특히 비동기 asyncio를 활용한 디비 트랜잭션을 사람들은 어떻게 관리하나 찾아봤다. 그리고 이 들의 사례를 조합해서 다음 섹션에서 설명할 DDD, Dependency Injector를 활용한 방안을 고려해서 Transactional이라는 데코레이터를 만들었다.

 

class Transactional:

    def __init__(self, is_readonly: bool):
        self.is_readonly = is_readonly

    def __call__(self, func: Callable[..., Any]) -> Callable[..., Any]:
    
        @wraps(func)
        async def _wrapper(*args, **kwargs):
            async with session_factory() as session:
                try:
                    result = await func(*args, **kwargs, session=session)
                    if not self.is_readonly:
                        await session.commit()
                    return result
                except Exception as e:
                    await session.rollback()
                    raise e
                finally:
                    await session.close()

        return _wrapper
        

...

AsyncScopedSession = async_scoped_session(
    session_factory=_async_session_factory, scopefunc=get_session_context
)

@asynccontextmanager
async def session_factory() -> AsyncGenerator[AsyncSession]:

    _session = AsyncScopedSession
    try:
        yield _session()
    finally:
        await _session.remove()

 

위의 코드가 DDD에서 Repository단에서 직접 디비에 접근해서 트랜잭션을 수행할 때 걸 데코레이터 코드이다. 위의 코드는 session_factory()를 통해 async_scoped_session으로 생성된 AsyncSession을 받아 디비 관련 업무를 수행한다.

 

일단 이 코드를 간단하게 설명하기 전에 SQLAlchemy의 Session에 대해서 이야기 해야 한다. SQLAlchmey에서 Session은 데이터베이스의 트랜잭션을 설정하고 관리합니다. 그리고 이 트랜잭션은 위의 코드에서 보이듯이 commit()이나 rollback()이 있기 전까지 유효하다. 데이터베이스의 원칙인 ACID를 잘 지키는 역할이라고 보면 될 것이다. 근데 동기 버전인 scoped_session의 경우 스레드당 하나의 세션만을 유지한다. 하지만 asyncio의 경우 하나의 스레드에서 코루틴 들이 Event Loop를 돌면서 여러 context에 접근하는데 이때 동일한 Session에 접근하게 되면 thread-safe를 지키지 못하게 된다. 즉 의도치 않게 세션에 접근하면서 세션들이 의도치않게 서로 공유되거나 이미 롤백 된 세션을 다른 곳에서 가져가서 다시 사용할 수 있다. 그래서 SQLAlchemy에서는 async_scoped_session클래스를 지원하고 나는 그것을 사용했다. async_scoped_session을 보면 scopefunc에 get_session_context가 들어간 것이 보일텐데, 따로 get_session_context, set_session_context, remove_session_context라는 함수를 만든 뒤에 (Async)Session을 안전하게 관리할 태그를 달아주는 것이라고 보면 된다.

 

아무튼 이렇게 해서 FastAPI에서 안전하게 데이터베이스 트랜잭션을 관리할 기본 작업도 끝냈고, 이제는 어떤 식으로 백엔드 구조를 가져갈 지 고민할 차례였고, 예전부터 희망이자 염원이었던 - 장고에서는 거의 사용하기 힘들었던 - DDD, 속칭 Domain Driven Design을 사용해보기로 결심했다.

 

4. DDD 사용하기 : Dependency Injector

 

 

일단 DDD를 사용하기로 결정했지만 대부분의 자료는 스프링에 맞춰져 있었고, 실제로 파이썬에서는 DDD를 사용하기 어렵겠다는 생각을 하기도 했다. 하지만 Dependency Injector라는 라이브러리를 알게 되고 이렇게 하면 - 완벽하게 DDD라는 것이 있는 것이 아니지만, 그래도 흉내라도 내거나 - 사용할 수 있겠다고 생각했다.

 

자바와 스프링에서는 DI를 Spring Container가 관리해준다고 들었다. 그리고 이를 IoC, 즉 Inversion of Control, 제어의 역전이라고 부른다고 한다. 하지만 파이썬에서는 순수하게는 힘들고 이 Dependency Injector를 사용하면 가능하다고 해서 자료를 찾아서 적용해 보았다. 일단, Dependency Injector 는 크게 provider, container, wiring이 있다. provider는 객체를 생성해주고 관리해주며 container:는 이 provider의 집합이고 wiring은 container가 의존성 주입을 할 수 있도록 연결을 해준다. 근데 이 wiring을 사용하기 위해서는 3가지 조건이 필요하다.

 

1) @inject 데코레이터를 통해 객체를 주입할 곳을 명시해야 한다.

2) container.wire() 메소드를 통해 container 구현체를 모듈과 패키지에 연결해야 한다.

3) provide 함수를 통해 연결 고리를 만들어야 한다.

 

그리고 이를 코드로 적용한 부분은 먼저 다음과 같다.

class Container(DeclarativeContainer):
    
    wiring_config = WiringConfiguration(packages=["server"])

    user_repository = Singleton(UserSQLRepository)
    
    user_repository_adaptor = Factory(
        UserRepositoryAdaptor, user_repository=user_repository
    )
    
    user_service = Factory(UserService, repository=user_repository_adaptor)
    
    ...
    
    user_dashboard_service = Factory(
        UserDashboardService,
        thread_repository=thread_repository_adaptor,
        like_history_repository=like_history_repository_adaptor,
        comment_repository=comment_repository_adaptor,
        comment_like_history_repository=comment_like_history_repository_adaptor,
    )

 

먼저 Container에 DeclarativeContainer클래스를 상속받아 이 Container를 통해 객체를 주입하고 관리할 것을 설정했다. 그 다음에 WiringConfiguration을 통해 어느 패키지를 연결할 것인지를 설정했다. 나는 server라는 폴더에서 작업하고 있어서 server를 명시해 줬다. 그 다음은 싱글톤 패턴을 사용했다. 이 싱글톤 패턴은 클래스는 오직 인스턴스를 하나만 생성하고, 이 인스턴스에 전역적으로 접근 가능하도록 하는 원칙이라고 한다. 그래서 위의 코드에서 보이는 UserSQLRepository처럼 "~~SQLRepository"라는 직접 디비와의 연산을 처리하는 레포지토리를 만들고 이를 일반 Repository에다가 주입하는 식으로 설정했다. 그 다음 Factory를 통해 객체 생성 과정에서 발생할 수 있는 모든 복잡한 로직은 모두 캡슐화한 뒤, 생성된 객체는 비즈니스 로직에만 집중하도록 이를 "~~RepositoryAdaptor"와 같이 팩토리에서 생성 로직을 통해 생성된 객체가 레포지토리를 통해 저장되게 했다. 위의 코드에서 처럼 user_dashboard_service같이 다양한 레포지토리가 필요하면, 원래라면 이 로직들을 하나의 서비스에서 다 처리했겠지만, 위의 코드처럼 다르고도 다양한 repository_adaptor를 생성해준다.

 

그리고 이렇게 설정한 Container를 실제 코드에 주입하면 다음과 같은 코드로 예시를 들 수 있다.

 

@user_router.post("/login", response_model=LoginResponse)
@inject
async def login(
    request: LoginRequest,
    usecase: UserUseCase = Depends(Provide[Container.user_service]),
):
    return await usecase.login(email=request.email, password=request.password)

 

위의 코드처럼 @inject 데코레이터를 통해 주입할 대상을 명시해 주고, Provide를 통해 Container에서 설정한 Factory를 주입해 준다. 이렇게 하여 대부분의 DDD를 나는 나름대로 구성했다고 생각한다. 이렇게 직접 DDD를 맛보면서 느낀 점은 일단 역할이 상당히 정확하게 분리되어서 각각의 폴더 혹은 파일의 역할이 명확해 진다는 것을 느꼈다. 그러면서 이를 통해 테스트 코드를 작성한다면 분명 역할과 책임이 분리되어 있어 테스트를 하기 유용할 것이라고 생각했다. 하지만 단점도 느꼈는데, 굉장히 파일과 폴더의 구조가 방대해 진다는 것이었다. 굉장히 관리가 힘들어진다는 것을 느꼈고 오히려 이게 개발에 있어서 유지보수는 좋을 수 있지만 속도감이나 프로젝트의 빌드 속도에 영향을 끼치는 것이 아닐까 하는 생각이 들었다.

 

뭐 일단은 단점보다는 장점이 나에게는 더 크게 다가왔다.

 

5. 추후 개발 개선 방향

 

일단 이렇게 백엔드 서버 개발을 하고 나니 느낀 점은 오랜만에 재밌으면서 몰입하는 경험을 했다는 것이다. 하지만 아쉬운 점은 이 DDD 부분이었다. 이 DDD 아키텍처를 충분히 활용할 테스트 코드 작성이나 DTO, VO등의 작성을 조금 소홀히 해서 아쉽다는 생각을 했다. 아마 여기서 기능을 더 디벨롭하고 시간이 남으면 테스트 코드를 더 작성하면서 코드 퀄리티나 프로덕트의 완성도를 높일 수 있지 않을까 생각한다. 더 나아가 내가 놓친 에러 부분을 더 쉽게 디버그할 수 있지 않을까 생각하기도 한다. 아무튼 테스트 코드를 나중에 작성해 봐야 겠다.

 

일단 이렇게 백엔드 부분을 작성해 봤다. 다음 글에서는 프론트엔드 부분을 작성해 보겠다.

728x90
반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유