개발 이야기/frontend

내가 그동안 사용했던 디자인 패턴에 대한 고찰

thisisamrd 2024. 10. 8.

서론

저는 그동안 디자인 패턴에 대해 개념적으로만 알고, 몇 가지 대표적인 패턴에 대해서도 이름과 간단한 소개 정도로만 익숙했습니다. 그러던 중, 면접 과정에서 디자인 패턴에 대한 흥미를 묻는 질문을 받았고, 그 순간 제가 일하면서 사용한 패턴이 무엇인지 깊게 생각해 보지 않았다는 걸 깨달았습니다.

저는 회사에서 사용했던 디자인 패턴을 MVCMVVM 구조라고 하며 이 둘의 개념에 대해 설명할 수는 있었지만, 실제로 디자인 패턴을 어떻게 적용했는지에 대해선 명확하게 말하진 못했습니다.

 

결과적으로 그 면접은 탈락했답니다. 집에 돌아와 면접에서 받은 질문들을 돌이켜보며, 제가 실무에서 적용한 것들이 어떤 디자인 패턴에 해당하는지, 그리고 그 패턴들이 어떤 문제를 해결하기 위해 어떻게 사용됐는지를 더 깊이 고민해보았습니다.

 

 

 

 

 

 

 

 

 

 

 

Spring Framework를 사용한 프로젝트

 

제가 맡았던 첫 번째 프로젝트는 Spring Framework를 사용하여 웹 애플리케이션의 화면을 개발하는 것이었습니다. 하지만 백엔드 부분의 수정도 함께 진행했었어요. 이 프로젝트는 MVC 패턴을 따르고 있었는데요. 저는 주로 View 부분을 개발했습니다. 하지만 제가 맡은 백엔드 업무까지 수행하려면-저는 프론트 개발자입니다-Controller, Service, 그리고 DAO가 어떻게 상호작용하며, 데이터가 클라이언트에서 서버까지 어떤 흐름으로 이동하는지 알아야 했습니다.

 

 

보일러플레이트 구성

Spring에서 MVC 구조는 크게 Controller, Service, 그리고 DAO로 나눌 수 있습니다. 프로젝트의 주요 코드는 src/main/java에 위치하고, HTML을 사용하는 View는 src/main/webapp에 위치합니다. 여기서 Controller는 클라이언트의 요청을 받아 이를 Service로 전달하고, Service는 비즈니스 로직을 처리하며, DAO는 데이터베이스와 상호작용하여 데이터를 불러오거나 수정하는 역할을 합니다.

 

 

 

 

Controller, Service, DAO 예시 코드

예시 코드를 통해 각 컴포넌트가 어떻게 상호작용하는지 살펴보겠습니다.

 

 

 

Controller (ReadyDetailMornitorController.java)

@Controller
@RequestMapping("/v3/monitoring/readydetailmornitor")
public class ReadyDetailMornitorController {

    @Autowired
    private ReadyDetailMornitorService readyDetailMornitorService;

    @ResponseBody
    @RequestMapping(value="/first", method = RequestMethod.POST)
    public String getFirstDetail(@RequestParam Map<String, Object> param) {
        return readyDetailMornitorService.getFirstDetail(param).toString();
    }
}

 

 

Controller는 클라이언트의 요청을 받아 Service에 전달하는 역할을 합니다. 여기서 중요한 점은, Controller 자체가 비즈니스 로직을 처리하지 않고 Service에 그 역할을 위임한다는 것입니다. 즉, Controller는 사용자의 요청을 처리하고, 결과를 다시 클라이언트에게 전달하는 역할만 담당합니다.

 

 

 

 

 

Service (ReadyDetailMornitorServiceImpl.java)

@Service
public class ReadyDetailMornitorServiceImpl implements ReadyDetailMornitorService {

    @Autowired
    private ReadyDetailMornitorDAO readyDetailMornitorDAO;

    @Override
    public JSONObject getFirstDetail(Map<String, Object> param) {
        return readyDetailMornitorDAO.selectFirstDetail(param);
    }
}

 

Service는 핵심 비즈니스 로직을 처리하는 부분입니다. 여기서 getFirstDetail 메서드는 DAO를 통해 데이터를 가져오고, 필요한 경우 그 데이터를 가공하여 Controller에 전달합니다. 이처럼 Service는 데이터베이스와 직접적인 상호작용을 하지 않지만, 비즈니스 로직을 담당하는 핵심 모듈입니다.

 

 

 

DAO (ReadyDetailMornitorDAO.java)

@Repository
public class ReadyDetailMornitorDAO {

    @Autowired
    private SqlSession sqlSession;

    public JSONObject selectFirstDetail(Map<String, Object> param) {
        return sqlSession.selectOne("ReadyDetailMornitor.selectFirstDetail", param);
    }
}

 

DAO는 데이터베이스와 상호작용하는 역할을 하며, SQL 쿼리를 실행하여 데이터를 가져옵니다. SQL 쿼리는 MyBatis XML 파일에 정의되며, DAO는 이를 호출하여 데이터를 처리합니다.

 

 

 

 

비즈니스 로직과 DB 상호작용

비즈니스 로직은 단순한 데이터 조회 이상의 작업을 말합니다. 예를 들어, 특정 조건에 따라 데이터를 가공하거나, 여러 데이터를 조합해 결과를 만들어내는 작업이 비즈니스 로직입니다. 이러한 로직은 주로 Service 계층에서 구현되며, 데이터베이스와의 상호작용은 DAO에서 처리됩니다

 

 

 

XML에 정의된 쿼리 예시 (MyBatis Mapper XML)

<mapper namespace="ReadyDetailMornitor">
  <select id="selectFirstDetail" parameterType="map" resultType="json">
    SELECT * FROM monitoring WHERE id = #{id}
  </select>
</mapper>

 

 

 

이 XML 파일에서 SQL 쿼리가 정의되며, DAO는 이 쿼리를 실행해 데이터를 가져옵니다. 이 데이터는 Service에서 가공되어 Controller를 통해 클라이언트에게 전달됩니다.

 

 

 

 

 

 

 

 

 

 

React 기반 채팅 애플리케이션 프로젝트

두 번째 프로젝트는 ReactRedux를 사용하여 채팅 애플리케이션을 만드는 것이었습니다. 이 프로젝트에서는 주로 상태 관리를 위해 Redux를 사용하였고, 각 컴포넌트는 독립적으로 UI를 구성했습니다.

 

 

내 리액트 프로젝트를 MVVM이라고 할 수 있는지에 관한 고민

처음에는 제가 리덕스를 사용했기 때문에 이 구조가 MVVM 패턴이라고 생각했습니다. Model은 백엔드에서 제공하는 데이터, View는 리액트 컴포넌트, 그리고 ViewModel은 리덕스가 상태를 관리하는 역할을 하는 것처럼 보였기 때문입니다. 

 

하지만 리덕스를 사용했다고 해서 반드시 MVVM 패턴을 적용했다고 말하기는 어렵습니다. MVVM 패턴은 모델, 뷰, 그리고 뷰모델의 역할을 명확히 구분하는 아키텍처인데, 리덕스는 상태 관리 라이브러리로서 상태와 UI 간의 데이터를 주고받는 역할을 하지만, 리액트 자체의 컴포넌트 기반 아키텍처와 결합되면 MVVM 패턴처럼 동작할 수 있는 부분이 있죠.

 

리액트에서 리덕스를 사용한 상태 관리 구조가 MVVM 패턴과 유사한 특성을 보여줄 수는 있지만, 리액트 자체는 컴포넌트 기반 아키텍처가 중심이므로 이를 MVVM 패턴과 동일하게 정의하는 것은 적절하지 않을 수 있어요. 이 두 개념은 상호 배타적인 개념이 아니라, 상황에 따라 어떤 패턴을 적용했는지에 따라 달라지는 것입니다.

 

그러니까, "리덕스를 사용한 상태 관리 방식이 MVVM 패턴의 뷰모델 역할을 한다고 볼 수 있지만, 리액트 자체의 구조는 기본적으로 컴포넌트 기반 아키텍처에 더 가깝다"라고 설명하는 게 좀 더 정확한 표현일 것 같아요.

 

 

 

 

 

 

 

HOC (Higher-Order Component) 패턴

리액트로 구성한 두번째 프로젝트에서는 리액트 디자인 패턴 중 HOC 패턴이 사용되었다고 볼 수 있을 것 같습니다.

HOC는 공통 로직이나 UI 요소를 추상화하여 여러 컴포넌트에 재사용할 수 있도록 하는 패턴입니다. 제가 참여한 채팅 앱 프로젝트의 코드에서는 HOC 패턴을 사용해 반복적인 기능을 추상화해서 여러 컴포넌트에서 재사용할 수 있게 만들어 주는 방식으로 구현됩니다.

 

 

 

HOC 패턴 사용 예시

채팅 목록을 가져오거나 알람 데이터를 불러오는 코드가 반복적으로 필요할 때, 이를 하나의 HOC로 감싸고 각 컴포넌트에 적용할 수 있습니다.

// withDataFetching.js (HOC)
import React, { useState, useEffect } from 'react';

const withDataFetching = (WrappedComponent, fetchFunction) => {
  return (props) => {
    const [data, setData] = useState([]);
    const [loading, setLoading] = useState(true);

    useEffect(() => {
      const fetchData = async () => {
        try {
          const response = await fetchFunction();
          setData(response);
        } catch (error) {
          console.error(error);
        } finally {
          setLoading(false);
        }
      };
      fetchData();
    }, []);

    return <WrappedComponent data={data} loading={loading} {...props} />;
  };
};

export default withDataFetching;

 

 

 

이제 이 HOC를 여러 컴포넌트에서 공통적으로 사용해, 데이터 fetching 로직을 추상화할 수 있습니다. Alarm, ChatList, ChatPopup 같은 컴포넌트가 HOC를 통해 동일한 방식으로 데이터를 가져오게 할 수 있습니다.

// Alarm.js
import React from 'react';
import withDataFetching from './withDataFetching';
import { fetchAlarmData } from './api';

const Alarm = ({ data, loading }) => {
  if (loading) return <p>Loading...</p>;

  return (
    <div>
      {data.map((alarm) => (
        <div key={alarm.id}>{alarm.message}</div>
      ))}
    </div>
  );
};

export default withDataFetching(Alarm, fetchAlarmData);
// ChatList.js
import React from 'react';
import withDataFetching from './withDataFetching';
import { fetchChatListData } from './api';

const ChatList = ({ data, loading }) => {
  if (loading) return <p>Loading...</p>;

  return (
    <ul>
      {data.map((chat) => (
        <li key={chat.id}>{chat.name}</li>
      ))}
    </ul>
  );
};

export default withDataFetching(ChatList, fetchChatListData);

 

 

컴포지트 패턴

 

이 프로젝트에서는 컴포지트 패턴도 활용되었습니다. 컴포지트 패턴은 여러 개별 컴포넌트를 하나의 상위 컴포넌트로 묶어 계층 구조를 형성하는 패턴입니다. 이 패턴을 사용하면 복잡한 UI를 구성할 때 일관성을 유지하면서도 재사용성을 극대화할 수 있습니다. 예를 들어, 다양한 UI 컴포넌트들이 서로 독립적이지만, 상위 컴포넌트에서 이를 통합하여 관리할 수 있습니다.

 

 

 

 

컴포지트 패턴 사용 예시

예를 들어, 채팅 애플리케이션에서는 알림(Alarm), 채팅 목록(ChatList), 채팅 팝업(ChatPopup) 등의 UI 컴포넌트가 각각 독립적으로 동작합니다. 그러나 이들을 하나의 상위 컴포넌트에서 묶어 관리하면, 동일한 데이터 흐름을 유지하면서도 재사용성과 일관성을 확보할 수 있습니다.

 

// ChatLayout.js
import React from 'react';
import Alarm from './Alarm';
import ChatList from './ChatList';
import ChatPopup from './ChatPopup';

const ChatLayout = () => {
  return (
    <div className="chat-layout">
      <Alarm />
      <ChatList />
      <ChatPopup />
    </div>
  );
};

export default ChatLayout;

 

ChatLayout 컴포넌트는 하위에 여러 개의 독립된 컴포넌트를 포함하고 있지만, 상위 컴포넌트에서 이들을 일관성 있게 관리합니다. 이처럼 컴포지트 패턴을 사용하면 복잡한 UI를 계층적으로 관리할 수 있어 유지보수가 편리해지고, 코드의 가독성도 향상됩니다.

 

 

 

컴포넌트 기반 아키텍처와 컴포지트 패턴의 관계

리액트는 본래 컴포넌트 기반 아키텍처를 따르기 때문에, 컴포지트 패턴이 자연스럽게 도입되는 경우가 많습니다. 컴포지트 패턴은 컴포넌트 기반 아키텍처에서 UI를 구성할 때 특히 유용하게 사용됩니다. 그러나 아키텍처와 패턴은 상호 보완적 개념이며, 컴포지트 패턴은 아키텍처 내에서 복잡한 UI를 효율적으로 구성하기 위한 하나의 설계 방법일 뿐입니다.

 

 

 

 

결론

이번 포스팅을 통해 제가 실제로 일하면서 사용한 다양한 디자인 패턴들을 돌아볼 수 있었습니다. 처음에는 개념적으로만 알고 있던 디자인 패턴들이, 실무에서 어떻게 적용되는지 구체적으로 이해하게 되었습니다. 특히 Spring 프레임워크와 React를 활용한 두 프로젝트에서, 각각의 디자인 패턴들이 문제 해결에 어떻게 기여했는지를 되짚어보며, 디자인 패턴에 대한 이해가 더 깊어졌습니다.

디자인 패턴은 단순히 책에서 배우는 개념이 아니라, 실제 코드 작성에서 일관성과 확장성을 보장하고 유지보수를 쉽게 하는 데 중요한 역할을 합니다. 앞으로도 실무에서 디자인 패턴을 적극적으로 활용하여 더 나은 아키텍처를 설계하고, 더욱 발전된 개발자가 되기 위해 노력해야겠다는 생각을 하게 되었습니다.

댓글